feat(ui): render assistant replies as Markdown

This commit is contained in:
2026-10-05 12:04:11 -03:00
parent c5ddb3f333
commit 0965077cac
15 changed files with 3430 additions and 6 deletions
+17 -5
View File
@@ -18,6 +18,7 @@
*/
import { $, escHtml, DEBUG } from "./dom.js";
import { renderMarkdown } from "./markdown.js";
/**
* @param {string} hypothesisId
@@ -99,6 +100,8 @@ export class ChatView {
// ── Assistant transcript state (keyed by response_id) ──────────────────
/** @type {Map<string, { bubble: HTMLElement, hist: HTMLElement }>} */
this._asstByResp = new Map();
/** Original transcript text is retained independently of rendered markup. @type {WeakMap<HTMLElement, string>} */
this._messageText = new WeakMap();
// ── Ephemeral bubble auto-dismiss ──────────────────────────────────────
// Per-element expiry (epoch ms). A bubble fades once its expiry passes —
@@ -310,9 +313,10 @@ export class ChatView {
el.setAttribute("data-asst-name", this._assistantName);
el.setAttribute("data-timestamp", stamp);
}
el.innerHTML = `<div class="${prefix}-role">${label}</div><div class="${prefix}-body${partial ? " partial" : ""}">${escHtml(text)}</div>`;
el.innerHTML = `<div class="${prefix}-role">${label}</div><div class="${prefix}-body${partial ? " partial" : ""}"></div>`;
const body = /** @type {HTMLDivElement} */ (el.lastElementChild);
if (body) {
this._renderMessageBody(body, text, role === "assistant");
if (role === "user") {
body.setAttribute("data-user-name", el.getAttribute("data-user-name") || "");
} else {
@@ -323,6 +327,14 @@ export class ChatView {
return el;
}
/** @param {HTMLElement} body @param {string} text @param {boolean} assistant */
_renderMessageBody(body, text, assistant) {
this._messageText.set(body, text);
body.classList.toggle("markdown-content", assistant);
if (assistant) body.replaceChildren(renderMarkdown(text));
else body.textContent = text;
}
// ── Ephemeral bubbles ───────────────────────────────────────────────────
/** @param {"user"|"assistant"|"tool"} role @param {string} text @returns {HTMLElement} */
@@ -348,8 +360,8 @@ export class ChatView {
/** @param {HTMLElement} el @param {string} text */
_updateBubbleText(el, text) {
const t = el.querySelector(".bubble-body");
if (t) t.textContent = text;
const t = /** @type {HTMLElement | null} */ (el.querySelector(".bubble-body"));
if (t) this._renderMessageBody(t, text, el.classList.contains("assistant"));
}
/** @param {HTMLElement} el */
@@ -433,7 +445,7 @@ export class ChatView {
if (!el) return;
const body = /** @type {HTMLElement | null} */ (el.querySelector(".hist-body"));
if (!body) return;
body.textContent = text;
this._renderMessageBody(body, text, el.classList.contains("assistant"));
body.classList.toggle("partial", partial);
this._scrollToBottom();
}
@@ -544,7 +556,7 @@ export class ChatView {
const role = row.classList.contains("user") ? "user" : "assistant";
const out = /** @type {{ type: string; text: string; timestamp?: string; name?: string; initials?: string; interrupted?: boolean }} */ ({
type: role,
text: body.textContent || "",
text: this._messageText.get(body) ?? body.textContent ?? "",
});
if (ts) out.timestamp = ts;
if (role === "user") {