Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
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%)
2026-01-14 19:15:01 +08:00

1332 lines
38 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工具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>¯ç”¨
---
## 🎯 核心决策
| 决策é¡?| 最终方æ¡?| ç<>†ç”± |
|--------|---------|------|
| **表格组件** | **AG Grid Community** | 用户强烈è¦<C3A8>æ±ï¼ŒExcel级体éª?|
| **å¼€å<E282AC>优先级** | **AI核心功能优先** | 先验è¯<C3A8>å<EFBFBD>Žç«¯ï¼ŒUI细èŠå<E2809A>Žç»­ä¼˜åŒ |
| **UI风格** | **严格还原原åžV6** | Emerald绿色主题,符å<C2A6>ˆTool C定ä½<C3A4> |
| **æµè¯•æ•°æ<C2B0>®** | **çœŸå®žåŒ»ç—æ•°æ<C2B0>®** | cqol-demo.csv (21列x300+è¡? |
| **Day 4-5ç®æ ** | **AI核心功能å<C2BD>¯ç”¨** | 上传â†AI对è¯<C3A8>â†æ‰§è¡Œâ†æ´æ°è¡¨æ ¼ |
| **代ç <C3A7>风格** | **å<>ŒTool B标准** | TypeScript + Tailwind + Lucide |
---
## 📦 技术栈
### 核心ä¾<C3A4>èµ
```json
{
"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>ï¼?
---
## ðŸ“<C5B8> 页é<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>(¸ªæŒ‰é®ï¼‰
� ├── 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>装ï¼?个方法)
```
---
## â<>±ï¸<C3AF> Day 4 å¼€å<E282AC>计åˆï¼ˆ6-8å°<C3A5>æ—¶ï¼?
### 阶段1:项ç®åˆ<C3A5>å§åŒï¼?å°<C3A5>æ—¶ï¼?
#### 1.1 安装ä¾<C3A4>èµ
```bash
cd frontend-v2
npm install ag-grid-community ag-grid-react
npm install prismjs @types/prismjs
npm install react-markdown
```
#### 1.2 创建文件结构
```bash
mkdir -p src/modules/dc/pages/tool-c/{components,hooks,types}
touch src/modules/dc/pages/tool-c/index.tsx
# ... 创建其他文件
```
#### 1.3 æ´æ°è·¯ç”±é…<C3A9>ç½®
```typescript
// src/App.tsx æˆè·¯ç”±é…<C3A9>ç½®æ‡ä»?
<Route path="/data-cleaning/tool-c" element={<ToolC />} />
```
#### 1.4 æ´æ°Portal页é<C2B5>¢
```typescript
// 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:页é<C2B5>¢æ¡†æž¶æ<C2B6>­å»ºï¼ˆ2å°<C3A5>æ—¶ï¼?
#### 2.1 åˆå»ºä¸»å…¥å<C2A5>?(index.tsx)
```typescript
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组件
```typescript
// 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组件
```typescript
// 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组件
```typescript
// 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
/* 在全局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>®åŠ è½½
```typescript
// 临时æµè¯•:在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>¯è°ƒæ•?
---
## â<>±ï¸<C3AF> Day 5 å¼€å<E282AC>计åˆï¼ˆ6-8å°<C3A5>æ—¶ï¼?
### 阶段4:AI Chaté<74>¢æ<C2A2>¿ï¼?å°<C3A5>æ—¶ï¼?
#### 4.1 åˆå»ºSidebar组件
```typescript
// 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组件
```typescript
// 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组件
```typescript
// 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组件
```typescript
// 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组件
```typescript
// 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醿ˆ<C3A6>ï¼?-3å°<C3A5>æ—¶ï¼?
#### 5.1 åˆå»ºAPIå°<C3A5>装
```typescript
// 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
```typescript
// 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: `â<EFBFBD>?上传失败: ${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: `â<EFBFBD>?执行失败: ${result.data.executeResult.error}`,
}]);
}
}
} catch (error: any) {
console.error('AI处ç<E2809E>†å¤±è´¥:', error);
setMessages(prev => [...prev, {
id: Date.now() + 1,
role: 'system',
content: `â<EFBFBD>?处ç<E2809E>†å¤±è´¥: ${error.response?.data?.error || error.message}`,
}]);
} finally {
setIsLoading(false);
}
}, [sessionId]);
return {
sessionId,
fileName,
data,
columns,
messages,
isLoading,
handleFileUpload,
handleSendMessage,
};
};
```
#### 5.3 在主组件中使用Hook
```typescript
// 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
```typescript
// 在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组件
```typescript
// 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ï¼?
- [x] **页é<C2B5>¢å<C2A2>¯è®¿é—?*:Portalå<6C>¯ç¹å‡»è¿å…¥Tool C
- [x] **文件上传**:支æŒ<C3A6>CSV/Excel上传
- [x] **表格展示**:AG Grid渲染真实数æ<C2B0>®ï¼ˆcqol-demo.csvï¼?
- [x] **AI对è¯<C3A8>**:å<C5A1>³ä¾§Chaté<74>¢æ<C2A2>¿å<C2BF>¯å<C2AF>é€<C3A9>消æ<CB86>?
- [x] **代ç <C3A7>生æˆ<C3A6>**:AI生æˆ<C3A6>Python代ç <C3A7>并显ç¤?
- [x] **代ç <C3A7>执行**:ç¹å‡»æ‰§è¡ŒæŒ‰é®ï¼Œè¡¨æ ¼æ•°æ<C2B0>®æ´æ°
- [x] **对è¯<C3A8>历å<E280A0>²**:多轮对è¯<C3A8>正常显ç¤?
### å<>¯é€‰ç®æ ‡ï¼ˆDay 6优åŒï¼?
- [ ] å<>•元格æ‰åЍç¼è¾?
- [ ] 工具æ <C3A6>按é®åŠŸèƒ?
- [ ] 撤销/é‡<C3A9>å<EFBFBD>š
- [ ] 导出Excel
- [ ] æ•°æ<C2B0>®æ´žå¯Ÿå<C5B8>¡ç‰‡å®Œå
- [ ] å“<C3A5>应å¼<C3A5>布局
---
## ðŸ“<C5B8> 测试场景
### 场景1:上传æ‡ä»?
1. 访问 `/data-cleaning/tool-c`
2. 点击"上传CSV/Excelæ‡ä»¶"
3. 选择 `cqol-demo.csv`
4. 验è¯<C3A8>:表格显ç¤?1列x300+行数æ<C2B0>?
### 场景2:AI缺失值处ç<E2809E>?
1. 在Chat输入�把sex列的缺失值填补为众数"
2. 验è¯<C3A8>:AI生æˆ<C3A6>代ç <C3A7>
3. 点击"执行代ç <C3A7>"
4. 验è¯<C3A8>:表格数æ<C2B0>®æ´æ°ï¼Œç¼ºå¤±å€¼æ¶ˆå¤?
### 场景3:AI年龄分组
1. 在Chat输入ï¼?把age列按18ã€?0分为未æˆ<C3A6>å¹´ã€<C3A3>æˆ<C3A6>å¹´ã€<C3A3>è€<C3A8>年三组"
2. 验è¯<C3A8>:AI生æˆ<C3A6>代ç <C3A7>
3. 点击"执行代ç <C3A7>"
4. 验è¯<C3A8>:表格æ°å¢žage_groupåˆ?
### 场景4:对è¯<C3A8>历å<E280A0>?
1. å<>é€<C3A9>多æ<C5A1>¡æ¶ˆæ<CB86>?
2. 验è¯<C3A8>:所有对è¯<C3A8>ä¿<C3A4>ç•?
3. 刷æ°é¡µé<C2B5>¢
4. 验è¯<C3A8>:对è¯<C3A8>历å<E280A0>²åŠ è½½æ­£å¸?
---
## 🚨 风险与应�
| 风险 | 概率 | å½±å“<C3A5> | 应对措施 |
|------|------|------|---------|
| AG Grid学习æˆ<C3A6>本é«?| ä¸?| ä¸?| Day 4上å<C5A0>ˆé†ä¸­å­¦ä¹ å®˜æ¹æ‡æ¡£ |
| API调试时间é•?| é«?| ä¸?| 先用Mockæ•°æ<C2B0>®ï¼Œå†<C3A5>对接真实API |
| 代ç <C3A7>语法高亮问题 | ä½?| ä½?| Prism.jsé…<C3A9>ç½®ä¸<C3A4>å¤<C3A5>æ<EFBFBD>,有现æˆ<C3A6>æ¹æ¡?|
| 性能问题ï¼?00+行数æ<C2B0>®ï¼‰| ä½?| ä½?| AG Gridè‡ªå¸¦è™šæŸæ»šåŠ¨ï¼Œæ— éœ€ä¼˜åŒ |
---
## 📦 ä¾<C3A4>èµå®‰è£…清å<E280A6>
```bash
# 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
```
---
## 📚 å<>考资æº?
- [AG Grid React Documentation](https://www.ag-grid.com/react-data-grid/)
- [Prism.js Syntax Highlighting](https://prismjs.com/)
- [Tool B实现代ç <C3A7>](../../frontend-v2/src/modules/dc/pages/tool-b/)
- [原åžè®¾è®¡V6](../../03-UI设计/工具C_原åžè®¾è®¡V6%20.html)
---
## âœ?最终交付物清å<E280A6>
### Day 4
- [x] 文件结构创建ï¼?0个æ‡ä»¶ï¼‰
- [x] Header组件
- [x] Toolbar组件
- [x] DataGrid组件(AG Grid�
- [x] 基础布局完æˆ<C3A6>
### Day 5
- [x] Sidebar组件
- [x] ChatPanel组件
- [x] MessageItem组件
- [x] CodeBlock组件
- [x] InputArea组件
- [x] InsightsPanel组件
- [x] APIå°<C3A5>装(toolC.tsï¼?
- [x] useToolC Hook
- [x] 端到端æµ<C3A6>ç¨æµè¯•通过
### 文档
- [x] 本开å<E282AC>计åˆ?
- [ ] Day 4-5å¼€å<E282AC>完æˆ<C3A6>总结(Day 5结æ<E2809C>Ÿå<C5B8>Žï¼‰
- [ ] APIå¯¹æŽ¥æ‡æ¡£ï¼ˆDay 5结æ<E2809C>Ÿå<C5B8>Žï¼‰
---
**制定�*: AI Assistant
**确认�*: 用户
**开始时é—?*: Day 4上å<C5A0>ˆ
**预期完æˆ<C3A6>**: Day 5ä¸å<E280B9>ˆ
---
🚀 **准备好开å§Day 4å¼€å<E282AC>了å<E280A0>—?**