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

38 KiB
Raw Blame History

工具C Day 4-5 å‰<C3A5>端开å<E282AC>计åˆï¼ˆæœ€ç»ˆç‰ˆï¼?

制定日期: 2025-12-07
**å¼€å<E282AC>ç®æ ?*: Tool Cå‰<C3A5>端MVP - AI驱动的ç§ç ”æ•°æ<C2B0>®ç¼è¾å™¨
预计工时: 12-16å°<C3A5>时(Day 4: 6-8h, Day 5: 6-8hï¼? 核心目标: 端到端AIæ•°æ<C2B0>®æ¸…æ´—æµ<C3A6>ç¨å<E280B9>¯ç”¨


🎯 核心决策

决策é¡? 最终方æ¡? ç<EFBFBD>†ç”±
表格组件 AG Grid Community 用户强烈è¦<EFBFBD>æ±ï¼ŒExcel级体éª?
å¼€å<EFBFBD>优先级 AI核心功能优先 先验è¯<EFBFBD>å<EFBFBD>Žç«¯ï¼ŒUI细èŠå<EFBFBD>Žç»­ä¼˜åŒ
UI风格 严格还原原åžV6 Emerald绿色主题,符å<EFBFBD>ˆTool C定ä½<C3A4>
æµè¯•æ•°æ<EFBFBD>® çœŸå®žåŒ»ç—æ•°æ<EFBFBD>® cqol-demo.csv (21列x300+è¡?
Day 4-5ç®æ ‡ AI核心功能å<EFBFBD>¯ç”¨ 上传â†AI对è¯<EFBFBD>â†æ‰§è¡Œâ†æ´æ°è¡¨æ ¼
代ç <EFBFBD>风格 å<EFBFBD>ŒTool B标准 TypeScript + Tailwind + Lucide

📦 技术栈

核心ä¾<EFBFBD>èµ

{
  "ag-grid-community": "^31.0.0",        // 表格核心
  "ag-grid-react": "^31.0.0",            // React醿ˆ<C3A6>
  "react": "^18.2.0",
  "react-router-dom": "^6.x",
  "lucide-react": "^0.x",                // 图标�
  "axios": "^1.x"                        // HTTP客户�
}

技术选型

  • React 18: Hooks + TypeScript
  • Tailwind CSS: 原å­<C3A5>åŒCSS
  • AG Grid Community: å¼€æº<C3A6>版,功能足够MVP
  • Prism.js: 代ç <C3A7>语法高亮
  • React Markdown: Markdown渲染(AIåžå¤<C3A5>ï¼?

ðŸ“<EFBFBD> 页é<C2B5>¢å¸ƒå±€è®¾è®¡

┌────────────────────────────────────────────────────────────�
â”?Header (h-14)                                              â”?
â”?[🟢 ç§ç ”æ•°æ<C2B0>®ç¼è¾å™¨] lung_cancer.csv  [撤销/é‡<C3A9>å<EFBFBD>š] [导出]    â”?
└────────────────────────────────────────────────────────────�
┌──────────────────────────┬─────────────────────────────────�
â”?Left Panel (flex-1)      â”?Right Sidebar (w-[420px])       â”?
â”?                         â”?                                â”?
�┌──────────────────────��┌───────────────────────────��
â”?â”?Toolbar (h-16)       â”?â”?â”?Tab: [Chat] [Insights]    â”?â”?
â”?â”?7个快æ<C2AB>·æŒ‰é?+ æ<>œç´¢    â”?â”?└───────────────────────────â”?â”?
�└──────────────────────��                                �
�                         �┌───────────────────────────��
�┌──────────────────────���Chat Messages Area        ��
â”?â”?                     â”?â”?â”?- 用户消æ<CB86>¯                 â”?â”?
â”?â”? AG Grid Table       â”?â”?â”?- AI消æ<CB86>¯ï¼ˆå<CB86>«ä»£ç <C3A7>å<EFBFBD>—)       â”?â”?
â”?â”? (Excel风格)         â”?â”?â”?- 系统消æ<CB86>¯                 â”?â”?
â”?â”?                     â”?â”?â”?                          â”?â”?
�� 21�x 300+�      ��└───────────────────────────��
â”?â”?                     â”?â”?                                â”?
â”?â”? 支æŒ<C3A6>ï¼?             â”?â”?┌───────────────────────────â”?â”?
�� - 列排�            ���Input + Send Button       ��
�� - 列宽调整           ��└───────────────────────────��
â”?â”? - å<>•元格ç¼è¾ï¼ˆå<CB86>ŽæœŸï¼?â”?â”?                                â”?
�� - 选择高亮           ��                                �
�└──────────────────────��                                �
└──────────────────────────┴─────────────────────────────────�

🗂�文件结构规划

frontend-v2/src/modules/dc/pages/tool-c/
├── index.tsx                        # 主入å<C2A5>£ï¼ˆçжæ€<C3A6>管ç<C2A1>?布局ï¼?
├── components/
�  ├── Header.tsx                   # 顶部�
â”?  ├── Toolbar.tsx                  # 工具æ <C3A6>(¸ªæŒ‰é®ï¼‰
�  ├── DataGrid.tsx                 # AG Grid表格(核心)
â”?  ├── Sidebar.tsx                  # å<>³ä¾§æ <C3A6>容å™?
â”?  ├── ChatPanel.tsx                # Chaté<74>¢æ<C2A2>¿
â”?  ├── InsightsPanel.tsx            # Insightsé<73>¢æ<C2A2>¿
â”?  ├── MessageList.tsx              # 消æ<CB86>¯åˆ—表
â”?  ├── MessageItem.tsx              # å<>•æ<E280A2>¡æ¶ˆæ<CB86>¯
â”?  ├── CodeBlock.tsx                # 代ç <C3A7>å<EFBFBD>—渲æŸ?
�  └── InputArea.tsx                # 输入�
├── hooks/
â”?  ├── useToolC.ts                  # 核心状æ€<C3A6>管ç<C2A1>†Hook
â”?  ├── useSession.ts                # Session管ç<C2A1>†
â”?  ├── useChat.ts                   # AI对è¯<C3A8>逻è¾
â”?  └── useDataGrid.ts               # 表格数æ<C2B0>®ç®¡ç<C2A1>†
└── types/
    └── index.ts                     # TypeScriptç±»åžå®šä¹‰

frontend-v2/src/modules/dc/api/
└── toolC.ts                         # APIå°<C3A5>装ï¼?个方法)

â<EFBFBD>±ï¸<EFBFBD> Day 4 å¼€å<E282AC>计åˆï¼ˆ6-8å°<C3A5>æ—¶ï¼?

阶段1:项ç®åˆ<EFBFBD>å§åŒï¼?å°<C3A5>æ—¶ï¼?

1.1 安装ä¾<C3A4>èµ

cd frontend-v2
npm install ag-grid-community ag-grid-react
npm install prismjs @types/prismjs
npm install react-markdown

1.2 创建文件结构

mkdir -p src/modules/dc/pages/tool-c/{components,hooks,types}
touch src/modules/dc/pages/tool-c/index.tsx
# ... 创建其他文件

1.3 æ´æ°è·¯ç”±é…<C3A9>ç½®

// src/App.tsx æˆè·¯ç”±é…<C3A9>ç½®æ‡ä»?
<Route path="/data-cleaning/tool-c" element={<ToolC />} />

1.4 æ´æ°Portal页é<C2B5>¢

// src/modules/dc/pages/Portal.tsx
// 将Tool C的status�disabled'改为'ready'
{
  id: 'tool-c',
  title: 'ç§ç ”æ•°æ<C2B0>®ç¼è¾å™?,
  status: 'ready', // �修改这里
  route: '/data-cleaning/tool-c'
}

**交付�*�

  • âœ?ä¾<C3A4>èµå®‰è£…完æˆ<C3A6>
  • âœ?文件结构创建
  • âœ?Portalå<6C>¯ç¹å‡»è¿å…¥Tool C

阶段2:页é<EFBFBD>¢æ¡†æž¶æ<EFBFBD>­å»ºï¼ˆ2å°<EFBFBD>æ—¶ï¼?

2.1 åˆå»ºä¸»å…¥å<C2A5>?(index.tsx)

import { useState } from 'react';
import Header from './components/Header';
import Toolbar from './components/Toolbar';
import DataGrid from './components/DataGrid';
import Sidebar from './components/Sidebar';

interface ToolCState {
  sessionId: string | null;
  fileName: string;
  data: any[];
  columns: any[];
  messages: any[];
  isLoading: boolean;
}

const ToolC = () => {
  const [state, setState] = useState<ToolCState>({
    sessionId: null,
    fileName: 'cqol-demo.csv',
    data: [],
    columns: [],
    messages: [],
    isLoading: false,
  });

  return (
    <div className="h-screen w-screen flex flex-col bg-slate-50">
      <Header fileName={state.fileName} />
      <div className="flex-1 flex overflow-hidden">
        <div className="flex-1 flex flex-col">
          <Toolbar />
          <div className="flex-1 overflow-auto p-6">
            <DataGrid data={state.data} columns={state.columns} />
          </div>
        </div>
        <Sidebar messages={state.messages} />
      </div>
    </div>
  );
};

export default ToolC;

2.2 åˆå»ºHeader组件

// components/Header.tsx
import { Table2, Undo2, Redo2, Download, ArrowLeft } from 'lucide-react';
import { useNavigate } from 'react-router-dom';

interface HeaderProps {
  fileName: string;
  onUndo?: () => void;
  onRedo?: () => void;
  onExport?: () => void;
}

const Header: React.FC<HeaderProps> = ({ fileName, onUndo, onRedo, onExport }) => {
  const navigate = useNavigate();

  return (
    <header className="bg-white border-b border-slate-200 h-14 flex-none flex items-center justify-between px-4 z-20 shadow-sm">
      <div className="flex items-center gap-3">
        <button
          onClick={() => navigate('/data-cleaning')}
          className="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-slate-100 text-slate-600"
        >
          <ArrowLeft className="w-4 h-4" />
          <span className="text-sm">返回</span>
        </button>
        <div className="h-4 w-px bg-slate-300"></div>
        <div className="w-8 h-8 bg-emerald-600 rounded-lg flex items-center justify-center text-white shadow-md">
          <Table2 size={20} />
        </div>
        <span className="font-bold text-lg text-slate-900">
          科研数æ<EFBFBD>®ç¼è¾å™?
          <span className="text-emerald-600 text-xs px-1.5 py-0.5 bg-emerald-50 rounded-full ml-1">
            Pro
          </span>
        </span>
        <div className="h-4 w-[1px] bg-slate-300 mx-2"></div>
        <span className="text-xs text-slate-500 font-mono">{fileName}</span>
      </div>
      <div className="flex items-center gap-3">
        <div className="flex items-center bg-slate-100 rounded-lg p-1">
          <button
            onClick={onUndo}
            className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"
            disabled
          >
            <Undo2 size={16} />
          </button>
          <button
            onClick={onRedo}
            className="p-1.5 text-slate-400 hover:text-slate-700 rounded-md hover:bg-white transition-all"
            disabled
          >
            <Redo2 size={16} />
          </button>
        </div>
        <button
          onClick={onExport}
          className="flex items-center gap-2 px-4 py-1.5 bg-slate-900 text-white rounded-lg text-xs font-medium hover:bg-slate-800 transition-all shadow-md"
        >
          <Download size={12} /> 导出结果
        </button>
      </div>
    </header>
  );
};

export default Header;

2.3 åˆå»ºToolbar组件

// components/Toolbar.tsx
import { Calculator, CalendarClock, ArrowLeftRight, FileSearch, Wand2, Filter, Search } from 'lucide-react';

const ToolbarButton = ({ icon: Icon, label, colorClass }: any) => (
  <button className={`flex flex-col items-center justify-center w-20 h-14 rounded-lg transition-all hover:shadow-sm ${colorClass}`}>
    <Icon className="w-5 h-5 mb-1" />
    <span className="text-[10px] font-medium">{label}</span>
  </button>
);

const Toolbar = () => {
  return (
    <div className="bg-white border-b border-slate-200 px-4 py-2 flex items-center gap-1 overflow-x-auto flex-none shadow-sm z-10">
      <ToolbarButton icon={Calculator} label="生æˆ<C3A6>æ°å<C2B0>˜é‡? colorClass="text-emerald-600 bg-emerald-50 hover:bg-emerald-100" />
      <ToolbarButton icon={CalendarClock} label="æ—¶é—´å·? colorClass="text-blue-600 bg-blue-50 hover:bg-blue-100" />
      <ToolbarButton icon={ArrowLeftRight} label="横纵转æ<C2AC>¢" colorClass="text-cyan-600 bg-cyan-50 hover:bg-cyan-100" />
      <div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
      <ToolbarButton icon={FileSearch} label="查é‡<C3A9>" colorClass="text-orange-600 bg-orange-50 hover:bg-orange-100" />
      <ToolbarButton icon={Wand2} label="多é‡<C3A9>æ<EFBFBD>è¡¥" colorClass="text-rose-600 bg-rose-50 hover:bg-rose-100" />
      <div className="w-[1px] h-8 bg-slate-200 mx-2"></div>
      <ToolbarButton icon={Filter} label=­é€‰åˆ†æž<C3A6>é†" colorClass="text-indigo-600 bg-indigo-50 hover:bg-indigo-100" />
      <div className="flex-1"></div>
      <div className="relative">
        <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
        <input
          className="pl-9 pr-4 py-1.5 text-sm bg-slate-100 border-none rounded-full w-48 focus:w-64 transition-all outline-none focus:ring-2 focus:ring-emerald-500/20"
          placeholder=<>œç´¢å€?.."
        />
      </div>
    </div>
  );
};

export default Toolbar;

**交付�*�

  • âœ?Header完æˆ<C3A6>ï¼ˆå¸¦è¿”åžæŒ‰é®ï¼?
  • âœ?Toolbar完æˆ<C3A6>ï¼?个按钮)
  • âœ?基础布局å<E282AC>¯è§<C3A8>

阶段3:AG Grid醿ˆ<C3A6>ï¼?-4å°<C3A5>æ—¶ï¼?

3.1 åˆå»ºDataGrid组件

// components/DataGrid.tsx
import { AgGridReact } from 'ag-grid-react';
import { ColDef } from 'ag-grid-community';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
import { useMemo } from 'react';

interface DataGridProps {
  data: any[];
  columns: Array<{ id: string; name: string; type?: string }>;
  onCellValueChanged?: (params: any) => void;
}

const DataGrid: React.FC<DataGridProps> = ({ data, columns, onCellValueChanged }) => {
  // 将列定义转æ<C2AC>¢ä¸ºAG Gridæ ¼å¼<C3A5>
  const columnDefs: ColDef[] = useMemo(() => {
    return columns.map(col => ({
      field: col.id,
      headerName: col.name,
      sortable: true,
      filter: true,
      resizable: true,
      editable: false, // MVP阶段æšä¸<C3A4>支æŒ<C3A6>ç¼è¾
      width: 120,
      minWidth: 80,
      // æ ¹æ<C2B9>®ç±»åžè®¾ç½®æ ·å¼<C3A5>
      cellClass: (params) => {
        if (params.value === null || params.value === undefined || params.value === '') {
          return 'bg-red-50 text-red-400 italic';
        }
        return '';
      },
    }));
  }, [columns]);

  // 默认列é…<C3A9>ç½?
  const defaultColDef: ColDef = {
    flex: 0,
    sortable: true,
    filter: true,
    resizable: true,
  };

  // 妿žœæ²¡æœ‰æ•°æ<C2B0>®ï¼Œæ˜¾ç¤ºç©ºçжæ€?
  if (data.length === 0) {
    return (
      <div className="bg-white border border-slate-200 shadow-sm rounded-xl p-12 text-center">
        <div className="text-slate-400 text-sm">
          <p className="mb-2">暂无数æ<EFBFBD>®</p>
          <p>请在å<EFBFBD>³ä¾§AI助手中上传文ä»?/p>
        </div>
      </div>
    );
  }

  return (
    <div className="bg-white border border-slate-200 shadow-sm rounded-xl overflow-hidden h-full">
      <div className="ag-theme-alpine h-full">
        <AgGridReact
          rowData={data}
          columnDefs={columnDefs}
          defaultColDef={defaultColDef}
          animateRows={true}
          rowSelection="multiple"
          onCellValueChanged={onCellValueChanged}
          domLayout="normal"
          suppressCellFocus={false}
          enableCellTextSelection={true}
        />
      </div>
    </div>
  );
};

export default DataGrid;

3.2 自定义AG Gridæ ·å¼<C3A5>

/* 在全局CSSæˆç»„件内添加 */
.ag-theme-alpine {
  --ag-header-background-color: #f8fafc;
  --ag-header-foreground-color: #475569;
  --ag-border-color: #e2e8f0;
  --ag-row-hover-color: #f0fdf4;
  --ag-selected-row-background-color: #d1fae5;
  --ag-font-family: inherit;
  --ag-font-size: 13px;
}

.ag-theme-alpine .ag-header-cell {
  font-weight: 600;
}

.ag-theme-alpine .ag-cell {
  display: flex;
  align-items: center;
}

3.3 æµè¯•æ•°æ<C2B0>®åŠ è½½

// 临时æµè¯•:在index.tsx中硬ç¼ç <C3A7>æµè¯•æ•°æ<C2B0>®
const mockData = [
  { id: 'P001', age: 27, sex: 'å¥?, bmi: 23.1, smoke: 'å<EFBFBD>? },
  { id: 'P002', age: 24, sex: '�, bmi: 16.7, smoke: '� },
  { id: 'P003', age: null, sex: '�, bmi: null, smoke: '� },
];

const mockColumns = [
  { id: 'id', name: '病人ID', type: 'text' },
  { id: 'age', name: '年龄', type: 'number' },
  { id: 'sex', name: '性别', type: 'category' },
  { id: 'bmi', name: 'BMI', type: 'number' },
  { id: 'smoke', name: <>¸çƒŸå<C5B8>?, type: 'category' },
];

**交付�*�

  • âœ?AG Gridæˆ<C3A6>功渲染
  • âœ?缺失值高亮显ç¤?
  • âœ?列排åº?过滤å<C2A4>¯ç”¨
  • âœ?列宽å<C2BD>¯è°ƒæ•?

â<EFBFBD>±ï¸<EFBFBD> Day 5 å¼€å<E282AC>计åˆï¼ˆ6-8å°<C3A5>æ—¶ï¼?

阶段4:AI Chaté<74>¢æ<C2A2>¿ï¼?å°<C3A5>æ—¶ï¼?

4.1 åˆå»ºSidebar组件

// components/Sidebar.tsx
import { useState } from 'react';
import { MessageSquare, Lightbulb, X } from 'lucide-react';
import ChatPanel from './ChatPanel';
import InsightsPanel from './InsightsPanel';

interface SidebarProps {
  isOpen: boolean;
  onClose: () => void;
  messages: any[];
  onSendMessage: (message: string) => void;
  dataStats?: any;
}

const Sidebar: React.FC<SidebarProps> = ({ isOpen, onClose, messages, onSendMessage, dataStats }) => {
  const [activeTab, setActiveTab] = useState<'chat' | 'insights'>('chat');

  if (!isOpen) return null;

  return (
    <div className="w-[420px] bg-white border-l border-slate-200 flex flex-col">
      {/* Tab Header */}
      <div className="h-14 border-b border-slate-200 flex items-center justify-between px-4">
        <div className="flex gap-1">
          <button
            onClick={() => setActiveTab('chat')}
            className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
              activeTab === 'chat'
                ? 'bg-emerald-50 text-emerald-700'
                : 'text-slate-600 hover:bg-slate-50'
            }`}
          >
            <MessageSquare size={16} />
            AI助手
          </button>
          <button
            onClick={() => setActiveTab('insights')}
            className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
              activeTab === 'insights'
                ? 'bg-emerald-50 text-emerald-700'
                : 'text-slate-600 hover:bg-slate-50'
            }`}
          >
            <Lightbulb size={16} />
            æ•°æ<EFBFBD>®æ´žå¯Ÿ
          </button>
        </div>
        <button
          onClick={onClose}
          className="p-1 rounded-md hover:bg-slate-100 text-slate-400 hover:text-slate-600"
        >
          <X size={18} />
        </button>
      </div>

      {/* Tab Content */}
      <div className="flex-1 overflow-hidden">
        {activeTab === 'chat' ? (
          <ChatPanel messages={messages} onSendMessage={onSendMessage} />
        ) : (
          <InsightsPanel dataStats={dataStats} />
        )}
      </div>
    </div>
  );
};

export default Sidebar;

4.2 åˆå»ºChatPanel组件

// components/ChatPanel.tsx
import { useRef, useEffect } from 'react';
import MessageItem from './MessageItem';
import InputArea from './InputArea';

interface ChatPanelProps {
  messages: any[];
  onSendMessage: (message: string) => void;
  isLoading?: boolean;
}

const ChatPanel: React.FC<ChatPanelProps> = ({ messages, onSendMessage, isLoading }) => {
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  return (
    <div className="flex flex-col h-full">
      {/* Messages Area */}
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.length === 0 && (
          <div className="text-center text-slate-400 text-sm py-12">
            <p className="mb-2">👋 æ¨å¥½ï¼<EFBFBD>æˆæ˜¯æ¨çš„AIæ•°æ<EFBFBD>®åˆ†æž<EFBFBD>å¸?/p>
            <p>试试说:"把年龄大äº?0的标记为è€<C3A8>å¹´ç»?</p>
          </div>
        )}
        {messages.map((msg) => (
          <MessageItem key={msg.id} message={msg} />
        ))}
        {isLoading && (
          <div className="flex items-center gap-2 text-slate-400 text-sm">
            <div className="animate-spin rounded-full h-4 w-4 border-2 border-emerald-500 border-t-transparent"></div>
            <span>AI正在æ€<EFBFBD>è€?..</span>
          </div>
        )}
        <div ref={messagesEndRef} />
      </div>

      {/* Input Area */}
      <InputArea onSend={onSendMessage} disabled={isLoading} />
    </div>
  );
};

export default ChatPanel;

4.3 åˆå»ºMessageItem组件

// components/MessageItem.tsx
import { User, Bot, CheckCircle2, XCircle, Play } from 'lucide-react';
import CodeBlock from './CodeBlock';

interface MessageItemProps {
  message: {
    id: string;
    role: 'user' | 'assistant' | 'system';
    content: string;
    code?: {
      content: string;
      status: 'pending' | 'running' | 'success' | 'error';
    };
    onExecute?: () => void;
  };
}

const MessageItem: React.FC<MessageItemProps> = ({ message }) => {
  const { role, content, code } = message;

  // 系统消æ<CB86>¯
  if (role === 'system') {
    return (
      <div className="text-center">
        <div className="inline-block bg-slate-100 text-slate-600 text-xs px-3 py-1 rounded-full">
          {content}
        </div>
      </div>
    );
  }

  // 用户消æ<CB86>¯
  if (role === 'user') {
    return (
      <div className="flex items-start gap-3 justify-end">
        <div className="bg-emerald-600 text-white px-4 py-2 rounded-2xl rounded-tr-sm max-w-[80%] text-sm">
          {content}
        </div>
        <div className="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center flex-shrink-0">
          <User size={16} className="text-slate-600" />
        </div>
      </div>
    );
  }

  // AI消æ<CB86>¯
  return (
    <div className="flex items-start gap-3">
      <div className="w-8 h-8 bg-emerald-100 rounded-full flex items-center justify-center flex-shrink-0">
        <Bot size={16} className="text-emerald-600" />
      </div>
      <div className="flex-1 space-y-2">
        <div className="bg-slate-50 text-slate-800 px-4 py-2 rounded-2xl rounded-tl-sm text-sm">
          {content}
        </div>
        {code && (
          <div className="space-y-2">
            <CodeBlock code={code.content} language="python" />
            <div className="flex items-center gap-2">
              <button
                onClick={message.onExecute}
                disabled={code.status === 'running'}
                className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
                  code.status === 'pending'
                    ? 'bg-emerald-600 text-white hover:bg-emerald-700'
                    : code.status === 'running'
                    ? 'bg-slate-300 text-slate-500 cursor-not-allowed'
                    : code.status === 'success'
                    ? 'bg-green-100 text-green-700'
                    : 'bg-red-100 text-red-700'
                }`}
              >
                {code.status === 'pending' && (
                  <>
                    <Play size={12} /> 执行代ç <EFBFBD>
                  </>
                )}
                {code.status === 'running' && '执行�..'}
                {code.status === 'success' && (
                  <>
                    <CheckCircle2 size={12} /> 执行æˆ<EFBFBD>功
                  </>
                )}
                {code.status === 'error' && (
                  <>
                    <XCircle size={12} /> 执行失败
                  </>
                )}
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default MessageItem;

4.4 åˆå»ºCodeBlock组件

// components/CodeBlock.tsx
import { Copy, Check } from 'lucide-react';
import { useState } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism-tomorrow.css';
import 'prismjs/components/prism-python';

interface CodeBlockProps {
  code: string;
  language?: string;
}

const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = 'python' }) => {
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    navigator.clipboard.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };

  const highlightedCode = Prism.highlight(
    code,
    Prism.languages[language] || Prism.languages.python,
    language
  );

  return (
    <div className="relative bg-slate-900 rounded-lg overflow-hidden">
      <div className="flex items-center justify-between px-4 py-2 bg-slate-800 border-b border-slate-700">
        <span className="text-slate-400 text-xs font-mono">{language}</span>
        <button
          onClick={handleCopy}
          className="flex items-center gap-1 text-slate-400 hover:text-white text-xs transition-colors"
        >
          {copied ? (
            <>
              <Check size={12} /> å·²å¤<EFBFBD>åˆ?
            </>
          ) : (
            <>
              <Copy size={12} /> å¤<EFBFBD>åˆ
            </>
          )}
        </button>
      </div>
      <pre className="p-4 overflow-x-auto text-xs">
        <code
          className={`language-${language}`}
          dangerouslySetInnerHTML={{ __html: highlightedCode }}
        />
      </pre>
    </div>
  );
};

export default CodeBlock;

4.5 åˆå»ºInputArea组件

// components/InputArea.tsx
import { Send } from 'lucide-react';
import { useState, KeyboardEvent } from 'react';

interface InputAreaProps {
  onSend: (message: string) => void;
  disabled?: boolean;
}

const InputArea: React.FC<InputAreaProps> = ({ onSend, disabled }) => {
  const [input, setInput] = useState('');

  const handleSend = () => {
    if (!input.trim() || disabled) return;
    onSend(input);
    setInput('');
  };

  const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      handleSend();
    }
  };

  return (
    <div className="border-t border-slate-200 p-4">
      <div className="flex gap-2">
        <textarea
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder=Šè¯‰AI你想å<C2B3>šä»€ä¹?.."
          className="flex-1 resize-none border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500"
          rows={3}
          disabled={disabled}
        />
        <button
          onClick={handleSend}
          disabled={!input.trim() || disabled}
          className="bg-emerald-600 text-white p-3 rounded-lg hover:bg-emerald-700 disabled:bg-slate-200 disabled:text-slate-400 disabled:cursor-not-allowed transition-colors"
        >
          <Send size={18} />
        </button>
      </div>
      <div className="text-xs text-slate-400 mt-2">
        æŒ?<kbd className="px-1 py-0.5 bg-slate-100 rounded">Enter</kbd> å<EFBFBD>é€<EFBFBD>,
        <kbd className="px-1 py-0.5 bg-slate-100 rounded">Shift + Enter</kbd> æ<EFBFBD>¢è¡Œ
      </div>
    </div>
  );
};

export default InputArea;

**交付�*�

  • âœ?Chaté<74>¢æ<C2A2>¿å®Œæ•´UI
  • âœ?消æ<CB86>¯åˆ—表渲染
  • âœ?代ç <C3A7>å<EFBFBD>—语法高äº?
  • âœ?输入框交äº?

阶段5:API醿ˆ<EFBFBD>ï¼?-3å°<C3A5>æ—¶ï¼?

5.1 åˆå»ºAPIå°<C3A5>装

// api/toolC.ts
import axios from 'axios';

const BASE_URL = '/api/v1/dc/tool-c';

// Session管ç<C2A1>export const uploadFile = async (file: File) => {
  const formData = new FormData();
  formData.append('file', file);
  
  const response = await axios.post(`${BASE_URL}/sessions/upload`, formData, {
    headers: { 'Content-Type': 'multipart/form-data' },
  });
  
  return response.data;
};

export const getSession = async (sessionId: string) => {
  const response = await axios.get(`${BASE_URL}/sessions/${sessionId}`);
  return response.data;
};

export const getPreviewData = async (sessionId: string) => {
  const response = await axios.get(`${BASE_URL}/sessions/${sessionId}/preview`);
  return response.data;
};

export const updateHeartbeat = async (sessionId: string) => {
  const response = await axios.post(`${BASE_URL}/sessions/${sessionId}/heartbeat`);
  return response.data;
};

// AI功能
export const generateCode = async (sessionId: string, message: string) => {
  const response = await axios.post(`${BASE_URL}/ai/generate`, {
    sessionId,
    message,
  });
  return response.data;
};

export const executeCode = async (sessionId: string, code: string, messageId: string) => {
  const response = await axios.post(`${BASE_URL}/ai/execute`, {
    sessionId,
    code,
    messageId,
  });
  return response.data;
};

export const processMessage = async (sessionId: string, message: string) => {
  const response = await axios.post(`${BASE_URL}/ai/process`, {
    sessionId,
    message,
    maxRetries: 3,
  });
  return response.data;
};

export const getChatHistory = async (sessionId: string, limit = 10) => {
  const response = await axios.get(`${BASE_URL}/ai/history/${sessionId}?limit=${limit}`);
  return response.data;
};

5.2 åˆå»ºæ ¸å¿ƒHook

// hooks/useToolC.ts
import { useState, useCallback } from 'react';
import * as api from '../../api/toolC';

export const useToolC = () => {
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [fileName, setFileName] = useState('');
  const [data, setData] = useState<any[]>([]);
  const [columns, setColumns] = useState<any[]>([]);
  const [messages, setMessages] = useState<any[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  // 上传文件
  const handleFileUpload = useCallback(async (file: File) => {
    try {
      setIsLoading(true);
      const result = await api.uploadFile(file);
      
      if (result.success) {
        setSessionId(result.data.sessionId);
        setFileName(file.name);
        
        // 获å<C2B7>预览数æ<C2B0>®
        const preview = await api.getPreviewData(result.data.sessionId);
        if (preview.success) {
          setData(preview.data.rows);
          setColumns(preview.data.columns.map((col: string) => ({
            id: col,
            name: col,
            type: 'text',
          })));
        }
        
        // 添加欢迎消æ<CB86>¯
        setMessages([{
          id: Date.now(),
          role: 'system',
          content: `âœ?æ‡ä»¶ä¸Šä¼ æˆ<C3A6>功ï¼<C3AF>å…±${preview.data.totalRows}è¡?x ${preview.data.totalCols}列`,
        }]);
      }
    } catch (error: any) {
      console.error('上传失败:', error);
      setMessages([{
        id: Date.now(),
        role: 'system',
        content: <>?上传失败: ${error.message}`,
      }]);
    } finally {
      setIsLoading(false);
    }
  }, []);

  // å<>é€<C3A9>AI消æ<CB86>¯ï¼ˆä¸€æ­¥åˆ°ä½<C3A4>:生æˆ<C3A6>+执行ï¼?
  const handleSendMessage = useCallback(async (message: string) => {
    if (!sessionId) {
      alert('请先上传文件');
      return;
    }

    // 添加用户消æ<CB86>¯
    setMessages(prev => [...prev, {
      id: Date.now(),
      role: 'user',
      content: message,
    }]);

    try {
      setIsLoading(true);
      
      // 调用process接å<C2A5>£ï¼ˆç”Ÿæˆ?执行,一步到ä½<C3A4>)
      const result = await api.processMessage(sessionId, message);
      
      if (result.success) {
        // 添加AI消æ<CB86>¯ï¼ˆå¸¦ä»£ç <C3A7>åŒæ‰§è¡Œç»“果)
        setMessages(prev => [...prev, {
          id: Date.now() + 1,
          role: 'assistant',
          content: result.data.explanation,
          code: {
            content: result.data.code,
            status: result.data.executeResult.success ? 'success' : 'error',
          },
        }]);

        // 妿žœæ‰§è¡Œæˆ<C3A6>åŠŸï¼Œæ´æ°è¡¨æ ¼æ•°æ<C2B0>?
        if (result.data.executeResult.success && result.data.executeResult.newDataPreview) {
          setData(result.data.executeResult.newDataPreview);
          
          setMessages(prev => [...prev, {
            id: Date.now() + 2,
            role: 'system',
            content: `âœ?代ç <C3A7>执行æˆ<C3A6>功ï¼<C3AF>è¡¨æ ¼å·²æ´æ°ã€?{result.data.retryCount > 0 ? `(é‡<EFBFBD>è¯?{result.data.retryCount}次å<EFBFBD>Žæˆ<EFBFBD>功)` : ''}`,
          }]);
        } else if (!result.data.executeResult.success) {
          setMessages(prev => [...prev, {
            id: Date.now() + 2,
            role: 'system',
            content: <>?执行失败: ${result.data.executeResult.error}`,
          }]);
        }
      }
    } catch (error: any) {
      console.error('AI处ç<E2809E>†å¤±è´¥:', error);
      setMessages(prev => [...prev, {
        id: Date.now() + 1,
        role: 'system',
        content: <>?处ç<E2809E>†å¤±è´¥: ${error.response?.data?.error || error.message}`,
      }]);
    } finally {
      setIsLoading(false);
    }
  }, [sessionId]);

  return {
    sessionId,
    fileName,
    data,
    columns,
    messages,
    isLoading,
    handleFileUpload,
    handleSendMessage,
  };
};

5.3 在主组件中使用Hook

// index.tsx (æ›´æ–°)
import { useToolC } from './hooks/useToolC';

const ToolC = () => {
  const {
    fileName,
    data,
    columns,
    messages,
    isLoading,
    handleFileUpload,
    handleSendMessage,
  } = useToolC();

  return (
    <div className="h-screen w-screen flex flex-col bg-slate-50">
      <Header fileName={fileName} />
      <div className="flex-1 flex overflow-hidden">
        <div className="flex-1 flex flex-col">
          <Toolbar />
          <div className="flex-1 overflow-auto p-6">
            <DataGrid data={data} columns={columns} />
          </div>
        </div>
        <Sidebar
          isOpen={true}
          onClose={() => {}}
          messages={messages}
          onSendMessage={handleSendMessage}
        />
      </div>
    </div>
  );
};

**交付�*�

  • âœ?6个APIæ¹æ³•å°<C3A5>装
  • âœ?useToolC核心Hook
  • âœ?端到端æµ<C3A6>ç¨å<E280B9>¯ç”?

阶段6:æ‡ä»¶ä¸Šä¼?+ InsightsPanelï¼?å°<C3A5>æ—¶ï¼?

6.1 添加æ‡ä»¶ä¸Šä¼ UI

// 在ChatPanel中添加上传区域(messages为空时显示)
{messages.length === 0 && (
  <div className="text-center py-12">
    <div className="mb-6">
      <input
        type="file"
        accept=".csv,.xlsx,.xls"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) handleFileUpload(file);
        }}
        className="hidden"
        id="file-upload"
      />
      <label
        htmlFor="file-upload"
        className="inline-flex items-center gap-2 px-6 py-3 bg-emerald-600 text-white rounded-lg cursor-pointer hover:bg-emerald-700 transition-all"
      >
        <Upload size={18} />
        上传CSV/Excel文件
      </label>
    </div>
    <p className="text-slate-400 text-sm">
      支æŒ<EFBFBD>æ ¼å¼<EFBFBD>:CSV, XLSX, XLS(最å¤?0MBï¼?
    </p>
  </div>
)}

6.2 åˆå»ºInsightsPanel组件

// components/InsightsPanel.tsx
import { BarChart3, AlertCircle, Database, Layers } from 'lucide-react';

interface InsightsPanelProps {
  dataStats?: {
    totalRows: number;
    totalCols: number;
    missingCount: number;
    missingRate: number;
  };
}

const InsightsPanel: React.FC<InsightsPanelProps> = ({ dataStats }) => {
  if (!dataStats) {
    return (
      <div className="p-6 text-center text-slate-400">
        <p>暂无数æ<EFBFBD>®</p>
      </div>
    );
  }

  return (
    <div className="p-4 space-y-4 overflow-y-auto">
      <div className="bg-slate-50 rounded-lg p-4">
        <div className="flex items-center gap-2 mb-3">
          <Database size={16} className="text-slate-600" />
          <h3 className="font-semibold text-sm text-slate-900">æ•°æ<EFBFBD>®è§„模</h3>
        </div>
        <div className="space-y-2 text-sm">
          <div className="flex justify-between">
            <span className="text-slate-600">总行�/span>
            <span className="font-medium">{dataStats.totalRows}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-slate-600">总列�/span>
            <span className="font-medium">{dataStats.totalCols}</span>
          </div>
        </div>
      </div>

      <div className="bg-red-50 rounded-lg p-4">
        <div className="flex items-center gap-2 mb-3">
          <AlertCircle size={16} className="text-red-600" />
          <h3 className="font-semibold text-sm text-red-900">缺失�/h3>
        </div>
        <div className="space-y-2 text-sm">
          <div className="flex justify-between">
            <span className="text-red-700">缺失数é‡<EFBFBD></span>
            <span className="font-medium text-red-900">{dataStats.missingCount}</span>
          </div>
          <div className="flex justify-between">
            <span className="text-red-700">缺失�/span>
            <span className="font-medium text-red-900">{dataStats.missingRate.toFixed(1)}%</span>
          </div>
        </div>
      </div>
    </div>
  );
};

export default InsightsPanel;

**交付�*�

  • âœ?文件上传功能
  • âœ?Insightsé<73>¢æ<C2A2>¿

🎯 Day 4-5 验收标准

å¿…è¾¾ç®æ ‡ï¼ˆMVPï¼?

  • **页é<C2B5>¢å<C2A2>¯è®¿é—?*:Portalå<6C>¯ç¹å‡»è¿å…¥Tool C
  • **文件上传**:支æŒ<C3A6>CSV/Excel上传
  • 表格展示:AG Grid渲染真实数æ<C2B0>®ï¼ˆcqol-demo.csvï¼?
  • **AI对è¯<C3A8>**:å<C5A1>³ä¾§Chaté<74>¢æ<C2A2>¿å<C2BF>¯å<C2AF>é€<C3A9>消æ<CB86>?
  • **代ç <C3A7>生æˆ<C3A6>**:AI生æˆ<C3A6>Python代ç <C3A7>并显ç¤?
  • 代ç <EFBFBD>执行:ç¹å‡»æ‰§è¡ŒæŒ‰é®ï¼Œè¡¨æ ¼æ•°æ<EFBFBD>®æ´æ°
  • 对è¯<EFBFBD>历å<EFBFBD>²ï¼šå¤šè½®å¯¹è¯<EFBFBD>正常显ç¤?

å<EFBFBD>¯é€‰ç®æ ‡ï¼ˆDay 6优åŒï¼?

  • å<EFBFBD>•元格æ‰åЍç¼è¾?
  • 工具æ <EFBFBD>按é®åŠŸèƒ?
  • 撤销/é‡<C3A9>å<EFBFBD>š
  • 导出Excel
  • æ•°æ<EFBFBD>®æ´žå¯Ÿå<EFBFBD>¡ç‰‡å®Œå
  • å“<EFBFBD>应å¼<EFBFBD>布局

ðŸ“<EFBFBD> 测试场景

场景1:上传æ‡ä»?

  1. 访问 /data-cleaning/tool-c
  2. 点击"上传CSV/Excelæ‡ä»¶"
  3. 选择 cqol-demo.csv
  4. 验è¯<EFBFBD>:表格显ç¤?1列x300+行数æ<C2B0>?

场景2:AI缺失值处ç<EFBFBD>?

  1. 在Chat输入�把sex列的缺失值填补为众数"
  2. 验è¯<EFBFBD>:AI生æˆ<EFBFBD>代ç <EFBFBD>
  3. 点击"执行代ç <C3A7>"
  4. 验è¯<EFBFBD>:表格数æ<EFBFBD>®æ´æ°ï¼Œç¼ºå¤±å€¼æ¶ˆå¤?

场景3:AI年龄分组

  1. 在Chat输入ï¼?把age列按18ã€?0分为未æˆ<C3A6>å¹´ã€<C3A3>æˆ<C3A6>å¹´ã€<C3A3>è€<C3A8>年三组"
  2. 验è¯<EFBFBD>:AI生æˆ<EFBFBD>代ç <EFBFBD>
  3. 点击"执行代ç <C3A7>"
  4. 验è¯<EFBFBD>:表格æ°å¢žage_groupåˆ?

场景4:对è¯<EFBFBD>历å<EFBFBD>?

  1. å<EFBFBD>é€<EFBFBD>多æ<EFBFBD>¡æ¶ˆæ<EFBFBD>?
  2. 验è¯<EFBFBD>:所有对è¯<EFBFBD>ä¿<EFBFBD>ç•?
  3. 刷æ°é¡µé<EFBFBD>¢
  4. 验è¯<EFBFBD>:对è¯<EFBFBD>历å<EFBFBD>²åŠ è½½æ­£å¸?

🚨 风险与应�

风险 概率 å½±å“<EFBFBD> 应对措施
AG Grid学习æˆ<C3A6>本é«? ä¸? ä¸? Day 4上å<C5A0>ˆé†ä¸­å­¦ä¹ å®˜æ¹æ‡æ¡£
API调试时间é•? é«? ä¸? 先用Mockæ•°æ<EFBFBD>®ï¼Œå†<EFBFBD>对接真实API
代ç <EFBFBD>语法高亮问题 ä½? ä½? Prism.jsé…<C3A9>ç½®ä¸<C3A4>å¤<C3A5>æ<EFBFBD>,有现æˆ<C3A6>æ¹æ¡?
性能问题ï¼?00+行数æ<C2B0>®ï¼‰ ä½? ä½? AG Gridè‡ªå¸¦è™šæŸæ»šåŠ¨ï¼Œæ— éœ€ä¼˜åŒ

📦 ä¾<C3A4>èµå®‰è£…清å<E280A6>

# AG Grid
npm install ag-grid-community ag-grid-react

# 代ç <C3A7>高亮
npm install prismjs @types/prismjs

# Markdown渲染(å<CB86>¯é€‰ï¼‰
npm install react-markdown

# 已有ä¾<C3A4>èµï¼ˆæ— éœ€å®‰è£…ï¼?
# - react
# - react-router-dom
# - lucide-react
# - axios
# - tailwindcss

📚 å<>考资æº?


âœ?最终交付物清å<E280A6>

Day 4

  • 文件结构创建ï¼?0个æ‡ä»¶ï¼‰
  • Header组件
  • Toolbar组件
  • DataGrid组件(AG Gridï¼?
  • 基础布局完æˆ<EFBFBD>

Day 5

  • Sidebar组件
  • ChatPanel组件
  • MessageItem组件
  • CodeBlock组件
  • InputArea组件
  • InsightsPanel组件
  • APIå°<EFBFBD>装(toolC.tsï¼?
  • useToolC Hook
  • 端到端æµ<EFBFBD>ç¨æµè¯•通过

文档

  • 本开å<EFBFBD>计åˆ?
  • Day 4-5å¼€å<E282AC>完æˆ<C3A6>总结(Day 5结æ<E2809C>Ÿå<C5B8>Žï¼‰
  • APIå¯¹æŽ¥æ‡æ¡£ï¼ˆDay 5结æ<E2809C>Ÿå<C5B8>Žï¼‰

**制定�: AI Assistant
**确认�
: 用户
**开始时é—?*: Day 4上å<C5A0>ˆ
预期完æˆ<EFBFBD>: Day 5ä¸å<E280B9>ˆ


🚀 准备好开å§Day 4å¼€å<E282AC>了å<E280A0>—?