mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[a360b6e3] Redesign A2A page with conversation-first layout and agent identity (#401)
* [54b94e44] A2A page: filter controls + agent identity consistency (#387) (#392) * [54b94e44] feat(a2a): add filter bar and unify agent avatars + pulse across views Adds a status (active/all) + free-text search filter bar above the A2A switchboard/list content, backed by a shared a2a-filter-utils module so both A2ASwitchboard's pairs and A2AConversationList's conversations narrow identically. Extracts A2APairCard's pulse-flash state into a reusable usePulseFlash hook and exports its PairAvatar so the classic conversation list now renders the same two-participant avatar and emerald pulse-flash affordance the switchboard already had. * [54b94e44] docs(a2a): add comprehensive filtering and avatar documentation Documented the new A2A filter bar, filter utilities, pulse-flash hook, and conversation list API changes. Includes examples, testing guidance, and migration notes for the pulses prop requirement. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [54417f0c] UX/UI: design A2A conversation-first layout and agent identity (#399) * [f612a5ab] Add conversation-first layout, agent identity, and live-stream affordance spec (#384) Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> * [7ed2ef71] docs(ux_ui): add filter-control design spec for A2A conversations (#383) Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> --------- Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> * [f563bbc9] Implement conversation-first A2A layout, identity colors, connection states, transcript motion, and empty/error states (#423) (#427) * [f563bbc9] feat(a2a): conversation-first layout, team-color identity, connection states, transcript motion, empty/error states Implements docs/ux_ui/design/02-conversation-first-layout-agent-identity-live-stream.md: - xl:+ collapsible Context pane (identity cards, linked-task summary, no-task hint), persisted via the existing zustand ui-store - getAgentTeamColor + TEAM_COLOR_CLASSES in agent-utils.ts, applied to PairAvatar, the transcript row avatar, and the context pane - A2AConnectionBadge/A2AConnectionBanner rendering all four ConnectionState values distinctly with a motion-reduce-guarded pulsing dot and a dismissable reconnecting/disconnected strip - A2ATranscript: transform/opacity-only new-row entrance transition, scrolled-up "New messages" pill, split hasSelection/empty/error states with a scoped Retry - Unit tests for every new pure helper and component * [f563bbc9] docs(a2a): conversation-first layout, team-color identity, connection states, transcript motion, empty/error states Document the new conversation-first A2A layout features: - Agent team-color system (getAgentTeamColor, TEAM_COLOR_CLASSES) for six cell buckets - A2AContextPane component with identity cards, linked task summary, no-task hint - Connection state rendering (A2AConnectionBadge, A2AConnectionBanner) for all four ConnectionState values - Transcript entrance motion with transform/opacity-only transitions and prefers-reduced-motion guards - Split empty/error states (no selection, no messages, fetch error with scoped retry) - Page-level integration with xl:+ responsive grid layout Includes component API, usage examples, testing guidance, accessibility notes, and design rationale. --------- Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> * [478f027c] Implement A2A conversations filter control per conversations-filter-control.md (#445) (#448) * [478f027c] feat(a2a): add multi-dimension Popover filter panel for A2A conversations Replace the free-text search + active/all toggle with the Popover-triggered filter control from conversations-filter-control.md: Agent multi-select checkboxes, a Task id-fragment input with a "No linked task" toggle, Status toggle buttons, and a date range, plus an active-filter chip row and Clear all. Filtering applies to both the switchboard (Agent only) and conversation list (all four dimensions) per the design doc's per-view rules. * [478f027c] docs(a2a): add comprehensive filter-control guide covering component API, filter dimensions, and per-view rules Documents A2AFilterBar component and filter utilities with: - Component API and props - All 4 filter dimensions (Agent, Task, Status, Date range) - Per-view rules (Switchboard vs List) - Usage examples and parent setup - Filter logic and match predicates - Testing guide and accessibility notes - Design notes on client-side filtering limitation - Links to related components and the design spec Helps developers understand, use, and maintain the A2A conversations filter control without needing to read the design doc or component source. --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> --------- Co-authored-by: Frontend Developer 1 <fe-dev-1@roboco.tech> Co-authored-by: Frontend Documenter <fe-doc@roboco.tech> Co-authored-by: UX/UI Developer 1 <ux-dev-1@roboco.tech> Co-authored-by: UX/UI Developer 2 <ux-dev-2@roboco.tech> Co-authored-by: Frontend Developer 2 <fe-dev-2@roboco.tech> Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
co-authored by
Frontend Developer 1
Frontend Documenter
UX/UI Developer 1
UX/UI Developer 2
Frontend Developer 2
Renn F
parent
58354a364e
commit
6e57066bd6
@@ -0,0 +1,303 @@
|
||||
# A2A Page Filtering & Agent Identity
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A (Agent-to-Agent) page now features a unified filter bar and consistent agent identity rendering across both the switchboard (org-chart view) and classic conversation list. Both views respond identically to filtering and pulse animations, creating a cohesive experience regardless of the active view.
|
||||
|
||||
## Filter Bar
|
||||
|
||||
The `A2AFilterBar` component renders above the switchboard/list content and provides two independent filtering controls:
|
||||
|
||||
### Component
|
||||
|
||||
**File:** `panel/src/components/a2a/a2a-filter-bar.tsx`
|
||||
|
||||
**Props:**
|
||||
|
||||
- `status` (`A2AStatusFilter`): Current status filter, either `"active"` or `"all"`
|
||||
- `onStatusChange` (callback): Fires when the Active/All toggle changes
|
||||
- `search` (string): Current search query
|
||||
- `onSearchChange` (callback): Fires as the user types in the search input
|
||||
|
||||
**Rendering:**
|
||||
|
||||
- Search input with placeholder "Search agent or topic..." accepts free-text queries
|
||||
- Two toggle buttons: "Active" narrows to conversations with live activity; "All" shows everything
|
||||
- Compact styling (7px button height, 3px font size) to avoid crowding the view
|
||||
|
||||
**Usage:**
|
||||
|
||||
```tsx
|
||||
import { A2AFilterBar } from "@/components/a2a/a2a-filter-bar";
|
||||
|
||||
<A2AFilterBar
|
||||
status={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
```
|
||||
|
||||
## Filter Utilities
|
||||
|
||||
The `a2a-filter-utils.ts` module exports pure, testable filtering logic shared by both views.
|
||||
|
||||
**File:** `panel/src/components/a2a/a2a-filter-utils.ts`
|
||||
|
||||
### `A2AStatusFilter` Type
|
||||
|
||||
```typescript
|
||||
type A2AStatusFilter = "active" | "all";
|
||||
```
|
||||
|
||||
### `filterConversations()`
|
||||
|
||||
Narrows the conversation list to the matching subset based on status and search query.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `conversations`: `ReadonlyArray<AdminConversationSummary>`
|
||||
- `status`: `A2AStatusFilter` — `"active"` filters to `conversation.status === "active"`; `"all"` passes all
|
||||
- `search`: `string` — free-text query (case-insensitive)
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Searches across both agent slugs (raw IDs), their display names (via `getAgentDisplayName`), and the topic
|
||||
- Empty search passes all conversations
|
||||
- Status filter is applied first, then search
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const filtered = filterConversations(
|
||||
conversations,
|
||||
"active",
|
||||
"backend qa"
|
||||
);
|
||||
// Returns only active conversations where one agent is Backend QA or mentions "backend qa"
|
||||
```
|
||||
|
||||
### `filterPairs()`
|
||||
|
||||
Narrows the switchboard pairs to the matching subset based on status and search query.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `pairs`: `ReadonlyArray<AdminPairSummary>`
|
||||
- `status`: `A2AStatusFilter` — `"active"` filters to pairs that have a `conversation_id` (have A2A'd); `"all"` passes all
|
||||
- `search`: `string` — free-text query (case-insensitive)
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Searches across both agent slugs (raw IDs) and their display names
|
||||
- Empty search passes all pairs
|
||||
- Status filter is applied first, then search
|
||||
- Note: `AdminPairSummary` has no backend `status` field, so "active" is interpreted as "has a conversation"
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
const filtered = filterPairs(pairs, "active", "auditor");
|
||||
// Returns only pairs where at least one agent matches "auditor" and the pair has an active conversation
|
||||
```
|
||||
|
||||
## Conversation List Updates
|
||||
|
||||
The conversation list now accepts a `pulses` prop and renders agent avatars.
|
||||
|
||||
**File:** `panel/src/components/a2a/a2a-conversation-list.tsx`
|
||||
|
||||
### `A2AConversationListProps`
|
||||
|
||||
**New prop:**
|
||||
|
||||
- `pulses` (`Record<string, number>`): Maps `pairKey(agent_a, agent_b)` to the epoch timestamp of the latest pulse frame. This is the same `pulses` map the switchboard uses, keyed identically, so a conversation row flashes in sync with its pair's card on the switchboard.
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
<A2AConversationList
|
||||
conversations={filteredConversations}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleSelect}
|
||||
isLoading={loadingConversations}
|
||||
pulses={pulses} // New: from page state
|
||||
/>
|
||||
```
|
||||
|
||||
### `ConversationRow` Subcomponent
|
||||
|
||||
Each conversation renders as a `ConversationRow` that mirrors the switchboard's pair card styling:
|
||||
|
||||
- **Avatars:** Two `PairAvatar` components (initials, colors) matching `A2APairCard`
|
||||
- **Pulse animation:** Uses `usePulseFlash()` to determine if the row should flash hot
|
||||
- **Styling:** Emerald background + shadow while pulsing, matches switchboard
|
||||
- **Selection state:** Bordered/highlighted when selected, same as switchboard selection
|
||||
|
||||
### Breaking Change
|
||||
|
||||
The `pulses` prop is **required**. If you're calling `A2AConversationList` from outside the A2A page, you must supply it:
|
||||
|
||||
```typescript
|
||||
// Old code (will TypeScript error)
|
||||
<A2AConversationList conversations={data} ... />
|
||||
|
||||
// New code
|
||||
const pulses = { "be-dev-1|be-qa": 1700000000000 };
|
||||
<A2AConversationList conversations={data} ... pulses={pulses} />
|
||||
```
|
||||
|
||||
If you don't have a pulses map available, pass an empty object `{}` — rows won't flash, but selection/interaction will work normally.
|
||||
|
||||
## Pulse-Flash Hook
|
||||
|
||||
**File:** `panel/src/hooks/use-pulse-flash.ts`
|
||||
|
||||
The `usePulseFlash()` hook extracts the pulse-flash animation logic from inline state in `A2APairCard`, making it reusable across the switchboard and conversation list.
|
||||
|
||||
### `usePulseFlash(pulsedAt: number | null): boolean`
|
||||
|
||||
Returns `true` for one paint frame after `pulsedAt` changes to a non-null value, then `false`. The consumer's CSS `transition-duration` does the actual fade-out.
|
||||
|
||||
**How it works:**
|
||||
|
||||
1. Seeded to `null` (not the initial `pulsedAt`), so a component that *mounts* already carrying a live pulse still flashes hot
|
||||
2. On render, if `pulsedAt !== lastSeenPulse`, updates `lastSeenPulse` and sets `isPulsing = true` if `pulsedAt !== null`
|
||||
3. On the next paint frame (via `requestAnimationFrame`), flips `isPulsing` back to `false`
|
||||
4. CSS transition handles the fade — `transition-duration: PAIR_PULSE_FADE_MS` applied to elements that conditionally render the hot styling
|
||||
|
||||
**Example:**
|
||||
|
||||
```tsx
|
||||
import { usePulseFlash } from "@/hooks/use-pulse-flash";
|
||||
import { PAIR_PULSE_FADE_MS } from "@/components/a2a/a2a-switchboard-utils";
|
||||
|
||||
export function MyPulsedRow({ pulsedAt }: { pulsedAt: number | null }) {
|
||||
const isPulsing = usePulseFlash(pulsedAt);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("p-2", isPulsing && "bg-emerald-500/15")}
|
||||
style={{ transitionDuration: `${PAIR_PULSE_FADE_MS}ms` }}
|
||||
>
|
||||
{/* content */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## A2A Page Integration
|
||||
|
||||
**File:** `panel/src/app/(dashboard)/a2a/page.tsx`
|
||||
|
||||
The page composes these pieces:
|
||||
|
||||
1. **Filter state:** Lifts `statusFilter` and `search` to page level
|
||||
2. **Derived state:** Computes `filteredPairs` and `filteredConversations` via `useMemo` on each render
|
||||
3. **Filter bar:** Mounts `A2AFilterBar` above the view content
|
||||
4. **Synchronized pulses:** Both `A2ASwitchboard` and `A2AConversationList` receive the same `pulses` map, keyed identically, so pulse animations sync across views
|
||||
|
||||
**Code sketch:**
|
||||
|
||||
```tsx
|
||||
const [statusFilter, setStatusFilter] = useState<A2AStatusFilter>("all");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredPairs = useMemo(
|
||||
() => filterPairs(pairs, statusFilter, search),
|
||||
[pairs, statusFilter, search]
|
||||
);
|
||||
|
||||
const filteredConversations = useMemo(
|
||||
() => filterConversations(conversations, statusFilter, search),
|
||||
[conversations, statusFilter, search]
|
||||
);
|
||||
|
||||
// Both views receive the same pulses map
|
||||
<A2AFilterBar
|
||||
status={statusFilter}
|
||||
onStatusChange={setStatusFilter}
|
||||
search={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
|
||||
{view === "switchboard" ? (
|
||||
<A2ASwitchboard pairs={filteredPairs} pulses={pulses} ... />
|
||||
) : (
|
||||
<A2AConversationList conversations={filteredConversations} pulses={pulses} ... />
|
||||
)}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
All filtering and pulse behavior is covered by tests:
|
||||
|
||||
- **`a2a-filter-utils.test.ts`:** `filterConversations()` and `filterPairs()` with status/search combinations
|
||||
- **`a2a-filter-bar.test.tsx`:** Button pressed state, search input changes, status toggle callbacks
|
||||
- **`a2a-conversation-list.test.tsx`:** Avatar rendering, pulse flash detection (tests the `data-pulsing` attribute)
|
||||
- **`page.test.tsx`:** Filter bar renders, narrows switchboard independently, narrows classic list independently
|
||||
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
cd panel
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
### Status Filter Semantics
|
||||
|
||||
The "active" filter has different semantics across views due to data availability:
|
||||
|
||||
- **Conversations:** `active` filters to `conversation.status === "active"` (backend-provided status)
|
||||
- **Pairs:** `active` filters to pairs with a non-null `conversation_id` (have A2A'd at least once)
|
||||
|
||||
This is intentional and documented in code comments. If the backend adds an explicit `AdminPairSummary.status` field in the future, update `filterPairs()` to use it instead of the `conversation_id` heuristic.
|
||||
|
||||
### Pulse Animation Timing
|
||||
|
||||
The pulse flash is a brief, high-contrast alert (emerald bg + shadow) that fades over `PAIR_PULSE_FADE_MS` (200ms by default, defined in `a2a-switchboard-utils.ts`). If you're experiencing chop or seeing the animation cut off, check:
|
||||
|
||||
1. The `usePulseFlash()` hook is being called (not bypassed)
|
||||
2. The consumer element has `transition-[background-color,box-shadow]` or equivalent CSS
|
||||
3. The `transitionDuration` inline style matches the fade constant
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### If you use `A2AConversationList` in another context:
|
||||
|
||||
**Before:**
|
||||
|
||||
```tsx
|
||||
<A2AConversationList
|
||||
conversations={conversations}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleSelect}
|
||||
isLoading={false}
|
||||
/>
|
||||
```
|
||||
|
||||
**After:**
|
||||
|
||||
```tsx
|
||||
<A2AConversationList
|
||||
conversations={conversations}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleSelect}
|
||||
isLoading={false}
|
||||
pulses={{}} // Add this prop (empty object if no pulses available)
|
||||
/>
|
||||
```
|
||||
|
||||
### If you extract pulse-flash logic into other components:
|
||||
|
||||
Import and use the `usePulseFlash()` hook instead of rolling your own state management:
|
||||
|
||||
```tsx
|
||||
import { usePulseFlash } from "@/hooks/use-pulse-flash";
|
||||
import { PAIR_PULSE_FADE_MS } from "@/components/a2a/a2a-switchboard-utils";
|
||||
|
||||
const isPulsing = usePulseFlash(pulsedAt);
|
||||
// Now use isPulsing to conditionally render the hot styling
|
||||
```
|
||||
Reference in New Issue
Block a user