hongjli
2025-04-25 229af563f799c9e9daad503d5353b6d29e7f3b34
聊天页面优化
已修改1个文件
612 ■■■■ 文件已修改
src/app/chat/page.tsx 612 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/app/chat/page.tsx
@@ -11,7 +11,7 @@
interface Message {
  role: 'user' | 'assistant';
  content: string;
  content: string | null;
  timestamp: number;
  id: string;
  conversation_id?: string;
@@ -48,8 +48,20 @@
  // 添加一个新的状态来控制消息的显示
  const [showMessages, setShowMessages] = useState(false);
  // 在组件顶部增加一个强制更新计数器
  const [forceUpdateCounter, setForceUpdateCounter] = useState(0);
  // 添加状态来控制思考内容的显示/隐藏
  const [expandedThinkMessages, setExpandedThinkMessages] = useState<Record<string, boolean>>({});
  // 在组件顶部添加显示/隐藏密码的状态
  const [showApiKey, setShowApiKey] = useState(false);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const errorTimeoutRef = useRef<any>(null);
  // 在组件顶部添加一个引用,用于跟踪组件是否已卸载
  const isMountedRef = useRef(true);
  useEffect(() => {
    // 获取存储的API Key
@@ -98,6 +110,22 @@
      }
    };
    return cleanup;
  }, []);
  // 添加组件卸载时的清理工作
  useEffect(() => {
    // 组件挂载时,设置为true
    isMountedRef.current = true;
    // 组件卸载时,设置为false
    return () => {
      isMountedRef.current = false;
      // 清除所有定时器
      if (errorTimeoutRef.current) {
        clearTimeout(errorTimeoutRef.current);
      }
    };
  }, []);
  const handleApiKeySubmit = () => {
@@ -149,6 +177,8 @@
    setCurrentMessageId(assistantMessage.id);
    setMessage('');
    let controller: AbortController | null = new AbortController();
    try {
      const response = await fetch(`${BASE_URL}/v1/chat-messages`, {
        method: 'POST',
@@ -164,112 +194,289 @@
          conversation_id: conversationId || '',
          user: 'abc-123',
          files: []
        })
        }),
        signal: controller.signal // 添加中止信号
      });
      console.log("API响应状态:", response.status, response.statusText);
      if (!response.ok) {
        const errorText = await response.text();
        console.error("API错误响应:", errorText);
        let errorMessage;
        try {
          const errorJson = JSON.parse(errorText);
          errorMessage = errorJson.error || `Error: ${response.status}`;
          errorMessage = errorJson.error || `错误: ${response.status}`;
        } catch {
          errorMessage = `Error: ${response.status}`;
          errorMessage = `错误: ${response.status}`;
        }
        throw new Error(errorMessage);
      }
      const reader = response.body?.getReader();
      if (!reader) throw new Error('No reader available');
      // 确保response.body存在
      if (!response.body) {
        throw new Error('响应没有提供数据流');
      }
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      while (true) {
        const { done, value } = await reader.read();
        if (done) {
          setIsMessageComplete(true);
          break;
        }
        try {
          const { done, value } = await reader.read();
          if (done) {
            console.log("流式响应接收完毕");
            setIsMessageComplete(true);
            break;
          }
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('\n');
        buffer = lines.pop() || '';
          const chunk = decoder.decode(value, { stream: true });
          console.log("接收到数据块:", chunk);
          buffer += chunk;
          const lines = buffer.split('\n');
          buffer = lines.pop() || '';
        for (const line of lines) {
          if (line.trim() === '') continue;
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              // 忽略ping事件
              if (data.event === 'ping') continue;
          for (const line of lines) {
            if (line.trim() === '') continue;
            console.log("处理行数据:", line);
            if (line.startsWith('data: ')) {
              try {
                const jsonStr = line.slice(6);
                console.log("完整原始数据:", line);
                console.log("解析JSON字符串:", jsonStr);
                const data = JSON.parse(jsonStr);
                console.log("解析后的数据对象:", data);
                // 忽略ping事件
                if (data.event === 'ping') {
                  console.log("忽略ping事件");
                  continue;
                }
              switch (data.event) {
                case 'message':
                  setMessages(prev => {
                    const newMessages = [...prev];
                    const lastMessage = newMessages[newMessages.length - 1];
                    if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) {
                      lastMessage.content = data.answer || lastMessage.content;
                      if (data.message_id) {
                        lastMessage.id = data.message_id;
                        setCurrentMessageId(data.message_id);
                switch (data.event) {
                  case 'message':
                    console.log(`收到message事件:`, data);
                    // 提取message数据
                    const messageId = data.message_id;
                    const answerChunk = data.answer || '';
                    const convId = data.conversation_id;
                    // 如果不在循环中,直接调用更新函数
                    handleMessageChunk(messageId, answerChunk, convId);
                    break;
                  case 'message_end':
                    console.log('收到message_end事件:', data);
                    // 检查是否有最终答案
                    if (data.metadata && data.id) {
                      setIsMessageComplete(true);
                      setConversationId(data.conversation_id || null);
                    }
                    break;
                  case 'workflow_finished':
                    console.log('工作流完成:', data);
                    // 检查工作流输出是否有答案
                    if (data.data && data.data.outputs && data.data.outputs.answer) {
                      // 如果工作流有最终答案,更新消息内容
                      const finalAnswer = data.data.outputs.answer;
                      const messageId = data.message_id;
                      if (finalAnswer) {
                        console.log('从工作流获取最终答案:', finalAnswer);
                        handleFinalAnswer(messageId, finalAnswer, data.conversation_id);
                      }
                    }
                    return newMessages;
                  });
                  break;
                case 'message_end':
                  if (data.conversation_id) {
                    setConversationId(data.conversation_id);
                    setMessages(prev => {
                      const newMessages = [...prev];
                      const lastMessage = newMessages[newMessages.length - 1];
                      if (lastMessage?.role === 'assistant' && lastMessage.id === currentMessageId) {
                        lastMessage.conversation_id = data.conversation_id;
                        if (data.metadata) {
                          lastMessage.metadata = data.metadata;
                        }
                    break;
                  case 'node_finished':
                    // 检查节点类型是否为answer节点
                    if (data.data && data.data.node_type === 'answer' && data.data.outputs && data.data.outputs.answer) {
                      console.log('从answer节点获取答案:', data.data.outputs.answer);
                      const answer = data.data.outputs.answer;
                      if (answer) {
                        handleFinalAnswer(data.message_id, answer, data.conversation_id);
                      }
                      return newMessages;
                    });
                  }
                  setIsMessageComplete(true);
                  break;
                case 'error':
                  console.error('Error event received:', data);
                  setIsMessageComplete(true);
                  throw new Error(data.message || '发送消息时出错');
              }
            } catch (e) {
              console.error('Error parsing SSE data:', e);
              if (!messages[messages.length - 1]?.content) {
                    }
                    break;
                  // 处理其他工作流事件
                  case 'workflow_started':
                  case 'node_started':
                    // 这些事件只需记录,不需要更新UI
                    console.log(`工作流事件 ${data.event}:`, data);
                    break;
                  case 'error':
                    console.error('服务器返回错误事件:', data);
                    setIsMessageComplete(true);
                    throw new Error(data.message || '发送消息时出错');
                }
              } catch (e) {
                console.error('解析SSE数据出错:', e, '原始行:', line);
                setIsMessageComplete(true);
                throw e;
              }
            }
          }
        } catch (err) {
          console.error('处理流式响应时出错:', err);
          throw err;
        }
      }
    } catch (err) {
      console.error('Chat error:', err);
      showErrorMessage(err instanceof Error ? err.message : '发送消息时出错');
      // 检查是否是中止错误
      if (err instanceof Error && err.name === 'AbortError') {
        console.log('请求被中止,可能是组件卸载导致的');
        return; // 中止错误不需要显示给用户
      }
      
      setMessages(prev => {
        const newMessages = [...prev];
        const lastMessage = newMessages[newMessages.length - 1];
        if (lastMessage?.role === 'assistant') {
          lastMessage.content = '抱歉,消息发送失败,请稍后重试。';
        }
        return newMessages;
      });
      console.error('聊天请求错误:', err);
      let errorMsg = err instanceof Error ? err.message : '发送消息时出错';
      // 只有在组件仍然挂载时才更新UI
      if (isMountedRef.current) {
        showErrorMessage(errorMsg);
        setMessages(prev => {
          const newMessages = [...prev];
          const lastMessage = newMessages[newMessages.length - 1];
          if (lastMessage?.role === 'assistant') {
            lastMessage.content = `抱歉,无法获取回复: ${errorMsg}`;
          }
          return newMessages;
        });
      }
    } finally {
      setIsStreaming(false);
      // 清除控制器
      controller = null;
      // 只有在组件仍然挂载时才更新UI
      if (isMountedRef.current) {
        setIsStreaming(false);
        setIsMessageComplete(true);
      }
    }
  };
  // 修改handleMessageChunk函数,添加组件挂载检查
  const handleMessageChunk = useCallback((messageId: string, answerChunk: string, convId?: string) => {
    // 如果组件已卸载,则不执行更新
    if (!isMountedRef.current) return;
    console.log(`处理消息片段: ID=${messageId}, 片段="${answerChunk}"`);
    // 使用函数式更新确保获取最新状态
    setMessages(prevMessages => {
      // 如果组件已卸载,则不执行更新
      if (!isMountedRef.current) return prevMessages;
      // 创建消息数组的拷贝
      const newMessages = [...prevMessages];
      // 查找最后一条助手消息
      const lastMessage = newMessages[newMessages.length - 1];
      if (lastMessage?.role !== 'assistant') {
        console.warn('找不到助手消息来更新');
        return prevMessages; // 不需要更新
      }
      // 更新消息内容和ID
      const updatedContent = (lastMessage.content || '') + answerChunk;
      // 创建消息的新副本以确保React检测到变化
      const updatedMessage = {
        ...lastMessage,
        id: messageId,
        content: updatedContent,
        timestamp: Date.now()
      };
      if (convId) {
        updatedMessage.conversation_id = convId;
      }
      // 更新当前正在处理的消息ID
      if (isMountedRef.current) {
        setCurrentMessageId(messageId);
      }
      // 替换最后一条消息
      newMessages[newMessages.length - 1] = updatedMessage;
      console.log(`更新后的内容长度: ${updatedContent.length}`);
      // 触发强制更新
      if (isMountedRef.current) {
        setTimeout(() => {
          if (isMountedRef.current) {
            setForceUpdateCounter(count => count + 1);
          }
        }, 0);
      }
      return newMessages;
    });
  }, []);
  // 修改handleFinalAnswer函数,添加组件挂载检查
  const handleFinalAnswer = useCallback((messageId: string, answer: string, convId?: string) => {
    // 如果组件已卸载,则不执行更新
    if (!isMountedRef.current) return;
    console.log(`设置最终答案: ID=${messageId}, 内容="${answer}"`);
    setMessages(prevMessages => {
      // 如果组件已卸载,则不执行更新
      if (!isMountedRef.current) return prevMessages;
      const newMessages = [...prevMessages];
      const lastMessage = newMessages[newMessages.length - 1];
      if (lastMessage?.role !== 'assistant') {
        console.warn('找不到助手消息来更新最终答案');
        return prevMessages;
      }
      // 创建消息的新副本,设置最终答案
      const updatedMessage = {
        ...lastMessage,
        id: messageId,
        content: answer,
        timestamp: Date.now()
      };
      if (convId) {
        updatedMessage.conversation_id = convId;
      }
      // 更新状态
      if (isMountedRef.current) {
        setIsMessageComplete(true);
        setCurrentMessageId(messageId);
      }
      // 替换最后一条消息
      newMessages[newMessages.length - 1] = updatedMessage;
      // 触发强制更新
      if (isMountedRef.current) {
        setTimeout(() => {
          if (isMountedRef.current) {
            setForceUpdateCounter(count => count + 1);
          }
        }, 0);
      }
      return newMessages;
    });
  }, []);
  // 更新发送按钮的禁用状态
  const isSendDisabled = isStreaming || !message.trim() || !isMessageComplete;
@@ -287,6 +494,59 @@
      setShowMessages(true);
    }, 100);
    return () => clearTimeout(timer);
  }, []);
  // 添加自定义CSS动画样式
  useEffect(() => {
    const style = document.createElement('style');
    style.innerHTML = `
      @keyframes dotBounce {
        0%, 80%, 100% { transform: translateY(0); }
        40% { transform: translateY(-6px); }
      }
      .dot-animation span:nth-child(1) { animation: dotBounce 1.4s -0.32s infinite ease-in-out; }
      .dot-animation span:nth-child(2) { animation: dotBounce 1.4s -0.16s infinite ease-in-out; }
      .dot-animation span:nth-child(3) { animation: dotBounce 1.4s 0s infinite ease-in-out; }
    `;
    document.head.appendChild(style);
    return () => {
      document.head.removeChild(style);
    };
  }, []);
  // 添加处理思考内容的辅助函数
  const parseMessageContent = (content: string | null) => {
    if (!content) return { mainContent: '', thinkContent: null };
    // 匹配<think>标签内容 (不区分大小写)
    const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/i);
    if (thinkMatch) {
      // 提取思考内容
      const thinkContent = thinkMatch[1].trim();
      // 移除<think>标签,保留主要内容
      const mainContent = content.replace(/<think>[\s\S]*?<\/think>/i, '').trim();
      return { mainContent, thinkContent };
    }
    return { mainContent: content, thinkContent: null };
  };
  // 切换思考内容的显示/隐藏
  const toggleThinkContent = (messageId: string) => {
    setExpandedThinkMessages(prev => ({
      ...prev,
      [messageId]: !prev[messageId]
    }));
  };
  // 添加组件卸载时的清理函数
  useEffect(() => {
    return () => {
      // 组件卸载时的清理工作
      console.log('聊天组件卸载,清理资源');
    };
  }, []);
  return (
@@ -307,13 +567,34 @@
              </button>
            </div>
            <div className="space-y-2">
              <input
                type="password"
                value={apiKey}
                onChange={(e) => setApiKey(e.target.value)}
                placeholder="请输入您的 API Key"
                className="w-full px-4 py-2 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-100/50"
              />
              <div className="relative">
                <input
                  type={showApiKey ? "text" : "password"}
                  value={apiKey}
                  onChange={(e) => setApiKey(e.target.value)}
                  placeholder="请输入您的 API Key"
                  className="w-full px-4 py-2 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-100/50"
                />
                <button
                  type="button"
                  onClick={() => setShowApiKey(!showApiKey)}
                  className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
                >
                  {showApiKey ? (
                    // 眼睛关闭图标
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                      <path fillRule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clipRule="evenodd" />
                      <path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
                    </svg>
                  ) : (
                    // 眼睛图标
                    <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                      <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
                      <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
                    </svg>
                  )}
                </button>
              </div>
            </div>
            <div className="flex justify-end gap-2">
              <button
@@ -371,74 +652,119 @@
        <div className="flex-1 overflow-y-auto pt-20 pb-32">
          <div className="max-w-4xl mx-auto px-6">
            <div className={`space-y-6 ${showMessages ? 'opacity-100' : 'opacity-0'} transition-opacity duration-200`}>
              {messages.map((msg, index) => (
                <div
                  key={msg.id}
                  className={`flex items-start gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
                >
                  <div className="relative flex-shrink-0">
                    <div className="w-8 h-8 rounded-lg overflow-hidden shadow-inner bg-gray-50">
                      <Image
                        src={msg.role === 'assistant' ? "/images/logo.jpg" : DEFAULT_USER_AVATAR}
                        alt={msg.role === 'assistant' ? "AI助手" : "用户"}
                        width={32}
                        height={32}
                        className="w-full h-full object-cover"
                      />
                    </div>
                    {msg.role === 'assistant' && (
                      <div className="absolute -bottom-1 -right-1 w-3 h-3 bg-green-400 rounded-full border-2 border-white"></div>
                    )}
                  </div>
                  <div className={`flex-1 min-w-0 ${msg.role === 'user' ? 'text-right' : ''}`}>
                    <div className={`${
                      msg.role === 'assistant'
                        ? 'bg-white rounded-xl shadow-sm border border-gray-100'
                        : 'bg-[#E8F4FF] rounded-xl'
                      } inline-block max-w-[85%] relative min-h-[42px]`}>
                      {msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id && (
                        <div className="absolute left-0 top-0 w-full h-full flex items-center justify-center">
                          <div className="w-5 h-5 border-2 border-blue-500/30 border-t-blue-500/80 rounded-full animate-spin"></div>
                        </div>
                      )}
                      <div className={`flex items-center px-4 min-h-[42px] ${
                        msg.role === 'assistant' && !isMessageComplete && currentMessageId === msg.id ? 'invisible' : ''
                      }`}>
                        <div className="prose prose-sm max-w-none text-[14px] leading-[1.3]
                          prose-p:my-0 prose-p:leading-[1.3]
                          prose-headings:font-medium prose-headings:text-gray-800
                          prose-h1:text-lg prose-h1:my-2
                          prose-h2:text-base prose-h2:my-2
                          prose-h3:text-base prose-h3:my-1.5
                          prose-ul:my-1.5 prose-ul:pl-4 prose-li:my-0.5
                          prose-ol:my-1.5 prose-ol:pl-4
                          prose-code:px-1 prose-code:py-0.5 prose-code:bg-gray-100 prose-code:rounded prose-code:text-gray-800 prose-code:before:content-[''] prose-code:after:content-['']
                          prose-pre:my-2 prose-pre:p-2.5 prose-pre:bg-gray-800 prose-pre:rounded-lg
                          prose-a:text-blue-500 prose-a:no-underline hover:prose-a:underline
                          prose-blockquote:my-2 prose-blockquote:pl-3 prose-blockquote:border-l-4 prose-blockquote:border-gray-200
                          prose-strong:font-medium prose-strong:text-gray-800
                          prose-table:my-2 prose-tr:border-gray-200 prose-td:py-1 prose-td:px-2">
                          {msg.content.split('\n').map((line, i) => (
                            <div key={i} className="my-0">
                              {line.trim() && (
                                <ReactMarkdown
                                  remarkPlugins={[remarkGfm]}
                                  rehypePlugins={[rehypeRaw, rehypeSanitize]}
                                >
                                  {line.trim()}
                                </ReactMarkdown>
                              )}
                            </div>
                          ))}
                        </div>
              {messages.length === 0 ? (
                <div className="flex items-center justify-center h-[200px] text-gray-400">
                  <p>发送消息开始对话</p>
                </div>
              ) : (
                messages.map((msg, index) => (
                  <div
                    key={msg.id}
                    className={`flex items-start gap-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
                  >
                    <div className="relative flex-shrink-0">
                      <div className="w-8 h-8 rounded-lg overflow-hidden shadow-inner bg-gray-50">
                        <Image
                          src={msg.role === 'assistant' ? "/images/logo.jpg" : DEFAULT_USER_AVATAR}
                          alt={msg.role === 'assistant' ? "AI助手" : "用户"}
                          width={32}
                          height={32}
                          className="w-full h-full object-cover"
                        />
                      </div>
                    </div>
                    <div className="mt-0.5 text-xs text-gray-400">
                      {msg.role === 'user' && new Date(msg.timestamp).toLocaleTimeString()}
                    <div className={`flex-1 min-w-0 ${msg.role === 'user' ? 'text-right' : ''}`}>
                      <div className={`${
                        msg.role === 'assistant'
                          ? 'bg-white rounded-xl shadow-sm border border-gray-100'
                          : 'bg-[#E8F4FF] rounded-xl'
                        } inline-block max-w-[85%] relative overflow-hidden`}>
                        {/* 调试信息 - 移除这部分 */}
                        {/*
                        {process.env.NODE_ENV !== 'production' && (
                          <div className="bg-gray-100 px-2 py-1 text-xs text-gray-500">
                            长度: {(msg.content || '').length} | 计数: {forceUpdateCounter}
                          </div>
                        )}
                        */}
                        {msg.role === 'assistant' && (
                          <>{(() => {
                            // 解析消息内容,提取思考部分
                            const { mainContent, thinkContent } = parseMessageContent(msg.content);
                            const isExpanded = expandedThinkMessages[msg.id] || false;
                            const thinkingDuration = 8; // 思考时间(秒)
                            return (
                              <>
                                {/* 思考内容区域 (如果存在) */}
                                {thinkContent && (
                                  <div className="border-b border-gray-200">
                                    <button
                                      onClick={() => toggleThinkContent(msg.id)}
                                      className="w-full flex items-center justify-between px-3 py-2 bg-gray-50 hover:bg-gray-100 transition-colors text-sm text-gray-700"
                                    >
                                      <div className="flex items-center space-x-2">
                                        <span>已深度思考 (用时 {thinkingDuration} 秒)</span>
                                      </div>
                                      <svg
                                        className={`w-4 h-4 transform transition-transform ${isExpanded ? 'rotate-180' : ''}`}
                                        fill="none"
                                        stroke="currentColor"
                                        viewBox="0 0 24 24"
                                      >
                                        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
                                      </svg>
                                    </button>
                                    {/* 可折叠的思考内容 */}
                                    {isExpanded && (
                                      <div className="bg-gray-50 px-4 py-3 text-sm text-gray-700 border-t border-gray-200 whitespace-pre-wrap">
                                        {thinkContent}
                                      </div>
                                    )}
                                  </div>
                                )}
                                {/* 主要内容 */}
                                <div className="p-3">
                                  <div className="text-gray-800 leading-relaxed whitespace-pre-wrap">
                                    {mainContent || (msg.role === 'assistant' && !isMessageComplete ? '正在思考...' : '')}
                                  </div>
                                </div>
                                {/* 加载指示器 */}
                                {msg.role === 'assistant' && !isMessageComplete && (
                                  <div className="absolute bottom-1 right-2">
                                    <div className="flex space-x-1">
                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-0"></div>
                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-150"></div>
                                      <div className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse delay-300"></div>
                                    </div>
                                  </div>
                                )}
                              </>
                            );
                          })()}</>
                        )}
                        {/* 用户消息简单显示 */}
                        {msg.role === 'user' && (
                          <div className="p-3">
                            <div className="text-gray-800 leading-relaxed whitespace-pre-wrap">
                              {msg.content}
                            </div>
                          </div>
                        )}
                      </div>
                      <div className="mt-0.5 text-xs text-gray-400">
                        {msg.role === 'user' && new Date(msg.timestamp).toLocaleTimeString()}
                      </div>
                    </div>
                  </div>
                </div>
              ))}
                ))
              )}
              <div ref={messagesEndRef} />
            </div>
          </div>