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, Package, Globe, BookOpen, MessageSquare, Zap, Settings, FileText, Info, RefreshCw, AlertCircle, Brain, Save, } from 'lucide-react'; // ─── Types ──────────────────────────────────────────────────────────────────── 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; report_examples: Record; field_requirements: Record; agents: AgentData[]; coordinator_config: CoordinatorConfig; } // ─── Constants ──────────────────────────────────────────────────────────────── const FIELD_LABELS: Record = { 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 = { task_background: '任务背景与领导指示研判', resource_assessment: '各方口径与己方资源研判', information_environment: '战场/舆论场环境分析', historical_cultural_context: '历史与人文环境分析', actor_dynamics: '敌我双方动态与优劣势', opponent_narrative_logic: '对方核心叙事逻辑梳理', strategic_weakness: '核心战略薄弱点研判', assessment_conclusion: '研判结论', }; interface AgentVisual { Icon: React.ElementType; lightCard: string; darkCard: string; iconLight: string; iconDark: string; tagLight: string; tagDark: string; } const AGENT_VISUALS: Record = { background: { 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', }, resource: { Icon: Package, 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', }, information_environment: { Icon: Globe, 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', }, history_culture: { Icon: BookOpen, 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', }, opponent_narrative: { Icon: MessageSquare, 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', }, strategy: { 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', }, }; 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 parseExpertiseConfig(expertise: string, fields: string[]) { const match = expertise.match(/(你的分析应服务于?|你服务于)\s*/); if (!match || match.index === undefined) { return { prefix: expertise, marker: '你的分析应服务于', fields: fields.filter(f => expertise.includes(f)), suffix: '。', }; } const prefix = expertise.slice(0, match.index); const marker = match[1] === '你服务于' ? '你的分析应服务于' : match[1]; const rest = expertise.slice(match.index + match[0].length); const endIndex = rest.indexOf('。'); const serviceText = endIndex >= 0 ? rest.slice(0, endIndex) : rest; const suffix = endIndex >= 0 ? rest.slice(endIndex) : '。'; return { prefix, marker, fields: fields.filter(f => serviceText.includes(f)), suffix, }; } function buildExpertise(prefix: string, fields: string[], suffix: string) { const normalizedPrefix = prefix.trim(); const prefixSeparator = normalizedPrefix && !/[。!?.!?]$/.test(normalizedPrefix) ? '。' : ''; const serviceFields = fields.join('、'); const normalizedSuffix = suffix.trim() || '。'; return `${normalizedPrefix}${prefixSeparator}你的分析应服务于 ${serviceFields}${normalizedSuffix.startsWith('。') ? normalizedSuffix : `。${normalizedSuffix}`}`; } function replaceFieldKeysWithLabels(text: string, fields: string[]) { return fields.reduce((next, field) => next.replaceAll(field, mapFieldLabel(field)), text); } function replaceFieldLabelsWithKeys(text: string, fields: string[]) { return fields.reduce((next, field) => next.replaceAll(mapFieldLabel(field), field), text); } function extractFieldTokens(text: string, fields: string[]) { return fields.filter(field => text.includes(field)); } function removeFieldToken(text: string, field: string) { return text .replaceAll(field, '') .replace(/、{2,}/g, '、') .replace(/\s{2,}/g, ' ') .replace(/、([,。;;,.])/g, '$1') .replace(/([、,;;,])\s*([。,.])/g, '$2') .trim(); } function appendFieldTokens(text: string, fields: string[]) { const missing = fields.filter(field => !text.includes(field)); if (!missing.length) return text; const separator = text.trim().endsWith('。') ? '' : '。'; return `${text.trim()}${separator}相关字段:${missing.join('、')}。`; } function getValueType(value: unknown) { if (Array.isArray(value)) return 'array'; if (value === null) return 'null'; return typeof value; } function compareJsonShape(reference: unknown, current: unknown, path = 'root') { const issues: string[] = []; const refType = getValueType(reference); const curType = getValueType(current); if (refType !== curType) { issues.push(`${path}: 类型不一致,期望 ${refType},实际 ${curType}`); return issues; } if (refType === 'array') { const refArr = reference as unknown[]; const curArr = current as unknown[]; if (refArr.length > 0 && curArr.length > 0) { issues.push(...compareJsonShape(refArr[0], curArr[0], `${path}[]`)); } return issues; } if (refType === 'object') { const refObj = reference as Record; const curObj = current as Record; const refKeys = Object.keys(refObj); const curKeys = Object.keys(curObj); refKeys.filter(key => !(key in curObj)).forEach(key => issues.push(`${path}.${key}: 缺少字段`)); curKeys.filter(key => !(key in refObj)).forEach(key => issues.push(`${path}.${key}: 多余字段`)); refKeys .filter(key => key in curObj) .forEach(key => issues.push(...compareJsonShape(refObj[key], curObj[key], `${path}.${key}`))); } return issues; } // ─── Sub-components ─────────────────────────────────────────────────────────── function RestoreAgentModal({ agents, onClose, onRestore, isDark, }: { agents: AgentData[]; onClose: () => void; onRestore: (agent: AgentData) => void; isDark: boolean; }) { return (
e.stopPropagation()} >

恢复已删除智能体

{agents.map(agent => ( ))}
); } function CoordinatorRuleList({ rules, fields, onChange, isDark, }: { rules: string[]; fields: string[]; onChange: (rules: string[]) => void; isDark: boolean; }) { return (
{rules.map((rule, index) => (
onChange(rules.map((item, i) => i === index ? nextRule : item))} isDark={isDark} />
))}
); } function CoordinatorFieldText({ value, fields, onChange, isDark, }: { value: string; fields: string[]; onChange: (v: string) => void; isDark: boolean; }) { const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(replaceFieldKeysWithLabels(value, fields)); const [pickerOpen, setPickerOpen] = useState(false); const selectedFields = extractFieldTokens(value, fields); const fieldOptions = fields.map(f => ({ value: f, label: mapFieldLabel(f) })); 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'; const confirm = () => { onChange(replaceFieldLabelsWithKeys(draft, fields)); setEditing(false); }; const removeField = (field: string) => { onChange(removeFieldToken(value, field)); setDraft(replaceFieldKeysWithLabels(removeFieldToken(value, field), fields)); }; const addFields = (nextFields: string[]) => { const next = appendFieldTokens(value, nextFields); onChange(next); setDraft(replaceFieldKeysWithLabels(next, fields)); setPickerOpen(false); }; return (
{editing ? (