262 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|