133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
import type { BubbleItemType } from "@ant-design/x";
|
|
|
|
import { useChat } from "@ai-sdk/react";
|
|
import { Bubble, Sender } from "@ant-design/x";
|
|
import { DefaultChatTransport } from "ai";
|
|
import { App, Empty, Spin } from "antd";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
import { fetchMessages } from "../../../../hooks/use-conversations";
|
|
import { MessageBubble } from "./MessageBubble";
|
|
|
|
interface ChatPanelProps {
|
|
conversationId: null | string;
|
|
onConversationCreated: (id: string) => void;
|
|
projectId: string;
|
|
}
|
|
|
|
export function ChatPanel({ conversationId, projectId }: ChatPanelProps) {
|
|
const { message } = App.useApp();
|
|
const [input, setInput] = useState("");
|
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
|
const fetchRef = useRef(fetchMessages);
|
|
|
|
const conversationIdRef = useRef(conversationId);
|
|
useEffect(() => {
|
|
conversationIdRef.current = conversationId;
|
|
});
|
|
|
|
const { messages, sendMessage, setMessages, status } = useChat({
|
|
onError: (err) => {
|
|
void message.error(`发送失败:${err.message}`);
|
|
},
|
|
transport: new DefaultChatTransport({
|
|
api: `/api/projects/${projectId}/chat`,
|
|
}),
|
|
});
|
|
|
|
const isLoading = status === "submitted" || status === "streaming";
|
|
|
|
useEffect(() => {
|
|
if (!conversationId) {
|
|
setMessages([]);
|
|
return;
|
|
}
|
|
|
|
let cancelled = false;
|
|
|
|
const load = async () => {
|
|
setLoadingHistory(true);
|
|
setMessages([]);
|
|
try {
|
|
const data = await fetchRef.current(projectId, conversationId);
|
|
if (cancelled) return;
|
|
const history = data.items
|
|
.filter((m: { role: string }) => m.role === "user" || m.role === "assistant")
|
|
.reverse()
|
|
.map((m: { content: string; id: string; role: string }) => ({
|
|
id: m.id,
|
|
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);
|
|
void message.error(`加载历史失败:${msg}`);
|
|
}
|
|
} finally {
|
|
if (!cancelled) setLoadingHistory(false);
|
|
}
|
|
};
|
|
|
|
void load();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [conversationId, projectId, setMessages, message]);
|
|
|
|
const bubbleItems: BubbleItemType[] = messages.map((msg) => ({
|
|
content: msg.parts
|
|
.filter((p): p is { text: string; type: "text" } => p.type === "text")
|
|
.map((p) => p.text)
|
|
.join(""),
|
|
key: msg.id,
|
|
role: msg.role === "user" ? "user" : "ai",
|
|
}));
|
|
|
|
const onSubmit = useCallback(
|
|
(nextInput: string) => {
|
|
if (!nextInput.trim()) return;
|
|
setInput("");
|
|
void sendMessage({ text: nextInput }, { body: { conversationId: conversationIdRef.current } });
|
|
},
|
|
[sendMessage],
|
|
);
|
|
|
|
if (!conversationId) {
|
|
return (
|
|
<div className="app-chat-panel app-chat-panel-empty">
|
|
<Empty description="选择或创建一个会话开始聊天" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="app-chat-panel">
|
|
{loadingHistory ? (
|
|
<div className="app-chat-panel-loading">
|
|
<Spin />
|
|
</div>
|
|
) : (
|
|
<Bubble.List
|
|
items={bubbleItems}
|
|
role={{
|
|
ai: {
|
|
contentRender: (content: string) => <MessageBubble content={content} />,
|
|
placement: "start",
|
|
},
|
|
user: {
|
|
placement: "end",
|
|
},
|
|
}}
|
|
style={{ flex: 1, overflow: "auto", padding: "16px" }}
|
|
/>
|
|
)}
|
|
<div className="app-chat-panel-sender">
|
|
<Sender loading={isLoading} onChange={setInput} onSubmit={onSubmit} placeholder="输入消息..." value={input} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|