feat(ssa): Complete Phase 2A frontend integration - multi-step workflow end-to-end

Phase 2A: WorkflowPlannerService, WorkflowExecutorService, Python data quality, 6 bug fixes, DescriptiveResultView, multi-step R code/Word export, MVP UI reuse. V11 UI: Gemini-style, multi-task, single-page scroll, Word export. Architecture: Block-based rendering consensus (4 block types). New R tools: chi_square, correlation, descriptive, logistic_binary, mann_whitney, t_test_paired. Docs: dev summary, block-based plan, status updates, task list v2.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-20 23:09:27 +08:00
parent 23b422f758
commit 428a22adf2
62 changed files with 15416 additions and 299 deletions

View File

@@ -17,16 +17,18 @@ import {
ArrowLeft,
FileSignature,
ArrowRight,
Zap,
Loader2,
AlertCircle,
CheckCircle
CheckCircle,
BarChart2
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useSSAStore } from '../stores/ssaStore';
import { useAnalysis } from '../hooks/useAnalysis';
import { useWorkflow } from '../hooks/useWorkflow';
import type { SSAMessage } from '../types';
import { TypeWriter } from './TypeWriter';
import { DataProfileCard } from './DataProfileCard';
export const SSAChatPane: React.FC = () => {
const navigate = useNavigate();
@@ -45,9 +47,12 @@ export const SSAChatPane: React.FC = () => {
setError,
addToast,
selectAnalysisRecord,
dataProfile,
dataProfileLoading,
} = useSSAStore();
const { uploadData, generatePlan, isUploading, uploadProgress } = useAnalysis();
const { generateDataProfile, generateWorkflowPlan, isProfileLoading, isPlanLoading } = useWorkflow();
const [inputValue, setInputValue] = useState('');
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'parsing' | 'success' | 'error'>('idle');
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -109,7 +114,14 @@ export const SSAChatPane: React.FC = () => {
rowCount: result.schema.rowCount,
});
setUploadStatus('success');
addToast('数据读取成功,正在分析结构...', 'success');
addToast('数据读取成功,正在进行质量核查...', 'success');
// Phase 2A: 自动触发数据质量核查
try {
await generateDataProfile(result.sessionId);
} catch (profileErr) {
console.warn('数据画像生成失败,继续使用基础模式:', profileErr);
}
} catch (err: any) {
setUploadStatus('error');
const errorMsg = err?.message || '上传失败,请检查文件格式';
@@ -132,7 +144,13 @@ export const SSAChatPane: React.FC = () => {
if (!inputValue.trim()) return;
try {
await generatePlan(inputValue);
// Phase 2A: 如果已有 session使用多步骤工作流规划
if (currentSession?.id) {
await generateWorkflowPlan(currentSession.id, inputValue);
} else {
// 没有数据时,使用旧流程
await generatePlan(inputValue);
}
setInputValue('');
} catch (err: any) {
addToast(err?.message || '生成计划失败', 'error');
@@ -178,8 +196,9 @@ export const SSAChatPane: React.FC = () => {
</div>
<EngineStatus
isExecuting={isExecuting}
isLoading={isLoading}
isLoading={isLoading || isPlanLoading}
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
isProfileLoading={isProfileLoading || dataProfileLoading}
/>
</header>
@@ -198,6 +217,18 @@ export const SSAChatPane: React.FC = () => {
</div>
</div>
{/* Phase 2A: 数据质量核查报告卡片 - 在欢迎语之后、用户消息之前显示 */}
{dataProfile && (
<div className="message message-ai slide-up">
<div className="message-avatar ai-avatar">
<Bot size={12} />
</div>
<div className="message-bubble ai-bubble profile-bubble">
<DataProfileCard profile={dataProfile} />
</div>
</div>
)}
{/* 动态消息 */}
{messages.map((msg: SSAMessage, idx: number) => {
const isLastAiMessage = msg.role === 'assistant' && idx === messages.length - 1;
@@ -219,8 +250,8 @@ export const SSAChatPane: React.FC = () => {
msg.content
)}
{/* SAP 卡片 */}
{msg.artifactType === 'sap' && (
{/* SAP 卡片 - 只有消息中明确标记为 sap 类型时才显示 */}
{msg.artifactType === 'sap' && msg.recordId && (
<button className="sap-card" onClick={() => handleOpenWorkspace(msg.recordId)}>
<div className="sap-card-left">
<div className="sap-card-icon">
@@ -239,8 +270,23 @@ export const SSAChatPane: React.FC = () => {
);
})}
{/* 数据画像生成中指示器 */}
{dataProfileLoading && (
<div className="message message-ai slide-up">
<div className="message-avatar ai-avatar">
<Bot size={12} />
</div>
<div className="message-bubble ai-bubble">
<div className="profile-loading">
<BarChart2 size={16} className="spin text-blue-500" />
<span>...</span>
</div>
</div>
</div>
)}
{/* AI 正在思考指示器 */}
{isLoading && (
{(isLoading || isPlanLoading) && (
<div className="message message-ai slide-up">
<div className="message-avatar ai-avatar">
<Bot size={12} />
@@ -255,32 +301,12 @@ export const SSAChatPane: React.FC = () => {
</div>
)}
{/* 数据挂载成功消息 */}
{mountedFile && currentPlan && !messages.some((m: SSAMessage) => m.artifactType === 'sap') && (
<div className="message message-ai slide-up">
<div className="message-avatar ai-avatar">
<Bot size={12} />
</div>
<div className="message-bubble ai-bubble">
<div className="data-mounted-msg">
<Zap size={14} className="text-amber-500" />
<b></b> (SAP)
</div>
<button className="sap-card" onClick={() => handleOpenWorkspace()}>
<div className="sap-card-left">
<div className="sap-card-icon">
<FileSignature size={16} />
</div>
<div className="sap-card-content">
<div className="sap-card-title"> (SAP)</div>
<div className="sap-card-hint"></div>
</div>
</div>
<ArrowRight size={16} className="sap-card-arrow" />
</button>
</div>
</div>
)}
{/*
Phase 2A 新流程:
1. 上传数据 → 显示数据质量报告(已在上方处理)
2. 用户输入分析问题 → AI 回复消息中包含 SAP 卡片(通过 msg.artifactType === 'sap'
旧的"数据挂载成功消息"已移除,避免在没有用户输入时就显示 SAP 卡片
*/}
{/* 自动滚动锚点与占位垫片 - 解决底部输入框遮挡的终极方案 */}
<div ref={chatEndRef} className="scroll-spacer" />
@@ -409,12 +435,14 @@ interface EngineStatusProps {
isExecuting: boolean;
isLoading: boolean;
isUploading: boolean;
isProfileLoading?: boolean;
}
const EngineStatus: React.FC<EngineStatusProps> = ({
isExecuting,
isLoading,
isUploading
isUploading,
isProfileLoading
}) => {
const getStatus = () => {
if (isExecuting) {
@@ -423,6 +451,9 @@ const EngineStatus: React.FC<EngineStatusProps> = ({
if (isLoading) {
return { text: 'AI Processing...', className: 'status-processing' };
}
if (isProfileLoading) {
return { text: 'Data Profiling...', className: 'status-profiling' };
}
if (isUploading) {
return { text: 'Parsing Data...', className: 'status-uploading' };
}