feat(ssa): Complete Phase I-IV intelligent dialogue and tool system development

Phase I - Session Blackboard + READ Layer:
- SessionBlackboardService with Postgres-Only cache
- DataProfileService for data overview generation
- PicoInferenceService for LLM-driven PICO extraction
- Frontend DataContextCard and VariableDictionaryPanel
- E2E tests: 31/31 passed

Phase II - Conversation Layer LLM + Intent Router:
- ConversationService with SSE streaming
- IntentRouterService (rule-first + LLM fallback, 6 intents)
- SystemPromptService with 6-segment dynamic assembly
- TokenTruncationService for context management
- ChatHandlerService as unified chat entry
- Frontend SSAChatPane and useSSAChat hook
- E2E tests: 38/38 passed

Phase III - Method Consultation + AskUser Standardization:
- ToolRegistryService with Repository Pattern
- MethodConsultService with DecisionTable + LLM enhancement
- AskUserService with global interrupt handling
- Frontend AskUserCard component
- E2E tests: 13/13 passed

Phase IV - Dialogue-Driven Analysis + QPER Integration:
- ToolOrchestratorService (plan/execute/report)
- analysis_plan SSE event for WorkflowPlan transmission
- Dual-channel confirmation (ask_user card + workspace button)
- PICO as optional hint for LLM parsing
- E2E tests: 25/25 passed

R Statistics Service:
- 5 new R tools: anova_one, baseline_table, fisher, linear_reg, wilcoxon
- Enhanced guardrails and block helpers
- Comprehensive test suite (run_all_tools_test.js)

Documentation:
- Updated system status document (v5.9)
- Updated SSA module status and development plan (v1.8)

Total E2E: 107/107 passed (Phase I: 31, Phase II: 38, Phase III: 13, Phase IV: 25)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-22 18:53:39 +08:00
parent bf10dec4c8
commit 3446909ff7
68 changed files with 11583 additions and 412 deletions

View File

@@ -1,18 +1,22 @@
/**
* SSAChatPane - V11 对话区
*
* 100% 还原 V11 原型图
* - 顶部 Header标题 + 返回按钮 + 状态指示
* - 居中对话列表
* - 底部悬浮输入框
* SSAChatPane - V11 对话区Phase II 升级)
*
* Phase II 改造:
* - handleSend 走统一 /chat SSE API替换 parseIntent → generateWorkflowPlan
* - 流式消息渲染(替换 TypeWriter 逐字效果)
* - ThinkingBlock 深度思考折叠
* - 意图标签intent badge
* - H3streaming 期间锁定输入
*
* 保留不变Header、文件上传、DataProfileCard、SAP/Result 卡片
*/
import React, { useState, useRef, useEffect, useCallback } from 'react';
import {
Bot,
User,
Paperclip,
ArrowUp,
FileSpreadsheet,
import {
Bot,
User,
Paperclip,
ArrowUp,
FileSpreadsheet,
X,
ArrowLeft,
FileSignature,
@@ -20,17 +24,23 @@ import {
Loader2,
AlertCircle,
CheckCircle,
BarChart2
BarChart2,
Square,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useSSAStore } from '../stores/ssaStore';
import type { AnalysisRecord } from '../stores/ssaStore';
import { useAnalysis } from '../hooks/useAnalysis';
import { useWorkflow } from '../hooks/useWorkflow';
import { useSSAChat } from '../hooks/useSSAChat';
import type { ChatMessage, ChatIntentType } from '../hooks/useSSAChat';
import type { SSAMessage } from '../types';
import { TypeWriter } from './TypeWriter';
import { DataProfileCard } from './DataProfileCard';
import { ClarificationCard } from './ClarificationCard';
import { AskUserCard } from './AskUserCard';
import type { AskUserResponseData } from './AskUserCard';
import { ThinkingBlock } from '@/shared/components/Chat';
import type { ClarificationCardData, IntentResult } from '../types';
export const SSAChatPane: React.FC = () => {
@@ -55,7 +65,21 @@ export const SSAChatPane: React.FC = () => {
} = useSSAStore();
const { uploadData, generatePlan, isUploading, uploadProgress } = useAnalysis();
const { generateDataProfile, generateWorkflowPlan, parseIntent, handleClarify, isProfileLoading, isPlanLoading } = useWorkflow();
const { generateDataProfile, handleClarify, executeWorkflow, isProfileLoading, isPlanLoading } = useWorkflow();
const {
chatMessages,
isGenerating,
currentIntent,
pendingQuestion,
pendingPlanConfirm,
sendChatMessage,
respondToQuestion,
skipQuestion,
loadHistory,
abort: abortChat,
clearMessages,
} = useSSAChat();
const [inputValue, setInputValue] = useState('');
const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'parsing' | 'success' | 'error'>('idle');
const [pendingClarification, setPendingClarification] = useState<{
@@ -67,6 +91,24 @@ export const SSAChatPane: React.FC = () => {
const chatEndRef = useRef<HTMLDivElement>(null);
const messagesContainerRef = useRef<HTMLDivElement>(null);
// Phase IV: plan_confirmed → 自动触发 executeWorkflow
useEffect(() => {
if (pendingPlanConfirm?.workflowId && currentSession?.id) {
executeWorkflow(currentSession.id, pendingPlanConfirm.workflowId).catch((err: any) => {
addToast(err?.message || '执行失败', 'error');
});
}
}, [pendingPlanConfirm, currentSession?.id, executeWorkflow, addToast]);
// Phase II: session 切换时加载对话历史
useEffect(() => {
if (currentSession?.id) {
loadHistory(currentSession.id);
} else {
clearMessages();
}
}, [currentSession?.id, loadHistory, clearMessages]);
// 自动滚动到底部,确保最新内容可见
const scrollToBottom = useCallback(() => {
if (messagesContainerRef.current) {
@@ -80,7 +122,7 @@ export const SSAChatPane: React.FC = () => {
useEffect(() => {
const timer = setTimeout(scrollToBottom, 100);
return () => clearTimeout(timer);
}, [messages, scrollToBottom]);
}, [messages, chatMessages, isGenerating, scrollToBottom]);
const handleBack = () => {
navigate(-1);
@@ -149,45 +191,32 @@ export const SSAChatPane: React.FC = () => {
const handleSend = async () => {
if (!inputValue.trim()) return;
if (isGenerating) return;
const query = inputValue;
const query = inputValue.trim();
setInputValue('');
// Immediately show user message in chat
addMessage({
id: crypto.randomUUID(),
role: 'user',
content: query,
createdAt: new Date().toISOString(),
});
try {
if (currentSession?.id) {
// Phase Q: 先做意图解析,低置信度时追问
const intentResp = await parseIntent(currentSession.id, query);
if (intentResp.needsClarification && intentResp.clarificationCards?.length > 0) {
addMessage({
id: crypto.randomUUID(),
role: 'assistant',
content: `我大致理解了你的意图(${intentResp.intent.reasoning}),但为了生成更精确的分析方案,想确认几个细节:`,
createdAt: new Date().toISOString(),
});
setPendingClarification({
cards: intentResp.clarificationCards,
originalQuery: query,
intent: intentResp.intent,
});
return;
}
// 置信度足够 → 直接生成工作流计划(不再重复添加用户消息)
await generateWorkflowPlan(currentSession.id, query);
} else {
await generatePlan(query);
if (currentSession?.id) {
// Phase II: 统一走 /chat SSE API
// useSSAChat 内部处理 user message 添加 + assistant placeholder + 流式接收
try {
await sendChatMessage(currentSession.id, query);
} catch (err: any) {
addToast(err?.message || '对话失败', 'error');
}
} else {
// 没有 session未上传数据走老的 generatePlan 流程
addMessage({
id: crypto.randomUUID(),
role: 'user',
content: query,
createdAt: new Date().toISOString(),
});
try {
await generatePlan(query);
} catch (err: any) {
addToast(err?.message || '生成计划失败', 'error');
}
} catch (err: any) {
addToast(err?.message || '生成计划失败', 'error');
}
};
@@ -264,11 +293,13 @@ export const SSAChatPane: React.FC = () => {
{currentSession?.title || '新的统计分析'}
</span>
</div>
<EngineStatus
isExecuting={isExecuting}
<EngineStatus
isExecuting={isExecuting}
isLoading={isLoading || isPlanLoading}
isUploading={uploadStatus === 'uploading' || uploadStatus === 'parsing'}
isProfileLoading={isProfileLoading || dataProfileLoading}
isStreaming={isGenerating}
streamIntent={currentIntent}
/>
</header>
@@ -299,13 +330,13 @@ export const SSAChatPane: React.FC = () => {
</div>
)}
{/* 动态消息 */}
{/* 旧消息store 中的 SSAMessage保留向后兼容 */}
{messages.map((msg: SSAMessage, idx: number) => {
const isLastAiMessage = msg.role === 'assistant' && idx === messages.length - 1;
const showTypewriter = isLastAiMessage && !msg.artifactType;
return (
<div
<div
key={msg.id || idx}
className={`message ${msg.role === 'user' ? 'message-user' : 'message-ai'} slide-up`}
style={{ animationDelay: `${idx * 0.1}s` }}
@@ -320,7 +351,6 @@ export const SSAChatPane: React.FC = () => {
msg.content
)}
{/* SAP / Result 卡片 - 统一行为selectRecord + 打开工作区 */}
{(msg.artifactType === 'sap' || msg.artifactType === 'result') && msg.recordId && (() => {
const rec = analysisHistory.find((r: AnalysisRecord) => r.id === msg.recordId);
const isCompleted = rec?.status === 'completed';
@@ -350,6 +380,49 @@ export const SSAChatPane: React.FC = () => {
);
})}
{/* Phase II: 流式对话消息(来自 useSSAChat */}
{chatMessages.map((msg: ChatMessage) => (
<div
key={msg.id}
className={`message ${msg.role === 'user' ? 'message-user' : 'message-ai'} slide-up`}
>
<div className={`message-avatar ${msg.role === 'user' ? 'user-avatar' : 'ai-avatar'}`}>
{msg.role === 'user' ? <User size={12} /> : <Bot size={12} />}
</div>
<div className={`message-bubble ${msg.role === 'user' ? 'user-bubble' : 'ai-bubble'}`}>
{/* 意图标签 */}
{msg.role === 'assistant' && msg.intent && (
<IntentBadge intent={msg.intent} />
)}
{/* 深度思考折叠 */}
{msg.role === 'assistant' && msg.thinking && (
<ThinkingBlock
content={msg.thinking}
isThinking={msg.status === 'generating'}
defaultExpanded={false}
/>
)}
{/* 消息内容 */}
{msg.status === 'generating' && !msg.content ? (
<div className="thinking-dots">
<span className="dot" />
<span className="dot" />
<span className="dot" />
</div>
) : msg.status === 'error' ? (
<div className="chat-error-msg">
<AlertCircle size={14} className="text-red-500" />
<span>{msg.content}</span>
</div>
) : (
<div className="chat-msg-content">{msg.content}</div>
)}
</div>
</div>
))}
{/* 数据画像生成中指示器 */}
{dataProfileLoading && (
<div className="message message-ai slide-up">
@@ -365,8 +438,8 @@ export const SSAChatPane: React.FC = () => {
</div>
)}
{/* AI 正在思考指示器 */}
{(isLoading || isPlanLoading) && (
{/* AI 正在思考指示器仅老流程使用Phase II 由 chatMessages 中的 generating 状态处理) */}
{(isLoading || isPlanLoading) && !isGenerating && (
<div className="message message-ai slide-up">
<div className="message-avatar ai-avatar">
<Bot size={12} />
@@ -381,14 +454,7 @@ export const SSAChatPane: React.FC = () => {
</div>
)}
{/*
Phase 2A 新流程:
1. 上传数据 → 显示数据质量报告(已在上方处理)
2. 用户输入分析问题 → AI 回复消息中包含 SAP 卡片(通过 msg.artifactType === 'sap'
旧的"数据挂载成功消息"已移除,避免在没有用户输入时就显示 SAP 卡片
*/}
{/* Phase Q: 追问卡片 */}
{/* Phase Q: 追问卡片(保留向后兼容) */}
{pendingClarification && (
<div className="message-row assistant">
<div className="avatar-col"><Bot size={18} /></div>
@@ -396,7 +462,22 @@ export const SSAChatPane: React.FC = () => {
<ClarificationCard
cards={pendingClarification.cards}
onSelect={handleClarificationSelect}
disabled={isLoading}
disabled={isLoading || isGenerating}
/>
</div>
</div>
)}
{/* Phase III/IV: AskUser 交互卡片H3 统一模型) */}
{pendingQuestion && currentSession?.id && (
<div className="message-row assistant">
<div className="avatar-col"><Bot size={18} /></div>
<div className="msg-content">
<AskUserCard
event={pendingQuestion}
onRespond={(response: AskUserResponseData) => respondToQuestion(currentSession.id, response)}
onSkip={(questionId: string) => skipQuestion(currentSession.id, questionId)}
disabled={isGenerating || isExecuting}
/>
</div>
</div>
@@ -489,31 +570,41 @@ export const SSAChatPane: React.FC = () => {
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
<button
<button
className={`upload-btn ${mountedFile ? 'disabled' : ''}`}
onClick={() => fileInputRef.current?.click()}
disabled={!!mountedFile || isUploading}
disabled={!!mountedFile || isUploading || isGenerating}
>
<Paperclip size={18} />
</button>
<textarea
className="message-input"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="发送消息,或点击回形针 📎 上传数据触发分析..."
placeholder={isGenerating ? 'AI 正在回复...' : '发送消息,或点击回形针 📎 上传数据触发分析...'}
rows={1}
disabled={isLoading}
disabled={isLoading || isGenerating}
/>
<button
className="send-btn"
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
>
<ArrowUp size={14} />
</button>
{isGenerating ? (
<button
className="send-btn abort-btn"
onClick={abortChat}
title="中断回复"
>
<Square size={14} />
</button>
) : (
<button
className="send-btn"
onClick={handleSend}
disabled={!inputValue.trim() || isLoading}
>
<ArrowUp size={14} />
</button>
)}
</div>
</div>
<div className="input-hint">
@@ -530,15 +621,32 @@ interface EngineStatusProps {
isLoading: boolean;
isUploading: boolean;
isProfileLoading?: boolean;
isStreaming?: boolean;
streamIntent?: ChatIntentType | null;
}
const EngineStatus: React.FC<EngineStatusProps> = ({
isExecuting,
isLoading,
const INTENT_LABELS: Record<ChatIntentType, string> = {
chat: '对话',
explore: '数据探索',
consult: '方法咨询',
analyze: '统计分析',
discuss: '结果讨论',
feedback: '结果改进',
};
const EngineStatus: React.FC<EngineStatusProps> = ({
isExecuting,
isLoading,
isUploading,
isProfileLoading
isProfileLoading,
isStreaming,
streamIntent,
}) => {
const getStatus = () => {
if (isStreaming) {
const label = streamIntent ? INTENT_LABELS[streamIntent] : 'AI';
return { text: `${label} 生成中...`, className: 'status-streaming' };
}
if (isExecuting) {
return { text: 'R Engine Running...', className: 'status-running' };
}
@@ -564,4 +672,16 @@ const EngineStatus: React.FC<EngineStatusProps> = ({
);
};
/**
* 意图标签组件
*/
const IntentBadge: React.FC<{ intent: ChatIntentType }> = ({ intent }) => {
const label = INTENT_LABELS[intent] || intent;
return (
<span className={`intent-badge intent-${intent}`}>
{label}
</span>
);
};
export default SSAChatPane;