feat(dc): Complete Tool C quick action buttons Phase 1-2 - 7 functions
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
This commit is contained in:
@@ -5,11 +5,10 @@
|
||||
* Day 5: 使用通用 Chat 组件(基于 Ant Design X + X SDK)
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { MessageSquare, X, Upload } from 'lucide-react';
|
||||
import { ChatContainer } from '@/shared/components/Chat';
|
||||
import { MessageRenderer } from '@/shared/components/Chat/MessageRenderer';
|
||||
import { message as antdMessage } from 'antd';
|
||||
import { StreamingSteps, StreamStep } from './StreamingSteps';
|
||||
import { App } from 'antd';
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
@@ -26,51 +25,118 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
onFileUpload,
|
||||
onDataUpdate,
|
||||
}) => {
|
||||
const [isExecuting, setIsExecuting] = useState(false);
|
||||
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 handleExecuteCode = async (code: string, messageId?: string) => {
|
||||
/**
|
||||
* ✨ 流式处理用户请求(自动执行代码)
|
||||
*/
|
||||
const handleStreamProcess = useCallback(async (userMessage: string) => {
|
||||
if (!sessionId) {
|
||||
antdMessage.error('会话未初始化');
|
||||
message.error('会话未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsExecuting(true);
|
||||
// ✅ 修复:清空输入框
|
||||
setInputValue('');
|
||||
setIsStreaming(true);
|
||||
setStreamSteps([]);
|
||||
|
||||
try {
|
||||
// 调用后端执行代码的 API
|
||||
const response = await fetch(`/api/v1/dc/tool-c/ai/execute`, {
|
||||
const response = await fetch('/api/v1/dc/tool-c/ai/stream-process', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
code,
|
||||
messageId: messageId || Date.now().toString()
|
||||
body: JSON.stringify({
|
||||
sessionId,
|
||||
message: userMessage,
|
||||
maxRetries: 3,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('执行失败');
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 更新数据表格
|
||||
if (result.data?.newDataPreview) {
|
||||
onDataUpdate(result.data.newDataPreview);
|
||||
antdMessage.success('代码执行成功');
|
||||
} else {
|
||||
throw new Error(result.data?.error || '执行失败');
|
||||
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) {
|
||||
antdMessage.error(error.message || '执行失败');
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
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 shadow-lg">
|
||||
<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">
|
||||
@@ -121,69 +187,71 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// ⭐ 使用通用 Chat 组件(基于 Ant Design X)
|
||||
<ChatContainer
|
||||
conversationType="tool-c"
|
||||
conversationKey={sessionId}
|
||||
providerConfig={{
|
||||
apiEndpoint: `/api/v1/dc/tool-c/ai/generate`,
|
||||
requestFn: async (message: string) => {
|
||||
try {
|
||||
// 只生成代码,不执行
|
||||
const response = await fetch(`/api/v1/dc/tool-c/ai/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sessionId, message }),
|
||||
});
|
||||
<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>
|
||||
|
||||
if (!response.ok) throw new Error('请求失败');
|
||||
const result = await response.json();
|
||||
|
||||
// 返回代码和解释,不执行
|
||||
return {
|
||||
messageId: result.data?.messageId,
|
||||
explanation: result.data?.explanation,
|
||||
code: result.data?.code,
|
||||
success: true, // 生成成功
|
||||
metadata: {
|
||||
messageId: result.data?.messageId, // 保存 messageId 用于执行
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
// 如果生成代码失败,可能是简单问答
|
||||
// 返回纯文本回复
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}}
|
||||
customMessageRenderer={(msgInfo) => (
|
||||
<MessageRenderer
|
||||
messageInfo={msgInfo}
|
||||
onExecuteCode={handleExecuteCode}
|
||||
isExecuting={isExecuting}
|
||||
/>
|
||||
)}
|
||||
senderProps={{
|
||||
placeholder: '输入数据处理需求...(Enter发送)',
|
||||
value: inputValue,
|
||||
onChange: (value) => setInputValue(value),
|
||||
}}
|
||||
onMessageSent={() => {
|
||||
// 发送消息后清空输入框
|
||||
setInputValue('');
|
||||
}}
|
||||
onMessageReceived={(msg) => {
|
||||
// 如果返回了新数据,更新表格
|
||||
if (msg.metadata?.newDataPreview) {
|
||||
onDataUpdate(msg.metadata.newDataPreview);
|
||||
}
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('Chat error:', error);
|
||||
antdMessage.error(error.message || '处理失败');
|
||||
}}
|
||||
style={{ height: '100%' }}
|
||||
/>
|
||||
{/* 输入框区域 */}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user