feat(ui): add chat reading indicator

This commit is contained in:
Peter Steinberger
2026-01-05 16:16:27 +00:00
parent 7f3f73af1c
commit 234059811c
3 changed files with 76 additions and 9 deletions

View File

@@ -17,6 +17,7 @@
- Model: `/model` output now includes auth source location (env/auth.json/models.json).
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
- Control UI: show a reading indicator bubble while the assistant is responding.
- Status: show runtime (docker/direct) and move shortcuts to `/help`.
- Status: show model auth source (api-key/oauth).

View File

@@ -595,6 +595,56 @@
}
}
.chat-bubble.chat-reading-indicator {
width: fit-content;
padding: 10px 14px;
}
.chat-reading-indicator__dots {
display: inline-flex;
align-items: center;
gap: 6px;
height: 10px;
}
.chat-reading-indicator__dots > span {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--chat-text);
opacity: 0.55;
transform: translateY(0);
animation: chatReadingDot 1.1s ease-in-out infinite;
}
.chat-reading-indicator__dots > span:nth-child(2) {
animation-delay: 0.12s;
}
.chat-reading-indicator__dots > span:nth-child(3) {
animation-delay: 0.24s;
}
@keyframes chatReadingDot {
0%,
80%,
100% {
opacity: 0.45;
transform: translateY(0);
}
40% {
opacity: 0.95;
transform: translateY(-2px);
}
}
@media (prefers-reduced-motion: reduce) {
.chat-reading-indicator__dots > span {
animation: none;
opacity: 0.75;
}
}
.chat-text {
overflow-wrap: anywhere;
word-break: break-word;

View File

@@ -78,15 +78,17 @@ export function renderChat(props: ChatProps) {
<div class="chat-thread" role="log" aria-live="polite">
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
${props.messages.map((m) => renderMessage(m))}
${props.stream
? renderMessage(
{
role: "assistant",
content: [{ type: "text", text: props.stream }],
timestamp: Date.now(),
},
{ streaming: true },
)
${props.stream !== null
? props.stream.trim().length > 0
? renderMessage(
{
role: "assistant",
content: [{ type: "text", text: props.stream }],
timestamp: Date.now(),
},
{ streaming: true },
)
: renderReadingIndicator()
: nothing}
</div>
@@ -171,6 +173,20 @@ function resolveSessionOptions(
return result;
}
function renderReadingIndicator() {
return html`
<div class="chat-line assistant">
<div class="chat-msg">
<div class="chat-bubble chat-reading-indicator" aria-hidden="true">
<span class="chat-reading-indicator__dots">
<span></span><span></span><span></span>
</span>
</div>
</div>
</div>
`;
}
function renderMessage(message: unknown, opts?: { streaming?: boolean }) {
const m = message as Record<string, unknown>;
const role = typeof m.role === "string" ? m.role : "unknown";