{% from "_macros.html" import icon %} {# The composer, used both inside an existing chat and on /chat where no chat row exists yet. The only difference is where it posts. With a chat, the turn is appended to the thread in place; without one, /api/chats/start creates the chat and redirects, and the reply streams on arrival because the page renders the unfinished assistant message with its sse-connect. That is what stops an opened-and-abandoned chat ever being written to the database. The attachment chips live INSIDE this form on purpose. Each carries a hidden file_ids input, and being inside the form is what gets them serialised with the message. Keeping them outside and reaching for hx-include does not work: that attribute only has an effect on the element issuing the request. #}
{% if can.get("files.upload") %} {# Outside the form: it is only ever read by JavaScript, and inside it would be submitted as an empty file part on every message. Two inputs rather than one whose accept attribute is rewritten: changing accept and then calling click() in the same tick is unreliable in Safari, and two hidden inputs cost nothing. #} {% endif %}
{% if not chat and current_model %} {% endif %} {% if not chat and starting_temporary %} {% endif %}
{% if can.get("files.upload") %} {# A menu rather than the file picker straight away: there are four ways to attach something now, and only one of them is a file on disk. Uses the same picker machinery as the model chooser -- see ui.js. #}
{% endif %} {% if can_dictate %} {# Recording is started and stopped by the same button; audio.js swaps data-mic-state and the icon with it. #} {% endif %} {# One button, two jobs. While a reply is being written it becomes Stop, because that is where the hand already is and a second button sitting permanently beside Send is clutter that is wrong most of the time. ui.js flips data-composer-action, and the type with it: as `submit` the form's own handler sends, as `button` the click handler stops. Both icons are rendered here and chosen in CSS, so the swap costs no layout and cannot flash an empty button. #}

Enter to send, Shift+Enter for a new line. {% if can.get("files.upload") %} Drag files in, or paste an image. {% if current_model and not current_model.capabilities_json.get("vision") %} {{ current_model.label }} has no vision, so images will not be sent — documents still will. {% endif %} {% endif %}

{% if can.get("files.upload") %} {% endif %}