Files
deerflow-code/web-code/components/qa/GeneralQAConfigSidebar.tsx
T
2026-05-27 19:43:15 +08:00

262 lines
15 KiB
TypeScript

import { BookOpen, Bot, Brain, ChevronLeft, ChevronRight, Sparkles, User2Icon } from 'lucide-react';
import type { AnswerStyle, CompareLayout, KnowledgeScope, QAMode, qaModeItem, ViewMode } from './GeneralQA.types';
/**
* 左侧配置栏 Props。
*
* 组件内不保存业务状态,所有选中值和 setter 都来自父组件。
* 这样父组件可以统一处理“生成中切换需要确认停止”的逻辑,配置栏只负责触发用户意图。
*/
type Props = {
isDark: boolean; // 当前是否为暗黑主题,用于样式适配
viewMode: ViewMode; // 当前视图模式:'single' 单一模式 或 'compare' 对比模式
qaMode: QAMode; // 单一模式下的问答模式:'single-round' 单轮RAG / 'multi-round' 多轮RAG / 'deep-search' 深度检索
compareLeftMode: QAMode; // 对比模式下左侧的问答模式
compareRightMode: QAMode; // 对比模式下右侧的问答模式
compareLayout: CompareLayout; // 对比模式的布局方式:'horizontal' 水平 或 'vertical' 垂直
knowledgeScope: KnowledgeScope; // 知识范围设置:'all' 全部 / 'specific-kb' 指定知识库 / 'specific-domain' 指定领域
answerStyle: AnswerStyle; // 回答风格偏好:'concise' 简洁 或 'detailed' 详细
showSources: boolean; // 是否在回答中展示引用来源
showProcess: boolean; // 是否展示模型的思考过程
autoSummarizeDiff: boolean; // 对比模式下是否自动总结左右差异
recommendBetter: boolean; // 对比模式下是否推荐更好的回答方案
isCollapsed: boolean; // 左侧配置面板是否折叠
qaModeOptions?: qaModeItem[];
setQaMode: (mode: QAMode) => void; // 设置单一模式的问答模式
setCompareLeftMode: (mode: QAMode) => void; // 设置对比模式左侧的模式
setCompareRightMode: (mode: QAMode) => void; // 设置对比模式右侧的模式
setCompareLayout: (layout: CompareLayout) => void; // 设置对比布局方式
setKnowledgeScope: (scope: KnowledgeScope) => void; // 设置知识范围
setAnswerStyle: (style: AnswerStyle) => void; // 设置回答风格
setShowSources: (show: boolean) => void; // 设置是否展示引用来源
setShowProcess: (show: boolean) => void; // 设置是否展示思考过程
setAutoSummarizeDiff: (enabled: boolean) => void; // 设置是否自动总结差异
setRecommendBetter: (enabled: boolean) => void; // 设置是否推荐更好的方案
setIsCollapsed: (collapsed: boolean) => void; // 设置配置面板折叠状态
};
/**
* 单一模式下可选的问答模式。
*
* 每个选项同时提供展示文案、图标和简短说明,便于配置栏用统一结构渲染卡片。
*/
const qaModeList: qaModeItem[] = [
{ value: 'single-chat' as const, label: '快速', icon: Bot, desc: '快速直接' },
{ value: 'single-round' as const, label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
{ value: 'multi-round' as const, label: '多轮RAG', icon: Brain, desc: '深入理解' },
{ value: 'deep-search' as const, label: '任务检索', icon: BookOpen, desc: '检索相关任务' },
{ value: 'hx-search' as const, label: '画像检索', icon: User2Icon, desc: '检索相关账号' },
{ value: 'news-report' as const, label: '新闻报告生成', icon: User2Icon, desc: '检索相关新闻' },
];
/**
* 知识范围选项。
*
* 当前只影响 UI 配置状态,后续接入真实知识库筛选时可以直接把该值映射到请求参数。
*/
const knowledgeScopeOptions = [
{ value: 'all' as const, label: '全部知识源' },
{ value: 'specific-kb' as const, label: '指定知识库' },
{ value: 'specific-domain' as const, label: '指定资源域' },
];
/**
* 左侧问答配置栏。
*
* 该组件集中承载页面的“模式选择、知识范围、回答偏好、对比设置”等表单控件。
* 父组件仍是唯一状态源,配置栏通过 setter 把用户选择同步回父组件;这样不会改变原有请求逻辑,
* 同时把 300 多行配置 JSX 从 `GeneralQA.tsx` 中移出,便于后续维护表单项和样式。
*/
export default function GeneralQAConfigSidebar({
isDark,
viewMode,
qaMode,
compareLeftMode,
compareRightMode,
compareLayout,
knowledgeScope,
answerStyle,
showSources,
showProcess,
autoSummarizeDiff,
recommendBetter,
isCollapsed,
qaModeOptions = qaModeList,
setQaMode,
setCompareLeftMode,
setCompareRightMode,
setCompareLayout,
setKnowledgeScope,
setAnswerStyle,
setShowSources,
setShowProcess,
setAutoSummarizeDiff,
setRecommendBetter,
setIsCollapsed,
}: Props): JSX.Element {
/**
* 自定义勾选图标。
*
* 原生 checkbox 被隐藏后,所有勾选项都通过这个 SVG 保持一致的视觉风格。
*/
const renderCheckIcon = () => (
<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>
);
return (
<div className={`${isCollapsed ? 'w-14 p-2' : 'w-72 p-4'} border-r flex-shrink-0 overflow-y-auto transition-all duration-300 ${
isDark ? 'border-slate-700/50 bg-slate-800/30' : 'border-red-100 bg-gray-50/50'
}`}>
<div className={`flex items-center ${isCollapsed ? 'justify-center' : 'justify-between'} mb-4`}>
{!isCollapsed && (
<span className={`text-xs font-medium uppercase tracking-wider ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>
问答配置
</span>
)}
<button
type="button"
onClick={() => setIsCollapsed(!isCollapsed)}
className={`p-2 rounded-lg transition-all ${
isDark
? 'text-slate-400 hover:text-white hover:bg-slate-700/50'
: 'text-gray-500 hover:text-red-600 hover:bg-red-50'
}`}
title={isCollapsed ? '展开侧边栏' : '收起侧边栏'}
>
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronLeft className="w-4 h-4" />}
</button>
</div>
{!isCollapsed && (
<>
{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">
{qaModeOptions.map((mode) => {
const Icon = mode.icon;
return (
<button
key={mode.value}
onClick={() => setQaMode(mode.value)}
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'
}`}
>
<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'}`}>
{
qaModeOptions.filter(item => item.value !== compareRightMode).map(item => (
<option value={item.value} key={item.value}>{item.label}</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'}`}>
{
qaModeOptions.filter(item => item.value !== compareLeftMode).map(item => (
<option value={item.value} key={item.value}>{item.label}</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">
{(['horizontal', 'vertical'] as const).map((layout) => (
<button key={layout} onClick={() => setCompareLayout(layout)} className={`flex-1 px-3 py-2 rounded-lg text-sm transition-all ${compareLayout === layout ? 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'}`}>
{layout === 'horizontal' ? '左右对比' : '上下对比'}
</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">
{knowledgeScopeOptions.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)} 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' && renderCheckIcon()}</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 && renderCheckIcon()}</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 && renderCheckIcon()}</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 && renderCheckIcon()}</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 && renderCheckIcon()}</div>
<span className="text-sm">推荐更优答案</span>
</label>
</div>
</div>
)} */}
</>
)}
</div>
);
}