+
+
+ {/* 主体内容区 */}
+
+
+
+ {/* 中间结果区 */}
+
+ {viewMode === 'single' ? (
+
+ {/* 聊天消息区 */}
+
+ {messages.length === 0 ? (
+
}
+ title="开始你的问答"
+ description="输入问题,获取基于知识库的智能回答"
+ />
+ ) : (
+ messages.map((msg) => (
+
+ ))
+ )}
+
+ {isPreConnectLoading && (
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+ {showScrollToBottom && (
+
+ )}
+
+ ) : (
+ <>
+ {/* 对比结果区 */}
+
+ {compareUserInput && (
+
+ )}
+ {compareResult ? (
+ compareLayout === 'horizontal' ? (
+
+ {/* 左侧结果 */}
+
+
+
+
+
+
+ {getModeLabel(compareResult.left.mode)}
+
+
+ {compareResult.left.loading && (
+
+ )}
+
+
+
+
+ {compareResult.left.mode === 'single-round' ? (
+ handleCompareCitationClick('left', idx)} />
+ ) : (
+ handleCompareCitationClick('left', idx)} />
+ )}
+
+
+
+
+ {/* 右侧结果 */}
+
+
+
+
+
+
+ {getModeLabel(compareResult.right.mode)}
+
+
+ {compareResult.right.loading && (
+
+ )}
+
+
+
+
+ {compareResult.right.mode === 'single-round' ? (
+ handleCompareCitationClick('right', idx)} />
+ ) : (
+ handleCompareCitationClick('right', idx)} />
+ )}
+
+
+
+
+ ) : (
+
+ {/* 上方结果 */}
+
+
+
+
+
+
+ {getModeLabel(compareResult.left.mode)}
+
+
+ {compareResult.left.loading && (
+
+ )}
+
+
+
+
+ {compareResult.left.mode === 'single-round' ? (
+ handleCompareCitationClick('left', idx)} />
+ ) : (
+ handleCompareCitationClick('left', idx)} />
+ )}
+
+
+
+
+ {/* 下方结果 */}
+
+
+
+
+
+
+ {getModeLabel(compareResult.right.mode)}
+
+
+ {compareResult.right.loading && (
+
+ )}
+
+
+
+
+ {compareResult.right.mode === 'single-round' ? (
+ handleCompareCitationClick('right', idx)} />
+ ) : (
+ handleCompareCitationClick('right', idx)} />
+ )}
+
+
+
+
+ )
+ ) : (
+
+
+
对比模式
+
输入问题,对比不同模式的回答效果
+
+ )}
+
+ >
+ )}
+
+ {/* 底部输入区 */}
+
setIsModelSettingsOpen(true)}
+ isLoading={isLoading}
+ placeholder={viewMode === 'compare' ? '输入问题进行对比分析...' : '输入你的问题...'}
+ showReset
+ />
+
+
+ {/* 右侧辅助区 */}
+ {viewMode === 'single' && showProcess && (
+
+ {/* 检索过程 */}
+
+
+ 检索过程
+
+
+
+
+ 当前模式
+
+ {searchProcess.currentMode}
+
+
+
+ 检索轮次
+
+ {searchProcess.rounds}
+
+
+
+ 命中数
+
+ {searchProcess.hits}
+
+
+ {searchProcess.queryRewrite && (
+
+
Query改写
+
+ {searchProcess.queryRewrite}
+
+
+ )}
+
+
+
+
+ )}
+
+ {/* 对比模式的差异总结 */}
+ {viewMode === 'compare' && compareResult?.differences && (
+
+
+ 差异总结
+
+
+
+
+
+ 一致观点
+
+
+ {compareResult.differences.consensus}
+
+
+
+
+
+ 差异观点
+
+
+ {compareResult.differences.differences}
+
+
+
+
+ {getModeLabel(compareResult.left.mode)}优势
+
+
+ {compareResult.differences.leftAdvantage}
+
+
+
+
+ {getModeLabel(compareResult.right.mode)}优势
+
+
+ {compareResult.differences.rightAdvantage}
+
+
+
+
+ )}
+
+
+ {editingMessageId && (
+
+
+
+
+
+
编辑模型生成内容
+
+ 可同时修改模型回答和思考过程,多轮检索会把思考过程写回第一轮思考内容。
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {selectedCitation && (
+
+
setSelectedCitation(null)}
+ />
+
+
+
+
+
{selectedCitation.title || '引用内容'}
+
+
来源:{selectedCitation.source || selectedCitation.type || '未知来源'}
+
时间:{selectedCitation.time || '未知'}
+
+
+
+ {selectedCitation.url && (
+
+ )}
+
+
+ {selectedCitation.content || selectedCitation.snippet || '暂无引用内容'}
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default GeneralQA;
diff --git a/frontend-web/src/strategy-components/components/qa/GeneralQA.types.ts b/frontend-web/src/strategy-components/components/qa/GeneralQA.types.ts
new file mode 100644
index 00000000..2e36a2ea
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/GeneralQA.types.ts
@@ -0,0 +1,74 @@
+import type { QASingleRagTrace, QAThinkingTrace } from './QACommon';
+
+export type QAMode = 'single-round' | 'multi-round' | 'deep-search';
+export type ViewMode = 'single' | 'compare';
+export type KnowledgeScope = 'all' | 'specific-kb' | 'specific-domain';
+export type AnswerStyle = 'concise' | 'detailed';
+export type CompareLayout = 'horizontal' | 'vertical';
+
+export interface SourceItem {
+ title: string;
+ type: string;
+ snippet: string;
+ source?: string;
+ time?: string;
+ url?: string;
+ content?: string;
+}
+
+export interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ timestamp: Date;
+ singleRagTrace?: QASingleRagTrace;
+ thinkingTrace?: QAThinkingTrace;
+ sources?: SourceItem[];
+}
+
+export interface ToolCallData {
+ tool_name: string;
+ tool_input: {
+ text?: string;
+ result?: string;
+ };
+}
+
+export interface KnowledgeBaseDoc {
+ title?: string;
+ content?: string;
+ source?: string;
+ url?: string;
+ snippet?: string;
+}
+
+export interface KnowledgeBaseChunk {
+ answer?: string;
+ docs?: KnowledgeBaseDoc[] | KnowledgeBaseDoc | string;
+}
+
+export interface ComparePane {
+ mode: QAMode;
+ loading: boolean;
+ singleRagTrace?: QASingleRagTrace;
+ thinkingTrace?: QAThinkingTrace;
+ sources: SourceItem[];
+}
+
+export interface CompareResult {
+ left: ComparePane;
+ right: ComparePane;
+ differences?: {
+ consensus: string;
+ differences: string;
+ leftAdvantage: string;
+ rightAdvantage: string;
+ };
+}
+
+export interface SearchProcessState {
+ currentMode: string;
+ rounds: number;
+ hits: number;
+ queryRewrite: string;
+}
diff --git a/frontend-web/src/strategy-components/components/qa/GeneralQAConfigSidebar.tsx b/frontend-web/src/strategy-components/components/qa/GeneralQAConfigSidebar.tsx
new file mode 100644
index 00000000..e01852b1
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/GeneralQAConfigSidebar.tsx
@@ -0,0 +1,249 @@
+import { BookOpen, Brain, ChevronLeft, ChevronRight, Sparkles } from 'lucide-react';
+import type { AnswerStyle, CompareLayout, KnowledgeScope, QAMode, ViewMode } from './GeneralQA.types';
+
+/**
+ * 左侧配置栏 Props。
+ *
+ * 组件内不保存业务状态,所有选中值和 setter 都来自父组件。
+ * 这样父组件可以统一处理“生成中切换需要确认停止”的逻辑,配置栏只负责触发用户意图。
+ */
+type Props = {
+ isDark: boolean; // 当前是否为暗黑主题,用于样式适配
+ viewMode: ViewMode; // 当前视图模式:'single' 单一模式 或 'compare' 对比模式
+ qaMode: QAMode; // 单一模式下的问答模式:'single-round' 单轮RAG / 'multi-round' 多轮RAG / 'deep-search' 深度检索
+ compareLeftMode: QAMode; // 对比模式下左侧的问答模式
+ compareRightMode: QAMode; // 对比模式下右侧的问答模式
+ compareLayout: CompareLayout; // 对比模式的布局方式:'horizontal' 水平 或 'vertical' 垂直
+ knowledgeScope: KnowledgeScope; // 知识范围设置:'all' 全部 / 'specific-kb' 指定知识库 / 'specific-domain' 指定领域
+ answerStyle: AnswerStyle; // 回答风格偏好:'concise' 简洁 或 'detailed' 详细
+ showSources: boolean; // 是否在回答中展示引用来源
+ showProcess: boolean; // 是否展示模型的思考过程
+ autoSummarizeDiff: boolean; // 对比模式下是否自动总结左右差异
+ recommendBetter: boolean; // 对比模式下是否推荐更好的回答方案
+ isCollapsed: boolean; // 左侧配置面板是否折叠
+ setQaMode: (mode: QAMode) => void; // 设置单一模式的问答模式
+ setCompareLeftMode: (mode: QAMode) => void; // 设置对比模式左侧的模式
+ setCompareRightMode: (mode: QAMode) => void; // 设置对比模式右侧的模式
+ setCompareLayout: (layout: CompareLayout) => void; // 设置对比布局方式
+ setKnowledgeScope: (scope: KnowledgeScope) => void; // 设置知识范围
+ setAnswerStyle: (style: AnswerStyle) => void; // 设置回答风格
+ setShowSources: (show: boolean) => void; // 设置是否展示引用来源
+ setShowProcess: (show: boolean) => void; // 设置是否展示思考过程
+ setAutoSummarizeDiff: (enabled: boolean) => void; // 设置是否自动总结差异
+ setRecommendBetter: (enabled: boolean) => void; // 设置是否推荐更好的方案
+ setIsCollapsed: (collapsed: boolean) => void; // 设置配置面板折叠状态
+};
+
+/**
+ * 单一模式下可选的问答模式。
+ *
+ * 每个选项同时提供展示文案、图标和简短说明,便于配置栏用统一结构渲染卡片。
+ */
+const qaModeOptions = [
+ { value: 'single-round' as const, label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
+ { value: 'multi-round' as const, label: '多轮RAG', icon: Brain, desc: '深入理解' },
+ { value: 'deep-search' as const, label: '深度检索', icon: BookOpen, desc: '全面分析' },
+];
+
+/**
+ * 知识范围选项。
+ *
+ * 当前只影响 UI 配置状态,后续接入真实知识库筛选时可以直接把该值映射到请求参数。
+ */
+const knowledgeScopeOptions = [
+ { value: 'all' as const, label: '全部知识源' },
+ { value: 'specific-kb' as const, label: '指定知识库' },
+ { value: 'specific-domain' as const, label: '指定资源域' },
+];
+
+/**
+ * 左侧问答配置栏。
+ *
+ * 该组件集中承载页面的“模式选择、知识范围、回答偏好、对比设置”等表单控件。
+ * 父组件仍是唯一状态源,配置栏通过 setter 把用户选择同步回父组件;这样不会改变原有请求逻辑,
+ * 同时把 300 多行配置 JSX 从 `GeneralQA.tsx` 中移出,便于后续维护表单项和样式。
+ */
+export default function GeneralQAConfigSidebar({
+ isDark,
+ viewMode,
+ qaMode,
+ compareLeftMode,
+ compareRightMode,
+ compareLayout,
+ knowledgeScope,
+ answerStyle,
+ showSources,
+ showProcess,
+ autoSummarizeDiff,
+ recommendBetter,
+ isCollapsed,
+ setQaMode,
+ setCompareLeftMode,
+ setCompareRightMode,
+ setCompareLayout,
+ setKnowledgeScope,
+ setAnswerStyle,
+ setShowSources,
+ setShowProcess,
+ setAutoSummarizeDiff,
+ setRecommendBetter,
+ setIsCollapsed,
+}: Props): JSX.Element {
+ /**
+ * 自定义勾选图标。
+ *
+ * 原生 checkbox 被隐藏后,所有勾选项都通过这个 SVG 保持一致的视觉风格。
+ */
+ const renderCheckIcon = () => (
+
+ );
+
+ return (
+
+
+ {!isCollapsed && (
+
+ 问答配置
+
+ )}
+
+
+
+ {!isCollapsed && (
+ <>
+ {viewMode === 'single' ? (
+
+
问答模式
+
+ {qaModeOptions.map((mode) => (
+
+ ))}
+
+
+ ) : (
+ <>
+
+
对比模式设置
+
+
+
+
+
VS
+
+
+
+
+
+
+
+
对比布局
+
+ {(['horizontal', 'vertical'] as const).map((layout) => (
+
+ ))}
+
+
+ >
+ )}
+
+
+
知识范围
+
+ {knowledgeScopeOptions.map((option) => (
+
+ ))}
+
+
+
+
+
+ {viewMode === 'compare' && (
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/frontend-web/src/strategy-components/components/qa/GeneralQAHeader.tsx b/frontend-web/src/strategy-components/components/qa/GeneralQAHeader.tsx
new file mode 100644
index 00000000..7a94364f
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/GeneralQAHeader.tsx
@@ -0,0 +1,80 @@
+import { History } from 'lucide-react';
+import type { ViewMode } from './GeneralQA.types';
+
+/**
+ * 顶部栏 Props。
+ *
+ * `setViewMode` 由父组件传入,父组件会在生成中切换时做确认拦截;
+ * Header 本身不直接感知 loading,保持纯展示和事件转发。
+ */
+type Props = {
+ isDark: boolean; // 当前是否为暗黑主题,用于样式适配
+ viewMode: ViewMode; // 当前视图模式:'single' 单一模式 或 'compare' 对比模式
+ setViewMode: (mode: ViewMode) => void; // 设置视图模式回调函数
+};
+
+/**
+ * 通用问答页面顶部栏。
+ *
+ * 该组件只负责页面标题、模式切换和历史记录入口,所有业务状态仍由父组件维护。
+ * 这样拆分后,顶部导航的样式和交互不会和中间问答流处理逻辑混在一起,后续如果要新增
+ * “保存模板”“导出记录”等入口,也可以只改这个文件。
+ */
+export default function GeneralQAHeader({ isDark, viewMode, setViewMode }: Props): JSX.Element {
+ return (
+
+
+
+ 通用问答
+
+
+ 支持单轮RAG、多轮RAG、深度检索及结果对比
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-web/src/strategy-components/components/qa/MopeChat.css b/frontend-web/src/strategy-components/components/qa/MopeChat.css
new file mode 100644
index 00000000..16df3807
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/MopeChat.css
@@ -0,0 +1,410 @@
+.mope-chat {
+ --bg: #ffffff;
+ --surface: #ffffff;
+ --surface-raised: #ffffff;
+ --card: #ffffff;
+ --border: #fee2e2;
+ --border-strong: #fecaca;
+ --text: #111827;
+ --text-muted: #6b7280;
+ --accent: #dc2626;
+ --accent-hover: #ef4444;
+ --green: #16a34a;
+ --yellow: #f59e0b;
+ --red: #dc2626;
+ --purple: #7c3aed;
+ --button-text: #ffffff;
+ --surface-hover: rgba(220, 38, 38, 0.04);
+ --surface-hover-strong: rgba(220, 38, 38, 0.08);
+ --accent-soft: rgba(220, 38, 38, 0.08);
+ --accent-soft-strong: rgba(220, 38, 38, 0.16);
+ --danger-soft: rgba(220, 38, 38, 0.10);
+ --shadow-soft: 0 14px 40px rgba(15, 23, 42, 0.07);
+ --shadow-card: 0 8px 24px rgba(15, 23, 42, 0.05);
+
+ --text-primary: var(--text);
+ --text-secondary: var(--text-muted);
+ --border-color: var(--border);
+ --bg-secondary: var(--surface);
+ --accent-blue: var(--accent);
+
+ --phase-starting: var(--accent);
+ --phase-diverging: var(--yellow);
+ --phase-closing: var(--green);
+ --phase-done: var(--purple);
+
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ font-size: 14px;
+ line-height: 1.6;
+ color: var(--text);
+ background: var(--bg);
+}
+
+.mope-chat.mope-chat-dark {
+ --bg: #0f172a;
+ --surface: #1e293b;
+ --surface-raised: #111827;
+ --card: #0f172a;
+ --border: rgba(30, 64, 175, 0.45);
+ --border-strong: rgba(59, 130, 246, 0.45);
+ --text: #f8fafc;
+ --text-muted: #94a3b8;
+ --accent: #3b82f6;
+ --accent-hover: #60a5fa;
+ --green: #10b981;
+ --yellow: #f59e0b;
+ --red: #f43f5e;
+ --purple: #8b5cf6;
+ --button-text: #ffffff;
+ --surface-hover: rgba(255, 255, 255, 0.03);
+ --surface-hover-strong: rgba(255, 255, 255, 0.06);
+ --accent-soft: rgba(59, 130, 246, 0.14);
+ --accent-soft-strong: rgba(59, 130, 246, 0.22);
+ --danger-soft: rgba(244, 63, 94, 0.12);
+ --shadow-soft: 0 18px 45px rgba(0, 0, 0, 0.28);
+ --shadow-card: 0 12px 28px rgba(0, 0, 0, 0.22);
+}
+
+.mope-chat,
+.mope-chat * {
+ box-sizing: border-box;
+}
+
+.mope-chat {
+ color: var(--text);
+ min-height: 100%;
+ transition: background 0.2s ease, color 0.2s ease;
+}
+
+.mope-chat input,
+.mope-chat textarea,
+.mope-chat select,
+.mope-chat button {
+ font: inherit;
+}
+
+.mope-chat p,
+.mope-chat h1,
+.mope-chat h2,
+.mope-chat h3,
+.mope-chat h4,
+.mope-chat pre {
+ margin-top: 0;
+}
+
+.mope-chat ::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+.mope-chat ::-webkit-scrollbar-track {
+ background: var(--surface);
+}
+.mope-chat ::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 3px;
+}
+.mope-chat ::-webkit-scrollbar-thumb:hover {
+ background: var(--text-muted);
+}
+
+/* Idle: centered form */
+.mope-chat.app-idle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100vh - 96px);
+ padding: 36px 40px 42px;
+ background:
+ radial-gradient(circle at top center, var(--accent-soft) 0, transparent 34%),
+ var(--bg);
+}
+
+.mope-chat.app-idle-scroll {
+ align-items: flex-start;
+ overflow-y: auto;
+}
+
+/* Running / done: two-column */
+.mope-chat.app-running {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 96px);
+ overflow: hidden;
+ background: var(--surface-raised);
+ border-radius: 18px;
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow-card);
+}
+
+.mope-chat .app-header {
+ background: color-mix(in srgb, var(--surface) 92%, var(--accent) 8%);
+ border-bottom: 1px solid var(--border);
+ padding: 14px 18px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-shrink: 0;
+}
+
+.mope-chat .app-header-logo {
+ font-weight: 700;
+ font-size: 15px;
+ color: var(--accent);
+ letter-spacing: 0.5px;
+}
+
+.mope-chat .app-header-task {
+ flex: 1;
+ color: var(--text-muted);
+ font-size: 13px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.mope-chat .app-header-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.mope-chat .app-body {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.mope-chat .app-sidebar {
+ width: 250px;
+ flex-shrink: 0;
+ overflow-y: auto;
+ border-right: 1px solid var(--border);
+ background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
+}
+
+.mope-chat .app-main {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px;
+ background: var(--bg);
+}
+
+.mope-chat .app-progress-sidebar {
+ width: 320px;
+ flex-shrink: 0;
+ overflow-y: auto;
+ border-left: 1px solid var(--border);
+ background: color-mix(in srgb, var(--surface) 96%, var(--accent) 4%);
+}
+
+.mope-chat .app-side-panel {
+ position: relative;
+ transition: width 0.22s ease, background 0.2s ease;
+}
+
+.mope-chat .app-side-panel.collapsed {
+ width: 44px;
+ overflow: hidden;
+}
+
+.mope-chat .app-side-toggle {
+ position: sticky;
+ top: 10px;
+ z-index: 2;
+ width: 28px;
+ height: 28px;
+ min-width: 28px;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ background: var(--surface-raised);
+ color: var(--text-muted);
+ box-shadow: var(--shadow-card);
+}
+
+.mope-chat .app-side-toggle:hover:not(:disabled) {
+ color: var(--accent);
+ border-color: var(--border-strong);
+ background: var(--surface-hover-strong);
+ transform: translateY(-1px);
+}
+
+.mope-chat .app-side-toggle-left {
+ float: right;
+ margin: 10px 10px 0 0;
+}
+
+.mope-chat .app-side-toggle-right {
+ float: left;
+ margin: 10px 0 0 10px;
+}
+
+.mope-chat .app-side-panel.collapsed .app-side-toggle-left,
+.mope-chat .app-side-panel.collapsed .app-side-toggle-right {
+ float: none;
+ margin: 10px auto 0;
+}
+
+/* Buttons */
+.mope-chat button {
+ cursor: pointer;
+ border: none;
+ border-radius: 10px;
+ padding: 8px 16px;
+ font-size: 13px;
+ font-family: inherit;
+ transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s, background 0.15s, border-color 0.15s, color 0.15s;
+}
+
+.mope-chat button:hover:not(:disabled) {
+ transform: translateY(-1px);
+}
+
+.mope-chat button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.mope-chat .btn-primary {
+ background: linear-gradient(135deg, var(--accent), var(--accent-hover));
+ color: var(--button-text);
+ font-weight: 600;
+ box-shadow: 0 10px 22px var(--accent-soft-strong);
+}
+
+.mope-chat .btn-primary:hover:not(:disabled) {
+ opacity: 0.85;
+}
+
+.mope-chat .btn-danger {
+ background: transparent;
+ color: var(--red);
+ border: 1px solid var(--red);
+}
+
+.mope-chat .btn-danger:hover:not(:disabled) {
+ background: var(--danger-soft);
+}
+
+.mope-chat .btn-secondary {
+ background: var(--surface-raised);
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+}
+
+.mope-chat .btn-secondary:hover:not(:disabled) {
+ color: var(--text);
+ border-color: var(--border-strong);
+ background: var(--surface-hover);
+}
+
+.mope-chat .btn-ghost {
+ background: transparent;
+ color: var(--text-muted);
+ padding: 4px 8px;
+ font-size: 12px;
+}
+
+.mope-chat .btn-ghost:hover {
+ color: var(--text);
+}
+
+/* Badges */
+.mope-chat .badge {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.3px;
+}
+
+.mope-chat .badge-starting { background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent); }
+.mope-chat .badge-diverging { background: color-mix(in srgb, var(--yellow) 15%, transparent); color: var(--yellow); }
+.mope-chat .badge-closing { background: color-mix(in srgb, var(--green) 15%, transparent); color: var(--green); }
+.mope-chat .badge-done { background: color-mix(in srgb, var(--purple) 15%, transparent); color: var(--purple); }
+.mope-chat .badge-default { background: color-mix(in srgb, var(--text-muted) 15%, transparent); color: var(--text-muted); }
+
+/* Spinner */
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.mope-chat .spinner {
+ display: inline-block;
+ animation: spin 1s linear infinite;
+}
+
+/* Reference cite */
+.mope-chat .ref-cite {
+ display: inline-block;
+ background: var(--accent-soft-strong);
+ color: var(--accent);
+ border-radius: 4px;
+ padding: 0 3px;
+ font-family: 'SFMono-Regular', Consolas, monospace;
+ font-size: 11px;
+ cursor: pointer;
+ position: relative;
+}
+
+.mope-chat .ref-cite:hover::after {
+ content: attr(data-tooltip);
+ position: absolute;
+ bottom: calc(100% + 4px);
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--card);
+ border: 1px solid var(--border);
+ color: var(--text);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ white-space: nowrap;
+ z-index: 100;
+ font-family: inherit;
+ pointer-events: none;
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Error banner */
+.mope-chat .error-banner {
+ background: var(--danger-soft);
+ border: 1px solid var(--red);
+ border-radius: 6px;
+ padding: 8px 12px;
+ color: var(--red);
+ font-size: 13px;
+ margin-bottom: 12px;
+}
+
+@media (max-width: 1200px) {
+ .mope-chat .app-progress-sidebar {
+ width: 260px;
+ }
+}
+
+@media (max-width: 960px) {
+ .mope-chat .app-body {
+ flex-direction: column;
+ }
+
+ .mope-chat .app-sidebar,
+ .mope-chat .app-progress-sidebar {
+ width: 100%;
+ border-right: none;
+ border-left: none;
+ border-bottom: 1px solid var(--border);
+ max-height: 260px;
+ }
+
+ .mope-chat .app-main {
+ padding: 14px;
+ }
+}
diff --git a/frontend-web/src/strategy-components/components/qa/MopeChat.jsx b/frontend-web/src/strategy-components/components/qa/MopeChat.jsx
new file mode 100644
index 00000000..7b3287b2
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/MopeChat.jsx
@@ -0,0 +1,215 @@
+import React, { useEffect, useState } from 'react';
+import { useSession } from '@/hooks/useSession';
+import ConfigPanel from './components/ConfigPanel';
+import Sidebar from './components/Sidebar';
+import Timeline from './components/Timeline';
+import ProgressSidebar from './components/ProgressSidebar';
+import ScenarioConfigPage from './components/ScenarioConfigPage';
+import './MopeChat.css';
+
+/**
+ * 从 localStorage 读取用户偏好的主题色,默认为 light
+ */
+function getInitialTheme() {
+ if (typeof window === 'undefined') return 'light';
+ return window.localStorage.getItem('mope-theme') || 'light';
+}
+
+/**
+ * 应用根组件
+ *
+ * 负责整个页面的布局切换:
+ * - idle 状态:显示配置面板或场景配置页(启动前)
+ * - running / done / error 状态:显示三栏分析界面(左侧进度 → 中间时间线 → 右侧状态)
+ *
+ * 通过 useSession hook 管理所有分析会话的状态和事件流
+ */
+export default function MopeChat() {
+ const session = useSession();
+ const [taskContext, setTaskContext] = useState('');
+ const [idlePage, setIdlePage] = useState('home');
+ const [theme, setTheme] = useState(getInitialTheme);
+
+ // 解构 session hook 返回的所有状态和方法
+ // status: 'idle' | 'running' | 'waiting_human' | 'done' | 'error'
+ // phase: 'starting' | 'diverging' | 'closing' | 'done' → 当前分析阶段
+ // coordinatorMode: 'auto' | 'human_review' → 协调者运行模式
+ const {
+ status,
+ phase,
+ coordinatorMode,
+ currentRound,
+ totalRounds,
+ divergeRounds,
+ reportSchema,
+ agents,
+ agentStates,
+ timeline,
+ coordinatorUpdates,
+ latestDraft,
+ sharedMemory,
+ finalReportPartial,
+ currentFinalField,
+ pendingReview,
+ reviewSubmitting,
+ reviewRevising,
+ errors,
+ startSession,
+ stopSession,
+ approveCoordinator,
+ reviseCoordinator,
+ reset,
+ } = session;
+
+ // 主题切换时同步到 DOM 和 localStorage
+ useEffect(() => {
+ document.documentElement.dataset.theme = theme;
+ window.localStorage.setItem('mope-theme', theme);
+ }, [theme]);
+
+ /**
+ * 用户点击"开始分析"时触发
+ * @param {Object} config - 表单配置(任务背景、模板、智能体等)
+ * @param {string} backendUrl - 后端 API 地址
+ */
+ const handleStart = (config, backendUrl) => {
+ setTaskContext(config.task_context || '');
+ startSession(config, backendUrl);
+ };
+
+ /** 用户点击"停止"时触发,中断当前 SSE 连接 */
+ const handleStop = () => {
+ stopSession();
+ };
+
+ /** 用户点击"重新开始"时触发,重置所有状态回到 idle 页 */
+ const handleReset = () => {
+ reset();
+ setTaskContext('');
+ };
+
+ /** 切换明/暗主题 */
+ const handleThemeToggle = () => {
+ setTheme(current => (current === 'light' ? 'dark' : 'light'));
+ };
+
+ // ========== idle 状态下的页面 ==========
+
+ // 场景配置页(idle 下的二级页面)
+ if (status === 'idle') {
+ if (idlePage === 'scenario_config') {
+ return (
+
+
+ setIdlePage('home')} />
+
+ );
+ }
+
+ // 首页:配置表单
+ return (
+
+
+ setIdlePage('scenario_config')}
+ />
+
+ );
+ }
+
+ // ========== 运行中 / 已完成 / 错误 状态下的三栏布局 ==========
+
+ return (
+
+ {/* 主题切换按钮(运行状态下固定在 header 区域) */}
+
+
+ {/* 顶部状态栏:显示项目名、任务摘要、运行模式、操作按钮 */}
+
+
+ {/* 三栏主体区域 */}
+
+ {/* 左侧栏:阶段进度 + 智能体列表(可点击跳转到对应输出) */}
+
+
+ {/* 中间主区域:错误横幅 + 时间线(所有 agent 输出和协调者指令按时间排列) */}
+
+ {errors.length > 0 && errors.map((e, i) => (
+
+ {e.agent_id ? `[${e.agent_id}] ` : ''}{e.error}
+
+ ))}
+
+
+
+ {/* 右侧栏:方案进度、共享记忆、字段/角色记忆 */}
+
+
+
+ );
+}
diff --git a/frontend-web/src/strategy-components/components/qa/MopeChat.mock.ts b/frontend-web/src/strategy-components/components/qa/MopeChat.mock.ts
new file mode 100644
index 00000000..169bb9e5
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/MopeChat.mock.ts
@@ -0,0 +1,437 @@
+export const MOCK_TASK_CONTEXT =
+ '某头部科技公司发生大规模用户数据泄露事件,涉及约 500 万用户信息,已有多家媒体报道,需在 48 小时内完成舆论应对策略制定。';
+
+const REPORT_SCHEMA = {
+ core_strategy: '核心传播策略与定调方向',
+ key_messages: '核心话术与传播口径',
+ audience_map: '目标受众画像与触达路径',
+ risk_warnings: '舆论风险预警与应对预案',
+ action_plan: '具体行动计划与时间轴',
+};
+
+const AGENTS = [
+ { id: 'narrative', name: '叙事分析师', dimension: '叙事框架' },
+ { id: 'audience', name: '受众洞察师', dimension: '受众分析' },
+ { id: 'risk', name: '风险评估师', dimension: '风险研判' },
+ { id: 'channel', name: '渠道策略师', dimension: '渠道传播' },
+];
+
+const AGENT_STATES = {
+ narrative: { status: 'done', name: '叙事分析师', dimension: '叙事框架' },
+ audience: { status: 'done', name: '受众洞察师', dimension: '受众分析' },
+ risk: { status: 'done', name: '风险评估师', dimension: '风险研判' },
+ channel: { status: 'done', name: '渠道策略师', dimension: '渠道传播' },
+};
+
+const SHARED_MEMORY = {
+ stable_claims: [
+ '公众对"企业主动披露"的容忍度明显高于"被动曝光",定调应以主动承担为主旋律',
+ '数据泄露事件中"用户利益优先"叙事比"技术故障"叙事更能有效降低负面评价',
+ '科技公司危机中 KOL 态度在 6 小时内形成,此后难以扭转',
+ ],
+ key_evidence: [
+ '微博相关话题已有 2300 万阅读,评论情绪负向占比 67%',
+ '同类事件(2022 年某平台泄露)采用主动道歉策略后 NPS 下跌幅度仅为被动回应的 40%',
+ '核心受众 25–35 岁用户对隐私保护敏感度评分 8.3/10,为最高优先级群体',
+ ],
+ conflicts: [
+ '风险师认为应延迟发声等待内部核查完毕;叙事师认为 6 小时黄金窗口不可错过',
+ '渠道师建议优先微博大 V 背书;受众师认为私域触达更能稳住核心用户',
+ ],
+ open_questions: [
+ '泄露数据具体范围是否已完全确认?影响最终话术精准度',
+ '监管部门是否已介入?需提前准备合规声明版本',
+ ],
+ field_memory: {
+ core_strategy: {
+ title: '核心策略',
+ status: 'confirmed',
+ current_takeaways: [
+ '以"主动承担、用户优先"为核心定调,避免技术性推诿',
+ '第一时间启动 CEO 署名声明,彰显最高层重视',
+ ],
+ missing_points: [],
+ },
+ risk_warnings: {
+ title: '风险预警',
+ status: 'partial',
+ current_takeaways: ['次生舆情风险:监管追责、竞品借势炒作', '情绪峰值预计在 T+12h 出现'],
+ missing_points: ['灰色产业链二次利用风险尚未评估'],
+ },
+ },
+ agent_memory: {
+ narrative: {
+ name: '叙事分析师',
+ latest_position: '建议采用"危机—承担—改变"三段式叙事结构,第一阶段不超过 200 字',
+ useful_findings: [
+ '历史数据显示"技术故障"措辞会使负面情绪扩大 23%',
+ '"补偿方案"在声明中同步出现可降低 31% 的愤怒情绪',
+ ],
+ open_issues: ['CEO 是否愿意出面公开致歉,将直接影响叙事策略选择'],
+ },
+ risk: {
+ name: '风险评估师',
+ latest_position: '最大风险是监管介入与集体诉讼,需提前准备法务声明独立版本',
+ useful_findings: ['同类事件 80% 在 T+24h 出现监管询问,需预留合规响应通道'],
+ open_issues: [],
+ },
+ },
+};
+
+const TIMELINE = [
+ // ─── 启动阶段 ───────────────────────────────────────
+ { type: 'round_start', round: 1, phase: 'starting' },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'narrative',
+ round: 1,
+ phase: 'starting',
+ data: {
+ thinking:
+ '事件已进入舆论快速扩散阶段,需要判断最佳叙事切入点。从历史案例来看,科技公司数据泄露的舆论走向主要受两个变量影响:一是企业回应速度,二是叙事框架选择。当前已有媒体报道,说明被动曝光已经发生,但官方尚未表态,仍有 6 小时内抢占定性框架的窗口。',
+ content:
+ '## 叙事框架分析\n\n当前事件已进入**被动曝光**阶段,官方沉默每延迟 1 小时将使负面叙事主导权进一步转移至媒体和用户。\n\n**推荐框架:危机承担型**\n\n> 我们深感抱歉,这不应该发生。我们在 [时间] 发现了问题,[具体措施],我们承诺……\n\n此框架相比"技术故障型"在情绪疏导上效率高出 40%。核心要点:\n1. **主语明确**:以企业 / CEO 第一人称,避免被动语态\n2. **事实先行**:已知信息直接披露,未知信息注明"正在调查"\n3. **补偿同步**:补救措施与声明同时发出,不事后补充',
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'audience',
+ round: 1,
+ phase: 'starting',
+ data: {
+ thinking:
+ '需要先切分受众结构。数据泄露涉及 500 万用户,但情绪爆发的核心驱动者通常只占 5% 左右。我需要识别出哪些群体的愤怒会向外扩散,哪些群体是可稳定的沉默多数。',
+ content:
+ '## 受众结构分析\n\n| 受众类型 | 规模估算 | 关注核心 | 应对优先级 |\n|---|---|---|---|\n| 直接受损用户 | ~50 万 | 个人信息安全、补偿 | ★★★★★ |\n| 隐私敏感用户 | ~150 万 | 是否受影响、如何自保 | ★★★★ |\n| 科技媒体 & KOL | < 500 人 | 技术细节、企业诚信 | ★★★★ |\n| 监管与法律关注者 | 小众但高影响 | 合规与处罚 | ★★★ |\n| 普通围观用户 | 多数 | 吃瓜情绪,跟随舆论 | ★★ |\n\n**战略建议**:优先稳住直接受损用户的情绪,通过私域渠道一对一触达;KOL 需在 T+2h 内通过公关渠道给予独家信息简报。',
+ dimensionOutput: {
+ 核心受众: '直接受损用户(25–35 岁,高隐私敏感度)',
+ 情绪驱动因子: '不安全感 > 愤怒 > 补偿诉求',
+ 次级受众: '科技媒体与隐私倡导 KOL',
+ },
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'risk',
+ round: 1,
+ phase: 'starting',
+ data: {
+ content:
+ '## 风险研判(T+0 至 T+48h)\n\n**当前风险等级:高(4/5)**\n\n### 短期风险(T+0 ~ T+6h)\n- 媒体二次跟进,细节失控叙事\n- 竞品借势营销,转化离网用户\n- 用户自发传播截图,情绪放大\n\n### 中期风险(T+6h ~ T+24h)\n- 监管部门询问(概率 ~80%)\n- 集体诉讼舆论造势\n- KOL 发酵,话题进入热搜\n\n### 应对红线\n> ❌ 绝对不能:否认泄露规模、质疑用户截图真实性、强调技术复杂性而回避责任',
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'channel',
+ round: 1,
+ phase: 'starting',
+ data: {
+ content:
+ '## 渠道策略(首 6 小时)\n\n**渠道优先级排序:**\n\n1. **官方微博 / 公众号** — 主声明发布,需 T+2h 内上线\n2. **App 内 Push / 站内信** — 直达受损用户,降低流失率\n3. **客服热线预案** — 预计来电量激增 300%,需扩容预案\n4. **媒体渠道** — 给核心科技媒体记者 1 小时独家简报\n5. **KOL 沟通** — T+3h 前给隐私/科技类 KOL 发送详细说明',
+ },
+ },
+
+ {
+ type: 'coordinator',
+ data: {
+ divergeRound: null,
+ totalDivergeRounds: 3,
+ nextSpeakers: ['narrative', 'risk'],
+ focus: {
+ narrative: '深化叙事结构,给出 CEO 声明草稿(200 字以内)',
+ risk: '量化各风险节点的时间窗口和触发阈值',
+ },
+ draft: {
+ core_strategy: '主动承担型叙事,CEO 第一人称,T+2h 内首发声明',
+ key_messages: null,
+ audience_map: '直接受损用户优先,私域触达 + KOL 独家简报',
+ risk_warnings: null,
+ action_plan: null,
+ },
+ },
+ thinking:
+ '启动阶段各维度已基本就位。叙事师和受众师的框架高度吻合,渠道策略也清晰。接下来最需要深化的是风险时间轴的精确度和 CEO 声明的具体文本。',
+ },
+
+ // ─── 辩论阶段 第 1 轮 ─────────────────────────────
+ { type: 'round_start', round: 2, phase: 'diverging' },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'narrative',
+ round: 2,
+ phase: 'diverging',
+ data: {
+ thinking: '需要给出一个实际可用的 CEO 声明草稿。200 字以内,需要包含:承认事实、表达歉意、说明措施、给出补偿方案。',
+ content:
+ '## CEO 声明草稿(200 字)\n\n---\n\n> 我代表公司,向所有受影响的用户致以最诚挚的歉意。\n>\n> [日期],我们发现系统存在安全漏洞,部分用户数据遭到未经授权的访问,涉及用户约 500 万。我们在确认情况后第一时间启动了应急响应,已封堵漏洞,并向监管部门报告。\n>\n> 我们将在 24 小时内通过站内信向每位受影响用户发出个人通知,提供免费的一年身份保护服务,并设立专项补偿通道。\n>\n> 这是我们的责任。我们会把每一步进展告诉你。\n\n---\n\n**注**:声明中故意不使用"技术故障"措辞,也不使用"不可抗力",全程第一人称承担。',
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'risk',
+ round: 2,
+ phase: 'diverging',
+ data: {
+ content:
+ '## 风险时间轴(精化版)\n\n```\nT+0h ── 事件媒体首报(已触发)\nT+2h ── 官方声明发布窗口(最晚截止)\nT+4h ── 微博热搜触发阈值(~5000 评论)\nT+6h ── KOL 立场形成(此后成本 ×3)\nT+12h ── 情绪峰值预测(负面评论密度最高)\nT+18h ── 监管询问概率 80%(需准备合规版声明)\nT+24h ── 集体诉讼舆论造势窗口\nT+48h ── 舆情自然衰减期(若前期处置得当)\n```\n\n**最关键节点**:T+2h 声明发布。每延迟 1 小时,负面情绪扩散系数约增加 0.3σ。\n\n**法务建议**:声明中"约 500 万用户"措辞需法务确认,避免后续数字出入引发新一轮信任危机。',
+ },
+ },
+
+ {
+ type: 'coordinator',
+ data: {
+ divergeRound: 1,
+ totalDivergeRounds: 3,
+ nextSpeakers: ['audience', 'channel'],
+ focus: {
+ audience: '给出分层受众的具体触达话术,特别是直接受损用户的私域文案',
+ channel: '细化 T+0 至 T+6h 各渠道的具体执行 SOP,要有责任人和时间节点',
+ },
+ draft: {
+ core_strategy: '主动承担型叙事,CEO 第一人称署名,T+2h 发出首版声明',
+ key_messages:
+ '第一条:承认事实 + 道歉;第二条:已采取措施;第三条:补偿方案;第四条:持续透明沟通承诺',
+ audience_map: '直接受损用户(私域专属通知)> 媒体 KOL(独家简报)> 公域大众(官方声明)',
+ risk_warnings:
+ 'T+2h 声明是关键节点;T+6h KOL 立场冻结;T+18h 监管接触概率高',
+ action_plan: null,
+ },
+ },
+ thinking:
+ '叙事师的 CEO 草稿质量很高,风险师的时间轴也很精确。现在需要补全受众触达的具体话术和渠道 SOP,才能形成可执行方案。',
+ },
+
+ // ─── 辩论阶段 第 2 轮 ─────────────────────────────
+ { type: 'round_start', round: 3, phase: 'diverging' },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'audience',
+ round: 3,
+ phase: 'diverging',
+ data: {
+ content:
+ '## 分层触达话术\n\n### 直接受损用户(站内信 / Push)\n\n> 您好,您的账号涉及本次安全事件。我们已完成漏洞修复,您的密码和支付信息**未受影响**。您可以在「设置 > 安全中心」查看详细报告,并领取您的专属保障服务。我们为您提前道歉。\n\n### 未受影响用户(公众号推文)\n\n> 关于近期数据安全事件,我们想向您说清楚……(附常见问题 FAQ,覆盖"我受影响吗""我的数据安全吗""我能做什么"三类高频问题)\n\n### 科技媒体 KOL(邮件 / 微信)\n\n> 提供独家:1) 技术修复细节简报;2) 受影响数据字段明细;3) CEO 电话采访机会(限 3 家)',
+ dimensionOutput: {
+ 直接受损用户核心诉求: '我的信息具体泄露了什么?我怎么保护自己?我能得到什么补偿?',
+ 最高情绪风险点: '若发现公司早知道却没告知,信任崩溃将不可逆',
+ 'KOL 沟通关键': '给技术细节,不给"公关说辞",换取客观报道',
+ },
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'channel',
+ round: 3,
+ phase: 'diverging',
+ data: {
+ content:
+ '## 渠道执行 SOP(T+0 ~ T+6h)\n\n| 时间 | 渠道 | 动作 | 责任人 |\n|---|---|---|---|\n| T+0.5h | 内部 | 危机小组集结,信息核查 | CEO / PR 总监 |\n| T+1h | 法务 | 声明文本法务审核 | 法务负责人 |\n| T+1.5h | 媒体 | 独家简报发送至核心媒体 | PR 经理 |\n| T+2h | 官方微博 / 公众号 | 首版声明发布 | PR 总监 |\n| T+2h | App Push / 站内信 | 受损用户专属通知推送 | 产品负责人 |\n| T+3h | KOL 微信群 | 科技 / 隐私类 KOL 定向沟通 | KOL 运营 |\n| T+4h | 客服热线 | 扩容至 2× 正常容量 | 客服总监 |\n| T+6h | 官方微博 | 第二轮进展更新 | PR 总监 |\n\n**备注**:声明发出前严格管控内部信息,防止员工个人账号提前扩散造成口径不一。',
+ },
+ },
+
+ {
+ type: 'coordinator',
+ data: {
+ divergeRound: 2,
+ totalDivergeRounds: 3,
+ nextSpeakers: ['narrative', 'risk', 'audience', 'channel'],
+ focus: {
+ narrative: '结合受众话术,给出完整的多版本话术库(含 FAQ 20 条)',
+ risk: '补充监管应对预案和集体诉讼舆论防御策略',
+ audience: '量化各渠道触达转化率预期,给出效果评估指标',
+ channel: '补充 T+6h 之后的渠道持续运营节奏(T+24h、T+48h)',
+ },
+ draft: {
+ core_strategy: '主动承担型叙事,CEO 第一人称署名,T+2h 发出首版声明,配合多渠道协同执行',
+ key_messages:
+ '① 承认事实 + 道歉;② 已修复 + 措施说明;③ 补偿方案(1 年身份保护服务);④ 持续透明沟通承诺',
+ audience_map:
+ '直接受损用户(专属 Push/站内信)→ 科技媒体 KOL(独家简报)→ 公域大众(官方声明 + FAQ)',
+ risk_warnings:
+ '关键节点:T+2h 声明、T+6h KOL 立场、T+18h 监管询问。红线:不否认规模、不质疑用户、不推卸技术责任',
+ action_plan: '见渠道 SOP;T+2h 首轮声明,T+6h 进展更新,T+24h 详细报告,T+48h 补偿通道上线',
+ },
+ },
+ thinking:
+ '方案已基本成型,所有五个报告字段都有实质内容。第三轮将进行质量收敛,特别是风险监管预案和效果指标的补全。',
+ },
+
+ // ─── 辩论阶段 第 3 轮 ─────────────────────────────
+ { type: 'round_start', round: 4, phase: 'diverging' },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'risk',
+ round: 4,
+ phase: 'diverging',
+ data: {
+ content:
+ '## 监管应对预案\n\n### 合规声明独立版本\n需准备区别于公众声明的**监管版声明**,包含:\n- 精确影响范围(字段级别)\n- 漏洞技术原因(可向监管披露的版本)\n- 已通知/将通知的时间节点\n- 补救措施完成时间承诺\n\n### 集体诉讼舆论防御\n- 避免"私了"暗示:公开补偿方案优先于私下协商\n- 主动设立补偿通道比等待诉讼更能降低法律风险\n- 关键话术:**"我们的补偿方案对每位用户平等有效,无需诉讼即可申请"**',
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'audience',
+ round: 4,
+ phase: 'diverging',
+ data: {
+ content:
+ '## 效果评估指标体系\n\n| 指标 | 目标值 | 监测节点 |\n|---|---|---|\n| 微博话题情绪负向比 | < 50%(当前 67%) | T+6h、T+12h、T+24h |\n| 官方声明互动率 | > 3%(转发+评论/阅读) | T+4h |\n| 受损用户 App 卸载率 | < 5%(7 日内) | T+168h |\n| KOL 正向报道占比 | > 60% | T+24h |\n| 客服解决率 | > 85% | T+48h |\n\n**预警机制**:若 T+6h 负向情绪比仍 > 60%,需触发 Plan B(CEO 视频道歉 + 补偿方案提级)。',
+ },
+ },
+
+ {
+ type: 'coordinator',
+ data: {
+ divergeRound: 3,
+ totalDivergeRounds: 3,
+ nextSpeakers: [],
+ focus: {},
+ draft: {
+ core_strategy:
+ '主动承担型叙事,CEO 第一人称署名,T+2h 发出首版声明,配合多渠道协同执行,核心定调:承认→道歉→修复→补偿→持续透明',
+ key_messages:
+ '① 承认事实 + 真诚道歉;② 已修复漏洞 + 措施说明;③ 一年免费身份保护服务;④ 24h 内个人通知;⑤ 持续透明沟通承诺',
+ audience_map:
+ '第一梯队:直接受损 500 万用户(专属 Push/站内信);第二梯队:科技媒体 KOL(独家简报 + CEO 采访);第三梯队:公域大众(官方声明 + FAQ)',
+ risk_warnings:
+ '关键节点:T+2h 声明(最高优先)、T+6h KOL 立场冻结、T+18h 监管询问(概率 80%)。Plan B 触发条件:T+6h 负向情绪 > 60%。红线三条:不否认规模、不质疑用户、不推卸技术责任',
+ action_plan:
+ 'T+0.5h 危机小组集结 → T+1h 法务审核 → T+1.5h 媒体独家简报 → T+2h 官方声明 + Push → T+3h KOL 定向沟通 → T+6h 第二轮更新 → T+24h 详细报告 → T+48h 补偿通道上线',
+ },
+ },
+ thinking: '三轮辩论已完成,所有字段已确认,方案逻辑自洽。进入收敛阶段生成最终报告。',
+ },
+
+ // ─── 收敛阶段 ─────────────────────────────────────
+ { type: 'round_start', round: 5, phase: 'closing' },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'narrative',
+ round: 5,
+ phase: 'closing',
+ data: {
+ content:
+ '## 最终确认:叙事框架\n\n完整话术库已整理,包含:\n- CEO 主声明(200 字正式版)\n- 用户 FAQ(20 条,覆盖 95% 预期问题)\n- KOL 独家简报模板\n- 监管合规声明模板\n\n所有版本均已通过"不否认、不推卸、不沉默"三条红线审核。',
+ },
+ },
+
+ {
+ type: 'agent_contribution',
+ agentId: 'channel',
+ round: 5,
+ phase: 'closing',
+ data: {
+ content:
+ '## 最终确认:渠道执行\n\nSOP 已锁定,T+0 ~ T+48h 全流程节点清晰,责任人到位。补充:T+72h 需发布第三方安全审计结果公告,进一步夯实信任修复。',
+ },
+ },
+
+ // ─── 最终报告 ──────────────────────────────────────
+ {
+ type: 'final_report',
+ data: {
+ report: {
+ core_strategy:
+ '**核心定调:主动承担,用户优先**\n\n本次危机响应采用"危机—承担—改变"三段式叙事结构,以 CEO 第一人称署名声明为核心,在媒体首报后 2 小时内完成官方表态,抢占舆论定性窗口。\n\n核心原则:\n- **承认事实**:不回避泄露规模,不使用"技术故障"等推卸性措辞\n- **承担责任**:以企业主体第一人称,而非被动语态\n- **行动优先**:补偿方案与声明同步发出,不事后补充\n- **持续透明**:承诺 24/48/72 小时三轮进展更新',
+ key_messages:
+ '**核心话术五条(按优先级)**\n\n1. **道歉承担**:"我们深感抱歉,这不应该发生,这是我们的责任。"\n2. **已采取行动**:"漏洞已于 [时间] 修复完毕,您的密码和支付信息未受影响。"\n3. **补偿承诺**:"我们将为每位受影响用户提供一年免费身份保护服务,并设立专项补偿通道。"\n4. **个人通知**:"我们将在 24 小时内通过站内信向每位受影响用户发出个人通知。"\n5. **透明承诺**:"我们会把每一步进展告诉你,包括监管沟通结果和第三方审计报告。"',
+ audience_map:
+ '**受众分层触达策略**\n\n**第一梯队:直接受损用户(约 500 万)**\n- 触达渠道:App Push + 站内信(T+2h 同步发出)\n- 核心内容:个人影响说明 + 补偿申请入口 + 自保指南\n- 效果指标:7 日卸载率 < 5%\n\n**第二梯队:科技媒体 & 隐私 KOL(< 500 人)**\n- 触达渠道:邮件 / 微信独家简报(T+1.5h),CEO 采访机会(限 3 家)\n- 核心内容:技术细节简报(非公关口径)+ 后续透明节奏\n- 效果指标:T+24h 正向报道占比 > 60%\n\n**第三梯队:公域大众**\n- 触达渠道:官方微博 / 公众号 + FAQ 长图文\n- 效果指标:T+12h 情绪负向比 < 50%',
+ risk_warnings:
+ '**关键风险节点与应对预案**\n\n| 时间节点 | 风险 | 触发阈值 | 预案 |\n|---|---|---|---|\n| T+2h | 声明延迟 | 未能按时发出 | 临时简短声明"正在调查,1h内更新" |\n| T+6h | KOL 立场冻结 | 负向情绪 > 60% | 触发 Plan B:CEO 视频道歉 |\n| T+18h | 监管询问 | 概率 80% | 合规版声明已备,法务直接响应 |\n| T+24h | 集体诉讼舆论 | 法律类话题进热搜 | 公开补偿通道,阻断诉讼动机 |\n\n**绝对红线(不可触碰):**\n- ❌ 否认或缩小泄露规模\n- ❌ 质疑用户截图或举报真实性\n- ❌ 以技术复杂性规避问责',
+ action_plan:
+ '**48 小时行动计划**\n\n```\nT+0.5h 危机小组集结(CEO / PR总监 / 法务 / 产品)\nT+1h 声明文本完成法务审核\nT+1.5h 核心媒体独家简报发出(3家)\nT+2h ★ 官方声明发布(微博 + 公众号)\n + App Push / 站内信 推送受损用户\nT+3h KOL 微信定向沟通(20人名单)\nT+4h 客服热线扩容至 2× 正常容量\nT+6h 第二轮进展更新发布\nT+18h 监管询问响应准备就位\nT+24h 详细技术说明 + 影响范围报告发布\nT+48h 专项补偿通道正式上线\nT+72h 第三方安全审计启动公告\n```',
+ },
+ dimensions: {
+ 叙事框架: {
+ label: '叙事分析师',
+ latest_analysis:
+ '采用"危机承担型"框架,CEO 署名,200 字主声明 + 话术库覆盖全场景。关键洞察:补偿与声明同步发出可降低 31% 愤怒情绪。',
+ },
+ 受众分析: {
+ label: '受众洞察师',
+ latest_analysis:
+ '直接受损用户为最高优先级(隐私敏感度 8.3/10),KOL 态度在 6h 内冻结。分层触达策略已量化,7 日卸载率目标 < 5%。',
+ },
+ 风险研判: {
+ label: '风险评估师',
+ latest_analysis:
+ 'T+2h 为最关键窗口,每延迟 1h 负面情绪扩散系数增加 0.3σ。T+18h 监管询问概率 80%,合规预案已备。',
+ },
+ 渠道传播: {
+ label: '渠道策略师',
+ latest_analysis:
+ '五路渠道协同(官方微博/公众号、App Push、媒体简报、KOL 沟通、客服扩容),SOP 已锁定到人到时间。',
+ },
+ },
+ meta: {
+ total_rounds: 5,
+ diverge_rounds: 3,
+ agent_count: 4,
+ },
+ },
+ },
+];
+
+export const MOCK_SESSION = {
+ taskContext: MOCK_TASK_CONTEXT,
+ status: 'waiting_human',
+ phase: 'diverging',
+ coordinatorMode: 'human_review',
+ currentRound: 4,
+ divergeRounds: 3,
+ reportSchema: REPORT_SCHEMA,
+ agents: AGENTS,
+ agentStates: AGENT_STATES,
+ timeline: TIMELINE,
+ sharedMemory: SHARED_MEMORY,
+ finalReportPartial: {
+ core_strategy: '主动承担型叙事,CEO 第一人称署名,T+2h 发出首版声明',
+ key_messages: '① 道歉承担;② 已修复;③ 补偿方案;④ 个人通知;⑤ 透明承诺',
+ audience_map: '直接受损用户(私域)> 媒体 KOL(独家)> 公域大众(声明)',
+ risk_warnings: 'T+2h 声明关键节点;Plan B 触发条件;三条红线',
+ action_plan: 'T+2h 声明 → T+6h 更新 → T+24h 报告 → T+48h 补偿通道',
+ },
+ currentFinalField: null,
+ pendingReview: {
+ divergeRound: 2,
+ totalDivergeRounds: 3,
+ nextSpeakers: ['narrative', 'risk'],
+ focus: {
+ narrative: '深化叙事结构,给出 CEO 声明草稿(200 字以内),避免"技术故障"措辞',
+ risk: '量化各风险节点的时间窗口和触发阈值,补充监管应对预案',
+ },
+ draft: {
+ core_strategy: '主动承担型叙事,CEO 第一人称署名,T+2h 发出首版声明',
+ key_messages: '① 道歉承担;② 已修复;③ 补偿方案;④ 个人通知;⑤ 透明承诺',
+ audience_map: '直接受损用户(私域)> 媒体 KOL(独家)> 公域大众(声明)',
+ risk_warnings: null,
+ action_plan: null,
+ },
+ gaps: [
+ 'CEO 是否愿意出面公开致歉尚未确认,影响叙事策略选择',
+ '泄露数据的具体字段范围尚未完全核查,影响话术精准度',
+ '监管是否已介入需在声明前确认,需准备合规版本',
+ ],
+ },
+ reviewSubmitting: false,
+ reviewRevising: false,
+ errors: [],
+ startSession: () => {},
+ stopSession: () => {},
+ approveCoordinator: async () => {},
+ reviseCoordinator: async () => {},
+ hydrateFromSnapshot: () => {},
+ reset: () => {},
+};
diff --git a/frontend-web/src/strategy-components/components/qa/MopeChat.tsx b/frontend-web/src/strategy-components/components/qa/MopeChat.tsx
new file mode 100644
index 00000000..abc48e34
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/MopeChat.tsx
@@ -0,0 +1,309 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+import { useSession } from '@/hooks/useSession';
+import { useTheme } from '../../../contexts/ThemeContext';
+import ConfigPanel from './components/ConfigPanel';
+import Sidebar from './components/Sidebar';
+import Timeline from './components/Timeline';
+import ProgressSidebar from './components/ProgressSidebar';
+import ScenarioConfigPage from './components/ScenarioConfigPage';
+import './MopeChat.css';
+
+type StartConfig = {
+ task_context?: string;
+ [key: string]: any;
+};
+
+type SessionError = {
+ agent_id?: string;
+ error?: string;
+};
+
+type SessionHistoryPayload = {
+ typeName: string;
+ templateName: string;
+ resultJson: Record
;
+};
+
+const MOPE_HISTORY_API_BASE = '/api/mope/history';
+
+/**
+ * 保存模型完整生成结果。
+ *
+ * 接口暂未提供,先把调用方法和请求体结构固定下来:
+ * - typeName:类型名字,目前使用用户输入的任务背景。
+ * - templateName:用户选择的生成模板名称。
+ * - resultJson:完整结果 JSON,包含 timeline、agentStates、reportSchema、finalReport 等恢复页面所需数据。
+ */
+async function saveMopeHistory(payload: SessionHistoryPayload) {
+ const response = await fetch(MOPE_HISTORY_API_BASE, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ throw new Error(`保存 MOPE 历史失败:HTTP ${response.status}`);
+ }
+
+ return response.json().catch(() => null);
+}
+
+/**
+ * 获取当前用户最近一次 MOPE 生成历史。
+ *
+ * 接口暂未提供,先约定返回值就是保存时的 resultJson。
+ * 如果接口没有历史数据,应返回 null / 空对象 / 404,页面会保持当前未生成状态。
+ */
+async function fetchMopeHistory(): Promise | null> {
+ const response = await fetch(MOPE_HISTORY_API_BASE, { method: 'GET' });
+
+ if (response.status === 404 || response.status === 204) return null;
+ if (!response.ok) {
+ throw new Error(`获取 MOPE 历史失败:HTTP ${response.status}`);
+ }
+
+ const data = await response.json();
+ return data?.resultJson || data || null;
+}
+
+export default function MopeChat() {
+ const session = useSession() as any;
+ const { theme } = useTheme() as { theme?: string };
+ const isDark = theme === 'dark';
+ const [taskContext, setTaskContext] = useState('');
+ const [idlePage, setIdlePage] = useState('home');
+ const [isLeftSidebarCollapsed, setIsLeftSidebarCollapsed] = useState(false);
+ const [isRightSidebarCollapsed, setIsRightSidebarCollapsed] = useState(false);
+
+ const {
+ status,
+ phase,
+ coordinatorMode,
+ currentRound,
+ divergeRounds,
+ reportSchema,
+ agents,
+ agentStates,
+ timeline,
+ sharedMemory,
+ finalReportPartial,
+ currentFinalField,
+ pendingReview,
+ reviewSubmitting,
+ reviewRevising,
+ errors,
+ startSession,
+ stopSession,
+ approveCoordinator,
+ reviseCoordinator,
+ hydrateFromSnapshot,
+ reset,
+ } = session;
+
+ const historyLoadedRef = useRef(false);
+ const savedHistoryKeyRef = useRef(null);
+ const currentConfigRef = useRef(null);
+
+ const buildHistorySnapshot = useCallback(() => ({
+ status,
+ phase,
+ coordinatorMode,
+ currentRound,
+ divergeRounds,
+ reportSchema,
+ agents,
+ agentStates,
+ timeline,
+ sharedMemory,
+ finalReportPartial,
+ currentFinalField,
+ config: currentConfigRef.current,
+ }), [
+ status,
+ phase,
+ coordinatorMode,
+ currentRound,
+ divergeRounds,
+ reportSchema,
+ agents,
+ agentStates,
+ timeline,
+ sharedMemory,
+ finalReportPartial,
+ currentFinalField,
+ ]);
+
+ const handleStart = (config: StartConfig, backendUrl: string) => {
+ currentConfigRef.current = config;
+ savedHistoryKeyRef.current = null;
+ setTaskContext(config.task_context || '');
+ startSession(config, backendUrl);
+ };
+
+ const handleStop = () => {
+ stopSession();
+ };
+
+ const handleReset = () => {
+ reset();
+ currentConfigRef.current = null;
+ savedHistoryKeyRef.current = null;
+ setTaskContext('');
+ };
+
+ useEffect(() => {
+ if (historyLoadedRef.current) return;
+ historyLoadedRef.current = true;
+
+ void fetchMopeHistory()
+ .then((snapshot) => {
+ if (!snapshot || !Array.isArray(snapshot.timeline) || snapshot.timeline.length === 0) return;
+ currentConfigRef.current = snapshot.config || null;
+ setTaskContext(snapshot.config?.task_context || snapshot.typeName || '');
+ hydrateFromSnapshot(snapshot);
+ })
+ .catch((error) => {
+ console.warn('MOPE 历史记录获取失败,保持默认空状态:', error);
+ });
+ }, [hydrateFromSnapshot]);
+
+ useEffect(() => {
+ if (status !== 'done' || !timeline?.length) return;
+ const snapshot = buildHistorySnapshot();
+ const templateName = currentConfigRef.current?.template_name || '未命名模板';
+ const typeName = taskContext || currentConfigRef.current?.task_context || '未命名任务';
+ const saveKey = `${typeName}::${templateName}::${timeline.length}`;
+ if (savedHistoryKeyRef.current === saveKey) return;
+ savedHistoryKeyRef.current = saveKey;
+
+ void saveMopeHistory({
+ typeName,
+ templateName,
+ resultJson: {
+ ...snapshot,
+ typeName,
+ templateName,
+ savedAt: new Date().toISOString(),
+ },
+ }).catch((error) => {
+ console.warn('MOPE 历史记录保存失败:', error);
+ savedHistoryKeyRef.current = null;
+ });
+ }, [status, timeline, taskContext, buildHistorySnapshot]);
+
+ if (status === 'idle') {
+ if (idlePage === 'scenario_config') {
+ return (
+
+ setIdlePage('home')} />
+
+ );
+ }
+
+ return (
+
+ setIdlePage('scenario_config')}
+ />
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {errors.length > 0 && errors.map((e: SessionError, i: number) => (
+
+ {e.agent_id ? `[${e.agent_id}] ` : ''}{e.error}
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/frontend-web/src/strategy-components/components/qa/QA.tsx b/frontend-web/src/strategy-components/components/qa/QA.tsx
new file mode 100644
index 00000000..8581f91b
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/QA.tsx
@@ -0,0 +1,2184 @@
+import React, { useState, useRef, useEffect } from 'react';
+import {
+ Send,
+ Upload,
+ RotateCcw,
+ Bookmark,
+ History,
+ Sparkles,
+ BookOpen,
+ Brain,
+ GitCompare,
+ MessageSquare,
+ FileText,
+ CheckCircle,
+ AlertCircle,
+ Bot,
+ Database,
+ Layers,
+ Cpu,
+ Target,
+ Search,
+ CircleHelp,
+ ChevronRight,
+ Plus,
+ X,
+ Copy,
+ RefreshCw,
+ ArrowRight,
+ ArrowLeft,
+ Download,
+ Save,
+ ChevronDown,
+ ChevronUp,
+ MessageCircle,
+ Users,
+ FileCheck,
+ AlertTriangle,
+ Handshake,
+ Edit3,
+ ListChecks,
+ Play,
+ Pause,
+ UserCheck,
+ UserX,
+ Hash,
+ Clock,
+ Zap,
+ ClipboardList,
+ TrendingUp,
+} from 'lucide-react';
+import { useTheme } from '../../../contexts/ThemeContext';
+import { useChatContext } from '../../../contexts/ChatContext';
+import QATypeSwitch from './QATypeSwitch';
+
+type QAMode = 'single-round' | 'multi-round' | 'deep-search';
+type QAProvider = 'general' | 'agent' | 'multi-agent';
+type ViewMode = 'single' | 'compare';
+
+interface Agent {
+ id: string;
+ name: string;
+ category: string;
+ tags: string[];
+ description: string;
+ skills: string[];
+ dataSources: string[];
+}
+
+interface Message {
+ id: string;
+ role: 'user' | 'assistant';
+ content: string;
+ timestamp: Date;
+ sources?: SourceItem[];
+ agentName?: string;
+ skills?: string[];
+ dataSources?: string[];
+ functions?: string[];
+}
+
+interface SourceItem {
+ title: string;
+ type: string;
+ snippet: string;
+}
+
+interface CompareResult {
+ leftMode: QAMode;
+ rightMode: QAMode;
+ leftContent: string;
+ rightContent: string;
+ differences?: {
+ consensus: string;
+ differences: string;
+ leftAdvantage: string;
+ rightAdvantage: string;
+ };
+}
+
+// 多智能体协同分析相关类型
+interface AgentResponse {
+ id: string;
+ agentId: string;
+ agentName: string;
+ agentCategory: string;
+ status: 'pending' | 'responding' | 'completed' | 'adopted' | 'pending-confirm';
+ coreJudgment: string;
+ content: string;
+ structuredSummary?: {
+ key: string;
+ value: string;
+ }[];
+ timestamp: Date;
+}
+
+// 多智能体消息类型
+interface MultiAgentMessage {
+ id: string;
+ type: 'user' | 'agent';
+ agentName?: string;
+ content: string;
+ timestamp: Date;
+ isFollowUp?: boolean;
+ parentMessageId?: string;
+}
+
+interface AdoptedView {
+ id: string;
+ agentName: string;
+ content: string;
+ timestamp: Date;
+}
+
+interface PendingQuestion {
+ id: string;
+ content: string;
+ relatedAgents: string[];
+ timestamp: Date;
+}
+
+interface SummaryDraft {
+ title: string;
+ sections: {
+ title: string;
+ content: string;
+ }[];
+}
+
+// 场景模板类型
+interface SceneTemplate {
+ id: string;
+ name: string;
+ type: string;
+ description: string;
+ agentIds: string[];
+}
+
+// 场景模板数据
+const sceneTemplates: SceneTemplate[] = [
+ {
+ id: 'scene-1',
+ name: '宣传策略生成',
+ type: '策略生成',
+ description: '生成完整的品牌传播策略方案',
+ agentIds: ['goal-agent', 'audience-agent', 'brand-agent', 'channel-agent', 'content-agent', 'narrative-agent', 'behavior-agent', 'risk-agent'],
+ },
+ {
+ id: 'scene-2',
+ name: '宣传方案推演',
+ type: '方案推演',
+ description: '对现有方案进行多角度推演验证',
+ agentIds: ['goal-agent', 'audience-agent', 'behavior-agent', 'risk-agent', 'content-agent', 'channel-agent'],
+ },
+ {
+ id: 'scene-3',
+ name: '宣传活动复盘',
+ type: '效果复盘',
+ description: '对宣传活动效果进行多维度复盘分析',
+ agentIds: ['goal-agent', 'audience-agent', 'behavior-agent', 'channel-agent', 'risk-agent'],
+ },
+ {
+ id: 'scene-4',
+ name: '舆情应对策略',
+ type: '风险应对',
+ description: '制定舆情风险应对和处置策略',
+ agentIds: ['risk-agent', 'audience-agent', 'brand-agent', 'content-agent', 'narrative-agent', 'channel-agent', 'behavior-agent'],
+ },
+ {
+ id: 'scene-5',
+ name: '多版本策略比较',
+ type: '策略对比',
+ description: '对比分析多个策略方案的优劣',
+ agentIds: ['goal-agent', 'audience-agent', 'brand-agent', 'content-agent'],
+ },
+];
+
+// 智能体列表 - 按五大类别分类
+const agents: Agent[] = [
+ // 规划类智能体
+ {
+ id: 'goal-agent',
+ name: 'GoalAgent',
+ category: '规划类',
+ tags: ['目标拆解', 'KPI设定', '优先级'],
+ description: '专业的目标规划智能体,能够帮助拆解任务目标并设定可衡量的KPI',
+ skills: ['目标拆解', 'KPI设定', '优先级排序'],
+ dataSources: ['知识库', '历史数据分析'],
+ },
+ {
+ id: 'brand-agent',
+ name: 'BrandAgent',
+ category: '规划类',
+ tags: ['品牌定位', '传播策略', '调性设计'],
+ description: '专注品牌建设与传播策略的规划智能体',
+ skills: ['品牌定位分析', '传播路径规划', '品牌调性设计'],
+ dataSources: ['知识库', '行业数据库'],
+ },
+ {
+ id: 'content-agent',
+ name: 'ContentAgent',
+ category: '规划类',
+ tags: ['文案撰写', '创意生成', '内容规划'],
+ description: '智能内容创作规划助手,支持多种形式的内容生成规划',
+ skills: ['文案撰写', '创意生成', '多形式内容'],
+ dataSources: ['知识库', '优秀案例库'],
+ },
+ {
+ id: 'channel-agent',
+ name: 'ChannelAgent',
+ category: '规划类',
+ tags: ['渠道选择', '投放策略', '媒介规划'],
+ description: '专业的渠道规划智能体,制定最优渠道组合策略',
+ skills: ['渠道选择', '投放策略规划', '效果评估'],
+ dataSources: ['Y系统', 'A系统', '行业数据'],
+ },
+ {
+ id: 'narrative-agent',
+ name: 'NarrativeAgent',
+ category: '规划类',
+ tags: ['故事线', '情感设计', '叙事策略'],
+ description: '专业的叙事规划智能体,构建有感染力的内容故事线',
+ skills: ['故事线设计', '情感共鸣设计', '内容叙事'],
+ dataSources: ['优秀案例库', '内容分析'],
+ },
+ // MNTY类智能体
+ {
+ id: 'mnty-agent',
+ name: 'MNTYAgent',
+ category: 'MNTY类',
+ tags: ['心智模型', '用户认知', '心智定位'],
+ description: '专注用户心智模型分析,挖掘用户深层认知和心智定位',
+ skills: ['心智模型构建', '认知路径分析', '心智定位设计'],
+ dataSources: ['用户研究数据', '心理学理论库'],
+ },
+ // 效果PG类智能体
+ {
+ id: 'behavior-agent',
+ name: 'BehaviorAgent',
+ category: '效果PG类',
+ tags: ['用户行为', '行为路径', '效果预测'],
+ description: '专注用户行为模式分析,预测和优化用户行为效果',
+ skills: ['用户行为分析', '行为路径设计', '转化优化'],
+ dataSources: ['Y系统', '行为数据'],
+ },
+ {
+ id: 'pg-agent',
+ name: 'PGAgent',
+ category: '效果PG类',
+ tags: ['效果评估', 'PG分析', '数据洞察'],
+ description: '专业的效果评估智能体,提供精准的数据分析和洞察',
+ skills: ['效果评估', '数据可视化', '趋势预测'],
+ dataSources: ['数据分析平台', '监控系统'],
+ },
+ // 知识分析类智能体
+ {
+ id: 'audience-agent',
+ name: 'AudienceAgent',
+ category: '知识分析类',
+ tags: ['受众分层', '画像抽取', '知识分析'],
+ description: '专业的受众知识分析智能体,识别目标受众特征并生成画像',
+ skills: ['受众分层', '核心受众识别', '画像抽取'],
+ dataSources: ['知识库', 'Y系统', 'A系统'],
+ },
+ {
+ id: 'knowledge-agent',
+ name: 'KnowledgeAgent',
+ category: '知识分析类',
+ tags: ['知识图谱', '语义分析', '关联挖掘'],
+ description: '专业的知识分析智能体,构建知识图谱并进行深度语义分析',
+ skills: ['知识图谱构建', '语义分析', '关联挖掘'],
+ dataSources: ['知识库', '文献数据库'],
+ },
+ // 协同管理类智能体
+ {
+ id: 'risk-agent',
+ name: 'RiskAgent',
+ category: '协同管理类',
+ tags: ['风险识别', '预案设计', '协同管理'],
+ description: '专注风险识别与协同管理的智能体,制定完善的应急预案',
+ skills: ['风险识别', '影响评估', '预案设计'],
+ dataSources: ['历史案例', '风险数据库'],
+ },
+ {
+ id: 'collab-agent',
+ name: 'CollabAgent',
+ category: '协同管理类',
+ tags: ['协同调度', '资源协调', '进度管理'],
+ description: '专业的协同管理智能体,统筹调度多方资源协调推进',
+ skills: ['协同调度', '资源协调', '进度管理'],
+ dataSources: ['任务管理系统', '资源数据库'],
+ },
+];
+
+interface QAProps {
+ initialMode?: 'new' | 'general' | 'agent' | 'multi-agent';
+}
+
+const QA: React.FC = ({ initialMode }) => {
+ const { theme } = useTheme();
+ const { addMessage } = useChatContext();
+ const isDark = theme === 'dark';
+
+ // 状态管理
+ const [viewMode, setViewMode] = useState('single');
+ const [qaProvider, setQaProvider] = useState('general');
+ const [qaMode, setQaMode] = useState('single-round');
+ const [selectedAgent, setSelectedAgent] = useState(agents[0]);
+ const [compareLeftMode, setCompareLeftMode] = useState('single-round');
+ const [compareRightMode, setCompareRightMode] = useState('deep-search');
+
+ // 新建问答确认对话框
+ const [showNewQADialog, setShowNewQADialog] = useState(false);
+
+ // 处理 initialMode - 新建问答时清空会话
+ useEffect(() => {
+ if (initialMode === 'new') {
+ // 新建问答模式:清空会话,保持通用问答模式
+ setMessages([]);
+ setCompareResult(null);
+ setInputValue('');
+ setQaProvider('general');
+ setViewMode('single');
+ } else if (initialMode === 'agent') {
+ // 智能体问答模式
+ setQaProvider('agent');
+ } else if (initialMode === 'general') {
+ // 通用问答模式
+ setQaProvider('general');
+ } else if (initialMode === 'multi-agent') {
+ // 多智能体协同模式
+ setQaProvider('multi-agent');
+ setMessages([]);
+ setCompareResult(null);
+ setInputValue('');
+ }
+ }, [initialMode]);
+
+ // 知识范围
+ const [knowledgeScope, setKnowledgeScope] = useState<'all' | 'specific-kb' | 'specific-domain'>('all');
+
+ // 回答偏好
+ const [showSources, setShowSources] = useState(true);
+ const [showProcess, setShowProcess] = useState(false);
+
+ // 对比设置
+ const [compareLayout, setCompareLayout] = useState<'horizontal' | 'vertical'>('horizontal');
+ const [autoSummarizeDiff, setAutoSummarizeDiff] = useState(true);
+ const [recommendBetter, setRecommendBetter] = useState(true);
+
+ // 消息和结果
+ const [messages, setMessages] = useState([]);
+ const [inputValue, setInputValue] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [compareResult, setCompareResult] = useState(null);
+ const [agentSearchQuery, setAgentSearchQuery] = useState('');
+
+ // 检索过程展示
+ const [searchProcess, setSearchProcess] = useState({
+ currentMode: '单轮RAG',
+ rounds: 1,
+ hits: 5,
+ queryRewrite: '',
+ });
+
+ // 多智能体会商状态
+ const [discussionStatus, setDiscussionStatus] = useState<'idle' | 'in_progress' | 'paused' | 'completed'>('idle');
+ const [currentRound, setCurrentRound] = useState(1);
+ const [currentTopic, setCurrentTopic] = useState('五四青年节宣传方向讨论');
+ const [speakingMode, setSpeakingMode] = useState<'broadcast' | 'targeted' | 'response'>('broadcast');
+ const [selectedTargetAgent, setSelectedTargetAgent] = useState(null);
+
+ // 共识与分歧
+ const [consensusItems, setConsensusItems] = useState<{id: string; content: string; agents: string[]}[]>([]);
+ const [disagreementItems, setDisagreementItems] = useState<{id: string; content: string; agents: string[]; details: string}[]>([]);
+ const [conclusionDraft, setConclusionDraft] = useState('');
+
+ // 消息操作状态
+ const [messageAction, setMessageAction] = useState<'none' | 'adopt' | 'disagree' | 'followup' | 'respond'>('none');
+ const [selectedMessageId, setSelectedMessageId] = useState(null);
+
+ // 原有状态(保持兼容性)
+ const [selectedMultiAgents, setSelectedMultiAgents] = useState([agents[0], agents[4]]);
+ const [agentResponses, setAgentResponses] = useState([]);
+ const [adoptedViews, setAdoptedViews] = useState([]);
+ const [pendingQuestions, setPendingQuestions] = useState([]);
+ const [summaryDraft, setSummaryDraft] = useState({
+ title: '多智能体联合分析报告',
+ sections: [],
+ });
+ const [taskInfo, setTaskInfo] = useState({
+ name: '五四青年节宣传方向预分析',
+ goal: '形成一版多智能体联合分析报告',
+ status: '讨论中',
+ background: '五四青年节品牌传播活动需要明确目标受众和传播策略',
+ keyQuestions: ['核心受众是谁?', '如何设计参与行为?', '叙事方式如何选择?', '有哪些风险需要关注?'],
+ });
+ const [sendTarget, setSendTarget] = useState<'all' | string>('all');
+ const [multiAgentFilter, setMultiAgentFilter] = useState<'all' | 'completed' | 'pending' | 'adopted'>('all');
+ const [isMultiAgentLoading, setIsMultiAgentLoading] = useState(false);
+ const [multiAgentMessages, setMultiAgentMessages] = useState([]);
+ const [rightPanelTab, setRightPanelTab] = useState<'structured' | 'report'>('structured');
+ const [messageFilter, setMessageFilter] = useState<'all' | 'user' | 'adopted'>('all');
+ const [agentMessageFilter, setAgentMessageFilter] = useState('all');
+ const [messageSort, setMessageSort] = useState<'time' | 'agent'>('time');
+
+ // 左侧菜单折叠状态
+ const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(false);
+ const [qaConfigCollapsed, setQaConfigCollapsed] = useState(false);
+ const [agentListCollapsed, setAgentListCollapsed] = useState(false);
+
+ // 多智能体协同 - 智能体选择状态
+ const [showAgentSelector, setShowAgentSelector] = useState(false);
+ const [showSceneSelector, setShowSceneSelector] = useState(false);
+ const [selectedSceneTemplate, setSelectedSceneTemplate] = useState(null);
+
+ // 过滤智能体
+ const filteredAgents = agents.filter(agent =>
+ agent.name.toLowerCase().includes(agentSearchQuery.toLowerCase()) ||
+ agent.category.toLowerCase().includes(agentSearchQuery.toLowerCase()) ||
+ agent.tags.some(tag => tag.toLowerCase().includes(agentSearchQuery.toLowerCase()))
+ );
+
+ // 多智能体协同分析处理函数
+ const handleMultiAgentSend = () => {
+ if (!inputValue.trim()) return;
+
+ setIsMultiAgentLoading(true);
+ const targets = sendTarget === 'all' ? selectedMultiAgents : selectedMultiAgents.filter(a => a.id === sendTarget);
+
+ // 添加用户消息到消息流
+ const userMessageId = `user-${Date.now()}`;
+ const userMessage: MultiAgentMessage = {
+ id: userMessageId,
+ type: 'user',
+ content: inputValue,
+ timestamp: new Date(),
+ };
+ setMultiAgentMessages(prev => [...prev, userMessage]);
+
+ // 为每个目标智能体创建响应
+ const newResponses: AgentResponse[] = targets.map((agent, idx) => ({
+ id: `${Date.now()}-${agent.id}`,
+ agentId: agent.id,
+ agentName: agent.name,
+ agentCategory: agent.category,
+ status: 'responding' as const,
+ coreJudgment: '分析中...',
+ content: '',
+ timestamp: new Date(),
+ }));
+
+ setAgentResponses(prev => [...prev, ...newResponses]);
+ setInputValue('');
+
+ // 模拟智能体响应
+ setTimeout(() => {
+ const completedResponses = targets.map((agent, idx) => {
+ const responses: Record = {
+ 'audience-agent': {
+ core: '核心受众应优先聚焦18-25岁基层青年群体',
+ content: '从任务背景和活动语境看,核心传播对象应聚焦能够形成参与、表达与扩散的人群,年轻基层群体更符合当前任务目标。该群体活跃于社交媒体,对节日话题有天然共鸣,是实现传播裂变的关键。',
+ summary: [
+ { key: 'core_audience', value: '18-25岁基层青年' },
+ { key: 'secondary_audience', value: '25-30岁职场新人' },
+ { key: 'barriers', value: '表达门槛、品牌距离感' },
+ ],
+ },
+ 'goal-agent': {
+ core: '目标应分为参与层和传播层两层设计',
+ content: '建议将五四青年节活动目标拆解为两个层次:参与层目标是引导青年参与活动并产生内容;传播层目标是鼓励青年主动分享传播,形成话题裂变。两层目标相互关联,传播层目标的达成依赖于参与层的体验设计。',
+ summary: [
+ { key: '参与目标', value: '引导10000+青年参与' },
+ { key: '传播目标', value: '产生5000+UGC内容' },
+ { key: '核心KPI', value: '话题阅读量突破500万' },
+ ],
+ },
+ 'behavior-agent': {
+ core: '用户行为路径应设计为"触发-参与-分享"三阶段',
+ content: '基于对年轻用户行为模式的分析,建议设计"触发-参与-分享"的完整行为路径。触发阶段通过话题挑战赛引发兴趣;参与阶段通过互动任务降低门槛;分享阶段通过激励机制促进主动传播。',
+ summary: [
+ { key: '触发机制', value: '话题挑战赛' },
+ { key: '参与路径', value: '任务闯关+创作征集' },
+ { key: '分享激励', value: '荣誉体系+实物奖励' },
+ ],
+ },
+ 'narrative-agent': {
+ core: '叙事建议采用"真实青年故事"为主线',
+ content: '建议采用纪实风格的叙事策略,讲述真实青年人物的故事,而非虚构的营销叙事。通过真实故事的感染力和共鸣感,提升青年群体对活动的认同和参与意愿。',
+ summary: [
+ { key: '叙事风格', value: '纪实风格' },
+ { key: '故事原型', value: '真实青年人物' },
+ { key: '情感基调', value: '正向、热血、有共鸣' },
+ ],
+ },
+ 'risk-agent': {
+ core: '主要风险在于话题敏感性和UGC内容合规',
+ content: '需要关注两方面风险:一是政治敏感话题的把控,需严格审核所有内容;二是UGC内容可能带来的舆论风险。建议建立完善的内容审核机制和应急预案。',
+ summary: [
+ { key: '风险类型', value: '内容合规风险' },
+ { key: '风险等级', value: '中等' },
+ { key: '应对策略', value: '前置审核+应急预案' },
+ ],
+ },
+ };
+
+ const mockData = responses[agent.id] || {
+ core: '该智能体已完成分析',
+ content: '基于专业能力进行了深入分析。',
+ summary: [{ key: '分析结论', value: '待补充' }],
+ };
+
+ const responseId = `${Date.now()}-${agent.id}`;
+
+ // 添加智能体回答到消息流
+ const agentMessage: MultiAgentMessage = {
+ id: responseId,
+ type: 'agent',
+ agentName: agent.name,
+ content: `${mockData.core}\n\n${mockData.content}`,
+ timestamp: new Date(),
+ };
+ setMultiAgentMessages(prev => [...prev, agentMessage]);
+
+ return {
+ id: responseId,
+ agentId: agent.id,
+ agentName: agent.name,
+ agentCategory: agent.category,
+ status: 'completed' as const,
+ coreJudgment: mockData.core,
+ content: mockData.content,
+ structuredSummary: mockData.summary,
+ timestamp: new Date(),
+ };
+ });
+
+ setAgentResponses(prev => {
+ const existing = prev.filter(r => !targets.find(t => `${Date.now()}-${t.id}`.includes(r.id)));
+ return [...existing, ...completedResponses];
+ });
+ setIsMultiAgentLoading(false);
+ }, 1500);
+ };
+
+ const handleAdoptResponse = (responseId: string) => {
+ const response = agentResponses.find(r => r.id === responseId);
+ if (response) {
+ setAdoptedViews(prev => [...prev, {
+ id: `adopted-${Date.now()}`,
+ agentName: response.agentName,
+ content: response.coreJudgment,
+ timestamp: new Date(),
+ }]);
+ setAgentResponses(prev => prev.map(r =>
+ r.id === responseId ? { ...r, status: 'adopted' as const } : r
+ ));
+ }
+ };
+
+ const handlePendingConfirm = (responseId: string) => {
+ setAgentResponses(prev => prev.map(r =>
+ r.id === responseId ? { ...r, status: 'pending-confirm' as const } : r
+ ));
+ };
+
+ const handleWriteToReport = (responseId: string) => {
+ const response = agentResponses.find(r => r.id === responseId);
+ if (response) {
+ setSummaryDraft(prev => ({
+ ...prev,
+ sections: [...prev.sections, {
+ title: `${response.agentName}分析结论`,
+ content: response.coreJudgment + '\n\n' + response.content,
+ }],
+ }));
+ }
+ };
+
+ const handleGenerateSummary = () => {
+ const summaryPoints = agentResponses
+ .filter(r => r.status === 'completed' || r.status === 'adopted')
+ .map(r => `- ${r.agentName}:${r.coreJudgment}`);
+
+ setSummaryDraft(prev => ({
+ ...prev,
+ sections: [
+ { title: '任务背景', content: taskInfo.name },
+ { title: '参与智能体', content: selectedMultiAgents.map(a => a.name).join('、') },
+ { title: '综合结论', content: summaryPoints.join('\n') },
+ { title: '待确认问题', content: pendingQuestions.map(q => `- ${q.content}`).join('\n') || '无' },
+ ],
+ }));
+ };
+
+ const handleExportReport = () => {
+ alert('报告导出功能开发中...');
+ };
+
+ const handleSaveMultiAgentRecord = () => {
+ alert('协同分析记录已保存');
+ };
+
+ const handleTransferToScene = () => {
+ alert('转场景任务功能开发中...');
+ };
+
+ const handleMultiAgentFollowUp = (agentId: string) => {
+ setSendTarget(agentId);
+ };
+
+ // 添加智能体到协同分析
+ const handleAddAgent = (agent: Agent) => {
+ if (!selectedMultiAgents.find(a => a.id === agent.id)) {
+ setSelectedMultiAgents([...selectedMultiAgents, agent]);
+ }
+ setShowAgentSelector(false);
+ };
+
+ // 从协同分析中移除智能体
+ const handleRemoveAgent = (agentId: string) => {
+ setSelectedMultiAgents(selectedMultiAgents.filter(a => a.id !== agentId));
+ // 同时清除该智能体的响应
+ setAgentResponses(prev => prev.filter(r => r.agentId !== agentId));
+ setMultiAgentMessages(prev => prev.filter(msg => {
+ const response = agentResponses.find(r => r.agentId === agentId);
+ return !response || msg.agentName !== agents.find(a => a.id === agentId)?.name;
+ }));
+ };
+
+ // 应用场景模板
+ const handleApplySceneTemplate = (template: SceneTemplate) => {
+ const templateAgents = agents.filter(a => template.agentIds.includes(a.id));
+ setSelectedMultiAgents(templateAgents);
+ setSelectedSceneTemplate(template);
+ // 清空当前分析数据
+ setAgentResponses([]);
+ setMultiAgentMessages([]);
+ setAdoptedViews([]);
+ setPendingQuestions([]);
+ setSummaryDraft({ title: `${template.name}分析报告`, sections: [] });
+ setTaskInfo({
+ name: template.name,
+ goal: template.description,
+ status: '准备中',
+ background: template.description,
+ keyQuestions: [],
+ });
+ setShowSceneSelector(false);
+ };
+
+ // 获取未选择的智能体列表
+ const availableAgentsForSelection = agents.filter(
+ agent => !selectedMultiAgents.find(a => a.id === agent.id)
+ );
+
+ // 处理新建问答 - 清空会话并切换到指定类型
+ const handleNewQA = (targetProvider?: QAProvider) => {
+ // 清空对话
+ setMessages([]);
+ setCompareResult(null);
+ setInputValue('');
+
+ // 切换到指定类型
+ if (targetProvider) {
+ setQaProvider(targetProvider);
+ }
+ };
+
+ const handleSend = () => {
+ if (!inputValue.trim()) return;
+
+ setIsLoading(true);
+
+ const userMessage: Message = {
+ id: Date.now().toString(),
+ role: 'user',
+ content: inputValue,
+ timestamp: new Date(),
+ };
+
+ // 保存用户消息到聊天记录
+ addMessage('user', inputValue, qaProvider === 'agent' ? 'agent' : 'general', qaProvider === 'agent' ? selectedAgent.name : undefined);
+
+ setMessages([...messages, userMessage]);
+ setInputValue('');
+
+ setTimeout(() => {
+ const responseContent = qaProvider === 'agent'
+ ? `基于${selectedAgent.name}的专业能力,我来为您分析:\n\n根据您的问题,我识别到核心受众特征如下:\n\n**核心受众识别**:\n- 主要年龄段:25-35岁\n- 职业特征:都市白领\n- 消费能力:中高端\n- 关注点:品质生活、个人成长\n\n**关键特质**:\n1. 追求个性化表达\n2. 注重社交认同\n3. 愿意为品质付费\n\n以上分析是否满足您的需求?如需调整维度,请继续补充信息。`
+ : `RAG(检索增强生成)技术的工作原理主要包括以下几个步骤:\n\n1. **检索阶段**:系统首先将用户问题转换为查询向量,在知识库中进行相似度搜索。\n\n2. **增强阶段**:将检索到的相关文档与原始问题组合。\n\n3. **生成阶段**:将增强后的提示词发送给大语言模型。`;
+
+ const response: Message = {
+ id: (Date.now() + 1).toString(),
+ role: 'assistant',
+ content: responseContent,
+ timestamp: new Date(),
+ sources: qaProvider === 'agent' ? undefined : [
+ { title: 'RAG技术白皮书', type: 'PDF', snippet: 'RAG是一种结合检索和生成的技术架构...' },
+ ],
+ agentName: qaProvider === 'agent' ? selectedAgent.name : undefined,
+ skills: qaProvider === 'agent' ? selectedAgent.skills : undefined,
+ dataSources: qaProvider === 'agent' ? selectedAgent.dataSources : undefined,
+ };
+
+ // 保存助手回复到聊天记录
+ addMessage('assistant', responseContent, qaProvider === 'agent' ? 'agent' : 'general', qaProvider === 'agent' ? selectedAgent.name : undefined);
+
+ setMessages(prev => [...prev, response]);
+ setIsLoading(false);
+ setSearchProcess({
+ currentMode: qaMode === 'single-round' ? '单轮RAG' : qaMode === 'multi-round' ? '多轮RAG' : '深度检索',
+ rounds: qaMode === 'multi-round' ? 3 : 1,
+ hits: 5,
+ queryRewrite: qaMode === 'deep-search' ? '原问题→分解子问题→独立检索→综合回答' : '',
+ });
+ }, 1500);
+ };
+
+ const handleCompare = () => {
+ if (!inputValue.trim()) return;
+
+ setIsLoading(true);
+
+ setTimeout(() => {
+ setCompareResult({
+ leftMode: compareLeftMode,
+ rightMode: compareRightMode,
+ leftContent: '单轮RAG回答:直接检索并生成答案,响应速度快,但可能遗漏跨文档的关联信息。适用于简单事实查询。',
+ rightContent: '深度检索回答:通过问题分解、多轮检索和综合分析,提供更全面的答案。包含详细的推理过程和更多的引用来源。',
+ differences: {
+ consensus: '两种方式都使用向量检索技术,都能引用原始文档',
+ differences: '深度检索采用多轮查询改写,覆盖更全面',
+ leftAdvantage: '响应更快,资源消耗更低',
+ rightAdvantage: '答案更完整,引用更充分',
+ },
+ });
+ setIsLoading(false);
+ }, 2000);
+ };
+
+ const handleSaveRecord = () => {
+ alert('问答记录已保存');
+ };
+
+ // 新建问答 - 通用/智能体模式
+ const handleStartNewQA = () => {
+ // 如果有现有消息,弹出确认对话框
+ if (messages.length > 0 || compareResult) {
+ setShowNewQADialog(true);
+ return;
+ }
+
+ // 直接新建 - 清空会话
+ performNewQA();
+ };
+
+ // 执行新建问答
+ const performNewQA = () => {
+ // 清空对话
+ setMessages([]);
+ setCompareResult(null);
+ setInputValue('');
+ setShowNewQADialog(false);
+ };
+
+ // 保存并新建
+ const handleSaveAndNewQA = () => {
+ handleSaveRecord();
+ performNewQA();
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ if (viewMode === 'compare') {
+ handleCompare();
+ } else {
+ handleSend();
+ }
+ }
+ };
+
+ // 获取模式标签
+ const getModeLabel = (mode: QAMode) => {
+ switch (mode) {
+ case 'single-round': return '单轮RAG';
+ case 'multi-round': return '多轮RAG';
+ case 'deep-search': return '深度检索';
+ }
+ };
+
+ // 空状态快捷入口
+ const QuickStartCards = () => (
+
+
+
+
+
+ );
+
+ return (
+
+ {/* 左侧面板 - MiniMax风格 - 可折叠 */}
+
+ {/* 顶部标题 */}
+
+ {!leftPanelCollapsed && (
+
+
+
+ {qaProvider === 'agent' ? '智能体列表' : '问答配置'}
+
+
+ )}
+
+
+
+ {/* 左侧面板折叠内容 */}
+ {!leftPanelCollapsed && (
+ <>
+ {/* 通用问答配置 */}
+ {qaProvider === 'general' && (
+
+ {/* 问答模式选择 - 卡片式 */}
+
+
+ 问答模式
+
+
+ {[
+ { value: 'single-round', label: '单轮RAG', icon: Sparkles, desc: '快速直接' },
+ { value: 'multi-round', label: '多轮RAG', icon: Brain, desc: '深入理解' },
+ { value: 'deep-search', label: '深度检索', icon: BookOpen, desc: '全面分析' },
+ ].map((mode) => (
+
+ ))}
+
+
+
+ {/* 知识范围 */}
+
+
+ 知识范围
+
+
+ {[
+ { value: 'all', label: '全部知识源' },
+ { value: 'specific-kb', label: '指定知识库' },
+ { value: 'specific-domain', label: '指定资源域' },
+ ].map((option) => (
+
+ ))}
+
+
+
+ {/* 回答偏好 */}
+
+
+ 回答偏好
+
+
+ {[
+ { checked: showSources, onChange: () => setShowSources(!showSources), label: '展示引用来源' },
+ { checked: showProcess, onChange: () => setShowProcess(!showProcess), label: '展示检索过程' },
+ ].map((item, idx) => (
+
+ ))}
+
+
+
+ {/* 检索过程 */}
+ {showProcess && messages.length > 0 && (
+
+
+ 检索过程
+
+
+
+ 当前模式
+
+ {searchProcess.currentMode}
+
+
+
+ 检索轮次
+ {searchProcess.rounds}
+
+
+ 命中数
+ {searchProcess.hits}
+
+
+
+ )}
+
+ )}
+
+ {/* 智能体问答配置 - MiniMax单专家风格优化 */}
+ {qaProvider === 'agent' && (
+
+ {/* 简洁的智能体选择区 */}
+
+ {/* 当前选中智能体 - 大卡片突出显示 */}
+
+
+
+
+
+
+
+ {selectedAgent.name}
+
+
+ {selectedAgent.category}
+
+
+
+
+ {selectedAgent.description}
+
+
+
+ {/* 智能体列表 - MiniMax风格网格 */}
+
+
+ 选择其他智能体
+
+
+ {agents.slice(0, 6).map((agent) => (
+
+ ))}
+
+
+
+ {/* 智能体能力标签 - 底部展示 */}
+
+ {/* 技能标签 */}
+
+
+
+ 核心技能
+
+
+ {selectedAgent.skills.slice(0, 4).map((skill, idx) => (
+
+ {skill}
+
+ ))}
+
+
+ {/* 数据源标签 */}
+
+
+
+ 数据源
+
+
+ {selectedAgent.dataSources.slice(0, 3).map((source, idx) => (
+
+ {source}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* 底部按钮 - 所有模式通用 */}
+
+
+
+ >
+ )}
+
+
+ {/* 右侧面板 - 对话区 */}
+
+ {/* 顶部模式切换 */}
+
+
+
+ {/* 右侧 - 新建问答按钮(仅通用/智能体模式显示) */}
+ {qaProvider !== 'multi-agent' && (
+
+ )}
+
+
+ {/* 消息区域 */}
+
+ {/* 多智能体协同分析面板 - 正确设计 */}
+ {qaProvider === 'multi-agent' && (
+
+ {/* 顶部标题栏 - 简洁版 */}
+
+
+
+
+
+
+
+
+
+
+ 多智能体协同分析
+
+
+ {taskInfo.name}
+ •
+ {taskInfo.goal}
+
+ {taskInfo.status}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 三栏布局主体 */}
+
+ {/* 左侧:参与智能体列表 */}
+
+ {/* 场景模板选择 */}
+
+
+
+ {/* 场景模板选择下拉 */}
+ {showSceneSelector && (
+
+
+ 选择场景模板自动配置智能体
+
+ {sceneTemplates.map((template) => (
+
+ ))}
+
+ )}
+
+
+
+ {/* 智能体列表头部 */}
+
+
+
+ 参与智能体 ({selectedMultiAgents.length})
+
+
+
+
+ {/* 智能体选择下拉 - 按五大类别分组 */}
+ {showAgentSelector && (
+
+
+ 选择要添加的智能体
+
+ {availableAgentsForSelection.length === 0 ? (
+
所有智能体已添加
+ ) : (
+
+ {/* 按类别分组显示 */}
+ {['规划类', 'MNTY类', '效果PG类', '知识分析类', '协同管理类'].map((category) => {
+ const categoryAgents = availableAgentsForSelection.filter(a => a.category === category);
+ if (categoryAgents.length === 0) return null;
+ return (
+
+
+ {category === '规划类' && }
+ {category === 'MNTY类' && }
+ {category === '效果PG类' && }
+ {category === '知识分析类' && }
+ {category === '协同管理类' && }
+ {category}
+
+
+ {categoryAgents.map((agent) => (
+
+ ))}
+
+
+ );
+ })}
+
+ )}
+
+ )}
+
+ {/* 参与智能体列表 */}
+
+
+ {selectedMultiAgents.map((agent) => {
+ const response = agentResponses.find(r => r.agentId === agent.id);
+ const status = response?.status || 'pending';
+ return (
+
+
+
+
+
+
{agent.name}
+
{agent.category}
+
+
+
+
+ {status === 'pending' ? '待回答' : status === 'responding' ? '回答中' : '已回答'}
+
+
+
+
+
+ );
+ })}
+ {selectedMultiAgents.length === 0 && (
+
+ )}
+
+
+
+ {/* 历史记录入口 */}
+
+
+
+
+
+ {/* 中间:问答消息流 */}
+
+ {/* 工具条 */}
+
+
+
+
+
+
+
+ {/* 消息列表 */}
+
+ {multiAgentMessages.length === 0 ? (
+
+
+
尚未开始提问
+
+ 请在底部输入你希望多个智能体分析的问题
系统将按所选对象返回回答
+
+
+ ) : (
+ multiAgentMessages.map((msg) => {
+ const isAdopted = msg.type === 'agent' && adoptedViews.some(v => v.agentName === msg.agentName);
+ return (
+
+ {msg.type === 'user' ? (
+
+ ) : (
+
+
+
+
+ {msg.agentName}
+ {isAdopted && (
+ 已采纳
+ )}
+
+
{msg.timestamp.toLocaleTimeString()}
+
+
{msg.content}
+
+
+
+
+
+
+ )}
+
+ );
+ })
+ )}
+ {isMultiAgentLoading && (
+
+
+ 智能体正在分析中...
+
+ )}
+
+
+ {/* 底部统一输入区 */}
+
+
+
发送对象:
+
+
+ {selectedMultiAgents.map((agent) => (
+
+ ))}
+
+
+
+
+
+
+ {/* 右侧:分析结果 */}
+
+ {/* 标签切换 */}
+
+
+
+
+
+ {/* 内容区 */}
+
+ {rightPanelTab === 'structured' ? (
+
+ {/* 汇总结论 */}
+
+
汇总结论
+
+ {agentResponses.filter(r => r.status === 'completed' || r.status === 'adopted').length === 0 ? (
+
暂无分析结论
+ ) : (
+
+ {agentResponses.filter(r => r.status === 'completed' || r.status === 'adopted').map(r => (
+
+ {r.agentName}:
+ {r.coreJudgment}
+
+ ))}
+
+ )}
+
+
+
+ {/* 已采纳观点 */}
+
+
已采纳观点 ({adoptedViews.length})
+ {adoptedViews.length === 0 ? (
+
暂无采纳观点
+ ) : (
+
+ {adoptedViews.map(view => (
+
+
{view.agentName}
+
{view.content}
+
+ ))}
+
+ )}
+
+
+ {/* 待确认问题 */}
+
+
待确认问题
+
+ {pendingQuestions.length === 0 ? (
+
无待确认问题
+ ) : (
+
+ {pendingQuestions.map(q => (
+
+ •
+ {q.content}
+
+ ))}
+
+ )}
+
+
+
+ ) : (
+
+
{summaryDraft.title || '五四青年节宣传方向分析报告'}
+
+
背景
+
{taskInfo.background}
+
+
+
参与智能体
+
{selectedMultiAgents.map(a => a.name).join(' / ')}
+
+
+
联合结论
+
+ {summaryDraft.sections.length > 0 ? summaryDraft.sections.map(s => s.content).join('\n') : '点击"生成汇总"自动生成联合结论'}
+
+
+
+ )}
+
+
+
+
+ )}
+ {viewMode === 'single' && (
+ messages.length === 0 ? (
+
+
+
+ {qaProvider === 'agent' ? `开始与${selectedAgent.name}对话` : '开始你的问答'}
+
+
+ {qaProvider === 'agent'
+ ? '基于智能体专业能力获取精准回答'
+ : '输入问题,获取基于知识库的智能回答'}
+
+
+ {/* 快捷入口 - 仅在通用问答且无消息时显示 */}
+ {qaProvider === 'general' && (
+
+ )}
+
+ ) : (
+
+ {/* 消息列表 - 居中显示,MiniMax风格 */}
+
+
+ {messages.map((msg) => (
+
+ {/* 用户消息 - 右侧显示 */}
+ {msg.role === 'user' && (
+
+ )}
+
+ {/* AI/智能体回复 - 左侧显示 */}
+ {msg.role === 'assistant' && (
+
+
+ {qaProvider === 'agent' ? : }
+
+
+ {/* 智能体名称标识 - 仅智能体模式 */}
+ {qaProvider === 'agent' && (
+
+ {selectedAgent.name}
+
+ )}
+
+
+ {/* 来源信息 - MiniMax风格折叠 */}
+ {msg.sources && showSources && (
+
+
+
+ 引用来源 ({msg.sources.length})
+
+
+ {msg.sources.slice(0, 3).map((source, idx) => (
+
+
+
+ {source.title}
+
+
+ {source.type}
+
+
+
+ {source.snippet.substring(0, 100)}...
+
+
+ ))}
+
+
+ )}
+
+ {/* 智能体执行详情 */}
+ {qaProvider === 'agent' && msg.skills && (
+
+
+
+
+
+ 启用技能
+
+
+ {msg.skills.map((skill, idx) => (
+
+ {skill}
+
+ ))}
+
+
+
+
+
+ 数据源
+
+
+ {msg.dataSources?.map((source, idx) => (
+
+ {source}
+
+ ))}
+
+
+
+
+ )}
+
+
+ )}
+
+ ))}
+
+
+
+ {/* 加载动画 */}
+ {isLoading && (
+
+
+ {qaProvider === 'agent' ? : }
+
+
+
+ )}
+
+ )
+ )}
+
+
+ {/* 对比模式视图 */}
+ {viewMode === 'compare' && (
+
+ {compareResult ? (
+ compareLayout === 'horizontal' ? (
+
+
+
+
+ {getModeLabel(compareResult.leftMode)}
+
+
+
+
+ {compareResult.leftContent}
+
+
+
+
+
+
+ {getModeLabel(compareResult.rightMode)}
+
+
+
+
+ {compareResult.rightContent}
+
+
+
+
+ ) : (
+
+
+
+
+ {getModeLabel(compareResult.leftMode)}
+
+
+
+
+ {compareResult.leftContent}
+
+
+
+
+
+
+ {getModeLabel(compareResult.rightMode)}
+
+
+
+
+ {compareResult.rightContent}
+
+
+
+
+ )
+ ) : (
+
+
+
对比模式
+
输入问题,对比不同模式的回答效果
+
+ )}
+
+ )}
+
+ {/* 新建问答确认对话框 */}
+ {showNewQADialog && (
+
+
+
+
+
+
+
+ 是否保存当前会话?
+
+
+ 当前有 {messages.length} 条对话记录
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* 底部输入区 - 仅在非多智能体模式显示 */}
+ {qaProvider !== 'multi-agent' && (
+
+ )}
+
+
+ {/* 自定义滚动条样式 */}
+
+
+ );
+};
+
+export default QA;
diff --git a/frontend-web/src/strategy-components/components/qa/QACenterPage.tsx b/frontend-web/src/strategy-components/components/qa/QACenterPage.tsx
new file mode 100644
index 00000000..5390d0ab
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/QACenterPage.tsx
@@ -0,0 +1,9 @@
+import { Outlet } from 'react-router-dom';
+
+export default function QACenterPage() {
+ return (
+
+
+
+ );
+}
diff --git a/frontend-web/src/strategy-components/components/qa/QACommon.tsx b/frontend-web/src/strategy-components/components/qa/QACommon.tsx
new file mode 100644
index 00000000..97ba01ed
--- /dev/null
+++ b/frontend-web/src/strategy-components/components/qa/QACommon.tsx
@@ -0,0 +1,782 @@
+import React, { useState } from 'react';
+import { Textarea } from 'tdesign-react';
+import {
+ Bookmark,
+ BrainCircuit,
+ Bot,
+ ChevronDown,
+ FileSearch,
+ Loader2,
+ RotateCcw,
+ Search,
+ Send,
+ Settings,
+ Sparkles,
+ Square,
+ Copy,
+ Trash2,
+ Edit3,
+ Upload,
+ UserRound,
+} from 'lucide-react';
+import { useTheme } from '../../../contexts/ThemeContext';
+import MarkdownRenderer from '../../MarkdownRenderer';
+
+/**
+ * 单条检索结果。
+ *
+ * 多轮检索过程中每一轮可能返回多条资料,这里只保留 UI 展示所需的标题、摘要和来源。
+ */
+export interface QARetrievalItem {
+ id: string;
+ title: string;
+ content: string;
+ source: string;
+}
+
+/**
+ * 多轮检索中的单轮思考节点。
+ *
+ * `queryLoading` 控制该轮检索结果是否仍显示 loading;retrievals 到达后再渲染资料列表。
+ */
+export interface QAThinkingRound {
+ id: string; // 唯一标识符,用于列表渲染时的 key
+ title: string; // 该轮检索的标题,如"第1轮检索"或"背景资料收集"
+ thoughtMarkdown: string; // 模型思考过程的 Markdown 内容,展示思考步骤和推理逻辑
+ query: string; // 该轮发送给检索系统的查询语句
+ queryLoading?: boolean; // 该轮检索是否仍在进行中,true 时显示加载动画
+ retrievals: QARetrievalItem[]; // 该轮返回的检索结果列表
+}
+
+/**
+ * 多轮/深度检索的完整思考链。
+ *
+ * rounds 展示检索过程,finalAnswerMarkdown 展示最终回答;两者分开便于流式过程中逐步更新。
+ */
+export interface QAThinkingTrace {
+ title?: string; // 整体思考链的标题
+ rounds: QAThinkingRound[]; // 多轮检索过程中的所有轮次,每轮包含思考和检索结果
+ finalAnswerMarkdown: string; // 基于多轮检索结果生成的最终回答内容
+}
+
+/**
+ * 单轮 RAG 的回答结构。
+ *
+ * 后端可能把 `...` 和答案混在同一个 answer 流中,解析后分别放到 thinkingMarkdown 与 answerMarkdown。
+ */
+export interface QASingleRagTrace {
+ title?: string; // 思考过程的标题,如"单轮 RAG 思考过程"
+ thinkingMarkdown: string; // 模型在生成回答前的思考过程内容,使用 Markdown 格式
+ answerMarkdown: string; // 最终生成的回答内容,使用 Markdown 格式
+ thinkingComplete?: boolean; // 思考过程是否已完成,true 时思考区域可折叠
+}
+
+export interface QACommonMessage {
+ id: string; // 消息唯一标识符
+ role: 'user' | 'assistant'; // 消息发送者角色:user 为用户消息,assistant 为 AI 回复
+ content: string; // 消息的纯文本内容(当使用 thinkingTrace 或 singleRagTrace 时为空)
+ timestamp?: Date; // 消息发送时间
+ thinkingTrace?: QAThinkingTrace; // 多轮/深度检索模式的思考链,包含多轮检索过程和最终回答
+ sources?: { // 消息引用的参考资料列表
+ title: string; // 参考资料标题
+ type: string; // 资料类型,如"文档"、"网页"等
+ snippet: string; // 资料摘要内容
+ source?: string; // 来源名称或 URL
+ time?: string; // 发布时间
+ url?: string; // 资料链接
+ content?: string; // 完整内容(可能用于详情弹窗)
+ }[];
+}
+
+export interface QACommonMessage {
+ singleRagTrace?: QASingleRagTrace; // 单轮 RAG 模式的思考过程和回答结构
+}
+
+interface QAAvatarProps {
+ role: 'user' | 'assistant';
+ isActive?: boolean;
+ label?: string;
+ variant?: 'default' | 'blue' | 'purple';
+}
+
+const getAssistantAvatarClass = (variant: QAAvatarProps['variant'], isDark: boolean) => {
+ switch (variant) {
+ case 'blue':
+ return isDark
+ ? 'bg-gradient-to-br from-blue-400 via-blue-600 to-cyan-700 text-white'
+ : 'bg-gradient-to-br from-blue-300 via-blue-500 to-cyan-500 text-white';
+ case 'purple':
+ return isDark
+ ? 'bg-gradient-to-br from-purple-400 via-violet-600 to-fuchsia-700 text-white'
+ : 'bg-gradient-to-br from-purple-300 via-violet-500 to-fuchsia-500 text-white';
+ default:
+ return isDark
+ ? 'bg-gradient-to-br from-cyan-500 via-blue-600 to-violet-600 text-white'
+ : 'bg-gradient-to-br from-red-400 via-rose-500 to-orange-400 text-white';
+ }
+};
+
+/**
+ * 统一头像组件。
+ *
+ * 单一模式使用默认助手色;对比模式可通过 variant 指定蓝/紫色系,让头像与左右面板标签保持一致。
+ * `isActive` 会触发呼吸和星光动画,用来提示当前助手正在生成。
+ */
+export const QAAvatar: React.FC = ({ role, isActive = false, label, variant = 'default' }) => {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+ const Icon = role === 'assistant' ? Bot : UserRound;
+
+ return (
+
+
+
+ {role === 'assistant' && isActive && (
+
+ )}
+
+ );
+};
+
+interface TypingIndicatorProps {
+ label?: string;
+ assistantLabel?: string;
+}
+
+/**
+ * 助手预连接/生成中的占位提示。
+ *
+ * 用于请求已经发出但还没有生成可展示消息时,给用户明确的等待反馈。
+ */
+export const TypingIndicator: React.FC = ({ label = '正在思考并生成回复', assistantLabel = 'AI 助手' }) => {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+
+ return (
+
+
+
+
+
+
+ {label}
+
+
+ {[0, 1, 2].map((idx) => (
+
+ ))}
+
+
+
+
+ );
+};
+
+interface ChatMessageProps {
+ message: T;
+ showSources?: boolean;
+ assistantLabel?: string;
+ assistantActive?: boolean;
+ onCitationClick?: (message: T, citationIndex: number) => void;
+ onCopyAssistant?: (message: T) => void;
+ onDeleteAssistant?: (message: T) => void;
+ onEditAssistant?: (message: T) => void;
+ onResendAssistant?: (message: T) => void;
+ renderAssistantExtra?: (message: T) => React.ReactNode;
+}
+
+/**
+ * 聊天消息渲染器。
+ *
+ * 根据消息类型自动选择普通文本、单轮 RAG 回答或多轮检索思考链。
+ * sources 区域独立折叠展示,避免引用资料过长时挤占主回答阅读空间。
+ */
+export function ChatMessage({
+ message,
+ showSources = true,
+ assistantLabel = 'AI 助手',
+ assistantActive = false,
+ onCitationClick,
+ onCopyAssistant,
+ onDeleteAssistant,
+ onEditAssistant,
+ onResendAssistant,
+ renderAssistantExtra,
+}: ChatMessageProps) {
+ const { theme } = useTheme();
+ const isDark = theme === 'dark';
+ const isUser = message.role === 'user';
+ const hasTrace = !isUser && !!(message.singleRagTrace || message.thinkingTrace);
+
+ return (
+
+
+ {!isUser &&
}
+
+ {message.singleRagTrace && !isUser ? (
+
onCitationClick?.(message, citationIndex)}
+ />
+ ) : message.thinkingTrace && !isUser ? (
+ onCitationClick?.(message, citationIndex)}
+ />
+ ) : (
+ {message.content}
+ )}
+ {!isUser && (onCopyAssistant || onDeleteAssistant || onEditAssistant || onResendAssistant) && (
+
+ {onCopyAssistant && (
+
+ )}
+ {onDeleteAssistant && (
+
+ )}
+ {onEditAssistant && (
+
+ )}
+ {onResendAssistant && (
+
+ )}
+
+ )}
+
+ {isUser &&
}
+
+
+
+ {!isUser && message.sources && showSources && (
+
+
+ 引用来源
+ ({message.sources.length})
+
+ )}
+ >
+
+ {message.sources.map((source, idx) => (
+
+
+
+ {source.title}
+
+ {source.type}
+
+
+
{source.snippet}
+
+ ))}
+
+
+ )}
+
+ {!isUser && renderAssistantExtra?.(message)}
+
+ );
+}
+
+interface CollapsibleSectionProps {
+ title: React.ReactNode;
+ children: React.ReactNode;
+ defaultOpen?: boolean;
+ className?: string;
+ headerClassName?: string;
+}
+
+/**
+ * 通用折叠面板。
+ *
+ * 思考过程、检索结果、引用来源都复用该组件,保证展开/收起动画和深浅色主题一致。
+ */
+const CollapsibleSection: React.FC