116 lines
5.7 KiB
HTML
116 lines
5.7 KiB
HTML
{{ define "chat_response" }}
|
|
<!-- The user's message, appended immediately (no streaming needed). -->
|
|
<div class="message-row role-user">
|
|
<div class="message-content">
|
|
<div class="bubble">{{ .UserPromptHTML }}</div>
|
|
</div>
|
|
</div>
|
|
{{ template "assistant_stream" . }}
|
|
{{ end }}
|
|
|
|
{{/*
|
|
The assistant's reply streams in over Server-Sent Events using the htmx
|
|
SSE extension (htmx-ext-sse). Shared by "chat_response" (a reply that just
|
|
started) and "index" (re-attaching to a run that is still in flight after a
|
|
page reload). Data: any value with .StreamID and .CSRFToken.
|
|
|
|
Event flow (see internal/handlers/chat.go StreamChat):
|
|
"tool" -> a fragment wrapped in hx-swap-oob="beforebegin:#assistant-row-<id>"
|
|
so each tool-call card is inserted ABOVE this row, in the order
|
|
the agent ran them, while the bubble below keeps streaming.
|
|
"deliverable" -> a Download-button card for a file a tool call just created
|
|
or changed (create_document, edit_spreadsheet, ...), sent right
|
|
after that tool call's "tool" event rather than waiting for
|
|
"done" — same beforebegin OOB placement, so the button shows up
|
|
as soon as the document exists even if the agent keeps working
|
|
afterwards. See handlers.liveDeliverable / message.html's
|
|
"deliverable_oob".
|
|
"thinking_round" -> same OOB-insertion mechanism as "tool", one per
|
|
completed round's own reasoning trace (see
|
|
agentrun.Callbacks.OnThinkingRound) — gives each round's
|
|
thinking its own card, positioned right before the tool
|
|
card it led to, instead of only ever being visible as
|
|
the single live "thinking" blob below.
|
|
"thinking" -> the model's reasoning so far, shown live in an
|
|
auto-expanded card (open by default — see .thinking-card
|
|
below) so it's visible AS the model thinks, the same way
|
|
it streams before the reply on claude.ai. app.js times
|
|
how long it stayed open and, on the first "token"/"done"
|
|
it sees afterwards, collapses the card to a "Thought for
|
|
Ns" summary — a pure enhancement: with JS disabled the
|
|
card just stays open and unlabeled, still fully readable.
|
|
"token" -> the reply so far (HTML-escaped), swapped into the bubble.
|
|
"done" -> the final Markdown-rendered reply. sse-close="done" closes the
|
|
EventSource; the trailing .stream-done marker hides Stop and
|
|
(via CSS :has()) stops the thinking icon's pulse animation.
|
|
*/}}
|
|
{{ define "assistant_stream" }}
|
|
<div
|
|
class="message-row role-assistant"
|
|
id="assistant-row-{{ .StreamID }}"
|
|
hx-ext="sse"
|
|
sse-connect="/chat/stream/{{ .StreamID }}"
|
|
sse-close="done"
|
|
data-stream-id="{{ .StreamID }}"
|
|
>
|
|
<span class="avatar avatar-assistant" aria-hidden="true">
|
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 21c1.2-2.5 1.5-5.5 1.3-8.5-.5-1.2-1.2-2.4-2.1-3.5C3.5 9 2 7.8 2 6.2 2 4.6 3.5 3.5 5.2 3.5c.8 0 1.5.3 2.1.8C8.5 3 10.1 2.2 12 2.2c1.9 0 3.5.8 4.7 2.1.6-.5 1.3-.8 2.1-.8 1.7 0 3.2 1.1 3.2 2.7 0 1.6-1.5 2.8-3.2 2.8-.9 1.1-1.6 2.3-2.1 3.5-.2 3 .1 6 1.3 8.5H6Zm4-12c.5 1.8 1.2 3.5 2 3.5s1.5-1.7 2-3.5h-4Z"/>
|
|
</svg>
|
|
</span>
|
|
<div class="message-content">
|
|
<div class="message-meta">
|
|
<span class="meta-name">enkly</span>
|
|
<form method="post" action="/chat/stream/{{ .StreamID }}/stop" class="stop-form" data-role="stop-form">
|
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}" />
|
|
<button type="submit" class="stop-btn" aria-label="Stop generating">
|
|
<span class="stop-icon" aria-hidden="true"></span> Stop
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<details class="thinking-card" open>
|
|
<summary>
|
|
<span class="thinking-icon" aria-hidden="true"></span>
|
|
<span class="thinking-label">Thinking</span>
|
|
<span class="thinking-elapsed"></span>
|
|
</summary>
|
|
<div class="thinking-body" sse-swap="thinking" hx-swap="innerHTML"></div>
|
|
</details>
|
|
<div
|
|
class="bubble"
|
|
id="assistant-bubble-{{ .StreamID }}"
|
|
sse-swap="token,done"
|
|
hx-swap="innerHTML"
|
|
>
|
|
<span class="typing-dots" aria-label="enkly is responding"><span></span><span></span><span></span></span>
|
|
</div>
|
|
</div>
|
|
<!--
|
|
"tool" events carry a wrapper with hx-swap-oob (see message.html
|
|
"tool_call_oob"). This listener element's own swap is "none" — htmx still
|
|
extracts and places the out-of-band content.
|
|
-->
|
|
<div sse-swap="tool" hx-swap="none" hidden></div>
|
|
<!--
|
|
"deliverable" events carry a wrapper with hx-swap-oob (see
|
|
deliverables.html "deliverable_oob"), same beforebegin placement as
|
|
"tool" above. This listener element's own swap is "none" — htmx still
|
|
extracts and places the out-of-band content.
|
|
-->
|
|
<div sse-swap="deliverable" hx-swap="none" hidden></div>
|
|
<!--
|
|
"thinking_round" events — one per agent round's own standalone
|
|
reasoning trace, each its own OOB card (see message.html
|
|
"thinking_round_oob") inserted above this row in execution order,
|
|
same mechanism as "tool" above. This is in addition to, not a
|
|
replacement for, the single live-updating .thinking-card above:
|
|
that one shows reasoning AS it streams in for the round currently
|
|
in flight; these are the settled, per-step record of every round
|
|
once it's done, interleaved with the tool cards the same way
|
|
Claude's interleaved thinking blocks sit next to the tool use they
|
|
preceded.
|
|
-->
|
|
<div sse-swap="thinking_round" hx-swap="none" hidden></div>
|
|
</div>
|
|
{{ end }}
|