Apply renderer from chat widget history to workspace chats (#4456)

Apply renderer from chat widget history to workspace chats #4455
resolves #4455
Fixes bug with codeblocks since hljs import was missing
This commit is contained in:
Timothy Carambat 2025-09-30 14:48:41 -07:00 committed by GitHub
parent d800f8a073
commit be7e2b6bc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 102 additions and 24 deletions

View File

@ -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)}
</td>
<td className="px-6">{chat.createdAt}</td>
<td className="px-6 flex items-center gap-x-6 h-full mt-1">

View File

@ -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";

View File

@ -46,16 +46,15 @@ const exportOptions = {
};
export default function EmbedChatsView() {
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef();
const openMenuButton = useRef();
const { t } = useTranslation();
const menuRef = useRef();
const query = useQuery();
const openMenuButton = useRef();
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");

View File

@ -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)}
</td>
<td className="px-6">{chat.createdAt}</td>
<td className="px-6 flex items-center gap-x-6 h-full mt-1">
@ -80,7 +66,11 @@ export default function ChatRow({ chat, onDelete }) {
</ModalWrapper>
<ModalWrapper isOpen={isResponseOpen}>
<TextPreview
text={parseText(chat.response)}
text={
<MarkdownRenderer
content={safeJsonParse(chat.response, {})?.text}
/>
}
closeModal={closeResponseModal}
/>
</ModalWrapper>

View File

@ -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 (
<div className="mb-3">
<div
onClick={() => setIsExpanded(!isExpanded)}
className="cursor-pointer flex items-center gap-x-2 text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-2"
>
<CaretDown
size={14}
weight="bold"
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
/>
<span className="text-xs font-medium">View thoughts</span>
</div>
{isExpanded && (
<div className="bg-theme-bg-chat-input rounded-md p-3 border-l-2 border-theme-text-secondary/30">
<div className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap">
{cleanThought}
</div>
</div>
)}
</div>
);
};
function parseContent(content) {
const parts = [];
let lastIndex = 0;
content.replace(/<think>([^]*?)<\/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 (
<div className="whitespace-normal">
{parts.map((part, index) => {
const html = md.render(part.text);
if (part.type === "think")
return <ThoughtBubble key={index} thought={part.text} />;
return (
<div
key={index}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
/>
);
})}
</div>
);
}