Message
One turn. A user's question is a bubble of plain text; an assistant's answer is unbubbled prose, because a question is a remark and an answer is a document.
A question and an answer
The assistant's body is the host's rendered HTML, styled by UnmagicProse.
Done — Ana's interview is now Friday at 2pm. I've:
- moved the calendar event and kept the same video link,
- sent Ana an updated invitation, and
- let the hiring panel know.
| Who | Status |
|---|---|
| Ana Silva | Invited |
| Panel | Notified |
The event is evt_4821 if you need it.
<div class="mx-auto flex max-w-2xl flex-col gap-6">
<%= ai_chat_message "Can you move Thursday's interview with Ana to Friday?\nSame time is fine.", role: :user, id: "turns_q" %>
<%= ai_chat_message role: :assistant, id: "turns_a" do %>
<p>Done — Ana's interview is now <strong>Friday at 2pm</strong>. I've:</p>
<ol>
<li>moved the calendar event and kept the same video link,</li>
<li>sent Ana an updated invitation, and</li>
<li>let <a href="#">the hiring panel</a> know.</li>
</ol>
<table>
<thead><tr><th>Who</th><th>Status</th></tr></thead>
<tbody><tr><td>Ana Silva</td><td>Invited</td></tr><tr><td>Panel</td><td>Notified</td></tr></tbody>
</table>
<p>The event is <code>evt_4821</code> if you need it.</p>
<% end %>
</div>Thinking, stopped and empty
streaming: true shows a spinner until the first token; final: true refuses later flushes; an empty settled turn is hidden.
Response stopped.
Above: an optimistic bubble as its template renders it, before the composer fills it in. Below this line is an empty settled turn, hidden.
<div class="mx-auto flex max-w-2xl flex-col gap-6">
<%= ai_chat_message role: :assistant, id: "states_thinking", streaming: true %>
<%= ai_chat_message role: :assistant, id: "states_stopped", final: true do %>
<p class="text-neutral-500">Response stopped.</p>
<% end %>
<%= ai_chat_message role: :user, id: "states_optimistic", optimistic: { id: "x", text: "y" } %>
<p class="text-sm text-neutral-500">Above: an optimistic bubble as its template renders it, before the composer fills it in. Below this line is an empty settled turn, hidden.</p>
<%= ai_chat_message role: :assistant, id: "states_empty" %>
</div>Reasoning, actions and branches
The parts a turn can carry, in the order they render. Hover the reply to see its actions.
Thought for 7s
The brief is four pages. The panel cares about the must-haves and the salary band, so lead with those.
Must-haves: Go, Kubernetes, and on-call experience. Band: $180–210k.
<div class="mx-auto flex max-w-2xl flex-col gap-6">
<%= ai_chat_message role: :user, id: "parts_q" do |turn| %>
<% turn.attachments(align: :end) { |files| files.file "brief.pdf", size: 204_800 } %>
Summarise this brief for the hiring panel.
<% end %>
<%= ai_chat_message role: :assistant, id: "parts_a" do |turn| %>
<% turn.reasoning duration: 7 do %>
<p>The brief is four pages. The panel cares about the must-haves and the salary band, so lead with those.</p>
<% end %>
<% turn.branches { ai_chat_branch_picker index: 2, count: 3, previous: "#", next: "#" } %>
<% turn.actions do %>
<%= message_actions for: "parts_a" do |bar| %>
<% bar.copy "Must-haves: Go, Kubernetes, on-call experience." %>
<% bar.action "Try again", "#", icon: :rotate_cw, method: :post %>
<% end %>
<% end %>
<p><strong>Must-haves:</strong> Go, Kubernetes, and on-call experience. <strong>Band:</strong> $180–210k.</p>
<% end %>
</div>