639 lines
28 KiB
TypeScript
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>
|
|
);
|
|
}
|