Skip to content

Commit 01a3cc9

Browse files
MateKristoftimothycarambatshatfield4
authored
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 <[email protected]> Co-authored-by: shatfield4 <[email protected]>
1 parent 226802d commit 01a3cc9

File tree

3 files changed

+110
-15
lines changed

3 files changed

+110
-15
lines changed

frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/ChatRow/index.jsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import truncate from "truncate";
2-
import { X, Trash, LinkSimple } from "@phosphor-icons/react";
2+
import { X } from "@phosphor-icons/react";
33
import ModalWrapper from "@/components/ModalWrapper";
44
import { useModal } from "@/hooks/useModal";
55
import paths from "@/utils/paths";
66
import Embed from "@/models/embed";
7+
import MarkdownRenderer from "../MarkdownRenderer";
8+
import { safeJsonParse } from "@/utils/request";
79

810
export default function ChatRow({ chat, onDelete }) {
911
const {
@@ -83,7 +85,11 @@ export default function ChatRow({ chat, onDelete }) {
8385
</ModalWrapper>
8486
<ModalWrapper isOpen={isResponseOpen}>
8587
<TextPreview
86-
text={JSON.parse(chat.response)?.text}
88+
text={
89+
<MarkdownRenderer
90+
content={safeJsonParse(chat.response, {})?.text}
91+
/>
92+
}
8793
closeModal={closeResponseModal}
8894
/>
8995
</ModalWrapper>
@@ -118,9 +124,9 @@ const TextPreview = ({ text, closeModal }) => {
118124
</button>
119125
</div>
120126
<div className="w-full p-6">
121-
<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">
127+
<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">
122128
{text}
123-
</pre>
129+
</div>
124130
</div>
125131
</div>
126132
</div>
@@ -132,11 +138,7 @@ const ConnectionDetails = ({
132138
verbose = false,
133139
connection_information,
134140
}) => {
135-
let details = {};
136-
try {
137-
details = JSON.parse(connection_information);
138-
} catch {}
139-
141+
const details = safeJsonParse(connection_information, {});
140142
if (Object.keys(details).length === 0) return null;
141143

142144
if (verbose) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { useState } from "react";
2+
import MarkdownIt from "markdown-it";
3+
import { CaretDown } from "@phosphor-icons/react";
4+
import "highlight.js/styles/github-dark.css";
5+
import DOMPurify from "@/utils/chat/purify";
6+
7+
const md = new MarkdownIt({
8+
html: true,
9+
breaks: true,
10+
highlight: function (str, lang) {
11+
if (lang && hljs.getLanguage(lang)) {
12+
try {
13+
return hljs.highlight(str, { language: lang }).value;
14+
} catch (__) {}
15+
}
16+
return ""; // use external default escaping
17+
},
18+
});
19+
20+
const ThoughtBubble = ({ thought }) => {
21+
const [isExpanded, setIsExpanded] = useState(false);
22+
23+
if (!thought) return null;
24+
25+
const cleanThought = thought.replace(/<\/?think>/g, "").trim();
26+
if (!cleanThought) return null;
27+
28+
return (
29+
<div className="mb-3">
30+
<div
31+
onClick={() => setIsExpanded(!isExpanded)}
32+
className="cursor-pointer flex items-center gap-x-2 text-theme-text-secondary hover:text-theme-text-primary transition-colors mb-2"
33+
>
34+
<CaretDown
35+
size={14}
36+
weight="bold"
37+
className={`transition-transform ${isExpanded ? "rotate-180" : ""}`}
38+
/>
39+
<span className="text-xs font-medium">View thoughts</span>
40+
</div>
41+
{isExpanded && (
42+
<div className="bg-theme-bg-chat-input rounded-md p-3 border-l-2 border-theme-text-secondary/30">
43+
<div className="text-xs text-theme-text-secondary font-mono whitespace-pre-wrap">
44+
{cleanThought}
45+
</div>
46+
</div>
47+
)}
48+
</div>
49+
);
50+
};
51+
52+
function parseContent(content) {
53+
const parts = [];
54+
let lastIndex = 0;
55+
content.replace(/<think>([^]*?)<\/think>/g, (match, thinkContent, offset) => {
56+
if (offset > lastIndex) {
57+
parts.push({ type: "normal", text: content.slice(lastIndex, offset) });
58+
}
59+
parts.push({ type: "think", text: thinkContent });
60+
lastIndex = offset + match.length;
61+
});
62+
if (lastIndex < content.length) {
63+
parts.push({ type: "normal", text: content.slice(lastIndex) });
64+
}
65+
return parts;
66+
}
67+
68+
export default function MarkdownRenderer({ content }) {
69+
if (!content) return null;
70+
71+
const parts = parseContent(content);
72+
return (
73+
<div className="whitespace-normal">
74+
{parts.map((part, index) => {
75+
const html = md.render(part.text);
76+
if (part.type === "think")
77+
return <ThoughtBubble key={index} thought={part.text} />;
78+
return (
79+
<div
80+
key={index}
81+
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }}
82+
/>
83+
);
84+
})}
85+
</div>
86+
);
87+
}

frontend/src/pages/GeneralSettings/ChatEmbedWidgets/EmbedChats/index.jsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default function EmbedChatsView() {
5555
const query = useQuery();
5656
const [offset, setOffset] = useState(Number(query.get("offset") || 0));
5757
const [canNext, setCanNext] = useState(false);
58+
const [showThinking, setShowThinking] = useState(true);
5859

5960
const handleDumpChats = async (exportType) => {
6061
const chats = await System.exportChats(exportType, "embed");
@@ -92,10 +93,15 @@ export default function EmbedChatsView() {
9293

9394
useEffect(() => {
9495
async function fetchChats() {
95-
const { chats: _chats, hasPages = false } = await Embed.chats(offset);
96-
setChats(_chats);
97-
setCanNext(hasPages);
98-
setLoading(false);
96+
setLoading(true);
97+
await Embed.chats(offset)
98+
.then(({ chats: _chats, hasPages = false }) => {
99+
setChats(_chats);
100+
setCanNext(hasPages);
101+
})
102+
.finally(() => {
103+
setLoading(false);
104+
});
99105
}
100106
fetchChats();
101107
}, [offset]);
@@ -211,7 +217,7 @@ export default function EmbedChatsView() {
211217
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
212218
}`}
213219
>
214-
{t("embed-chats.previous")}
220+
{t("common.previous")}
215221
</button>
216222
<button
217223
onClick={handleNext}
@@ -222,7 +228,7 @@ export default function EmbedChatsView() {
222228
: "bg-theme-bg-secondary text-theme-text-primary hover:bg-theme-hover"
223229
}`}
224230
>
225-
{t("embed-chats.next")}
231+
{t("common.next")}
226232
</button>
227233
</div>
228234
)}

0 commit comments

Comments
 (0)