Major Changes: - Add StreamingService with OpenAI Compatible format - Upgrade Chat component V2 with Ant Design X integration - Implement AIA module with 12 intelligent agents - Update API routes to unified /api/v1 prefix - Update system documentation Backend (~1300 lines): - common/streaming: OpenAI Compatible adapter - modules/aia: 12 agents, conversation service, streaming integration - Update route versions (RVW, PKB to v1) Frontend (~3500 lines): - modules/aia: AgentHub + ChatWorkspace (100% prototype restoration) - shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook - Update API endpoints to v1 Documentation: - AIA module status guide - Universal capabilities catalog - System overview updates - All module documentation sync Tested: Stream response verified, authentication working Status: AIA V2.0 core completed (85%)
38 KiB
38 KiB
工具C Day 4-5 å‰<C3A5>端开å<E282AC>‘计划(最终版ï¼?
制定日期: 2025-12-07
**å¼€å<E282AC>‘ç›®æ ?*: Tool Cå‰<C3A5>端MVP - AIé©±åŠ¨çš„ç§‘ç ”æ•°æ<C2B0>®ç¼–辑器
预计工时: 12-16å°<C3A5>时(Day 4: 6-8h, Day 5: 6-8hï¼? æ ¸å¿ƒç›®æ ‡: 端到端AIæ•°æ<C2B0>®æ¸…æ´—æµ<C3A6>程å<E280B9>¯ç”¨
🎯 æ ¸å¿ƒå†³ç–
| 决ç–é¡? | 最终方æ¡? | ç<EFBFBD>†ç”± |
|---|---|---|
| è¡¨æ ¼ç»„ä»¶ | AG Grid Community | 用户强烈è¦<EFBFBD>求,Excel级体éª? |
| å¼€å<EFBFBD>‘优先级 | AIæ ¸å¿ƒåŠŸèƒ½ä¼˜å…ˆ | 先验è¯<EFBFBD>å<EFBFBD>Žç«¯ï¼ŒUI细节å<EFBFBD>Žç»ä¼˜åŒ– |
| UIé£Žæ ¼ | ä¸¥æ ¼è¿˜åŽŸåŽŸåž‹V6 | Emerald绿色主题,符å<EFBFBD>ˆTool C定ä½<C3A4> |
| 测试数æ<EFBFBD>® | 真实医疗数æ<EFBFBD>® | cqol-demo.csv (21列x300+è¡? |
| Day 4-5ç›®æ ‡ | AIæ ¸å¿ƒåŠŸèƒ½å<EFBFBD>¯ç”¨ | ä¸Šä¼ â†’AI对è¯<EFBFBD>â†’æ‰§è¡Œâ†’æ›´æ–°è¡¨æ ¼ |
| 代ç <EFBFBD>é£Žæ ¼ | å<EFBFBD>ŒTool Bæ ‡å‡† | TypeScript + Tailwind + Lucide |
📦 æŠ€æœ¯æ ˆ
æ ¸å¿ƒä¾<EFBFBD>èµ–
{
"ag-grid-community": "^31.0.0", // è¡¨æ ¼æ ¸å¿ƒ
"ag-grid-react": "^31.0.0", // React集æˆ<C3A6>
"react": "^18.2.0",
"react-router-dom": "^6.x",
"lucide-react": "^0.x", // å›¾æ ‡åº?
"axios": "^1.x" // HTTP客户�
}
技术选型
- React 18: Hooks + TypeScript
- Tailwind CSS: 原å<C3A5>化CSS
- AG Grid Community: å¼€æº<C3A6>版,功能足够MVP
- Prism.js: 代ç <C3A7>è¯æ³•高亮
- React Markdown: Markdown渲染(AI回å¤<C3A5>ï¼?
ðŸ“<EFBFBD> 页é<C2B5>¢å¸ƒå±€è®¾è®¡
┌────────────────────────────────────────────────────────────�
â”?Header (h-14) â”?
â”?[🟢 ç§‘ç ”æ•°æ<C2B0>®ç¼–辑器] lung_cancer.csv [撤销/é‡<C3A9>å<EFBFBD>š] [导出] â”?
└────────────────────────────────────────────────────────────�
┌──────────────────────────┬─────────────────────────────────�
â”?Left Panel (flex-1) â”?Right Sidebar (w-[420px]) â”?
â”? â”? â”?
�┌──────────────────────��┌───────────────────────────��
â”?â”?Toolbar (h-16) â”?â”?â”?Tab: [Chat] [Insights] â”?â”?
â”?â”?7个快æ<C2AB>·æŒ‰é’?+ æ<>œç´¢ â”?â”?└───────────────────────────â”?â”?
�└──────────────────────�� �
� �┌───────────────────────────��
�┌──────────────────────���Chat Messages Area ��
â”?â”? â”?â”?â”?- 用户消æ<CB86>¯ â”?â”?
â”?â”? AG Grid Table â”?â”?â”?- AI消æ<CB86>¯ï¼ˆå<CB86>«ä»£ç <C3A7>å<EFBFBD>—) â”?â”?
â”?â”? (Excelé£Žæ ¼) â”?â”?â”?- 系统消æ<CB86>¯ â”?â”?
â”?â”? â”?â”?â”? â”?â”?
�� 21�x 300+� ��└───────────────────────────��
â”?â”? â”?â”? â”?
â”?â”? 支æŒ<C3A6>ï¼? â”?â”?┌───────────────────────────â”?â”?
�� - 列排� ���Input + Send Button ��
�� - 列宽调整 ��└───────────────────────────��
â”?â”? - å<>•å…ƒæ ¼ç¼–è¾‘ï¼ˆå<CB86>ŽæœŸï¼?â”?â”? â”?
�� - 选择高亮 �� �
�└──────────────────────�� �
└──────────────────────────┴─────────────────────────────────�
🗂�文件结构规划
frontend-v2/src/modules/dc/pages/tool-c/
├── index.tsx # 主入å<C2A5>£ï¼ˆçжæ€<C3A6>管ç<C2A1>?布局ï¼?
├── components/
â”? ├── Header.tsx # 顶部æ ?
â”? ├── Toolbar.tsx # 工具æ <C3A6>(7个按钮)
â”? ├── DataGrid.tsx # AG Gridè¡¨æ ¼ï¼ˆæ ¸å¿ƒï¼‰
â”? ├── Sidebar.tsx # å<>³ä¾§æ <C3A6>容å™?
â”? ├── ChatPanel.tsx # Chaté<74>¢æ<C2A2>¿
â”? ├── InsightsPanel.tsx # Insightsé<73>¢æ<C2A2>¿
â”? ├── MessageList.tsx # 消æ<CB86>¯åˆ—表
â”? ├── MessageItem.tsx # å<>•æ<E280A2>¡æ¶ˆæ<CB86>¯
â”? ├── CodeBlock.tsx # 代ç <C3A7>å<EFBFBD>—渲æŸ?
� └── InputArea.tsx # 输入�
├── hooks/
â”? ├── useToolC.ts # æ ¸å¿ƒçŠ¶æ€<C3A6>管ç<C2A1>†Hook
â”? ├── useSession.ts # Session管ç<C2A1>†
â”? ├── useChat.ts # AI对è¯<C3A8>逻辑
â”? └── useDataGrid.ts # è¡¨æ ¼æ•°æ<C2B0>®ç®¡ç<C2A1>†
└── types/
└── index.ts # TypeScript类型定义
frontend-v2/src/modules/dc/api/
└── toolC.ts # APIå°<C3A5>装ï¼?个方法)
â<EFBFBD>±ï¸<EFBFBD> Day 4 å¼€å<E282AC>‘计划(6-8å°<C3A5>æ—¶ï¼?
阶段1:项目åˆ<EFBFBD>始化ï¼?å°<C3A5>æ—¶ï¼?
1.1 安装ä¾<C3A4>èµ–
cd frontend-v2
npm install ag-grid-community ag-grid-react
npm install prismjs @types/prismjs
npm install react-markdown
1.2 创建文件结构
mkdir -p src/modules/dc/pages/tool-c/{components,hooks,types}
touch src/modules/dc/pages/tool-c/index.tsx
# ... 创建其他文件
1.3 更新路由é…<C3A9>ç½®
// src/App.tsx 或路由é…<C3A9>置文ä»?
<Route path="/data-cleaning/tool-c" element={<ToolC />} />
1.4 æ›´æ–°Portal页é<C2B5>¢
// src/modules/dc/pages/Portal.tsx
// 将Tool C的status�disabled'改为'ready'
{
id: 'tool-c',
title: 'ç§‘ç ”æ•°æ<C2B0>®ç¼–辑å™?,
status: 'ready', // â?修改这里
route: '/data-cleaning/tool-c'
}
**交付�*�
- âœ?ä¾<C3A4>赖安装完æˆ<C3A6>
- �文件结构创建
- âœ?Portalå<6C>¯ç‚¹å‡»è¿›å…¥Tool C
阶段2:页é<EFBFBD>¢æ¡†æž¶æ<EFBFBD>建(2å°<EFBFBD>æ—¶ï¼?
2.1 创建主入å<C2A5>?(index.tsx)
import { useState } from 'react';
import Header from './components/Header';
import Toolbar from './components/Toolbar';
import DataGrid from './components/DataGrid';
import Sidebar from './components/Sidebar';
interface ToolCState {
sessionId: string | null;
fileName: string;
data: any[];
columns: any[];
messages: any[];
isLoading: boolean;
}
const ToolC = () => {
const [state, setState] = useState<ToolCState>({
sessionId: null,
fileName: 'cqol-demo.csv',
data: [],
columns: [],
messages: [],
isLoading: false,
});
return (
<div className="h-screen w-screen flex flex-col bg-slate-50">
<Header fileName={state.fileName} />
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex flex-col">
<Toolbar />
<div className="flex-1 overflow-auto p-6">
<DataGrid data={state.data} columns={state.columns} />
</div>
</div>
<Sidebar messages={state.messages} />
</div>
</div>
);
};
export default ToolC;
2.2 创建Header组件
// components/Header.tsx
import { Table2, Undo2, Redo2, Download, ArrowLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
interface HeaderProps {
fileName: string;
onUndo?: () => void;
onRedo?: () => void;
onExport?: () => void;
}
const Header: React.FC<HeaderProps> = ({ fileName, onUndo, onRedo, onExport }) => {
const navigate = useNavigate();
return (
<header className="bg-white border-b border-slate-200 h-14 flex-none flex items-center justify-between px-4 z-20 shadow-sm">
<div className="flex items-center gap-3">
<button
onClick={() => navigate('/data-cleaning')}
className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-slate-100 text-slate-600"
>
<ArrowLeft className="w-4 h-4" />
<span className="text-sm">返回</span>
</button>
<div className="h-4 w-px bg-slate-300"></div>
<div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center text-white shadow-md">
<Table2 size={20} />
</div>
<span className="font-bold text-lg text-slate-900">
ç§‘ç ”æ•°æ<EFBFBD>®ç¼–辑å™?
<span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">
Pro
</span>
</span>
<div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
<span className="text-xs text-slate-500 font-mono">{fileName}</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center bg-slate-100 rounded-lg p-1">
<button
onClick={onUndo}
className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"
disabled
>
<Undo2 size={16} />
</button>
<button
onClick={onRedo}
className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"
disabled
>
<Redo2 size={16} />
</button>
</div>
<button
onClick={onExport}
className="flex items-center gap-2 px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-all shadow-md"
>
<Download size={12} /> 导出结果
</button>
</div>
</header>
);
};
export default Header;
2.3 创建Toolbar组件
// components/Toolbar.tsx
import { Calculator, CalendarClock, ArrowLeftRight, FileSearch, Wand2, Filter, Search } from 'lucide-react';
const ToolbarButton = ({ icon: Icon, label, colorClass }: any) => (
<button className={`flex flex-col items-center justify-center w-20 h-14 rounded-lg transition-all hover:shadow-sm ${colorClass}`}>
<Icon className="w-5 h-5 mb-1" />
<span className="text-[10px] font-medium">{label}</span>
</button>
);
const Toolbar = () => {
return (
<div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
<ToolbarButton icon={Calculator} label="生æˆ<C3A6>æ–°å<C2B0>˜é‡? colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
<ToolbarButton icon={CalendarClock} label="æ—¶é—´å·? colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
<ToolbarButton icon={ArrowLeftRight} label="横纵转æ<C2AC>¢" colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100" />
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton icon={FileSearch} label="查é‡<C3A9>" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
<ToolbarButton icon={Wand2} label="多é‡<C3A9>æ<EFBFBD>’è¡¥" colorClass="text-rose-600 bg-rose-50 hover:bg-rose-100" />
<div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
<ToolbarButton icon={Filter} label="ç›é€‰åˆ†æž<C3A6>集" colorClass="text-indigo-600 bg-indigo-50 hover:bg-indigo-100" />
<div className="flex-1"></div>
<div className="relative">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
<input
className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20"
placeholder="æ<>œç´¢å€?.."
/>
</div>
</div>
);
};
export default Toolbar;
**交付�*�
- âœ?Header完æˆ<C3A6>(带返回按钮ï¼?
- âœ?Toolbar完æˆ<C3A6>ï¼?个按钮)
- âœ?基础布局å<E282AC>¯è§<C3A8>
阶段3:AG Grid集æˆ<C3A6>ï¼?-4å°<C3A5>æ—¶ï¼?
3.1 创建DataGrid组件
// components/DataGrid.tsx
import { AgGridReact } from 'ag-grid-react';
import { ColDef } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { useMemo } from 'react';
interface DataGridProps {
data: any[];
columns: Array<{ id: string; name: string; type?: string }>;
onCellValueChanged?: (params: any) => void;
}
const DataGrid: React.FC<DataGridProps> = ({ data, columns, onCellValueChanged }) => {
// 将列定义转æ<C2AC>¢ä¸ºAG Gridæ ¼å¼<C3A5>
const columnDefs: ColDef[] = useMemo(() => {
return columns.map(col => ({
field: col.id,
headerName: col.name,
sortable: true,
filter: true,
resizable: true,
editable: false, // MVP阶段暂ä¸<C3A4>支æŒ<C3A6>编辑
width: 120,
minWidth: 80,
// æ ¹æ<C2B9>®ç±»åž‹è®¾ç½®æ ·å¼<C3A5>
cellClass: (params) => {
if (params.value === null || params.value === undefined || params.value === '') {
return 'bg-red-50 text-red-400 italic';
}
return '';
},
}));
}, [columns]);
// 默认列é…<C3A9>ç½?
const defaultColDef: ColDef = {
flex: 0,
sortable: true,
filter: true,
resizable: true,
};
// 如果没有数æ<C2B0>®ï¼Œæ˜¾ç¤ºç©ºçжæ€?
if (data.length === 0) {
return (
<div className="bg-white border border-slate-200 shadow-sm rounded-xl p-12 text-center">
<div className="text-slate-400 text-sm">
<p className="mb-2">æš‚æ— æ•°æ<EFBFBD>®</p>
<p>请在å<EFBFBD>³ä¾§AI助手ä¸ä¸Šä¼ æ–‡ä»?/p>
</div>
</div>
);
}
return (
<div className="bg-white border border-slate-200 shadow-sm rounded-xl overflow-hidden h-full">
<div className="ag-theme-alpine h-full">
<AgGridReact
rowData={data}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
animateRows={true}
rowSelection="multiple"
onCellValueChanged={onCellValueChanged}
domLayout="normal"
suppressCellFocus={false}
enableCellTextSelection={true}
/>
</div>
</div>
);
};
export default DataGrid;
3.2 自定义AG Gridæ ·å¼<C3A5>
/* 在全局CSSæˆ–ç»„ä»¶å†…æ·»åŠ */
.ag-theme-alpine {
--ag-header-background-color: #f8fafc;
--ag-header-foreground-color: #475569;
--ag-border-color: #e2e8f0;
--ag-row-hover-color: #f0fdf4;
--ag-selected-row-background-color: #d1fae5;
--ag-font-family: inherit;
--ag-font-size: 13px;
}
.ag-theme-alpine .ag-header-cell {
font-weight: 600;
}
.ag-theme-alpine .ag-cell {
display: flex;
align-items: center;
}
3.3 测试数æ<C2B0>®åŠ è½½
// 临时测试:在index.tsxä¸ç¡¬ç¼–ç <C3A7>测试数æ<C2B0>®
const mockData = [
{ id: 'P001', age: 27, sex: 'å¥?, bmi: 23.1, smoke: 'å<EFBFBD>? },
{ id: 'P002', age: 24, sex: '�, bmi: 16.7, smoke: '� },
{ id: 'P003', age: null, sex: '�, bmi: null, smoke: '� },
];
const mockColumns = [
{ id: 'id', name: '病人ID', type: 'text' },
{ id: 'age', name: '年龄', type: 'number' },
{ id: 'sex', name: '性别', type: 'category' },
{ id: 'bmi', name: 'BMI', type: 'number' },
{ id: 'smoke', name: 'å<>¸çƒŸå<C5B8>?, type: 'category' },
];
**交付�*�
- âœ?AG Gridæˆ<C3A6>功渲染
- �缺失值高亮显�
- âœ?列排åº?过滤å<C2A4>¯ç”¨
- âœ?列宽å<C2BD>¯è°ƒæ•?
â<EFBFBD>±ï¸<EFBFBD> Day 5 å¼€å<E282AC>‘计划(6-8å°<C3A5>æ—¶ï¼?
阶段4:AI Chaté<74>¢æ<C2A2>¿ï¼?å°<C3A5>æ—¶ï¼?
4.1 创建Sidebar组件
// components/Sidebar.tsx
import { useState } from 'react';
import { MessageSquare, Lightbulb, X } from 'lucide-react';
import ChatPanel from './ChatPanel';
import InsightsPanel from './InsightsPanel';
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
messages: any[];
onSendMessage: (message: string) => void;
dataStats?: any;
}
const Sidebar: React.FC<SidebarProps> = ({ isOpen, onClose, messages, onSendMessage, dataStats }) => {
const [activeTab, setActiveTab] = useState<'chat' | 'insights'>('chat');
if (!isOpen) return null;
return (
<div className="w-[420px] bg-white border-l border-slate-200 flex flex-col">
{/* Tab Header */}
<div className="h-14 border-b border-slate-200 flex items-center justify-between px-4">
<div className="flex gap-1">
<button
onClick={() => setActiveTab('chat')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
activeTab === 'chat'
? 'bg-emerald-50 text-emerald-700'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
<MessageSquare size={16} />
AI助手
</button>
<button
onClick={() => setActiveTab('insights')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
activeTab === 'insights'
? 'bg-emerald-50 text-emerald-700'
: 'text-slate-600 hover:bg-slate-50'
}`}
>
<Lightbulb size={16} />
æ•°æ<EFBFBD>®æ´žå¯Ÿ
</button>
</div>
<button
onClick={onClose}
className="p-1 rounded-md hover:bg-slate-100 text-slate-400 hover:text-slate-600"
>
<X size={18} />
</button>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-hidden">
{activeTab === 'chat' ? (
<ChatPanel messages={messages} onSendMessage={onSendMessage} />
) : (
<InsightsPanel dataStats={dataStats} />
)}
</div>
</div>
);
};
export default Sidebar;
4.2 创建ChatPanel组件
// components/ChatPanel.tsx
import { useRef, useEffect } from 'react';
import MessageItem from './MessageItem';
import InputArea from './InputArea';
interface ChatPanelProps {
messages: any[];
onSendMessage: (message: string) => void;
isLoading?: boolean;
}
const ChatPanel: React.FC<ChatPanelProps> = ({ messages, onSendMessage, isLoading }) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<div className="flex flex-col h-full">
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.length === 0 && (
<div className="text-center text-slate-400 text-sm py-12">
<p className="mb-2">👋 您好ï¼<EFBFBD>我是您的AIæ•°æ<EFBFBD>®åˆ†æž<EFBFBD>å¸?/p>
<p>试试说:"把年龄大äº?0çš„æ ‡è®°ä¸ºè€<C3A8>å¹´ç»?</p>
</div>
)}
{messages.map((msg) => (
<MessageItem key={msg.id} message={msg} />
))}
{isLoading && (
<div className="flex items-center gap-2 text-slate-400 text-sm">
<div className="animate-spin rounded-full h-4 w-4 border-2 border-emerald-500 border-t-transparent"></div>
<span>AIæ£åœ¨æ€<EFBFBD>è€?..</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<InputArea onSend={onSendMessage} disabled={isLoading} />
</div>
);
};
export default ChatPanel;
4.3 创建MessageItem组件
// components/MessageItem.tsx
import { User, Bot, CheckCircle2, XCircle, Play } from 'lucide-react';
import CodeBlock from './CodeBlock';
interface MessageItemProps {
message: {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
code?: {
content: string;
status: 'pending' | 'running' | 'success' | 'error';
};
onExecute?: () => void;
};
}
const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
const { role, content, code } = message;
// 系统消æ<CB86>¯
if (role === 'system') {
return (
<div className="text-center">
<div className="inline-block bg-slate-100 text-slate-600 text-xs px-3 py-1 rounded-full">
{content}
</div>
</div>
);
}
// 用户消æ<CB86>¯
if (role === 'user') {
return (
<div className="flex items-start gap-3 justify-end">
<div className="bg-emerald-600 text-white px-4 py-2 rounded-2xl rounded-tr-sm max-w-[80%] text-sm">
{content}
</div>
<div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center flex-shrink-0">
<User size={16} className="text-slate-600" />
</div>
</div>
);
}
// AI消æ<CB86>¯
return (
<div className="flex items-start gap-3">
<div className="w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center flex-shrink-0">
<Bot size={16} className="text-emerald-600" />
</div>
<div className="flex-1 space-y-2">
<div className="bg-slate-50 text-slate-800 px-4 py-2 rounded-2xl rounded-tl-sm text-sm">
{content}
</div>
{code && (
<div className="space-y-2">
<CodeBlock code={code.content} language="python" />
<div className="flex items-center gap-2">
<button
onClick={message.onExecute}
disabled={code.status === 'running'}
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
code.status === 'pending'
? 'bg-emerald-600 text-white hover:bg-emerald-700'
: code.status === 'running'
? 'bg-slate-300 text-slate-500 cursor-not-allowed'
: code.status === 'success'
? 'bg-green-100 text-green-700'
: 'bg-red-100 text-red-700'
}`}
>
{code.status === 'pending' && (
<>
<Play size={12} /> 执行代ç <EFBFBD>
</>
)}
{code.status === 'running' && '执行�..'}
{code.status === 'success' && (
<>
<CheckCircle2 size={12} /> 执行æˆ<EFBFBD>功
</>
)}
{code.status === 'error' && (
<>
<XCircle size={12} /> 执行失败
</>
)}
</button>
</div>
</div>
)}
</div>
</div>
);
};
export default MessageItem;
4.4 创建CodeBlock组件
// components/CodeBlock.tsx
import { Copy, Check } from 'lucide-react';
import { useState } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism-tomorrow.css';
import 'prismjs/components/prism-python';
interface CodeBlockProps {
code: string;
language?: string;
}
const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = 'python' }) => {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const highlightedCode = Prism.highlight(
code,
Prism.languages[language] || Prism.languages.python,
language
);
return (
<div className="relative bg-slate-900 rounded-lg overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 bg-slate-800 border-b border-slate-700">
<span className="text-slate-400 text-xs font-mono">{language}</span>
<button
onClick={handleCopy}
className="flex items-center gap-1 text-slate-400 hover:text-white text-xs transition-colors"
>
{copied ? (
<>
<Check size={12} /> å·²å¤<EFBFBD>åˆ?
</>
) : (
<>
<Copy size={12} /> å¤<EFBFBD>制
</>
)}
</button>
</div>
<pre className="p-4 overflow-x-auto text-xs">
<code
className={`language-${language}`}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
</pre>
</div>
);
};
export default CodeBlock;
4.5 创建InputArea组件
// components/InputArea.tsx
import { Send } from 'lucide-react';
import { useState, KeyboardEvent } from 'react';
interface InputAreaProps {
onSend: (message: string) => void;
disabled?: boolean;
}
const InputArea: React.FC<InputAreaProps> = ({ onSend, disabled }) => {
const [input, setInput] = useState('');
const handleSend = () => {
if (!input.trim() || disabled) return;
onSend(input);
setInput('');
};
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="border-t border-slate-200 p-4">
<div className="flex gap-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="告诉AIä½ æƒ³å<C2B3>šä»€ä¹?.."
className="flex-1 resize-none border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500"
rows={3}
disabled={disabled}
/>
<button
onClick={handleSend}
disabled={!input.trim() || disabled}
className="bg-emerald-600 text-white p-3 rounded-lg hover:bg-emerald-700 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed transition-colors"
>
<Send size={18} />
</button>
</div>
<div className="text-xs text-slate-400 mt-2">
æŒ?<kbd className="px-1 py-0.5 bg-slate-100 rounded">Enter</kbd> å<EFBFBD>‘é€<EFBFBD>,
<kbd className="px-1 py-0.5 bg-slate-100 rounded">Shift + Enter</kbd> æ<EFBFBD>¢è¡Œ
</div>
</div>
);
};
export default InputArea;
**交付�*�
- âœ?Chaté<74>¢æ<C2A2>¿å®Œæ•´UI
- âœ?消æ<CB86>¯åˆ—表渲染
- âœ?代ç <C3A7>å<EFBFBD>—è¯æ³•高äº?
- �输入框交�
阶段5:API集æˆ<EFBFBD>ï¼?-3å°<C3A5>æ—¶ï¼?
5.1 创建APIå°<C3A5>装
// api/toolC.ts
import axios from 'axios';
const BASE_URL = '/api/v1/dc/tool-c';
// Session管ç<C2A1>†
export const uploadFile = async (file: File) => {
const formData = new FormData();
formData.append('file', file);
const response = await axios.post(`${BASE_URL}/sessions/upload`, formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
return response.data;
};
export const getSession = async (sessionId: string) => {
const response = await axios.get(`${BASE_URL}/sessions/${sessionId}`);
return response.data;
};
export const getPreviewData = async (sessionId: string) => {
const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/preview`);
return response.data;
};
export const updateHeartbeat = async (sessionId: string) => {
const response = await axios.post(`${BASE_URL}/sessions/${sessionId}/heartbeat`);
return response.data;
};
// AI功能
export const generateCode = async (sessionId: string, message: string) => {
const response = await axios.post(`${BASE_URL}/ai/generate`, {
sessionId,
message,
});
return response.data;
};
export const executeCode = async (sessionId: string, code: string, messageId: string) => {
const response = await axios.post(`${BASE_URL}/ai/execute`, {
sessionId,
code,
messageId,
});
return response.data;
};
export const processMessage = async (sessionId: string, message: string) => {
const response = await axios.post(`${BASE_URL}/ai/process`, {
sessionId,
message,
maxRetries: 3,
});
return response.data;
};
export const getChatHistory = async (sessionId: string, limit = 10) => {
const response = await axios.get(`${BASE_URL}/ai/history/${sessionId}?limit=${limit}`);
return response.data;
};
5.2 åˆ›å»ºæ ¸å¿ƒHook
// hooks/useToolC.ts
import { useState, useCallback } from 'react';
import * as api from '../../api/toolC';
export const useToolC = () => {
const [sessionId, setSessionId] = useState<string | null>(null);
const [fileName, setFileName] = useState('');
const [data, setData] = useState<any[]>([]);
const [columns, setColumns] = useState<any[]>([]);
const [messages, setMessages] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
// ä¸Šä¼ æ–‡ä»¶
const handleFileUpload = useCallback(async (file: File) => {
try {
setIsLoading(true);
const result = await api.uploadFile(file);
if (result.success) {
setSessionId(result.data.sessionId);
setFileName(file.name);
// 获å<C2B7>–预览数æ<C2B0>®
const preview = await api.getPreviewData(result.data.sessionId);
if (preview.success) {
setData(preview.data.rows);
setColumns(preview.data.columns.map((col: string) => ({
id: col,
name: col,
type: 'text',
})));
}
// æ·»åŠ æ¬¢è¿Žæ¶ˆæ<CB86>¯
setMessages([{
id: Date.now(),
role: 'system',
content: `âœ?æ–‡ä»¶ä¸Šä¼ æˆ<C3A6>功ï¼<C3AF>å…±${preview.data.totalRows}è¡?x ${preview.data.totalCols}列`,
}]);
}
} catch (error: any) {
console.error('ä¸Šä¼ å¤±è´¥:', error);
setMessages([{
id: Date.now(),
role: 'system',
content: `â<>?ä¸Šä¼ å¤±è´¥: ${error.message}`,
}]);
} finally {
setIsLoading(false);
}
}, []);
// å<>‘é€<C3A9>AI消æ<CB86>¯ï¼ˆä¸€æ¥åˆ°ä½<C3A4>:生æˆ<C3A6>+执行ï¼?
const handleSendMessage = useCallback(async (message: string) => {
if (!sessionId) {
alert('è¯·å…ˆä¸Šä¼ æ–‡ä»¶');
return;
}
// æ·»åŠ ç”¨æˆ·æ¶ˆæ<CB86>¯
setMessages(prev => [...prev, {
id: Date.now(),
role: 'user',
content: message,
}]);
try {
setIsLoading(true);
// 调用process接å<C2A5>£ï¼ˆç”Ÿæˆ?执行,一æ¥åˆ°ä½<C3A4>)
const result = await api.processMessage(sessionId, message);
if (result.success) {
// æ·»åŠ AI消æ<CB86>¯ï¼ˆå¸¦ä»£ç <C3A7>和执行结果)
setMessages(prev => [...prev, {
id: Date.now() + 1,
role: 'assistant',
content: result.data.explanation,
code: {
content: result.data.code,
status: result.data.executeResult.success ? 'success' : 'error',
},
}]);
// 如果执行æˆ<C3A6>åŠŸï¼Œæ›´æ–°è¡¨æ ¼æ•°æ<C2B0>?
if (result.data.executeResult.success && result.data.executeResult.newDataPreview) {
setData(result.data.executeResult.newDataPreview);
setMessages(prev => [...prev, {
id: Date.now() + 2,
role: 'system',
content: `âœ?代ç <C3A7>执行æˆ<C3A6>功ï¼<C3AF>è¡¨æ ¼å·²æ›´æ–°ã€?{result.data.retryCount > 0 ? `(é‡<EFBFBD>è¯?{result.data.retryCount}次å<EFBFBD>Žæˆ<EFBFBD>功)` : ''}`,
}]);
} else if (!result.data.executeResult.success) {
setMessages(prev => [...prev, {
id: Date.now() + 2,
role: 'system',
content: `â<>?执行失败: ${result.data.executeResult.error}`,
}]);
}
}
} catch (error: any) {
console.error('AI处ç<E2809E>†å¤±è´¥:', error);
setMessages(prev => [...prev, {
id: Date.now() + 1,
role: 'system',
content: `â<>?处ç<E2809E>†å¤±è´¥: ${error.response?.data?.error || error.message}`,
}]);
} finally {
setIsLoading(false);
}
}, [sessionId]);
return {
sessionId,
fileName,
data,
columns,
messages,
isLoading,
handleFileUpload,
handleSendMessage,
};
};
5.3 在主组件ä¸ä½¿ç”¨Hook
// index.tsx (æ›´æ–°)
import { useToolC } from './hooks/useToolC';
const ToolC = () => {
const {
fileName,
data,
columns,
messages,
isLoading,
handleFileUpload,
handleSendMessage,
} = useToolC();
return (
<div className="h-screen w-screen flex flex-col bg-slate-50">
<Header fileName={fileName} />
<div className="flex-1 flex overflow-hidden">
<div className="flex-1 flex flex-col">
<Toolbar />
<div className="flex-1 overflow-auto p-6">
<DataGrid data={data} columns={columns} />
</div>
</div>
<Sidebar
isOpen={true}
onClose={() => {}}
messages={messages}
onSendMessage={handleSendMessage}
/>
</div>
</div>
);
};
**交付�*�
- âœ?6个API方法å°<C3A5>装
- âœ?useToolCæ ¸å¿ƒHook
- âœ?端到端æµ<C3A6>程å<E280B9>¯ç”?
阶段6:文件上ä¼?+ InsightsPanelï¼?å°<C3A5>æ—¶ï¼?
6.1 æ·»åŠ æ–‡ä»¶ä¸Šä¼ UI
// 在ChatPanel䏿·»åŠ ä¸Šä¼ åŒºåŸŸï¼ˆmessages为空时显示)
{messages.length === 0 && (
<div className="text-center py-12">
<div className="mb-6">
<input
type="file"
accept=".csv,.xlsx,.xls"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload(file);
}}
className="hidden"
id="file-upload"
/>
<label
htmlFor="file-upload"
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"
>
<Upload size={18} />
ä¸Šä¼ CSV/Excel文件
</label>
</div>
<p className="text-slate-400 text-sm">
支æŒ<EFBFBD>æ ¼å¼<EFBFBD>:CSV, XLSX, XLS(最å¤?0MBï¼?
</p>
</div>
)}
6.2 创建InsightsPanel组件
// components/InsightsPanel.tsx
import { BarChart3, AlertCircle, Database, Layers } from 'lucide-react';
interface InsightsPanelProps {
dataStats?: {
totalRows: number;
totalCols: number;
missingCount: number;
missingRate: number;
};
}
const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
if (!dataStats) {
return (
<div className="p-6 text-center text-slate-400">
<p>æš‚æ— æ•°æ<EFBFBD>®</p>
</div>
);
}
return (
<div className="p-4 space-y-4 overflow-y-auto">
<div className="bg-slate-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<Database size={16} className="text-slate-600" />
<h3 className="font-semibold text-sm text-slate-900">æ•°æ<EFBFBD>®è§„模</h3>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-600">总行�/span>
<span className="font-medium">{dataStats.totalRows}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-600">总列�/span>
<span className="font-medium">{dataStats.totalCols}</span>
</div>
</div>
</div>
<div className="bg-red-50 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<AlertCircle size={16} className="text-red-600" />
<h3 className="font-semibold text-sm text-red-900">缺失�/h3>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-red-700">缺失数é‡<EFBFBD></span>
<span className="font-medium text-red-900">{dataStats.missingCount}</span>
</div>
<div className="flex justify-between">
<span className="text-red-700">缺失�/span>
<span className="font-medium text-red-900">{dataStats.missingRate.toFixed(1)}%</span>
</div>
</div>
</div>
</div>
);
};
export default InsightsPanel;
**交付�*�
- âœ?æ–‡ä»¶ä¸Šä¼ åŠŸèƒ½
- âœ?Insightsé<73>¢æ<C2A2>¿
🎯 Day 4-5 éªŒæ”¶æ ‡å‡†
å¿…è¾¾ç›®æ ‡ï¼ˆMVPï¼?
- **页é<C2B5>¢å<C2A2>¯è®¿é—?*:Portalå<6C>¯ç‚¹å‡»è¿›å…¥Tool C
- **æ–‡ä»¶ä¸Šä¼ **:支æŒ<C3A6>CSV/Excel上ä¼
- è¡¨æ ¼å±•ç¤ºï¼šAG Grid渲染真实数æ<C2B0>®ï¼ˆcqol-demo.csvï¼?
- **AI对è¯<C3A8>**:å<C5A1>³ä¾§Chaté<74>¢æ<C2A2>¿å<C2BF>¯å<C2AF>‘é€<C3A9>消æ<CB86>?
- **代ç <C3A7>生æˆ<C3A6>**:AI生æˆ<C3A6>Python代ç <C3A7>并显ç¤?
- 代ç <EFBFBD>æ‰§è¡Œï¼šç‚¹å‡»æ‰§è¡ŒæŒ‰é’®ï¼Œè¡¨æ ¼æ•°æ<EFBFBD>®æ›´æ–°
- 对è¯<EFBFBD>历å<EFBFBD>²ï¼šå¤šè½®å¯¹è¯<EFBFBD>æ£å¸¸æ˜¾ç¤?
å<EFBFBD>¯é€‰ç›®æ ‡ï¼ˆDay 6优化ï¼?
- å<EFBFBD>•å…ƒæ ¼æ‰‹åŠ¨ç¼–è¾?
- 工具æ <EFBFBD>按钮功èƒ?
- 撤销/é‡<C3A9>å<EFBFBD>š
- 导出Excel
- æ•°æ<EFBFBD>®æ´žå¯Ÿå<EFBFBD>¡ç‰‡å®Œå–„
- å“<EFBFBD>应å¼<EFBFBD>布局
ðŸ“<EFBFBD> 测试场景
场景1ï¼šä¸Šä¼ æ–‡ä»?
- 访问
/data-cleaning/tool-c - 点击"ä¸Šä¼ CSV/Excel文件"
- 选择
cqol-demo.csv - 验è¯<EFBFBD>ï¼šè¡¨æ ¼æ˜¾ç¤?1列x300+行数æ<C2B0>?
场景2:AI缺失值处ç<EFBFBD>?
- 在Chat输入�把sex列的缺失值填补为众数"
- 验è¯<EFBFBD>:AI生æˆ<EFBFBD>代ç <EFBFBD>
- 点击"执行代ç <C3A7>"
- 验è¯<EFBFBD>ï¼šè¡¨æ ¼æ•°æ<EFBFBD>®æ›´æ–°ï¼Œç¼ºå¤±å€¼æ¶ˆå¤?
场景3:AI年龄分组
- 在Chat输入ï¼?把age列按18ã€?0分为未æˆ<C3A6>å¹´ã€<C3A3>æˆ<C3A6>å¹´ã€<C3A3>è€<C3A8>年三组"
- 验è¯<EFBFBD>:AI生æˆ<EFBFBD>代ç <EFBFBD>
- 点击"执行代ç <C3A7>"
- 验è¯<EFBFBD>ï¼šè¡¨æ ¼æ–°å¢žage_groupåˆ?
场景4:对è¯<EFBFBD>历å<EFBFBD>?
- å<EFBFBD>‘é€<EFBFBD>多æ<EFBFBD>¡æ¶ˆæ<EFBFBD>?
- 验è¯<EFBFBD>:所有对è¯<EFBFBD>ä¿<EFBFBD>ç•?
- 刷新页é<EFBFBD>¢
- 验è¯<EFBFBD>:对è¯<EFBFBD>历å<EFBFBD>²åŠ è½½æ£å¸?
🚨 风险与应�
| 风险 | 概率 | å½±å“<EFBFBD> | 应对措施 |
|---|---|---|---|
| AG Gridå¦ä¹ æˆ<C3A6>本é«? | ä¸? | ä¸? | Day 4上å<C5A0>ˆé›†ä¸å¦ä¹ 官方文档 |
| API调试时间é•? | é«? | ä¸? | 先用Mockæ•°æ<EFBFBD>®ï¼Œå†<EFBFBD>对接真实API |
| 代ç <EFBFBD>è¯æ³•高亮问题 | ä½? | ä½? | Prism.jsé…<C3A9>ç½®ä¸<C3A4>å¤<C3A5>æ<EFBFBD>‚,有现æˆ<C3A6>æ–¹æ¡? |
| 性能问题ï¼?00+行数æ<C2B0>®ï¼‰ | ä½? | ä½? | AG Gridè‡ªå¸¦è™šæ‹Ÿæ»šåŠ¨ï¼Œæ— éœ€ä¼˜åŒ– |
📦 ä¾<C3A4>赖安装清å<E280A6>•
# AG Grid
npm install ag-grid-community ag-grid-react
# 代ç <C3A7>高亮
npm install prismjs @types/prismjs
# Markdown渲染(å<CB86>¯é€‰ï¼‰
npm install react-markdown
# 已有ä¾<C3A4>èµ–ï¼ˆæ— éœ€å®‰è£…ï¼?
# - react
# - react-router-dom
# - lucide-react
# - axios
# - tailwindcss
📚 å<>‚考资æº?
âœ?最终交付物清å<E280A6>•
Day 4
- 文件结构创建�0个文件)
- Header组件
- Toolbar组件
- DataGrid组件(AG Grid�
- 基础布局完æˆ<EFBFBD>
Day 5
- Sidebar组件
- ChatPanel组件
- MessageItem组件
- CodeBlock组件
- InputArea组件
- InsightsPanel组件
- APIå°<EFBFBD>装(toolC.tsï¼?
- useToolC Hook
- 端到端æµ<EFBFBD>程测试通过
文档
- 本开å<EFBFBD>‘计åˆ?
- Day 4-5å¼€å<E282AC>‘完æˆ<C3A6>总结(Day 5结æ<E2809C>Ÿå<C5B8>Žï¼‰
- API对接文档(Day 5结æ<E2809C>Ÿå<C5B8>Žï¼‰
**制定�: AI Assistant
**确认�: 用户
**开始时é—?*: Day 4上å<C5A0>ˆ
预期完æˆ<EFBFBD>: Day 5下å<E280B9>ˆ
🚀 准备好开始Day 4å¼€å<E282AC>‘了å<E280A0>—?