{% from "_macros.html" import icon, mark %} {# One message bubble, in either of two states. An incomplete assistant message renders the streaming shell: it carries the sse-connect that opens the reply stream. This is deliberately the ONLY thing that starts a generation, which means a page load showing an unfinished reply picks it up again -- reloading after a dropped connection retries rather than leaving a permanently half-written answer. A complete message renders its finished body: Markdown for the assistant, escaped plain text for everyone else. #} {% set streaming = (message.role == "assistant" and not message.complete) %}
{{ "LLeMbas" if message.role == "assistant" else (user.name or "You") }} {% if message.model_id %} {{ message.model_id }} {% endif %}
{% if streaming %} {# Reasoning arrives before the answer, so this block sits above it. It starts open (watching a model think is the point) and the :has() rule in chat.css hides the whole thing while it is still empty, so models that emit no reasoning never show an empty box. #}
{{ icon("sparkle", "icon--sm reasoning__icon") }} Thinking {{ icon("chevron-down", "icon--sm reasoning__chevron") }}
{# Tokens are appended here as they arrive. The cursor is a CSS pseudo-element on the empty parent, so it disappears by itself once the first token lands. #}
{% elif message.reasoning and not message.error %} {# Collapsed once finished: the answer is what the reader came for, and the thinking is there if they want to audit it. #}
{{ icon("sparkle", "icon--sm reasoning__icon") }} {% if message.reasoning_ms %} Thought for {{ message.reasoning_ms | duration }} {% else %} Reasoning {% endif %} {{ icon("chevron-down", "icon--sm reasoning__chevron") }}
{{ message.reasoning }}
{{ body_html|safe }}
{% elif message.error %} {% if message.content %}
{{ body_html|safe }}
{% endif %} {% elif message.role == "assistant" %}
{{ body_html|safe }}
{% else %}
{{ message.content }}
{% endif %} {% if not streaming %}
{% if message.role == "assistant" %} {% endif %}
{# The raw source, so the copy button yields Markdown rather than rendered text. A hidden div and not a