feat(ui): add chat reading indicator
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user