hongjli
2025-04-28 554d8f1c8c59962d7ef64fec705463b7ebd4cbb6
src/app/chat/page.tsx
@@ -1,70 +1,1296 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef, useCallback, useContext, createContext } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import dynamic from 'next/dynamic';
import { ReactNode } from 'react';
export default function Page() {
  const router = useRouter();
  const [token, setToken] = useState<string | null>(null);
// 创建一个消息完成状态的Context
const MessageCompletionContext = createContext<boolean>(true);
// 动态导入 ECharts,确保它只在客户端渲染
const ReactECharts = dynamic(() => import('echarts-for-react'), { ssr: false });
interface Message {
  role: 'user' | 'assistant';
  content: string | null;
  timestamp: number;
  id: string;
  conversation_id?: string;
  feedback?: 'like' | 'dislike' | null;
  metadata?: {
    usage?: {
      prompt_tokens: number;
      completion_tokens: number;
      total_tokens: number;
      total_price: string;
    };
  };
}
const BASE_URL = 'http://121.43.139.99:7000';
// 默认用户头像
const DEFAULT_USER_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23999999'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z'/%3E%3C/svg%3E";
const AI_AVATAR = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234F46E5'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 1c4.96 0 9 4.04 9 9s-4.04 9-9 9-9-4.04-9-9 4.04-9 9-9zm0 3.5c-2.48 0-4.5 2.02-4.5 4.5h1.5c0-1.66 1.34-3 3-3s3 1.34 3 3h1.5c0-2.48-2.02-4.5-4.5-4.5zM8 13h2v2H8v-2zm6 0h2v2h-2v-2z'/%3E%3C/svg%3E";
// 添加用于解析和渲染 ECharts 的组件
function EchartsRenderer({ code }: { code: string }) {
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [isModalOpen, setIsModalOpen] = useState(false);
  const chartContainerRef = useRef<HTMLDivElement>(null);
  const modalChartRef = useRef<HTMLDivElement>(null);
  const hasRenderedRef = useRef<boolean>(false);
  const chartInstanceRef = useRef<any>(null);
  // 初始化DOM容器 - 提前设置固定高度
  useEffect(() => {
    // 获取token
    const storedToken = localStorage.getItem('token');
    setToken(storedToken);
    // 如果没有token,直接跳转到登录页面
    if (!storedToken) {
      router.push('/login');
    // 确保容器准备好了并且有确定的高度
    if (chartContainerRef.current) {
      // 设置一个固定的高度,避免渲染后变化
      const container = chartContainerRef.current;
      container.style.width = '100%';
      container.style.height = '400px'; // 固定高度400px
      container.style.minHeight = '400px'; // 防止高度变小
      // 添加临时内容,确保DOM渲染完成
      container.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#f5f7f9;"><span>正在准备图表...</span></div>';
    }
  }, []);
  // 一次性加载图表,不进行动态更新
  useEffect(() => {
    // 如果已经渲染过,跳过
    if (hasRenderedRef.current) {
      return;
    }
    setIsLoading(true);
    // 确保DOM元素已经完全挂载和渲染
    const timer = setTimeout(() => {
      // 确保DOM元素仍然存在
      if (!chartContainerRef.current) {
        console.error('图表容器不存在,跳过初始化');
        setIsLoading(false);
        return;
      }
      // 清除临时内容
      chartContainerRef.current.innerHTML = '';
      // 标记为已渲染
      hasRenderedRef.current = true;
      // 初始化图表
      const initializeChart = async () => {
        try {
          const echarts = await import('echarts');
          // 初始化图表
          const chartInstance = echarts.init(chartContainerRef.current);
          chartInstanceRef.current = chartInstance;
          // 尝试解析和设置图表选项
          try {
            const safeCode = code.replace(/window\.option/g, 'option');
            const safeFunc = new Function(`
              "use strict";
              let option;
              try {
                ${safeCode}
                return option;
              } catch (e) {
                console.error("图表代码执行错误:", e);
                return null;
              }
            `);
            const chartOption = safeFunc();
            if (chartOption) {
              // 禁用动画,避免渲染时的布局变化
              chartOption.animation = false;
              // 修改tooltip配置,确保显示在适当位置
              if (chartOption.tooltip) {
                chartOption.tooltip = {
                  ...chartOption.tooltip,
                  confine: false, // 不限制在图表区域内
                  extraCssText: 'z-index:9999; pointer-events:auto; margin-top:0;'
                };
              }
              chartInstance.setOption(chartOption);
              setError(null);
            } else {
              throw new Error("无法获取图表配置");
            }
          } catch (e) {
            console.error('图表代码执行错误:', e);
            setError('图表配置错误');
            // 设置一个默认图表以显示错误
            chartInstance.setOption({
              title: { text: '图表配置错误' },
              xAxis: { type: 'category', data: ['错误'] },
              yAxis: { type: 'value' },
              series: [{ data: [0], type: 'bar' }]
            });
          }
          // 添加窗口大小变化监听器
          const handleResize = () => chartInstance.resize();
          window.addEventListener('resize', handleResize);
          // 清理函数
          return () => {
            window.removeEventListener('resize', handleResize);
            chartInstance.dispose();
          };
        } catch (e) {
          console.error('ECharts加载失败:', e);
          setError('图表库加载失败');
        } finally {
          setIsLoading(false);
        }
      };
      initializeChart();
    }, 500); // 增加延迟,确保DOM完全渲染
    return () => clearTimeout(timer);
  }, [code]);
  // 当全屏状态变化时重新调整图表大小
  useEffect(() => {
    if (chartInstanceRef.current) {
      setTimeout(() => {
        chartInstanceRef.current.resize();
      }, 300); // 给DOM一些时间来更新
    }
  }, [isModalOpen]);
  // 处理全屏切换
  const toggleModal = useCallback(() => {
    setIsModalOpen(prev => !prev);
  }, []);
  // 如果没有token,不渲染任何内容
  if (!token) {
    return null;
  }
  // 处理模态窗口的图表
  useEffect(() => {
    if (!isModalOpen || !modalChartRef.current) return;
    const initModalChart = async () => {
      try {
        const echarts = await import('echarts');
        // 初始化模态窗口中的图表
        const modalChartInstance = echarts.init(modalChartRef.current);
        // 如果主图表已经初始化,复用其配置
        if (chartInstanceRef.current) {
          const option = chartInstanceRef.current.getOption();
          // 直接使用主图表的完整配置,不做额外修改
          modalChartInstance.setOption(option);
        } else {
          // 如果主图表没有初始化,尝试从代码初始化
          try {
            const safeCode = code.replace(/window\.option/g, 'option');
            const safeFunc = new Function(`
              "use strict";
              let option;
              try {
                ${safeCode}
                return option;
              } catch (e) {
                console.error("模态图表代码执行错误:", e);
                return null;
              }
            `);
            const chartOption = safeFunc();
            if (chartOption) {
              // 与主图表使用完全相同的配置
              modalChartInstance.setOption(chartOption);
            } else {
              throw new Error("无法获取模态图表配置");
            }
          } catch (e) {
            console.error('模态图表代码执行错误:', e);
            // 设置一个默认图表
            modalChartInstance.setOption({
              title: { text: '图表配置错误' },
              xAxis: { type: 'category', data: ['错误'] },
              yAxis: { type: 'value' },
              series: [{ data: [0], type: 'bar' }]
            });
          }
        }
        // 添加窗口大小变化监听器
        const handleResize = () => modalChartInstance.resize();
        window.addEventListener('resize', handleResize);
        // 返回清理函数
        return () => {
          window.removeEventListener('resize', handleResize);
          modalChartInstance.dispose();
        };
      } catch (e) {
        console.error('模态图表初始化失败:', e);
      }
    };
    // 延迟一点执行,确保DOM已更新完成
    const timer = setTimeout(initModalChart, 100);
    return () => clearTimeout(timer);
  }, [isModalOpen, code]);
  // 简化的渲染逻辑,确保容器有明确的尺寸
  return (
    <div className="min-h-screen bg-[#0A1033] text-white">
      {/* 顶部导航 */}
      <div className="bg-[#131C41] p-4 border-b border-[#6ADBFF]/20">
        <div className="max-w-4xl mx-auto flex justify-between items-center">
          <Link href="/" className="text-[#6ADBFF] hover:underline flex items-center gap-2">
            <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
              <path fillRule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
            </svg>
            返回首页
          </Link>
          <h1 className="text-xl font-bold">AI助手对话</h1>
    <div className="relative my-4">
      {isLoading && (
        <div className="absolute inset-0 flex items-center justify-center bg-white/70 z-10">
          <div className="flex flex-col items-center">
            <div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-500 mb-3"></div>
            <p className="text-gray-500">图表加载中...</p>
          </div>
        </div>
      </div>
      {/* 聊天区域 */}
      <div className="max-w-4xl mx-auto p-4">
        <div className="space-y-4 mb-20">
          <div className="flex justify-start">
            <div className="max-w-[80%] rounded-lg p-4 bg-[#131C41]">
              <p>你好!欢迎来到聊天室。</p>
      )}
      {error && (
        <div className="absolute top-0 left-0 right-0 bg-red-50 border border-red-200 text-red-600 px-3 py-2 rounded-md text-xs z-10">
          {error}
        </div>
      )}
      {/* 图表容器 - 固定高度 */}
      <div
        ref={chartContainerRef}
        className="w-full bg-white border border-gray-200 rounded-lg overflow-hidden chart-container"
        style={{
          height: '400px',
          minHeight: '400px',
          visibility: 'visible',
          position: 'relative'
        }}
        data-echarts-container="true"
      />
      {/* 打开弹窗按钮 */}
      <button
        onClick={() => setIsModalOpen(true)}
        className="absolute top-2 right-2 bg-white/90 hover:bg-white p-2 rounded-full shadow-md z-20 transition-all duration-300 hover:scale-110 cursor-pointer"
        title="在弹窗中查看"
      >
        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 3h6v6m0-6L14 9M9 21H3v-6m0 6l7-7" />
        </svg>
      </button>
      {/* 弹出式对话框 */}
      {isModalOpen && (
        <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 p-4">
          <div className="bg-white rounded-lg w-[90vw] max-w-6xl h-[80vh] relative flex flex-col">
            {/* 对话框头部 */}
            <div className="flex justify-between items-center p-4 border-b">
              <h3 className="text-xl font-semibold text-gray-800">数据可视化图表</h3>
              <button
                onClick={() => setIsModalOpen(false)}
                className="p-1 rounded-full hover:bg-gray-100 cursor-pointer"
                aria-label="关闭"
              >
                <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                </svg>
              </button>
            </div>
            {/* 对话框主体 - 图表 */}
            <div className="flex-1 overflow-hidden p-4">
              <div
                ref={modalChartRef}
                className="w-full h-full"
                data-echarts-modal
              ></div>
            </div>
          </div>
        </div>
      </div>
      )}
    </div>
  );
}
      {/* 输入区域 */}
      <div className="fixed bottom-0 left-0 right-0 bg-[#131C41] border-t border-[#6ADBFF]/20 p-4">
        <div className="max-w-4xl mx-auto flex gap-4">
          <input
            type="text"
            placeholder="输入消息..."
            className="flex-1 bg-[#1E2B63] rounded-lg px-4 py-2 text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#6ADBFF]/50"
          />
          <button
            className="bg-gradient-to-r from-[#6ADBFF] to-[#5E72EB] text-white px-6 py-2 rounded-lg font-medium hover:opacity-90 transition-opacity"
          >
            发送
          </button>
// 修改代码块渲染组件,添加更好的类型检测
function CodeBlockRenderer({ language, value }: { language: string; value: string }) {
  // 判断是否是JavaScript代码,检测是否包含图表相关特征
  const isEchartsCode = useCallback(() => {
    if (language !== 'javascript') return false;
    // 检查是否包含ECharts特有的配置项
    return value.includes('option') &&
      (value.includes('series') ||
       value.includes('chart') ||
       value.includes('echarts') ||
       value.includes('xAxis') ||
       value.includes('yAxis'));
  }, [language, value]);
  // 检查消息是否完整 - 通过父组件传递的isMessageComplete状态
  const isComplete = useContext(MessageCompletionContext);
  // 在消息未完成时显示加载动画 - 固定高度匹配图表
  if (language === 'javascript' && !isComplete) {
    return (
      <div className="w-full bg-gray-50 rounded-md my-4 p-6 text-center" style={{height: '400px'}}>
        <div className="flex flex-col items-center justify-center h-full">
          <div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-400 mb-4"></div>
          <p className="text-gray-500">加载中...</p>
          <p className="text-xs text-gray-400 mt-2">等待消息完成后将渲染图表</p>
        </div>
      </div>
    );
  }
  // 消息完成后,如果是图表代码则渲染为图表 - 在外部包装一层固定高度容器
  if (isEchartsCode() && isComplete) {
    return (
      <div style={{height: '400px', minHeight: '400px', position: 'relative'}}>
        <EchartsRenderer code={value} />
      </div>
    );
  }
  // 普通JavaScript代码或其他语言的代码块,直接显示代码
  return (
    <div className="bg-gray-800 rounded-md my-2 overflow-hidden">
      <div className="flex items-center justify-between px-4 py-2 border-b border-gray-700">
        <span className="text-xs text-gray-400">{language}</span>
      </div>
      <pre className="p-4 text-gray-300 overflow-x-auto">
        <code>{value}</code>
      </pre>
    </div>
  );
}
// 创建独立的输入组件,避免状态共享导致的重渲染
interface ChatInputProps {
  onSendMessage: () => void;
  isStreaming: boolean;
  isMessageComplete: boolean;
}
function ChatInput({ onSendMessage, isStreaming, isMessageComplete }: ChatInputProps) {
  // 内部状态,与外部完全隔离
  const [inputText, setInputText] = useState('');
  const inputRef = useRef<HTMLTextAreaElement>(null);
  // 处理输入变化
  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setInputText(e.target.value);
  };
  // 处理按键事件
  const handleKeyDown = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  };
  // 处理发送
  const handleSend = () => {
    if (isStreaming || !inputText.trim() || !isMessageComplete) return;
    // 通知父组件发送消息前更新消息内容
    (window as any).messageToSend = inputText.trim();
    // 清空输入
    setInputText('');
    // 调用父组件的发送方法
    onSendMessage();
  };
  return (
    <div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white via-white to-white/95 pt-4 pb-6" style={{ zIndex: 50 }}>
      <div className="max-w-4xl mx-auto px-6">
        <div className="relative">
          <div className="absolute -top-6 left-1/2 -translate-x-1/2 w-48 h-[1px] bg-gradient-to-r from-transparent via-gray-200 to-transparent"></div>
          <div className="flex gap-4 items-start">
            <div className="flex-1 relative">
              <textarea
                ref={inputRef}
                value={inputText}
                onChange={handleInputChange}
                onKeyDown={handleKeyDown}
                placeholder="输入消息..."
                disabled={isStreaming}
                className="w-full resize-none rounded-xl border-0 bg-gray-50 px-4 py-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-100/50 focus:bg-white min-h-[48px] max-h-32 shadow-inner transition-all duration-300 ease-in-out hover:bg-gray-100/70 disabled:opacity-50 disabled:cursor-not-allowed"
                style={{ height: '48px' }}
              />
              <div className="absolute right-4 bottom-2 text-xs text-gray-400 bg-gray-50 px-2">
                按Enter发送,Shift+Enter换行
              </div>
            </div>
            <button
              onClick={handleSend}
              disabled={isStreaming || !inputText.trim() || !isMessageComplete}
              className="h-12 px-5 bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-medium rounded-xl transition-all duration-300 flex items-center gap-2 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/30 hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none cursor-pointer"
            >
              <span>{isStreaming ? '回复中...' : (isMessageComplete ? '发送' : '处理中...')}</span>
              <svg xmlns="http://www.w3.org/2000/svg" className={`h-4 w-4 transform rotate-45 ${isStreaming ? 'animate-pulse' : ''}`} viewBox="0 0 20 20" fill="currentColor">
                <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" />
              </svg>
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}
export default function Page() {
  const router = useRouter();
  const [apiKey, setApiKey] = useState<string>('');
  const [showApiKeyInput, setShowApiKeyInput] = useState(false);
  const [message, setMessage] = useState('');
  const [messages, setMessages] = useState<Message[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [showError, setShowError] = useState(false);
  const [conversationId, setConversationId] = useState<string | null>(null);
  const [isMessageComplete, setIsMessageComplete] = useState(true);
  const [currentMessageId, setCurrentMessageId] = useState<string | null>(null);
  // 添加一个新的状态来控制消息的显示
  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);
  // 分离消息输入状态,避免触发不必要的重渲染
  const messageInputRef = useRef<HTMLTextAreaElement>(null);
  const [localMessage, setLocalMessage] = useState('');
  const handleMessageChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    // 只更新本地状态,不触发全局重渲染
    setLocalMessage(e.target.value);
  };
  // 仅在发送时更新全局消息状态
  const handleKeyPress = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      // 更新全局状态并发送
      setMessage(localMessage);
      setTimeout(() => handleSendMessage(), 10);
    }
  };
  // 点击发送按钮时
  const handleSendButtonClick = () => {
    // 检查消息是否为空
    if (!localMessage.trim() || !isMessageComplete) return;
    // 更新全局状态并发送
    setMessage(localMessage);
    setTimeout(() => handleSendMessage(), 10);
  };
  // 同步消息状态到本地状态
  useEffect(() => {
    setLocalMessage(message);
  }, [message]);
  useEffect(() => {
    // 获取存储的API Key
    const storedApiKey = localStorage.getItem('api-key');
    if (storedApiKey) {
      setApiKey(storedApiKey);
    } else {
      setShowApiKeyInput(true);
    }
  }, []);
  useEffect(() => {
    // 滚动到最新消息
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);
  // 处理错误显示和自动消失
  const showErrorMessage = useCallback((message: string) => {
    setError(message);
    // 先设置为false确保动画能重新触发
    setShowError(false);
    // 使用 requestAnimationFrame 确保状态变化被正确渲染
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        setShowError(true);
      });
    });
    if (errorTimeoutRef.current) {
      clearTimeout(errorTimeoutRef.current);
    }
    errorTimeoutRef.current = setTimeout(() => {
      setShowError(false);
      setTimeout(() => {
        setError(null);
      }, 400);
    }, 3000);
  }, []);
  // 组件卸载时清除定时器
  useEffect(() => {
    const cleanup = () => {
      if (errorTimeoutRef.current) {
        clearTimeout(errorTimeoutRef.current);
      }
    };
    return cleanup;
  }, []);
  // 添加组件卸载时的清理工作
  useEffect(() => {
    // 组件挂载时,设置为true
    isMountedRef.current = true;
    // 组件卸载时,设置为false
    return () => {
      isMountedRef.current = false;
      // 清除所有定时器
      if (errorTimeoutRef.current) {
        clearTimeout(errorTimeoutRef.current);
      }
    };
  }, []);
  const handleApiKeySubmit = () => {
    if (apiKey.trim()) {
      localStorage.setItem('api-key', apiKey);
      setShowApiKeyInput(false);
      setError(null);
    }
  };
  const handleSendMessage = async () => {
    // 从window对象获取消息内容
    const inputMessage = (window as any).messageToSend || '';
    if (!inputMessage.trim() || !isMessageComplete) return;
    if (!apiKey) {
      showErrorMessage('请先设置API Key');
      return;
    }
    setIsStreaming(true);
    setIsMessageComplete(false);
    // 创建新消息
    const userMessage: Message = {
      role: 'user',
      content: inputMessage.trim(),
      timestamp: Date.now(),
      id: 'user-' + Date.now().toString()
    };
    const assistantMessage: Message = {
      role: 'assistant',
      content: '',
      timestamp: Date.now(),
      id: 'temp-' + Date.now().toString()
    };
    // 使用函数式更新确保状态更新的一致性
    setMessages(prevMessages => {
      const newMessages = [...prevMessages];
      // 如果存在未完成的助手消息,先移除它
      if (!isMessageComplete && currentMessageId) {
        const lastMessage = newMessages[newMessages.length - 1];
        if (lastMessage && lastMessage.role === 'assistant' && lastMessage.id === currentMessageId) {
          newMessages.pop();
        }
      }
      return [...newMessages, userMessage, assistantMessage];
    });
    setCurrentMessageId(assistantMessage.id);
    setMessage('');
    // 清除输入内容
    (window as any).messageToSend = '';
    let controller: AbortController | null = new AbortController();
    try {
      const response = await fetch(`${BASE_URL}/v1/chat-messages`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
        },
        mode: 'cors',
        body: JSON.stringify({
          inputs: {},
          query: userMessage.content,
          response_mode: 'streaming',
          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 || `错误: ${response.status}`;
        } catch {
          errorMessage = `错误: ${response.status}`;
        }
        throw new Error(errorMessage);
      }
      // 确保response.body存在
      if (!response.body) {
        throw new Error('响应没有提供数据流');
      }
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      while (true) {
        try {
          const { done, value } = await reader.read();
          if (done) {
            console.log("流式响应接收完毕");
            setIsMessageComplete(true);
            break;
          }
          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;
            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':
                    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);
                      }
                    }
                    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);
                      }
                    }
                    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) {
      // 检查是否是中止错误
      if (err instanceof Error && err.name === 'AbortError') {
        console.log('请求被中止,可能是组件卸载导致的');
        return; // 中止错误不需要显示给用户
      }
      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 {
      // 清除控制器
      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;
    });
  }, []);
  useEffect(() => {
    // 页面加载后延迟显示消息列表,避免闪烁
    const timer = setTimeout(() => {
      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; }
      /* 优化图表容器样式,不再使用isolation和will-change */
      .echart-wrapper {
        position: relative;
        z-index: auto;
      }
      /* 使用更温和的性能优化属性,避免层级错误 */
      canvas {
        transition: none !important;
      }
      /* 修复输入框遮挡问题 */
      .fixed.bottom-0 {
        z-index: 10;
      }
      /* 防止图表闪烁但不影响其他元素 */
      .echart-wrapper > div {
        transform: translateZ(0);
        will-change: transform;
        transition: none !important;
      }
      /* 修复echarts渲染问题 */
      [data-echarts-container] {
        visibility: visible !important;
        opacity: 1 !important;
        width: 100% !important;
        contain: none !important;
      }
    `;
    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 (
    <div className="min-h-screen bg-gradient-to-b from-gray-50 to-white text-gray-900 flex flex-col">
      {/* API Key Modal */}
      {showApiKeyInput && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
          <div className="bg-white rounded-2xl p-6 w-[400px] space-y-4 shadow-xl">
            <div className="flex justify-between items-center">
              <h2 className="text-xl font-semibold">设置 API Key</h2>
              <button
                onClick={() => setShowApiKeyInput(false)}
                className="text-gray-400 hover:text-gray-600"
              >
                <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                  <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
                </svg>
              </button>
            </div>
            <div className="space-y-2">
              <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
                onClick={() => setShowApiKeyInput(false)}
                className="px-4 py-2 text-gray-600 hover:text-gray-900"
              >
                取消
              </button>
              <button
                onClick={handleApiKeySubmit}
                className="px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600"
              >
                确定
              </button>
            </div>
          </div>
        </div>
      )}
      {/* 错误提示 */}
      {error && (
        <div
          className={`fixed top-20 left-1/2 transform -translate-x-1/2
            flex items-center gap-2 px-4 py-2 text-sm text-white
            transition-all duration-400 ease-out
            ${showError
              ? 'opacity-100 translate-y-0 scale-100'
              : 'opacity-0 -translate-y-2 scale-95'
            }
            before:content-[''] before:absolute before:inset-0 before:bg-red-500
            before:rounded-lg before:opacity-90 before:-z-10
            after:content-[''] after:absolute after:inset-0 after:bg-red-500/50
            after:blur-md after:rounded-lg after:-z-20`}
        >
          <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
            <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
          </svg>
          <span className="relative">{error}</span>
        </div>
      )}
      {/* API Key 按钮 */}
      <button
        onClick={() => setShowApiKeyInput(true)}
        className="fixed top-24 right-6 z-10 w-9 h-9 flex items-center justify-center rounded-full bg-white/80 hover:bg-white shadow-sm border border-gray-100 transition-colors"
        title="设置API Key"
      >
        <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
        </svg>
      </button>
      {/* 聊天区域 */}
      <div className="flex-1 flex flex-col h-screen">
        <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.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={`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`}>
                        {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">
                                    {mainContent ? (
                                      <MessageCompletionContext.Provider
                                        value={msg.id !== currentMessageId || isMessageComplete}
                                      >
                                        <ReactMarkdown
                                          remarkPlugins={[remarkGfm]}
                                          rehypePlugins={[rehypeRaw, rehypeSanitize]}
                                          components={{
                                            // @ts-ignore - ReactMarkdown 组件类型定义的兼容性问题
                                            code: ({ node, inline, className, children, ...props }) => {
                                              const match = /language-(\w+)/.exec(className || '');
                                              const language = match ? match[1] : '';
                                              const value = String(children).replace(/\n$/, '');
                                              if (!inline && match) {
                                                return <CodeBlockRenderer language={language} value={value} />;
                                              }
                                              return (
                                                <code className={className} {...props}>
                                                  {children}
                                                </code>
                                              );
                                            }
                                          }}
                                        >
                                          {mainContent}
                                        </ReactMarkdown>
                                      </MessageCompletionContext.Provider>
                                    ) : (
                                      msg.role === 'assistant' && !isMessageComplete ? '处理回复中...' : ''
                                    )}
                                  </div>
                                </div>
                                {/* 加载指示器 */}
                                {msg.role === 'assistant' &&
                                  !isMessageComplete &&
                                  msg.id === currentMessageId && ( // 只在当前处理的消息显示加载指示器
                                  <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 ref={messagesEndRef} />
            </div>
          </div>
        </div>
        {/* 输入区域 - 抽取为独立组件 */}
        <ChatInput
          onSendMessage={handleSendMessage}
          isStreaming={isStreaming}
          isMessageComplete={isMessageComplete}
        />
      </div>
    </div>
  );
}