{% extends "base.html" %} {% from "_macros.html" import icon, mark, model_avatar %} {% block title %}{{ chat.title if chat else "New chat" }} - LLeMbas{% endblock %} {% block head %} {% if terminal_enabled %} {% endif %} {% endblock %} {% block body_attrs %} data-authenticated="true"{% endblock %} {% block body %}
{% include "partials/sidebar.html" %}

{{ chat.title if chat else "New chat" }} {# Rename, where the name is. A themed dialog through `data-prompt` rather than an inline field: the heading is in a flex row beside the badges and the connection chip, and swapping it for a text box moves all of them. The response is the same out-of-band pair the `done` frame sends, so the sidebar row follows without a second request. #} {% if chat %} {% endif %} {# Beside the title because it describes the chat rather than acting on it. The way out of it is Keep, in the overflow menu. #} {% if chat and chat.temporary %} Temporary {% endif %}

{# Where this chat runs. Beside the title because it describes the chat and cannot be changed -- update_chat refuses the connection and the directory with a 409 -- so it is of a kind with the Temporary badge rather than with the controls on the right. The mode is the one thing here that moves, and it stays down by the message box. #} {% if chat and chat.kind == "agent" %} {{ icon("bolt", "icon--sm") }} {{ agent_profile.name if agent_profile else "connection missing" }} {{ chat.project_dir }} {% endif %}
{# A link, not a script: the flag lives in the URL, so it survives a reload and can be bookmarked. #} {% if not chat and can.get("chat.create") %} {{ icon("clock") }} {% endif %} {% if models %} {% if can.get("chat.model_select") or not chat %} {% include "chat/_model_picker.html" %} {% elif current_model %} {{ current_model.label }} {% endif %} {% endif %} {% if canvas_enabled %} {# Nearest the conversation of the three, being the widest and the one most likely to be open beside it. All three share one slot: at 1280px the sidebar plus two panels leaves about seventy pixels of chat. #} {% endif %} {% if terminal_enabled %} {# To the left of the inspector, and never open beside it: see the toggle group in app.js. #} {% endif %} {% if chat and user.is_admin %} {% endif %} {# Everything else about this chat, behind one button. The bar carried eight controls, which is a toolbar nobody can read at a glance. What is left in it is what you reach for while writing -- the model, and the two panels. These three are occasional, and every one of them is also a slash command now. #} {% if chat %}
{% endif %}
{% 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. #} {% endif %} {% if not models %} {# Every fresh install lands here, so it points at the fix. #}
{{ icon("server", "empty__mark") }}

No models available

{% 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 %}

{% if user.is_admin %} {{ icon("server", "icon--sm") }} Set up a connection {% endif %}
{% else %}
{% if not messages %}
{{ mark(cls="empty__mark", uid="intro") }}

What would you ask?

Speak, friend, and enter.

{# 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 %}
{% for suggestion in suggestions %} {% endfor %}
{% endif %}
{% 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" %}
{% if chat %} {# Where a reply that started outside a request arrives. A background job finishing wakes the chat server-side and there is no channel to say so: the only stream is per-message, opened by a bubble this page has not got. See `GET /api/chats/{id}/tail`, and `app.js` for the cursor -- which is read from the DOM on `htmx:configRequest`, not rendered here, because the DOM is the honest answer to what this page already holds. OUTSIDE `#thread`, not in it: a rewind and a compaction both swap `chat/_thread.html` into that container with `innerHTML`, and a poller living inside would be swapped away by the first one and never fire again. OUTSIDE the composer's form as well, and it names its own target regardless -- htmx inherits `hx-target`, the form carries `#thread`, and the jobs chip has already demonstrated once what an unstated target does to a transcript. `hx-sync="this:drop"` because a poll fires whether or not the last one has come back, and two answers computed from the same cursor are two copies of one bubble. #} {% endif %} {% include "chat/_composer.html" %} {% endif %}
{# The panels, mirroring the sidebar opposite, in the order they sit on screen: the canvas nearest the conversation, then the terminal, then the inspector. Only ever one of them is open -- see the toggle group. #} {% if canvas_enabled %} {% include "chat/_canvas.html" %} {% endif %} {% if terminal_enabled %} {% include "chat/_terminal.html" %} {% endif %} {% if chat and user.is_admin %} {% include "chat/_inspector.html" %} {% endif %}
{% endblock %} {% block scripts %} {# Unconditional: every chat has a transcript, and this is what keeps a block somebody opened open across the swaps that arrive twelve times a second. #} {% if not chat and (canvas_enabled or terminal_enabled) %} {# Only where there is no chat yet. It points both panels at a draft id for whatever the composer has selected, and does nothing at all once a chat exists -- which is every other page this block renders on. #} {% endif %} {% if canvas_enabled %} {% endif %} {% if terminal_enabled %} {# Only where it can be used. xterm is nearly three times everything else vendored, so a plain chat must never load it. #} {% endif %} {% endblock %}