50484b2483
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>
176 lines
7.1 KiB
HTML
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 %}
|