26793b1317
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>
218 lines
9.2 KiB
HTML
218 lines
9.2 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_macros.html" import icon, mark, model_avatar %}
|
|
|
|
{% block title %}{{ chat.title if chat else "New chat" }} - LLeMbas{% endblock %}
|
|
|
|
{% block head %}
|
|
<link rel="stylesheet" href="{{ url_for('static', path='css/chat.css') }}">
|
|
{% endblock %}
|
|
|
|
{% block body_attrs %} data-authenticated="true"{% endblock %}
|
|
|
|
{% block body %}
|
|
<div class="shell">
|
|
{% include "partials/sidebar.html" %}
|
|
|
|
<main class="main">
|
|
<header class="topbar">
|
|
<button class="btn btn--icon" type="button" aria-label="Toggle sidebar"
|
|
aria-expanded="true" data-toggle="#sidebar">
|
|
{{ icon("sidebar") }}
|
|
</button>
|
|
|
|
<h1 class="topbar__title">
|
|
<span id="chat-title">{{ chat.title if chat else "New chat" }}</span>
|
|
</h1>
|
|
|
|
<div class="topbar__actions">
|
|
{#
|
|
A link, not a script: the flag lives in the URL, so it survives a
|
|
reload and can be bookmarked. On an existing temporary chat the same
|
|
corner explains what temporary means and offers the way out -- without
|
|
one, a conversation that turns out to matter is destroyed a day later
|
|
with no recourse.
|
|
#}
|
|
{% if chat and chat.temporary %}
|
|
<span class="badge badge--warning"
|
|
title="Not listed in the sidebar, and removed 24 hours after the last message.">
|
|
Temporary
|
|
</span>
|
|
<button class="btn btn--sm" type="button"
|
|
hx-post="/api/chats/{{ chat.id }}/keep" hx-swap="none"
|
|
title="Keep this chat and list it in the sidebar">
|
|
{{ icon("pin", "icon--sm") }} Keep
|
|
</button>
|
|
{% elif not chat and can.get("chat.create") %}
|
|
<a class="btn btn--icon {{ 'is-active' if starting_temporary }}"
|
|
href="{{ '/chat' if starting_temporary else '/chat?temporary=1' }}"
|
|
aria-label="Temporary chat"
|
|
title="{% if starting_temporary %}Starting a temporary chat. Click to go back to a normal one.{% else %}Start a temporary chat: not listed in the sidebar, and removed after a day.{% endif %}">
|
|
{{ icon("clock") }}
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if models %}
|
|
{% if can.get("chat.model_select") or not chat %}
|
|
{% include "chat/_model_picker.html" %}
|
|
{% elif current_model %}
|
|
<span class="badge">{{ current_model.label }}</span>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if chat and (can.get("chat.system_prompt") or can.get("chat.params")) %}
|
|
<button class="btn btn--icon" type="button" aria-label="Chat settings"
|
|
title="Chat settings" data-toggle="#chat-settings">
|
|
{{ icon("sliders") }}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</header>
|
|
|
|
{% if chat and (can.get("chat.system_prompt") or can.get("chat.params")) %}
|
|
{# Collapsed by default: per-chat overrides, not everyday controls. Each
|
|
field saves on change, so there is no half-applied state. #}
|
|
<section class="panel" id="chat-settings" hidden>
|
|
<div class="panel__inner">
|
|
{% if current_model and current_model.description %}
|
|
<p class="panel__note">{{ current_model.description }}</p>
|
|
{% endif %}
|
|
|
|
{% if can.get("chat.system_prompt") %}
|
|
<div class="field">
|
|
<label class="field__label" for="system-prompt">System prompt</label>
|
|
<textarea class="textarea" id="system-prompt" name="system_prompt" rows="3"
|
|
placeholder="{{ inherited_prompt or 'Instructions that apply to every message in this chat.' }}"
|
|
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none"
|
|
hx-trigger="change">{{ chat.system_prompt }}</textarea>
|
|
<p class="field__hint">
|
|
{% if inherited_prompt %}
|
|
Leave empty to use the {{ inherited_from }} prompt shown above.
|
|
{% else %}
|
|
Overrides the model and instance prompts for this chat only.
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if knowledge_bases %}
|
|
{# Which bases this chat draws on. None ticked means everything you can
|
|
see, which is what a chat with nothing chosen should do. #}
|
|
<div class="field">
|
|
<label class="field__label">Knowledge</label>
|
|
<form hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
|
{# Always submitted, so unticking the last box still says something.
|
|
An absent checkbox carries no signal of its own. #}
|
|
<input type="hidden" name="knowledge_base_ids" value="">
|
|
<div class="checkbox-row">
|
|
{% for base in knowledge_bases %}
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="knowledge_base_ids" value="{{ base.id }}"
|
|
{{ 'checked' if base.id in attached_base_ids }}>
|
|
<span>{{ base.name }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
</form>
|
|
<p class="field__hint">
|
|
{% if attached_base_ids %}
|
|
Searches in this chat are limited to what is ticked.
|
|
{% else %}
|
|
Nothing ticked, so this chat can search everything in your
|
|
<a href="/library/knowledge">library</a>.
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can.get("chat.params") %}
|
|
<div class="grid grid--3">
|
|
<div class="field">
|
|
<label class="field__label" for="temperature">Temperature</label>
|
|
<input class="input" id="temperature" name="temperature" type="number"
|
|
min="0" max="2" step="0.05" placeholder="default"
|
|
value="{{ chat.params_json.get('temperature') if chat.params_json.get('temperature') is not none else '' }}"
|
|
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field__label" for="top-p">Top-p</label>
|
|
<input class="input" id="top-p" name="top_p" type="number"
|
|
min="0" max="1" step="0.05" placeholder="default"
|
|
value="{{ chat.params_json.get('top_p') if chat.params_json.get('top_p') is not none else '' }}"
|
|
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
|
</div>
|
|
<div class="field">
|
|
<label class="field__label" for="max-tokens">Max tokens</label>
|
|
<input class="input" id="max-tokens" name="max_tokens" type="number"
|
|
min="1" step="1" placeholder="default"
|
|
value="{{ chat.params_json.get('max_tokens') if chat.params_json.get('max_tokens') is not none else '' }}"
|
|
hx-patch="/api/chats/{{ chat.id }}" hx-swap="none" hx-trigger="change">
|
|
</div>
|
|
</div>
|
|
<p class="field__hint">
|
|
Leave a field empty to let the provider decide. Values outside the
|
|
allowed range are ignored rather than clamped.
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{% if not models %}
|
|
{# Every fresh install lands here, so it points at the fix. #}
|
|
<div class="empty">
|
|
{{ icon("server", "empty__mark") }}
|
|
<h2 class="empty__title">No models available</h2>
|
|
<p class="empty__text">
|
|
{% if user.is_admin %}
|
|
Add an OpenAI-compatible connection and LLeMbas will load its models.
|
|
{% else %}
|
|
No model connections have been set up yet. Ask an administrator.
|
|
{% endif %}
|
|
</p>
|
|
{% if user.is_admin %}
|
|
<a class="btn btn--primary" href="/admin/connections">
|
|
{{ icon("server", "icon--sm") }} Set up a connection
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% else %}
|
|
<div class="thread-scroll" id="thread-scroll">
|
|
<div class="thread" id="thread">
|
|
{% if not messages %}
|
|
<div class="thread__intro">
|
|
{{ mark(cls="empty__mark", uid="intro") }}
|
|
<h2 class="empty__title">What would you ask?</h2>
|
|
<p class="empty__text">Speak, friend, and enter.</p>
|
|
|
|
{# Only on a chat that does not exist yet. An empty chat someone
|
|
opened on purpose already has a model and a prompt chosen. #}
|
|
{% if not chat and suggestions %}
|
|
<div class="suggestions">
|
|
{% for suggestion in suggestions %}
|
|
<button class="suggestion" type="button"
|
|
data-suggestion="{{ suggestion.prompt }}">
|
|
<span class="suggestion__name">{{ suggestion.name }}</span>
|
|
{% if suggestion.description %}
|
|
<span class="suggestion__note">{{ suggestion.description }}</span>
|
|
{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# The same include the rewind response returns, so the conversation
|
|
is described in one place. Markdown was rendered server-side in
|
|
pages.py, keyed by message id. #}
|
|
{% include "chat/_thread.html" %}
|
|
</div>
|
|
</div>
|
|
|
|
{% include "chat/_composer.html" %}
|
|
{% endif %}
|
|
</main>
|
|
</div>
|
|
{% endblock %}
|