/** * Tool C Sidebar组件 * * 右侧栏:AI Copilot * Day 5: 使用通用 Chat 组件(基于 Ant Design X + X SDK) */ import React, { useState, useCallback } from 'react'; import { MessageSquare, X, Upload } from 'lucide-react'; import { StreamingSteps, StreamStep } from './StreamingSteps'; import { App } from 'antd'; interface SidebarProps { isOpen: boolean; onClose: () => void; sessionId: string | null; onFileUpload: (file: File) => void; onDataUpdate: (newData: any[]) => void; } const Sidebar: React.FC = ({ isOpen, onClose, sessionId, onFileUpload, onDataUpdate, }) => { const [inputValue, setInputValue] = useState(''); const [streamSteps, setStreamSteps] = useState([]); const [isStreaming, setIsStreaming] = useState(false); // ✅ 使用 App.useApp() hook 获取 message API(支持动态主题) const { message } = App.useApp(); if (!isOpen) return null; /** * ✨ 流式处理用户请求(自动执行代码) */ const handleStreamProcess = useCallback(async (userMessage: string) => { if (!sessionId) { message.error('会话未初始化'); return; } // ✅ 修复:清空输入框 setInputValue(''); setIsStreaming(true); setStreamSteps([]); try { const response = await fetch('/api/v1/dc/tool-c/ai/stream-process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId, message: userMessage, maxRetries: 3, }), }); if (!response.ok) { throw new Error('请求失败'); } const reader = response.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('无法读取响应流'); } let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // 处理SSE消息(可能有多条) const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留最后一个未完成的行 for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6); if (data === '[DONE]') { setIsStreaming(false); continue; } try { const step: StreamStep = JSON.parse(data); // ✅ 修复:智能更新步骤(同stepName则更新,否则追加) setStreamSteps(prev => { const existingIndex = prev.findIndex(s => s.stepName === step.stepName); if (existingIndex >= 0) { // 更新现有步骤 const updated = [...prev]; updated[existingIndex] = step; return updated; } else { // 追加新步骤 return [...prev, step]; } }); // Step 6完成时,更新左侧数据 if (step.stepName === 'complete' && step.status === 'success' && step.data?.newDataPreview) { onDataUpdate(step.data.newDataPreview); message.success('数据处理完成!'); } // 失败时显示错误 if (step.status === 'failed' && step.stepName === 'complete') { message.error('处理失败:' + (step.error || '未知错误')); } } catch (e) { console.error('解析SSE消息失败:', e); } } } } } catch (error: any) { message.error(error.message || '流式处理失败'); setIsStreaming(false); } }, [sessionId, onDataUpdate, message]); return (
{/* Header */}
AI 数据清洗助手
{/* Content */}
{/* 如果没有Session,显示上传区域 */} {!sessionId ? (

上传数据文件

支持 CSV、Excel 格式
最大 10MB

{ const file = e.target.files?.[0]; if (file) onFileUpload(file); }} className="hidden" id="file-upload-sidebar" />
) : (
{/* ✨ 流式步骤展示区域 */}
{streamSteps.length > 0 ? ( ) : ( /* ✅ 修复:添加欢迎语 */

👋 您好!我是 AI 数据清洗助手

我可以帮您处理数据清洗任务,比如:

✨ 填补缺失值(均值、中位数、众数)
🔄 数据类型转换(文本→数字、日期格式化)
📊 统计分析(分组计数、描述性统计)
🧹 数据清洗(删除重复值、异常值处理)
❓ 数据探索(列信息、缺失率查询)

💡 请在下方输入框描述您的需求,我将自动生成并执行代码

)}
{/* 输入框区域 */}
setInputValue(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter' && !isStreaming && inputValue.trim()) { handleStreamProcess(inputValue); } }} placeholder="输入数据处理需求...(Enter发送)" disabled={isStreaming} className="flex-1 px-4 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 disabled:bg-slate-100 disabled:cursor-not-allowed" />
)}
); }; export default Sidebar;