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

639 lines
28 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useTheme } from '../../contexts/ThemeContext';
import {
Edit2, Plus, Trash2, Check, X, ChevronDown, ChevronRight,
Target, Users, Activity, BookOpen, Zap, AlertTriangle,
Settings, FileText, Info, RefreshCw, AlertCircle, Brain, Save,
} from 'lucide-react';
interface FieldRequirement {
title: string;
goal: string;
must_cover: string[];
done_criteria: string[];
blockers: string[];
dependencies: string[];
evidence_needs: string[];
preferred_agents: string[];
style_notes: string;
min_chars?: number;
min_items?: number;
}
interface AgentData {
id: string;
name: string;
dimension: string;
expertise: string;
relevant_fields: string[];
analysis_contract: string[];
}
interface CoordinatorConfig {
system_prompt_prefix?: string;
round1_guidance?: string;
middle_round_guidance?: string;
final_round_guidance?: string;
speaker_selection_rules?: string;
focus_instruction_rules?: string;
dependency_driving_rules?: string[];
consistency_check_rules?: string[];
revise_prompt_prefix?: string;
}
interface TemplateData {
template_id: string;
template_name: string;
template_brief: string;
usage_notes: string[];
report_schema: Record<string, string>;
report_examples: Record<string, string>;
field_requirements: Record<string, FieldRequirement>;
agents: AgentData[];
coordinator_config: CoordinatorConfig;
}
const FIELD_LABELS: Record<string, string> = {
template_id: '模板ID',
template_name: '模板名称',
template_brief: '模板简介',
usage_notes: '使用说明',
report_schema: '报告字段结构',
report_examples: '报告字段示例',
field_requirements: '字段要求配置',
agents: '智能体配置',
coordinator_config: '协调者配置',
title: '字段标题',
goal: '字段目标',
must_cover: '必须覆盖内容',
done_criteria: '完成标准',
blockers: '阻塞条件',
dependencies: '依赖字段',
evidence_needs: '证据需求',
preferred_agents: '推荐智能体',
style_notes: '风格说明',
min_chars: '最小字符数',
min_items: '最小条目数',
id: '智能体ID',
name: '智能体名称',
dimension: '分析维度',
expertise: '专业能力',
relevant_fields: '关联字段',
analysis_contract: '分析契约',
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: '修改提示前缀',
};
const REPORT_SCHEMA_LABELS: Record<string, string> = {
objective_analysis: '一、目的分析',
target_actor_analysis: '二、目标行为体分析',
expected_behavior_drivers: '三、预期行为及行为驱动因素分析',
narrative_theme_design: '四、叙事主题设计',
action_plan_list: '五、认知行动方案列表',
};
interface AgentVisual {
Icon: React.ElementType;
lightCard: string;
darkCard: string;
iconLight: string;
iconDark: string;
tagLight: string;
tagDark: string;
}
const AGENT_VISUALS: Record<string, AgentVisual> = {
objective: {
Icon: Target,
lightCard: 'from-blue-50 to-sky-50 border-blue-200 hover:border-blue-400 hover:shadow-blue-200/60',
darkCard: 'from-slate-900/80 to-blue-950/20 border-slate-700/70 hover:border-blue-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-blue-100 text-blue-600',
iconDark: 'bg-slate-800/90 text-blue-300 ring-1 ring-blue-500/20',
tagLight: 'bg-blue-100 text-blue-700 border-blue-200',
tagDark: 'bg-slate-800/70 text-blue-200 border-blue-500/25',
},
actor: {
Icon: Users,
lightCard: 'from-emerald-50 to-teal-50 border-emerald-200 hover:border-emerald-400 hover:shadow-emerald-200/60',
darkCard: 'from-slate-900/80 to-emerald-950/20 border-slate-700/70 hover:border-emerald-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-emerald-100 text-emerald-600',
iconDark: 'bg-slate-800/90 text-emerald-300 ring-1 ring-emerald-500/20',
tagLight: 'bg-emerald-100 text-emerald-700 border-emerald-200',
tagDark: 'bg-slate-800/70 text-emerald-200 border-emerald-500/25',
},
behavior: {
Icon: Activity,
lightCard: 'from-amber-50 to-orange-50 border-amber-200 hover:border-amber-400 hover:shadow-amber-200/60',
darkCard: 'from-slate-900/80 to-amber-950/20 border-slate-700/70 hover:border-amber-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-amber-100 text-amber-600',
iconDark: 'bg-slate-800/90 text-amber-300 ring-1 ring-amber-500/20',
tagLight: 'bg-amber-100 text-amber-700 border-amber-200',
tagDark: 'bg-slate-800/70 text-amber-200 border-amber-500/25',
},
narrative: {
Icon: BookOpen,
lightCard: 'from-purple-50 to-violet-50 border-purple-200 hover:border-purple-400 hover:shadow-purple-200/60',
darkCard: 'from-slate-900/80 to-purple-950/20 border-slate-700/70 hover:border-purple-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-purple-100 text-purple-600',
iconDark: 'bg-slate-800/90 text-purple-300 ring-1 ring-purple-500/20',
tagLight: 'bg-purple-100 text-purple-700 border-purple-200',
tagDark: 'bg-slate-800/70 text-purple-200 border-purple-500/25',
},
action: {
Icon: Zap,
lightCard: 'from-indigo-50 to-slate-50 border-indigo-200 hover:border-indigo-400 hover:shadow-indigo-200/60',
darkCard: 'from-slate-900/80 to-indigo-950/20 border-slate-700/70 hover:border-indigo-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-indigo-100 text-indigo-600',
iconDark: 'bg-slate-800/90 text-indigo-300 ring-1 ring-indigo-500/20',
tagLight: 'bg-indigo-100 text-indigo-700 border-indigo-200',
tagDark: 'bg-slate-800/70 text-indigo-200 border-indigo-500/25',
},
risk: {
Icon: AlertTriangle,
lightCard: 'from-rose-50 to-pink-50 border-rose-200 hover:border-rose-400 hover:shadow-rose-200/60',
darkCard: 'from-slate-900/80 to-rose-950/20 border-slate-700/70 hover:border-rose-500/40 hover:shadow-slate-950/50',
iconLight: 'bg-rose-100 text-rose-600',
iconDark: 'bg-slate-800/90 text-rose-300 ring-1 ring-rose-500/20',
tagLight: 'bg-rose-100 text-rose-700 border-rose-200',
tagDark: 'bg-slate-800/70 text-rose-200 border-rose-500/25',
},
};
const DEFAULT_VISUAL: AgentVisual = {
Icon: Brain,
lightCard: 'from-gray-50 to-slate-50 border-gray-200 hover:border-gray-400 hover:shadow-gray-200/60',
darkCard: 'from-slate-900/80 to-slate-800/50 border-slate-700/70 hover:border-slate-500/50 hover:shadow-slate-950/50',
iconLight: 'bg-gray-100 text-gray-600',
iconDark: 'bg-slate-800/90 text-slate-300 ring-1 ring-slate-500/20',
tagLight: 'bg-gray-100 text-gray-700 border-gray-200',
tagDark: 'bg-slate-800/70 text-slate-200 border-slate-500/25',
};
const mapFieldLabel = (v: string) => REPORT_SCHEMA_LABELS[v] || v;
function EditableText({
value,
onChange,
isDark,
multiline = false,
}: {
value: string;
onChange: (v: string) => void;
isDark: boolean;
multiline?: boolean;
}) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(value);
const confirm = () => { onChange(draft); setEditing(false); };
const cancel = () => { setDraft(value); setEditing(false); };
const inputCls = isDark
? 'bg-slate-800 border-blue-500/60 text-slate-100 focus:border-blue-400'
: 'bg-white border-blue-300 text-gray-800 focus:border-blue-500';
if (editing) {
return (
<div className="space-y-2">
{multiline ? (
<textarea
autoFocus
value={draft}
onChange={e => setDraft(e.target.value)}
rows={5}
className={`w-full px-3 py-2 text-sm rounded-lg border outline-none resize-y font-mono leading-relaxed transition-colors ${inputCls}`}
/>
) : (
<input
autoFocus
value={draft}
onChange={e => setDraft(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirm(); if (e.key === 'Escape') cancel(); }}
className={`w-full px-3 py-1.5 text-sm rounded-lg border outline-none transition-colors ${inputCls}`}
/>
)}
<div className="flex gap-2">
<button onClick={confirm} className="flex items-center gap-1 px-3 py-1 text-xs rounded-lg bg-green-500 hover:bg-green-400 text-white transition-colors">
<Check className="w-3 h-3" /> 确认
</button>
<button onClick={cancel} className={`flex items-center gap-1 px-3 py-1 text-xs rounded-lg border transition-colors ${isDark ? 'border-slate-600 text-slate-400 hover:text-slate-200' : 'border-gray-300 text-gray-500 hover:text-gray-700'}`}>
<X className="w-3 h-3" /> 取消
</button>
</div>
</div>
);
}
return (
<div className="group/editable flex items-start gap-2 rounded-lg px-2 py-1.5 -mx-2 -my-1.5 transition-all duration-200 hover:bg-black/5 dark:hover:bg-white/5 cursor-pointer" onClick={() => { setDraft(value); setEditing(true); }}>
<span className={`flex-1 text-sm leading-relaxed break-words ${isDark ? 'text-slate-300' : 'text-gray-700'} ${multiline ? 'whitespace-pre-wrap font-mono text-xs' : ''}`}>
{value || <span className={isDark ? 'text-slate-600 italic' : 'text-gray-300 italic'}>(空)</span>}
</span>
<Edit2 className={`flex-shrink-0 w-3.5 h-3.5 mt-0.5 opacity-0 group-hover/editable:opacity-100 transition-all duration-200 ${isDark ? 'text-blue-400' : 'text-blue-500'}`} />
</div>
);
}
function TagList({
tags,
onChange,
isDark,
tagLight = 'bg-sky-50 text-sky-700 border-sky-200',
tagDark = 'bg-slate-800/70 text-sky-200 border-sky-500/25',
}: {
tags: string[];
onChange: (tags: string[]) => void;
isDark: boolean;
tagLight?: string;
tagDark?: string;
}) {
const [editIdx, setEditIdx] = useState<number | null>(null);
const [editVal, setEditVal] = useState('');
const [adding, setAdding] = useState(false);
const [addVal, setAddVal] = useState('');
const tagCls = isDark ? tagDark : tagLight;
const inputCls = isDark
? 'bg-slate-800 border-blue-500/60 text-slate-100 placeholder-slate-500 focus:border-blue-400'
: 'bg-white border-blue-400 text-gray-800 placeholder-gray-400 focus:border-blue-500';
const confirmEdit = () => {
if (editIdx === null) return;
const next = [...tags];
if (editVal.trim()) next[editIdx] = editVal.trim();
else next.splice(editIdx, 1);
onChange(next);
setEditIdx(null);
};
const confirmAdd = () => {
if (addVal.trim()) onChange([...tags, addVal.trim()]);
setAdding(false);
setAddVal('');
};
return (
<div className="flex flex-wrap gap-1.5 items-center">
{tags.map((tag, i) =>
editIdx === i ? (
<span key={i} className="inline-flex items-center gap-1">
<input
autoFocus
value={editVal}
onChange={e => setEditVal(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmEdit(); if (e.key === 'Escape') setEditIdx(null); }}
className={`px-2 py-0.5 text-xs rounded border outline-none w-36 transition-colors ${inputCls}`}
/>
<button onClick={confirmEdit} className="text-green-500 hover:text-green-400 transition-colors"><Check className="w-3 h-3" /></button>
<button onClick={() => setEditIdx(null)} className="text-red-400 hover:text-red-300 transition-colors"><X className="w-3 h-3" /></button>
</span>
) : (
<span key={i} className="group/tag inline-flex items-center gap-1">
<span
onClick={() => { setEditIdx(i); setEditVal(tag); setAdding(false); }}
className={`inline-flex items-center gap-1 px-2.5 py-0.5 text-xs rounded-full border cursor-pointer transition-all duration-200 hover:scale-105 hover:opacity-90 ${tagCls}`}
>
{tag}
<button
onClick={e => { e.stopPropagation(); onChange(tags.filter((_, j) => j !== i)); }}
className="opacity-0 group-hover/tag:opacity-100 transition-opacity hover:text-red-500 ml-0.5"
>
<X className="w-2.5 h-2.5" />
</button>
</span>
</span>
)
)}
{adding ? (
<span className="inline-flex items-center gap-1">
<input
autoFocus
value={addVal}
onChange={e => setAddVal(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') confirmAdd(); if (e.key === 'Escape') { setAdding(false); setAddVal(''); } }}
placeholder="输入内容…"
className={`px-2 py-0.5 text-xs rounded border outline-none w-36 transition-colors ${inputCls}`}
/>
<button onClick={confirmAdd} className="text-green-500 hover:text-green-400 transition-colors"><Check className="w-3 h-3" /></button>
<button onClick={() => { setAdding(false); setAddVal(''); }} className="text-red-400 hover:text-red-300 transition-colors"><X className="w-3 h-3" /></button>
</span>
) : (
<button
onClick={() => { setAdding(true); setEditIdx(null); }}
className={`inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full border border-dashed transition-all duration-200 hover:scale-105 ${isDark ? 'border-slate-600 text-slate-500 hover:border-blue-500 hover:text-blue-400' : 'border-gray-300 text-gray-400 hover:border-blue-400 hover:text-blue-500'}`}
>
<Plus className="w-3 h-3" /> 添加
</button>
)}
</div>
);
}
function AgentCard({
agent,
onUpdate,
onDelete,
isDark,
}: {
agent: AgentData;
onUpdate: (a: AgentData) => void;
onDelete: () => void;
isDark: boolean;
}) {
const [editing, setEditing] = useState(false);
const [hovered, setHovered] = useState(false);
const vis = AGENT_VISUALS[agent.id] ?? DEFAULT_VISUAL;
const { Icon } = vis;
const cardCls = isDark ? vis.darkCard : vis.lightCard;
const iconCls = isDark ? vis.iconDark : vis.iconLight;
const tagCls = isDark ? vis.tagDark : vis.tagLight;
const handleFieldChange = (field: keyof AgentData, value: unknown) => {
onUpdate({ ...agent, [field]: value });
};
return (
<>
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`relative rounded-xl border p-4 bg-gradient-to-br transition-all duration-300 ${cardCls} ${hovered ? 'scale-[1.02] shadow-lg -translate-y-0.5' : 'scale-100 shadow-sm'}`}
>
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 transition-transform duration-300 ${iconCls} ${hovered ? 'scale-110 rotate-3' : ''}`}>
<Icon className="w-5 h-5" />
</div>
<div>
<p className={`font-semibold text-sm ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>{agent.name}</p>
<p className={`text-xs mt-0.5 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{agent.dimension}</p>
</div>
</div>
<div className={`flex items-center gap-1 transition-opacity duration-200 ${hovered ? 'opacity-100' : 'opacity-0'}`}>
<button
onClick={() => setEditing(true)}
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-700 text-slate-400 hover:text-blue-300' : 'hover:bg-white/70 text-gray-400 hover:text-blue-500'}`}
>
<Edit2 className="w-3.5 h-3.5" />
</button>
<button
onClick={onDelete}
className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-700 text-slate-400 hover:text-red-400' : 'hover:bg-white/70 text-gray-400 hover:text-red-500'}`}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
<code className={`inline-block text-xs px-2 py-0.5 rounded-full border mb-2 font-mono ${tagCls}`}>{agent.id}</code>
<div className="mb-3">
<p className={`text-xs leading-relaxed mb-1.5 line-clamp-2 ${isDark ? 'text-slate-400' : 'text-gray-600'}`}>{agent.expertise.slice(0, 80)}</p>
</div>
{agent.relevant_fields.length > 0 && (
<div className="mb-2">
<p className={`text-xs font-medium mb-1.5 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS.relevant_fields}</p>
<div className="flex flex-wrap gap-1">
{agent.relevant_fields.map(f => (
<span key={f} className={`text-xs px-2 py-0.5 rounded-full border ${tagCls}`}>{f}</span>
))}
</div>
</div>
)}
</div>
{editing && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4" onClick={() => setEditing(false)}>
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
<div
className={`relative w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-2xl border shadow-2xl ${isDark ? 'bg-slate-900 border-slate-700' : 'bg-white border-gray-200'}`}
onClick={e => e.stopPropagation()}
>
<div className={`flex items-center justify-between px-6 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
<h2 className={`font-semibold ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>编辑智能体</h2>
<button onClick={() => setEditing(false)} className={`p-1.5 rounded-lg transition-colors ${isDark ? 'hover:bg-slate-800 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}><X className="w-4 h-4" /></button>
</div>
<div className="p-6 space-y-4">
{(['id', 'name', 'dimension'] as const).map(k => (
<div key={k} className="space-y-1">
<label className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS[k]}</label>
<EditableText value={agent[k]} onChange={v => handleFieldChange(k, v)} isDark={isDark} />
</div>
))}
<div className="space-y-1">
<label className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS.expertise}</label>
<EditableText value={agent.expertise} onChange={v => handleFieldChange('expertise', v)} isDark={isDark} multiline />
</div>
<div className="space-y-1.5">
<label className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS.relevant_fields}</label>
<TagList tags={agent.relevant_fields} onChange={v => handleFieldChange('relevant_fields', v)} isDark={isDark} tagLight="bg-blue-50 text-blue-700 border-blue-200" tagDark="bg-slate-800/70 text-blue-200 border-blue-500/25" />
</div>
<div className="space-y-1.5">
<label className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS.analysis_contract}</label>
<TagList tags={agent.analysis_contract} onChange={v => handleFieldChange('analysis_contract', v)} isDark={isDark} tagLight="bg-violet-50 text-violet-700 border-violet-200" tagDark="bg-slate-800/70 text-violet-200 border-violet-500/25" />
</div>
</div>
<div className={`flex justify-end gap-3 px-6 py-4 border-t ${isDark ? 'border-slate-700' : 'border-gray-200'}`}>
<button onClick={() => setEditing(false)} className={`px-4 py-2 text-sm rounded-lg border transition-colors ${isDark ? 'border-slate-600 text-slate-400 hover:text-slate-200' : 'border-gray-300 text-gray-500 hover:text-gray-700'}`}>取消</button>
<button onClick={() => setEditing(false)} className="px-4 py-2 text-sm rounded-lg bg-blue-500 hover:bg-blue-400 text-white transition-colors flex items-center gap-1.5">
<Check className="w-3.5 h-3.5" /> 保存
</button>
</div>
</div>
</div>
)}
</>
);
}
function SectionWrapper({
title,
icon: Icon,
children,
isDark,
defaultOpen = true,
}: {
title: string;
icon: React.ElementType;
children: React.ReactNode;
isDark: boolean;
defaultOpen?: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className={`rounded-xl border overflow-hidden transition-all duration-200 ${isDark ? 'bg-slate-800/40 border-slate-700/60' : 'bg-white border-gray-200 shadow-sm'}`}>
<button
onClick={() => setOpen(o => !o)}
className={`w-full flex items-center justify-between px-5 py-4 transition-colors duration-200 ${isDark ? 'hover:bg-slate-700/30' : 'hover:bg-gray-50'}`}
>
<div className="flex items-center gap-2.5">
<div className={`w-7 h-7 rounded-lg flex items-center justify-center ${isDark ? 'bg-slate-700' : 'bg-red-50'}`}>
<Icon className={`w-4 h-4 ${isDark ? 'text-blue-400' : 'text-red-500'}`} />
</div>
<span className={`font-semibold text-sm ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>{title}</span>
</div>
{open
? <ChevronDown className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />
: <ChevronRight className={`w-4 h-4 ${isDark ? 'text-slate-500' : 'text-gray-400'}`} />}
</button>
<div className={`grid transition-all duration-300 ease-in-out ${open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-70'}`}>
<div className="overflow-hidden">
<div className="px-5 pb-5 space-y-4">{children}</div>
</div>
</div>
</div>
);
}
export default function Template6BEditor() {
const { theme } = useTheme();
const isDark = theme === 'dark';
const [template, setTemplate] = useState<TemplateData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
useEffect(() => {
fetch('https://ch1.b.uat.4.cn/zncmweb/templates/6b_template.json')
.then(r => r.json())
.then((data: TemplateData) => { setTemplate(data); setLoading(false); })
.catch(e => { setError(e.message); setLoading(false); });
}, []);
const update = <K extends keyof TemplateData>(key: K, value: TemplateData[K]) => {
setTemplate(prev => prev ? { ...prev, [key]: value } : null);
setDirty(true);
};
const handleSave = () => {
if (!template) return;
console.log('[Template6BEditor 保存]', JSON.stringify(template, null, 2));
window.alert('配置已保存,详情请查看控制台');
setDirty(false);
};
if (loading) {
return (
<div className={`flex items-center justify-center min-h-64 ${isDark ? 'text-slate-400' : 'text-gray-400'}`}>
<RefreshCw className="w-5 h-5 animate-spin mr-2" />
<span className="text-sm">加载模板中…</span>
</div>
);
}
if (error || !template) {
return (
<div className={`flex items-center gap-2 p-4 rounded-xl ${isDark ? 'bg-red-950/30 text-red-400 border border-red-900/50' : 'bg-red-50 text-red-600 border border-red-200'}`}>
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm">模板加载失败:{error}</span>
</div>
);
}
return (
<div className="h-[calc(100vh-24px)] max-h-[calc(100vh-24px)] flex flex-col max-w-5xl mx-auto px-1 pb-3">
<div className={`flex-shrink-0 flex items-center justify-between gap-4 py-3 border-b ${isDark ? 'border-slate-800' : 'border-gray-200'}`}>
<div>
<h1 className={`text-xl font-bold ${isDark ? 'text-slate-100' : 'text-gray-900'}`}>模板配置编辑器</h1>
<p className={`text-sm mt-0.5 ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>六步行动方案 · {template.template_id}</p>
</div>
<div className="flex items-center gap-3">
{dirty && (
<div className={`flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border ${isDark ? 'bg-amber-900/20 border-amber-700/40 text-amber-300' : 'bg-amber-50 border-amber-200 text-amber-600'}`}>
<Info className="w-3.5 h-3.5" />
已有未保存修改
</div>
)}
<button
onClick={handleSave}
className={`inline-flex items-center gap-2 px-4 py-2 rounded-xl border shadow-sm transition-all duration-200 hover:scale-[1.02] ${isDark ? 'bg-slate-900 border-slate-700 text-slate-100 hover:border-blue-500/40' : 'bg-white border-gray-200 text-gray-800 hover:border-blue-300'}`}
>
<Save className="w-4 h-4" /> 保存
</button>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto py-3 pr-1 space-y-4">
<SectionWrapper title="基本信息" icon={FileText} isDark={isDark}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{(['template_id', 'template_name'] as const).map(k => (
<div key={k} className="space-y-1.5">
<span className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS[k]}</span>
<EditableText value={template[k]} onChange={v => update(k, v)} isDark={isDark} />
</div>
))}
</div>
<div className="space-y-1.5">
<span className={`text-xs font-semibold uppercase tracking-wide ${isDark ? 'text-slate-500' : 'text-gray-400'}`}>{FIELD_LABELS.template_brief}</span>
<EditableText value={template.template_brief} onChange={v => update('template_brief', v)} isDark={isDark} multiline />
</div>
</SectionWrapper>
<SectionWrapper title={FIELD_LABELS.usage_notes} icon={Info} isDark={isDark}>
<TagList
tags={template.usage_notes}
onChange={v => update('usage_notes', v)}
isDark={isDark}
tagLight="bg-amber-50 text-amber-700 border-amber-200"
tagDark="bg-slate-800/70 text-amber-200 border-amber-500/25"
/>
</SectionWrapper>
<SectionWrapper title={FIELD_LABELS.report_schema} icon={Settings} isDark={isDark}>
<div className="space-y-3">
{Object.entries(template.report_schema).map(([key, desc]) => (
<div key={key} className={`group/row rounded-lg border px-4 py-3 transition-all duration-200 hover:scale-[1.005] ${isDark ? 'border-slate-700/50 bg-slate-800/30 hover:border-slate-600' : 'border-gray-200 bg-gray-50/50 hover:border-gray-300 hover:shadow-sm'}`}>
<div className="flex items-center gap-2 mb-1.5">
<code className={`text-xs px-2 py-0.5 rounded font-mono ${isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-500'}`}>{key}</code>
<span className={`text-sm font-medium ${isDark ? 'text-slate-200' : 'text-gray-700'}`}>{REPORT_SCHEMA_LABELS[key] || key}</span>
</div>
<EditableText
value={desc}
onChange={v => update('report_schema', { ...template.report_schema, [key]: v })}
isDark={isDark}
multiline
/>
</div>
))}
</div>
</SectionWrapper>
<SectionWrapper title={FIELD_LABELS.agents} icon={Brain} isDark={isDark}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{template.agents.map((agent, i) => (
<AgentCard
key={agent.id + i}
agent={agent}
onUpdate={updated => {
const next = [...template.agents];
next[i] = updated;
update('agents', next);
}}
onDelete={() => {
if (template.agents.length <= 1) {
window.alert('智能体最少必须保留 1 个');
return;
}
update('agents', template.agents.filter((_, j) => j !== i));
}}
isDark={isDark}
/>
))}
</div>
</SectionWrapper>
</div>
</div>
);
}