diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index eac479f..4f0f238 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -36,6 +36,8 @@ import VolumeUpRounded from "@mui/icons-material/VolumeUpRounded"; import PauseRounded from "@mui/icons-material/PauseRounded"; import PlayArrowRounded from "@mui/icons-material/PlayArrowRounded"; import MicRounded from "@mui/icons-material/MicRounded"; +import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; +import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; // Logic import { streamCopilotChat } from "@/lib/chatStream"; @@ -113,6 +115,12 @@ type PersistedChatState = { conversationId?: string; }; +const PRESET_PROMPTS = [ + "帮我分析当前管网压力异常点,并按风险等级排序。", + "基于当前状态,给出今天的巡检优先级和建议路线。", + "帮我生成一份今日运行简报,包含问题、原因和建议。", +]; + const getInitialChatState = (): PersistedChatState => { if (typeof window === "undefined") { return { messages: [], conversationId: undefined }; @@ -537,6 +545,7 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { initialChatStateRef.current.conversationId ); const [headerMenuAnchorEl, setHeaderMenuAnchorEl] = useState(null); + const [isPresetPanelOpen, setIsPresetPanelOpen] = useState(false); const abortRef = useRef(null); const bottomRef = useRef(null); const inputRef = useRef(null); @@ -590,79 +599,88 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { } }, [messages, conversationId]); + const sendPrompt = useCallback( + async (rawPrompt: string) => { + const prompt = rawPrompt.trim(); + if (!prompt || isStreaming) return; + stopListening(); + + const userId = createId(); + const assistantId = createId(); + setInput(""); + setIsStreaming(true); + + setMessages((prev) => [ + ...prev, + { id: userId, role: "user", content: prompt }, + { id: assistantId, role: "assistant", content: "" }, + ]); + + const controller = new AbortController(); + abortRef.current = controller; + + try { + await streamCopilotChat({ + message: prompt, + conversationId, + signal: controller.signal, + onEvent: (event) => { + if (event.type === "token") { + if (!conversationId && event.conversationId) setConversationId(event.conversationId); + const normalizedToken = normalizeThoughtTagToken(event.content); + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { ...m, content: m.content + normalizedToken, isError: false } + : m + ) + ); + } else if (event.type === "done") { + if (!conversationId && event.conversationId) setConversationId(event.conversationId); + setIsStreaming(false); + } else if (event.type === "error") { + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + content: m.content || `⚠️ **错误:** ${event.message}`, + isError: true, + } + : m + ) + ); + setIsStreaming(false); + } + }, + }); + } catch (error) { + if (abortRef.current?.signal.aborted) { + setMessages((prev) => + prev.filter((m) => !(m.id === assistantId && m.role === "assistant" && m.content.trim().length === 0)) + ); + return; + } + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { ...m, content: `⚠️ **错误:** ${String(error)}`, isError: true } + : m + ) + ); + setIsStreaming(false); + } finally { + abortRef.current = null; + setIsStreaming(false); + } + }, + [conversationId, isStreaming, stopListening], + ); + const handleSend = async () => { const prompt = input.trim(); if (!prompt || isStreaming) return; - stopListening(); - - const userId = createId(); - const assistantId = createId(); - setInput(""); - setIsStreaming(true); - - setMessages((prev) => [ - ...prev, - { id: userId, role: "user", content: prompt }, - { id: assistantId, role: "assistant", content: "" }, - ]); - - const controller = new AbortController(); - abortRef.current = controller; - - try { - await streamCopilotChat({ - message: prompt, - conversationId, - signal: controller.signal, - onEvent: (event) => { - if (event.type === "token") { - if (!conversationId && event.conversationId) setConversationId(event.conversationId); - const normalizedToken = normalizeThoughtTagToken(event.content); - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { ...m, content: m.content + normalizedToken, isError: false } - : m - ) - ); - } else if (event.type === "done") { - if (!conversationId && event.conversationId) setConversationId(event.conversationId); - setIsStreaming(false); - } else if (event.type === "error") { - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { - ...m, - content: m.content || `⚠️ **错误:** ${event.message}`, - isError: true, - } - : m - ) - ); - setIsStreaming(false); - } - }, - }); - } catch (error) { - if (abortRef.current?.signal.aborted) { - setMessages((prev) => - prev.filter((m) => !(m.id === assistantId && m.role === "assistant" && m.content.trim().length === 0)) - ); - return; - } - setMessages((prev) => - prev.map((m) => - m.id === assistantId - ? { ...m, content: `⚠️ **错误:** ${String(error)}`, isError: true } - : m - ) - ); - setIsStreaming(false); - } finally { - abortRef.current = null; - setIsStreaming(false); - } + await sendPrompt(prompt); }; const handleAbort = () => { @@ -670,6 +688,14 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { setIsStreaming(false); }; + const handlePresetPromptSelect = useCallback((prompt: string) => { + setInput(prompt); + setIsPresetPanelOpen(false); + window.setTimeout(() => { + inputRef.current?.focus(); + }, 0); + }, []); + const handleHeaderMenuOpen = useCallback( (event: React.MouseEvent) => { setHeaderMenuAnchorEl(event.currentTarget); @@ -913,7 +939,18 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { {/* Messages - Bouncy List */} - + {messages.length === 0 && ( = ({ open, onClose }) => { {/* Input Area - Floating Capsule */} + + + + {isPresetPanelOpen && ( + + + + {PRESET_PROMPTS.map((prompt, index) => ( + handlePresetPromptSelect(prompt)} + sx={{ + textAlign: "left", + width: "100%", + px: 1.1, + py: 0.9, + borderRadius: 2, + border: `1px solid ${alpha(theme.palette.divider, 0.24)}`, + bgcolor: alpha("#fff", 0.72), + color: "text.secondary", + fontSize: "0.84rem", + lineHeight: 1.45, + cursor: "pointer", + transition: "all 0.18s ease", + "&:hover": { + borderColor: alpha(theme.palette.primary.main, 0.45), + color: "text.primary", + transform: "translateY(-1px)", + boxShadow: `0 8px 24px -16px ${alpha(theme.palette.primary.main, 0.6)}`, + }, + }} + > + {prompt} + + ))} + + + + )} + + + + + + + + + + 常用功能 + + setIsPresetPanelOpen((prev) => !prev)} + aria-label={isPresetPanelOpen ? "收起常用功能" : "展开常用功能"} + sx={{ color: "text.secondary" }} + > + {isPresetPanelOpen ? : } + + + + + + +