{
{/* Phase II: 流式对话消息(来自 useSSAChat) */}
{chatMessages.map((msg: ChatMessage) => {
- const isSystemAudit = (msg as any).intent === 'system';
+ const isSystemAudit = msg.intent === 'system';
// 系统审计消息(方案 B:右侧操作的审计纪要)
if (isSystemAudit) {
return (
{msg.content}
+ {msg.executionId && (
+
+ )}
);
}
@@ -448,27 +471,6 @@ export const SSAChatPane: React.FC = () => {
);
})}
- {/* 时光机卡片:Agent 执行历史(点击可切换右侧工作区) */}
- {agentExecutionHistory.filter(e => e.status === 'completed').length > 1 && (
-
- {agentExecutionHistory
- .filter(e => e.status === 'completed')
- .slice(0, -1)
- .map(exec => (
-
- ))}
-
- )}
-
{/* 数据画像生成中指示器 */}
{dataProfileLoading && (
@@ -678,6 +680,7 @@ const INTENT_LABELS: Record = {
analyze: '统计分析',
discuss: '结果讨论',
feedback: '结果改进',
+ system: '系统',
};
const EngineStatus: React.FC = ({
diff --git a/frontend-v2/src/modules/ssa/components/SSACodeModal.tsx b/frontend-v2/src/modules/ssa/components/SSACodeModal.tsx
index f099a344..beb13e5e 100644
--- a/frontend-v2/src/modules/ssa/components/SSACodeModal.tsx
+++ b/frontend-v2/src/modules/ssa/components/SSACodeModal.tsx
@@ -1,12 +1,154 @@
/**
* SSACodeModal - R 代码模态框 (Unified Record Architecture)
*
- * 从 currentRecord.steps 聚合所有步骤的可复现代码。
+ * 导出时自动包裹 Export Wrapper:
+ * 1. 数据加载层(read.csv / read_excel + 文件名自动填入)
+ * 2. 平台辅助函数 Polyfill(make_*_block → 本地 print / 保存 PNG)
+ * 3. Agent 生成的核心分析代码
+ *
+ * 在线查看显示原始代码;下载/复制时注入 Wrapper。
*/
-import React, { useEffect, useState } from 'react';
-import { X, Download, Loader2 } from 'lucide-react';
+import React, { useEffect, useState, useMemo } from 'react';
+import { X, Download, Copy, Loader2 } from 'lucide-react';
import { useSSAStore } from '../stores/ssaStore';
+/**
+ * 从 R 代码中提取所有 library(xxx) 调用的包名
+ */
+function extractPackages(code: string): string[] {
+ const pkgs = new Set();
+ const re = /library\((\w+)\)/g;
+ let m: RegExpExecArray | null;
+ while ((m = re.exec(code)) !== null) pkgs.add(m[1]);
+ return [...pkgs];
+}
+
+/**
+ * 生成本地复现 Wrapper:数据加载 + Polyfill + 包检查
+ */
+function buildExportWrapper(agentCode: string, fileName: string, query: string): string {
+ const packages = extractPackages(agentCode);
+ const pkgVector = packages.map(p => `"${p}"`).join(', ');
+
+ const dataLoader = `# =====================================================================
+# SSA-Pro 智能统计分析 — 本地复现脚本
+# 分析任务: ${query}
+# 导出时间: ${new Date().toLocaleString('zh-CN')}
+# =====================================================================
+# 使用说明:
+# 1. 将本脚本与数据文件放在同一目录
+# 2. 在 RStudio 中打开,设置工作目录为脚本所在目录
+# 3. 全选运行 (Ctrl+A → Ctrl+Enter)
+# 4. 图表将自动保存为 PNG 文件到当前目录
+# =====================================================================
+
+# ── 1. 环境检查:缺失包提示 ──
+required_packages <- c(${pkgVector})
+missing_packages <- required_packages[!(required_packages %in% installed.packages()[,"Package"])]
+if (length(missing_packages) > 0) {
+ stop(paste0(
+ "缺少以下 R 包,请先安装:\\n ",
+ paste(missing_packages, collapse = ", "),
+ "\\n\\n运行以下命令安装:\\n install.packages(c(",
+ paste0('"', missing_packages, '"', collapse = ", "), "))"
+ ))
+}
+
+# ── 2. 数据加载(请确保数据文件与本脚本在同一目录) ──
+.DATA_FILE <- "${fileName}"
+
+if (!file.exists(.DATA_FILE)) {
+ stop(paste0(
+ "找不到数据文件: ", .DATA_FILE, "\\n",
+ "请将数据文件复制到脚本所在目录,或修改上方 .DATA_FILE 变量为正确路径。"
+ ))
+}
+
+if (grepl("\\\\.csv$", .DATA_FILE, ignore.case = TRUE)) {
+ df <- read.csv(.DATA_FILE, stringsAsFactors = FALSE, fileEncoding = "UTF-8")
+} else if (grepl("\\\\.xlsx?$", .DATA_FILE, ignore.case = TRUE)) {
+ if (!requireNamespace("readxl", quietly = TRUE)) {
+ stop("读取 Excel 文件需要 readxl 包,请运行: install.packages(\\"readxl\\")")
+ }
+ df <- readxl::read_excel(.DATA_FILE)
+ df <- as.data.frame(df)
+} else {
+ stop(paste0("不支持的文件格式: ", .DATA_FILE, "(仅支持 .csv / .xlsx)"))
+}
+cat(paste0("✓ 数据已加载: ", nrow(df), " 行 × ", ncol(df), " 列\\n"))`;
+
+ const polyfills = `
+# ── 3. 平台辅助函数 — 本地兼容层 (Polyfill) ──
+# 以下函数在平台沙箱中用于构建 UI 报告块,
+# 本地运行时替换为控制台输出 + 图片文件保存。
+
+make_markdown_block <- function(content, title = NULL) {
+ cat("\\n")
+ if (!is.null(title)) cat("【", title, "】\\n")
+ cat(content, "\\n")
+ invisible(list(type = "markdown", content = content))
+}
+
+make_table_block <- function(headers, rows, title = NULL, footnote = NULL) {
+ cat("\\n")
+ if (!is.null(title)) cat("── ", title, " ──\\n")
+ mat <- do.call(rbind, rows)
+ colnames(mat) <- headers
+ print(as.data.frame(mat, stringsAsFactors = FALSE))
+ if (!is.null(footnote)) cat("注: ", footnote, "\\n")
+ invisible(list(type = "table"))
+}
+
+make_table_block_from_df <- function(df_arg, title = NULL, footnote = NULL, digits = NULL) {
+ cat("\\n")
+ if (!is.null(title)) cat("── ", title, " ──\\n")
+ if (!is.null(digits)) {
+ nums <- sapply(df_arg, is.numeric)
+ df_arg[nums] <- lapply(df_arg[nums], round, digits = digits)
+ }
+ print(df_arg, row.names = FALSE)
+ if (!is.null(footnote)) cat("注: ", footnote, "\\n")
+ invisible(list(type = "table"))
+}
+
+make_image_block <- function(base64_data, title = "", alt = "") {
+ # 将 base64 图片数据解码并保存为本地 PNG 文件
+ tryCatch({
+ raw_b64 <- sub("^data:image/[^;]+;base64,", "", base64_data)
+ safe_name <- gsub("[^a-zA-Z0-9_\\u4e00-\\u9fa5]", "_", title)
+ if (nchar(safe_name) == 0 || nchar(safe_name) > 60) safe_name <- "plot"
+ out_file <- paste0(safe_name, ".png")
+ # 避免同名覆盖
+ counter <- 1
+ while (file.exists(out_file)) {
+ out_file <- paste0(safe_name, "_", counter, ".png")
+ counter <- counter + 1
+ }
+ writeBin(base64enc::base64decode(raw_b64), out_file)
+ cat(paste0("✓ 图表已保存: ", out_file, "\\n"))
+ }, error = function(e) {
+ cat(paste0("✗ 图表保存失败: ", e$message, "\\n"))
+ })
+ invisible(list(type = "image"))
+}
+
+make_kv_block <- function(items, title = NULL) {
+ cat("\\n")
+ if (!is.null(title)) cat("── ", title, " ──\\n")
+ for (nm in names(items)) {
+ cat(sprintf(" %s: %s\\n", nm, items[[nm]]))
+ }
+ invisible(list(type = "key_value"))
+}`;
+
+ const coreSection = `
+# =====================================================================
+# 4. 核心分析代码(由 AI Agent 生成,无需修改)
+# =====================================================================`;
+
+ return [dataLoader, polyfills, coreSection, agentCode].join('\n\n');
+}
+
export const SSACodeModal: React.FC = () => {
const {
codeModalVisible,
@@ -16,9 +158,10 @@ export const SSACodeModal: React.FC = () => {
analysisHistory,
executionMode,
agentExecution,
+ mountedFile,
} = useSSAStore();
- const [code, setCode] = useState('');
+ const [rawCode, setRawCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const record = currentRecordId
@@ -30,8 +173,7 @@ export const SSACodeModal: React.FC = () => {
setIsLoading(true);
try {
if (executionMode === 'agent' && agentExecution?.generatedCode) {
- const header = `# ========================================\n# Agent 生成的 R 代码\n# 分析任务: ${agentExecution.query || '统计分析'}\n# ========================================\n`;
- setCode(header + agentExecution.generatedCode);
+ setRawCode(agentExecution.generatedCode);
} else {
const steps = record?.steps ?? [];
const successSteps = steps.filter(
@@ -45,9 +187,9 @@ export const SSACodeModal: React.FC = () => {
return header + (stepCode || '# 该步骤暂无可用代码');
})
.join('\n\n');
- setCode(allCode);
+ setRawCode(allCode);
} else {
- setCode('# 暂无可用代码\n# 请先执行分析');
+ setRawCode('');
}
}
} finally {
@@ -55,6 +197,22 @@ export const SSACodeModal: React.FC = () => {
}
}, [codeModalVisible, record, executionMode, agentExecution]);
+ const fileName = mountedFile?.name || 'data.csv';
+ const query = agentExecution?.query || record?.query || '统计分析';
+
+ // 带 Wrapper 的完整导出代码(用于下载和复制)
+ const exportCode = useMemo(() => {
+ if (!rawCode) return '# 暂无可用代码\n# 请先执行分析';
+ if (executionMode === 'agent') {
+ return buildExportWrapper(rawCode, fileName, query);
+ }
+ // QPER 模式:原样输出(已含 reproducible_code)
+ return rawCode;
+ }, [rawCode, executionMode, fileName, query]);
+
+ // 在线预览:显示带 Wrapper 的完整代码
+ const displayCode = exportCode;
+
if (!codeModalVisible) return null;
const handleClose = () => setCodeModalVisible(false);
@@ -74,14 +232,14 @@ export const SSACodeModal: React.FC = () => {
const handleDownload = () => {
try {
- const blob = new Blob([code], { type: 'text/plain' });
+ const blob = new Blob([exportCode], { type: 'text/plain; charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = generateFilename();
a.click();
URL.revokeObjectURL(url);
- addToast('R 脚本已下载', 'success');
+ addToast('R 脚本已下载(含本地复现环境)', 'success');
handleClose();
} catch {
addToast('下载失败', 'error');
@@ -89,8 +247,8 @@ export const SSACodeModal: React.FC = () => {
};
const handleCopy = () => {
- navigator.clipboard.writeText(code);
- addToast('代码已复制', 'success');
+ navigator.clipboard.writeText(exportCode);
+ addToast('代码已复制(含本地复现环境)', 'success');
};
return (
@@ -114,16 +272,17 @@ export const SSACodeModal: React.FC = () => {
) : (
- {code}
+ {displayCode}
)}