prjct-cli 0.11.4 → 0.11.5

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 (33) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/serve.js +22 -6
  3. package/core/__tests__/utils/date-helper.test.js +416 -0
  4. package/core/agentic/agent-router.js +30 -18
  5. package/core/agentic/command-executor.js +20 -24
  6. package/core/agentic/context-builder.js +7 -8
  7. package/core/agentic/memory-system.js +14 -19
  8. package/core/agentic/prompt-builder.js +41 -27
  9. package/core/agentic/template-loader.js +8 -2
  10. package/core/infrastructure/agent-detector.js +7 -4
  11. package/core/infrastructure/migrator.js +10 -13
  12. package/core/infrastructure/session-manager.js +10 -10
  13. package/package.json +1 -1
  14. package/packages/web/app/project/[id]/stats/page.tsx +102 -343
  15. package/packages/web/components/stats/ActivityTimeline.tsx +201 -0
  16. package/packages/web/components/stats/AgentsCard.tsx +56 -0
  17. package/packages/web/components/stats/BentoCard.tsx +88 -0
  18. package/packages/web/components/stats/BentoGrid.tsx +22 -0
  19. package/packages/web/components/stats/EmptyState.tsx +67 -0
  20. package/packages/web/components/stats/HeroSection.tsx +172 -0
  21. package/packages/web/components/stats/IdeasCard.tsx +59 -0
  22. package/packages/web/components/stats/NowCard.tsx +71 -0
  23. package/packages/web/components/stats/ProgressRing.tsx +74 -0
  24. package/packages/web/components/stats/QueueCard.tsx +58 -0
  25. package/packages/web/components/stats/RoadmapCard.tsx +97 -0
  26. package/packages/web/components/stats/ShipsCard.tsx +70 -0
  27. package/packages/web/components/stats/SparklineChart.tsx +44 -0
  28. package/packages/web/components/stats/StreakCard.tsx +59 -0
  29. package/packages/web/components/stats/VelocityCard.tsx +60 -0
  30. package/packages/web/components/stats/index.ts +17 -0
  31. package/packages/web/components/ui/tooltip.tsx +2 -2
  32. package/packages/web/next-env.d.ts +1 -1
  33. package/packages/web/package.json +2 -1
@@ -0,0 +1,201 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState } from 'react'
4
+ import { BentoCard } from './BentoCard'
5
+ import { EmptyState } from './EmptyState'
6
+ import { Activity, CheckCircle2, Rocket, Target, RefreshCw, ChevronDown } from 'lucide-react'
7
+ import { cn } from '@/lib/utils'
8
+ import { Button } from '@/components/ui/button'
9
+ import type { TimelineEvent } from '@/lib/parse-prjct-files'
10
+
11
+ interface ActivityTimelineProps {
12
+ timeline: TimelineEvent[]
13
+ className?: string
14
+ }
15
+
16
+ function getEventIcon(type: string) {
17
+ switch (type) {
18
+ case 'task_complete':
19
+ return CheckCircle2
20
+ case 'task_start':
21
+ return Target
22
+ case 'feature_ship':
23
+ return Rocket
24
+ case 'sync':
25
+ return RefreshCw
26
+ default:
27
+ return Activity
28
+ }
29
+ }
30
+
31
+ function getEventColor(type: string) {
32
+ switch (type) {
33
+ case 'task_complete':
34
+ return 'text-emerald-500'
35
+ case 'task_start':
36
+ return 'text-amber-500'
37
+ case 'feature_ship':
38
+ return 'text-blue-500'
39
+ case 'sync':
40
+ return 'text-muted-foreground'
41
+ default:
42
+ return 'text-muted-foreground'
43
+ }
44
+ }
45
+
46
+ function getEventLabel(event: TimelineEvent): string {
47
+ const e = event as Record<string, unknown>
48
+ switch (event.type) {
49
+ case 'task_complete':
50
+ return (e.task as string) || 'Task completed'
51
+ case 'task_start':
52
+ return (e.task as string) || 'Task started'
53
+ case 'feature_ship':
54
+ return (e.name as string) || 'Feature shipped'
55
+ case 'sync':
56
+ return 'Project synced'
57
+ default:
58
+ return event.type
59
+ }
60
+ }
61
+
62
+ function getEventBadge(type: string): string {
63
+ switch (type) {
64
+ case 'task_complete':
65
+ return 'DONE'
66
+ case 'task_start':
67
+ return 'START'
68
+ case 'feature_ship':
69
+ return 'SHIP'
70
+ case 'sync':
71
+ return 'SYNC'
72
+ default:
73
+ return type.toUpperCase()
74
+ }
75
+ }
76
+
77
+ function formatTime(dateString: string): string {
78
+ const date = new Date(dateString)
79
+ return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
80
+ }
81
+
82
+ function formatDate(dateString: string): string {
83
+ const date = new Date(dateString)
84
+ const today = new Date()
85
+ const yesterday = new Date(today)
86
+ yesterday.setDate(yesterday.getDate() - 1)
87
+
88
+ if (date.toDateString() === today.toDateString()) return 'Today'
89
+ if (date.toDateString() === yesterday.toDateString()) return 'Yesterday'
90
+
91
+ return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
92
+ }
93
+
94
+ // Group events by date
95
+ function groupEventsByDate(events: TimelineEvent[]): Map<string, TimelineEvent[]> {
96
+ const groups = new Map<string, TimelineEvent[]>()
97
+
98
+ events.forEach(event => {
99
+ if (!event.ts) return
100
+ const dateKey = event.ts.split('T')[0]
101
+ const existing = groups.get(dateKey) || []
102
+ groups.set(dateKey, [...existing, event])
103
+ })
104
+
105
+ return groups
106
+ }
107
+
108
+ export function ActivityTimeline({ timeline, className }: ActivityTimelineProps) {
109
+ const [showAll, setShowAll] = useState(false)
110
+
111
+ const displayEvents = showAll ? timeline.slice(0, 20) : timeline.slice(0, 8)
112
+ const groupedEvents = useMemo(() => groupEventsByDate(displayEvents), [displayEvents])
113
+ const hasMore = timeline.length > displayEvents.length
114
+
115
+ return (
116
+ <BentoCard
117
+ size="full"
118
+ title="Recent Activity"
119
+ icon={Activity}
120
+ count={timeline.length > 0 ? `${timeline.length} events` : undefined}
121
+ className={className}
122
+ >
123
+ {timeline.length === 0 ? (
124
+ <EmptyState
125
+ icon={Activity}
126
+ title="No recent activity"
127
+ description="Activity will appear as you work"
128
+ compact
129
+ />
130
+ ) : (
131
+ <div className="space-y-4">
132
+ {Array.from(groupedEvents.entries()).map(([dateKey, events]) => (
133
+ <div key={dateKey}>
134
+ {/* Date header */}
135
+ <p className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground mb-2">
136
+ {formatDate(dateKey + 'T00:00:00')}
137
+ </p>
138
+
139
+ {/* Events for this date */}
140
+ <div className="space-y-1">
141
+ {events.map((event, i) => {
142
+ const Icon = getEventIcon(event.type)
143
+ const e = event as Record<string, unknown>
144
+
145
+ const duration = typeof e.duration === 'string' ? e.duration : null
146
+
147
+ return (
148
+ <div
149
+ key={i}
150
+ className="flex items-center gap-3 py-1.5 px-2 -mx-2 rounded-md hover:bg-muted/50 transition-colors group"
151
+ >
152
+ {/* Time */}
153
+ {event.ts && (
154
+ <span className="text-[10px] text-muted-foreground w-14 shrink-0 tabular-nums">
155
+ {formatTime(event.ts)}
156
+ </span>
157
+ )}
158
+
159
+ {/* Icon */}
160
+ <Icon className={cn('h-3.5 w-3.5 shrink-0', getEventColor(event.type))} />
161
+
162
+ {/* Label */}
163
+ <span className="text-sm truncate flex-1 group-hover:text-foreground transition-colors">
164
+ {getEventLabel(event)}
165
+ </span>
166
+
167
+ {/* Badge */}
168
+ <span className="text-[9px] font-bold tracking-wider text-muted-foreground shrink-0">
169
+ {getEventBadge(event.type)}
170
+ </span>
171
+
172
+ {/* Duration if available */}
173
+ {duration && (
174
+ <span className="text-[10px] text-muted-foreground shrink-0">
175
+ {duration}
176
+ </span>
177
+ )}
178
+ </div>
179
+ )
180
+ })}
181
+ </div>
182
+ </div>
183
+ ))}
184
+
185
+ {/* Load more button */}
186
+ {hasMore && (
187
+ <Button
188
+ variant="ghost"
189
+ size="sm"
190
+ onClick={() => setShowAll(!showAll)}
191
+ className="w-full text-muted-foreground"
192
+ >
193
+ <ChevronDown className={cn('h-4 w-4 mr-1 transition-transform', showAll && 'rotate-180')} />
194
+ {showAll ? 'Show less' : `Show ${timeline.length - 8} more`}
195
+ </Button>
196
+ )}
197
+ </div>
198
+ )}
199
+ </BentoCard>
200
+ )
201
+ }
@@ -0,0 +1,56 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { Bot } from 'lucide-react'
6
+ import { Badge } from '@/components/ui/badge'
7
+
8
+ interface Agent {
9
+ name: string
10
+ description?: string
11
+ }
12
+
13
+ interface AgentsCardProps {
14
+ agents: Agent[]
15
+ className?: string
16
+ }
17
+
18
+ export function AgentsCard({ agents, className }: AgentsCardProps) {
19
+ const displayAgents = agents.slice(0, 8)
20
+
21
+ return (
22
+ <BentoCard
23
+ size="1x1"
24
+ title="Agents"
25
+ icon={Bot}
26
+ count={agents.length}
27
+ className={className}
28
+ >
29
+ {agents.length === 0 ? (
30
+ <EmptyState
31
+ icon={Bot}
32
+ title="No agents"
33
+ description="Run /p:sync to generate"
34
+ compact
35
+ />
36
+ ) : (
37
+ <div className="flex flex-wrap gap-1.5">
38
+ {displayAgents.map((agent) => (
39
+ <Badge
40
+ key={agent.name}
41
+ variant="secondary"
42
+ className="text-xs px-2 py-0.5 font-mono"
43
+ >
44
+ @{agent.name}
45
+ </Badge>
46
+ ))}
47
+ {agents.length > 8 && (
48
+ <Badge variant="outline" className="text-xs px-2 py-0.5">
49
+ +{agents.length - 8}
50
+ </Badge>
51
+ )}
52
+ </div>
53
+ )}
54
+ </BentoCard>
55
+ )
56
+ }
@@ -0,0 +1,88 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import type { LucideIcon } from 'lucide-react'
4
+
5
+ export type BentoSize = '1x1' | '1x2' | '2x1' | '2x2' | 'full'
6
+
7
+ const sizeClasses: Record<BentoSize, string> = {
8
+ '1x1': 'col-span-1 row-span-1',
9
+ '1x2': 'col-span-1 row-span-2',
10
+ '2x1': 'col-span-2 row-span-1',
11
+ '2x2': 'col-span-2 row-span-2',
12
+ 'full': 'col-span-full',
13
+ }
14
+
15
+ export interface BentoCardProps {
16
+ size?: BentoSize
17
+ title?: string
18
+ count?: number | string
19
+ icon?: LucideIcon
20
+ accentColor?: 'default' | 'success' | 'warning' | 'destructive'
21
+ className?: string
22
+ headerClassName?: string
23
+ children: React.ReactNode
24
+ }
25
+
26
+ const accentStyles: Record<NonNullable<BentoCardProps['accentColor']>, string> = {
27
+ default: '',
28
+ success: 'border-emerald-500/20 bg-emerald-500/5',
29
+ warning: 'border-amber-500/20 bg-amber-500/5',
30
+ destructive: 'border-destructive/20 bg-destructive/5',
31
+ }
32
+
33
+ export function BentoCard({
34
+ size = '1x1',
35
+ title,
36
+ count,
37
+ icon: Icon,
38
+ accentColor = 'default',
39
+ className,
40
+ headerClassName,
41
+ children,
42
+ }: BentoCardProps) {
43
+ return (
44
+ <div
45
+ className={cn(
46
+ 'relative overflow-hidden rounded-xl border bg-card p-4 transition-all duration-200',
47
+ 'hover:shadow-md hover:border-foreground/20',
48
+ sizeClasses[size],
49
+ accentStyles[accentColor],
50
+ className
51
+ )}
52
+ >
53
+ {(title || count !== undefined || Icon) && (
54
+ <div className={cn('flex items-center justify-between mb-3', headerClassName)}>
55
+ <div className="flex items-center gap-2">
56
+ {Icon && <Icon className="h-3.5 w-3.5 text-muted-foreground" />}
57
+ {title && (
58
+ <span className="text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
59
+ {title}
60
+ </span>
61
+ )}
62
+ </div>
63
+ {count !== undefined && (
64
+ <span className="text-xs font-medium text-muted-foreground tabular-nums">
65
+ {count}
66
+ </span>
67
+ )}
68
+ </div>
69
+ )}
70
+ {children}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export function BentoCardSkeleton({ size = '1x1' }: { size?: BentoSize }) {
76
+ return (
77
+ <div
78
+ className={cn(
79
+ 'rounded-xl border bg-card p-4 animate-pulse',
80
+ sizeClasses[size]
81
+ )}
82
+ >
83
+ <div className="h-3 w-16 bg-muted rounded mb-3" />
84
+ <div className="h-6 w-24 bg-muted rounded mb-2" />
85
+ <div className="h-3 w-full bg-muted rounded" />
86
+ </div>
87
+ )
88
+ }
@@ -0,0 +1,22 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+
4
+ interface BentoGridProps {
5
+ className?: string
6
+ children: React.ReactNode
7
+ }
8
+
9
+ export function BentoGrid({ className, children }: BentoGridProps) {
10
+ return (
11
+ <div
12
+ className={cn(
13
+ 'grid gap-4',
14
+ 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
15
+ 'auto-rows-[minmax(140px,auto)]',
16
+ className
17
+ )}
18
+ >
19
+ {children}
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { cn } from '@/lib/utils'
4
+ import { Copy } from 'lucide-react'
5
+ import type { LucideIcon } from 'lucide-react'
6
+
7
+ interface EmptyStateProps {
8
+ icon: LucideIcon
9
+ title: string
10
+ description?: string
11
+ command?: string
12
+ className?: string
13
+ compact?: boolean
14
+ }
15
+
16
+ export function EmptyState({
17
+ icon: Icon,
18
+ title,
19
+ description,
20
+ command,
21
+ className,
22
+ compact = false,
23
+ }: EmptyStateProps) {
24
+ const handleCopy = () => {
25
+ if (command) {
26
+ navigator.clipboard.writeText(command)
27
+ }
28
+ }
29
+
30
+ if (compact) {
31
+ return (
32
+ <div className={cn('flex items-center gap-2 text-muted-foreground', className)}>
33
+ <Icon className="h-4 w-4" />
34
+ <span className="text-sm">{title}</span>
35
+ {command && (
36
+ <button
37
+ onClick={handleCopy}
38
+ className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-muted text-xs font-mono hover:bg-muted/80"
39
+ >
40
+ {command}
41
+ </button>
42
+ )}
43
+ </div>
44
+ )
45
+ }
46
+
47
+ return (
48
+ <div className={cn('flex flex-col items-center justify-center text-center py-4', className)}>
49
+ <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center mb-3">
50
+ <Icon className="h-5 w-5 text-muted-foreground" />
51
+ </div>
52
+ <p className="text-sm font-medium text-muted-foreground">{title}</p>
53
+ {description && (
54
+ <p className="text-xs text-muted-foreground/70 mt-1">{description}</p>
55
+ )}
56
+ {command && (
57
+ <button
58
+ onClick={handleCopy}
59
+ className="mt-2 inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-muted text-xs font-mono text-muted-foreground hover:bg-muted/80 hover:text-foreground transition-colors group"
60
+ >
61
+ {command}
62
+ <Copy className="h-3 w-3 opacity-0 group-hover:opacity-100 transition-opacity" />
63
+ </button>
64
+ )}
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,172 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useMemo } from 'react'
4
+ import Link from 'next/link'
5
+ import { cn } from '@/lib/utils'
6
+ import { ArrowLeft, TrendingUp, TrendingDown } from 'lucide-react'
7
+ import { ProgressRing } from './ProgressRing'
8
+ import { SparklineChart } from './SparklineChart'
9
+ import type { TimelineEvent } from '@/lib/parse-prjct-files'
10
+
11
+ interface HeroSectionProps {
12
+ projectId: string
13
+ projectName: string
14
+ tasksCompleted: number
15
+ healthScore: number
16
+ velocity: number
17
+ velocityChange: number
18
+ insightMessage: string
19
+ timeline?: TimelineEvent[]
20
+ }
21
+
22
+ // Animated count-up hook
23
+ function useCountUp(target: number, duration: number = 800) {
24
+ const [count, setCount] = useState(0)
25
+
26
+ useEffect(() => {
27
+ if (target === 0) {
28
+ setCount(0)
29
+ return
30
+ }
31
+
32
+ let start = 0
33
+ const startTime = performance.now()
34
+
35
+ const animate = (currentTime: number) => {
36
+ const elapsed = currentTime - startTime
37
+ const progress = Math.min(elapsed / duration, 1)
38
+
39
+ // Easing function (ease-out cubic)
40
+ const easeOut = 1 - Math.pow(1 - progress, 3)
41
+ const current = Math.floor(easeOut * target)
42
+
43
+ setCount(current)
44
+
45
+ if (progress < 1) {
46
+ requestAnimationFrame(animate)
47
+ } else {
48
+ setCount(target)
49
+ }
50
+ }
51
+
52
+ requestAnimationFrame(animate)
53
+ }, [target, duration])
54
+
55
+ return count
56
+ }
57
+
58
+ // Calculate 7-day activity data from timeline
59
+ function getWeeklyActivityData(timeline: TimelineEvent[]): number[] {
60
+ const today = new Date()
61
+ const counts: number[] = []
62
+
63
+ for (let i = 6; i >= 0; i--) {
64
+ const date = new Date(today)
65
+ date.setDate(date.getDate() - i)
66
+ const dateStr = date.toISOString().split('T')[0]
67
+
68
+ const count = timeline.filter(e => {
69
+ if (!e.ts) return false
70
+ return e.ts.startsWith(dateStr)
71
+ }).length
72
+
73
+ counts.push(count)
74
+ }
75
+
76
+ return counts
77
+ }
78
+
79
+ export function HeroSection({
80
+ projectId,
81
+ projectName,
82
+ tasksCompleted,
83
+ healthScore,
84
+ velocity,
85
+ velocityChange,
86
+ insightMessage,
87
+ timeline = [],
88
+ }: HeroSectionProps) {
89
+ const animatedCount = useCountUp(tasksCompleted)
90
+ const weeklyData = useMemo(() => getWeeklyActivityData(timeline), [timeline])
91
+
92
+ const healthColor = healthScore >= 70 ? 'success' : healthScore >= 40 ? 'warning' : 'destructive'
93
+
94
+ return (
95
+ <div className="relative mb-8">
96
+ {/* Background gradient based on health */}
97
+ <div
98
+ className={cn(
99
+ 'absolute inset-0 -m-8 rounded-2xl opacity-30 blur-3xl transition-colors duration-1000',
100
+ healthScore >= 70 && 'bg-gradient-to-br from-emerald-500/20 to-transparent',
101
+ healthScore >= 40 && healthScore < 70 && 'bg-gradient-to-br from-amber-500/20 to-transparent',
102
+ healthScore < 40 && 'bg-gradient-to-br from-destructive/20 to-transparent'
103
+ )}
104
+ />
105
+
106
+ <div className="relative flex items-start justify-between">
107
+ {/* Left: Health ring + Main metric */}
108
+ <div className="flex items-start gap-6">
109
+ <ProgressRing
110
+ value={healthScore}
111
+ size="xl"
112
+ accentColor={healthColor}
113
+ />
114
+
115
+ <div>
116
+ {/* Tasks completed - big number */}
117
+ <div className="flex items-baseline gap-3">
118
+ <span className="text-7xl font-bold tracking-tighter tabular-nums">
119
+ {animatedCount}
120
+ </span>
121
+ <span className="text-lg text-muted-foreground">tasks completed</span>
122
+ </div>
123
+
124
+ {/* Velocity trend */}
125
+ {velocityChange !== 0 && (
126
+ <div className="flex items-center gap-2 mt-2">
127
+ <span
128
+ className={cn(
129
+ 'inline-flex items-center gap-1 text-sm font-medium px-2 py-0.5 rounded-md',
130
+ velocityChange >= 0
131
+ ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
132
+ : 'bg-muted text-muted-foreground'
133
+ )}
134
+ >
135
+ {velocityChange >= 0 ? (
136
+ <TrendingUp className="h-3.5 w-3.5" />
137
+ ) : (
138
+ <TrendingDown className="h-3.5 w-3.5" />
139
+ )}
140
+ {velocityChange >= 0 ? '+' : ''}{velocityChange}%
141
+ </span>
142
+ <span className="text-sm text-muted-foreground">vs last week</span>
143
+ </div>
144
+ )}
145
+
146
+ {/* Insight message */}
147
+ <p className="text-muted-foreground mt-3 max-w-md">{insightMessage}</p>
148
+ </div>
149
+ </div>
150
+
151
+ {/* Right: Sparkline + Navigation */}
152
+ <div className="flex flex-col items-end gap-4">
153
+ <Link
154
+ href={`/project/${projectId}`}
155
+ className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
156
+ >
157
+ <ArrowLeft className="w-4 h-4" />
158
+ {projectName}
159
+ </Link>
160
+
161
+ {/* 7-day sparkline */}
162
+ <div className="w-32">
163
+ <p className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1 text-right">
164
+ 7-day activity
165
+ </p>
166
+ <SparklineChart data={weeklyData} height={40} />
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
@@ -0,0 +1,59 @@
1
+ 'use client'
2
+
3
+ import { BentoCard } from './BentoCard'
4
+ import { EmptyState } from './EmptyState'
5
+ import { Lightbulb } from 'lucide-react'
6
+ import { cn } from '@/lib/utils'
7
+
8
+ interface Idea {
9
+ title: string
10
+ impact?: string
11
+ }
12
+
13
+ interface IdeasCardProps {
14
+ ideas: Idea[]
15
+ className?: string
16
+ }
17
+
18
+ export function IdeasCard({ ideas, className }: IdeasCardProps) {
19
+ const displayIdeas = ideas.slice(0, 4)
20
+ const highImpactCount = ideas.filter(i => i.impact === 'HIGH').length
21
+
22
+ return (
23
+ <BentoCard
24
+ size="1x1"
25
+ title="Ideas"
26
+ icon={Lightbulb}
27
+ count={ideas.length}
28
+ className={className}
29
+ >
30
+ {ideas.length === 0 ? (
31
+ <EmptyState
32
+ icon={Lightbulb}
33
+ title="No ideas yet"
34
+ command="/p:idea"
35
+ compact
36
+ />
37
+ ) : (
38
+ <div className="space-y-1.5">
39
+ {displayIdeas.map((idea, i) => (
40
+ <div key={i} className="flex items-start gap-2">
41
+ <Lightbulb
42
+ className={cn(
43
+ 'h-3 w-3 mt-0.5 shrink-0',
44
+ idea.impact === 'HIGH' ? 'text-amber-500' : 'text-muted-foreground'
45
+ )}
46
+ />
47
+ <p className="text-sm truncate">{idea.title}</p>
48
+ </div>
49
+ ))}
50
+ {highImpactCount > 0 && (
51
+ <p className="text-[10px] text-amber-600 dark:text-amber-400 mt-2 pl-5 font-medium">
52
+ {highImpactCount} high impact
53
+ </p>
54
+ )}
55
+ </div>
56
+ )}
57
+ </BentoCard>
58
+ )
59
+ }