Files
LLeMbas/src/lembas/web/templates/chat/index.html
T
Jaroslav Beneš 50484b2483 One thread template, one sidebar toggle
chat/index.html and chat/_thread.html held the same loop, so anything added
to the conversation -- a compaction divider, say -- would have had to be
written into both and kept in step by hand. index.html includes the partial
instead.

The sidebar toggle was a raw inline onclick, the only one left in the
application. app.js already delegates [data-toggle="#selector"] and gives
open/close, aria-expanded and an is-active button state for free; the chat
settings gear has used it all along. Also deletes the
.sidebar[data-collapsed="true"] rule, which nothing has ever set.

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

176 lines
7.1 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">
{% 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>
</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 %}