Summary: - Implement 7 quick action functions (filter, recode, binning, conditional, dropna, compute, pivot) - Refactor to pre-written Python functions architecture (stable and secure) - Add 7 Python operations modules with full type hints - Add 7 frontend Dialog components with user-friendly UI - Fix NaN serialization issues and auto type conversion - Update all related documentation Technical Details: - Python: operations/ module (filter.py, recode.py, binning.py, conditional.py, dropna.py, compute.py, pivot.py) - Backend: QuickActionService.ts with 7 execute methods - Frontend: 7 Dialog components with complete validation - Toolbar: Enable 7 quick action buttons Status: Phase 1-2 completed, basic testing passed, ready for further testing
262 lines
9.6 KiB
TypeScript
262 lines
9.6 KiB
TypeScript
/**
|
||
* 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<SidebarProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
sessionId,
|
||
onFileUpload,
|
||
onDataUpdate,
|
||
}) => {
|
||
const [inputValue, setInputValue] = useState('');
|
||
const [streamSteps, setStreamSteps] = useState<StreamStep[]>([]);
|
||
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 (
|
||
<div
|
||
className="w-[480px] bg-white border-l-2 border-slate-200 flex flex-col"
|
||
style={{ boxShadow: '-8px 0 16px rgba(0, 0, 0, 0.06)' }} /* ✅ 优化4.1:左侧柔和阴影 */
|
||
>
|
||
{/* Header */}
|
||
<div className="h-14 border-b border-slate-200 flex items-center justify-between px-4 bg-gradient-to-r from-emerald-50 to-white">
|
||
<div className="flex items-center gap-2 text-emerald-700 font-semibold">
|
||
<MessageSquare size={18} />
|
||
<span>AI 数据清洗助手</span>
|
||
</div>
|
||
<button
|
||
onClick={onClose}
|
||
className="p-1.5 rounded-lg hover:bg-slate-100 text-slate-400 hover:text-slate-700 transition-colors"
|
||
title="关闭助手"
|
||
>
|
||
<X size={16} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Content */}
|
||
<div className="flex-1 flex flex-col overflow-hidden">
|
||
{/* 如果没有Session,显示上传区域 */}
|
||
{!sessionId ? (
|
||
<div className="flex-1 flex items-center justify-center p-6">
|
||
<div className="text-center space-y-4">
|
||
<div className="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto">
|
||
<Upload className="w-8 h-8 text-emerald-600" />
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-slate-900 mb-2">上传数据文件</h3>
|
||
<p className="text-sm text-slate-500 mb-4">
|
||
支持 CSV、Excel 格式<br />最大 10MB
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="file"
|
||
accept=".csv,.xlsx,.xls"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) onFileUpload(file);
|
||
}}
|
||
className="hidden"
|
||
id="file-upload-sidebar"
|
||
/>
|
||
<label
|
||
htmlFor="file-upload-sidebar"
|
||
className="inline-flex items-center gap-2 px-6 py-3 bg-emerald-600 text-white rounded-lg cursor-pointer hover:bg-emerald-700 transition-all text-sm font-medium"
|
||
>
|
||
<Upload size={16} />
|
||
选择文件
|
||
</label>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex-1 flex flex-col overflow-hidden">
|
||
{/* ✨ 流式步骤展示区域 */}
|
||
<div className="flex-1 overflow-y-auto bg-gradient-to-b from-slate-50 to-white">
|
||
{streamSteps.length > 0 ? (
|
||
<StreamingSteps
|
||
steps={streamSteps}
|
||
/>
|
||
) : (
|
||
/* ✅ 修复:添加欢迎语 */
|
||
<div className="flex flex-col items-center justify-center h-full p-6 text-center space-y-4">
|
||
<div className="w-16 h-16 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-full flex items-center justify-center shadow-lg">
|
||
<MessageSquare className="w-8 h-8 text-white" />
|
||
</div>
|
||
<div>
|
||
<h3 className="text-lg font-semibold text-slate-900 mb-2">
|
||
👋 您好!我是 AI 数据清洗助手
|
||
</h3>
|
||
<p className="text-sm text-slate-600 leading-relaxed max-w-xs">
|
||
我可以帮您处理数据清洗任务,比如:
|
||
</p>
|
||
</div>
|
||
<div className="space-y-2 text-xs text-left text-slate-600 bg-white p-4 rounded-lg border border-slate-200 shadow-sm max-w-sm">
|
||
<div>✨ 填补缺失值(均值、中位数、众数)</div>
|
||
<div>🔄 数据类型转换(文本→数字、日期格式化)</div>
|
||
<div>📊 统计分析(分组计数、描述性统计)</div>
|
||
<div>🧹 数据清洗(删除重复值、异常值处理)</div>
|
||
<div>❓ 数据探索(列信息、缺失率查询)</div>
|
||
</div>
|
||
<p className="text-xs text-slate-400">
|
||
💡 请在下方输入框描述您的需求,我将自动生成并执行代码
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 输入框区域 */}
|
||
<div className="border-t border-slate-200 p-4 bg-white">
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="text"
|
||
value={inputValue}
|
||
onChange={(e) => 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"
|
||
/>
|
||
<button
|
||
onClick={() => {
|
||
if (inputValue.trim() && !isStreaming) {
|
||
handleStreamProcess(inputValue);
|
||
}
|
||
}}
|
||
disabled={isStreaming || !inputValue.trim()}
|
||
className="px-4 py-2 bg-emerald-600 text-white rounded-lg text-sm font-medium hover:bg-emerald-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed"
|
||
>
|
||
{isStreaming ? '处理中...' : '发送'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default Sidebar;
|