Files
LLeMbas/src/lembas/web/templates/admin/suggestions.html
T
Jaroslav Beneš 26793b1317 Prompt suggestions on the new-chat screen
A blank composer is the least helpful thing a chat client can show someone
who has just installed one. Three cards now sit under the empty state, and
an administrator manages them at /admin/suggestions.

Clicking a card fills the composer and stops there. It deliberately does not
send: every default ends mid-sentence, because a card is a starting point
rather than a question somebody already asked, and the caret lands where the
person has to start typing.

Seeding is guarded by a settings flag, not by "is the table empty" --
otherwise an administrator who decided against them would get all three back
on every restart. Capped at twelve, six shown: past a dozen this is a menu,
and a menu on the empty screen is a worse blank page than a blank page.

The cards are gated on there being no chat at all, not on the thread being
empty. An empty chat someone opened on purpose already has a model and a
prompt chosen.

Also fixes a pre-existing bug the position test caught. Both this and
_refresh_models wrote `coalesce(max(position), -1) or -1`, and position 0 is
falsy -- so the second row landed back on 0 on top of the first. The
coalesce was already doing that job; the `or` was undoing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 00:49:12 +02:00

121 lines
4.6 KiB
HTML

{% extends "admin/_layout.html" %}
{% from "_macros.html" import icon %}
{% set section = "suggestions" %}
{% block title %}Suggestions - LLeMbas{% endblock %}
{% block heading %}Suggestions{% endblock %}
{% block admin_content %}
<p class="admin-lede">
Cards on the new-chat screen. Clicking one puts its prompt in the composer
without sending it — the built-in ones deliberately end mid-sentence, so the
caret lands where the person has to start typing. The first
{{ max_shown }} enabled ones are shown, in this order.
</p>
{% if saved %}
<div class="alert alert--success">{{ icon("check", "icon--sm") }} <span>{{ saved }}</span></div>
{% endif %}
{% for suggestion in suggestions %}
<form class="card" method="post" action="/admin/suggestions/{{ suggestion.id }}">
<div class="card__header">
<h2 class="card__title">
{{ suggestion.name }}
{% if not suggestion.enabled %}<span class="badge">hidden</span>{% endif %}
{% if loop.index > max_shown and suggestion.enabled %}
<span class="badge badge--warning" title="Only the first {{ max_shown }} are shown">
below the cut
</span>
{% endif %}
</h2>
<button class="btn btn--sm btn--danger" type="submit"
formaction="/admin/suggestions/{{ suggestion.id }}/delete"
data-confirm-button="Delete the suggestion “{{ suggestion.name }}”?"
data-confirm-title="Delete suggestion">
{{ icon("trash", "icon--sm") }}
</button>
</div>
<div class="grid grid--2">
<div class="field">
<label class="field__label" for="name-{{ suggestion.id }}">Name</label>
<input class="input" id="name-{{ suggestion.id }}" name="name"
value="{{ suggestion.name }}" maxlength="120">
<p class="field__hint">The heading on the card.</p>
</div>
<div class="field">
<label class="field__label" for="position-{{ suggestion.id }}">Position</label>
<input class="input" id="position-{{ suggestion.id }}" name="position" type="number"
min="1" value="{{ suggestion.position + 1 }}">
<p class="field__hint">Order on the screen, lowest first.</p>
</div>
</div>
<div class="field">
<label class="field__label" for="description-{{ suggestion.id }}">Description</label>
<input class="input" id="description-{{ suggestion.id }}" name="description"
value="{{ suggestion.description }}" maxlength="300">
<p class="field__hint">One line under the name, saying what it is for.</p>
</div>
<div class="field">
<label class="field__label" for="prompt-{{ suggestion.id }}">Prompt</label>
<textarea class="textarea" id="prompt-{{ suggestion.id }}" name="prompt"
rows="4">{{ suggestion.prompt }}</textarea>
<p class="field__hint">
Put in the composer, not sent. Ending it mid-sentence is usually right:
the person still has to say what they are asking about.
</p>
</div>
<div class="field">
<label class="checkbox">
<input type="checkbox" name="enabled" value="true" {{ 'checked' if suggestion.enabled }}>
<span>Show this one</span>
</label>
</div>
<div class="card__footer">
<button class="btn btn--primary btn--sm" type="submit">Save</button>
</div>
</form>
{% else %}
<div class="empty">
{{ icon("sparkle", "empty__mark") }}
<h2 class="empty__title">No suggestions</h2>
<p class="empty__text">The new-chat screen shows its empty state instead.</p>
</div>
{% endfor %}
{% if not at_limit %}
<form class="card" method="post" action="/admin/suggestions">
<h2 class="card__title">Add a suggestion</h2>
<div class="field">
<label class="field__label" for="new-name">Name</label>
<input class="input" id="new-name" name="name" maxlength="120"
placeholder="Summarise a document" required>
</div>
<div class="field">
<label class="field__label" for="new-description">Description</label>
<input class="input" id="new-description" name="description" maxlength="300"
placeholder="What it is for, in one line.">
</div>
<div class="field">
<label class="field__label" for="new-prompt">Prompt</label>
<textarea class="textarea" id="new-prompt" name="prompt" rows="4"
placeholder="What lands in the composer when the card is clicked."></textarea>
</div>
<div class="card__footer">
<button class="btn btn--primary" type="submit">Add</button>
</div>
</form>
{% else %}
<p class="admin-lede">
{{ max_suggestions }} is the limit. Delete one to add another — past a dozen
this is a menu, and a menu on the empty screen is a worse blank page than a
blank page.
</p>
{% endif %}
{% endblock %}