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,68 @@
|
||||
{#
|
||||
Shared template macros.
|
||||
|
||||
Import at the top of any template that needs them:
|
||||
{% from "_macros.html" import icon, brand, mark %}
|
||||
#}
|
||||
|
||||
{# An icon from the inlined sprite. `name` omits the "i-" prefix. #}
|
||||
{% macro icon(name, cls="") -%}
|
||||
<svg class="icon {{ cls }}" aria-hidden="true"><use href="#i-{{ name }}"/></svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{#
|
||||
The leaf-and-wafer mark, inlined rather than referenced as <img> so it can
|
||||
scale with the surrounding font size. Gradient ids are suffixed with `uid`
|
||||
because ids are document-global: two marks on one page with the same ids
|
||||
means the second silently reuses the first one's gradients.
|
||||
#}
|
||||
{% macro mark(cls="brand-mark", uid="a") -%}
|
||||
<svg class="{{ cls }}" viewBox="0 0 64 64" role="img" aria-label="LLeMbas">
|
||||
<defs>
|
||||
<linearGradient id="mk-{{ uid }}-w" x1="0" y1="0" x2="0.3" y2="1">
|
||||
<stop offset="0" stop-color="#EACB74"/>
|
||||
<stop offset="0.5" stop-color="#C9A227"/>
|
||||
<stop offset="1" stop-color="#916F13"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="mk-{{ uid }}-l" x1="0.1" y1="1" x2="0.9" y2="0">
|
||||
<stop offset="0" stop-color="#93A5B6"/>
|
||||
<stop offset="0.4" stop-color="#F1F6FA"/>
|
||||
<stop offset="1" stop-color="#B8C7D5"/>
|
||||
</linearGradient>
|
||||
<clipPath id="mk-{{ uid }}-c"><rect x="6" y="6" width="52" height="52" rx="13"/></clipPath>
|
||||
</defs>
|
||||
<rect x="6" y="6" width="52" height="52" rx="13" fill="url(#mk-{{ uid }}-w)"/>
|
||||
<g clip-path="url(#mk-{{ uid }}-c)" fill="none" stroke-linecap="round">
|
||||
<g stroke="#7A5C10" stroke-opacity="0.38" stroke-width="2">
|
||||
<path d="M32 6 V58"/><path d="M6 32 H58"/>
|
||||
</g>
|
||||
<g stroke="#F6E3A8" stroke-opacity="0.3" stroke-width="1">
|
||||
<path d="M33.2 6 V58"/><path d="M6 33.2 H58"/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="7.1" y="7.1" width="49.8" height="49.8" rx="11.9" fill="none"
|
||||
stroke="#7A5C10" stroke-opacity="0.3" stroke-width="1.2"/>
|
||||
<path d="M21.2 44.8 L17 49.4" stroke="#8A9AA8" stroke-width="3"
|
||||
stroke-linecap="round" fill="none"/>
|
||||
<path d="M20.5 45.5 C13.8 31.7 23.8 20.9 45.5 18.5 C49.8 35 39.8 45.8 20.5 45.5 Z"
|
||||
fill="url(#mk-{{ uid }}-l)"/>
|
||||
<path d="M20.5 45.5 C28 38 36 29 45.5 18.5" fill="none" stroke="#61758A"
|
||||
stroke-opacity="0.5" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{%- endmacro %}
|
||||
|
||||
{#
|
||||
The wordmark as live text rather than the outlined SVG: it stays selectable,
|
||||
searchable and readable to a screen reader, and scales with the user's font
|
||||
size. The capitals spelling LLM take the gold accent.
|
||||
#}
|
||||
{% macro wordmark() -%}
|
||||
<span class="brand-llm">LL</span>e<span class="brand-llm">M</span>bas
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro brand(href="/", uid="a") -%}
|
||||
<a class="sidebar__brand" href="{{ href }}">
|
||||
{{ mark(uid=uid) }}
|
||||
<span>{{ wordmark() }}</span>
|
||||
</a>
|
||||
{%- endmacro %}
|
||||
Reference in New Issue
Block a user