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(null); const [highlightId, setHighlightId] = useState(null); const [overrides, setOverrides] = useState({}); const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false); const [mcpDrawerOpen, setMcpDrawerOpen] = useState(false); const [paramsDrawerOpen, setParamsDrawerOpen] = useState(false); const [tplDrawerOpen, setTplDrawerOpen] = useState(false); const abortRef = useRef(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 (
navigate('/agents/new')} onSelect={(aid) => navigate(`/chat/${aid}`)} />
{!agent ? (
) : ( <> setHistoryDrawerOpen(true)} onOpenParams={() => setParamsDrawerOpen(true)} onOpenMcp={() => setMcpDrawerOpen(true)} onManageAgent={() => navigate(`/agents/${id}`)} onClear={sender.handleClear} />
{ const content = mode === 'markdown' ? text : markdownToPlainText(text); return navigator.clipboard ?.writeText(content) .then(() => message.success(mode === 'markdown' ? '已复制(Markdown)' : '已复制(纯文本)')); }} /> { setHighlightId(msgId); const el = document.getElementById('msg-' + msgId); if (el) el.scrollIntoView({ block: 'start', behavior: 'smooth' }); }} />
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={() => {}} /> )}
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) }} />
); }