Files
AIclinicalresearch/docs/03-业务模块/DC-数据清洗整理/04-开发计划/工具C_Day4-5前端开发计划.md
HaHafeng 9b81aef9a7 feat(dc): Add multi-metric transformation feature (direction 1+2)
Summary:
- Implement intelligent multi-metric grouping detection algorithm
- Add direction 1: timepoint-as-row, metric-as-column (analysis format)
- Add direction 2: timepoint-as-column, metric-as-row (display format)
- Fix column name pattern detection (FMA___ issue)
- Maintain original Record ID order in output
- Add full-select/clear buttons in UI
- Integrate into TransformDialog with Radio selection
- Update 3 documentation files

Technical Details:
- Python: detect_metric_groups(), apply_multi_metric_to_long(), apply_multi_metric_to_matrix()
- Backend: 3 new methods in QuickActionService
- Frontend: MultiMetricPanel.tsx (531 lines)
- Total: ~1460 lines of new code

Status: Fully tested and verified, ready for production
2025-12-21 15:06:15 +08:00

1297 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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 前端开发计划(最终版)
> **制定日期**: 2025-12-07
> **开发目标**: Tool C前端MVP - AI驱动的科研数据编辑器
> **预计工时**: 12-16小时Day 4: 6-8h, Day 5: 6-8h
> **核心目标**: 端到端AI数据清洗流程可用
---
## 🎯 核心决策
| 决策项 | 最终方案 | 理由 |
|--------|---------|------|
| **表格组件** | **AG Grid Community** | 用户强烈要求Excel级体验 |
| **开发优先级** | **AI核心功能优先** | 先验证后端UI细节后续优化 |
| **UI风格** | **严格还原原型V6** | Emerald绿色主题符合Tool C定位 |
| **测试数据** | **真实医疗数据** | cqol-demo.csv (21列x300+行) |
| **Day 4-5目标** | **AI核心功能可用** | 上传→AI对话→执行→更新表格 |
| **代码风格** | **同Tool B标准** | TypeScript + Tailwind + Lucide |
---
## 📦 技术栈
### 核心依赖
```json
{
"ag-grid-community": "^31.0.0", // 表格核心
"ag-grid-react": "^31.0.0", // React集成
"react": "^18.2.0",
"react-router-dom": "^6.x",
"lucide-react": "^0.x", // 图标库
"axios": "^1.x" // HTTP客户端
}
```
### 技术选型
- **React 18**: Hooks + TypeScript
- **Tailwind CSS**: 原子化CSS
- **AG Grid Community**: 开源版功能足够MVP
- **Prism.js**: 代码语法高亮
- **React Markdown**: Markdown渲染AI回复
---
## 📐 页面布局设计
```
┌────────────────────────────────────────────────────────────┐
│ Header (h-14) │
│ [🟢 科研数据编辑器] lung_cancer.csv [撤销/重做] [导出] │
└────────────────────────────────────────────────────────────┘
┌──────────────────────────┬─────────────────────────────────┐
│ Left Panel (flex-1) │ Right Sidebar (w-[420px]) │
│ │ │
│ ┌──────────────────────┐ │ ┌───────────────────────────┐ │
│ │ Toolbar (h-16) │ │ │ Tab: [Chat] [Insights] │ │
│ │ 7个快捷按钮 + 搜索 │ │ └───────────────────────────┘ │
│ └──────────────────────┘ │ │
│ │ ┌───────────────────────────┐ │
│ ┌──────────────────────┐ │ │ Chat Messages Area │ │
│ │ │ │ │ - 用户消息 │ │
│ │ AG Grid Table │ │ │ - AI消息含代码块 │ │
│ │ (Excel风格) │ │ │ - 系统消息 │ │
│ │ │ │ │ │ │
│ │ 21列 x 300+行 │ │ └───────────────────────────┘ │
│ │ │ │ │
│ │ 支持: │ │ ┌───────────────────────────┐ │
│ │ - 列排序 │ │ │ Input + Send Button │ │
│ │ - 列宽调整 │ │ └───────────────────────────┘ │
│ │ - 单元格编辑(后期) │ │ │
│ │ - 选择高亮 │ │ │
│ └──────────────────────┘ │ │
└──────────────────────────┴─────────────────────────────────┘
```
---
## 🗂️ 文件结构规划
```
frontend-v2/src/modules/dc/pages/tool-c/
├── index.tsx # 主入口(状态管理+布局)
├── components/
│ ├── Header.tsx # 顶部栏
│ ├── Toolbar.tsx # 工具栏7个按钮
│ ├── DataGrid.tsx # AG Grid表格核心
│ ├── Sidebar.tsx # 右侧栏容器
│ ├── ChatPanel.tsx # Chat面板
│ ├── InsightsPanel.tsx # Insights面板
│ ├── MessageList.tsx # 消息列表
│ ├── MessageItem.tsx # 单条消息
│ ├── CodeBlock.tsx # 代码块渲染
│ └── InputArea.tsx # 输入框
├── hooks/
│ ├── useToolC.ts # 核心状态管理Hook
│ ├── useSession.ts # Session管理
│ ├── useChat.ts # AI对话逻辑
│ └── useDataGrid.ts # 表格数据管理
└── types/
└── index.ts # TypeScript类型定义
frontend-v2/src/modules/dc/api/
└── toolC.ts # API封装6个方法
```
---
## ⏱️ Day 4 开发计划6-8小时
### 阶段1项目初始化1小时
#### 1.1 安装依赖
```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 更新路由配置
```typescript
// src/App.tsx 或路由配置文件
<Route path="/data-cleaning/tool-c" element={<ToolC />} />
```
#### 1.4 更新Portal页面
```typescript
// src/modules/dc/pages/Portal.tsx
// 将Tool C的status从'disabled'改为'ready'
{
id: 'tool-c',
title: '科研数据编辑器',
status: 'ready', // ⭐ 修改这里
route: '/data-cleaning/tool-c'
}
```
**交付物**
- ✅ 依赖安装完成
- ✅ 文件结构创建
- ✅ Portal可点击进入Tool C
---
### 阶段2页面框架搭建2小时
#### 2.1 创建主入口 (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">
<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="生成新变量" 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="横纵转换" 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="查重" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
<ToolbarButton icon={Wand2} label="多重插补" 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="筛选分析集" 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完成带返回按钮
- ✅ Toolbar完成7个按钮
- ✅ 基础布局可见
---
### 阶段3AG Grid集成3-4小时
#### 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 }) => {
// 将列定义转换为AG Grid格式
const columnDefs: ColDef[] = useMemo(() => {
return columns.map(col => ({
field: col.id,
headerName: col.name,
sortable: true,
filter: true,
resizable: true,
editable: false, // MVP阶段暂不支持编辑
width: 120,
minWidth: 80,
// 根据类型设置样式
cellClass: (params) => {
if (params.value === null || params.value === undefined || params.value === '') {
return 'bg-red-50 text-red-400 italic';
}
return '';
},
}));
}, [columns]);
// 默认列配置
const defaultColDef: ColDef = {
flex: 0,
sortable: true,
filter: true,
resizable: true,
};
// 如果没有数据,显示空状态
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"></p>
<p>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样式
```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 测试数据加载
```typescript
// 临时测试在index.tsx中硬编码测试数据
const mockData = [
{ id: 'P001', age: 27, sex: '女', bmi: 23.1, smoke: '否' },
{ 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: '吸烟史', type: 'category' },
];
```
**交付物**
- ✅ AG Grid成功渲染
- ✅ 缺失值高亮显示
- ✅ 列排序/过滤可用
- ✅ 列宽可调整
---
## ⏱️ Day 5 开发计划6-8小时
### 阶段4AI Chat面板3小时
#### 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} />
</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">👋 AI数据分析师</p>
<p>"把年龄大于60的标记为老年组"</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正在思考...</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;
// 系统消息
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>
);
}
// 用户消息
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消息
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} />
</>
)}
{code.status === 'running' && '执行中...'}
{code.status === 'success' && (
<>
<CheckCircle2 size={12} />
</>
)}
{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} />
</>
) : (
<>
<Copy size={12} />
</>
)}
</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你想做什么..."
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>
<kbd className="px-1 py-0.5 bg-slate-100 rounded">Shift + Enter</kbd>
</div>
</div>
);
};
export default InputArea;
```
**交付物**
- ✅ Chat面板完整UI
- ✅ 消息列表渲染
- ✅ 代码块语法高亮
- ✅ 输入框交互
---
### 阶段5API集成2-3小时
#### 5.1 创建API封装
```typescript
// api/toolC.ts
import axios from 'axios';
const BASE_URL = '/api/v1/dc/tool-c';
// Session管理
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);
// 获取预览数据
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',
})));
}
// 添加欢迎消息
setMessages([{
id: Date.now(),
role: 'system',
content: `✅ 文件上传成功!共${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);
}
}, []);
// 发送AI消息一步到位生成+执行)
const handleSendMessage = useCallback(async (message: string) => {
if (!sessionId) {
alert('请先上传文件');
return;
}
// 添加用户消息
setMessages(prev => [...prev, {
id: Date.now(),
role: 'user',
content: message,
}]);
try {
setIsLoading(true);
// 调用process接口生成+执行,一步到位)
const result = await api.processMessage(sessionId, message);
if (result.success) {
// 添加AI消息带代码和执行结果
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',
},
}]);
// 如果执行成功,更新表格数据
if (result.data.executeResult.success && result.data.executeResult.newDataPreview) {
setData(result.data.executeResult.newDataPreview);
setMessages(prev => [...prev, {
id: Date.now() + 2,
role: 'system',
content: `✅ 代码执行成功!表格已更新。${result.data.retryCount > 0 ? `(重试${result.data.retryCount}次后成功)` : ''}`,
}]);
} 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处理失败:', error);
setMessages(prev => [...prev, {
id: Date.now() + 1,
role: 'system',
content: `❌ 处理失败: ${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方法封装
- ✅ useToolC核心Hook
- ✅ 端到端流程可用
---
### 阶段6文件上传 + InsightsPanel1小时
#### 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">
CSV, XLSX, XLS10MB
</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></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"></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"></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面板
---
## 🎯 Day 4-5 验收标准
### 必达目标MVP
- [x] **页面可访问**Portal可点击进入Tool C
- [x] **文件上传**支持CSV/Excel上传
- [x] **表格展示**AG Grid渲染真实数据cqol-demo.csv
- [x] **AI对话**右侧Chat面板可发送消息
- [x] **代码生成**AI生成Python代码并显示
- [x] **代码执行**:点击执行按钮,表格数据更新
- [x] **对话历史**:多轮对话正常显示
### 可选目标Day 6优化
- [ ] 单元格手动编辑
- [ ] 工具栏按钮功能
- [ ] 撤销/重做
- [ ] 导出Excel
- [ ] 数据洞察卡片完善
- [ ] 响应式布局
---
## 📝 测试场景
### 场景1上传文件
1. 访问 `/data-cleaning/tool-c`
2. 点击"上传CSV/Excel文件"
3. 选择 `cqol-demo.csv`
4. 验证表格显示21列x300+行数据
### 场景2AI缺失值处理
1. 在Chat输入"把sex列的缺失值填补为众数"
2. 验证AI生成代码
3. 点击"执行代码"
4. 验证:表格数据更新,缺失值消失
### 场景3AI年龄分组
1. 在Chat输入"把age列按18、60分为未成年、成年、老年三组"
2. 验证AI生成代码
3. 点击"执行代码"
4. 验证表格新增age_group列
### 场景4对话历史
1. 发送多条消息
2. 验证:所有对话保留
3. 刷新页面
4. 验证:对话历史加载正常
---
## 🚨 风险与应对
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|---------|
| AG Grid学习成本高 | 中 | 中 | Day 4上午集中学习官方文档 |
| API调试时间长 | 高 | 中 | 先用Mock数据再对接真实API |
| 代码语法高亮问题 | 低 | 低 | Prism.js配置不复杂有现成方案 |
| 性能问题300+行数据)| 低 | 低 | AG Grid自带虚拟滚动无需优化 |
---
## 📦 依赖安装清单
```bash
# AG Grid
npm install ag-grid-community ag-grid-react
# 代码高亮
npm install prismjs @types/prismjs
# Markdown渲染可选
npm install react-markdown
# 已有依赖(无需安装)
# - 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实现代码](../../frontend-v2/src/modules/dc/pages/tool-b/)
- [原型设计V6](../../03-UI设计/工具C_原型设计V6%20.html)
---
## ✅ 最终交付物清单
### Day 4
- [x] 文件结构创建10个文件
- [x] Header组件
- [x] Toolbar组件
- [x] DataGrid组件AG Grid
- [x] 基础布局完成
### Day 5
- [x] Sidebar组件
- [x] ChatPanel组件
- [x] MessageItem组件
- [x] CodeBlock组件
- [x] InputArea组件
- [x] InsightsPanel组件
- [x] API封装toolC.ts
- [x] useToolC Hook
- [x] 端到端流程测试通过
### 文档
- [x] 本开发计划
- [ ] Day 4-5开发完成总结Day 5结束后
- [ ] API对接文档Day 5结束后
---
**制定人**: AI Assistant
**确认人**: 用户
**开始时间**: Day 4上午
**预期完成**: Day 5下午
---
🚀 **准备好开始Day 4开发了吗**