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%)
1332 lines
38 KiB
Markdown
1332 lines
38 KiB
Markdown
# 工具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>(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>装ï¼?个方法)
|
||
```
|
||
|
||
---
|
||
|
||
## â<>±ï¸<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>—?**
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|