Enhanced Chat Embed History View (#4281)

* Enhanced Chat Embed History View

* Robust Markdown Rendering
Improved "Thinking" View

* feat: Improve markdown rendering in chat embed history

* update ui for show/hide thoughts in embed chat history

* refactor -always show thoughts if available

* patch unused imports and use safeJsonParse

* update fallback for loading state to always reset

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
This commit is contained in:
Máté Kristóf 2025-09-18 06:14:18 +02:00 committed by GitHub
parent 226802d35a
commit 01a3cc92d0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 110 additions and 15 deletions

View File

@ -1,9 +1,11 @@
import truncate from "truncate"; import truncate from "truncate";
import { X, Trash, LinkSimple } from "@phosphor-icons/react"; import { X } from "@phosphor-icons/react";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import paths from "@/utils/paths"; import paths from "@/utils/paths";
import Embed from "@/models/embed"; import Embed from "@/models/embed";
import MarkdownRenderer from "../MarkdownRenderer";
import { safeJsonParse } from "@/utils/request";
export default function ChatRow({ chat, onDelete }) { export default function ChatRow({ chat, onDelete }) {
const { const {
@ -83,7 +85,11 @@ export default function ChatRow({ chat, onDelete }) {
</ModalWrapper> </ModalWrapper>
<ModalWrapper isOpen={isResponseOpen}> <ModalWrapper isOpen={isResponseOpen}>
<TextPreview <TextPreview
text={JSON.parse(chat.response)?.text} text={
<MarkdownRenderer
content={safeJsonParse(chat.response, {})?.text}
/>
}
closeModal={closeResponseModal} closeModal={closeResponseModal}
/> />
</ModalWrapper> </ModalWrapper>
@ -118,9 +124,9 @@ const TextPreview = ({ text, closeModal }) => {
</button> </button>
</div> </div>
<div className="w-full p-6"> <div className="w-full p-6">
<pre className="w-full h-[200px] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm"> <div className="w-full h-[60vh] py-2 px-4 whitespace-pre-line overflow-auto rounded-lg bg-zinc-900 light:bg-theme-bg-secondary border border-gray-500 text-white text-sm">
{text} {text}
</pre> </div>
</div> </div>
</div> </div>
</div> </div>
@ -132,11 +138,7 @@ const ConnectionDetails = ({
verbose = false, verbose = false,
connection_information, connection_information,
}) => { }) => {
let details = {}; const details = safeJsonParse(connection_information, {});
try {
details = JSON.parse(connection_information);
} catch {}
if (Object.keys(details).length === 0) return null; if (Object.keys(details).length === 0) return null;
if (verbose) { if (verbose) {

View File

@ -0,0 +1,87 @@
import { useState } from "react";
import MarkdownIt from "markdown-it";
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>
);
}

View File

@ -55,6 +55,7 @@ export default function EmbedChatsView() {
const query = useQuery(); const query = useQuery();
const [offset, setOffset] = useState(Number(query.get("offset") || 0)); const [offset, setOffset] = useState(Number(query.get("offset") || 0));
const [canNext, setCanNext] = useState(false); const [canNext, setCanNext] = useState(false);
const [showThinking, setShowThinking] = useState(true);
const handleDumpChats = async (exportType) => { const handleDumpChats = async (exportType) => {
const chats = await System.exportChats(exportType, "embed"); const chats = await System.exportChats(exportType, "embed");
@ -92,10 +93,15 @@ export default function EmbedChatsView() {
useEffect(() => { useEffect(() => {
async function fetchChats() { async function fetchChats() {
const { chats: _chats, hasPages = false } = await Embed.chats(offset); setLoading(true);
await Embed.chats(offset)
.then(({ chats: _chats, hasPages = false }) => {
setChats(_chats); setChats(_chats);
setCanNext(hasPages); setCanNext(hasPages);
})
.finally(() => {
setLoading(false); setLoading(false);
});
} }
fetchChats(); fetchChats();
}, [offset]); }, [offset]);
@ -211,7 +217,7 @@ export default function EmbedChatsView() {
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover" : "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
}`} }`}
> >
{t("embed-chats.previous")} {t("common.previous")}
</button> </button>
<button <button
onClick={handleNext} onClick={handleNext}
@ -222,7 +228,7 @@ export default function EmbedChatsView() {
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover" : "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
}`} }`}
> >
{t("embed-chats.next")} {t("common.next")}
</button> </button>
</div> </div>
)} )}