Files
enkly-ai/services/gateway/web/templates/chat_response.html
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

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