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