From 6554e8d24096798cac245b13f165fac2cb042170 Mon Sep 17 00:00:00 2001 From: timothycarambat Date: Tue, 30 Sep 2025 14:47:35 -0700 Subject: [PATCH] Apply renderer from chat widget history to workspace chats #4455 resolves #4455 Fixes bug with codeblocks since hljs import was missing --- .../EmbedChats/ChatRow/index.jsx | 2 +- .../EmbedChats/MarkdownRenderer.jsx | 1 + .../ChatEmbedWidgets/EmbedChats/index.jsx | 7 +- .../GeneralSettings/Chats/ChatRow/index.jsx | 26 ++---- .../Chats/MarkdownRenderer.jsx | 88 +++++++++++++++++++ 5 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx diff --git a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx index bf50e61f57d..714e041df65 100644 --- a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx @@ -66,7 +66,7 @@ export default function ChatRow({ chat, onDelete }) { onClick={openResponseModal} className="px-6 cursor-pointer hover:shadow-lg" > - {truncate(JSON.parse(chat.response)?.text, 40)} + {truncate(safeJsonParse(chat.response, {})?.text, 40)} {chat.createdAt} diff --git a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/MarkdownRenderer.jsx b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/MarkdownRenderer.jsx index 11b4ca51c1e..cfaffc43535 100644 --- a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/MarkdownRenderer.jsx +++ b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/MarkdownRenderer.jsx @@ -1,5 +1,6 @@ import { useState } from "react"; import MarkdownIt from "markdown-it"; +import hljs from "highlight.js"; import { CaretDown } from "@phosphor-icons/react"; import "highlight.js/styles/github-dark.css"; import DOMPurify from "@/utils/chat/purify"; diff --git a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx index cd242422e71..9c9e7e1be68 100644 --- a/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx +++ b/frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx @@ -46,16 +46,15 @@ const exportOptions = { }; export default function EmbedChatsView() { - const [showMenu, setShowMenu] = useState(false); + const { t } = useTranslation(); const menuRef = useRef(); + const query = useQuery(); const openMenuButton = useRef(); - const { t } = useTranslation(); + const [showMenu, setShowMenu] = useState(false); const [loading, setLoading] = useState(true); const [chats, setChats] = useState([]); - const query = useQuery(); const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [canNext, setCanNext] = useState(false); - const [showThinking, setShowThinking] = useState(true); const handleDumpChats = async (exportType) => { const chats = await System.exportChats(exportType, "embed"); diff --git a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx index 52cb02dcd7a..91054cd000b 100644 --- a/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx +++ b/frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx @@ -3,22 +3,8 @@ import { X, Trash } from "@phosphor-icons/react"; import System from "@/models/system"; import ModalWrapper from "@/components/ModalWrapper"; import { useModal } from "@/hooks/useModal"; - -// Some LLMs may return a "valid" response that truncation fails to truncate because -// it stored an Object as opposed to a string for the `text` field. -function parseText(jsonResponse = "") { - try { - const json = JSON.parse(jsonResponse); - if (!json.hasOwnProperty("text")) - throw new Error('JSON response has no property "text".'); - return typeof json.text !== "string" - ? JSON.stringify(json.text) - : json.text; - } catch (e) { - console.error(e); - return "--failed to parse--"; - } -} +import MarkdownRenderer from "../MarkdownRenderer"; +import { safeJsonParse } from "@/utils/request"; export default function ChatRow({ chat, onDelete }) { const { @@ -63,7 +49,7 @@ export default function ChatRow({ chat, onDelete }) { onClick={openResponseModal} className="px-6 cursor-pointer transform transition-transform duration-200 hover:scale-105 hover:shadow-lg" > - {truncate(parseText(chat.response), 40)} + {truncate(safeJsonParse(chat.response, {})?.text, 40)} {chat.createdAt} @@ -80,7 +66,11 @@ export default function ChatRow({ chat, onDelete }) { + } closeModal={closeResponseModal} /> diff --git a/frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx b/frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx new file mode 100644 index 00000000000..cfaffc43535 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx @@ -0,0 +1,88 @@ +import { useState } from "react"; +import MarkdownIt from "markdown-it"; +import hljs from "highlight.js"; +import { CaretDown } from "@phosphor-icons/react"; +import "highlight.js/styles/github-dark.css"; +import DOMPurify from "@/utils/chat/purify"; + +const md = new MarkdownIt({ + html: true, + breaks: true, + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(str, { language: lang }).value; + } catch (__) {} + } + return ""; // use external default escaping + }, +}); + +const ThoughtBubble = ({ thought }) => { + const [isExpanded, setIsExpanded] = useState(false); + + if (!thought) return null; + + const cleanThought = thought.replace(/<\/?think>/g, "").trim(); + if (!cleanThought) return null; + + return ( +
+
setIsExpanded(!isExpanded)} + className="cursor-pointer flex items-center gap-x-2 text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-2" + > + + View thoughts +
+ {isExpanded && ( +
+
+ {cleanThought} +
+
+ )} +
+ ); +}; + +function parseContent(content) { + const parts = []; + let lastIndex = 0; + content.replace(/([^]*?)<\/think>/g, (match, thinkContent, offset) => { + if (offset > lastIndex) { + parts.push({ type: "normal", text: content.slice(lastIndex, offset) }); + } + parts.push({ type: "think", text: thinkContent }); + lastIndex = offset + match.length; + }); + if (lastIndex < content.length) { + parts.push({ type: "normal", text: content.slice(lastIndex) }); + } + return parts; +} + +export default function MarkdownRenderer({ content }) { + if (!content) return null; + + const parts = parseContent(content); + return ( +
+ {parts.map((part, index) => { + const html = md.render(part.text); + if (part.type === "think") + return ; + return ( +
+ ); + })} +
+ ); +}