|
1 | | -"use client"; |
| 1 | +import { redirect } from "next/navigation"; |
2 | 2 |
|
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 | | - ← 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">✓</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"); |
221 | 5 | } |
0 commit comments