feat: 工作台聊天室功能
This commit is contained in:
132
src/web/consoles/workbench/components/chat/ChatPanel.tsx
Normal file
132
src/web/consoles/workbench/components/chat/ChatPanel.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user