Working chat: auth, connections, streaming, folders
LLeMbas now runs end to end. Register, add an OpenAI-compatible connection, and hold a real streaming conversation organised into folders. Verified against the local llama-swap instance. Streaming is the one genuinely tricky part. Sending a message returns two HTML fragments -- the user bubble and an empty assistant bubble carrying an sse-connect -- and that attribute is the ONLY thing that starts a generation. Rendering an incomplete assistant message as a streaming shell falls out of the same template, which means loading a page whose last reply never finished simply picks it up again. Details worth knowing about, each commented where it matters: - SSE payloads are split across several data: lines. A raw newline in one data: line truncates the event, which shows up the first time a model emits a code block. - Markdown is rendered server-side by the same helper for both the page and the final streamed frame, so the two cannot disagree. The fence renderer is replaced outright rather than using markdown-it's highlight option, which re-wraps output in a second <pre>. - escape_text is html.escape, not nh3.clean_text: it escapes character by character, so escaping stream chunks separately equals escaping the whole string. - The stream opens its own session via session_scope(); it outlives the request handler and the dependency-scoped session may be closed. - Deleting a folder keeps the chats inside it (FK is SET NULL). Losing a conversation to a mis-clicked folder delete is unforgivable. - Login failures use one message for "no such account" and "wrong password" so the form cannot enumerate registered addresses. Also adds deploy/ for the gamebox install at https://chat.lan: system unit, nginx vhost with buffering off (buffering on turns streaming into one lump at the end), and install/update scripts following the same service-user and /srv bind-mount conventions as llama-swap and comfyui. 70 tests, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
Design tokens.
|
||||
|
||||
Every colour, space and radius in the application resolves through a variable
|
||||
declared here. Component CSS must never hard-code a hex value -- that is what
|
||||
makes adding a theme a matter of writing one new block rather than auditing
|
||||
every stylesheet.
|
||||
|
||||
Themes are selected with data-theme on <html>. `moria` is the default and is
|
||||
declared on :root so the page is styled even before the theme script runs.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* --- Type ------------------------------------------------------------- */
|
||||
--font-display: "Iowan Old Style", "Palatino Linotype", Palatino, Palladio,
|
||||
"URW Palladio L", "Book Antiqua", Baskerville, Georgia, serif;
|
||||
--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
|
||||
Arial, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code",
|
||||
"Cascadia Code", Menlo, Consolas, monospace;
|
||||
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.8125rem;
|
||||
--text-base: 0.9375rem;
|
||||
--text-md: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.375rem;
|
||||
--text-2xl: 1.75rem;
|
||||
--text-3xl: 2.25rem;
|
||||
|
||||
--leading-tight: 1.25;
|
||||
--leading-normal: 1.6;
|
||||
--leading-relaxed: 1.75;
|
||||
|
||||
/* --- Space (4px scale) ------------------------------------------------ */
|
||||
--sp-1: 0.25rem;
|
||||
--sp-2: 0.5rem;
|
||||
--sp-3: 0.75rem;
|
||||
--sp-4: 1rem;
|
||||
--sp-5: 1.25rem;
|
||||
--sp-6: 1.5rem;
|
||||
--sp-8: 2rem;
|
||||
--sp-10: 2.5rem;
|
||||
--sp-12: 3rem;
|
||||
--sp-16: 4rem;
|
||||
|
||||
/* --- Radius & shadow -------------------------------------------------- */
|
||||
--radius-sm: 4px;
|
||||
--radius: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 18px;
|
||||
--radius-full: 999px;
|
||||
|
||||
/* --- Layout ----------------------------------------------------------- */
|
||||
--sidebar-width: 17rem;
|
||||
--thread-max-width: 48rem;
|
||||
--header-height: 3.25rem;
|
||||
|
||||
--transition-fast: 120ms ease;
|
||||
--transition: 200ms ease;
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------------
|
||||
MORIA (default, dark)
|
||||
|
||||
Deep stone and lamplight: the halls under the mountain. Surfaces are cool and
|
||||
near-neutral so the gold and mithril accents carry all the colour.
|
||||
---------------------------------------------------------------------------
|
||||
*/
|
||||
:root,
|
||||
:root[data-theme="moria"] {
|
||||
color-scheme: dark;
|
||||
|
||||
--bg: #101317;
|
||||
--bg-sunken: #0B0E11;
|
||||
--surface: #171B21;
|
||||
--surface-raised: #1E242B;
|
||||
--surface-hover: #232A32;
|
||||
--surface-active: #2A323B;
|
||||
|
||||
--border: #2A313A;
|
||||
--border-strong: #3A434E;
|
||||
|
||||
--ink: #E4E8EC;
|
||||
--ink-muted: #A2ADB8;
|
||||
--ink-faint: #6E7883;
|
||||
--ink-inverse: #0B0E11;
|
||||
|
||||
/* Mithril: the cool primary, used for focus and interactive accents. */
|
||||
--accent: #8FB3CC;
|
||||
--accent-hover: #A9C6DA;
|
||||
--accent-ink: #0B0E11;
|
||||
--accent-soft: rgba(143, 179, 204, 0.14);
|
||||
|
||||
/* Rune gold: the warm accent. Brand colour, and the assistant's mark. */
|
||||
--gold: #E0B252;
|
||||
--gold-hover: #EDC46F;
|
||||
--gold-soft: rgba(224, 178, 82, 0.13);
|
||||
|
||||
/* Ember: destructive actions and errors. */
|
||||
--danger: #E2795A;
|
||||
--danger-hover: #EC8E72;
|
||||
--danger-soft: rgba(226, 121, 90, 0.14);
|
||||
|
||||
--success: #7FB77E;
|
||||
--success-soft: rgba(127, 183, 126, 0.14);
|
||||
--warning: #DFAE58;
|
||||
--warning-soft: rgba(223, 174, 88, 0.14);
|
||||
|
||||
--bubble-user: #232B34;
|
||||
--bubble-assistant: transparent;
|
||||
--code-bg: #0C0F13;
|
||||
--code-border: #262D36;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
--shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
|
||||
--shadow-lg: 0 12px 34px rgba(0, 0, 0, 0.55);
|
||||
|
||||
--scrim: rgba(6, 8, 10, 0.66);
|
||||
}
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------------------
|
||||
SHIRE (light)
|
||||
|
||||
Parchment, ink and moss: warm, low-contrast, easy to read for a long time.
|
||||
Backgrounds are deliberately off-white -- pure #FFF next to the gold accent
|
||||
reads as clinical rather than as paper.
|
||||
---------------------------------------------------------------------------
|
||||
*/
|
||||
:root[data-theme="shire"] {
|
||||
color-scheme: light;
|
||||
|
||||
--bg: #F6F1E4;
|
||||
--bg-sunken: #EDE6D4;
|
||||
--surface: #FDFBF5;
|
||||
--surface-raised: #FFFFFF;
|
||||
--surface-hover: #F1EADA;
|
||||
--surface-active: #E7DEC9;
|
||||
|
||||
--border: #DED3BB;
|
||||
--border-strong: #C6B896;
|
||||
|
||||
--ink: #2C2419;
|
||||
--ink-muted: #6A5C48;
|
||||
--ink-faint: #94856D;
|
||||
--ink-inverse: #FDFBF5;
|
||||
|
||||
/* Hobbit-door blue-green: the cool primary. */
|
||||
--accent: #3E6B7A;
|
||||
--accent-hover: #325867;
|
||||
--accent-ink: #FDFBF5;
|
||||
--accent-soft: rgba(62, 107, 122, 0.12);
|
||||
|
||||
--gold: #A8801A;
|
||||
--gold-hover: #8E6B12;
|
||||
--gold-soft: rgba(168, 128, 26, 0.13);
|
||||
|
||||
--danger: #A6432B;
|
||||
--danger-hover: #8C3722;
|
||||
--danger-soft: rgba(166, 67, 43, 0.11);
|
||||
|
||||
--success: #4F7A3F;
|
||||
--success-soft: rgba(79, 122, 63, 0.12);
|
||||
--warning: #98701A;
|
||||
--warning-soft: rgba(152, 112, 26, 0.13);
|
||||
|
||||
--bubble-user: #EDE4CF;
|
||||
--bubble-assistant: transparent;
|
||||
--code-bg: #F2EBD9;
|
||||
--code-border: #DED3BB;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(72, 58, 34, 0.09);
|
||||
--shadow: 0 4px 14px rgba(72, 58, 34, 0.11);
|
||||
--shadow-lg: 0 12px 34px rgba(72, 58, 34, 0.16);
|
||||
|
||||
--scrim: rgba(44, 36, 25, 0.4);
|
||||
}
|
||||
|
||||
/* Respect a stated preference for reduced motion everywhere, at once. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user