feat(ui): chat frontend, model picker, streaming, admin, a11y

Preview/UI work: chat renderer, streaming/think-trace, model picker
(instant-open/refresh-backoff), admin panel, a11y, gallery landing.

Source: vendor/photocraft (ad1cff44) static files, 3-way merged onto
upstream/dev (a8c147b2). Resolution policy in pr_work/D_conflicts.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-13 13:42:33 +00:00
co-authored by Claude Opus 5.5
parent a8c147b238
commit 8fda203d28
26 changed files with 571 additions and 144 deletions
+22 -1
View File
@@ -34,6 +34,8 @@ export function createStreamRenderer(contentEl, { render, hljs } = {}) {
let tailShownLen = 0; // rendered-text length of the live tail (drives token fade)
let appendMode = null; // { codeText: Text, appendedLen } while an open fence streams
let degraded = !ENABLED; // true once we fall back to full re-render
let tailRaf = null; // pending requestAnimationFrame id for the live-tail re-render
let pendingTail = ''; // most recent tail text awaiting a rAF re-render
function start() {
contentEl.textContent = '';
@@ -79,6 +81,24 @@ export function createStreamRenderer(contentEl, { render, hljs } = {}) {
while (holder.firstChild) contentEl.appendChild(holder.firstChild);
}
// Coalesce the per-token tail re-render into at most one render per animation
// frame. Fast providers emit many deltas per frame; the frozen-block pass is
// cheap and stays synchronous, but the markdown re-parse + innerHTML write of
// the still-growing tail only needs to happen once per frame for smooth output.
function scheduleTailRender(tailText) {
pendingTail = tailText;
if (tailRaf !== null) return;
tailRaf = requestAnimationFrame(() => {
tailRaf = null;
// The container may have been rewritten out from under us (chat.js writes
// contentEl.innerHTML for thinking indicators and tool blocks). Skip and
// let the next update() self-heal rather than appending stale tail nodes.
if (tailMarker && tailMarker.parentNode === contentEl) {
renderTail(pendingTail);
}
});
}
// Stream the body of an unterminated code fence by appending only the new
// characters to a stable <pre><code> text node — no re-parse, no re-highlight.
function appendOpenFence(tailText, fence) {
@@ -171,7 +191,7 @@ export function createStreamRenderer(contentEl, { render, hljs } = {}) {
appendMode = null; // whatever was streaming is now frozen
tailShownLen = 0;
}
renderTail(fullText.slice(committedLen));
scheduleTailRender(fullText.slice(committedLen));
} catch (err) {
degraded = true;
console.error('streamingRenderer: falling back to full render', err);
@@ -187,6 +207,7 @@ export function createStreamRenderer(contentEl, { render, hljs } = {}) {
function finalize() {
if (degraded) return;
try {
if (tailRaf !== null) { cancelAnimationFrame(tailRaf); tailRaf = null; }
if (!started) start();
clearTail();
appendMode = null;