前端是一个 React 应用,核心就是一个聊天界面。代码在 src/renderer/ 目录下:

第 5 课:前端界面——React 聊天窗


前端概览

前端是一个 React 应用,核心就是一个聊天界面。代码在 src/renderer/ 目录下:

code
src/renderer/
├── App.tsx               ← 应用入口
├── App.css               ← 样式文件
├── index.tsx             ← React 挂载点
└── components/
    ├── ChatInterface.tsx  ← 主容器(状态管理、IPC 监听)
    ├── MessageList.tsx    ← 消息列表(自动滚动)
    ├── Message.tsx        ← 单条消息渲染
    ├── MessageInput.tsx   ← 输入框 + 文件上传
    ├── ToolUseDisplay.tsx ← 工具调用展示
    ├── ThinkingDisplay.tsx← AI 思考过程展示
    ├── TodoListDisplay.tsx← 任务列表展示
    └── types.ts           ← TypeScript 类型定义

组件树

code
App
 └─ ChatInterface(大管家)
     ├─ MessageList(消息列表)
     │   ├─ Message(用户消息)    → 绿色背景
     │   └─ Message(AI 消息)     → 灰色背景
     │       ├─ 文本内容            → Markdown 渲染
     │       ├─ ToolUseDisplay     → "AI 在用什么工具"
     │       ├─ ThinkingDisplay    → "AI 在想什么"
     │       └─ 文件下载按钮        → 📥 下载 Excel
     │
     └─ MessageInput(输入区)
         ├─ 文本输入框
         ├─ 文件拖放区
         └─ 发送按钮

ChatInterface:大管家

ChatInterface.tsx 是整个前端的核心,它管理所有状态和通信。

状态管理

code
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [currentTodos, setCurrentTodos] = useState<TodoItem[]>([]);

四个状态就撑起了整个界面: - messages:所有对话消息 - isLoading:AI 是不是正在工作(控制 loading 动画) - error:有没有出错 - currentTodos:AI 创建的任务列表

IPC 监听

code
useEffect(() => {
  // 监听 AI 的回复
  const removeResponseListener = window.electron.ipcRenderer.on(
    'claude-code:response',
    (message: SDKMessage) => {
      // 把 AI 的回复添加到消息列表
      updateMessages(message);
    }
  );

  // 监听错误
  const removeErrorListener = window.electron.ipcRenderer.on(
    'claude-code:error',
    (errorMessage: string) => {
      setError(errorMessage);
      setIsLoading(false);
    }
  );

  // 监听输出文件
  const removeFilesListener = window.electron.ipcRenderer.on(
    'claude-code:output-files',
    (files) => {
      // 在最后一条 AI 消息上显示下载按钮
    }
  );

  return () => {
    removeResponseListener();
    removeErrorListener();
    removeFilesListener();
  };
}, []);

发送消息

code
const sendMessage = async (content: string, files?: File[]) => {
  // 1. 把用户消息加到列表
  setMessages(prev => [...prev, { role: 'user', content }]);

  // 2. 开始 loading
  setIsLoading(true);

  // 3. 通过 IPC 发给主进程
  window.electron.ipcRenderer.sendMessage('claude-code:query', {
    content,
    files: files?.map(f => ({
      name: f.name,
      data: /* 文件二进制数据 */,
    })),
  });
};

MessageInput:输入区

MessageInput.tsx 处理用户输入,最特别的是支持文件拖放上传

code
// 支持的文件类型
const ACCEPTED_TYPES = [
  '.xlsx', '.xls',    // Excel
  '.pdf',             // PDF
  '.docx', '.doc',    // Word
];

const MAX_FILE_SIZE = 10 * 1024 * 1024;  // 10 MB 上限

拖放逻辑

code
const handleDrop = (e: DragEvent) => {
  e.preventDefault();
  const droppedFiles = Array.from(e.dataTransfer.files);

  for (const file of droppedFiles) {
    // 检查文件类型
    if (!ACCEPTED_TYPES.some(t => file.name.endsWith(t))) {
      alert('不支持的文件类型');
      continue;
    }
    // 检查文件大小
    if (file.size > MAX_FILE_SIZE) {
      alert('文件太大(最大 10MB)');
      continue;
    }
    // 添加到待上传列表
    setAttachedFiles(prev => [...prev, file]);
  }
};

用户可以把 Excel 文件直接拖进聊天框,非常方便。

Message:消息渲染

Message.tsx 负责渲染每一条消息。它区分用户消息和 AI 消息:

code
// 用户消息:Excel 绿色背景
<div style={{ background: '#217346', color: 'white' }}>
  {message.content}
</div>

// AI 消息:灰色背景 + Markdown 渲染
<div style={{ background: '#f3f4f6' }}>
  <ReactMarkdown remarkPlugins={[remarkGfm]}>
    {message.content}
  </ReactMarkdown>
</div>

AI 的消息里可能包含多种内容块:

code
一条 AI 消息可能包含:
  ├── 文本内容          → ReactMarkdown 渲染
  ├── 工具调用          → ToolUseDisplay 渲染
  ├── 思考过程          → ThinkingDisplay 渲染
  └── 输出文件          → 下载按钮渲染

ToolUseDisplay:工具展示

这是这个项目的一个亮点——用户能实时看到 AI 在用什么工具

bash
// 每个工具有自己的图标和颜色
const TOOL_METADATA = {
  Read:      { icon: '📖', color: '#3B82F6', category: 'read' },
  Bash:      { icon: '⚙️', color: '#8B5CF6', category: 'execute' },
  Write:     { icon: '✏️', color: '#10B981', category: 'write' },
  Skill:     { icon: '🎯', color: '#EC4899', category: 'other' },
  WebSearch: { icon: '🔍', color: '#F59E0B', category: 'search' },
  // ...
};

渲染效果像这样:

python
┌────────────────────────────────────────┐
│ ⚙️ Bash                               │
│ │ python3 generate_budget.py           │
│ │                                      │
│ │ ↳ Result: Budget created successf... │
│ └──────────────────────────────────────│
│                                        │
│ 📖 Read                               │
│ │ File: problems/uploaded_data.xlsx    │
│ └──────────────────────────────────────│
└────────────────────────────────────────┘

工具调用可以展开/折叠,默认折叠以免占太多空间。

文件下载

当 AI 生成了 Excel 文件,界面会显示下载按钮:

code
// 下载按钮
<button onClick={() => downloadFile(file.path)}>
  📥 下载 {file.name}
</button>

// 下载逻辑(通过 IPC 调用主进程)
const downloadFile = async (filePath: string) => {
  await window.electron.ipcRenderer.invoke('download-file', filePath);
};

主进程收到下载请求后,会打开系统的"另存为"对话框。

样式设计

项目使用 Tailwind CSS,主色调是 Excel 的绿色(#217346):

code
┌──────────────────────────────┐
│  整体配色:                    │
│  用户消息:#217346(Excel绿)  │
│  AI 消息:#f3f4f6(浅灰)     │
│  工具展示:各色左边框          │
│  代码块:#1f2937(深灰)      │
│  字体:JetBrains Mono(等宽)  │
└──────────────────────────────┘

本课小结

  • 前端是一个 React 聊天界面,核心组件只有 7-8 个
  • ChatInterface 是"大管家",管理状态和 IPC 通信
  • MessageInput 支持文字输入和文件拖放上传
  • Message 用 ReactMarkdown 渲染 AI 回复
  • ToolUseDisplay 让用户实时看到 AI 在用什么工具
  • 文件下载通过 IPC 调用主进程的"另存为"对话框

课后练习

  1. 打开 src/renderer/components/Message.tsx,看看 ReactMarkdown 是怎么用的
  2. 尝试修改 #217346 为其他颜色(比如蓝色 #2563EB),看看效果
  3. 在 ToolUseDisplay 里找到工具图标的配置,给 Bash 换一个图标

沿着当前专题继续,或返回课程目录重新整理阅读顺序。

返回课程目录