agentfit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.claude/settings.local.json +26 -0
  2. package/.prettierignore +7 -0
  3. package/.prettierrc +11 -0
  4. package/CONTRIBUTING.md +209 -0
  5. package/LICENSE +21 -0
  6. package/README.md +109 -0
  7. package/app/(dashboard)/coach/page.tsx +11 -0
  8. package/app/(dashboard)/commands/page.tsx +7 -0
  9. package/app/(dashboard)/community/[slug]/page.tsx +23 -0
  10. package/app/(dashboard)/community/page.tsx +71 -0
  11. package/app/(dashboard)/daily/page.tsx +19 -0
  12. package/app/(dashboard)/images/page.tsx +5 -0
  13. package/app/(dashboard)/layout.tsx +12 -0
  14. package/app/(dashboard)/page.tsx +23 -0
  15. package/app/(dashboard)/personality/page.tsx +11 -0
  16. package/app/(dashboard)/projects/page.tsx +11 -0
  17. package/app/(dashboard)/sessions/page.tsx +11 -0
  18. package/app/(dashboard)/tokens/page.tsx +11 -0
  19. package/app/(dashboard)/tools/page.tsx +11 -0
  20. package/app/api/check/route.ts +13 -0
  21. package/app/api/commands/route.ts +16 -0
  22. package/app/api/images/[...path]/route.ts +33 -0
  23. package/app/api/images-analysis/route.ts +177 -0
  24. package/app/api/sync/route.ts +14 -0
  25. package/app/api/usage/route.ts +117 -0
  26. package/app/favicon.ico +0 -0
  27. package/app/globals.css +144 -0
  28. package/app/icon.svg +3 -0
  29. package/app/layout.tsx +35 -0
  30. package/bin/agentfit.mjs +69 -0
  31. package/components/.gitkeep +0 -0
  32. package/components/agent-coach.tsx +248 -0
  33. package/components/app-sidebar.tsx +161 -0
  34. package/components/command-usage.tsx +294 -0
  35. package/components/daily-chart.tsx +118 -0
  36. package/components/daily-table.tsx +115 -0
  37. package/components/dashboard-shell.tsx +149 -0
  38. package/components/data-provider.tsx +213 -0
  39. package/components/fitness-score.tsx +95 -0
  40. package/components/overview-cards.tsx +198 -0
  41. package/components/pagination-controls.tsx +104 -0
  42. package/components/personality-fit.tsx +446 -0
  43. package/components/projects-table.tsx +70 -0
  44. package/components/screenshots-analysis.tsx +359 -0
  45. package/components/sessions-table.tsx +97 -0
  46. package/components/theme-provider.tsx +71 -0
  47. package/components/token-breakdown.tsx +179 -0
  48. package/components/tool-usage-chart.tsx +63 -0
  49. package/components/ui/badge.tsx +52 -0
  50. package/components/ui/button.tsx +60 -0
  51. package/components/ui/card.tsx +103 -0
  52. package/components/ui/chart.tsx +373 -0
  53. package/components/ui/dialog.tsx +160 -0
  54. package/components/ui/input.tsx +20 -0
  55. package/components/ui/scroll-area.tsx +55 -0
  56. package/components/ui/select.tsx +201 -0
  57. package/components/ui/separator.tsx +25 -0
  58. package/components/ui/sheet.tsx +138 -0
  59. package/components/ui/sidebar.tsx +723 -0
  60. package/components/ui/skeleton.tsx +13 -0
  61. package/components/ui/table.tsx +116 -0
  62. package/components/ui/tabs.tsx +82 -0
  63. package/components/ui/tooltip.tsx +66 -0
  64. package/components.json +25 -0
  65. package/generated/prisma/browser.ts +34 -0
  66. package/generated/prisma/client.ts +58 -0
  67. package/generated/prisma/commonInputTypes.ts +237 -0
  68. package/generated/prisma/enums.ts +15 -0
  69. package/generated/prisma/internal/class.ts +224 -0
  70. package/generated/prisma/internal/prismaNamespace.ts +920 -0
  71. package/generated/prisma/internal/prismaNamespaceBrowser.ts +130 -0
  72. package/generated/prisma/models/Image.ts +1310 -0
  73. package/generated/prisma/models/Session.ts +1695 -0
  74. package/generated/prisma/models/SyncLog.ts +1203 -0
  75. package/generated/prisma/models.ts +14 -0
  76. package/hooks/.gitkeep +0 -0
  77. package/hooks/use-mobile.ts +19 -0
  78. package/hooks/use-pagination.ts +60 -0
  79. package/lib/.gitkeep +0 -0
  80. package/lib/coach.ts +425 -0
  81. package/lib/commands.ts +239 -0
  82. package/lib/db.ts +15 -0
  83. package/lib/format.ts +26 -0
  84. package/lib/parse-codex.ts +201 -0
  85. package/lib/parse-logs.ts +369 -0
  86. package/lib/personality.ts +481 -0
  87. package/lib/plugins.ts +107 -0
  88. package/lib/pricing.ts +112 -0
  89. package/lib/queries-codex.ts +130 -0
  90. package/lib/queries.ts +154 -0
  91. package/lib/resolve-icon.ts +12 -0
  92. package/lib/sync.ts +335 -0
  93. package/lib/utils.ts +6 -0
  94. package/next.config.mjs +4 -0
  95. package/package.json +73 -0
  96. package/plugins/cost-heatmap/component.test.tsx +52 -0
  97. package/plugins/cost-heatmap/component.tsx +227 -0
  98. package/plugins/cost-heatmap/manifest.ts +13 -0
  99. package/plugins/index.ts +18 -0
  100. package/prisma/migrations/20260328152517_init/migration.sql +41 -0
  101. package/prisma/migrations/20260328153801_add_image_model/migration.sql +18 -0
  102. package/prisma/migrations/migration_lock.toml +3 -0
  103. package/prisma/schema.prisma +57 -0
  104. package/prisma.config.ts +14 -0
  105. package/public/.gitkeep +0 -0
  106. package/public/logo.svg +3 -0
  107. package/setup.sh +73 -0
@@ -0,0 +1,446 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import type { UsageData } from '@/lib/parse-logs'
5
+ import { analyzePersonality, type PersonalityProfile, type BigFiveProfile } from '@/lib/personality'
6
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from '@/components/ui/select'
15
+ import { Button } from '@/components/ui/button'
16
+ import { Copy, Check, Brain, Target, Sparkles, ArrowRight } from 'lucide-react'
17
+
18
+ const TASK_OPTIONS = [
19
+ { value: 'frontendDev', label: 'Frontend Development' },
20
+ { value: 'backendDev', label: 'Backend Development' },
21
+ { value: 'dataEngineering', label: 'Data Engineering' },
22
+ { value: 'devOps', label: 'DevOps' },
23
+ { value: 'debugging', label: 'Debugging' },
24
+ { value: 'refactoring', label: 'Refactoring' },
25
+ ]
26
+
27
+ const MBTI_DESCRIPTIONS: Record<string, string> = {
28
+ ISTJ: 'The Inspector — Methodical, detail-oriented, follows established procedures',
29
+ ISFJ: 'The Protector — Supportive, reliable, focused on preserving working code',
30
+ INFJ: 'The Counselor — Insightful, sees patterns, anticipates architectural needs',
31
+ INTJ: 'The Architect — Strategic, independent, designs for long-term quality',
32
+ ISTP: 'The Craftsman — Practical, efficient, excels at targeted fixes',
33
+ ISFP: 'The Composer — Adaptable, aesthetic sense, good at UI refinement',
34
+ INFP: 'The Healer — Idealistic, explores creative solutions, values code clarity',
35
+ INTP: 'The Thinker — Analytical, explores edge cases, values logical consistency',
36
+ ESTP: 'The Dynamo — Action-oriented, quick iterations, bias toward execution',
37
+ ESFP: 'The Performer — Energetic, responsive, generates many alternatives',
38
+ ENFP: 'The Champion — Enthusiastic explorer, broad tool usage, creative approaches',
39
+ ENTP: 'The Visionary — Innovative, questions assumptions, proposes novel solutions',
40
+ ESTJ: 'The Supervisor — Organized, efficient, follows project conventions strictly',
41
+ ESFJ: 'The Provider — Cooperative, communicative, adapts to user preferences',
42
+ ENFJ: 'The Teacher — Explains reasoning, mentors through code, proactive guidance',
43
+ ENTJ: 'The Commander — Decisive, takes charge, designs comprehensive solutions',
44
+ }
45
+
46
+ function TraitBar({
47
+ label,
48
+ value,
49
+ idealValue,
50
+ color,
51
+ leftLabel,
52
+ rightLabel,
53
+ }: {
54
+ label: string
55
+ value: number
56
+ idealValue?: number
57
+ color: string
58
+ leftLabel?: string
59
+ rightLabel?: string
60
+ }) {
61
+ return (
62
+ <div className="space-y-1.5">
63
+ <div className="flex items-center justify-between text-sm">
64
+ <span className="font-medium">{label}</span>
65
+ <span className="text-muted-foreground">{Math.round(value)}/100</span>
66
+ </div>
67
+ {(leftLabel || rightLabel) && (
68
+ <div className="flex justify-between text-xs text-muted-foreground">
69
+ <span>{leftLabel}</span>
70
+ <span>{rightLabel}</span>
71
+ </div>
72
+ )}
73
+ <div className="relative h-3 w-full overflow-hidden rounded-full bg-muted">
74
+ <div
75
+ className="h-full rounded-full transition-all duration-500"
76
+ style={{
77
+ width: `${Math.max(2, value)}%`,
78
+ backgroundColor: color,
79
+ }}
80
+ />
81
+ {idealValue !== undefined && (
82
+ <div
83
+ className="absolute top-0 h-full w-0.5 bg-foreground/60"
84
+ style={{ left: `${idealValue}%` }}
85
+ title={`Ideal: ${Math.round(idealValue)}`}
86
+ />
87
+ )}
88
+ </div>
89
+ </div>
90
+ )
91
+ }
92
+
93
+ function MBTIDimension({
94
+ label,
95
+ value,
96
+ leftPole,
97
+ rightPole,
98
+ }: {
99
+ label: string
100
+ value: number
101
+ leftPole: string
102
+ rightPole: string
103
+ }) {
104
+ const normalized = (value + 100) / 2 // -100..+100 → 0..100
105
+ const isLeft = value < 0
106
+ return (
107
+ <div className="space-y-1.5">
108
+ <div className="flex items-center justify-between text-sm">
109
+ <span className={`font-medium ${isLeft ? 'text-foreground' : 'text-muted-foreground'}`}>
110
+ {leftPole}
111
+ </span>
112
+ <span className="text-xs text-muted-foreground">{label}</span>
113
+ <span className={`font-medium ${!isLeft ? 'text-foreground' : 'text-muted-foreground'}`}>
114
+ {rightPole}
115
+ </span>
116
+ </div>
117
+ <div className="relative h-3 w-full overflow-hidden rounded-full bg-muted">
118
+ <div className="absolute top-0 left-1/2 h-full w-px bg-foreground/20" />
119
+ {isLeft ? (
120
+ <div
121
+ className="absolute top-0 right-1/2 h-full rounded-l-full transition-all duration-500"
122
+ style={{ width: `${(100 - normalized)}%`, backgroundColor: 'var(--chart-1)' }}
123
+ />
124
+ ) : (
125
+ <div
126
+ className="absolute top-0 left-1/2 h-full rounded-r-full transition-all duration-500"
127
+ style={{ width: `${normalized - 50}%`, backgroundColor: 'var(--chart-2)' }}
128
+ />
129
+ )}
130
+ </div>
131
+ </div>
132
+ )
133
+ }
134
+
135
+ function FitScoreCard({ task, score, isSelected }: { task: string; score: number; isSelected: boolean }) {
136
+ const taskNames: Record<string, string> = {
137
+ frontendDev: 'Frontend',
138
+ backendDev: 'Backend',
139
+ dataEngineering: 'Data Eng',
140
+ devOps: 'DevOps',
141
+ debugging: 'Debug',
142
+ refactoring: 'Refactor',
143
+ }
144
+
145
+ const getColor = (s: number) => {
146
+ if (s >= 80) return 'text-foreground'
147
+ if (s >= 60) return 'text-foreground'
148
+ if (s >= 40) return 'text-muted-foreground'
149
+ return 'text-muted-foreground'
150
+ }
151
+
152
+ const getBg = (s: number) => {
153
+ if (s >= 80) return 'bg-chart-2/10'
154
+ if (s >= 60) return 'bg-chart-1/10'
155
+ if (s >= 40) return 'bg-chart-3/10'
156
+ return 'bg-muted/50'
157
+ }
158
+
159
+ return (
160
+ <div
161
+ className={`rounded-lg border p-3 text-center transition-all ${
162
+ isSelected ? 'border-chart-1 border-2' : ''
163
+ } ${getBg(score)}`}
164
+ >
165
+ <div className="text-xs text-muted-foreground mb-1">{taskNames[task]}</div>
166
+ <div className={`text-2xl font-bold ${getColor(score)}`}>{score}%</div>
167
+ </div>
168
+ )
169
+ }
170
+
171
+ export function PersonalityFit({ data }: { data: UsageData }) {
172
+ const [targetTask, setTargetTask] = useState('backendDev')
173
+ const targetTaskLabel = TASK_OPTIONS.find((t) => t.value === targetTask)?.label || targetTask
174
+ const [copied, setCopied] = useState(false)
175
+
176
+ const profile: PersonalityProfile = useMemo(
177
+ () => analyzePersonality(data, targetTask),
178
+ [data, targetTask]
179
+ )
180
+
181
+ const handleCopyPrompt = async () => {
182
+ await navigator.clipboard.writeText(profile.systemPrompt)
183
+ setCopied(true)
184
+ setTimeout(() => setCopied(false), 2000)
185
+ }
186
+
187
+ const bigFiveColors: Record<keyof BigFiveProfile, string> = {
188
+ openness: 'var(--chart-1)',
189
+ conscientiousness: 'var(--chart-2)',
190
+ extraversion: 'var(--chart-3)',
191
+ agreeableness: 'var(--chart-4)',
192
+ neuroticism: 'var(--chart-5)',
193
+ }
194
+
195
+ const idealProfile = {
196
+ frontendDev: { openness: 80, conscientiousness: 60, extraversion: 70, agreeableness: 75, neuroticism: 20 },
197
+ backendDev: { openness: 50, conscientiousness: 90, extraversion: 40, agreeableness: 60, neuroticism: 15 },
198
+ dataEngineering: { openness: 45, conscientiousness: 95, extraversion: 35, agreeableness: 55, neuroticism: 10 },
199
+ devOps: { openness: 55, conscientiousness: 85, extraversion: 50, agreeableness: 65, neuroticism: 15 },
200
+ debugging: { openness: 70, conscientiousness: 80, extraversion: 55, agreeableness: 50, neuroticism: 25 },
201
+ refactoring: { openness: 65, conscientiousness: 90, extraversion: 45, agreeableness: 70, neuroticism: 15 },
202
+ }[targetTask] || { openness: 50, conscientiousness: 50, extraversion: 50, agreeableness: 50, neuroticism: 50 }
203
+
204
+ return (
205
+ <div className="space-y-6">
206
+ {/* MBTI type */}
207
+ <Card>
208
+ <CardHeader className="pb-3">
209
+ <div className="flex items-center gap-2">
210
+ <Brain className="h-5 w-5 text-primary" />
211
+ <CardTitle>Agent Personality Type</CardTitle>
212
+ </div>
213
+ </CardHeader>
214
+ <CardContent>
215
+ <div className="flex items-center gap-4">
216
+ <div className="text-4xl font-bold tracking-wider text-primary">
217
+ {profile.mbtiType}
218
+ </div>
219
+ <div className="text-sm text-muted-foreground">
220
+ {MBTI_DESCRIPTIONS[profile.mbtiType] || 'Unique personality profile'}
221
+ </div>
222
+ </div>
223
+ </CardContent>
224
+ </Card>
225
+
226
+ {/* Task Fit Scores */}
227
+ <Card>
228
+ <CardHeader>
229
+ <div className="flex items-center justify-between">
230
+ <div>
231
+ <CardTitle>Task Fit Scores</CardTitle>
232
+ <CardDescription>
233
+ How well the agent's observed personality matches ideal profiles for each task type
234
+ </CardDescription>
235
+ </div>
236
+ <div className="flex items-center gap-2">
237
+ <Target className="h-4 w-4 text-muted-foreground" />
238
+ <span className="text-sm text-muted-foreground">Target Task</span>
239
+ <Select value={targetTask} onValueChange={(v) => v && setTargetTask(v)}>
240
+ <SelectTrigger className="w-[200px]">
241
+ <SelectValue placeholder={targetTaskLabel}>{targetTaskLabel}</SelectValue>
242
+ </SelectTrigger>
243
+ <SelectContent alignItemWithTrigger={false}>
244
+ {TASK_OPTIONS.map((opt) => (
245
+ <SelectItem key={opt.value} value={opt.value}>
246
+ {opt.label}
247
+ </SelectItem>
248
+ ))}
249
+ </SelectContent>
250
+ </Select>
251
+ </div>
252
+ </div>
253
+ </CardHeader>
254
+ <CardContent>
255
+ <div className="grid grid-cols-3 gap-3 sm:grid-cols-6">
256
+ {Object.entries(profile.fitScores).map(([task, score]) => (
257
+ <FitScoreCard
258
+ key={task}
259
+ task={task}
260
+ score={score}
261
+ isSelected={task === targetTask}
262
+ />
263
+ ))}
264
+ </div>
265
+ </CardContent>
266
+ </Card>
267
+
268
+ {/* Big Five and MBTI side by side */}
269
+ <div className="grid gap-6 lg:grid-cols-2">
270
+ {/* Big Five */}
271
+ <Card>
272
+ <CardHeader>
273
+ <CardTitle>Big Five (OCEAN) Profile</CardTitle>
274
+ <CardDescription>
275
+ Derived from behavioral signals. Vertical markers show ideal for selected task.
276
+ </CardDescription>
277
+ </CardHeader>
278
+ <CardContent className="space-y-5">
279
+ <TraitBar
280
+ label="Openness"
281
+ value={profile.bigFive.openness}
282
+ idealValue={idealProfile.openness}
283
+ color={bigFiveColors.openness}
284
+ leftLabel="Conventional"
285
+ rightLabel="Exploratory"
286
+ />
287
+ <TraitBar
288
+ label="Conscientiousness"
289
+ value={profile.bigFive.conscientiousness}
290
+ idealValue={idealProfile.conscientiousness}
291
+ color={bigFiveColors.conscientiousness}
292
+ leftLabel="Flexible"
293
+ rightLabel="Methodical"
294
+ />
295
+ <TraitBar
296
+ label="Extraversion"
297
+ value={profile.bigFive.extraversion}
298
+ idealValue={idealProfile.extraversion}
299
+ color={bigFiveColors.extraversion}
300
+ leftLabel="Concise"
301
+ rightLabel="Verbose"
302
+ />
303
+ <TraitBar
304
+ label="Agreeableness"
305
+ value={profile.bigFive.agreeableness}
306
+ idealValue={idealProfile.agreeableness}
307
+ color={bigFiveColors.agreeableness}
308
+ leftLabel="Challenging"
309
+ rightLabel="Compliant"
310
+ />
311
+ <TraitBar
312
+ label="Neuroticism"
313
+ value={profile.bigFive.neuroticism}
314
+ idealValue={idealProfile.neuroticism}
315
+ color={bigFiveColors.neuroticism}
316
+ leftLabel="Stable"
317
+ rightLabel="Reactive"
318
+ />
319
+ </CardContent>
320
+ </Card>
321
+
322
+ {/* MBTI */}
323
+ <Card>
324
+ <CardHeader>
325
+ <CardTitle>MBTI Dimensions</CardTitle>
326
+ <CardDescription>
327
+ Cognitive style inferred from interaction patterns
328
+ </CardDescription>
329
+ </CardHeader>
330
+ <CardContent className="space-y-5">
331
+ <MBTIDimension
332
+ label="Energy"
333
+ value={profile.mbti.ei}
334
+ leftPole="Introversion (I)"
335
+ rightPole="Extraversion (E)"
336
+ />
337
+ <MBTIDimension
338
+ label="Information"
339
+ value={profile.mbti.sn}
340
+ leftPole="Sensing (S)"
341
+ rightPole="iNtuition (N)"
342
+ />
343
+ <MBTIDimension
344
+ label="Decisions"
345
+ value={profile.mbti.tf}
346
+ leftPole="Thinking (T)"
347
+ rightPole="Feeling (F)"
348
+ />
349
+ <MBTIDimension
350
+ label="Structure"
351
+ value={profile.mbti.jp}
352
+ leftPole="Judging (J)"
353
+ rightPole="Perceiving (P)"
354
+ />
355
+ </CardContent>
356
+ </Card>
357
+ </div>
358
+
359
+ {/* Behavioral Signals */}
360
+ <Card>
361
+ <CardHeader>
362
+ <CardTitle>Behavioral Signals</CardTitle>
363
+ <CardDescription>
364
+ Raw metrics extracted from conversation traces that drive personality inference
365
+ </CardDescription>
366
+ </CardHeader>
367
+ <CardContent>
368
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5">
369
+ {[
370
+ { label: 'Tool Diversity', value: profile.signals.toolDiversity.toFixed(3) },
371
+ { label: 'Read-Before-Edit', value: profile.signals.readBeforeEditRatio.toFixed(2) },
372
+ { label: 'Output/Input Ratio', value: profile.signals.outputInputRatio.toFixed(2) },
373
+ { label: 'Avg Msg Length', value: `${Math.round(profile.signals.avgAssistantMsgLength)} tok` },
374
+ { label: 'Bash/Total Ratio', value: profile.signals.bashToTotalRatio.toFixed(3) },
375
+ { label: 'Edit/Read Ratio', value: profile.signals.editToReadRatio.toFixed(2) },
376
+ { label: 'Tool Entropy', value: profile.signals.toolTransitionEntropy.toFixed(2) },
377
+ { label: 'Duration CV', value: profile.signals.sessionDurationVariance.toFixed(2) },
378
+ { label: 'Tools/Message', value: profile.signals.avgToolCallsPerMessage.toFixed(2) },
379
+ { label: 'Overflow Rate', value: `${(profile.signals.contextOverflowRate * 100).toFixed(1)}%` },
380
+ ].map(({ label, value }) => (
381
+ <div key={label} className="rounded-lg border p-3">
382
+ <div className="text-xs text-muted-foreground">{label}</div>
383
+ <div className="text-lg font-semibold">{value}</div>
384
+ </div>
385
+ ))}
386
+ </div>
387
+ </CardContent>
388
+ </Card>
389
+
390
+ {/* Recommendations */}
391
+ {profile.recommendations.length > 0 && (
392
+ <Card>
393
+ <CardHeader>
394
+ <div className="flex items-center gap-2">
395
+ <Sparkles className="h-5 w-5 text-primary" />
396
+ <div>
397
+ <CardTitle>Personality Tuning Recommendations</CardTitle>
398
+ <CardDescription>
399
+ Adjustments to better fit the {TASK_OPTIONS.find(t => t.value === targetTask)?.label} personality profile
400
+ </CardDescription>
401
+ </div>
402
+ </div>
403
+ </CardHeader>
404
+ <CardContent>
405
+ <div className="space-y-4">
406
+ {profile.recommendations.map((rec) => (
407
+ <div key={rec.trait} className="rounded-lg border p-4">
408
+ <div className="flex items-center gap-2 mb-2">
409
+ <Badge variant="outline">{rec.trait}</Badge>
410
+ <span className="text-sm text-muted-foreground">{rec.current}</span>
411
+ <ArrowRight className="h-3 w-3 text-muted-foreground" />
412
+ <span className="text-sm font-medium">{rec.ideal}</span>
413
+ </div>
414
+ <p className="text-sm text-muted-foreground">{rec.promptSnippet}</p>
415
+ </div>
416
+ ))}
417
+ </div>
418
+ </CardContent>
419
+ </Card>
420
+ )}
421
+
422
+ {/* System Prompt */}
423
+ <Card>
424
+ <CardHeader>
425
+ <div className="flex items-center justify-between">
426
+ <div>
427
+ <CardTitle>Generated System Prompt</CardTitle>
428
+ <CardDescription>
429
+ Add this to your AI assistant's system prompt to tune its personality for {TASK_OPTIONS.find(t => t.value === targetTask)?.label}
430
+ </CardDescription>
431
+ </div>
432
+ <Button variant="outline" size="sm" onClick={handleCopyPrompt} className="gap-2">
433
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
434
+ {copied ? 'Copied' : 'Copy'}
435
+ </Button>
436
+ </div>
437
+ </CardHeader>
438
+ <CardContent>
439
+ <pre className="max-h-96 overflow-auto rounded-lg bg-muted p-4 text-sm whitespace-pre-wrap">
440
+ {profile.systemPrompt}
441
+ </pre>
442
+ </CardContent>
443
+ </Card>
444
+ </div>
445
+ )
446
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@/components/ui/table'
12
+ import { PaginationControls } from '@/components/pagination-controls'
13
+ import { usePagination } from '@/hooks/use-pagination'
14
+ import type { ProjectSummary } from '@/lib/parse-logs'
15
+ import { formatCost, formatTokens, formatDuration, formatNumber } from '@/lib/format'
16
+
17
+ export function ProjectsTable({ projects }: { projects: ProjectSummary[] }) {
18
+ const pagination = usePagination(projects, 20)
19
+
20
+ const topTools = (toolCalls: Record<string, number>) =>
21
+ Object.entries(toolCalls)
22
+ .sort((a, b) => b[1] - a[1])
23
+ .slice(0, 3)
24
+ .map(([name, count]) => `${name} (${count})`)
25
+ .join(', ')
26
+
27
+ return (
28
+ <Card>
29
+ <CardHeader>
30
+ <CardTitle>Projects</CardTitle>
31
+ <CardDescription>{projects.length} projects tracked</CardDescription>
32
+ </CardHeader>
33
+ <CardContent>
34
+ <Table>
35
+ <TableHeader>
36
+ <TableRow>
37
+ <TableHead>Project</TableHead>
38
+ <TableHead className="text-right">Sessions</TableHead>
39
+ <TableHead className="text-right">Messages</TableHead>
40
+ <TableHead className="text-right">Tokens</TableHead>
41
+ <TableHead className="text-right">Cost</TableHead>
42
+ <TableHead className="text-right">Duration</TableHead>
43
+ <TableHead>Top Tools</TableHead>
44
+ </TableRow>
45
+ </TableHeader>
46
+ <TableBody>
47
+ {pagination.pageItems.map((p) => (
48
+ <TableRow key={p.name}>
49
+ <TableCell className="font-medium">{p.name}</TableCell>
50
+ <TableCell className="text-right">{formatNumber(p.sessions)}</TableCell>
51
+ <TableCell className="text-right">{formatNumber(p.totalMessages)}</TableCell>
52
+ <TableCell className="text-right">{formatTokens(p.totalTokens)}</TableCell>
53
+ <TableCell className="text-right">{formatCost(p.totalCost)}</TableCell>
54
+ <TableCell className="text-right">
55
+ {formatDuration(p.totalDurationMinutes)}
56
+ </TableCell>
57
+ <TableCell className="text-xs text-muted-foreground">
58
+ {topTools(p.toolCalls)}
59
+ </TableCell>
60
+ </TableRow>
61
+ ))}
62
+ </TableBody>
63
+ </Table>
64
+ {pagination.totalPages > 1 && (
65
+ <PaginationControls pagination={pagination} noun="projects" />
66
+ )}
67
+ </CardContent>
68
+ </Card>
69
+ )
70
+ }