| | |
| | | 'use client'; |
| | | |
| | | import { useState, useEffect, useRef, useCallback } 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 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 }); |
| | |
| | | |
| | | // 修改代码块渲染组件,添加更好的类型检测 |
| | | function CodeBlockRenderer({ language, value }: { language: string; value: string }) { |
| | | // 更精确地检测ECharts代码 |
| | | const isEchartsCode = () => { |
| | | // 判断是否是JavaScript代码,检测是否包含图表相关特征 |
| | | const isEchartsCode = useCallback(() => { |
| | | if (language !== 'javascript') return false; |
| | | |
| | | // 检查是否包含ECharts特有的配置项 |
| | | const hasEchartsConfig = |
| | | value.includes('option =') || |
| | | value.includes('const option') || |
| | | value.includes('let option') || |
| | | value.includes('series') && |
| | | (value.includes('type:') || value.includes('tooltip:') || value.includes('xAxis:')); |
| | | |
| | | return hasEchartsConfig; |
| | | }; |
| | | return value.includes('option') && |
| | | (value.includes('series') || |
| | | value.includes('chart') || |
| | | value.includes('echarts') || |
| | | value.includes('xAxis') || |
| | | value.includes('yAxis')); |
| | | }, [language, value]); |
| | | |
| | | // 如果是 ECharts 代码则渲染图表 |
| | | if (isEchartsCode()) { |
| | | // 检查消息是否完整 - 通过父组件传递的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"> |
| | | <div className="flex flex-col items-center justify-center"> |
| | | <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 <EchartsRenderer code={value} />; |
| | | } |
| | | |
| | | // 否则按普通代码块渲染 |
| | | // 普通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"> |
| | |
| | | <div className="p-3"> |
| | | <div className="text-gray-800 leading-relaxed"> |
| | | {mainContent ? ( |
| | | <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} />; |
| | | <MessageCompletionContext.Provider value={isMessageComplete && msg.id === currentMessageId}> |
| | | <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> |
| | | ); |
| | | } |
| | | |
| | | return ( |
| | | <code className={className} {...props}> |
| | | {children} |
| | | </code> |
| | | ); |
| | | } |
| | | }} |
| | | > |
| | | {mainContent} |
| | | </ReactMarkdown> |
| | | }} |
| | | > |
| | | {mainContent} |
| | | </ReactMarkdown> |
| | | </MessageCompletionContext.Provider> |
| | | ) : ( |
| | | msg.role === 'assistant' && !isMessageComplete ? '处理回复中...' : '' |
| | | )} |