这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
26 changes: 8 additions & 18 deletions frontend/src/pages/GeneralSettings/Chats/ChatRow/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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">
Expand All @@ -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>
Expand Down
88 changes: 88 additions & 0 deletions frontend/src/pages/GeneralSettings/Chats/MarkdownRenderer.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
}