"use client"; import React, { useCallback, useEffect, useRef, useState, } from "react"; import { Box, Drawer, alpha, useTheme } from "@mui/material"; import { useNotification } from "@refinedev/core"; import { getAccessToken } from "@/lib/authToken"; import type { AgentApprovalMode, AgentModel } from "@/lib/chatStream"; import { useProjectStore } from "@/store/projectStore"; import { AgentComposer, type AgentComposerHandle } from "./AgentComposer"; import { AgentHeader } from "./AgentHeader"; import { AgentHistoryPanel } from "./AgentHistoryPanel"; import { AgentWorkspace } from "./AgentWorkspace"; import { Blob } from "./GlobalChatbox.parts"; import type { Props } from "./GlobalChatbox.types"; import { PRESET_PROMPTS } from "./GlobalChatbox.utils"; import { useSpeechRecognition, useSpeechSynthesis } from "./GlobalChatbox.voice"; import { useAgentChatSession } from "./hooks/useAgentChatSession"; import { useAgentToolActions } from "./hooks/useAgentToolActions"; export const GlobalChatbox: React.FC = ({ open, onClose }) => { const [width, setWidth] = useState(520); const [isResizing, setIsResizing] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isCheckingAuth, setIsCheckingAuth] = useState(false); const [selectedModel, setSelectedModel] = useState( "deepseek/deepseek-v4-pro", ); const [approvalMode, setApprovalMode] = useState("request"); const bottomRef = useRef(null); const composerRef = useRef(null); const hasResetForOpenRef = useRef(false); const theme = useTheme(); const { open: openNotification } = useNotification(); const currentProjectId = useProjectStore((state) => state.currentProjectId); const { speechState, speakingMessageId, speak: handleSpeak, pause: handlePauseSpeech, resume: handleResumeSpeech, stop: handleStopSpeech, isSupported: isTtsSupported, } = useSpeechSynthesis(); const handleSpeechResult = useCallback((text: string) => { composerRef.current?.append(text); }, []); const { isListening, start: startListening, stop: stopListening, isSupported: isSttSupported, } = useSpeechRecognition(handleSpeechResult); const handleToolCall = useAgentToolActions(); const { messages, chatSessions, activeSessionId, branchGroups, branchTransition, isHydrating, isStreaming, sessionTitle, sendPrompt, regenerate, editAndResubmit, cycleBranch, abort, replyPermission, createSession, renameSession, removeSession, switchSession, } = useAgentChatSession({ projectId: currentProjectId, onToolCall: handleToolCall, onBeforeSend: stopListening, getModel: () => selectedModel, getApprovalMode: () => approvalMode, }); const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { bottomRef.current?.scrollIntoView({ behavior }); }, []); useEffect(() => { scrollToBottom(isStreaming ? "auto" : "smooth"); }, [isStreaming, messages, scrollToBottom]); useEffect(() => { if (!open) { hasResetForOpenRef.current = false; return; } if (hasResetForOpenRef.current || isHydrating) return; hasResetForOpenRef.current = true; const timer = window.setTimeout(() => { createSession(); composerRef.current?.clear(); setIsHistoryOpen(false); composerRef.current?.focus(); scrollToBottom("auto"); }, 0); return () => window.clearTimeout(timer); }, [createSession, isHydrating, open, scrollToBottom]); const handleSend = useCallback(async (prompt: string) => { if (isStreaming || isCheckingAuth) return; setIsCheckingAuth(true); try { const accessToken = await getAccessToken(); if (!accessToken) { composerRef.current?.setValue(prompt); openNotification?.({ type: "error", message: "登录状态已失效", description: "请重新登录后再发送对话。", }); return; } void sendPrompt(prompt); } catch (error) { composerRef.current?.setValue(prompt); openNotification?.({ type: "error", message: "登录状态校验失败", description: error instanceof Error ? error.message : "请重新登录后再试。", }); } finally { setIsCheckingAuth(false); } }, [isCheckingAuth, isStreaming, openNotification, sendPrompt]); const handleNewConversation = useCallback(() => { handleStopSpeech(); stopListening(); createSession(); composerRef.current?.clear(); window.setTimeout(() => { composerRef.current?.focus(); scrollToBottom("auto"); }, 0); }, [createSession, handleStopSpeech, scrollToBottom, stopListening]); const handleHistoryToggle = useCallback(() => { setIsHistoryOpen((prev) => !prev); }, []); const handleSelectSession = useCallback( (sessionId: string) => { composerRef.current?.clear(); void switchSession(sessionId); }, [switchSession], ); const handleDeleteSession = useCallback( (sessionId: string) => { void removeSession(sessionId); }, [removeSession], ); const handleRenameSession = useCallback( (sessionId: string, title: string) => { void renameSession(sessionId, title); }, [renameSession], ); const handleRenameActiveSession = useCallback( (title: string) => { if (!activeSessionId) return; void renameSession(activeSessionId, title); }, [activeSessionId, renameSession], ); const handleMouseDown = useCallback((event: React.MouseEvent) => { event.preventDefault(); setIsResizing(true); }, []); useEffect(() => { const handleMouseMove = (event: MouseEvent) => { if (!isResizing) return; const newWidth = window.innerWidth - event.clientX; if (newWidth > 360 && newWidth < 800) { setWidth(newWidth); } }; const handleMouseUp = () => { setIsResizing(false); }; if (isResizing) { window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mouseup", handleMouseUp); } return () => { window.removeEventListener("mousemove", handleMouseMove); window.removeEventListener("mouseup", handleMouseUp); }; }, [isResizing]); return ( muiTheme.zIndex.modal + 100, pointerEvents: "none", }} PaperProps={{ sx: { width: { xs: "100%", sm: width }, background: "transparent", boxShadow: "none", overflow: open ? "visible" : "hidden", zIndex: (muiTheme) => muiTheme.zIndex.modal + 100, pointerEvents: "auto", transition: isResizing ? "none" : undefined, }, }} > setIsHistoryOpen(false)} sx={{ position: "absolute", inset: 0, bgcolor: alpha("#000", 0.05), backdropFilter: "blur(2px)", opacity: isHistoryOpen ? 1 : 0, pointerEvents: isHistoryOpen ? "auto" : "none", transition: "opacity 0.3s ease", zIndex: 10, }} /> { handleNewConversation(); setIsHistoryOpen(false); }} onSelectSession={(id) => { handleSelectSession(id); setIsHistoryOpen(false); }} onRenameSession={handleRenameSession} onDeleteSession={handleDeleteSession} /> ); };