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:
2025-12-08 17:38:08 +08:00
parent af325348b8
commit f729699510
158 changed files with 13814 additions and 273 deletions

View File

@@ -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>