/** * SSAChatPane - V11 对话区 * * 100% 还原 V11 原型图 * - 顶部 Header(标题 + 返回按钮 + 状态指示) * - 居中对话列表 * - 底部悬浮输入框 */ import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Bot, User, Paperclip, ArrowUp, FileSpreadsheet, X, ArrowLeft, FileSignature, ArrowRight, Loader2, AlertCircle, 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(); const { currentSession, messages, mountedFile, setMountedFile, setCurrentSession, setActivePane, setWorkspaceOpen, currentPlan, isLoading, isExecuting, error, 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(null); const chatEndRef = useRef(null); const messagesContainerRef = useRef(null); // 自动滚动到底部,确保最新内容可见 const scrollToBottom = useCallback(() => { if (messagesContainerRef.current) { messagesContainerRef.current.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: 'smooth' }); } }, []); useEffect(() => { // 延迟滚动,确保 DOM 更新完成 const timer = setTimeout(scrollToBottom, 100); return () => clearTimeout(timer); }, [messages, currentPlan, scrollToBottom]); const handleBack = () => { navigate(-1); }; const handleFileSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploadStatus('uploading'); setError(null); try { setUploadStatus('parsing'); const result = await uploadData(file); setCurrentSession({ id: result.sessionId, title: file.name.replace(/\.(csv|xlsx|xls)$/i, ''), mode: 'analysis', status: 'active', dataSchema: { columns: result.schema.columns.map((c: any) => ({ name: c.name, type: c.type as 'numeric' | 'categorical' | 'datetime' | 'text', uniqueValues: c.uniqueValues, nullCount: c.nullCount, })), rowCount: result.schema.rowCount, preview: [], }, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); setMountedFile({ name: file.name, size: file.size, rowCount: result.schema.rowCount, }); setUploadStatus('success'); addToast('数据读取成功,正在进行质量核查...', 'success'); // Phase 2A: 自动触发数据质量核查 try { await generateDataProfile(result.sessionId); } catch (profileErr) { console.warn('数据画像生成失败,继续使用基础模式:', profileErr); } } catch (err: any) { setUploadStatus('error'); const errorMsg = err?.message || '上传失败,请检查文件格式'; setError(errorMsg); addToast(errorMsg, 'error'); } }; const handleRemoveFile = () => { setMountedFile(null); setWorkspaceOpen(false); setUploadStatus('idle'); setError(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleSend = async () => { if (!inputValue.trim()) return; try { // Phase 2A: 如果已有 session,使用多步骤工作流规划 if (currentSession?.id) { await generateWorkflowPlan(currentSession.id, inputValue); } else { // 没有数据时,使用旧流程 await generatePlan(inputValue); } setInputValue(''); } catch (err: any) { addToast(err?.message || '生成计划失败', 'error'); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // 打开工作区,可选择特定的分析记录 const handleOpenWorkspace = useCallback((recordId?: string) => { if (recordId) { selectAnalysisRecord(recordId); } else { setWorkspaceOpen(true); setActivePane('sap'); } }, [selectAnalysisRecord, setWorkspaceOpen, setActivePane]); const formatFileSize = (bytes: number) => { if (bytes < 1024) return `${bytes}B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`; return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; }; return (
{/* Chat Header */}
{currentSession?.title || '新的统计分析'}
{/* Chat Messages */}
{/* 欢迎语 */}
你好!我是 SSA-Pro 智能统计助手。
你可以直接描述研究目标,我将为你生成分析方案;或者点击下方 📎 上传数据文件,我们将直接开始分析。
{/* Phase 2A: 数据质量核查报告卡片 - 在欢迎语之后、用户消息之前显示 */} {dataProfile && (
)} {/* 动态消息 */} {messages.map((msg: SSAMessage, idx: number) => { const isLastAiMessage = msg.role === 'assistant' && idx === messages.length - 1; const showTypewriter = isLastAiMessage && !msg.artifactType; return (
{msg.role === 'user' ? : }
{showTypewriter ? ( ) : ( msg.content )} {/* SAP 卡片 - 只有消息中明确标记为 sap 类型时才显示 */} {msg.artifactType === 'sap' && msg.recordId && ( )}
); })} {/* 数据画像生成中指示器 */} {dataProfileLoading && (
正在进行数据质量核查...
)} {/* AI 正在思考指示器 */} {(isLoading || isPlanLoading) && (
)} {/* Phase 2A 新流程: 1. 上传数据 → 显示数据质量报告(已在上方处理) 2. 用户输入分析问题 → AI 回复消息中包含 SAP 卡片(通过 msg.artifactType === 'sap') 旧的"数据挂载成功消息"已移除,避免在没有用户输入时就显示 SAP 卡片 */} {/* 自动滚动锚点与占位垫片 - 解决底部输入框遮挡的终极方案 */}
{/* Chat Input */}
{/* 上传进度条 */} {uploadStatus === 'uploading' && (
正在上传...
)} {/* 解析中状态 */} {uploadStatus === 'parsing' && (
正在解析数据结构...
)} {/* 上传错误 */} {uploadStatus === 'error' && error && (
{error}
)} {/* 数据挂载区 */} {mountedFile && uploadStatus !== 'error' && (
{mountedFile.name} {mountedFile.rowCount} rows • {formatFileSize(mountedFile.size)}
)} {/* 输入行 */}