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).
|
- Model: `/model` output now includes auth source location (env/auth.json/models.json).
|
||||||
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
|
- Docs: clarify auth storage, migration, and OpenAI Codex OAuth onboarding.
|
||||||
- Sandbox: copy inbound media into sandbox workspaces so agent tools can read attachments.
|
- 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 runtime (docker/direct) and move shortcuts to `/help`.
|
||||||
- Status: show model auth source (api-key/oauth).
|
- 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 {
|
.chat-text {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|||||||
@@ -78,15 +78,17 @@ export function renderChat(props: ChatProps) {
|
|||||||
<div class="chat-thread" role="log" aria-live="polite">
|
<div class="chat-thread" role="log" aria-live="polite">
|
||||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
||||||
${props.messages.map((m) => renderMessage(m))}
|
${props.messages.map((m) => renderMessage(m))}
|
||||||
${props.stream
|
${props.stream !== null
|
||||||
? renderMessage(
|
? props.stream.trim().length > 0
|
||||||
{
|
? renderMessage(
|
||||||
role: "assistant",
|
{
|
||||||
content: [{ type: "text", text: props.stream }],
|
role: "assistant",
|
||||||
timestamp: Date.now(),
|
content: [{ type: "text", text: props.stream }],
|
||||||
},
|
timestamp: Date.now(),
|
||||||
{ streaming: true },
|
},
|
||||||
)
|
{ streaming: true },
|
||||||
|
)
|
||||||
|
: renderReadingIndicator()
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -171,6 +173,20 @@ function resolveSessionOptions(
|
|||||||
return result;
|
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 }) {
|
function renderMessage(message: unknown, opts?: { streaming?: boolean }) {
|
||||||
const m = message as Record<string, unknown>;
|
const m = message as Record<string, unknown>;
|
||||||
const role = typeof m.role === "string" ? m.role : "unknown";
|
const role = typeof m.role === "string" ? m.role : "unknown";
|
||||||
|
|||||||
Reference in New Issue
Block a user