Files
amc/dashboard/components/ChatMessages.js
teernisse 8224acbba7 feat(dashboard): add working indicator for active sessions
Visual feedback when an agent is actively processing:

1. **Spinner on status dots** (SessionCard.js, Modal.js)
   - Status dot gets a spinning ring animation when session is active/starting
   - Uses CSS border trick with transparent borders except top

2. **Working indicator in chat** (ChatMessages.js, Modal.js)
   - Shows at bottom of conversation when agent is working
   - Bouncing dots animation ("...") next to "Agent is working" text
   - Only visible for active/starting statuses

3. **CSS animations** (styles.css)
   - spin-ring: 0.8s rotation for the status dot border
   - bounce-dot: staggered vertical bounce for the working dots

4. **Status metadata** (status.js)
   - Added `spinning: true` flag for active and starting statuses
   - Used by components to conditionally render spinner elements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 15:20:26 -05:00

82 lines
2.7 KiB
JavaScript

import { html, useRef, useEffect } from '../lib/preact.js';
import { getUserMessageBg, getStatusMeta } from '../utils/status.js';
import { MessageBubble, filterDisplayMessages } from './MessageBubble.js';
export function ChatMessages({ messages, status }) {
const statusMeta = getStatusMeta(status);
const containerRef = useRef(null);
const userBgClass = getUserMessageBg(status);
const wasAtBottomRef = useRef(true);
const prevMessagesLenRef = useRef(0);
// Scroll to bottom on initial mount
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// Always scroll to bottom on first render
container.scrollTop = container.scrollHeight;
}, []);
// Check if scrolled to bottom before render
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const checkScroll = () => {
const threshold = 50;
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < threshold;
wasAtBottomRef.current = isAtBottom;
};
container.addEventListener('scroll', checkScroll);
return () => container.removeEventListener('scroll', checkScroll);
}, []);
// Scroll to bottom on new messages if user was at bottom
useEffect(() => {
const container = containerRef.current;
if (!container || !messages) return;
const hasNewMessages = messages.length > prevMessagesLenRef.current;
prevMessagesLenRef.current = messages.length;
if (hasNewMessages && wasAtBottomRef.current) {
container.scrollTop = container.scrollHeight;
}
}, [messages]);
if (!messages || messages.length === 0) {
return html`
<div class="flex h-full items-center justify-center rounded-xl border border-dashed border-selection/70 bg-bg/30 px-4 text-center text-sm text-dim">
No messages yet
</div>
`;
}
const displayMessages = filterDisplayMessages(messages).slice(-20);
return html`
<div class="flex h-full flex-col">
<div ref=${containerRef} class="flex-1 space-y-2.5 overflow-y-auto overflow-x-hidden pr-0.5">
${displayMessages.map((msg, i) => html`
<${MessageBubble}
key=${i}
msg=${msg}
userBg=${userBgClass}
compact=${true}
/>
`)}
</div>
${statusMeta.spinning && html`
<div class="shrink-0 flex items-center gap-2 pt-2.5 pb-0.5 pl-1">
<span class="h-2 w-2 rounded-full spinner-dot" style=${{ backgroundColor: statusMeta.borderColor, color: statusMeta.borderColor }}></span>
<span class="working-dots font-mono text-xs" style=${{ color: statusMeta.borderColor }}>
<span>.</span><span>.</span><span>.</span>
</span>
</div>
`}
</div>
`;
}