# AI Chat 前端重构设计文档 ## 1. 项目概述 本项目旨在重构一个类似 Claude 的 AI 聊天系统前端,支持以下核心功能: - 多轮对话管理和消息树结构 - Function Call 集成(支持多厂商模型) - 消息版本控制和切换 - 实时流式输出和打字机效果 - 消息编辑、刷新、点赞、分享等操作 - 乐观更新和失败重试机制 - 与后端 API 的完整集成 ## 2. 核心设计理念 ### 2.1 数据流设计 - **线性存储**:从数据库加载的消息为线性数组,无需构建树结构 - **激活路径**:通过 `is_active` 字段标记用户当前查看的消息链 - **Session 分组**:相同 `session_id` 的消息在前端显示为一个对话组 - **乐观更新**:用户操作立即响应,异步同步到服务器 ### 2.2 版本管理 - 同一 `parent_id` 的消息作为不同版本 - 用户可通过版本切换器查看历史版本 - 编辑和刷新都创建新版本,不修改原消息 - 组件加载时载入全部 message, 版本切换时无需请求服务器,在已有的 message 数组中计算新的当前激活版本 ### 2.3 错误处理 - 手动重试机制,不自动重试 - 临时消息状态标记,区分已保存/待保存/失败状态 - 完整的错误恢复流程 ## 3. 架构设计 ```text ├── types/ │ └── chat.ts # 所有类型定义 ├── hooks/ # 数据管理层 │ ├── useChatData.ts # 主要数据管理 │ ├── useActivePath.ts # 激活路径计算 │ └── useSessionGroups.ts # Session 分组管理 ├── services/ # API调用层 │ ├── chatApi.ts # 聊天 API │ ├── messageApi.ts # 消息 API │ └── modelAdapters/ # 模型适配器 │ ├── base.ts # 基础适配器 │ ├── openai.ts # OpenAI 适配器 │ └── index.ts # 适配器工厂 └── components/ # 显示层 └── chat ├── ChatContainer.tsx # 主容器组件 ├── SessionGroup.tsx # Session 消息组 ├── UserMessage.tsx # 用户消息 ├── AssistantMessage.tsx # AI 回答 ├── MessageActions.tsx # 消息操作 ├── ChatInput.tsx # 输入组件 ├── StreamingMessage.tsx # 打字机输入组件 └── VersionSwitcher.tsx # 版本切换 ``` ## 4. 类型定义 ### types/chat.ts ```typescript // 工具调用相关类型 export interface ToolCall { id: string; function: string; arguments: Record; } // 消息元数据 export interface MessageMetadata { generation_params?: { temperature?: number; max_tokens?: number; top_p?: number; frequency_penalty?: number; presence_penalty?: number; }; token_usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number; }; performance?: { response_time_ms?: number; first_token_time_ms?: number; }; tool_stats?: { total_calls?: number; successful_calls?: number; execution_time_ms?: number; }; custom_data?: Record; } // 消息节点(对应数据库结构) export interface MessageNode { id: number; // DB自增ID,用于版本排序 uid: string; // UUID chat_id: string; parent_id?: string; session_id: string; role: "system" | "user" | "assistant" | "tool"; content?: string; model_id?: string; tool_calls?: ToolCall[]; tool_call_id?: string; metadata?: MessageMetadata; is_active: boolean; editor_id?: string; created_at: string; updated_at: string; deleted_at?: string; // 临时状态字段(前端使用) save_status?: "saved" | "pending" | "failed"; temp_id?: string; // 临时ID,用于未保存消息 } // 版本信息 export interface VersionInfo { version_index: number; // 版本索引(0,1,2...) model_id?: string; // 该版本使用的模型 model_name?: string; // 模型显示名称 created_at: string; // 版本创建时间 message_count: number; // 该版本包含的消息数量 token_usage?: number; // 该版本的token使用量 } // Session 信息 export interface SessionInfo { session_id: string; messages: MessageNode[]; // 该session的所有消息(按激活路径过滤) versions: VersionInfo[]; // 该session所有版本信息 current_version: number; // 当前显示的版本索引 user_message?: MessageNode; // 该session的用户消息(便于访问) ai_messages: MessageNode[]; // 该session的AI消息列表 } // 待保存消息组 export interface PendingMessage { temp_id: string; session_id: string; messages: MessageNode[]; // 待保存的消息组 retry_count: number; error?: string; created_at: string; } // 聊天状态 export interface ChatState { chat_id: string; title: string; raw_messages: MessageNode[]; // 从DB加载的原始线性数据 active_path: MessageNode[]; // 当前激活路径上的消息 session_groups: SessionInfo[]; // 按session分组的显示数据 pending_messages: PendingMessage[]; // 待保存的消息组 is_loading: boolean; streaming_message?: string; streaming_session_id?: string; current_model: string; error?: string; } // 聊天操作接口 export interface ChatActions { switchVersion: (sessionId: string, versionIndex: number) => Promise; editMessage: ( sessionId: string, content: string, role?: "user" | "assistant" ) => Promise; retryMessage: (tempId: string) => Promise; refreshResponse: (sessionId: string, modelId?: string) => Promise; loadMessages: () => Promise; likeMessage: (messageId: string) => Promise; dislikeMessage: (messageId: string) => Promise; copyMessage: (messageId: string) => void; shareMessage: (messageId: string) => Promise; deleteMessage: (messageId: string) => Promise; } // API 请求类型 export interface CreateMessageRequest { messages: Array<{ parent_id?: string; role: "user" | "assistant" | "tool"; content?: string; model_id?: string; tool_calls?: ToolCall[]; tool_call_id?: string; metadata?: MessageMetadata; }>; } export interface CreateChatRequest { title: string; user_id?: string; } // API 响应类型 export interface ApiResponse { ok: boolean; message: string; data: T; } export interface ChatResponse { id: string; title: string; user_id?: string; created_at: string; updated_at: string; } export interface MessageListResponse { rows: MessageNode[]; total: number; } // 模型适配器相关类型 export interface ModelAdapter { name: string; supportsFunctionCall: boolean; sendMessage( messages: OpenAIMessage[], options: SendOptions ): Promise; parseStreamChunk(chunk: string): ParsedChunk | null; handleFunctionCall(functionCall: ToolCall): Promise; } export interface OpenAIMessage { role: "system" | "user" | "assistant" | "function" | "tool"; content?: string; name?: string; tool_calls?: ToolCall[]; tool_call_id?: string; } export interface SendOptions { temperature?: number; max_tokens?: number; top_p?: number; functions?: Array<{ name: string; description: string; parameters: any; }>; function_call?: string | { name: string }; } export interface StreamResponse { messages: MessageNode[]; metadata?: MessageMetadata; } export interface ParsedChunk { content?: string; function_call?: { name?: string; arguments?: string; }; finish_reason?: string; } // 组件 Props 类型 export interface SessionGroupProps { session: SessionInfo; onVersionSwitch: (sessionId: string, versionIndex: number) => void; onRefresh: (sessionId: string, modelId?: string) => void; onEdit: (sessionId: string, content: string) => void; onRetry?: (tempId: string) => void; onLike?: (messageId: string) => void; onDislike?: (messageId: string) => void; onCopy?: (messageId: string) => void; onShare?: (messageId: string) => void; } export interface UserMessageProps { message: MessageNode; onEdit?: (content: string) => void; onCopy?: () => void; } export interface AssistantMessageProps { messages: MessageNode[]; onRefresh?: () => void; onEdit?: (content: string) => void; isPending?: boolean; onLike?: (messageId: string) => void; onDislike?: (messageId: string) => void; onCopy?: (messageId: string) => void; onShare?: (messageId: string) => void; } export interface VersionSwitcherProps { versions: VersionInfo[]; currentVersion: number; onSwitch: (versionIndex: number) => void; } export interface ChatInputProps { onSend: (content: string) => void; disabled?: boolean; placeholder?: string; } ``` ## 5. 数据管理层 ### hooks/useActivePath.ts ```typescript import { useMemo, useCallback } from "react"; import { MessageNode } from "../types/chat"; export function useActivePath(rawMessages: MessageNode[]) { const computeActivePath = useCallback(() => { // 从system消息开始,沿着is_active=true的路径构建激活链 const messageMap = new Map(rawMessages.map((m) => [m.uid, m])); const activePath: MessageNode[] = []; // 找到system消息(根节点) const systemMsg = rawMessages.find( (m) => m.role === "system" && !m.parent_id && m.is_active ); if (!systemMsg) return []; // 沿着激活路径构建链 let current: MessageNode | undefined = systemMsg; while (current) { activePath.push(current); // 找到当前消息的激活子消息 current = rawMessages.find( (m) => m.parent_id === current?.uid && m.is_active ); } return activePath; }, [rawMessages]); return useMemo(() => computeActivePath(), [computeActivePath]); } ``` ### hooks/useSessionGroups.ts ```typescript import { useMemo, useCallback } from "react"; import { MessageNode, SessionInfo, VersionInfo } from "../types/chat"; export function useSessionGroups( activePath: MessageNode[], rawMessages: MessageNode[] ) { const computeSessionVersions = useCallback( (sessionId: string): VersionInfo[] => { // 找到该session的所有消息 const sessionMessages = rawMessages.filter( (m) => m.session_id === sessionId ); // 按不同的创建时间和父消息分组,计算版本 const versionMap = new Map(); sessionMessages.forEach((msg) => { // 使用第一个AI消息的创建时间作为版本标识 const firstAiMsg = sessionMessages .filter((m) => m.role === "assistant") .sort((a, b) => a.id - b.id)[0]; const versionKey = firstAiMsg ? firstAiMsg.created_at : msg.created_at; if (!versionMap.has(versionKey)) { versionMap.set(versionKey, []); } versionMap.get(versionKey)!.push(msg); }); // 转换为VersionInfo数组 const versions: VersionInfo[] = Array.from(versionMap.entries()) .map(([timestamp, messages], index) => { const aiMessage = messages.find((m) => m.role === "assistant"); return { version_index: index, model_id: aiMessage?.model_id, created_at: timestamp, message_count: messages.length, token_usage: aiMessage?.metadata?.token_usage?.total_tokens, }; }) .sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); return versions; }, [rawMessages] ); const findCurrentVersion = useCallback( (sessionMessages: MessageNode[], versions: VersionInfo[]): number => { // 找到当前激活的AI消息 const activeAiMsg = sessionMessages.find( (m) => m.role === "assistant" && m.is_active ); if (!activeAiMsg) return 0; // 根据创建时间找到对应的版本索引 const versionIndex = versions.findIndex( (v) => v.created_at === activeAiMsg.created_at ); return Math.max(0, versionIndex); }, [] ); const computeSessionGroups = useCallback((): SessionInfo[] => { const sessionMap = new Map(); // 按session_id分组激活路径上的消息(排除system消息) activePath.forEach((msg) => { if (msg.role !== "system") { const sessionId = msg.session_id; if (!sessionMap.has(sessionId)) { sessionMap.set(sessionId, []); } sessionMap.get(sessionId)!.push(msg); } }); // 为每个session计算版本信息 const sessionGroups: SessionInfo[] = []; sessionMap.forEach((messages, sessionId) => { const versions = computeSessionVersions(sessionId); const currentVersion = findCurrentVersion(messages, versions); const userMessage = messages.find((m) => m.role === "user"); const aiMessages = messages.filter((m) => m.role !== "user"); sessionGroups.push({ session_id: sessionId, messages, versions, current_version: currentVersion, user_message: userMessage, ai_messages: aiMessages, }); }); // 按消息ID排序,保证显示顺序 return sessionGroups.sort((a, b) => { const aFirstId = Math.min(...a.messages.map((m) => m.id)); const bFirstId = Math.min(...b.messages.map((m) => m.id)); return aFirstId - bFirstId; }); }, [activePath, rawMessages, computeSessionVersions, findCurrentVersion]); return useMemo(() => computeSessionGroups(), [computeSessionGroups]); } ``` ### hooks/useChatData.ts ```typescript import { useState, useCallback, useMemo } from "react"; import { MessageNode, ChatState, ChatActions, PendingMessage, } from "../types/chat"; import { useActivePath } from "./useActivePath"; import { useSessionGroups } from "./useSessionGroups"; import { chatApi, messageApi } from "../services"; import { getModelAdapter } from "../services/modelAdapters"; export function useChatData(chatId: string): { chatState: ChatState; actions: ChatActions; } { const [rawMessages, setRawMessages] = useState([]); const [pendingMessages, setPendingMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [streamingMessage, setStreamingMessage] = useState(); const [streamingSessionId, setStreamingSessionId] = useState(); const [currentModel, setCurrentModel] = useState("gpt-4"); const [error, setError] = useState(); // 合并已保存和待保存的消息用于显示 const allMessages = useMemo(() => { const pending = pendingMessages.flatMap((p) => p.messages); return [...rawMessages, ...pending]; }, [rawMessages, pendingMessages]); const activePath = useActivePath(allMessages); const sessionGroups = useSessionGroups(activePath, allMessages); // 加载消息列表 const loadMessages = useCallback(async () => { try { setIsLoading(true); const response = await messageApi.getMessages(chatId); setRawMessages(response.data.rows); } catch (err) { setError(err instanceof Error ? err.message : "加载消息失败"); } finally { setIsLoading(false); } }, [chatId]); // 构建对话历史(用于AI API调用) const buildConversationHistory = useCallback( (baseMessages: MessageNode[], newUserMessage?: MessageNode) => { const history = activePath .filter((m) => m.role !== "tool") // 排除tool消息 .map((m) => ({ role: m.role as any, content: m.content || "", tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, })); if (newUserMessage) { history.push({ role: "user", content: newUserMessage.content || "", }); } return history; }, [activePath] ); // 发送消息给AI并处理响应 const sendMessageToAI = useCallback( async (userMessage: MessageNode, pendingGroup: PendingMessage) => { try { setIsLoading(true); setStreamingSessionId(pendingGroup.session_id); const conversationHistory = buildConversationHistory( rawMessages, userMessage ); const adapter = getModelAdapter(currentModel); // 处理Function Call的循环逻辑 let currentMessages = conversationHistory; let maxIterations = 10; let allAiMessages: MessageNode[] = []; while (maxIterations-- > 0) { // 流式处理AI响应 let responseContent = ""; let functionCalls: any[] = []; let metadata: any = {}; const streamResponse = await adapter.sendMessage(currentMessages, { temperature: 0.7, max_tokens: 2048, }); // 模拟流式输出处理 await new Promise((resolve, reject) => { const processStream = async () => { try { // 这里应该是实际的流处理逻辑 for await (const chunk of streamResponse) { const parsed = adapter.parseStreamChunk(chunk); if (parsed?.content) { responseContent += parsed.content; setStreamingMessage(responseContent); } if (parsed?.function_call) { // 处理function call } } resolve(undefined); } catch (err) { reject(err); } }; processStream(); }); // 创建AI响应消息 const aiMessage: MessageNode = { id: 0, uid: `temp_ai_${pendingGroup.temp_id}_${allAiMessages.length}`, temp_id: pendingGroup.temp_id, chat_id: chatId, session_id: pendingGroup.session_id, parent_id: allAiMessages.length === 0 ? userMessage.uid : allAiMessages[allAiMessages.length - 1].uid, role: "assistant", content: responseContent, model_id: currentModel, tool_calls: functionCalls.length > 0 ? functionCalls : undefined, metadata, is_active: true, save_status: "pending", created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; allAiMessages.push(aiMessage); // 如果有function calls,处理它们 if (functionCalls.length > 0) { const toolResults = await Promise.all( functionCalls.map((call) => adapter.handleFunctionCall(call)) ); const toolMessages = functionCalls.map((call, index) => ({ id: 0, uid: `temp_tool_${pendingGroup.temp_id}_${index}`, temp_id: pendingGroup.temp_id, chat_id: chatId, session_id: pendingGroup.session_id, parent_id: aiMessage.uid, role: "tool" as const, content: JSON.stringify(toolResults[index]), tool_call_id: call.id, is_active: true, save_status: "pending" as const, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), })); allAiMessages.push(...toolMessages); // 更新对话历史,继续循环 currentMessages.push( { role: "assistant", content: responseContent, tool_calls: functionCalls, }, ...toolMessages.map((tm) => ({ role: "tool" as const, content: tm.content || "", tool_call_id: tm.tool_call_id, })) ); continue; } // 没有function call,结束循环 break; } // 更新pending消息组 setPendingMessages((prev) => prev.map((p) => p.temp_id === pendingGroup.temp_id ? { ...p, messages: [...p.messages, ...allAiMessages] } : p ) ); // 保存整个消息组到数据库 await saveMessageGroup(pendingGroup.temp_id, [ userMessage, ...allAiMessages, ]); } catch (err) { console.error("AI响应失败:", err); setPendingMessages((prev) => prev.map((p) => p.temp_id === pendingGroup.temp_id ? { ...p, error: err instanceof Error ? err.message : "未知错误", retry_count: p.retry_count + 1, } : p ) ); } finally { setIsLoading(false); setStreamingMessage(undefined); setStreamingSessionId(undefined); } }, [rawMessages, currentModel, chatId, buildConversationHistory] ); // 保存消息组到数据库 const saveMessageGroup = useCallback( async (tempId: string, messages: MessageNode[]) => { try { const savedMessages = await messageApi.createMessages(chatId, { messages: messages.map((m) => ({ parent_id: m.parent_id, role: m.role as any, content: m.content, model_id: m.model_id, tool_calls: m.tool_calls, tool_call_id: m.tool_call_id, metadata: m.metadata, })), }); // 更新本地状态:移除pending,添加到已保存消息 setPendingMessages((prev) => prev.filter((p) => p.temp_id !== tempId)); setRawMessages((prev) => [...prev, ...savedMessages.data]); } catch (err) { console.error("保存消息组失败:", err); setPendingMessages((prev) => prev.map((p) => p.temp_id === tempId ? { ...p, error: err instanceof Error ? err.message : "保存失败", messages: p.messages.map((m) => ({ ...m, save_status: "failed" as const, })), } : p ) ); } }, [chatId] ); // 编辑消息 - 创建新版本 const editMessage = useCallback( async ( sessionId: string, content: string, role: "user" | "assistant" = "user" ) => { const tempId = `temp_${Date.now()}`; try { // 找到要编辑的消息的父消息 let parentId: string | undefined; if (sessionId === "new") { // 新消息,找到最后一个激活消息作为父消息 const lastMessage = activePath[activePath.length - 1]; parentId = lastMessage?.uid; } else { // 编辑现有session,找到该session的父消息 const sessionMessages = activePath.filter( (m) => m.session_id === sessionId ); const firstMessage = sessionMessages[0]; parentId = firstMessage?.parent_id; } const newSessionId = sessionId === "new" ? `session_${tempId}` : `session_${tempId}`; // 创建新的用户消息 const newUserMessage: MessageNode = { id: 0, uid: `temp_user_${tempId}`, temp_id: tempId, chat_id: chatId, parent_id: parentId, session_id: newSessionId, role: "user", content, is_active: true, save_status: "pending", created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; // 创建待保存消息组 const pendingGroup: PendingMessage = { temp_id: tempId, session_id: newSessionId, messages: [newUserMessage], retry_count: 0, created_at: new Date().toISOString(), }; setPendingMessages((prev) => [...prev, pendingGroup]); // 如果是用户消息,发送给AI if (role === "user") { await sendMessageToAI(newUserMessage, pendingGroup); } } catch (err) { console.error("编辑消息失败:", err); setPendingMessages((prev) => prev.map((p) => p.temp_id === tempId ? { ...p, messages: p.messages.map((m) => ({ ...m, save_status: "failed" as const, })), error: err instanceof Error ? err.message : "编辑失败", } : p ) ); } }, [chatId, activePath, sendMessageToAI] ); // 重试失败的消息 const retryMessage = useCallback( async (tempId: string) => { const pendingGroup = pendingMessages.find((p) => p.temp_id === tempId); if (!pendingGroup) return; const userMessage = pendingGroup.messages.find((m) => m.role === "user"); if (!userMessage) return; // 重置状态并重试 setPendingMessages((prev) => prev.map((p) => p.temp_id === tempId ? { ...p, messages: [{ ...userMessage, save_status: "pending" }], error: undefined, } : p ) ); await sendMessageToAI(userMessage, { ...pendingGroup, messages: [userMessage], }); }, [pendingMessages, sendMessageToAI] ); // 切换版本 const switchVersion = useCallback( async (sessionId: string, versionIndex: number) => { try { // 找到指定版本的消息 const sessionMessages = rawMessages.filter( (m) => m.session_id === sessionId ); const versions = sessionGroups.find((sg) => sg.session_id === sessionId)?.versions || []; if (versionIndex >= versions.length) return; const targetVersion = versions[versionIndex]; const versionMessages = sessionMessages.filter( (m) => m.created_at === targetVersion.created_at ); // 调用API更新激活状态 await messageApi.switchVersion( chatId, versionMessages.map((m) => m.uid) ); // 重新加载数据 await loadMessages(); } catch (err) { console.error("切换版本失败:", err); setError(err instanceof Error ? err.message : "切换版本失败"); } }, [rawMessages, sessionGroups, chatId, loadMessages] ); // 刷新AI回答 const refreshResponse = useCallback( async (sessionId: string, modelId?: string) => { const session = sessionGroups.find((sg) => sg.session_id === sessionId); if (!session?.user_message) return; // 使用指定的模型或当前模型 const useModel = modelId || currentModel; const tempId = `temp_refresh_${Date.now()}`; try { // 创建基于原用户消息的新AI回答 const userMsg = session.user_message; const newSessionId = `session_${tempId}`; const pendingGroup: PendingMessage = { temp_id: tempId, session_id: newSessionId, messages: [ { ...userMsg, temp_id: tempId, session_id: newSessionId, save_status: "pending", }, ], retry_count: 0, created_at: new Date().toISOString(), }; setPendingMessages((prev) => [...prev, pendingGroup]); // 发送给AI获取新回答 await sendMessageToAI(pendingGroup.messages[0], pendingGroup); } catch (err) { console.error("刷新回答失败:", err); setError(err instanceof Error ? err.message : "刷新失败"); } }, [sessionGroups, currentModel, sendMessageToAI] ); // 消息操作功能 const likeMessage = useCallback(async (messageId: string) => { try { await messageApi.likeMessage(messageId); // 可以添加本地状态更新 } catch (err) { console.error("点赞失败:", err); } }, []); const dislikeMessage = useCallback(async (messageId: string) => { try { await messageApi.dislikeMessage(messageId); // 可以添加本地状态更新 } catch (err) { console.error("点踩失败:", err); } }, []); const copyMessage = useCallback( (messageId: string) => { const message = allMessages.find((m) => m.uid === messageId); if (message?.content) { navigator.clipboard.writeText(message.content); } }, [allMessages] ); const shareMessage = useCallback( async (messageId: string): Promise => { try { const response = await messageApi.shareMessage(messageId); return response.data.shareUrl; } catch (err) { console.error("分享失败:", err); throw err; } }, [] ); const deleteMessage = useCallback( async (messageId: string) => { try { await messageApi.deleteMessage(messageId); await loadMessages(); // 重新加载数据 } catch (err) { console.error("删除失败:", err); setError(err instanceof Error ? err.message : "删除失败"); } }, [loadMessages] ); return { chatState: { chat_id: chatId, title: "", // 可以从props传入或另行管理 raw_messages: rawMessages, active_path: activePath, session_groups: sessionGroups, pending_messages: pendingMessages, is_loading: isLoading, streaming_message: streamingMessage, streaming_session_id: streamingSessionId, current_model: currentModel, error, }, actions: { switchVersion, editMessage, retryMessage, refreshResponse, loadMessages, likeMessage, dislikeMessage, copyMessage, shareMessage, deleteMessage, }, }; } ``` ## 6. API 调用层 ### services/chatApi.ts ```typescript import { CreateChatRequest, ChatResponse, ApiResponse } from "../types/chat"; export const chatApi = { async createChat( request: CreateChatRequest ): Promise> { const response = await fetch("/api/v2/chats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(request), }); return response.json(); }, async getChat(chatId: string): Promise> { const response = await fetch(`/api/v2/chats/${chatId}`); return response.json(); }, async updateChat( chatId: string, updates: Partial ): Promise> { const response = await fetch(`/api/v2/chats/${chatId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(updates), }); return response.json(); }, async deleteChat(chatId: string): Promise> { const response = await fetch(`/api/v2/chats/${chatId}`, { method: "DELETE", }); return response.json(); }, }; ``` ### services/messageApi.ts ```typescript import { CreateMessageRequest, MessageListResponse, ApiResponse, MessageNode, } from "../types/chat"; export const messageApi = { async getMessages(chatId: string): Promise> { const response = await fetch(`/api/v2/chat-messages?chat=${chatId}`); return response.json(); }, async createMessages( chatId: string, request: CreateMessageRequest ): Promise> { const response = await fetch("/api/v2/chat-messages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, ...request, }), }); return response.json(); }, async switchVersion( chatId: string, messageUids: string[] ): Promise> { const response = await fetch("/api/v2/chat-messages/switch-version", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chat_id: chatId, message_uids: messageUids, }), }); return response.json(); }, async likeMessage(messageId: string): Promise> { const response = await fetch(`/api/v2/chat-messages/${messageId}/like`, { method: "POST", }); return response.json(); }, async dislikeMessage(messageId: string): Promise> { const response = await fetch(`/api/v2/chat-messages/${messageId}/dislike`, { method: "POST", }); return response.json(); }, async shareMessage( messageId: string ): Promise> { const response = await fetch(`/api/v2/chat-messages/${messageId}/share`, { method: "POST", }); return response.json(); }, async deleteMessage(messageId: string): Promise> { const response = await fetch(`/api/v2/chat-messages/${messageId}`, { method: "DELETE", }); return response.json(); }, }; ``` ### services/modelAdapters/base.ts ```typescript import { ModelAdapter, OpenAIMessage, SendOptions, ParsedChunk, ToolCall, } from "../../types/chat"; export abstract class BaseModelAdapter implements ModelAdapter { abstract name: string; abstract supportsFunctionCall: boolean; abstract sendMessage( messages: OpenAIMessage[], options: SendOptions ): Promise>; abstract parseStreamChunk(chunk: string): ParsedChunk | null; abstract handleFunctionCall(functionCall: ToolCall): Promise; protected createStreamController() { return { addToken: (token: string) => { // 流式输出控制逻辑 }, complete: () => { // 完成处理逻辑 }, }; } protected buildRequestPayload( messages: OpenAIMessage[], options: SendOptions ) { return { model: this.name, messages, stream: true, temperature: options.temperature || 0.7, max_tokens: options.max_tokens || 2048, top_p: options.top_p || 1, functions: options.functions, function_call: options.function_call || "auto", }; } } ``` ### services/modelAdapters/openai.ts ```typescript import { BaseModelAdapter } from "./base"; import { OpenAIMessage, SendOptions, ParsedChunk, ToolCall, } from "../../types/chat"; export class OpenAIAdapter extends BaseModelAdapter { name = "gpt-4"; supportsFunctionCall = true; async *sendMessage( messages: OpenAIMessage[], options: SendOptions ): AsyncIterable { const payload = this.buildRequestPayload(messages, options); const response = await fetch(process.env.REACT_APP_OPENAI_PROXY!, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.REACT_APP_OPENAI_KEY}`, }, body: JSON.stringify({ model_id: "gpt-4", payload, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error("无法获取响应流"); } const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.trim() || !line.startsWith("data: ")) continue; const data = line.slice(6); if (data === "[DONE]") return; yield data; } } } finally { reader.releaseLock(); } } parseStreamChunk(chunk: string): ParsedChunk | null { try { const parsed = JSON.parse(chunk); const delta = parsed.choices?.[0]?.delta; const finishReason = parsed.choices?.[0]?.finish_reason; return { content: delta?.content, function_call: delta?.function_call, finish_reason: finishReason, }; } catch { return null; } } async handleFunctionCall(functionCall: ToolCall): Promise { // 实现具体的函数调用逻辑 // 这里应该根据function name调用相应的处理器 switch (functionCall.function) { case "searchTerm": return await this.searchTerm(functionCall.arguments.term); case "getWeather": return await this.getWeather(functionCall.arguments.city); default: throw new Error(`未知函数: ${functionCall.function}`); } } private async searchTerm(term: string) { // 调用搜索API const response = await fetch( `/v2/search-pali-wbw?view=pali&key=${term}&limit=20&offset=0` ); const result = await response.json(); return result.ok ? result.data.rows : { error: "搜索失败" }; } private async getWeather(city: string) { // 模拟天气查询 return { city, temperature: "25°C", condition: "晴朗", humidity: "60%", }; } } ``` ### services/modelAdapters/index.ts ```typescript import { ModelAdapter } from "../../types/chat"; import { OpenAIAdapter } from "./openai"; const adapters = new Map(); // 注册适配器 adapters.set("gpt-4", new OpenAIAdapter()); adapters.set("gpt-3.5-turbo", new OpenAIAdapter()); export function getModelAdapter(modelId: string): ModelAdapter { const adapter = adapters.get(modelId); if (!adapter) { throw new Error(`未找到模型适配器: ${modelId}`); } return adapter; } export function registerAdapter(modelId: string, adapter: ModelAdapter) { adapters.set(modelId, adapter); } ``` ## 7. 显示层组件 ### components/ChatContainer.tsx ```typescript import React, { useEffect } from "react"; import { useChatData } from "../hooks/useChatData"; import { SessionGroup } from "./SessionGroup"; import { ChatInput } from "./ChatInput"; import { StreamingMessage } from "./StreamingMessage"; interface ChatContainerProps { chatId: string; } export function ChatContainer({ chatId }: ChatContainerProps) { const { chatState, actions } = useChatData(chatId); useEffect(() => { actions.loadMessages(); }, [chatId, actions.loadMessages]); return (
{chatState.session_groups.map((session) => ( ))} {/* 流式消息显示 */} {chatState.streaming_message && ( )} {/* 错误提示 */} {chatState.error && (
{chatState.error}
)}
actions.editMessage("new", content)} disabled={chatState.is_loading} placeholder="输入你的问题..." />
); } ``` ### components/SessionGroup.tsx ```typescript import React from "react"; import { Button } from "antd"; import { SessionGroupProps } from "../types/chat"; import { UserMessage } from "./UserMessage"; import { AssistantMessage } from "./AssistantMessage"; import { VersionSwitcher } from "./VersionSwitcher"; export function SessionGroup({ session, onVersionSwitch, onRefresh, onEdit, onRetry, onLike, onDislike, onCopy, onShare, }: SessionGroupProps) { const hasFailed = session.messages.some((m) => m.save_status === "failed"); const isPending = session.messages.some((m) => m.save_status === "pending"); const hasMultipleVersions = session.versions.length > 1; return (
{/* 用户消息 */} {session.user_message && ( onEdit && onEdit(session.session_id, content)} onCopy={() => onCopy && onCopy(session.user_message!.uid)} /> )} {/* AI回答区域 */}
{/* 失败重试提示 */} {hasFailed && onRetry && (
回答生成失败
)} {/* 版本切换器 */} {hasMultipleVersions && !isPending && !hasFailed && ( onVersionSwitch(session.session_id, versionIndex) } /> )} {/* AI消息内容 */} {!hasFailed && session.ai_messages.length > 0 && ( onRefresh && onRefresh(session.session_id)} onEdit={(content) => onEdit && onEdit(session.session_id, content, "assistant") } isPending={isPending} onLike={onLike} onDislike={onDislike} onCopy={onCopy} onShare={onShare} /> )}
); } ``` ### components/UserMessage.tsx ```typescript import React, { useState } from "react"; import { Button, Input } from "antd"; import { EditOutlined, CopyOutlined } from "@ant-design/icons"; import { UserMessageProps } from "../types/chat"; const { TextArea } = Input; export function UserMessage({ message, onEdit, onCopy }: UserMessageProps) { const [isEditing, setIsEditing] = useState(false); const [editContent, setEditContent] = useState(message.content || ""); const handleEdit = () => { if (onEdit && editContent.trim()) { onEdit(editContent.trim()); setIsEditing(false); } }; const handleCancel = () => { setEditContent(message.content || ""); setIsEditing(false); }; return (
You
{!isEditing && ( <>
{isEditing ? (