Files
TJWaterFrontend_Refine/src/components/chat/GlobalChatbox.tsx
T
jiang 8058b7b859
Build Push and Deploy / docker-image (push) Successful in 1m3s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped
增加模型选择功能,支持不同 Agent 模型
2026-05-13 18:12:22 +08:00

315 lines
9.4 KiB
TypeScript

"use client";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Box, Drawer, alpha, useTheme } from "@mui/material";
import type { AgentModel } from "@/lib/chatStream";
import { AgentComposer } 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<Props> = ({ open, onClose }) => {
const [input, setInput] = useState("");
const [width, setWidth] = useState(520);
const [isResizing, setIsResizing] = useState(false);
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<AgentModel>(
"deepseek/deepseek-v4-pro",
);
const bottomRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const theme = useTheme();
const {
speechState,
speakingMessageId,
speak: handleSpeak,
pause: handlePauseSpeech,
resume: handleResumeSpeech,
stop: handleStopSpeech,
isSupported: isTtsSupported,
} = useSpeechSynthesis();
const handleSpeechResult = useCallback((text: string) => {
setInput((prev) => prev + text);
}, []);
const {
isListening,
start: startListening,
stop: stopListening,
isSupported: isSttSupported,
} = useSpeechRecognition(handleSpeechResult);
const handleToolCall = useAgentToolActions();
const {
messages,
chatSessions,
activeStorageSessionId,
branchGroups,
branchTransition,
isHydrating,
isStreaming,
sendPrompt,
regenerate,
editAndResubmit,
cycleBranch,
abort,
createSession,
removeSession,
switchSession,
} = useAgentChatSession({
onToolCall: handleToolCall,
onBeforeSend: stopListening,
getModel: () => selectedModel,
});
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages, isStreaming]);
useEffect(() => {
if (!open) return;
const timer = window.setTimeout(() => {
inputRef.current?.focus();
bottomRef.current?.scrollIntoView({ behavior: "auto" });
}, 0);
return () => window.clearTimeout(timer);
}, [open]);
const handleSend = useCallback(() => {
const prompt = input.trim();
if (!prompt || isStreaming) return;
setInput("");
void sendPrompt(prompt);
}, [input, isStreaming, sendPrompt]);
const handlePresetPromptSelect = useCallback((prompt: string) => {
setInput(prompt);
window.setTimeout(() => {
inputRef.current?.focus();
}, 0);
}, []);
const handleNewConversation = useCallback(() => {
handleStopSpeech();
stopListening();
void createSession();
setInput("");
window.setTimeout(() => {
inputRef.current?.focus();
}, 0);
}, [createSession, handleStopSpeech, stopListening]);
const handleHistoryToggle = useCallback(() => {
setIsHistoryOpen((prev) => !prev);
}, []);
const handleSelectSession = useCallback(
(storageSessionId: string) => {
setInput("");
void switchSession(storageSessionId);
},
[switchSession],
);
const handleDeleteSession = useCallback(
(storageSessionId: string) => {
void removeSession(storageSessionId);
},
[removeSession],
);
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 < 1240) {
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 (
<Drawer
anchor="right"
variant="temporary"
open={open}
onClose={onClose}
hideBackdrop
disableScrollLock
disableEnforceFocus
sx={{ zIndex: (muiTheme) => muiTheme.zIndex.modal + 100 }}
PaperProps={{
sx: {
width: { xs: "100%", sm: width },
background: "transparent",
boxShadow: "none",
overflow: open ? "visible" : "hidden",
zIndex: (muiTheme) => muiTheme.zIndex.modal + 100,
transition: isResizing ? "none" : undefined,
},
}}
>
<Box
sx={{
height: "100%",
display: "flex",
flexDirection: "column",
bgcolor: alpha("#fff", 0.76),
backdropFilter: "blur(30px)",
position: "relative",
}}
>
<Box
onMouseDown={handleMouseDown}
sx={{
position: "absolute",
left: 0,
top: 0,
bottom: 0,
width: "6px",
cursor: "col-resize",
zIndex: 200,
"&:hover": {
bgcolor: alpha(theme.palette.primary.main, 0.2),
},
"&::after": {
content: '""',
position: "absolute",
left: "50%",
top: "50%",
transform: "translate(-50%, -50%)",
width: "2px",
height: "40px",
bgcolor: alpha(theme.palette.divider, 0.4),
borderRadius: "1px",
},
}}
/>
<Blob color={alpha(theme.palette.primary.main, 0.28)} size={300} top="-10%" left="-20%" delay={0} />
<Blob color={alpha(theme.palette.secondary.main, 0.24)} size={250} top="40%" left="60%" delay={2} />
<Blob color={alpha(theme.palette.success.light, 0.18)} size={200} top="80%" left="-10%" delay={4} />
<AgentHeader
isStreaming={isStreaming}
isHistoryOpen={isHistoryOpen}
onHistoryToggle={handleHistoryToggle}
onNewConversation={handleNewConversation}
onClose={onClose}
/>
<Box sx={{ flex: 1, display: "flex", minHeight: 0, position: "relative", overflow: "hidden" }}>
<Box
onClick={() => 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,
}}
/>
<Box
sx={{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
width: 268,
zIndex: 20,
transform: isHistoryOpen ? "translateX(0)" : "translateX(-100%)",
transition: "transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)",
boxShadow: isHistoryOpen ? `4px 0 24px ${alpha("#000", 0.08)}` : "none",
}}
>
<AgentHistoryPanel
sessions={chatSessions}
activeSessionId={activeStorageSessionId}
isHydrating={isHydrating}
onNewSession={() => {
handleNewConversation();
setIsHistoryOpen(false);
}}
onSelectSession={(id) => {
handleSelectSession(id);
setIsHistoryOpen(false);
}}
onDeleteSession={handleDeleteSession}
/>
</Box>
<Box sx={{ flex: 1, display: "flex", minWidth: 0, flexDirection: "column" }}>
<AgentWorkspace
messages={messages}
branchGroups={branchGroups}
branchTransition={branchTransition}
isStreaming={isStreaming}
bottomRef={bottomRef}
speakingMessageId={speakingMessageId}
speechState={speechState}
onSpeak={handleSpeak}
onPauseSpeech={handlePauseSpeech}
onResumeSpeech={handleResumeSpeech}
onStopSpeech={handleStopSpeech}
isTtsSupported={isTtsSupported}
onRegenerate={regenerate}
onEditResubmit={editAndResubmit}
onCycleBranch={cycleBranch}
/>
<AgentComposer
input={input}
inputRef={inputRef}
isHydrating={isHydrating}
isStreaming={isStreaming}
isListening={isListening}
isSttSupported={isSttSupported}
presets={PRESET_PROMPTS}
onInputChange={setInput}
onSend={handleSend}
onAbort={abort}
onStartListening={startListening}
onStopListening={stopListening}
onPresetSelect={handlePresetPromptSelect}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
/>
</Box>
</Box>
</Box>
</Drawer>
);
};