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:
@@ -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' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user