第 5 课:前端界面——React 聊天窗
前端概览
前端是一个 React 应用,核心就是一个聊天界面。代码在 src/renderer/ 目录下:
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 类型定义
组件树
App
└─ ChatInterface(大管家)
├─ MessageList(消息列表)
│ ├─ Message(用户消息) → 绿色背景
│ └─ Message(AI 消息) → 灰色背景
│ ├─ 文本内容 → Markdown 渲染
│ ├─ ToolUseDisplay → "AI 在用什么工具"
│ ├─ ThinkingDisplay → "AI 在想什么"
│ └─ 文件下载按钮 → 📥 下载 Excel
│
└─ MessageInput(输入区)
├─ 文本输入框
├─ 文件拖放区
└─ 发送按钮
ChatInterface:大管家
ChatInterface.tsx 是整个前端的核心,它管理所有状态和通信。
状态管理
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 监听
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();
};
}, []);
发送消息
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 处理用户输入,最特别的是支持文件拖放上传:
// 支持的文件类型
const ACCEPTED_TYPES = [
'.xlsx', '.xls', // Excel
'.pdf', // PDF
'.docx', '.doc', // Word
];
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB 上限
拖放逻辑
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 消息:
// 用户消息:Excel 绿色背景
<div style={{ background: '#217346', color: 'white' }}>
{message.content}
</div>
// AI 消息:灰色背景 + Markdown 渲染
<div style={{ background: '#f3f4f6' }}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{message.content}
</ReactMarkdown>
</div>
AI 的消息里可能包含多种内容块:
一条 AI 消息可能包含:
├── 文本内容 → ReactMarkdown 渲染
├── 工具调用 → ToolUseDisplay 渲染
├── 思考过程 → ThinkingDisplay 渲染
└── 输出文件 → 下载按钮渲染
ToolUseDisplay:工具展示
这是这个项目的一个亮点——用户能实时看到 AI 在用什么工具。
// 每个工具有自己的图标和颜色
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' },
// ...
};
渲染效果像这样:
┌────────────────────────────────────────┐
│ ⚙️ Bash │
│ │ python3 generate_budget.py │
│ │ │
│ │ ↳ Result: Budget created successf... │
│ └──────────────────────────────────────│
│ │
│ 📖 Read │
│ │ File: problems/uploaded_data.xlsx │
│ └──────────────────────────────────────│
└────────────────────────────────────────┘
工具调用可以展开/折叠,默认折叠以免占太多空间。
文件下载
当 AI 生成了 Excel 文件,界面会显示下载按钮:
// 下载按钮
<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):
┌──────────────────────────────┐
│ 整体配色: │
│ 用户消息:#217346(Excel绿) │
│ AI 消息:#f3f4f6(浅灰) │
│ 工具展示:各色左边框 │
│ 代码块:#1f2937(深灰) │
│ 字体:JetBrains Mono(等宽) │
└──────────────────────────────┘
本课小结
- 前端是一个 React 聊天界面,核心组件只有 7-8 个
- ChatInterface 是"大管家",管理状态和 IPC 通信
- MessageInput 支持文字输入和文件拖放上传
- Message 用 ReactMarkdown 渲染 AI 回复
- ToolUseDisplay 让用户实时看到 AI 在用什么工具
- 文件下载通过 IPC 调用主进程的"另存为"对话框
课后练习
- 打开
src/renderer/components/Message.tsx,看看 ReactMarkdown 是怎么用的 - 尝试修改
#217346为其他颜色(比如蓝色#2563EB),看看效果 - 在 ToolUseDisplay 里找到工具图标的配置,给 Bash 换一个图标