aura-web/src/pages/chat/ChatPage.tsx

269 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { useEffect, useRef, useState } from 'react';
import { App as AntApp, Empty } from 'antd';
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
import type { ModelOverrides } from '../../api';
import { SessionAPI } from '../../api';
import AgentSidebar from './components/AgentSidebar';
import ChatHeader from './components/ChatHeader';
import ChatBody from './components/ChatBody';
import ChatInput from './components/ChatInput';
import ChatDrawers from './components/ChatDrawers';
import ChatOutline from './components/ChatOutline';
import { useChatScroll } from './hooks/useChatScroll';
import { useChatData } from './hooks/useChatData';
import { useChatSender } from './hooks/useChatSender';
import { markdownToPlainText } from './utils/copy';
const lastRoomKey = (agentId: string) => `chat:lastRoom:${agentId}`;
const isValidRoomId = (v: string | null): v is string => !!v && !String(v).startsWith('legacy_');
export default function ChatPage() {
const { id } = useParams();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const { message } = AntApp.useApp();
const [roomId, setRoomId] = useState<string | null>(null);
const [highlightId, setHighlightId] = useState<string | null>(null);
const [overrides, setOverrides] = useState<ModelOverrides>({});
const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false);
const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false);
const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false);
const [tplDrawerOpen, setTplDrawerOpen] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const { bodyRef, scrollBottom, initialScrollDoneRef } = useChatScroll();
useEffect(() => {
if (!id) return;
abortRef.current?.abort();
setRoomId(null);
setHighlightId(null);
const s = searchParams.get('session');
const m = searchParams.get('msg');
if (isValidRoomId(s)) {
setRoomId(s);
try {
localStorage.setItem(lastRoomKey(id), s);
} catch {
// ignore
}
if (m) {
setHighlightId(m);
const next = new URLSearchParams(searchParams);
next.delete('msg');
setSearchParams(next, { replace: true });
}
return;
}
if (s && !isValidRoomId(s)) {
const next = new URLSearchParams(searchParams);
next.delete('session');
next.delete('msg');
setSearchParams(next, { replace: true });
}
const saved = (() => {
try {
return localStorage.getItem(lastRoomKey(id));
} catch {
return null;
}
})();
if (isValidRoomId(saved)) {
setRoomId(saved);
return;
}
if (saved && !isValidRoomId(saved)) {
try {
localStorage.removeItem(lastRoomKey(id));
} catch {
// ignore
}
}
(async () => {
try {
const list = await SessionAPI.list(id, '0');
if (list.length > 0) {
setRoomId(list[0].id);
return;
}
const created = await SessionAPI.create(id);
setRoomId(created.id);
} catch (e: any) {
message.error('加载房间失败:' + (e?.message ?? e));
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
useEffect(() => {
if (!id || !roomId) return;
try {
localStorage.setItem(lastRoomKey(id), roomId);
} catch {
// ignore
}
const cur = searchParams.get('session');
if (cur === roomId) return;
const next = new URLSearchParams(searchParams);
next.set('session', roomId);
next.delete('msg');
setSearchParams(next, { replace: true });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, roomId]);
const { agent, agentList, messages, setMessages, branches, setBranches, loadMessages } = useChatData({
agentId: id,
roomId,
highlightId,
setHighlightId,
scrollBottom,
initialScrollDoneRef,
setOverrides: (updater) => setOverrides(updater),
abort: () => abortRef.current?.abort()
});
const sender = useChatSender({
agentId: id,
agent,
agentList,
roomId,
overrides,
setOverrides: (updater) => setOverrides(updater),
messages,
setMessages,
setBranches,
loadMessages,
scrollBottom,
notify: { success: (t) => message.success(t), error: (t) => message.error(t) },
abortRef
});
return (
<div className="chat-shell">
<AgentSidebar
agentList={agentList}
activeAgentId={id}
onCreate={() => navigate('/agents/new')}
onSelect={(aid) => navigate(`/chat/${aid}`)}
/>
<section className="chat-main">
{!agent ? (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Empty description="请在左侧选择一个智能体开始对话" />
</div>
) : (
<>
<ChatHeader
agent={agent}
useStream={sender.useStream}
setUseStream={sender.setUseStream}
onOpenHistory={() => setHistoryDrawerOpen(true)}
onOpenParams={() => setParamsDrawerOpen(true)}
onOpenMcp={() => setMcpDrawerOpen(true)}
onManageAgent={() => navigate(`/agents/${id}`)}
onClear={sender.handleClear}
/>
<div className="chat-content-row">
<ChatBody
bodyRef={bodyRef}
agent={agent}
agentList={agentList}
currentAgentId={id!}
messages={messages}
branches={branches}
highlightId={highlightId}
sending={sender.sending}
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
return navigator.clipboard
?.writeText(content)
.then(() => message.success(mode === 'markdown' ? '已复制Markdown' : '已复制(纯文本)'));
}}
/>
<ChatOutline
messages={messages}
activeId={highlightId}
onJump={(msgId) => {
setHighlightId(msgId);
const el = document.getElementById('msg-' + msgId);
if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' });
}}
/>
</div>
<ChatInput
input={sender.input}
setInput={sender.setInput}
sending={sender.sending}
attachments={sender.attachments}
setAttachments={(updater) => sender.setAttachments(updater)}
imageUrls={sender.imageUrls}
setImageUrls={(updater) => sender.setImageUrls(updater)}
onSend={sender.handleSend}
onStop={sender.handleStop}
onAttach={sender.handleAttach}
onOpenTpl={() => setTplDrawerOpen(true)}
modelOptions={sender.modelOptions}
activeModelValue={sender.activeModelValue}
onChangeModel={(modelId) => {
const selected = sender.modelOptions.find((m) => m.value === modelId);
setOverrides((o) => ({ ...o, model_id: String(modelId), model: selected?.label || String(modelId) }));
}}
onOpenHistory={() => setHistoryDrawerOpen(true)}
onNewSession={async () => {
if (!id) return;
abortRef.current?.abort();
try {
const created = await SessionAPI.create(id);
setHighlightId(null);
setMessages(() => []);
setBranches({});
setRoomId(created.id);
} catch (e: any) {
message.error('创建房间失败:' + (e?.message ?? e));
}
}}
agentList={agentList}
onInsertMention={() => {}}
/>
</>
)}
</section>
<ChatDrawers
agentId={id}
agent={agent}
input={sender.input}
setInput={(updater) => sender.setInput(updater(sender.input))}
overrides={overrides}
setOverrides={(updater) => setOverrides(updater)}
roomId={roomId}
setRoomId={setRoomId}
setHighlightId={setHighlightId}
sessionRefresh={sender.sessionRefresh}
mcpDrawerOpen={mcpDrawerOpen}
setMcpDrawerOpen={setMcpDrawerOpen}
tplDrawerOpen={tplDrawerOpen}
setTplDrawerOpen={setTplDrawerOpen}
paramsDrawerOpen={paramsDrawerOpen}
setParamsDrawerOpen={setParamsDrawerOpen}
historyDrawerOpen={historyDrawerOpen}
setHistoryDrawerOpen={setHistoryDrawerOpen}
notify={{ success: (t) => message.success(t) }}
/>
</div>
);
}