import { useChat } from "@ai-sdk/react"; import { CopyOutlined, EditOutlined, RedoOutlined, RobotOutlined } from "@ant-design/icons"; import { Sender } from "@ant-design/x"; import { useQueryClient } from "@tanstack/react-query"; import { DefaultChatTransport, type UIMessage } from "ai"; import { App, Button, Card, Divider, Flex, Input, Select, Spin, Typography } from "antd"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createConversation, fetchConversation, fetchMessages, updateConversation, } from "../../shared/hooks/use-conversations"; import { useLogger } from "../../shared/hooks/use-logger"; import { ChatScrollArea } from "./ChatScrollArea"; import { ReasoningPart } from "./parts/ReasoningPart"; import { TextPart } from "./parts/TextPart"; import { ToolPart } from "./parts/ToolPart"; interface ChatPanelProps { conversationId: null | string; defaultModelId: null | string; onConversationCreated: (id: string) => void; projectId: string; textModels: Array<{ id: string; name: string }>; } export function ChatPanel({ conversationId, defaultModelId: _defaultModelId, onConversationCreated, projectId, textModels, }: ChatPanelProps) { const { message } = App.useApp(); const logger = useLogger({ component: "ChatPanel", page: "workbench" }); const queryClient = useQueryClient(); const [input, setInput] = useState(""); const [editingMessageId, setEditingMessageId] = useState(null); const [editText, setEditText] = useState(""); const [loadingHistory, setLoadingHistory] = useState(false); const [selectedModelId, setSelectedModelId] = useState(null); const fetchRef = useRef(fetchMessages); const skipHistoryLoadRef = useRef(null); const modelOptions = useMemo(() => textModels.map((m) => ({ label: m.name, value: m.id })), [textModels]); const { messages, regenerate, sendMessage, setMessages, status, stop } = useChat({ onError: (err) => { logger.error("聊天发送失败", { error: err.message }); void message.error(`发送失败:${err.message}`); }, transport: new DefaultChatTransport({ api: `/api/projects/${projectId}/chat` }), }); const isLoading = status === "submitted" || status === "streaming"; useEffect(() => { if (!conversationId) { setMessages([]); return; } if (skipHistoryLoadRef.current === conversationId) { skipHistoryLoadRef.current = null; return; } let cancelled = false; const load = async () => { setLoadingHistory(true); setInput(""); setMessages([]); try { const msgPromise = fetchRef.current(projectId, conversationId); const data = await msgPromise; if (cancelled) return; const history = data.items .filter((m: { role: string }) => m.role === "user" || m.role === "assistant") .reverse() .map((m: { content: string; id: string; parts: null | string; role: string }) => ({ id: m.id, parts: m.parts ? (JSON.parse(m.parts) as UIMessage["parts"]) : [{ text: m.content, type: "text" as const }], role: m.role as "assistant" | "user", })); setMessages(history); } catch (err: unknown) { if (!cancelled) { const msg = err instanceof Error ? err.message : String(err); logger.error("加载历史失败", { conversationId, error: msg, projectId }); void message.error(`加载历史失败:${msg}`); } } finally { if (!cancelled) setLoadingHistory(false); } }; void load(); return () => { cancelled = true; }; }, [conversationId, projectId, setMessages, message, logger]); useEffect(() => { if (!conversationId) return; const firstTextId = textModels[0]?.id; if (!firstTextId) return; void fetchConversation(projectId, conversationId) .then((conv) => { if (textModels.every((m) => m.id !== conv.modelId)) { setSelectedModelId(firstTextId); void updateConversation(projectId, conversationId, { modelId: firstTextId }); } else { setSelectedModelId(conv.modelId); } }) .catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); logger.warn("获取会话模型信息失败", { conversationId, error: msg, projectId }); }); }, [conversationId, textModels, projectId, logger]); useEffect(() => { if (status === "ready" && conversationId) { void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] }); } }, [status, conversationId, projectId, queryClient]); const displayModelId = conversationId ? selectedModelId : (textModels[0]?.id ?? null); const handleModelChange = useCallback( (value: string) => { setSelectedModelId(value); if (conversationId) { void updateConversation(projectId, conversationId, { modelId: value }).catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); logger.warn("更新会话模型失败", { conversationId, error: msg, projectId }); }); } }, [projectId, conversationId, logger], ); const handleSenderSubmit = useCallback( (msg: string) => { if (!msg.trim()) return; if (!conversationId) { void (async () => { try { const conv = await createConversation(projectId, displayModelId ?? undefined); skipHistoryLoadRef.current = conv.id; void queryClient.invalidateQueries({ queryKey: ["conversations", projectId] }); void sendMessage({ text: msg }, { body: { conversationId: conv.id } }); setInput(""); onConversationCreated(conv.id); } catch (err: unknown) { setInput(msg); const errMsg = err instanceof Error ? err.message : String(err); logger.error("创建会话失败", { error: errMsg, projectId }); void message.error(`创建会话失败:${errMsg}`); } })(); return; } setInput(""); void sendMessage({ text: msg }, { body: { conversationId } }).catch((err: unknown) => { const errMsg = err instanceof Error ? err.message : String(err); logger.error("发送消息失败", { conversationId, error: errMsg, projectId }); }); }, [sendMessage, conversationId, projectId, onConversationCreated, message, queryClient, displayModelId, logger], ); const renderSenderFooter = useCallback( (actionNode: ReactNode) => (