/** * Tool C - 科研数据编辑器 * * AI驱动的Excel风格数据清洗工具 * 核心功能:AG Grid表格 + AI Copilot */ import { useState, useEffect } from 'react'; import Header from './components/Header'; import Toolbar from './components/Toolbar'; import DataGrid from './components/DataGrid'; import Sidebar from './components/Sidebar'; import FilterDialog from './components/FilterDialog'; import RecodeDialog from './components/RecodeDialog'; import BinningDialog from './components/BinningDialog'; import ConditionalDialog from './components/ConditionalDialog'; import MissingValueDialog from './components/MissingValueDialog'; import ComputeDialog from './components/ComputeDialog'; import TransformDialog from './components/TransformDialog'; import { useSessionStatus } from './hooks/useSessionStatus'; import * as api from '../../api/toolC'; // ==================== 类型定义 ==================== interface ToolCState { // Session信息 sessionId: string | null; fileName: string; // 表格数据 data: Record[]; columns: Array<{ id: string; name: string; type?: string }>; // AI对话 messages: Message[]; // UI状态 isLoading: boolean; isSidebarOpen: boolean; isAlertClosed: boolean; // ✨ 新增:提示条关闭状态 // ✨ 上传进度状态(Postgres-Only架构 - 异步处理) uploadProgress: number; // 0-100 uploadStatus: 'idle' | 'uploading' | 'parsing' | 'completed' | 'error'; uploadMessage: string; // ✨ 轮询控制(React Query) pollingInfo: { sessionId: string; jobId: string } | null; // ✨ 功能按钮对话框状态 filterDialogVisible: boolean; recodeDialogVisible: boolean; binningDialogVisible: boolean; conditionalDialogVisible: boolean; dropnaDialogVisible: boolean; computeDialogVisible: boolean; pivotDialogVisible: boolean; } interface Message { id: number; role: 'user' | 'assistant' | 'system'; content: string; code?: { content: string; status: 'pending' | 'running' | 'success' | 'error'; }; onExecute?: () => void; } // ==================== 主组件 ==================== const ToolC = () => { const [state, setState] = useState({ sessionId: null, fileName: '', data: [], columns: [], messages: [], isLoading: false, isSidebarOpen: true, isAlertClosed: false, // ✨ 初始状态:未关闭 uploadProgress: 0, uploadStatus: 'idle', uploadMessage: '', pollingInfo: null, // ✨ 轮询控制 filterDialogVisible: false, recodeDialogVisible: false, binningDialogVisible: false, conditionalDialogVisible: false, dropnaDialogVisible: false, computeDialogVisible: false, pivotDialogVisible: false, }); // 更新状态辅助函数 const updateState = (updates: Partial) => { setState((prev) => ({ ...prev, ...updates })); }; // ==================== React Query 轮询(Postgres-Only架构 - 自动串行) ==================== // ✅ 使用 React Query Hook 进行轮询(自动防并发、自动清理) const { progress, isReady, isError } = useSessionStatus({ sessionId: state.pollingInfo?.sessionId || null, jobId: state.pollingInfo?.jobId || null, enabled: !!state.pollingInfo, // ← 有 pollingInfo 时才启用 }); // ✅ 监听状态变化(解析完成时自动加载数据) useEffect(() => { if (isReady && state.pollingInfo) { console.log('[ToolC] ✅ 解析完成(React Query检测),开始加载数据'); // 停止轮询 const currentSessionId = state.pollingInfo.sessionId; updateState({ pollingInfo: null }); // 加载数据 loadPreviewData(currentSessionId); } }, [isReady, state.pollingInfo]); // ✅ 监听轮询错误 useEffect(() => { if (isError) { console.error('[ToolC] ❌ 解析失败(React Query检测)'); updateState({ pollingInfo: null, messages: [ { id: Date.now(), role: 'system', content: `❌ 解析失败,请检查文件格式后重试。`, }, ], isLoading: false, uploadProgress: 0, uploadStatus: 'error', uploadMessage: '解析失败', }); } }, [isError]); // ✅ 更新进度条(基于React Query的轮询结果) useEffect(() => { if (state.pollingInfo && progress > 0) { const progressMessage = progress < 30 ? '正在读取文件...' : progress < 70 ? '正在解析Excel...' : '正在清洗数据...'; updateState({ uploadProgress: progress, uploadStatus: 'parsing', uploadMessage: progressMessage, }); } }, [progress, state.pollingInfo]); // ==================== 加载预览数据(独立函数) ==================== const loadPreviewData = async (sessionId: string) => { try { console.log('[ToolC] 🔄 加载预览数据:', sessionId); // 显示100%进度 updateState({ uploadProgress: 100, uploadStatus: 'completed', uploadMessage: '解析完成!正在加载数据...', }); // 获取预览数据 const preview = await api.getPreviewData(sessionId); console.log('[ToolC] 📦 API 返回结果:', preview); if (preview.success) { const previewData = preview.data.previewData || preview.data.rows || []; console.log('[ToolC] 📊 加载数据成功:', { rows: previewData.length, cols: preview.data.columns?.length || 0, firstRow: previewData[0], }); updateState({ data: previewData, columns: (preview.data.columns || []).map((col) => ({ id: col, name: col, type: 'text', })), messages: [ { id: Date.now(), role: 'system', content: `✅ 解析完成!共 ${preview.data.totalRows || 0} 行 × ${preview.data.totalCols || 0} 列数据。`, }, ], isLoading: false, uploadProgress: 0, uploadStatus: 'idle', uploadMessage: '', }); } else { throw new Error('API returned success=false'); } } catch (error: any) { console.error('[ToolC] ❌ 加载数据失败:', error); updateState({ messages: [ { id: Date.now(), role: 'system', content: `❌ 加载数据失败:${error.message}`, }, ], isLoading: false, uploadProgress: 0, uploadStatus: 'error', uploadMessage: '加载失败', }); } }; // ==================== 文件上传(Postgres-Only架构 - 异步处理) ==================== const handleFileUpload = async (file: File) => { try { // 初始化状态 updateState({ isLoading: true, uploadProgress: 0, uploadStatus: 'uploading', uploadMessage: '正在上传文件...', }); // 1. ⚡ 上传文件(立即返回 sessionId + jobId) const result = await api.uploadFile(file); if (result.success) { const { sessionId, jobId } = result.data as any; console.log('[ToolC] ✅ 文件上传成功,启动 React Query 轮询'); console.log('[ToolC] 📊 sessionId:', sessionId, 'jobId:', jobId); updateState({ sessionId, fileName: file.name, uploadProgress: 10, uploadStatus: 'parsing', uploadMessage: '文件上传成功!正在解析中...', pollingInfo: { sessionId, jobId }, // ✅ 启动 React Query 轮询 messages: [ { id: Date.now(), role: 'system', content: `📤 文件上传成功!正在解析中,请稍候...`, }, ], }); } } catch (error: any) { console.error('[ToolC] 上传失败:', error); updateState({ messages: [ { id: Date.now(), role: 'system', content: `❌ 上传失败:${error.response?.data?.error || error.message}`, }, ], isLoading: false, uploadProgress: 0, uploadStatus: 'error', uploadMessage: '上传失败', }); } }; // ==================== 功能按钮数据更新 ==================== const handleQuickActionDataUpdate = (newData: any[]) => { if (newData && newData.length > 0) { const newColumns = Object.keys(newData[0]).map(key => ({ id: key, name: key, type: 'text', })); updateState({ data: newData, columns: newColumns, }); } }; // ==================== 导出Excel ==================== const handleExport = async () => { if (!state.sessionId) { alert('请先上传文件'); return; } try { // ✅ 从后端读取完整数据(AI处理后的数据已保存到OSS) const response = await fetch(`/api/v1/dc/tool-c/sessions/${state.sessionId}/export`); if (!response.ok) { throw new Error('导出失败'); } // 创建下载链接 const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${state.fileName.replace(/\.[^/.]+$/, '')}_cleaned.xlsx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (error: any) { console.error('导出失败:', error); alert('导出失败:' + error.message); } }; // ==================== 心跳机制 ==================== useEffect(() => { if (!state.sessionId) return; // 每5分钟更新一次心跳 const timer = setInterval(async () => { try { await api.updateHeartbeat(state.sessionId!); console.log('心跳更新成功'); } catch (error) { console.error('心跳更新失败:', error); } }, 5 * 60 * 1000); // 5分钟 return () => clearInterval(timer); }, [state.sessionId]); // ==================== 渲染 ==================== return (
{/* 顶部栏 */}
updateState({ isSidebarOpen: !state.isSidebarOpen })} /> {/* ✨ 上传进度提示(Postgres-Only 异步处理) */} {state.uploadStatus !== 'idle' && state.uploadStatus !== 'error' && (
{state.uploadMessage} {state.uploadProgress}%
)} {/* 主工作区 - ⭐ Phase 1: 添加overflow-hidden禁止页面滚动 */}
{/* 左侧:表格区域 - ⭐ 添加overflow-hidden */}
updateState({ filterDialogVisible: true })} onRecodeClick={() => updateState({ recodeDialogVisible: true })} onBinningClick={() => updateState({ binningDialogVisible: true })} onConditionalClick={() => updateState({ conditionalDialogVisible: true })} onDropnaClick={() => updateState({ dropnaDialogVisible: true })} onComputeClick={() => updateState({ computeDialogVisible: true })} onPivotClick={() => updateState({ pivotDialogVisible: true })} />
{/* ✨ 优化:提示只显示前50行(可关闭) */} {/* ⭐ 已删除:表格仅展示前50行提示(现在是全量显示)*/}
{/* 右侧:AI 数据清洗助手 - 独立滚动 */} {state.isSidebarOpen && ( updateState({ isSidebarOpen: false })} sessionId={state.sessionId} onFileUpload={handleFileUpload} onDataUpdate={(newData) => { // ✅ 修复:同时更新列定义(从新数据中提取列名) if (newData && newData.length > 0) { const newColumns = Object.keys(newData[0]).map(key => ({ id: key, name: key, type: 'text', })); updateState({ data: newData, columns: newColumns, }); } else { updateState({ data: newData }); } }} /> )}
{/* ✨ 功能按钮对话框 */} updateState({ filterDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> updateState({ recodeDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> updateState({ binningDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> ({ field: col.id, headerName: col.name }))} data={state.data} sessionId={state.sessionId} onClose={() => updateState({ conditionalDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> updateState({ dropnaDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> updateState({ computeDialogVisible: false })} onApply={handleQuickActionDataUpdate} /> updateState({ pivotDialogVisible: false })} onApply={handleQuickActionDataUpdate} />
); }; export default ToolC;