import React, { useState } from 'react'; import { Textarea } from 'tdesign-react'; import { Bookmark, BrainCircuit, Bot, ChevronDown, FileSearch, Loader2, RotateCcw, Search, Send, Settings, Sparkles, Square, Copy, Trash2, Edit3, Upload, UserRound, } from 'lucide-react'; import { useTheme } from '../../../contexts/ThemeContext'; import MarkdownRenderer from '../../MarkdownRenderer'; import './qa-animations.css'; /** * 单条检索结果。 * * 多轮检索过程中每一轮可能返回多条资料,这里只保留 UI 展示所需的标题、摘要和来源。 */ export interface QARetrievalItem { id: string; title: string; content: string; source: string; } /** * 多轮检索中的单轮思考节点。 * * `queryLoading` 控制该轮检索结果是否仍显示 loading;retrievals 到达后再渲染资料列表。 */ export interface QAThinkingRound { id: string; // 唯一标识符,用于列表渲染时的 key title: string; // 该轮检索的标题,如"第1轮检索"或"背景资料收集" thoughtMarkdown: string; // 模型思考过程的 Markdown 内容,展示思考步骤和推理逻辑 query: string; // 该轮发送给检索系统的查询语句 queryLoading?: boolean; // 该轮检索是否仍在进行中,true 时显示加载动画 retrievals: QARetrievalItem[]; // 该轮返回的检索结果列表 } /** * 多轮/深度检索的完整思考链。 * * rounds 展示检索过程,finalAnswerMarkdown 展示最终回答;两者分开便于流式过程中逐步更新。 */ export interface QAThinkingTrace { title?: string; // 整体思考链的标题 rounds: QAThinkingRound[]; // 多轮检索过程中的所有轮次,每轮包含思考和检索结果 finalAnswerMarkdown: string; // 基于多轮检索结果生成的最终回答内容 } /** * 单轮 RAG 的回答结构。 * * 后端可能把 `...` 和答案混在同一个 answer 流中,解析后分别放到 thinkingMarkdown 与 answerMarkdown。 */ export interface QASingleRagTrace { title?: string; // 思考过程的标题,如"单轮 RAG 思考过程" thinkingMarkdown: string; // 模型在生成回答前的思考过程内容,使用 Markdown 格式 answerMarkdown: string; // 最终生成的回答内容,使用 Markdown 格式 thinkingComplete?: boolean; // 思考过程是否已完成,true 时思考区域可折叠 } export interface QACommonMessage { id: string; // 消息唯一标识符 role: 'user' | 'assistant'; // 消息发送者角色:user 为用户消息,assistant 为 AI 回复 content: string; // 消息的纯文本内容(当使用 thinkingTrace 或 singleRagTrace 时为空) timestamp?: Date; // 消息发送时间 thinkingTrace?: QAThinkingTrace; // 多轮/深度检索模式的思考链,包含多轮检索过程和最终回答 sources?: { // 消息引用的参考资料列表 title: string; // 参考资料标题 type: string; // 资料类型,如"文档"、"网页"等 snippet: string; // 资料摘要内容 source?: string; // 来源名称或 URL time?: string; // 发布时间 url?: string; // 资料链接 content?: string; // 完整内容(可能用于详情弹窗) }[]; } export interface QACommonMessage { singleRagTrace?: QASingleRagTrace; // 单轮 RAG 模式的思考过程和回答结构 } interface QAAvatarProps { role: 'user' | 'assistant'; isActive?: boolean; label?: string; variant?: 'default' | 'blue' | 'purple'; } const getAssistantAvatarClass = (variant: QAAvatarProps['variant'], isDark: boolean) => { switch (variant) { case 'blue': return isDark ? 'bg-gradient-to-br from-blue-400 via-blue-600 to-cyan-700 text-white' : 'bg-gradient-to-br from-blue-300 via-blue-500 to-cyan-500 text-white'; case 'purple': return isDark ? 'bg-gradient-to-br from-purple-400 via-violet-600 to-fuchsia-700 text-white' : 'bg-gradient-to-br from-purple-300 via-violet-500 to-fuchsia-500 text-white'; default: return isDark ? 'bg-gradient-to-br from-cyan-500 via-blue-600 to-violet-600 text-white' : 'bg-gradient-to-br from-red-400 via-rose-500 to-orange-400 text-white'; } }; /** * 统一头像组件。 * * 单一模式使用默认助手色;对比模式可通过 variant 指定蓝/紫色系,让头像与左右面板标签保持一致。 * `isActive` 会触发呼吸和星光动画,用来提示当前助手正在生成。 */ export const QAAvatar: React.FC = ({ role, isActive = false, label, variant = 'default' }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; const Icon = role === 'assistant' ? Bot : UserRound; return (
{role === 'assistant' && isActive && ( )}
); }; interface TypingIndicatorProps { label?: string; assistantLabel?: string; } /** * 助手预连接/生成中的占位提示。 * * 用于请求已经发出但还没有生成可展示消息时,给用户明确的等待反馈。 */ export const TypingIndicator: React.FC = ({ label = '正在思考并生成回复', assistantLabel = 'AI 助手' }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{label}
{[0, 1, 2].map((idx) => ( ))}
); }; interface ChatMessageProps { message: T; showSources?: boolean; assistantLabel?: string; assistantActive?: boolean; onCitationClick?: (message: T, citationIndex: number) => void; onCopyAssistant?: (message: T) => void; onDeleteAssistant?: (message: T) => void; onEditAssistant?: (message: T) => void; onResendAssistant?: (message: T) => void; renderAssistantExtra?: (message: T) => React.ReactNode; } /** * 聊天消息渲染器。 * * 根据消息类型自动选择普通文本、单轮 RAG 回答或多轮检索思考链。 * sources 区域独立折叠展示,避免引用资料过长时挤占主回答阅读空间。 */ export function ChatMessage({ message, showSources = true, assistantLabel = 'AI 助手', assistantActive = false, onCitationClick, onCopyAssistant, onDeleteAssistant, onEditAssistant, onResendAssistant, renderAssistantExtra, }: ChatMessageProps) { const { theme } = useTheme(); const isDark = theme === 'dark'; const isUser = message.role === 'user'; const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace); return (
{!isUser && }
{message.singleRagTrace && !isUser ? ( onCitationClick?.(message, citationIndex)} /> ) : message.thinkingTrace && !isUser ? ( onCitationClick?.(message, citationIndex)} /> ) : (

{message.content}

)} {!isUser && (onCopyAssistant || onDeleteAssistant || onEditAssistant || onResendAssistant) && (
{onCopyAssistant && ( )} {onDeleteAssistant && ( )} {onEditAssistant && ( )} {onResendAssistant && ( )}
)}
{isUser && }
{!isUser && message.sources && showSources && ( 引用来源 ({message.sources.length})
)} >
{message.sources.map((source, idx) => (
{source.title} {source.type}

{source.snippet}

))}
)} {!isUser && renderAssistantExtra?.(message)}
); } interface CollapsibleSectionProps { title: React.ReactNode; children: React.ReactNode; defaultOpen?: boolean; className?: string; headerClassName?: string; } /** * 通用折叠面板。 * * 思考过程、检索结果、引用来源都复用该组件,保证展开/收起动画和深浅色主题一致。 */ const CollapsibleSection: React.FC = ({ title, children, defaultOpen = true, className = '', headerClassName = '', }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; const [open, setOpen] = useState(defaultOpen); return (
{children}
); }; interface SingleRagAnswerProps { trace: QASingleRagTrace; onCitationClick?: (citationIndex: number) => void; } /** * 单轮 RAG 回答展示。 * * 思考内容和最终回答分成两个区域:思考区可折叠,回答区始终展示。 * 这样既保留模型推理过程,也不会影响用户优先阅读最终答案。 */ export const SingleRagAnswer: React.FC = ({ trace, onCitationClick }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{trace.title || '思考过程'}
{trace.thinkingComplete ? (
思考已完成,可展开查看
) : (
正在接收 <think> 流式思考内容
)}
} > {trace.thinkingMarkdown ? ( ) : (
等待模型输出思考内容...
)}
生成的回答
{trace.answerMarkdown ? ( ) : (
{trace.thinkingComplete ? '正在流式生成回答...' : '思考结束后开始生成回答...'}
)}
); }; interface RetrievalListProps { items: QARetrievalItem[]; } /** * 检索结果列表。 * * 每条资料默认折叠,首条默认展开,便于用户快速看到最相关资料,同时保留完整资料明细。 */ const RetrievalList: React.FC = ({ items }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{items.map((item, index) => ( {index + 1}
{item.title}
来源:{item.source}
} >

{item.content}

))} ); }; interface SearchQueryBadgeProps { query: string; loading?: boolean; } /** * 检索词徽标。 * * 多轮检索每一轮都会产生一个 query;loading 状态下展示旋转图标,表示该 query 正在召回资料。 */ const SearchQueryBadge: React.FC = ({ query, loading = false }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{loading ? : } 检索词 {query}
); }; interface RetrievalThinkingTraceProps { trace: QAThinkingTrace; onCitationClick?: (citationIndex: number) => void; } /** * 多轮/深度检索思考链展示。 * * 组件按轮次渲染思考内容、检索词和检索结果,底部展示最终回答。 * 它只消费已经整理好的 trace 数据,不关心数据来自真实 SSE 还是本地模拟。 */ export const RetrievalThinkingTrace: React.FC = ({ trace, onCitationClick }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{trace.title || '思考过程'}
共 {trace.rounds.length} 轮推理与检索,可展开查看细节
} >
{trace.rounds.map((round, index) => (
{index < trace.rounds.length - 1 && (
)}
第 {index + 1} 轮 {round.title}
} >
思考内容
} >
检索内容 {round.retrievals.length} 条
} > {round.queryLoading ? (
正在召回相关内容...
) : ( )}
))}
最终回答
); }; interface EmptyChatStateProps { icon?: React.ReactNode; title: string; description: string; } /** * 空状态展示。 * * 当当前模式没有任何消息时使用,提示用户可以开始输入问题。 */ export const EmptyChatState: React.FC = ({ icon, title, description }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; return (
{icon ?? }

{title}

{description}

); }; interface QAComposerProps { value: string; placeholder: string; isLoading: boolean; canSubmit?: boolean; onChange: (value: string) => void; onSubmit: () => void; onStop: () => void; onKeyDown?: (e: React.KeyboardEvent) => void; onUpload?: () => void; onReset?: () => void; onSave?: () => void; onCollect?: () => void; onOpenSettings?: () => void; showUpload?: boolean; showReset?: boolean; showSave?: boolean; showCollect?: boolean; } /** * 问答输入区。 * * 负责输入、发送/停止、附件、重置、保存、收藏和模型参数设置入口。 * 具体业务动作通过 props 回调交给父组件处理,保证输入区本身不依赖某个问答模式。 */ export const QAComposer: React.FC = ({ value, placeholder, isLoading, canSubmit, onChange, onSubmit, onStop, onKeyDown, onUpload, onReset, onSave, onCollect, onOpenSettings, showUpload = true, showReset = false, showSave = true, showCollect = true, }) => { const { theme } = useTheme(); const isDark = theme === 'dark'; const submitEnabled = canSubmit ?? Boolean(value.trim()); const iconButtonClass = `p-2 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'}`; return (