Files
deerflow-code/web-code/pages/GeneralQA.tsx
T
2026-05-27 19:43:15 +08:00

985 lines
44 KiB
TypeScript

import React, { useState } from 'react';
import {
Send,
Upload,
RotateCcw,
Bookmark,
History,
Sparkles,
BookOpen,
Brain,
GitCompare,
ChevronDown,
ChevronRight,
MessageSquare,
FileText,
CheckCircle,
AlertCircle,
} from 'lucide-react';
import { useTheme } from '../contexts/ThemeContext';
import { PageType } from '../components/Sidebar';
type QAMode = 'single-round' | 'multi-round' | 'deep-search';
type CompareMode = 'compare-off' | 'compare-on';
type ViewMode = 'single' | 'compare';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
sources?: SourceItem[];
}
interface SourceItem {
title: string;
type: string;
snippet: string;
}
interface CompareResult {
leftMode: QAMode;
rightMode: QAMode;
leftContent: string;
rightContent: string;
sources: {
left: SourceItem[];
right: SourceItem[];
};
differences?: {
consensus: string;
differences: string;
leftAdvantage: string;
rightAdvantage: string;
};
}
const GeneralQA: React.FC<{ onNavigate: (page: PageType) => void }> = ({ onNavigate }) => {
const { theme } = useTheme();
const isDark = theme === 'dark';
// 状态管理
const [viewMode, setViewMode] = useState<ViewMode>('single');
const [qaMode, setQaMode] = useState<QAMode>('single-round');
const [compareLeftMode, setCompareLeftMode] = useState<QAMode>('single-round');
const [compareRightMode, setCompareRightMode] = useState<QAMode>('deep-search');
// 知识范围
const [knowledgeScope, setKnowledgeScope] = useState<'all' | 'specific-kb' | 'specific-domain'>('all');
// 回答偏好
const [answerStyle, setAnswerStyle] = useState<'concise' | 'detailed'>('detailed');
const [showSources, setShowSources] = useState(true);
const [showProcess, setShowProcess] = useState(false);
const [autoSummary, setAutoSummary] = useState(false);
// 对比设置
const [compareLayout, setCompareLayout] = useState<'horizontal' | 'vertical'>('horizontal');
const [autoSummarizeDiff, setAutoSummarizeDiff] = useState(true);
const [recommendBetter, setRecommendBetter] = useState(true);
// 消息和结果
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [compareResult, setCompareResult] = useState<CompareResult | null>(null);
// 检索过程展示
const [searchProcess, setSearchProcess] = useState({
currentMode: '单轮RAG',
rounds: 1,
hits: 5,
queryRewrite: '',
});
// 示例消息
const sampleMessages: Message[] = [
{
id: '1',
role: 'user',
content: 'RAG技术的工作原理是什么?',
timestamp: new Date(),
},
{
id: '2',
role: 'assistant',
content: 'RAG(检索增强生成)技术的工作原理主要包括以下几个步骤:\n\n1. **检索阶段**:系统首先将用户问题转换为查询向量,在知识库中进行相似度搜索,返回最相关的文档片段。\n\n2. **增强阶段**:将检索到的相关文档与原始问题组合,形成增强后的提示词上下文。\n\n3. **生成阶段**:将增强后的提示词发送给大语言模型,生成最终回答。\n\n这种方式可以有效解决LLM知识过时、幻觉等问题,提升回答的准确性和可信度。',
timestamp: new Date(),
sources: [
{ title: 'RAG技术白皮书', type: 'PDF', snippet: 'RAG是一种结合检索和生成的技术架构...' },
{ title: 'LLM应用实践', type: '文档', snippet: '检索增强生成可以有效提升回答质量...' },
],
},
];
const handleSend = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
// 模拟发送消息
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date(),
};
setMessages([...messages, userMessage]);
setInputValue('');
// 模拟AI响应
setTimeout(() => {
setMessages(prev => [...prev, ...sampleMessages]);
setIsLoading(false);
setSearchProcess({
currentMode: qaMode === 'single-round' ? '单轮RAG' : qaMode === 'multi-round' ? '多轮RAG' : '深度检索',
rounds: qaMode === 'multi-round' ? 3 : 1,
hits: 5,
queryRewrite: qaMode === 'deep-search' ? '原问题→分解子问题→独立检索→综合回答' : '',
});
}, 1500);
};
const handleCompare = () => {
if (!inputValue.trim()) return;
setIsLoading(true);
// 模拟对比结果
setTimeout(() => {
setCompareResult({
leftMode: compareLeftMode,
rightMode: compareRightMode,
leftContent: '单轮RAG回答:直接检索并生成答案,响应速度快,但可能遗漏跨文档的关联信息。适用于简单事实查询。',
rightContent: '深度检索回答:通过问题分解、多轮检索和综合分析,提供更全面的答案。包含详细的推理过程和更多的引用来源。',
sources: {
left: [
{ title: 'RAG基础教程', type: '文档', snippet: '单轮RAG是最基础的检索增强方式...' },
],
right: [
{ title: '高级RAG技术', type: '论文', snippet: '深度检索通过query改写和迭代优化...' },
{ title: 'RAG最佳实践', type: '指南', snippet: '多阶段检索可以显著提升召回率...' },
],
},
differences: {
consensus: '两种方式都使用向量检索技术,都能引用原始文档',
differences: '深度检索采用多轮查询改写,覆盖更全面',
leftAdvantage: '响应更快,资源消耗更低',
rightAdvantage: '答案更完整,引用更充分',
},
});
setIsLoading(false);
}, 2000);
};
const handleSaveRecord = () => {
alert('问答记录已保存');
};
const handleCollect = () => {
alert('已添加到收藏');
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (viewMode === 'compare') {
handleCompare();
} else {
handleSend();
}
}
};
// 获取模式标签
const getModeLabel = (mode: QAMode) => {
switch (mode) {
case 'single-round': return '单轮RAG';
case 'multi-round': return '多轮RAG';
case 'deep-search': return '深度检索';
}
};
return (
<div className={`h-[calc(100vh-120px)] flex flex-col theme-bg rounded-xl ${isDark ? 'bg-slate-800/50' : 'bg-white'} border ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
{/* 顶部模式栏 */}
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className="flex items-center gap-4">
<h1 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>
通用问答
</h1>
<span className={`text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
支持单轮RAG、多轮RAG、深度检索及结果对比
</span>
</div>
<div className="flex items-center gap-2">
{/* 模式切换按钮 */}
<div className={`flex rounded-lg p-1 ${isDark ? 'bg-slate-700/50' : 'bg-red-50'}`}>
<button
onClick={() => setViewMode('single')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'single'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'text-slate-400 hover:text-white'
: 'text-gray-600 hover:text-red-600'
}`}
>
单一模式
</button>
<button
onClick={() => setViewMode('compare')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-all ${
viewMode === 'compare'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'text-slate-400 hover:text-white'
: 'text-gray-600 hover:text-red-600'
}`}
>
对比模式
</button>
</div>
<button
onClick={() => onNavigate('qa-records')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
isDark
? 'bg-slate-700/50 text-slate-300 hover:bg-slate-600/50'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<History className="w-4 h-4" />
历史记录
</button>
</div>
</div>
{/* 主体内容区 */}
<div className="flex-1 flex overflow-hidden">
{/* 左侧参数区 */}
<div className={`w-72 border-r flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
{viewMode === 'single' ? (
<>
{/* 问答模式选择 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
问答模式
</h3>
<div className="space-y-2">
{[
{ value: 'single-round', label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
{ value: 'multi-round', label: '多轮RAG', icon: Brain, desc: '深入理解' },
{ value: 'deep-search', label: '深度检索', icon: BookOpen, desc: '全面分析' },
].map((mode) => (
<button
key={mode.value}
onClick={() => setQaMode(mode.value as QAMode)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all text-left ${
qaMode === mode.value
? isDark
? 'bg-blue-600/20 border border-blue-500/50 text-blue-400'
: 'bg-red-50 border border-red-200 text-red-600'
: isDark
? 'bg-slate-700/30 border border-transparent text-slate-400 hover:bg-slate-700/50'
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
<mode.icon className="w-5 h-5" />
<div className="flex-1">
<div className="text-sm font-medium">{mode.label}</div>
<div className={`text-xs ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{mode.desc}</div>
</div>
</button>
))}
</div>
</div>
</>
) : (
<>
{/* 对比模式设置 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比模式设置
</h3>
{/* 左侧模式 */}
<div className="mb-4">
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
模式A
</label>
<select
value={compareLeftMode}
onChange={(e) => setCompareLeftMode(e.target.value as QAMode)}
className={`w-full px-3 py-2 rounded-lg text-sm ${
isDark
? 'bg-slate-700/50 border border-slate-600 text-white'
: 'bg-white border border-gray-200 text-gray-700'
}`}
>
<option value="single-round">单轮RAG</option>
<option value="multi-round">多轮RAG</option>
<option value="deep-search">深度检索</option>
</select>
</div>
<div className={`text-center my-2 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<span className="text-lg">VS</span>
</div>
{/* 右侧模式 */}
<div className="mb-4">
<label className={`text-xs mb-2 block ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
模式B
</label>
<select
value={compareRightMode}
onChange={(e) => setCompareRightMode(e.target.value as QAMode)}
className={`w-full px-3 py-2 rounded-lg text-sm ${
isDark
? 'bg-slate-700/50 border border-slate-600 text-white'
: 'bg-white border border-gray-200 text-gray-700'
}`}
>
<option value="single-round">单轮RAG</option>
<option value="multi-round">多轮RAG</option>
<option value="deep-search">深度检索</option>
</select>
</div>
</div>
{/* 对比布局 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比布局
</h3>
<div className="flex gap-2">
<button
onClick={() => setCompareLayout('horizontal')}
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
compareLayout === 'horizontal'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}
>
左右对比
</button>
<button
onClick={() => setCompareLayout('vertical')}
className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${
compareLayout === 'vertical'
? isDark
? 'bg-blue-600 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-300'
: 'bg-gray-100 text-gray-600'
}`}
>
上下对比
</button>
</div>
</div>
</>
)}
{/* 知识范围 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
知识范围
</h3>
<div className="space-y-2">
{[
{ value: 'all', label: '全部知识源' },
{ value: 'specific-kb', label: '指定知识库' },
{ value: 'specific-domain', label: '指定资源域' },
].map((option) => (
<label
key={option.value}
className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
knowledgeScope === option.value
? isDark
? 'bg-blue-600/20 text-blue-400'
: 'bg-red-50 text-red-600'
: isDark
? 'text-slate-400 hover:bg-slate-700/30'
: 'text-gray-600 hover:bg-gray-50'
}`}
>
<input
type="radio"
name="knowledge-scope"
checked={knowledgeScope === option.value}
onChange={() => setKnowledgeScope(option.value as any)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
knowledgeScope === option.value
? isDark
? 'border-blue-500 bg-blue-500'
: 'border-red-500 bg-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{knowledgeScope === option.value && (
<div className="w-1.5 h-1.5 rounded-full bg-white" />
)}
</div>
<span className="text-sm">{option.label}</span>
</label>
))}
</div>
</div>
{/* 回答偏好 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
回答偏好
</h3>
<div className="space-y-3">
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={answerStyle === 'detailed'}
onChange={() => setAnswerStyle(answerStyle === 'detailed' ? 'concise' : 'detailed')}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
answerStyle === 'detailed'
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{answerStyle === 'detailed' && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">详细回答</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={showSources}
onChange={() => setShowSources(!showSources)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
showSources
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{showSources && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">展示引用来源</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={showProcess}
onChange={() => setShowProcess(!showProcess)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
showProcess
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{showProcess && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">展示检索过程</span>
</label>
</div>
</div>
{/* 对比设置 - 仅对比模式 */}
{viewMode === 'compare' && (
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
对比设置
</h3>
<div className="space-y-3">
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={autoSummarizeDiff}
onChange={() => setAutoSummarizeDiff(!autoSummarizeDiff)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
autoSummarizeDiff
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{autoSummarizeDiff && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">自动总结差异</span>
</label>
<label className={`flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-all ${
isDark ? 'text-slate-400 hover:bg-slate-700/30' : 'text-gray-600 hover:bg-gray-50'
}`}>
<input
type="checkbox"
checked={recommendBetter}
onChange={() => setRecommendBetter(!recommendBetter)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded border flex items-center justify-center ${
recommendBetter
? isDark
? 'bg-blue-500 border-blue-500'
: 'bg-red-500 border-red-500'
: isDark
? 'border-slate-500'
: 'border-gray-300'
}`}>
{recommendBetter && (
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
<span className="text-sm">推荐更优答案</span>
</label>
</div>
</div>
)}
</div>
{/* 中间结果区 */}
<div className="flex-1 flex flex-col overflow-hidden">
{viewMode === 'single' ? (
<>
{/* 聊天消息区 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 ? (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<MessageSquare className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">开始你的问答</p>
<p className="text-sm mt-2">输入问题,获取基于知识库的智能回答</p>
</div>
) : (
messages.map((msg) => (
<div key={msg.id} className="space-y-3">
{/* 用户消息 */}
<div className={`flex gap-3 ${msg.role === 'user' ? 'justify-end' : ''}`}>
{msg.role === 'assistant' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Brain className="w-4 h-4" />
</div>
)}
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${
msg.role === 'user'
? isDark
? 'bg-blue-600/30 text-white'
: 'bg-red-500 text-white'
: isDark
? 'bg-slate-700/50 text-slate-200'
: 'bg-gray-100 text-gray-800'
}`}>
<p className="whitespace-pre-wrap">{msg.content}</p>
</div>
{msg.role === 'user' && (
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${
isDark ? 'bg-slate-700 text-slate-300' : 'bg-gray-200 text-gray-600'
}`}>
<div className="w-4 h-4 rounded-full bg-current" />
</div>
)}
</div>
{/* 来源信息 */}
{msg.role === 'assistant' && msg.sources && showSources && (
<div className={`ml-11 p-3 rounded-lg ${isDark ? 'bg-slate-800/50' : 'bg-gray-50'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{msg.sources.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="flex items-center gap-2 mb-1">
<FileText className={`w-3 h-3 ${isDark ? 'text-blue-400' : 'text-red-400'}`} />
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
<span className={`px-1.5 py-0.5 rounded text-xs ${
isDark ? 'bg-slate-600 text-slate-400' : 'bg-gray-200 text-gray-500'
}`}>
{source.type}
</span>
</div>
<p className={isDark ? 'text-slate-400' : 'text-gray-500'}>{source.snippet}</p>
</div>
))}
</div>
</div>
)}
</div>
))
)}
{isLoading && (
<div className="flex gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
isDark ? 'bg-blue-600/20 text-blue-400' : 'bg-red-100 text-red-500'
}`}>
<Brain className="w-4 h-4" />
</div>
<div className={`max-w-[70%] rounded-xl px-4 py-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<div className="flex gap-1">
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '0ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '150ms' }} />
<div className={`w-2 h-2 rounded-full animate-bounce ${isDark ? 'bg-slate-500' : 'bg-gray-400'}`} style={{ animationDelay: '300ms' }} />
</div>
</div>
</div>
)}
</div>
</>
) : (
<>
{/* 对比结果区 */}
<div className="flex-1 overflow-y-auto p-4">
{compareResult ? (
compareLayout === 'horizontal' ? (
<div className="grid grid-cols-2 gap-4 h-full">
{/* 左侧结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
{getModeLabel(compareResult.leftMode)}
</span>
</div>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.leftContent}
</p>
{/* 左侧来源 */}
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{compareResult.sources.left.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
</div>
))}
</div>
</div>
</div>
</div>
{/* 右侧结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}>
{getModeLabel(compareResult.rightMode)}
</span>
</div>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.rightContent}
</p>
{/* 右侧来源 */}
<div className={`mt-4 pt-4 border-t ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<h4 className={`text-xs font-medium mb-2 ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
引用来源
</h4>
<div className="space-y-2">
{compareResult.sources.right.map((source, idx) => (
<div key={idx} className={`text-xs p-2 rounded ${isDark ? 'bg-slate-700/50' : 'bg-gray-50'}`}>
<span className={`font-medium ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{source.title}
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
) : (
<div className="space-y-4">
{/* 上方结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-blue-600/30 text-blue-400' : 'bg-blue-100 text-blue-600'
}`}>
{getModeLabel(compareResult.leftMode)}
</span>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.leftContent}
</p>
</div>
</div>
{/* 下方结果 */}
<div className={`rounded-xl border ${isDark ? 'border-slate-700/50' : 'border-red-200'}`}>
<div className={`px-4 py-3 border-b ${isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-red-50/50'}`}>
<span className={`px-2 py-1 rounded text-xs font-medium ${
isDark ? 'bg-purple-600/30 text-purple-400' : 'bg-purple-100 text-purple-600'
}`}>
{getModeLabel(compareResult.rightMode)}
</span>
</div>
<div className="p-4">
<p className={`whitespace-pre-wrap ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{compareResult.rightContent}
</p>
</div>
</div>
</div>
)
) : (
<div className={`h-full flex flex-col items-center justify-center ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
<GitCompare className="w-16 h-16 mb-4 opacity-50" />
<p className="text-lg">对比模式</p>
<p className="text-sm mt-2">输入问题,对比不同模式的回答效果</p>
</div>
)}
</div>
</>
)}
{/* 底部输入区 */}
<div className={`border-t p-4 ${isDark ? 'border-slate-700/50' : 'border-red-100'}`}>
<div className={`flex items-end gap-3 rounded-xl p-3 ${isDark ? 'bg-slate-700/50' : 'bg-gray-100'}`}>
<textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={viewMode === 'compare' ? '输入问题进行对比分析...' : '输入你的问题...'}
rows={1}
className={`flex-1 bg-transparent resize-none focus:outline-none ${
isDark ? 'text-white placeholder-slate-500' : 'text-gray-800 placeholder-gray-400'
}`}
/>
<div className="flex items-center gap-2">
<button className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}>
<Upload className="w-5 h-5" />
</button>
<button
onClick={() => {
setMessages([]);
setCompareResult(null);
}}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="重新提问"
>
<RotateCcw className="w-5 h-5" />
</button>
<button
onClick={handleSaveRecord}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="保存记录"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={handleCollect}
className={`p-2 rounded-lg transition-colors ${
isDark ? 'hover:bg-slate-600 text-slate-400' : 'hover:bg-gray-200 text-gray-500'
}`}
title="加入收藏"
>
<Bookmark className="w-5 h-5" />
</button>
<button
onClick={viewMode === 'compare' ? handleCompare : handleSend}
disabled={!inputValue.trim() || isLoading}
className={`px-4 py-2 rounded-lg font-medium transition-all flex items-center gap-2 ${
inputValue.trim() && !isLoading
? isDark
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
: isDark
? 'bg-slate-600 text-slate-400'
: 'bg-gray-300 text-gray-500'
}`}
>
<Send className="w-4 h-4" />
发送
</button>
</div>
</div>
</div>
</div>
{/* 右侧辅助区 */}
{viewMode === 'single' && showProcess && (
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
{/* 检索过程 */}
<div className="mb-6">
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
检索过程
</h3>
<div className={`p-4 rounded-xl ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>当前模式</span>
<span className={`text-sm font-medium ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
{searchProcess.currentMode}
</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>检索轮次</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{searchProcess.rounds}
</span>
</div>
<div className="flex items-center justify-between">
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>命中数</span>
<span className={`text-sm ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
{searchProcess.hits}
</span>
</div>
{searchProcess.queryRewrite && (
<div>
<span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>Query改写</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{searchProcess.queryRewrite}
</p>
</div>
)}
</div>
</div>
</div>
</div>
)}
{/* 对比模式的差异总结 */}
{viewMode === 'compare' && compareResult?.differences && (
<div className={`w-72 border-l flex-shrink-0 overflow-y-auto p-4 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
<h3 className={`text-sm font-medium mb-3 ${isDark ? 'text-slate-300' : 'text-gray-700'}`}>
差异总结
</h3>
<div className={`p-4 rounded-xl space-y-4 ${isDark ? 'bg-slate-700/50' : 'bg-white'}`}>
<div>
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-green-400' : 'text-green-600'}`}>
<CheckCircle className="w-3 h-3" />
一致观点
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.consensus}
</p>
</div>
<div>
<span className={`text-xs flex items-center gap-1 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`}>
<AlertCircle className="w-3 h-3" />
差异观点
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.differences}
</p>
</div>
<div>
<span className={`text-xs ${isDark ? 'text-blue-400' : 'text-blue-600'}`}>
{getModeLabel(compareResult.leftMode)}优势
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.leftAdvantage}
</p>
</div>
<div>
<span className={`text-xs ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
{getModeLabel(compareResult.rightMode)}优势
</span>
<p className={`text-xs mt-1 ${isDark ? 'text-slate-300' : 'text-gray-600'}`}>
{compareResult.differences.rightAdvantage}
</p>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default GeneralQA;