From 8713e5a46823fe7922d7e77583fa030cd10db7ec Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 26 Mar 2026 11:55:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9=E6=A1=86?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=8C=81=E4=B9=85=E5=8C=96=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Markdown=20=E6=A0=B7=E5=BC=8F=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=9B=E8=B0=83=E6=95=B4=E5=9C=B0=E5=9B=BE=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=B1=82=E7=BA=A7=EF=BC=8C=E9=81=BF=E5=85=8D=E5=92=8C?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E6=A1=86=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/chat/GlobalChatbox.tsx | 235 ++++++++---------- .../chat/GlobalChatboxMarkdown.module.css | 90 +++++++ .../olmap/core/Controls/BaseLayers.tsx | 2 +- .../olmap/core/Controls/LayerControl.tsx | 2 +- .../olmap/core/Controls/ScaleLine.tsx | 2 +- src/components/olmap/core/Controls/Zoom.tsx | 2 +- 6 files changed, 202 insertions(+), 131 deletions(-) create mode 100644 src/components/chat/GlobalChatboxMarkdown.module.css diff --git a/src/components/chat/GlobalChatbox.tsx b/src/components/chat/GlobalChatbox.tsx index 35aebc0..4261894 100644 --- a/src/components/chat/GlobalChatbox.tsx +++ b/src/components/chat/GlobalChatbox.tsx @@ -3,6 +3,7 @@ import React, { useMemo, useRef, useState, useEffect } from "react"; import ReactMarkdown from "react-markdown"; import { motion, AnimatePresence } from "framer-motion"; +import markdownStyles from "./GlobalChatboxMarkdown.module.css"; // MUI import { @@ -52,6 +53,27 @@ type PersistedChatState = { conversationId?: string; }; +const getInitialChatState = (): PersistedChatState => { + if (typeof window === "undefined") { + return { messages: [], conversationId: undefined }; + } + try { + const storedRaw = window.localStorage.getItem(CHAT_STORAGE_KEY); + if (!storedRaw) return { messages: [], conversationId: undefined }; + const parsed = JSON.parse(storedRaw) as PersistedChatState; + if (!Array.isArray(parsed.messages)) { + console.error("[GlobalChatbox] Invalid persisted messages format."); + window.localStorage.removeItem(CHAT_STORAGE_KEY); + return { messages: [], conversationId: undefined }; + } + return { messages: parsed.messages, conversationId: parsed.conversationId }; + } catch (error) { + console.error("[GlobalChatbox] Failed to read persisted chat state:", error); + window.localStorage.removeItem(CHAT_STORAGE_KEY); + return { messages: [], conversationId: undefined }; + } +}; + // --- Components --- const TypingIndicator = () => { @@ -115,14 +137,20 @@ const Blob = ({ color, size, top, left, delay }: { color: string; size: number; ); export const GlobalChatbox: React.FC = ({ open, onClose }) => { - const [messages, setMessages] = useState([]); + const initialChatStateRef = useRef(null); + if (initialChatStateRef.current === null) { + initialChatStateRef.current = getInitialChatState(); + } + + const [messages, setMessages] = useState(initialChatStateRef.current.messages); const [input, setInput] = useState(""); const [isStreaming, setIsStreaming] = useState(false); - const [conversationId, setConversationId] = useState(undefined); + const [conversationId, setConversationId] = useState( + initialChatStateRef.current.conversationId + ); const abortRef = useRef(null); const bottomRef = useRef(null); const inputRef = useRef(null); - const hasHydratedRef = useRef(false); const theme = useTheme(); const canSend = useMemo(() => input.trim().length > 0 && !isStreaming, [input, isStreaming]); @@ -136,36 +164,12 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { if (!open) return; const timer = window.setTimeout(() => { inputRef.current?.focus(); + bottomRef.current?.scrollIntoView({ behavior: "auto" }); }, 0); return () => window.clearTimeout(timer); }, [open]); useEffect(() => { - try { - const storedRaw = window.localStorage.getItem(CHAT_STORAGE_KEY); - if (!storedRaw) { - hasHydratedRef.current = true; - return; - } - const parsed = JSON.parse(storedRaw) as PersistedChatState; - if (!Array.isArray(parsed.messages)) { - console.error("[GlobalChatbox] Invalid persisted messages format."); - window.localStorage.removeItem(CHAT_STORAGE_KEY); - hasHydratedRef.current = true; - return; - } - setMessages(parsed.messages); - setConversationId(parsed.conversationId); - } catch (error) { - console.error("[GlobalChatbox] Failed to read persisted chat state:", error); - window.localStorage.removeItem(CHAT_STORAGE_KEY); - } finally { - hasHydratedRef.current = true; - } - }, []); - - useEffect(() => { - if (!hasHydratedRef.current) return; const state: PersistedChatState = { messages, conversationId }; try { window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state)); @@ -227,7 +231,12 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { }, }); } catch (error) { - if (abortRef.current?.signal.aborted) return; + 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 @@ -250,8 +259,10 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { return ( muiTheme.zIndex.modal + 100 }} PaperProps={{ sx: { @@ -262,11 +273,6 @@ export const GlobalChatbox: React.FC = ({ open, onClose }) => { zIndex: (muiTheme) => muiTheme.zIndex.modal + 100, }, }} - ModalProps={{ - BackdropProps: { - sx: { backdropFilter: "blur(6px)", bgcolor: alpha(theme.palette.background.default, 0.3) }, - }, - }} > = ({ open, onClose }) => { : isErrorMessage ? `0 4px 16px -4px ${alpha(theme.palette.error.main, 0.2)}` : `0 4px 16px -4px ${alpha("#000", 0.05)}`, - - // Markdown Styles - "& p": { - m: 0, - lineHeight: 1.75, - whiteSpace: "pre-wrap", - color: isUser ? alpha("#fff", 0.96) : isErrorMessage ? theme.palette.error.dark : theme.palette.text.primary, - }, - "& h1, & h2, & h3, & h4, & h5, & h6": { - mt: 0.6, - mb: 0.6, - lineHeight: 1.35, - fontWeight: 700, - color: isUser ? "#fff" : isErrorMessage ? theme.palette.error.dark : theme.palette.text.primary, - }, - "& h1": { fontSize: "1.2rem" }, - "& h2": { fontSize: "1.12rem" }, - "& h3": { fontSize: "1.04rem" }, - "& a": { - color: isUser ? "#E3F2FD" : isErrorMessage ? theme.palette.error.main : theme.palette.primary.dark, - textDecoration: "underline", - wordBreak: "break-all", - textUnderlineOffset: "2px", - "&:hover": { - color: isUser ? "#fff" : isErrorMessage ? theme.palette.error.dark : theme.palette.primary.main, - }, - }, - "& code": { - fontFamily: "monospace", - bgcolor: isUser - ? "rgba(255,255,255,0.2)" - : isErrorMessage - ? alpha(theme.palette.error.main, 0.08) - : alpha(theme.palette.grey[100], 0.95), - color: isUser ? "#fff" : isErrorMessage ? theme.palette.error.dark : alpha(theme.palette.text.primary, 0.95), - px: 0.8, - py: 0.2, - borderRadius: 1, - fontSize: "0.85em", - border: isUser - ? "none" - : isErrorMessage - ? `1px solid ${alpha(theme.palette.error.main, 0.25)}` - : `1px solid ${alpha(theme.palette.divider, 0.35)}`, - }, - "& pre": { - bgcolor: isUser - ? "rgba(11, 18, 32, 0.56)" - : isErrorMessage - ? alpha(theme.palette.error.main, 0.08) - : "#0F172A", - color: isUser - ? "#F8FAFC" - : isErrorMessage - ? theme.palette.error.dark - : "#E2E8F0", - p: 2, - borderRadius: 3, - overflowX: "auto", - my: 1.5, - fontSize: "0.88em", - border: isErrorMessage - ? `1px solid ${alpha(theme.palette.error.main, 0.3)}` - : `1px solid ${isUser ? alpha("#fff", 0.12) : alpha("#94A3B8", 0.35)}`, - }, - "& pre code": { - bgcolor: "transparent", - border: "none", - px: 0, - py: 0, - color: "inherit", - }, - "& ul, & ol": { pl: 2.5, my: 1 }, - "& li": { - my: 0.35, - lineHeight: 1.65, - }, - "& blockquote": { - m: 0, - my: 1, - pl: 1.5, - borderLeft: `3px solid ${isErrorMessage ? alpha(theme.palette.error.main, 0.5) : alpha(theme.palette.divider, 0.5)}`, - color: isUser ? alpha("#fff", 0.9) : isErrorMessage ? theme.palette.error.dark : "text.secondary", - bgcolor: isUser - ? alpha("#fff", 0.08) - : isErrorMessage - ? alpha(theme.palette.error.main, 0.06) - : alpha(theme.palette.grey[100], 0.7), - borderRadius: 1, - py: 0.5, - pr: 1, - }, + "--chat-md-text": isUser + ? alpha("#fff", 0.96) + : isErrorMessage + ? theme.palette.error.dark + : "#1f2937", + "--chat-md-heading": isUser + ? "#fff" + : isErrorMessage + ? theme.palette.error.dark + : "#111827", + "--chat-md-link": isUser + ? "#E3F2FD" + : isErrorMessage + ? theme.palette.error.main + : "#7C3AED", + "--chat-md-link-hover": isUser + ? "#fff" + : isErrorMessage + ? theme.palette.error.dark + : "#6D28D9", + "--chat-md-inline-code-bg": isUser + ? "rgba(255,255,255,0.2)" + : isErrorMessage + ? alpha(theme.palette.error.main, 0.08) + : "#EEF2FF", + "--chat-md-inline-code-border": isUser + ? alpha("#fff", 0.16) + : isErrorMessage + ? alpha(theme.palette.error.main, 0.25) + : "#CBD5E1", + "--chat-md-inline-code-text": isUser + ? "#fff" + : isErrorMessage + ? theme.palette.error.dark + : "#334155", + "--chat-md-pre-bg": isUser + ? "rgba(11, 18, 32, 0.56)" + : isErrorMessage + ? alpha(theme.palette.error.main, 0.08) + : "#111827", + "--chat-md-pre-border": isUser + ? alpha("#fff", 0.12) + : isErrorMessage + ? alpha(theme.palette.error.main, 0.3) + : "#64748B", + "--chat-md-pre-text": isUser + ? "#F8FAFC" + : isErrorMessage + ? theme.palette.error.dark + : "#E5E7EB", + "--chat-md-quote-border": isErrorMessage + ? alpha(theme.palette.error.main, 0.5) + : isUser + ? alpha("#fff", 0.5) + : "#7C3AED", + "--chat-md-quote-bg": isUser + ? alpha("#fff", 0.08) + : isErrorMessage + ? alpha(theme.palette.error.main, 0.06) + : "#F5F3FF", + "--chat-md-quote-text": isUser + ? alpha("#fff", 0.9) + : isErrorMessage + ? theme.palette.error.dark + : "#475569", }} > - {message.content || "..."} +
+ {message.content || "..."} +
); diff --git a/src/components/chat/GlobalChatboxMarkdown.module.css b/src/components/chat/GlobalChatboxMarkdown.module.css new file mode 100644 index 0000000..f76ef6f --- /dev/null +++ b/src/components/chat/GlobalChatboxMarkdown.module.css @@ -0,0 +1,90 @@ +.markdown { + color: var(--chat-md-text); + font-size: 0.95rem; + line-height: 1.75; + word-break: break-word; +} + +.markdown p { + margin: 0; + white-space: pre-wrap; +} + +.markdown p + p { + margin-top: 0.75rem; +} + +.markdown h1, +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + margin: 0.6rem 0; + line-height: 1.35; + font-weight: 700; + color: var(--chat-md-heading); +} + +.markdown h1 { font-size: 1.2rem; } +.markdown h2 { font-size: 1.12rem; } +.markdown h3 { font-size: 1.04rem; } + +.markdown a { + color: var(--chat-md-link); + text-decoration: underline; + text-underline-offset: 2px; + word-break: break-all; +} + +.markdown a:hover { + color: var(--chat-md-link-hover); +} + +.markdown :not(pre) > code { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + background: var(--chat-md-inline-code-bg); + border: 1px solid var(--chat-md-inline-code-border); + color: var(--chat-md-inline-code-text); + border-radius: 6px; + padding: 0.12rem 0.4rem; + font-size: 0.85em; +} + +.markdown pre { + background: var(--chat-md-pre-bg); + border: 1px solid var(--chat-md-pre-border); + color: var(--chat-md-pre-text); + border-radius: 10px; + padding: 0.75rem 0.9rem; + overflow-x: auto; + margin: 0.9rem 0; + font-size: 0.88em; +} + +.markdown pre code { + border: none; + background: transparent; + color: inherit; + padding: 0; +} + +.markdown ul, +.markdown ol { + padding-left: 1.4rem; + margin: 0.5rem 0; +} + +.markdown li { + margin: 0.3rem 0; + line-height: 1.65; +} + +.markdown blockquote { + margin: 0.8rem 0; + padding: 0.45rem 0.75rem; + border-left: 3px solid var(--chat-md-quote-border); + background: var(--chat-md-quote-bg); + color: var(--chat-md-quote-text); + border-radius: 6px; +} diff --git a/src/components/olmap/core/Controls/BaseLayers.tsx b/src/components/olmap/core/Controls/BaseLayers.tsx index 244323f..7a6c09a 100644 --- a/src/components/olmap/core/Controls/BaseLayers.tsx +++ b/src/components/olmap/core/Controls/BaseLayers.tsx @@ -180,7 +180,7 @@ const BaseLayers: React.FC = () => { }; return ( -
+
{ } return ( -
+
{layerItems.map((item) => ( { } `} -
+
{ }; return ( -
+