From 05e55761058e2089e81fb93dd4000dc3f42f40b3 Mon Sep 17 00:00:00 2001 From: hongjli <3117313295@qq.com> Date: 星期四, 05 六月 2025 16:17:12 +0800 Subject: [PATCH] 页面调整优化 --- src/app/supply-chain-chat/page.tsx | 1430 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 1,430 insertions(+), 0 deletions(-) diff --git a/src/app/supply-chain-chat/page.tsx b/src/app/supply-chain-chat/page.tsx new file mode 100644 index 0000000..5ae49f4 --- /dev/null +++ b/src/app/supply-chain-chat/page.tsx @@ -0,0 +1,1430 @@ +'use client'; + +import { useState, useEffect, useRef, useCallback, useContext, createContext, Suspense } 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'; +import DataPreviewDialog from '@/components/DataPreviewDialog'; + +// 鍒涘缓涓�涓秷鎭畬鎴愮姸鎬佺殑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 SUPPLY_CHAIN_SCENE = { + title: '渚涘簲閾惧叏鏅礊瀵�', + description: '閫氳繃AI Agent涓嶢PS鐨勬繁搴﹀崗鍚岋紝灏嗘彃鍗曞搷搴斾粠"琚姩鏁戠伀"鍗囩骇涓�"棰勬祴-鍐崇瓥-鎵ц"涓変綅涓�浣撶殑鏅鸿兘杩愯惀妯″紡锛屽姪鍔涙柊鑳芥簮涓氬姟澧為暱銆�', + imageUrl: '/images/333.png', + chatbotId: 'SCPanoramaInsight', + background: `渚涘簲閾惧叏鏅礊瀵熺郴缁熸槸鏂颁竴浠f櫤鑳戒緵搴旈摼绠$悊骞冲彴鐨勬牳蹇冨紩鎿庛�傝AI绯荤粺鑳藉瑙e喅浼犵粺渚涘簲閾剧鐞嗕腑鐨勫洓澶у叧閿棶棰橈細 + +**鐢熶骇璁″垝涓庝骇鑳藉奖鍝嶈瘎浼帮細** +- 瀹炴椂鐩戞帶鐢熶骇绾跨姸鎬侊紝鍔ㄦ�佽瘎浼颁骇鑳藉埄鐢ㄧ巼 +- 鍩轰簬鍘嗗彶鏁版嵁鍜屽疄鏃惰鍗曪紝鏅鸿兘棰勬祴鐢熶骇鐡堕 +- 鑷姩璋冩暣鐢熶骇璁″垝锛屾渶澶у寲浜ц兘鍒╃敤鏁堢巼 +- 鎻愪緵澶氬満鏅敓浜ц鍒掑姣斿垎鏋愶紝杈呭姪鍐崇瓥鍒跺畾 + +**渚涘簲鍗忓悓鑳藉姏璇勪及锛�** +- 瀹炴椂杩借釜渚涘簲鍟嗕氦浠樼姸鎬侊紝璇勪及渚涘簲椋庨櫓 +- 鏅鸿兘鍒嗘瀽渚涘簲鍟嗙哗鏁堬紝鎻愪緵渚涘簲鍟嗕紭鍖栧缓璁� +- 棰勬祴鍘熸潗鏂欓渶姹傦紝鎻愬墠鍚姩閲囪喘娴佺▼ +- 鏋勫缓渚涘簲閾剧綉缁滈煣鎬фā鍨嬶紝鎻愬崌鎶楅闄╄兘鍔� + +**浜や粯灞ョ害椋庨櫓璇勪及锛�** +- 鍩轰簬璁㈠崟銆佸簱瀛樸�佺敓浜ц繘搴﹁繘琛岀患鍚堥闄╄瘎浼� +- 鎻愬墠璇嗗埆娼滃湪寤舵湡椋庨櫓锛屽埗瀹氬簲瀵归妗� +- 瀹炴椂璺熻釜浜や粯杩涘害锛岀‘淇濆鎴锋弧鎰忓害 +- 寤虹珛瀹㈡埛棰勬湡绠$悊鏈哄埗锛屾彁鍗囨湇鍔′綋楠� + +**鎴愭湰涓庤储鍔″奖鍝嶈瘎浼帮細** +- 鍏ㄩ摼璺垚鏈拷韪紝绮剧‘璁$畻鍚勭幆鑺傛垚鏈瀯鎴� +- 鍔ㄦ�佹垚鏈紭鍖栧缓璁紝闄嶄綆鏁翠綋杩愯惀鎴愭湰 +- 璐㈠姟褰卞搷棰勬祴鍒嗘瀽锛屾敮鎸佸晢涓氬喅绛� +- ROI璇勪及妯″瀷锛岄噺鍖栨敼杩涙帾鏂界殑璐㈠姟鏁堢泭`, + instructions: `鎮ㄥ彲浠ラ�氳繃浠ヤ笅鏂瑰紡涓庝緵搴旈摼鍏ㄦ櫙娲炲療绯荤粺浜掑姩锛� + +**鍦烘櫙涓�锛氭彃鍗曞奖鍝嶅垎鏋�** +鎻愪緵鏂拌鍗曚俊鎭紝绯荤粺灏嗕粠鍥涗釜缁村害璇勪及鎻掑崟鍙鎬у拰褰卞搷銆� + +**鍦烘櫙浜岋細鐢熶骇璁″垝浼樺寲** +鎻忚堪褰撳墠鐢熶骇鐘跺喌锛岃幏鍙栨櫤鑳戒紭鍖栧缓璁拰椋庨櫓棰勮銆� + +**鍦烘櫙涓夛細渚涘簲閾鹃闄╄瘎浼�** +杈撳叆渚涘簲鍟嗘垨鍘熸潗鏂欎俊鎭紝鑾峰彇渚涘簲椋庨櫓鍒嗘瀽鍜屽簲瀵圭瓥鐣ャ�� + +**鍦烘櫙鍥涳細鎴愭湰浼樺寲鍒嗘瀽** +鎻愪緵鎴愭湰鐩稿叧鏁版嵁锛岃幏鍙栨垚鏈紭鍖栧缓璁拰璐㈠姟褰卞搷璇勪及銆� + +绯荤粺灏嗗熀浜庡疄鏃舵暟鎹拰AI绠楁硶锛屼负鎮ㄦ彁渚涗笓涓氱殑鍒嗘瀽鎶ュ憡鍜屽喅绛栧缓璁�俙, + dataDescription: '鏈満鏅暣鍚堜簡OMS銆丒RP銆乄MS绛夊涓郴缁熺殑瀹炴椂鏁版嵁锛岄�氳繃AI绠楁硶杩涜娣卞害鍒嗘瀽銆備互涓嬩负鏂拌兘婧愯涓氱殑妯℃嫙鏁版嵁锛屽疄闄呭簲鐢ㄤ腑灏嗚嚜鍔ㄥ鎺ヤ紒涓氱幇鏈夌郴缁熴��', + exampleData: ` +### 璁㈠崟闇�姹傛暟鎹� (OMS绯荤粺) + +| 璁㈠崟鍙� | 浜у搧鍨嬪彿 | 鏁伴噺 | 瀹㈡埛鍚嶇О | 棰勬湡浜や粯鏃ユ湡 | 浼樺厛绾� | 璁㈠崟閲戦 | +| :--: | :--: | :--: | :--: | :--: | :--: | :--: | +| SO20250201001 | NEV-Battery-A | 500 | 姣斾簹杩� | 2025-02-15 | 楂� | 2,500,000 | +| SO20250201002 | NEV-Motor-B | 200 | 钄氭潵姹借溅 | 2025-02-20 | 涓� | 1,800,000 | +| SO20250201003 | NEV-Controller-C | 300 | 鐞嗘兂姹借溅 | 2025-02-25 | 楂� | 3,200,000 | +| SO20250201004 | NEV-Charger-D | 150 | 灏忛箯姹借溅 | 2025-03-01 | 涓� | 1,200,000 | + +### 鐢熶骇宸ュ崟鏁版嵁 (ERP绯荤粺) + +| 宸ュ崟鍙� | 浜у搧鍨嬪彿 | 璁″垝鏁伴噺 | 宸插畬鎴愭暟閲� | 鐢熶骇鐘舵�� | 棰勮瀹屾垚鏃堕棿 | 鐢熶骇绾� | +| :--: | :--: | :--: | :--: | :--: | :--: | :--: | +| WO20250201001 | NEV-Battery-A | 600 | 350 | 杩涜涓� | 2025-02-12 | 浜х嚎1 | +| WO20250201002 | NEV-Motor-B | 250 | 180 | 杩涜涓� | 2025-02-18 | 浜х嚎2 | +| WO20250201003 | NEV-Controller-C | 200 | 50 | 璁″垝涓� | 2025-02-22 | 浜х嚎3 | +| WO20250201004 | NEV-Charger-D | 100 | 0 | 寰呮帓浜� | 2025-02-28 | 浜х嚎4 | + +### 搴撳瓨鏁版嵁 (WMS绯荤粺) + +| 鐗╂枡缂栫爜 | 鐗╂枡鍚嶇О | 褰撳墠搴撳瓨 | 瀹夊叏搴撳瓨 | 鍦ㄩ�旀暟閲� | 棰勮鍒拌揣鏃堕棿 | 搴撲綅 | +| :--: | :--: | :--: | :--: | :--: | :--: | :--: | +| MAT001 | 閿傜數姹犺姱 | 1,200 | 500 | 800 | 2025-02-10 | A鍖�-01 | +| MAT002 | 鐢垫満瀹氬瓙 | 350 | 200 | 150 | 2025-02-12 | B鍖�-02 | +| MAT003 | 鎺у埗鑺墖 | 180 | 100 | 200 | 2025-02-15 | C鍖�-03 | +| MAT004 | 鍏呯數妯″潡 | 95 | 50 | 100 | 2025-02-18 | D鍖�-04 | + +### 渚涘簲鍟嗙哗鏁堟暟鎹� + +| 渚涘簲鍟嗗悕绉� | 渚涘簲鐗╂枡 | 鎸夋椂浜や粯鐜� | 璐ㄩ噺鍚堟牸鐜� | 浠锋牸绔炰簤鍔� | 椋庨櫓绛夌骇 | +| :--: | :--: | :--: | :--: | :--: | :--: | +| 瀹佸痉鏃朵唬 | 閿傜數姹犺姱 | 95% | 99.5% | 楂� | 浣� | +| 姹囧窛鎶�鏈� | 鐢垫満瀹氬瓙 | 88% | 98.2% | 涓� | 涓� | +| 鍦板钩绾� | 鎺у埗鑺墖 | 92% | 99.8% | 涓� | 浣� | +| 鐗规潵鐢� | 鍏呯數妯″潡 | 85% | 97.5% | 楂� | 涓� | + +### 浜ц兘鏁版嵁 + +| 鐢熶骇绾� | 鏃ヤ骇鑳� | 褰撳墠鍒╃敤鐜� | 鐡堕宸ュ簭 | 鍙皟閰嶄骇鑳� | 缁存姢璁″垝 | +| :--: | :--: | :--: | :--: | :--: | :--: | +| 浜х嚎1 | 50鍙� | 85% | 缁勮 | 10鍙� | 2025-02-20 | +| 浜х嚎2 | 30鍙� | 75% | 娴嬭瘯 | 8鍙� | 2025-02-25 | +| 浜х嚎3 | 40鍙� | 60% | 鐒婃帴 | 15鍙� | 2025-03-01 | +| 浜х嚎4 | 25鍙� | 45% | 鍖呰 | 12鍙� | 2025-03-05 | +` +}; + +// 榛樿鐢ㄦ埛澶村儚 +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"; + +// 娣诲姞鐢ㄤ簬瑙f瀽鍜屾覆鏌� 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); + + // 鍒濆鍖朌OM瀹瑰櫒 - 鎻愬墠璁剧疆鍥哄畾楂樺害 + useEffect(() => { + // 纭繚瀹瑰櫒鍑嗗濂戒簡骞朵笖鏈夌‘瀹氱殑楂樺害 + if (chartContainerRef.current) { + // 璁剧疆涓�涓浐瀹氱殑楂樺害锛岄伩鍏嶆覆鏌撳悗鍙樺寲 + const container = chartContainerRef.current; + container.style.width = '100%'; + container.style.height = '400px'; // 鍥哄畾楂樺害400px + container.style.minHeight = '400px'; // 闃叉楂樺害鍙樺皬 + + // 娣诲姞涓存椂鍐呭锛岀‘淇滵OM娓叉煋瀹屾垚 + container.innerHTML = '<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#f5f7f9;"><span>姝e湪鍑嗗鍥捐〃...</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; + + // 灏濊瘯瑙f瀽鍜岃缃浘琛ㄩ�夐」 + 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("鍥捐〃浠g爜鎵ц閿欒:", 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('鍥捐〃浠g爜鎵ц閿欒:', 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); // 澧炲姞寤惰繜锛岀‘淇滵OM瀹屽叏娓叉煋 + + return () => clearTimeout(timer); + }, [code]); + + // 褰撳叏灞忕姸鎬佸彉鍖栨椂閲嶆柊璋冩暣鍥捐〃澶у皬 + useEffect(() => { + if (chartInstanceRef.current) { + setTimeout(() => { + chartInstanceRef.current.resize(); + }, 300); // 缁橠OM涓�浜涙椂闂存潵鏇存柊 + } + }, [isModalOpen]); + + // 澶勭悊鍏ㄥ睆鍒囨崲 + const toggleModal = useCallback(() => { + setIsModalOpen(prev => !prev); + }, []); + + // 澶勭悊妯℃�佺獥鍙g殑鍥捐〃 + useEffect(() => { + if (!isModalOpen || !modalChartRef.current) return; + + const initModalChart = async () => { + try { + const echarts = await import('echarts'); + + // 鍒濆鍖栨ā鎬佺獥鍙d腑鐨勫浘琛� + const modalChartInstance = echarts.init(modalChartRef.current); + + // 濡傛灉涓诲浘琛ㄥ凡缁忓垵濮嬪寲锛屽鐢ㄥ叾閰嶇疆 + if (chartInstanceRef.current) { + const option = chartInstanceRef.current.getOption(); + + // 閲嶆柊璁剧疆鍥捐〃閰嶇疆锛岀‘淇濆湪鏂板鍣ㄤ腑姝g‘鏄剧ず + 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"> + {/* 妯℃�佺獥鍙eご閮� */} + <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> + + {/* 妯℃�佺獥鍙e浘琛ㄥ鍣� */} + <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 浠g爜 + 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 || '浠g爜'}</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="澶嶅埗浠g爜" + > + <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"> + 鎸塃nter鍙戦�侊紝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> + ); +} + +// 鍒涘缓涓�涓嫭绔嬬殑缁勪欢鏉ュ鐞唘seSearchParams +function SupplyChainChatContent() { + 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); + + // 娣诲姞璇存槑瀵硅瘽妗嗙姸鎬� + const [showDataPreview, setShowDataPreview] = useState(false); + const [showSceneBackground, setShowSceneBackground] = useState(false); + const [showInstructions, setShowInstructions] = 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); + + // 鎸夌収鎺ュ彛鏂囨。鏍煎紡瑙f瀽鍝嶅簲 + 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 || '鑾峰彇瀵嗛挜澶辫触锛氬搷搴旀牸寮忎笉姝g‘'); + } + } 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 纭繚鐘舵�佸彉鍖栬姝g‘娓叉煋 + 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(() => { + // 缁勪欢鎸傝浇鏃讹紝璁剧疆涓簍rue + isMountedRef.current = true; + + // 缁勪欢鍗歌浇鏃讹紝璁剧疆涓篺alse + return () => { + isMountedRef.current = false; + + // 娓呴櫎鎵�鏈夊畾鏃跺櫒 + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + }; + }, []); + + const handleSendMessage = async () => { + // 浠巜indow瀵硅薄鑾峰彇娑堟伅鍐呭 + const inputMessage = (window as any).messageToSend || ''; + if (!inputMessage.trim() || !isMessageComplete) return; + + // 妫�鏌PI瀵嗛挜锛屽鏋滄病鏈夊垯灏濊瘯鑾峰彇 + let currentApiKey = apiKey; + if (!currentApiKey) { + showErrorMessage('姝e湪鑾峰彇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("瑙f瀽JSON瀛楃涓�:", jsonStr); + const data = JSON.parse(jsonStr); + console.log("瑙f瀽鍚庣殑鏁版嵁瀵硅薄:", 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('浠巃nswer鑺傜偣鑾峰彇绛旀:', 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': + // 杩欎簺浜嬩欢鍙渶璁板綍锛屼笉闇�瑕佹洿鏂癠I + console.log(`宸ヤ綔娴佷簨浠� ${data.event}:`, data); + break; + + case 'error': + console.error('鏈嶅姟鍣ㄨ繑鍥為敊璇簨浠�:', data); + setIsMessageComplete(true); + throw new Error(data.message || '鍙戦�佹秷鎭椂鍑洪敊'); + } + } catch (e) { + console.error('瑙f瀽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 : '鍙戦�佹秷鎭椂鍑洪敊'; + + // 鍙湁鍦ㄧ粍浠朵粛鐒舵寕杞芥椂鎵嶆洿鏂癠I + 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; + + // 鍙湁鍦ㄧ粍浠朵粛鐒舵寕杞芥椂鎵嶆洿鏂癠I + 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; // 涓嶉渶瑕佹洿鏂� + } + + // 鏇存柊娑堟伅鍐呭鍜孖D + const updatedContent = (lastMessage.content || '') + answerChunk; + + // 鍒涘缓娑堟伅鐨勬柊鍓湰浠ョ‘淇漅eact妫�娴嬪埌鍙樺寲 + const updatedMessage = { + ...lastMessage, + id: messageId, + content: updatedContent, + timestamp: Date.now() + }; + + if (convId) { + updatedMessage.conversation_id = convId; + } + + // 鏇存柊褰撳墠姝e湪澶勭悊鐨勬秷鎭疘D + 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="flex items-center space-x-2"> + <button + onClick={() => setShowSceneBackground(true)} + className="px-3 py-1.5 text-xs text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-300 border border-gray-200 hover:border-blue-200 cursor-pointer" + title="鏌ョ湅鍦烘櫙鑳屾櫙" + > + 鍦烘櫙鑳屾櫙 + </button> + <button + onClick={() => setShowInstructions(true)} + className="px-3 py-1.5 text-xs text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-300 border border-gray-200 hover:border-blue-200 cursor-pointer" + title="鏌ョ湅浣跨敤璇存槑" + > + 浣跨敤璇存槑 + </button> + <button + onClick={() => setShowDataPreview(true)} + className="px-3 py-1.5 text-xs text-gray-600 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-all duration-300 border border-gray-200 hover:border-blue-200 cursor-pointer" + title="鏌ョ湅鏁版嵁璇存槑" + > + 鏁版嵁璇存槑 + </button> + </div> + <div className="h-4 w-px bg-gray-200"></div> + <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' && ( + <>{(() => { + // 瑙f瀽娑堟伅鍐呭锛屾彁鍙栨�濊�冮儴鍒� + 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> + + {/* 鍦烘櫙鑳屾櫙瀵硅瘽妗� */} + {showSceneBackground && ( + <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> + <div className="bg-white rounded-2xl w-full max-w-4xl max-h-[80vh] overflow-hidden shadow-xl"> + <div className="flex justify-between items-center p-6 border-b border-gray-200"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center"> + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> + </svg> + </div> + <h2 className="text-xl font-semibold text-gray-900">鍦烘櫙鑳屾櫙</h2> + </div> + <button + onClick={() => setShowSceneBackground(false)} + className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors cursor-pointer" + > + <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="p-6 overflow-y-auto max-h-[60vh]"> + <div className="prose prose-sm max-w-none"> + <ReactMarkdown + remarkPlugins={[remarkGfm]} + rehypePlugins={[rehypeRaw, rehypeSanitize]} + > + {SUPPLY_CHAIN_SCENE.background} + </ReactMarkdown> + </div> + </div> + </div> + </div> + )} + + {/* 浣跨敤璇存槑瀵硅瘽妗� */} + {showInstructions && ( + <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> + <div className="bg-white rounded-2xl w-full max-w-4xl max-h-[80vh] overflow-hidden shadow-xl"> + <div className="flex justify-between items-center p-6 border-b border-gray-200"> + <div className="flex items-center gap-3"> + <div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center"> + <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /> + </svg> + </div> + <h2 className="text-xl font-semibold text-gray-900">浣跨敤璇存槑</h2> + </div> + <button + onClick={() => setShowInstructions(false)} + className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors cursor-pointer" + > + <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="p-6 overflow-y-auto max-h-[60vh]"> + <div className="prose prose-sm max-w-none"> + <ReactMarkdown + remarkPlugins={[remarkGfm]} + rehypePlugins={[rehypeRaw, rehypeSanitize]} + > + {SUPPLY_CHAIN_SCENE.instructions} + </ReactMarkdown> + </div> + </div> + </div> + </div> + )} + + {/* 鏁版嵁璇存槑瀵硅瘽妗� */} + <DataPreviewDialog + isOpen={showDataPreview} + onClose={() => setShowDataPreview(false)} + markdownContent={SUPPLY_CHAIN_SCENE.exampleData} + sceneType="panorama" + /> + </div> + ); +} + +// 涓诲鍑虹粍浠讹紝浣跨敤Suspense鍖呰鍘熸湁缁勪欢 +export default function SupplyChainChatPage() { + return ( + <Suspense fallback={ + <div className="min-h-screen bg-gradient-to-b from-gray-50 to-white flex items-center justify-center"> + <div className="text-center"> + <div className="w-8 h-8 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div> + <p className="text-gray-600">姝e湪鍔犺浇...</p> + </div> + </div> + }> + <SupplyChainChatContent /> + </Suspense> + ); +} \ No newline at end of file -- Gitblit v1.9.3