Files
LLeMbas/src/lembas/web/templates/partials/_folder.html
T
Jaroslav Beneš 5f020ef33f Live Markdown, stop, rewind, custom picker, dialogs
Seven things.

**Reasoning starts closed.** The answer is what the reader is waiting
for; the thinking is one click away.

**Image borders.** .attachments__image was a block-level <a>, so its
border stretched the full column around a narrow picture. inline-block,
and the frame is the picture. Same fix for the composer thumbnail.

**Markdown now renders during the stream.** The generator re-renders the
answer so far and sends it as a `render` event at most every 100ms,
swapped with innerHTML, instead of appending escaped tokens and
formatting everything at the end. Re-rendering whole rather than
appending is the point: a list or a code fence is only correct once its
context exists, and partial syntax resolves itself as more arrives.
Measured against a live model: 29 render events, formatting visible from
the first content token.

**Stop button.** A stop request goes into an in-process set the
generator checks between chunks; whatever arrived is kept, because a
half-written answer the reader chose to cut short is still worth having.
Measured: stream ended 0.2s after the request, 1155 characters
preserved, message marked stopped rather than errored. Navigating away
does the same thing via CancelledError.

**Rewind and edit.** Edit one of your own turns and everything after it
is deleted, then the conversation runs on from there. Deliberately not
branching: that needs a UI for choosing between versions, and "go back
and try again from here" is what was asked for. The form states how many
messages will be discarded before you confirm.

**Custom model picker.** A <select> renders only text in an <option>, so
it can never show an avatar. Built from buttons and a hidden input, with
descriptions, capability tags, a filter box past eight models, and
arrow-key navigation written out by hand since there is no native widget
doing it.

**Notification system.** lembas.notify/confirm/prompt in ui.js, built on
<dialog> so focus trapping, Escape and page inertness come from the
browser. htmx:confirm is intercepted, so every existing hx-confirm gets
the themed dialog with no change at the call site; the browser's grey
confirm() is gone from every template.

230 tests, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:33:04 +02:00

48 lines
1.8 KiB
HTML

{% from "_macros.html" import icon %}
{#
A folder and everything under it. Recursive: a folder includes this same
template for each of its children, so nesting has no fixed depth limit.
Open/closed state is local to the browser (Alpine), not persisted per
request -- collapsing a folder should not cost a round trip.
#}
<div class="folder" x-data="{ open: {{ 'false' if folder.collapsed else 'true' }} }">
<div class="nav-item folder__row">
<button class="folder__toggle" type="button" @click="open = !open"
:aria-expanded="open ? 'true' : 'false'">
<span class="folder__chevron" :class="open && 'is-open'">
{{ icon("chevron-right", "icon--sm") }}
</span>
<span x-show="open">{{ icon("folder-open", "icon--sm") }}</span>
<span x-show="!open" x-cloak>{{ icon("folder", "icon--sm") }}</span>
<span class="nav-item__label">{{ folder.name }}</span>
</button>
<span class="nav-item__actions">
<button class="btn btn--icon btn--sm" type="button"
hx-delete="/api/folders/{{ folder.id }}"
hx-confirm="Delete the folder “{{ folder.name }}”? Chats inside it are kept."
data-confirm-title="Delete folder"
hx-swap="none"
aria-label="Delete folder">
{{ icon("trash", "icon--sm") }}
</button>
</span>
</div>
<div class="folder__contents" x-show="open" x-cloak>
{% for child in folder.children %}
{% with folder = child %}
{% include "partials/_folder.html" %}
{% endwith %}
{% endfor %}
{% for chat_item in folder.chats %}
{% include "partials/_chat_link.html" %}
{% endfor %}
{% if not folder.children and not folder.chats %}
<p class="nav-empty">Empty</p>
{% endif %}
</div>
</div>