¶Ô±ÈÐÂÎļþ |
| | |
| | | 'use client'; |
| | | |
| | | import { useState, useEffect, useRef, useCallback, useContext, createContext } from 'react'; |
| | | import Link from 'next/link'; |
| | | import { useRouter, useSearchParams } 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'; |
| | | |
| | | // å建ä¸ä¸ªæ¶æ¯å®æç¶æç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(() => { |
| | | // ç¡®ä¿å®¹å¨åå¤å¥½äºå¹¶ä¸æç¡®å®çé«åº¦ |
| | | 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); |
| | | }, []); |
| | | |
| | | // å¤ç模æçªå£çå¾è¡¨ |
| | | 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); |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | const handleResize = () => modalChartInstance.resize(); |
| | | window.addEventListener('resize', handleResize); |
| | | |
| | | // æ¸
ç彿° |
| | | return () => { |
| | | window.removeEventListener('resize', handleResize); |
| | | modalChartInstance.dispose(); |
| | | }; |
| | | } |
| | | } catch (e) { |
| | | console.error('模æå¾è¡¨åå§å失败:', e); |
| | | } |
| | | }; |
| | | |
| | | initModalChart(); |
| | | }, [isModalOpen]); |
| | | |
| | | return ( |
| | | <> |
| | | <div className="w-full bg-gray-50 rounded-lg overflow-hidden border border-gray-200"> |
| | | {/* å¾è¡¨å·¥å
·æ */} |
| | | <div className="bg-white px-4 py-2 border-b border-gray-200 flex justify-between items-center"> |
| | | <span className="text-sm font-medium text-gray-700">æ°æ®å¾è¡¨</span> |
| | | <div className="flex items-center space-x-2"> |
| | | {error && ( |
| | | <span className="text-xs text-red-500">{error}</span> |
| | | )} |
| | | <button |
| | | onClick={toggleModal} |
| | | className="p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors" |
| | | title="å
¨å±æ¥ç" |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* å¾è¡¨å®¹å¨ */} |
| | | <div className="relative"> |
| | | <div |
| | | ref={chartContainerRef} |
| | | className="w-full" |
| | | style={{ height: '400px', minHeight: '400px' }} |
| | | /> |
| | | {isLoading && ( |
| | | <div className="absolute inset-0 flex items-center justify-center bg-gray-50/80"> |
| | | <div className="flex items-center space-x-2 text-gray-500"> |
| | | <div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent"></div> |
| | | <span className="text-sm">å è½½ä¸...</span> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </div> |
| | | </div> |
| | | |
| | | {/* å
¨å±æ¨¡æçªå£ */} |
| | | {isModalOpen && ( |
| | | <div className="fixed inset-0 bg-black bg-opacity-80 z-50 flex items-center justify-center p-4"> |
| | | <div className="bg-white rounded-lg w-full h-full max-w-6xl max-h-[90vh] flex flex-col"> |
| | | {/* 模æçªå£å¤´é¨ */} |
| | | <div className="flex justify-between items-center p-4 border-b border-gray-200"> |
| | | <h3 className="text-lg font-semibold text-gray-900">å¾è¡¨è¯¦ç»è§å¾</h3> |
| | | <button |
| | | onClick={toggleModal} |
| | | className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-colors" |
| | | > |
| | | <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="M6 18L18 6M6 6l12 12" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | |
| | | {/* 模æçªå£å¾è¡¨å®¹å¨ */} |
| | | <div className="flex-1 p-4"> |
| | | <div |
| | | ref={modalChartRef} |
| | | className="w-full h-full" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | )} |
| | | </> |
| | | ); |
| | | } |
| | | |
| | | function CodeBlockRenderer({ language, value }: { language: string; value: string }) { |
| | | // æ£æ¥æ¯å¦æ¯ ECharts 代ç |
| | | if (language === 'echarts' || language === 'javascript' && value.includes('option')) { |
| | | return <EchartsRenderer code={value} />; |
| | | } |
| | | |
| | | return ( |
| | | <div className="relative bg-gray-50 rounded-lg overflow-hidden border border-gray-200"> |
| | | <div className="bg-white px-4 py-2 border-b border-gray-200 flex justify-between items-center"> |
| | | <span className="text-sm font-medium text-gray-700">{language || '代ç '}</span> |
| | | <button |
| | | onClick={() => navigator.clipboard.writeText(value)} |
| | | className="p-1 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors" |
| | | title="å¤å¶ä»£ç " |
| | | > |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> |
| | | </svg> |
| | | </button> |
| | | </div> |
| | | <pre className="p-4 overflow-x-auto"> |
| | | <code className={`language-${language}`}> |
| | | {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 SupplyChainChatPage() { |
| | | const router = useRouter(); |
| | | const searchParams = useSearchParams(); |
| | | const [apiKey, setApiKey] = useState<string>(''); |
| | | 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>>({}); |
| | | |
| | | // æ·»å APIå¯é¥å è½½ç¶æ |
| | | const [isLoadingApiKey, setIsLoadingApiKey] = useState(false); |
| | | |
| | | // è·åURLåæ° |
| | | const keyParam = searchParams.get('key'); |
| | | |
| | | const messagesEndRef = useRef<HTMLDivElement>(null); |
| | | const errorTimeoutRef = useRef<any>(null); |
| | | |
| | | // å¨ç»ä»¶é¡¶é¨æ·»å ä¸ä¸ªå¼ç¨ï¼ç¨äºè·è¸ªç»ä»¶æ¯å¦å·²å¸è½½ |
| | | const isMountedRef = useRef(true); |
| | | |
| | | // åç¦»æ¶æ¯è¾å
¥ç¶æï¼é¿å
触åä¸å¿
è¦ç鿏²æ |
| | | const messageInputRef = useRef<HTMLTextAreaElement>(null); |
| | | const [localMessage, setLocalMessage] = useState(''); |
| | | |
| | | // æ·»å è·åAPIå¯é¥ç彿° |
| | | const fetchApiKey = async () => { |
| | | setIsLoadingApiKey(true); |
| | | try { |
| | | const response = await fetch('http://121.43.139.99:8080/api/secret-key', { |
| | | method: 'GET', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | }, |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error(`è·åå¯é¥å¤±è´¥: HTTP ${response.status}`); |
| | | } |
| | | |
| | | const result = await response.json(); |
| | | console.log('APIååº:', result); |
| | | |
| | | // æç
§æ¥å£ææ¡£æ ¼å¼è§£æååº |
| | | if (result.code === 200 && result.data && result.data.key) { |
| | | const apiKeyValue = result.data.key; |
| | | setApiKey(apiKeyValue); |
| | | setError(null); |
| | | console.log('æåè·åAPIå¯é¥:', apiKeyValue); |
| | | return apiKeyValue; |
| | | } else { |
| | | throw new Error(result.message || 'è·åå¯é¥å¤±è´¥ï¼ååºæ ¼å¼ä¸æ£ç¡®'); |
| | | } |
| | | } catch (err) { |
| | | console.error('è·åAPIå¯é¥å¤±è´¥:', err); |
| | | const errorMessage = err instanceof Error ? err.message : 'è·åå¯é¥æ¶åçæªç¥é误'; |
| | | showErrorMessage(`å¯é¥è·å失败: ${errorMessage}`); |
| | | return null; |
| | | } finally { |
| | | setIsLoadingApiKey(false); |
| | | } |
| | | }; |
| | | |
| | | 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è·åå¯é¥ |
| | | const initializeApiKey = async () => { |
| | | await fetchApiKey(); |
| | | }; |
| | | |
| | | initializeApiKey(); |
| | | }, []); |
| | | |
| | | 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 handleSendMessage = async () => { |
| | | // ä»window对象è·åæ¶æ¯å
容 |
| | | const inputMessage = (window as any).messageToSend || ''; |
| | | if (!inputMessage.trim() || !isMessageComplete) return; |
| | | |
| | | // æ£æ¥APIå¯é¥ï¼å¦ææ²¡æåå°è¯è·å |
| | | let currentApiKey = apiKey; |
| | | if (!currentApiKey) { |
| | | showErrorMessage('æ£å¨è·åAPIå¯é¥...'); |
| | | currentApiKey = await fetchApiKey(); |
| | | if (!currentApiKey) { |
| | | showErrorMessage('æ æ³è·åAPIå¯é¥ï¼è¯·æ£æ¥ç½ç»è¿æ¥'); |
| | | 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(''); |
| | | setShowMessages(true); |
| | | |
| | | // æ¸
é¤è¾å
¥å
容 |
| | | (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 ${currentApiKey}`, |
| | | '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; |
| | | }); |
| | | }, []); |
| | | |
| | | const parseMessageContent = (content: string | null) => { |
| | | if (!content) return { mainContent: null, thinkContent: null }; |
| | | |
| | | // æ£æ¥æ¯å¦å
嫿èå
容 |
| | | const thinkMatch = content.match(/<think>([\s\S]*?)<\/think>/); |
| | | const thinkContent = thinkMatch ? thinkMatch[1].trim() : null; |
| | | const mainContent = content.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); |
| | | |
| | | return { mainContent, thinkContent }; |
| | | }; |
| | | |
| | | const toggleThinkContent = (messageId: string) => { |
| | | setExpandedThinkMessages(prev => ({ |
| | | ...prev, |
| | | [messageId]: !prev[messageId] |
| | | })); |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | // 页é¢å è½½åå»¶è¿æ¾ç¤ºæ¶æ¯å表ï¼é¿å
éªç |
| | | const timer = setTimeout(() => { |
| | | setShowMessages(true); |
| | | }, 100); |
| | | return () => clearTimeout(timer); |
| | | }, []); |
| | | |
| | | 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"> |
| | | {/* é¡¶é¨å¯¼èªæ */} |
| | | <div className="fixed top-16 left-0 right-0 bg-white border-b border-gray-200 z-40"> |
| | | <div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"> |
| | | <div className="flex items-center space-x-4"> |
| | | <button |
| | | onClick={() => router.push('/ai-scene')} |
| | | className="group inline-flex items-center text-sm text-gray-500 hover:text-red-500 transition-all duration-300 cursor-pointer" |
| | | > |
| | | <svg |
| | | xmlns="http://www.w3.org/2000/svg" |
| | | className="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-0.5" |
| | | fill="none" |
| | | viewBox="0 0 24 24" |
| | | stroke="currentColor" |
| | | > |
| | | <path |
| | | strokeLinecap="round" |
| | | strokeLinejoin="round" |
| | | strokeWidth={2} |
| | | d="M10 19l-7-7m0 0l7-7m-7 7h18" |
| | | /> |
| | | </svg> |
| | | <span className="ml-1.5">è¿å</span> |
| | | </button> |
| | | <div className="h-4 w-px bg-gray-200"></div> |
| | | <h1 className="text-xl font-semibold text-gray-900"> |
| | | ä¾åºé¾å
¨æ¯æ´å¯ |
| | | </h1> |
| | | </div> |
| | | <div className="flex items-center space-x-4"> |
| | | <div className="px-3 py-1 bg-green-100 text-green-700 text-sm rounded-full"> |
| | | AIæºè½åæ |
| | | </div> |
| | | {keyParam && ( |
| | | <div className="text-xs text-gray-500"> |
| | | ID: {keyParam} |
| | | </div> |
| | | )} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | {/* é误æç¤º */} |
| | | {error && ( |
| | | <div |
| | | className={`fixed top-36 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> |
| | | )} |
| | | |
| | | {/* è天åºå */} |
| | | <div className="flex-1 flex flex-col h-screen"> |
| | | <div className="flex-1 overflow-y-auto pt-36 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 flex-col items-center justify-center h-[400px] text-center"> |
| | | <div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-4"> |
| | | <svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2-2V7a2 2 0 012-2h2a2 2 0 002-2V3a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 002 2h2a2 2 0 012 2v2a2 2 0 00-2 2h-2a2 2 0 00-2 2v6a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /> |
| | | </svg> |
| | | </div> |
| | | <h3 className="text-lg font-medium text-gray-900 mb-2">ä¾åºé¾å
¨æ¯æ´å¯</h3> |
| | | <p className="text-gray-500 max-w-md"> |
| | | 欢è¿ä½¿ç¨ä¾åºé¾å
¨æ¯æ´å¯ç³»ç»ï¼æ¨å¯ä»¥å¨è¯¢æåå½±ååæãç产计åä¼åãä¾åºé¾é£é©è¯ä¼°çé®é¢ã |
| | | </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> |
| | | ); |
| | | }, |
| | | // æ·»å è¡¨æ ¼æ ·å¼ç»ä»¶ |
| | | table: ({ node, ...props }) => ( |
| | | <div className="overflow-x-auto my-4 rounded-lg border border-gray-100 shadow-sm" style={{ width: '100%', minWidth: '100%' }}> |
| | | <table className="min-w-full divide-y divide-gray-100" style={{ tableLayout: 'auto', width: 'max-content', minWidth: '100%' }} {...props} /> |
| | | </div> |
| | | ), |
| | | thead: ({ node, ...props }) => ( |
| | | <thead className="bg-gray-50/70" {...props} /> |
| | | ), |
| | | th: ({ node, children, ...props }) => ( |
| | | <th className="px-4 py-3 text-sm font-semibold text-gray-700 border-b border-gray-100 text-center" style={{ whiteSpace: 'nowrap', minWidth: 'max-content' }} {...props}> |
| | | {children} |
| | | </th> |
| | | ), |
| | | td: ({ node, ...props }) => ( |
| | | <td className="px-4 py-3 text-sm text-gray-600 border-t border-gray-100 text-center" style={{ whiteSpace: 'nowrap' }} {...props} /> |
| | | ), |
| | | tr: ({ node, ...props }) => ( |
| | | <tr className="hover:bg-gray-50/70 transition-colors duration-150" {...props} /> |
| | | ) |
| | | }} |
| | | > |
| | | {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> |
| | | ); |
| | | } |