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,113 @@
|
||||
/* Administration screens. */
|
||||
|
||||
.admin-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-strong) transparent;
|
||||
}
|
||||
|
||||
.admin-page {
|
||||
max-width: 46rem;
|
||||
margin: 0 auto;
|
||||
padding: var(--sp-6) var(--sp-5) var(--sp-12);
|
||||
}
|
||||
|
||||
.admin-lede {
|
||||
color: var(--ink-muted);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--leading-relaxed);
|
||||
margin-bottom: var(--sp-6);
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.admin-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--text-lg);
|
||||
margin: var(--sp-8) 0 var(--sp-4);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-5);
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
.card__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
font-size: var(--text-md);
|
||||
margin-bottom: var(--sp-4);
|
||||
}
|
||||
|
||||
.form-grid { display: block; }
|
||||
.form-grid .field:last-of-type { margin-bottom: 0; }
|
||||
.field--actions { margin-top: var(--sp-5); margin-bottom: 0; }
|
||||
|
||||
.connection__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
margin-bottom: var(--sp-4);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.connection__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
margin-top: var(--sp-5);
|
||||
padding-top: var(--sp-4);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Reachable status at a glance: green working, red errored, grey disabled. */
|
||||
.status-dot {
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: var(--radius-full);
|
||||
flex: none;
|
||||
background: var(--ink-faint);
|
||||
}
|
||||
.status-dot.is-ok { background: var(--success); }
|
||||
.status-dot.is-bad { background: var(--danger); }
|
||||
.status-dot.is-off { background: var(--ink-faint); }
|
||||
|
||||
.model-list { list-style: none; margin: 0; padding: 0; }
|
||||
.model-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--sp-3);
|
||||
padding: var(--sp-2) 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.model-list__item:last-child { border-bottom: 0; }
|
||||
.model-list__id {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-sm);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-item.is-disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.nav-item.is-disabled:hover { background: none; color: var(--ink-muted); }
|
||||
|
||||
.field__hint code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.92em;
|
||||
padding: 0.05em 0.3em;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--code-border);
|
||||
}
|
||||
Reference in New Issue
Block a user