Files
AIclinicalresearch/frontend-v2/src/modules/ssa/components/SSACodeModal.tsx
HaHafeng 52989cd03f feat(ssa): Agent channel UX optimization (Solution B) + Plan-and-Execute architecture design
SSA Agent channel improvements (12 code files, +931/-203 lines):
- Solution B: left/right separation of concerns (gaze guiding + state mutex + time-travel)
- JWT token refresh mechanism (ensureFreshToken) to fix HTTP 401 during pipeline
- Code truncation fix: LLM maxTokens 4000->8000 + CSS max-height 60vh
- Retry streaming code generation with generateCodeStream()
- R Docker structured errors: 20+ pattern matching + format_agent_error + line extraction
- Prompt iron rules: strict output format in CoderAgent System Prompt
- parseCode robustness: XML/Markdown/inference 3-tier matching + length validation
- consoleOutput type defense: handle both array and scalar from R Docker unboxedJSON
- Agent progress bar sync: derive phase from agentExecution.status
- Export report / view code buttons restored for Agent mode
- ExecutingProgress component: real-time timer + dynamic tips + step pulse animation

Architecture design (3 review reports):
- Plan-and-Execute step-by-step execution architecture approved
- Code accumulation strategy (R Docker stays stateless)
- 5 engineering guardrails: XML tags, AST pre-check, defensive prompts, high-fidelity schema, error classification circuit breaker

Docs: update SSA module status v4.1, system status v6.7, deployment changelist
Made-with: Cursor
2026-03-07 22:32:32 +08:00

137 lines
4.4 KiB
TypeScript

/**
* SSACodeModal - R 代码模态框 (Unified Record Architecture)
*
* 从 currentRecord.steps 聚合所有步骤的可复现代码。
*/
import React, { useEffect, useState } from 'react';
import { X, Download, Loader2 } from 'lucide-react';
import { useSSAStore } from '../stores/ssaStore';
export const SSACodeModal: React.FC = () => {
const {
codeModalVisible,
setCodeModalVisible,
addToast,
currentRecordId,
analysisHistory,
executionMode,
agentExecution,
} = useSSAStore();
const [code, setCode] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
const record = currentRecordId
? analysisHistory.find((r) => r.id === currentRecordId) ?? null
: null;
useEffect(() => {
if (!codeModalVisible) return;
setIsLoading(true);
try {
if (executionMode === 'agent' && agentExecution?.generatedCode) {
const header = `# ========================================\n# Agent 生成的 R 代码\n# 分析任务: ${agentExecution.query || '统计分析'}\n# ========================================\n`;
setCode(header + agentExecution.generatedCode);
} else {
const steps = record?.steps ?? [];
const successSteps = steps.filter(
(s) => (s.status === 'success' || s.status === 'warning') && s.result
);
if (successSteps.length > 0) {
const allCode = successSteps
.map((s) => {
const stepCode = (s.result as any)?.reproducible_code;
const header = `# ========================================\n# 步骤 ${s.step_number}: ${s.tool_name}\n# ========================================\n`;
return header + (stepCode || '# 该步骤暂无可用代码');
})
.join('\n\n');
setCode(allCode);
} else {
setCode('# 暂无可用代码\n# 请先执行分析');
}
}
} finally {
setIsLoading(false);
}
}, [codeModalVisible, record, executionMode, agentExecution]);
if (!codeModalVisible) return null;
const handleClose = () => setCodeModalVisible(false);
const generateFilename = () => {
const now = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
const rawTitle = executionMode === 'agent'
? (agentExecution?.query || 'agent_analysis')
: (record?.plan?.title || 'analysis');
const title = rawTitle
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_')
.slice(0, 30);
return `SSA_${title}_${ts}.R`;
};
const handleDownload = () => {
try {
const blob = new Blob([code], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = generateFilename();
a.click();
URL.revokeObjectURL(url);
addToast('R 脚本已下载', 'success');
handleClose();
} catch {
addToast('下载失败', 'error');
}
};
const handleCopy = () => {
navigator.clipboard.writeText(code);
addToast('代码已复制', 'success');
};
return (
<div className="code-modal-overlay" onClick={handleClose}>
<div className="code-modal pop-in" onClick={(e) => e.stopPropagation()}>
<header className="code-modal-header">
<h3 className="code-modal-title">
<span className="r-icon">R</span>
R
</h3>
<button className="code-modal-close" onClick={handleClose}>
<X size={16} />
</button>
</header>
<div className="code-modal-body">
{isLoading ? (
<div className="code-loading">
<Loader2 size={24} className="spin" />
<span>...</span>
</div>
) : (
<pre className="code-block">
<code>{code}</code>
</pre>
)}
</div>
<footer className="code-modal-footer">
<button className="copy-btn" onClick={handleCopy} disabled={isLoading}>
</button>
<button className="download-btn" onClick={handleDownload} disabled={isLoading}>
<Download size={14} />
.R
</button>
</footer>
</div>
</div>
);
};
export default SSACodeModal;