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

420 lines
16 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import { DEFAULT_BACKEND_URL, DEFAULT_CONFIG } from '@/config/defaultConfig';
import { clone, hasTemplateOverride, loadTemplateByMeta, loadTemplateIndex } from '@/config/templateStore';
import './ConfigPanel.css';
import { useChatContext } from '@/contexts/ChatContext';
function Card({ title, description, children }) {
return (
<section className="config-card">
<div className="config-card-header">
<h3>{title}</h3>
{description && <p>{description}</p>}
</div>
{children}
</section>
);
}
function ReadOnlyBlock({ title, children }) {
return (
<div className="config-readonly-block">
<div className="config-readonly-title">{title}</div>
<div className="config-readonly-content">{children}</div>
</div>
);
}
export default function ConfigPanel({ onStart, isRunning, onStop, onOpenScenarioConfig }) {
const { startConfig } = useChatContext();
const [taskContext, setTaskContext] = useState(startConfig?.task_context || '');
const backendUrl = DEFAULT_BACKEND_URL;
const [config, setConfig] = useState(() => clone(DEFAULT_CONFIG));
const [rawOpen, setRawOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
const [templates, setTemplates] = useState([]);
const [templateLoading, setTemplateLoading] = useState(false);
const [templateError, setTemplateError] = useState('');
const [llmPresets, setLlmPresets] = useState([]);
const [llmLoading, setLlmLoading] = useState(false);
const [llmError, setLlmError] = useState('');
const updateConfig = (updater) => {
setConfig(prev => {
const next = clone(prev);
updater(next);
return next;
});
};
const setCoordinatorMode = (mode) => {
updateConfig(next => {
next.coordinator_mode = mode;
});
};
const setDivergeRounds = (value) => {
const parsed = Number.parseInt(value, 10);
const rounds = Number.isFinite(parsed) ? Math.min(8, Math.max(1, parsed)) : 3;
updateConfig(next => {
next.phase_config = {
...(next.phase_config || {}),
diverge_rounds: rounds,
};
});
};
const setLlmPreset = (presetId) => {
updateConfig(next => {
next.llm_preset_id = presetId;
});
};
const setConsistencyMode = (mode) => {
updateConfig(next => {
next.consistency_mode = mode;
});
};
const handleSubmit = (e) => {
e.preventDefault();
if (!taskContext.trim() || !config.template_id) return;
console.log('------------------点击了')
onStart({
...config,
task_context: taskContext.trim(),
coordinator_mode: config.coordinator_mode || 'auto',
}, backendUrl);
};
const loadTemplates = async () => {
setTemplateLoading(true);
setTemplateError('');
try {
setTemplates(await loadTemplateIndex());
} catch (error) {
setTemplateError(error.message || '模板列表加载失败');
} finally {
setTemplateLoading(false);
}
};
const loadConfigOptions = async () => {
setLlmLoading(true);
setLlmError('');
try {
const response = await fetch(`${backendUrl}/config/options`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
const presets = data.llm_presets || [];
setLlmPresets(presets);
updateConfig(next => {
if (!next.llm_preset_id) {
next.llm_preset_id = data.default_llm_preset_id || presets.find(item => item.is_default)?.id || '';
}
});
} catch (error) {
setLlmError(error.message || '模型配置加载失败');
} finally {
setLlmLoading(false);
}
};
const applyTemplate = async (templateId) => {
if (!templateId) return;
const meta = templates.find(item => item.template_id === templateId);
setTemplateLoading(true);
setTemplateError('');
try {
const template = await loadTemplateByMeta(meta || templateId);
updateConfig(next => {
next.template_id = template.template_id || templateId;
next.template_name = template.template_name || '';
next.template_brief = template.template_brief || '';
next.consistency_mode = template.consistency_mode || next.consistency_mode || 'free';
next.report_schema = clone(template.report_schema || {});
next.report_examples = clone(template.report_examples || {});
next.field_requirements = clone(template.field_requirements || {});
next.agents = clone(template.agents || next.agents || []);
next.coordinator_mode = next.coordinator_mode || 'auto';
next.coordinator_config = clone(template.coordinator_config || DEFAULT_CONFIG.coordinator_config);
next.agent_adaptation_policy = clone(template.agent_adaptation_policy || {});
});
} catch (error) {
setTemplateError(error.message || '模板加载失败');
} finally {
setTemplateLoading(false);
}
};
useEffect(() => {
if (!backendUrl) return;
loadTemplates();
loadConfigOptions();
}, [backendUrl]);
useEffect(() => {
if(startConfig) {
setConfig((pev) => ({
...pev,
template_id: startConfig.template_id,
}))
}
}, []);
const selectedLlmPreset = useMemo(
() => llmPresets.find(item => item.id === config.llm_preset_id) || null,
[llmPresets, config.llm_preset_id]
);
const rawConfig = useMemo(() => JSON.stringify({ ...config, task_context: '<在上方输入>' }, null, 2), [config]);
const canStart = taskContext.trim() && config.template_id;
return (
<div className="config-panel">
<form onSubmit={handleSubmit} className="config-layout">
<div className="config-task-section">
<label htmlFor="task-context">任务背景</label>
<textarea
id="task-context"
className="config-task-textarea"
value={taskContext}
onChange={e => setTaskContext(e.target.value)}
placeholder="请描述事件背景、传播目标、限制条件和你关心的判断标准。"
disabled={isRunning}
/>
<div className="config-run-mode-row">
<div className="config-mode-switch" role="radiogroup" aria-label="协调者模式">
<button
type="button"
className={`config-mode-option ${(config.coordinator_mode || 'auto') === 'auto' ? 'active' : ''}`}
onClick={() => setCoordinatorMode('auto')}
disabled={isRunning}
>
自动完成
</button>
<button
type="button"
className={`config-mode-option ${config.coordinator_mode === 'human_review' ? 'active' : ''}`}
onClick={() => setCoordinatorMode('human_review')}
disabled={isRunning}
>
人工逐轮确认
</button>
</div>
<div className="config-field-hint config-mode-hint">
自动完成会由协调者连续推进;人工逐轮确认会在每轮协调者提案后等待确认或修改。
</div>
<button
type="button"
className="btn-secondary config-scenario-button"
onClick={onOpenScenarioConfig}
disabled={isRunning}
>
场景配置
</button>
</div>
<div className="config-run-options-row">
<label htmlFor="diverge-rounds">讨论轮次</label>
<input
id="diverge-rounds"
className="config-input config-number-input"
type="number"
min="1"
max="8"
step="1"
value={config.phase_config?.diverge_rounds || 3}
onChange={e => setDivergeRounds(e.target.value)}
disabled={isRunning}
/>
<div className="config-field-hint">
控制协调者推进的中间讨论轮数;轮次越多,材料更充分但耗时更长。
</div>
</div>
<div className="config-run-options-row">
<label htmlFor="llm-preset">运行模型</label>
<select
id="llm-preset"
className="config-input"
value={config.llm_preset_id || ''}
onChange={e => setLlmPreset(e.target.value)}
disabled={isRunning || llmLoading || llmPresets.length === 0}
>
<option value="">选择模型...</option>
{llmPresets.map(preset => (
<option key={preset.id} value={preset.id}>
{preset.label}
{preset.is_default ? '(默认)' : ''}
</option>
))}
</select>
<div className="config-field-hint">
{selectedLlmPreset
? `${selectedLlmPreset.description || '后端预设模型'} · ${selectedLlmPreset.model_name}`
: '模型预设由后端 config.py 管理,前端只选择预设。'}
</div>
</div>
<div className="config-run-options-row">
<label htmlFor="consistency-mode">一致性模式</label>
<select
id="consistency-mode"
className="config-input"
value={config.consistency_mode || 'free'}
onChange={e => setConsistencyMode(e.target.value)}
disabled={isRunning}
>
<option value="free">free</option>
<option value="soft">soft</option>
</select>
<div className="config-field-hint">
{config.consistency_mode === 'soft'
? 'soft 会优先承接依赖字段、统一命名口径,并在最终成文时增强跨模块衔接。'
: 'free 只做基础一致性控制,适合依赖关系较弱或开放式的场景模板。'}
</div>
</div>
<div className="config-submit-row">
<div className="config-fixed-mode">报告结构、智能体和协调者策略由所选模板固定。</div>
{isRunning ? (
<button type="button" className="btn-danger" onClick={onStop}>停止分析</button>
) : (
<button type="submit" className="btn-start" disabled={!canStart}>
{config.coordinator_mode === 'human_review' ? '开始并逐轮确认' : '开始分析'}
</button>
)}
</div>
{!config.template_id && (
<div className="config-field-hint">开始前需要先选择一个报告模板。</div>
)}
{llmError && (
<div className="human-review-error">模型配置加载失败:{llmError}</div>
)}
</div>
<Card title="报告模板" description="选择后会自动固定报告字段、字段标准、参与智能体和协调者策略。">
<div className="config-submit-row">
<select
className="config-input"
value={config.template_id || ''}
onChange={e => applyTemplate(e.target.value)}
disabled={isRunning || templateLoading}
>
<option value="">选择模板...</option>
{templates.map(template => (
<option key={template.template_id} value={template.template_id}>
{template.template_name || template.template_id}
{hasTemplateOverride(template.template_id) ? '(已修改)' : ''}
</option>
))}
</select>
<button
type="button"
className="btn-primary"
onClick={loadTemplates}
disabled={isRunning || templateLoading}
>
{templateLoading ? '加载中' : '刷新模板'}
</button>
</div>
{config.template_name && (
<div className="config-field-hint">
当前模板:{config.template_name}
{config.template_brief ? ` · ${config.template_brief}` : ''}
</div>
)}
{templateError && (
<div className="human-review-error">模板加载失败:{templateError}</div>
)}
</Card>
<div className="config-field-hint">
运行模型预设由后端 `config.py` 管理;前端只选择预设 id,不接触密钥。
</div>
<div className="config-advanced-section">
<button
type="button"
className="config-advanced-toggle"
onClick={() => setDetailOpen(open => !open)}
>
<span>模板详情(只读)</span>
<span className={`config-advanced-chevron ${detailOpen ? 'open' : ''}`}>▾</span>
</button>
{detailOpen && (
<div className="config-advanced-body">
{!config.template_id ? (
<div className="config-empty-detail">选择模板后可查看字段、智能体和协调者策略。</div>
) : (
<>
<ReadOnlyBlock title="报告字段">
<div className="config-report-grid readonly">
{Object.entries(config.report_schema || {}).map(([field, desc]) => (
<div key={field} className="config-report-card">
<div className="config-agent-head">
<span>{field}</span>
</div>
<p>{desc}</p>
{config.field_requirements?.[field] && (
<pre>{JSON.stringify(config.field_requirements[field], null, 2)}</pre>
)}
</div>
))}
</div>
</ReadOnlyBlock>
<ReadOnlyBlock title="参与智能体">
<div className="config-agents readonly">
{(config.agents || []).map(agent => (
<div key={agent.id} className="config-agent-card">
<div className="config-agent-head">
<span>{agent.name}</span>
<code>{agent.id}</code>
</div>
<p className="config-readonly-meta">{agent.dimension}</p>
<p>{agent.expertise || agent.description || agent.skill}</p>
{(agent.relevant_fields || []).length > 0 && (
<p className="config-readonly-meta">关联字段:{agent.relevant_fields.join('、')}</p>
)}
</div>
))}
</div>
</ReadOnlyBlock>
<ReadOnlyBlock title="协调者策略">
<pre>{JSON.stringify(config.coordinator_config || {}, null, 2)}</pre>
</ReadOnlyBlock>
</>
)}
</div>
)}
</div>
<div className="config-advanced-section">
<button
type="button"
className="config-advanced-toggle"
onClick={() => setRawOpen(open => !open)}
>
<span>原始配置预览</span>
<span className={`config-advanced-chevron ${rawOpen ? 'open' : ''}`}>▾</span>
</button>
{rawOpen && (
<div className="config-advanced-body">
<textarea className="config-json-editor" value={rawConfig} readOnly spellCheck={false} />
</div>
)}
</div>
</form>
</div>
);
}