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

417 lines
15 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react';
import {
clearTemplateOverride,
clone,
hasTemplateOverride,
loadOriginalTemplate,
loadTemplateByMeta,
loadTemplateIndex,
saveTemplateOverride,
} from '@/config/templateStore';
import './ScenarioConfigPage.css';
const COORDINATOR_FIELDS = [
['system_prompt_prefix', '协调者全局规则'],
['round1_guidance', '首轮策略'],
['middle_round_guidance', '中间轮策略'],
['final_round_guidance', '末轮策略'],
['speaker_selection_rules', '选人规则'],
['focus_instruction_rules', '任务指令规则'],
['dependency_driving_rules', '依赖推进规则'],
['consistency_check_rules', '一致性/冲突检查规则'],
['revise_prompt_prefix', '人工修改解释规则'],
];
function updateAtIndex(items, index, updater) {
return items.map((item, current) => current === index ? updater(clone(item)) : item);
}
function linesToList(value) {
return value
.split('\n')
.map(item => item.trim())
.filter(Boolean);
}
export default function ScenarioConfigPage({ onBack }) {
const [templates, setTemplates] = useState([]);
const [selectedId, setSelectedId] = useState('');
const [draft, setDraft] = useState(null);
const [rawJson, setRawJson] = useState('');
const [activeAgentIndex, setActiveAgentIndex] = useState(0);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const selectedMeta = useMemo(
() => templates.find(item => item.template_id === selectedId),
[templates, selectedId],
);
const fieldOptions = useMemo(() => {
const schema = draft?.report_schema || {};
const requirements = draft?.field_requirements || {};
return Object.keys(schema).map(field => ({
key: field,
title: requirements?.[field]?.title || field,
description: schema[field] || '',
}));
}, [draft]);
const loadTemplates = async () => {
setLoading(true);
setError('');
try {
const list = await loadTemplateIndex();
setTemplates(list);
if (!selectedId && list[0]) {
setSelectedId(list[0].template_id);
}
} catch (err) {
setError(err.message || '模板列表加载失败');
} finally {
setLoading(false);
}
};
const loadTemplate = async (templateId) => {
if (!templateId) return;
const meta = templates.find(item => item.template_id === templateId) || templateId;
setLoading(true);
setError('');
setMessage('');
try {
const template = await loadTemplateByMeta(meta);
setDraft(template);
setRawJson(JSON.stringify(template, null, 2));
setActiveAgentIndex(0);
} catch (err) {
setError(err.message || '模板加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadTemplates();
}, []);
useEffect(() => {
if (selectedId && templates.length > 0) {
loadTemplate(selectedId);
}
}, [selectedId, templates.length]);
const syncDraft = (updater) => {
setDraft(prev => {
const next = clone(prev);
updater(next);
setRawJson(JSON.stringify(next, null, 2));
return next;
});
setMessage('');
};
const setCoordinatorField = (key, value) => {
syncDraft(next => {
next.coordinator_config = {
...(next.coordinator_config || {}),
[key]: value,
};
});
};
const setAgentField = (index, key, value) => {
syncDraft(next => {
next.agents = updateAtIndex(next.agents || [], index, agent => ({
...agent,
[key]: value,
}));
});
};
const applyRawJson = () => {
setError('');
setMessage('');
try {
const parsed = JSON.parse(rawJson);
if (!parsed.template_id) throw new Error('JSON 中缺少 template_id');
setDraft(parsed);
setSelectedId(parsed.template_id);
setMessage('完整 JSON 已应用,保存后生效。');
} catch (err) {
setError(err.message || 'JSON 解析失败');
}
};
const saveDraft = () => {
if (!draft?.template_id) return;
try {
saveTemplateOverride(draft.template_id, draft);
setMessage('已保存到浏览器本地覆盖。返回首页选择该模板即可使用修改后的提示。');
setError('');
} catch (err) {
setError(err.message || '保存失败');
}
};
const resetTemplate = async () => {
if (!selectedId) return;
setLoading(true);
setMessage('');
setError('');
try {
clearTemplateOverride(selectedId);
const original = await loadOriginalTemplate(selectedMeta || selectedId);
setDraft(original);
setRawJson(JSON.stringify(original, null, 2));
setMessage('已恢复为静态模板原始内容。');
} catch (err) {
setError(err.message || '恢复失败');
} finally {
setLoading(false);
}
};
const activeAgent = draft?.agents?.[activeAgentIndex];
const activeRelevantFields = new Set(activeAgent?.relevant_fields || []);
return (
<div className="scenario-page">
<header className="scenario-header">
<div>
<div className="scenario-kicker">场景配置</div>
<h1>模板提示资产</h1>
<p>查看和修改场景模板对应的协调者提示、推荐智能体配置及智能体提示。新增模板暂不支持。</p>
</div>
<div className="scenario-header-actions">
<button type="button" className="btn-secondary" onClick={onBack}>返回首页</button>
</div>
</header>
<section className="scenario-toolbar">
<select
className="scenario-select"
value={selectedId}
onChange={event => setSelectedId(event.target.value)}
disabled={loading}
>
<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" disabled>新增模板(暂不支持)</button>
<button type="button" className="btn-secondary" onClick={loadTemplates} disabled={loading}>
刷新
</button>
<button type="button" className="btn-secondary" onClick={resetTemplate} disabled={!draft || loading}>
恢复原始模板
</button>
<button type="button" className="btn-primary" onClick={saveDraft} disabled={!draft || loading}>
保存修改
</button>
</section>
{message && <div className="scenario-message">{message}</div>}
{error && <div className="scenario-error">{error}</div>}
{!draft ? (
<div className="scenario-empty">请选择一个模板。</div>
) : (
<main className="scenario-layout">
<section className="scenario-card scenario-template-card">
<div className="scenario-card-title">模板信息</div>
<div className="scenario-grid-2">
<label>
模板 ID
<input className="scenario-input" value={draft.template_id || ''} readOnly />
</label>
<label>
模板名称
<input
className="scenario-input"
value={draft.template_name || ''}
onChange={event => syncDraft(next => { next.template_name = event.target.value; })}
/>
</label>
</div>
<label>
模板说明
<textarea
className="scenario-textarea scenario-textarea-sm"
value={draft.template_brief || ''}
onChange={event => syncDraft(next => { next.template_brief = event.target.value; })}
/>
</label>
<label>
一致性模式
<select
className="scenario-select scenario-select-inline"
value={draft.consistency_mode || 'free'}
onChange={event => syncDraft(next => { next.consistency_mode = event.target.value; })}
>
<option value="free">free</option>
<option value="soft">soft</option>
</select>
<div className="scenario-inline-hint">
`free` 适合开放式模板;`soft` 会要求后续字段尽量承接前文依赖和命名。
</div>
</label>
{draft.usage_notes && (
<label>
使用说明
<textarea
className="scenario-textarea"
value={draft.usage_notes || ''}
onChange={event => syncDraft(next => { next.usage_notes = event.target.value; })}
/>
</label>
)}
</section>
<section className="scenario-card">
<div className="scenario-card-title">协调者提示</div>
<div className="scenario-coordinator-grid">
{COORDINATOR_FIELDS.map(([key, label]) => (
<label key={key}>
{label}
<span className="scenario-field-key">{key}</span>
<textarea
className="scenario-textarea"
value={draft.coordinator_config?.[key] || ''}
onChange={event => setCoordinatorField(key, event.target.value)}
/>
</label>
))}
</div>
</section>
<section className="scenario-card">
<div className="scenario-card-head">
<div>
<div className="scenario-card-title">推荐智能体及提示</div>
<div className="scenario-card-desc">当前支持修改已有智能体,不支持新增或删除。</div>
</div>
<button type="button" className="btn-primary" disabled>新增智能体(暂不支持)</button>
</div>
<div className="scenario-agent-layout">
<div className="scenario-agent-list">
{(draft.agents || []).map((agent, index) => (
<button
type="button"
key={agent.id || index}
className={`scenario-agent-tab ${index === activeAgentIndex ? 'active' : ''}`}
onClick={() => setActiveAgentIndex(index)}
>
<span>{agent.name || agent.id || `agent-${index + 1}`}</span>
<code>{agent.id}</code>
</button>
))}
</div>
{activeAgent && (
<div className="scenario-agent-editor">
<div className="scenario-grid-2">
<label>
ID
<input className="scenario-input" value={activeAgent.id || ''} readOnly />
</label>
<label>
名称
<input
className="scenario-input"
value={activeAgent.name || ''}
onChange={event => setAgentField(activeAgentIndex, 'name', event.target.value)}
/>
</label>
</div>
<label>
分析维度
<input
className="scenario-input"
value={activeAgent.dimension || ''}
onChange={event => setAgentField(activeAgentIndex, 'dimension', event.target.value)}
/>
</label>
<label>
智能体提示 / 专家角色
<textarea
className="scenario-textarea scenario-textarea-lg"
value={activeAgent.expertise || ''}
onChange={event => setAgentField(activeAgentIndex, 'expertise', event.target.value)}
/>
</label>
<label>
分析契约(每行一条)
<textarea
className="scenario-textarea"
value={(activeAgent.analysis_contract || []).join('\n')}
onChange={event => setAgentField(activeAgentIndex, 'analysis_contract', linesToList(event.target.value))}
/>
</label>
<label>
关联字段
<div className="scenario-field-picker">
{fieldOptions.map(option => (
<label key={option.key} className={`scenario-field-option ${activeRelevantFields.has(option.key) ? 'selected' : ''}`}>
<input
type="checkbox"
checked={activeRelevantFields.has(option.key)}
onChange={event => {
const current = activeAgent.relevant_fields || [];
const next = event.target.checked
? [...current, option.key]
: current.filter(item => item !== option.key);
setAgentField(activeAgentIndex, 'relevant_fields', next);
}}
/>
<div className="scenario-field-option-body">
<div className="scenario-field-option-title">{option.title}</div>
<div className="scenario-field-option-key">{option.key}</div>
{option.description && (
<div className="scenario-field-option-desc">{option.description}</div>
)}
</div>
</label>
))}
</div>
</label>
<label>
检索补充说明
<textarea
className="scenario-textarea scenario-textarea-sm"
value={activeAgent.retrieval_instruction || ''}
onChange={event => setAgentField(activeAgentIndex, 'retrieval_instruction', event.target.value)}
/>
</label>
</div>
)}
</div>
</section>
<section className="scenario-card">
<div className="scenario-card-head">
<div>
<div className="scenario-card-title">完整模板 JSON</div>
<div className="scenario-card-desc">用于修改页面暂未拆出的提示资产,例如字段要求、样例和报告结构。</div>
</div>
<button type="button" className="btn-secondary" onClick={applyRawJson}>应用 JSON</button>
</div>
<textarea
className="scenario-json"
value={rawJson}
onChange={event => setRawJson(event.target.value)}
spellCheck={false}
/>
</section>
</main>
)}
</div>
);
}