Agent Activity Wall
Opposing columns of agent requests and results with tinted mentions and tool badges.
Put everyday work in motion.
@Agent Every Monday at 8, chase any invoice over 14 days late
Runs every Monday, and asks before sending
@Agent Reply to Cedar Park and offer Thursday or Friday
Checked the calendar and drafted the reply
@Agent Make me a sheet of every job we did for Cedar Park this year
Created the customer’s job history in a sheet
@Agent Find an available technician for tomorrow morning
Two technicians are free before 10am
@Agent Who owes us money, and who should I chase first?
Found $9,840 overdue and wrote three reminders
@Agent Post this week’s numbers in #office on Friday
Sales, costs and cash, posted every Friday at 5
@Agent Add everyone who booked through the website to HubSpot
Added 14 contacts, with their bookings
@Agent Check whether my afternoon appointment can move to Thursday
Found two open times on Thursday
@Agent What did we quote Harbourview for the rooftop unit?
Q-1031, $14,860. Raj has opened it twice
@Agent Email tomorrow’s customers their arrival window
Emailed nine customers, and logged every one
@Agent Keep an eye on new enquiries and flag the urgent ones
Watching your inbox from now on
@Agent Get the customer’s recent jobs ready for my next call
The job history and open quote are ready
'use client';
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import './agent-activity-wall.css';
export interface AgentActivityItem {
id: string;
name: string;
role: string;
initial?: string;
mention?: string;
request: string;
response: string;
color: string;
mentionColor?: string;
avatar?: ReactNode;
tools?: { id:string; label:string; logo:ReactNode }[];
}
export interface AgentActivityWallProps {
columns: AgentActivityItem[][];
title?: ReactNode;
durations?: number[];
paused?: boolean;
showControls?: boolean;
className?: string;
}
/** Opposing vertical streams of requests and completed work, with a static accessible copy. */
export function AgentActivityWall({ columns, title='Put everyday work in motion.', durations=[64,76,70], paused=false, showControls=true, className='' }: AgentActivityWallProps) {
const uid = useId();
const root = useRef<HTMLElement>(null);
const [held,setHeld] = useState(false);
const [copies,setCopies] = useState<number[]>([]);
const [environment,setEnvironment] = useState({ visible:false,hidden:false,reduced:false });
useEffect(()=>{
const element=root.current;
if (!element) return;
const media=matchMedia('(prefers-reduced-motion: reduce)');
let visible=typeof IntersectionObserver==='undefined';
const sync=()=>setEnvironment({ visible,hidden:document.hidden,reduced:media.matches });
const observer=typeof IntersectionObserver==='undefined'?null:new IntersectionObserver(([entry])=>{ visible=entry.isIntersecting;sync(); });
observer?.observe(element);
media.addEventListener('change',sync);
document.addEventListener('visibilitychange',sync);
sync();
return ()=>{observer?.disconnect();media.removeEventListener('change',sync);document.removeEventListener('visibilitychange',sync);};
},[]);
useEffect(()=>{
const element=root.current;
if (!element) return;
const measure=()=>{
const viewport=element.querySelector<HTMLElement>('.ac-agent-wall__window');
const next=Array.from(element.querySelectorAll<HTMLElement>('.ac-agent-wall__column')).map(column=>{
const height=column.querySelector<HTMLElement>('.ac-agent-wall__group')?.getBoundingClientRect().height??0;
return height>0?Math.max(2,Math.min(32,Math.ceil((viewport?.clientHeight??600)/height)+1)):2;
});
setCopies(previous=>previous.length===next.length&&previous.every((count,index)=>count===next[index])?previous:next);
};
const observer=typeof ResizeObserver==='undefined'?null:new ResizeObserver(measure);
observer?.observe(element);
element.querySelectorAll('.ac-agent-wall__group').forEach(group=>observer?.observe(group));
window.addEventListener('resize',measure);
measure();
return ()=>{observer?.disconnect();window.removeEventListener('resize',measure);};
},[columns]);
const running=environment.visible&&!environment.hidden&&!environment.reduced&&!paused&&!held;
function card(item:AgentActivityItem,copy:number) {
return <figure className="ac-agent-wall__card" key={item.id} style={{'--ac-agent-color':item.color,'--ac-agent-mention':item.mentionColor??'#342c47'} as CSSProperties}>
<figcaption><span className="ac-agent-wall__initial">{item.initial??item.name.slice(0,1)}</span><span className="ac-agent-wall__name">{item.name}</span><span className="ac-agent-wall__role">{item.role}</span></figcaption>
<blockquote><span className="ac-agent-wall__mention">{item.mention??'@Agent'}</span>{' '}{item.request}</blockquote>
<div className="ac-agent-wall__result"><span className="ac-agent-wall__avatar" aria-hidden="true">{item.avatar??<svg viewBox="0 0 24 24" fill="none"><path d="m6 12 4 4 8-9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>}</span><p>{item.response}</p>{item.tools&&item.tools.length>0&&<span className="ac-agent-wall__tools">{item.tools.map(tool=><span key={tool.id} className="ac-agent-wall__tool" title={tool.label} role={copy===0?'img':undefined} aria-label={copy===0?tool.label:undefined}>{tool.logo}</span>)}</span>}</div>
</figure>;
}
return <section ref={root} className={`ac-agent-wall ${className}`.trim()} aria-labelledby={`${uid}-title`} data-running={running}>
<div className="ac-agent-wall__header"><h2 id={`${uid}-title`}>{title}</h2></div>
<div className="ac-agent-wall__window" role="region" aria-label="Agent activity examples" tabIndex={environment.reduced?0:undefined}>
{columns.filter(column=>column.length>0).slice(0,3).map((column,index)=><div className="ac-agent-wall__column" key={index}>
<div className="ac-agent-wall__track" data-reverse={index%2===1} style={{'--ac-agent-copies':copies[index]??2,'--ac-agent-duration':`${Number.isFinite(durations[index])?Math.max(20,Math.min(300,durations[index])):64}s`} as CSSProperties}>
{Array.from({length:copies[index]??2},(_,copy)=>copy).map(copy=><div className="ac-agent-wall__group" key={copy} aria-hidden={copy>0?true:undefined}>{column.map(item=>card(item,copy))}</div>)}
</div>
</div>)}
</div>
{showControls&&<div className="ac-agent-wall__controls"><button type="button" onClick={()=>setHeld(value=>!value)} disabled={paused||environment.reduced} aria-pressed={held||paused}>{environment.reduced?'Motion reduced':held||paused?'Play motion':'Pause motion'}</button></div>}
</section>;
}
Installation
Run this in your shadcn-configured React project. The component and stylesheet are added together.
npx shadcn@latest add http://localhost:5173/r/agent-activity-wall.jsonThis command uses the current site address. For localhost, keep this preview running on the same computer. Files follow your configured components alias.
Usage
Import the component and make the details your own.
import { AgentActivityWall } from '@/components/anticraft/AgentActivityWall';
export function Example() {
return <AgentActivityWall columns={[
[{ id: 'invoice', name: 'Priya', role: 'Office', mention: '@Agent', request: 'Find the overdue invoices.', response: 'Three invoices are ready to review.', color: '#89c2f9', mentionColor: '#032f53' }],
[{ id: 'calendar', name: 'Jordan', role: 'Owner', request: 'Check the team’s availability.', response: 'Two appointments are available on Friday.', color: '#d5b970', mentionColor: '#3a2b00' }],
[{ id: 'email', name: 'Finn', role: 'Sales', request: 'Draft a customer follow-up.', response: 'The draft is ready for approval.', color: '#c0a0df', mentionColor: '#2a133d' }],
]} />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
columns | AgentActivityItem[][] | Required | Up to three columns. Each card has unique id, name, role, request, response and color; optional initial, mention, mentionColor, ReactNode avatar and tool logos. Columns repeat decoratively to fill the activity window. |
title | ReactNode | Put everyday work in motion. | Heading above the activity columns. |
durations | number[] | [64, 76, 70] | Seconds per column cycle, clamped to 20–300; the middle column moves down while the others move up. |
paused / showControls | boolean | false / true | External pause override and native pause-control visibility. |
className | string | '' | Additional classes. Customize --ac-agent-surface, --ac-agent-ink, --ac-agent-muted, --ac-agent-line, --ac-agent-avatar and --ac-agent-font. |
Motion & accessibility
Three independently moving streams with top/bottom fades and decorative duplicate cards. Pause, hidden documents and offscreen state stop motion. Reduced motion removes loops and exposes a scrollable static window. The reference composition shows two columns below 980px and one below 600px; secondary columns are hidden at those widths. Illustrative cards contain no nested interactive elements.
Use semantic content and meaningful labels in your composition. Check contrast and keyboard behavior with your own styles and content.
Inspiration
The ruled app grid, logo ribbons and opposing activity columns take their visual structure from this reference. Demo copy is independent; artwork credits are in the repository.
Visit PlyxlThe linked site is the design or content reference. See the repository for demo artwork credits.