From 12d326edd94438705ddf24ca90ca7096ec76b6de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Feb 2026 18:24:22 +0000 Subject: [PATCH] chore: replace monolithic chat.js with TypeScript-built bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated chat.js is now built from TypeScript source in core/go/lem-chat/ via esbuild. Functionally identical, 15KB ESM bundle (was 833 lines, now 556 — esbuild compacts class syntax). Source: forge.lthn.ai/core/go → lem-chat/src/ Co-Authored-By: Claude Opus 4.6 --- cmd/chat.js | 306 +++------------------------------------------------- 1 file changed, 15 insertions(+), 291 deletions(-) diff --git a/cmd/chat.js b/cmd/chat.js index 8948708..c76083e 100644 --- a/cmd/chat.js +++ b/cmd/chat.js @@ -61,48 +61,6 @@ var chatStyles = ` background: #f04747; } `; -var messagesStyles = ` - :host { - display: block; - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 16px 0; - scroll-behavior: smooth; - } - - :host::-webkit-scrollbar { - width: 6px; - } - - :host::-webkit-scrollbar-track { - background: transparent; - } - - :host::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.12); - border-radius: 3px; - } - - .empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - gap: 12px; - color: rgba(255, 255, 255, 0.25); - } - - .empty-icon { - font-size: 36px; - opacity: 0.4; - } - - .empty-text { - font-size: 14px; - } -`; var messageStyles = ` :host { display: block; @@ -245,140 +203,6 @@ var messageStyles = ` 50% { opacity: 0; } } `; -var inputStyles = ` - :host { - display: block; - padding: 12px 16px 16px; - border-top: 1px solid rgba(255, 255, 255, 0.06); - flex-shrink: 0; - } - - .input-wrapper { - display: flex; - align-items: flex-end; - gap: 10px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 12px; - padding: 8px 12px; - transition: border-color 0.15s; - } - - .input-wrapper:focus-within { - border-color: var(--lem-accent, #5865f2); - } - - textarea { - flex: 1; - background: none; - border: none; - outline: none; - color: var(--lem-text, #e0e0e0); - font-family: inherit; - font-size: 14px; - line-height: 1.5; - resize: none; - max-height: 120px; - min-height: 22px; - padding: 0; - } - - textarea::placeholder { - color: rgba(255, 255, 255, 0.25); - } - - .send-btn { - background: var(--lem-accent, #5865f2); - border: none; - border-radius: 8px; - color: #fff; - width: 32px; - height: 32px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: opacity 0.15s, transform 0.1s; - } - - .send-btn:hover { - opacity: 0.85; - } - - .send-btn:active { - transform: scale(0.95); - } - - .send-btn:disabled { - opacity: 0.3; - cursor: default; - transform: none; - } - - .send-btn svg { - width: 16px; - height: 16px; - } -`; - -// src/lem-messages.ts -var LemMessages = class extends HTMLElement { - shadow; - container; - emptyEl; - shouldAutoScroll = true; - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const style = document.createElement("style"); - style.textContent = messagesStyles; - this.container = document.createElement("div"); - this.emptyEl = document.createElement("div"); - this.emptyEl.className = "empty"; - const emptyIcon = document.createElement("div"); - emptyIcon.className = "empty-icon"; - emptyIcon.textContent = "\u2728"; - const emptyText = document.createElement("div"); - emptyText.className = "empty-text"; - emptyText.textContent = "Start a conversation"; - this.emptyEl.appendChild(emptyIcon); - this.emptyEl.appendChild(emptyText); - this.shadow.appendChild(style); - this.shadow.appendChild(this.emptyEl); - this.shadow.appendChild(this.container); - this.addEventListener("scroll", () => { - const threshold = 60; - this.shouldAutoScroll = this.scrollHeight - this.scrollTop - this.clientHeight < threshold; - }); - } - addMessage(role, text) { - this.emptyEl.style.display = "none"; - const msg = document.createElement("lem-message"); - msg.setAttribute("role", role); - this.container.appendChild(msg); - if (text) { - msg.text = text; - } - this.scrollToBottom(); - return msg; - } - scrollToBottom() { - if (this.shouldAutoScroll) { - requestAnimationFrame(() => { - this.scrollTop = this.scrollHeight; - }); - } - } - clear() { - this.container.replaceChildren(); - this.emptyEl.style.display = ""; - this.shouldAutoScroll = true; - } -}; -customElements.define("lem-messages", LemMessages); // src/markdown.ts function escapeHtml(text) { @@ -393,6 +217,11 @@ function parseInline(text) { result = result.replace(/(?$1"); return result; } +function wrapParagraph(lines) { + const joined = lines.join("
"); + if (joined.startsWith("${joined}

`; +} function renderMarkdown(text) { const lines = text.split("\n"); const output = []; @@ -407,9 +236,7 @@ function renderMarkdown(text) { codeLines = []; } else { const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""; - output.push( - `${escapeHtml(codeLines.join("\n"))}` - ); + output.push(`${escapeHtml(codeLines.join("\n"))}`); inCodeBlock = false; codeLines = []; codeLang = ""; @@ -428,9 +255,7 @@ function renderMarkdown(text) { } if (inCodeBlock) { const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""; - output.push( - `${escapeHtml(codeLines.join("\n"))}` - ); + output.push(`${escapeHtml(codeLines.join("\n"))}`); } const paragraphs = []; let current = []; @@ -449,11 +274,6 @@ function renderMarkdown(text) { } return paragraphs.join(""); } -function wrapParagraph(lines) { - const joined = lines.join("
"); - if (joined.startsWith("${joined}

`; -} // src/lem-message.ts var LemMessage = class extends HTMLElement { @@ -462,7 +282,7 @@ var LemMessage = class extends HTMLElement { thinkContent; thinkLabel; contentEl; - cursorEl; + cursorEl = null; _text = ""; _streaming = false; _thinkCollapsed = false; @@ -504,7 +324,7 @@ var LemMessage = class extends HTMLElement { this.shadow.appendChild(style); this.shadow.appendChild(bubble); if (this._text) { - this.render(); + this.updateContent(); } } get text() { @@ -512,18 +332,18 @@ var LemMessage = class extends HTMLElement { } set text(value) { this._text = value; - this.render(); + this.updateContent(); } get streaming() { return this._streaming; } set streaming(value) { this._streaming = value; - this.render(); + this.updateContent(); } appendToken(token) { this._text += token; - this.render(); + this.updateContent(); } /** * Splits text into think/response portions and renders each. @@ -532,7 +352,7 @@ var LemMessage = class extends HTMLElement { * inline formatting is applied. The source is the local MLX model output, * not arbitrary user HTML. Shadow DOM provides additional isolation. */ - render() { + updateContent() { if (!this.contentEl) return; const { think, response } = this.splitThink(this._text); if (think !== null && this.thinkPanel) { @@ -554,10 +374,6 @@ var LemMessage = class extends HTMLElement { } } } - /** - * Split raw text into think content and response content. - * Returns { think: string | null, response: string } - */ splitThink(text) { const thinkStart = text.indexOf(""); if (thinkStart === -1) { @@ -580,99 +396,6 @@ var LemMessage = class extends HTMLElement { }; customElements.define("lem-message", LemMessage); -// src/lem-input.ts -var LemInput = class extends HTMLElement { - shadow; - textarea; - sendBtn; - _disabled = false; - constructor() { - super(); - this.shadow = this.attachShadow({ mode: "open" }); - } - connectedCallback() { - const style = document.createElement("style"); - style.textContent = inputStyles; - const wrapper = document.createElement("div"); - wrapper.className = "input-wrapper"; - this.textarea = document.createElement("textarea"); - this.textarea.rows = 1; - this.textarea.placeholder = "Message LEM..."; - this.sendBtn = document.createElement("button"); - this.sendBtn.className = "send-btn"; - this.sendBtn.type = "button"; - this.sendBtn.disabled = true; - this.sendBtn.appendChild(this.createSendIcon()); - wrapper.appendChild(this.textarea); - wrapper.appendChild(this.sendBtn); - this.shadow.appendChild(style); - this.shadow.appendChild(wrapper); - this.textarea.addEventListener("input", () => { - this.textarea.style.height = "auto"; - this.textarea.style.height = Math.min(this.textarea.scrollHeight, 120) + "px"; - this.sendBtn.disabled = this._disabled || this.textarea.value.trim() === ""; - }); - this.textarea.addEventListener("keydown", (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - this.submit(); - } - }); - this.sendBtn.addEventListener("click", () => this.submit()); - } - /** Build the send arrow SVG using DOM API (no innerHTML) */ - createSendIcon() { - const ns = "http://www.w3.org/2000/svg"; - const svg = document.createElementNS(ns, "svg"); - svg.setAttribute("viewBox", "0 0 24 24"); - svg.setAttribute("fill", "none"); - svg.setAttribute("stroke", "currentColor"); - svg.setAttribute("stroke-width", "2"); - svg.setAttribute("stroke-linecap", "round"); - svg.setAttribute("stroke-linejoin", "round"); - svg.setAttribute("width", "16"); - svg.setAttribute("height", "16"); - const line = document.createElementNS(ns, "line"); - line.setAttribute("x1", "22"); - line.setAttribute("y1", "2"); - line.setAttribute("x2", "11"); - line.setAttribute("y2", "13"); - const polygon = document.createElementNS(ns, "polygon"); - polygon.setAttribute("points", "22 2 15 22 11 13 2 9 22 2"); - svg.appendChild(line); - svg.appendChild(polygon); - return svg; - } - submit() { - const text = this.textarea.value.trim(); - if (!text || this._disabled) return; - this.dispatchEvent( - new CustomEvent("lem-send", { - bubbles: true, - composed: true, - detail: { text } - }) - ); - this.textarea.value = ""; - this.textarea.style.height = "auto"; - this.sendBtn.disabled = true; - this.textarea.focus(); - } - get disabled() { - return this._disabled; - } - set disabled(value) { - this._disabled = value; - this.textarea.disabled = value; - this.sendBtn.disabled = value || this.textarea.value.trim() === ""; - this.textarea.placeholder = value ? "LEM is thinking..." : "Message LEM..."; - } - focus() { - this.textarea?.focus(); - } -}; -customElements.define("lem-input", LemInput); - // src/lem-chat.ts var LemChat = class extends HTMLElement { shadow; @@ -715,7 +438,8 @@ var LemChat = class extends HTMLElement { this.shadow.appendChild(this.messages); this.shadow.appendChild(this.input); this.addEventListener("lem-send", ((e) => { - this.handleSend(e.detail.text); + const detail = e.detail; + this.handleSend(detail.text); })); const systemPrompt = this.getAttribute("system-prompt"); if (systemPrompt) {