| let markdownTools = null; | |
| export async function ensureMarkdownTools() { | |
| if (markdownTools) { | |
| return markdownTools; | |
| } | |
| const [{ marked }, { default: DOMPurify }] = await Promise.all([ | |
| import("https://cdn.jsdelivr.net/npm/marked@15.0.7/lib/marked.esm.js"), | |
| import("https://cdn.jsdelivr.net/npm/dompurify@3.2.4/dist/purify.es.min.js"), | |
| ]); | |
| marked.setOptions({ | |
| breaks: true, | |
| gfm: true, | |
| headerIds: false, | |
| mangle: false, | |
| }); | |
| markdownTools = { marked, DOMPurify }; | |
| return markdownTools; | |
| } | |
| export async function renderMarkdownHtml(content) { | |
| const text = String(content || "").trim(); | |
| if (!text) { | |
| return ""; | |
| } | |
| const { marked, DOMPurify } = await ensureMarkdownTools(); | |
| const html = marked.parse(text); | |
| const sanitized = DOMPurify.sanitize(html, { | |
| USE_PROFILES: { html: true }, | |
| ADD_ATTR: ["target", "rel"], | |
| }); | |
| return sanitized; | |
| } | |
| export async function renderMarkdownInto(element, content) { | |
| element.innerHTML = await renderMarkdownHtml(content); | |
| for (const link of element.querySelectorAll("a")) { | |
| link.target = "_blank"; | |
| link.rel = "noopener noreferrer"; | |
| } | |
| } | |
| export function plainTextSummary(content, limit = 280) { | |
| const text = String(content || "") | |
| .replace(/[#>*`_\-[\]()]/g, " ") | |
| .replace(/\s+/g, " ") | |
| .trim(); | |
| if (text.length <= limit) { | |
| return text; | |
| } | |
| return `${text.slice(0, limit - 1)}…`; | |
| } | |
| const THINK_OPEN = "<" + "think" + ">"; | |
| const THINK_CLOSE = "</" + "think" + ">"; | |
| export function splitThinkingContent(fullText, options = {}) { | |
| const metaThinking = String(options.thinking || "").trim(); | |
| let text = String(fullText || "") | |
| .replace(/<\|redacted_im_end\|>/g, "") | |
| .replace(/<think>/g, THINK_OPEN) | |
| .replace(/<\/redacted_thinking>/g, THINK_CLOSE); | |
| let thinking = metaThinking; | |
| let answer = text.trim(); | |
| const openIdx = text.indexOf(THINK_OPEN); | |
| const closeIdx = text.indexOf(THINK_CLOSE); | |
| if (openIdx !== -1 && closeIdx !== -1 && closeIdx > openIdx) { | |
| const extracted = text.slice(openIdx + THINK_OPEN.length, closeIdx).trim(); | |
| thinking = thinking ? `${thinking}\n\n${extracted}` : extracted; | |
| answer = `${text.slice(0, openIdx)}${text.slice(closeIdx + THINK_CLOSE.length)}`.trim(); | |
| } else if (openIdx !== -1 && closeIdx === -1) { | |
| const extracted = text.slice(openIdx + THINK_OPEN.length).trim(); | |
| thinking = thinking ? `${thinking}\n\n${extracted}` : extracted; | |
| answer = text.slice(0, openIdx).trim(); | |
| } | |
| return { | |
| thinking: thinking.trim(), | |
| answer: answer.trim(), | |
| }; | |
| } | |