Skip to content

Commit 0ede7af

Browse files
sonpiazclaude
andcommitted
feat: CEO UI feedback — text contrast, slide-over panel, heartbeat collapsing
- Fix WCAG AA contrast: text-secondary #a0a0a0 (7.3:1), text-tertiary #707070 (4.6:1), add text-disabled #484848 - Replace AgentProfileModal with AgentDetailSlidePanel (420px slide-from-right + Esc key) - Redirect /agents/[name] to /?view=team, delete dead agent-profile-modal.tsx - Collapse 3+ consecutive heartbeats in ActivityFeed into single summary row - Update Design System doc: P6 principle, sections 9.13-9.14, 16-17 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 43bd677 commit 0ede7af

7 files changed

Lines changed: 161 additions & 331 deletions

File tree

app/agents/[name]/page.tsx

Lines changed: 3 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -1,221 +1,5 @@
1-
"use client";
1+
import { redirect } from "next/navigation";
22

3-
/**
4-
* AGT-342: Agent Career Profile
5-
*
6-
* Route: /agents/[name]
7-
* Sections: Hero, Stats Grid, Skills, Feedback, Timeline, Learnings.
8-
*/
9-
10-
import { use } from "react";
11-
import Link from "next/link";
12-
import { useQuery } from "convex/react";
13-
import { api } from "@/convex/_generated/api";
14-
import { cn } from "@/lib/utils";
15-
import { formatDistanceToNow } from "date-fns";
16-
17-
const AGENT_COLORS: Record<string, string> = {
18-
max: "text-purple-400",
19-
sam: "text-emerald-400",
20-
leo: "text-blue-400",
21-
quinn: "text-amber-400",
22-
};
23-
24-
const STATUS_DOT: Record<string, string> = {
25-
online: "bg-green-500",
26-
busy: "bg-yellow-500",
27-
idle: "bg-gray-500",
28-
offline: "bg-red-500",
29-
};
30-
31-
function timeAgo(ts: number) {
32-
return formatDistanceToNow(ts, { addSuffix: true });
33-
}
34-
35-
export default function AgentProfilePage({ params }: { params: Promise<{ name: string }> }) {
36-
const { name } = use(params);
37-
const agentName = name.toLowerCase();
38-
39-
const profile = useQuery(api.agentProfiles.getCareerProfile, { agentName });
40-
const timeline = useQuery(api.agentProfiles.getCareerTimeline, { agentName, limit: 10 });
41-
const learningsData = useQuery(api.agentLearning.getLearnings, { agent: agentName, verifiedOnly: true, limit: 10 });
42-
43-
if (profile === undefined) {
44-
return (
45-
<div className="min-h-screen bg-black text-white flex items-center justify-center">
46-
<div className="text-tertiary text-sm">Loading profile...</div>
47-
</div>
48-
);
49-
}
50-
51-
if (profile === null) {
52-
return (
53-
<div className="min-h-screen bg-black text-white flex items-center justify-center">
54-
<div className="text-tertiary text-sm">Agent not found</div>
55-
</div>
56-
);
57-
}
58-
59-
const statusKey = (profile.status ?? "offline").toLowerCase();
60-
const stats = [
61-
{ label: "Tasks Done", value: profile.tasksCompleted, sub: `${profile.tasksCompleted24h} today` },
62-
{ label: "Success", value: `${Math.round(profile.successRate * 100)}%`, sub: `${profile.tasksCompleted7d} this week` },
63-
{ label: "Loop", value: `${Math.round(profile.loopCompletionRate * 100)}%`, sub: `${profile.slaBreaches} breaches` },
64-
{ label: "Rating", value: profile.avgRating.toFixed(1), sub: `${profile.totalFeedbackCount} reviews` },
65-
{ label: "Cost 7d", value: `$${profile.totalCost7d.toFixed(2)}`, sub: `$${profile.avgCostPerTask.toFixed(3)}/task` },
66-
{ label: "Learnings", value: profile.verifiedLearnings, sub: `${profile.totalLearnings} total` },
67-
];
68-
69-
return (
70-
<div className="min-h-screen bg-black text-white">
71-
{/* Header */}
72-
<header className="flex items-center gap-3 px-4 sm:px-6 py-4 border-b border-border-default/60">
73-
<Link href="/agents" className="text-tertiary hover:text-secondary text-sm transition-colors">
74-
&larr; Hall of Fame
75-
</Link>
76-
</header>
77-
78-
<main className="max-w-2xl mx-auto px-4 sm:px-6 py-6 space-y-6">
79-
{/* Hero */}
80-
<div className="flex items-start gap-4">
81-
<div className="flex-1">
82-
<h1 className={cn("text-3xl font-bold uppercase", AGENT_COLORS[agentName] ?? "text-primary")}>
83-
{profile.name}
84-
</h1>
85-
<div className="flex items-center gap-2 mt-1">
86-
<span className="text-sm text-primary0 capitalize">{profile.role}</span>
87-
<span className="text-tertiary">|</span>
88-
<span className="text-sm text-primary0">{profile.autonomyLevelName}</span>
89-
</div>
90-
<div className="flex items-center gap-2 mt-2">
91-
<div className={cn("h-2 w-2 rounded-full shrink-0", STATUS_DOT[statusKey] ?? STATUS_DOT.offline)} />
92-
<span className="text-xs text-primary0 capitalize">{statusKey}</span>
93-
{profile.daysSinceFirstTask > 0 && (
94-
<>
95-
<span className="text-tertiary">|</span>
96-
<span className="text-xs text-tertiary">{profile.daysSinceFirstTask}d tenure</span>
97-
</>
98-
)}
99-
</div>
100-
</div>
101-
</div>
102-
103-
{/* Stats Grid (2x3) */}
104-
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
105-
{stats.map((s) => (
106-
<div key={s.label} className="bg-surface-1/60 border border-border-default/60 rounded-xl p-3">
107-
<div className="text-[10px] font-bold uppercase tracking-wider text-primary0 mb-1">{s.label}</div>
108-
<div className="text-xl font-bold tabular-nums text-white">{s.value}</div>
109-
<div className="text-[10px] text-tertiary">{s.sub}</div>
110-
</div>
111-
))}
112-
</div>
113-
114-
{/* Skills */}
115-
{profile.skills.length > 0 && (
116-
<div className="bg-surface-1/40 border border-border-default/60 rounded-xl overflow-hidden">
117-
<div className="px-4 py-2.5 border-b border-border-default/40">
118-
<span className="text-[10px] font-bold uppercase tracking-wider text-primary0">Skills</span>
119-
</div>
120-
<div className="px-4 py-3 space-y-2.5">
121-
{profile.skills.map((skill) => (
122-
<div key={skill.name}>
123-
<div className="flex items-center justify-between mb-1">
124-
<span className="text-xs text-secondary">{skill.name}</span>
125-
<span className="text-[10px] tabular-nums text-primary0">{skill.proficiency}%</span>
126-
</div>
127-
<div className="h-1.5 bg-surface-4 rounded-full overflow-hidden">
128-
<div
129-
className={cn("h-full rounded-full", skill.verified ? "bg-emerald-500" : "bg-gray-500")}
130-
style={{ width: `${skill.proficiency}%` }}
131-
/>
132-
</div>
133-
</div>
134-
))}
135-
</div>
136-
</div>
137-
)}
138-
139-
{/* Feedback by Category */}
140-
{Object.keys(profile.feedbackByCategory).length > 0 && (
141-
<div className="bg-surface-1/40 border border-border-default/60 rounded-xl overflow-hidden">
142-
<div className="px-4 py-2.5 border-b border-border-default/40">
143-
<span className="text-[10px] font-bold uppercase tracking-wider text-primary0">
144-
Feedback ({profile.totalFeedbackCount})
145-
</span>
146-
</div>
147-
<div className="px-4 py-3 grid grid-cols-2 sm:grid-cols-3 gap-2">
148-
{Object.entries(profile.feedbackByCategory).map(([cat, rating]) => (
149-
<div key={cat} className="flex items-center justify-between bg-surface-1 rounded-lg px-3 py-2">
150-
<span className="text-[10px] text-primary0 capitalize">{cat}</span>
151-
<span className={cn(
152-
"text-xs font-medium tabular-nums",
153-
rating >= 4 ? "text-emerald-400" : rating >= 3 ? "text-yellow-400" : "text-red-400"
154-
)}>
155-
{rating.toFixed(1)}
156-
</span>
157-
</div>
158-
))}
159-
</div>
160-
</div>
161-
)}
162-
163-
{/* Timeline */}
164-
<div className="bg-surface-1/40 border border-border-default/60 rounded-xl overflow-hidden">
165-
<div className="px-4 py-2.5 border-b border-border-default/40">
166-
<span className="text-[10px] font-bold uppercase tracking-wider text-primary0">Timeline</span>
167-
</div>
168-
<div className="divide-y divide-border-default/30">
169-
{timeline && timeline.length > 0 ? (
170-
timeline.map((item, i) => (
171-
<div key={i} className="px-4 py-2.5 flex items-start gap-2.5">
172-
<div className={cn(
173-
"mt-1.5 h-1.5 w-1.5 rounded-full shrink-0",
174-
item.type === "task" ? "bg-emerald-500" : "bg-gray-600"
175-
)} />
176-
<div className="flex-1 min-w-0">
177-
<div className="flex items-center gap-2">
178-
<span className="text-xs text-primary truncate">{item.title}</span>
179-
{item.linearUrl && (
180-
<a
181-
href={item.linearUrl}
182-
target="_blank"
183-
rel="noopener noreferrer"
184-
className="text-[9px] text-tertiary hover:text-secondary shrink-0"
185-
>
186-
{item.linearIdentifier ?? "Linear"}
187-
</a>
188-
)}
189-
</div>
190-
<div className="text-[10px] text-tertiary mt-0.5">{timeAgo(item.timestamp)}</div>
191-
</div>
192-
</div>
193-
))
194-
) : (
195-
<div className="px-4 py-6 text-center text-xs text-tertiary">No recent activity</div>
196-
)}
197-
</div>
198-
</div>
199-
200-
{/* Learnings */}
201-
{learningsData && learningsData.learnings.length > 0 && (
202-
<div className="bg-surface-1/40 border border-border-default/60 rounded-xl overflow-hidden">
203-
<div className="px-4 py-2.5 border-b border-border-default/40">
204-
<span className="text-[10px] font-bold uppercase tracking-wider text-primary0">
205-
Verified Learnings ({learningsData.stats.verified})
206-
</span>
207-
</div>
208-
<div className="divide-y divide-border-default/30">
209-
{learningsData.learnings.slice(0, 8).map((learning, i) => (
210-
<div key={learning._id ?? i} className="px-4 py-2.5 flex items-start gap-2">
211-
<span className="text-emerald-500 text-xs shrink-0 mt-0.5">&#10003;</span>
212-
<span className="text-xs text-secondary">{learning.lesson}</span>
213-
</div>
214-
))}
215-
</div>
216-
</div>
217-
)}
218-
</main>
219-
</div>
220-
);
3+
export default function AgentProfilePage() {
4+
redirect("/?view=team");
2215
}

app/globals.css

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@
6060

6161
/* === Design System V2 — Semantic Text === */
6262
--color-text-primary: #EDEDED;
63-
--color-text-secondary: #A1A1A1;
64-
--color-text-tertiary: #666666;
63+
--color-text-secondary: #a0a0a0;
64+
--color-text-tertiary: #707070;
65+
--color-text-disabled: #484848;
6566
--color-primary0: #888888;
6667

6768
/* === Design System V2 — Borders === */
@@ -78,7 +79,7 @@
7879
--color-gray-600: #4A4A4A;
7980
--color-gray-700: #6B6B6B;
8081
--color-gray-800: #7A7A7A;
81-
--color-gray-900: #A1A1A1;
82+
--color-gray-900: #a0a0a0;
8283
--color-gray-1000: #EDEDED;
8384

8485
/* === Design System V2 — 10-Step Blue Scale (Accent) === */
@@ -306,8 +307,9 @@
306307

307308
/* Text */
308309
--text-primary: #EDEDED;
309-
--text-secondary: #A1A1A1;
310-
--text-tertiary: #666666;
310+
--text-secondary: #a0a0a0;
311+
--text-tertiary: #707070;
312+
--text-disabled: #484848;
311313

312314
/* Borders */
313315
--border-default: #222222;

app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { AgentSidebar } from "@/components/evox/AgentSidebar";
1313
import { AgentSettingsModal } from "@/components/evox/AgentSettingsModal";
1414
import { ShortcutsHelpModal } from "@/components/evox/ShortcutsHelpModal";
1515
import { useKeyboardShortcuts } from "@/hooks/useKeyboardShortcuts";
16-
import { AgentProfileModal } from "@/components/dashboard-v2/agent-profile-modal";
16+
import { AgentDetailSlidePanel } from "@/components/dashboard-v2/agent-detail-slide-panel";
1717
import { ActivityDrawer } from "@/components/dashboard-v2/activity-drawer";
1818
import { TaskDetailModal } from "@/components/dashboard-v2/task-detail-modal";
1919
import { ViewTabs, type MainViewTab } from "@/components/evox/ViewTabs";
@@ -183,7 +183,7 @@ function HomeContent() {
183183
</div>
184184

185185
{selectedAgent && (
186-
<AgentProfileModal
186+
<AgentDetailSlidePanel
187187
open={selectedAgentId !== null}
188188
agentId={selectedAgent._id}
189189
name={selectedAgent.name}

components/dashboard-v2/agent-detail-slide-panel.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { useEffect } from "react";
34
import { Id } from "@/convex/_generated/dataModel";
45
import { AgentProfile } from "./agent-profile";
56
import { cn } from "@/lib/utils";
@@ -24,6 +25,15 @@ export function AgentDetailSlidePanel({
2425
avatar,
2526
onClose,
2627
}: AgentDetailSlidePanelProps) {
28+
useEffect(() => {
29+
if (!open) return;
30+
const handleEscape = (e: KeyboardEvent) => {
31+
if (e.key === "Escape") onClose();
32+
};
33+
document.addEventListener("keydown", handleEscape);
34+
return () => document.removeEventListener("keydown", handleEscape);
35+
}, [open, onClose]);
36+
2737
if (!agentId) return null;
2838

2939
return (

components/dashboard-v2/agent-profile-modal.tsx

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)