985 lines
44 KiB
TypeScript
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;
|