stagent 0.1.11 → 0.1.13

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 (145) hide show
  1. package/README.md +74 -49
  2. package/package.json +3 -2
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-workflow-confirm.png +0 -0
  13. package/public/readme/home-below-fold.png +0 -0
  14. package/public/readme/home-list.png +0 -0
  15. package/public/readme/inbox-list.png +0 -0
  16. package/public/readme/playbook-list.png +0 -0
  17. package/public/readme/profiles-list.png +0 -0
  18. package/public/readme/settings-list.png +0 -0
  19. package/public/readme/workflows-list.png +0 -0
  20. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  21. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  22. package/src/__tests__/e2e/helpers.ts +286 -0
  23. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  24. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  25. package/src/__tests__/e2e/setup.ts +156 -0
  26. package/src/__tests__/e2e/single-task.test.ts +170 -0
  27. package/src/app/api/command-palette/recent/route.ts +41 -18
  28. package/src/app/api/context/batch/route.ts +44 -0
  29. package/src/app/api/permissions/presets/route.ts +80 -0
  30. package/src/app/api/playbook/status/route.ts +15 -0
  31. package/src/app/api/profiles/route.ts +23 -20
  32. package/src/app/api/settings/pricing/route.ts +15 -0
  33. package/src/app/api/tasks/[id]/route.ts +54 -3
  34. package/src/app/api/workflows/[id]/route.ts +43 -4
  35. package/src/app/api/workflows/[id]/status/route.ts +70 -2
  36. package/src/app/api/workflows/from-assist/route.ts +6 -32
  37. package/src/app/costs/page.tsx +53 -43
  38. package/src/app/dashboard/page.tsx +59 -21
  39. package/src/app/documents/[id]/page.tsx +10 -8
  40. package/src/app/globals.css +11 -0
  41. package/src/app/page.tsx +60 -3
  42. package/src/app/playbook/[slug]/page.tsx +76 -0
  43. package/src/app/playbook/page.tsx +54 -0
  44. package/src/app/profiles/page.tsx +7 -4
  45. package/src/app/settings/page.tsx +2 -2
  46. package/src/app/tasks/[id]/page.tsx +22 -2
  47. package/src/components/costs/cost-dashboard.tsx +226 -320
  48. package/src/components/dashboard/activity-feed.tsx +6 -2
  49. package/src/components/dashboard/greeting.tsx +3 -1
  50. package/src/components/dashboard/priority-queue.tsx +58 -9
  51. package/src/components/dashboard/stats-cards.tsx +16 -2
  52. package/src/components/documents/document-chip-bar.tsx +183 -0
  53. package/src/components/documents/document-content-renderer.tsx +146 -0
  54. package/src/components/documents/document-detail-view.tsx +16 -239
  55. package/src/components/documents/image-zoom-view.tsx +60 -0
  56. package/src/components/documents/smart-extracted-text.tsx +47 -0
  57. package/src/components/documents/utils.ts +70 -0
  58. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  59. package/src/components/notifications/inbox-list.tsx +4 -5
  60. package/src/components/notifications/notification-item.tsx +73 -6
  61. package/src/components/notifications/pending-approval-host.tsx +63 -14
  62. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  63. package/src/components/playbook/journey-card.tsx +110 -0
  64. package/src/components/playbook/playbook-action-button.tsx +22 -0
  65. package/src/components/playbook/playbook-browser.tsx +143 -0
  66. package/src/components/playbook/playbook-card.tsx +102 -0
  67. package/src/components/playbook/playbook-detail-view.tsx +225 -0
  68. package/src/components/playbook/playbook-homepage.tsx +142 -0
  69. package/src/components/playbook/playbook-toc.tsx +90 -0
  70. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  71. package/src/components/playbook/related-docs.tsx +30 -0
  72. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  73. package/src/components/profiles/context-proposal-review.tsx +7 -3
  74. package/src/components/profiles/learned-context-panel.tsx +116 -8
  75. package/src/components/profiles/profile-browser.tsx +1 -0
  76. package/src/components/profiles/profile-card.tsx +16 -8
  77. package/src/components/profiles/profile-detail-view.tsx +12 -4
  78. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  79. package/src/components/settings/api-key-form.tsx +5 -43
  80. package/src/components/settings/auth-config-section.tsx +10 -6
  81. package/src/components/settings/auth-status-badge.tsx +8 -0
  82. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  83. package/src/components/settings/connection-test-control.tsx +63 -0
  84. package/src/components/settings/permissions-section.tsx +85 -75
  85. package/src/components/settings/permissions-sections.tsx +24 -0
  86. package/src/components/settings/presets-section.tsx +159 -0
  87. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  88. package/src/components/shared/app-sidebar.tsx +4 -2
  89. package/src/components/shared/command-palette.tsx +30 -0
  90. package/src/components/shared/light-markdown.tsx +134 -0
  91. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
  92. package/src/components/tasks/ai-assist-panel.tsx +108 -78
  93. package/src/components/tasks/content-preview.tsx +2 -1
  94. package/src/components/tasks/kanban-board.tsx +57 -5
  95. package/src/components/tasks/kanban-column.tsx +34 -23
  96. package/src/components/tasks/task-bento-cell.tsx +50 -0
  97. package/src/components/tasks/task-bento-grid.tsx +155 -0
  98. package/src/components/tasks/task-card.tsx +14 -16
  99. package/src/components/tasks/task-chip-bar.tsx +207 -0
  100. package/src/components/tasks/task-detail-view.tsx +42 -190
  101. package/src/components/tasks/task-result-renderer.tsx +33 -0
  102. package/src/components/workflows/blueprint-gallery.tsx +19 -12
  103. package/src/components/workflows/blueprint-preview.tsx +8 -1
  104. package/src/components/workflows/loop-status-view.tsx +2 -4
  105. package/src/components/workflows/swarm-dashboard.tsx +2 -3
  106. package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
  107. package/src/components/workflows/workflow-full-output.tsx +80 -0
  108. package/src/components/workflows/workflow-kanban-card.tsx +121 -0
  109. package/src/components/workflows/workflow-list.tsx +47 -42
  110. package/src/components/workflows/workflow-status-view.tsx +163 -16
  111. package/src/lib/agents/learned-context.ts +27 -15
  112. package/src/lib/agents/learning-session.ts +354 -0
  113. package/src/lib/agents/pattern-extractor.ts +19 -0
  114. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  115. package/src/lib/agents/profiles/sort.ts +7 -0
  116. package/src/lib/constants/card-icons.tsx +202 -0
  117. package/src/lib/constants/prose-styles.ts +7 -0
  118. package/src/lib/constants/settings.ts +1 -0
  119. package/src/lib/constants/task-status.ts +3 -0
  120. package/src/lib/db/schema.ts +3 -0
  121. package/src/lib/docs/adoption.ts +105 -0
  122. package/src/lib/docs/journey-tracker.ts +21 -0
  123. package/src/lib/docs/reader.ts +107 -0
  124. package/src/lib/docs/types.ts +54 -0
  125. package/src/lib/docs/usage-stage.ts +60 -0
  126. package/src/lib/documents/context-builder.ts +41 -0
  127. package/src/lib/notifications/actionable.ts +18 -10
  128. package/src/lib/queries/chart-data.ts +20 -1
  129. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  130. package/src/lib/settings/budget-guardrails.ts +213 -85
  131. package/src/lib/settings/permission-presets.ts +150 -0
  132. package/src/lib/settings/runtime-setup.ts +71 -0
  133. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  134. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  135. package/src/lib/usage/ledger.ts +1 -1
  136. package/src/lib/usage/pricing-registry.ts +570 -0
  137. package/src/lib/usage/pricing.ts +15 -95
  138. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  139. package/src/lib/utils/learned-context-history.ts +150 -0
  140. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  141. package/src/lib/validators/settings.ts +3 -9
  142. package/src/lib/workflows/engine.ts +75 -61
  143. package/src/lib/workflows/types.ts +2 -0
  144. package/tsconfig.json +2 -1
  145. package/src/components/documents/document-preview.tsx +0 -68
@@ -2,9 +2,11 @@
2
2
 
3
3
  import { Badge } from "@/components/ui/badge";
4
4
  import { Card } from "@/components/ui/card";
5
- import { Shield, MessageCircle, CheckCircle, XCircle, Eye, EyeOff, Trash2, Wallet } from "lucide-react";
5
+ import { Shield, MessageCircle, CheckCircle, XCircle, Eye, EyeOff, Trash2, Wallet, Brain, ChevronRight, ChevronDown } from "lucide-react";
6
6
  import { Button } from "@/components/ui/button";
7
+ import { LightMarkdown } from "@/components/shared/light-markdown";
7
8
  import { useState } from "react";
9
+ import { useRouter } from "next/navigation";
8
10
  import { PermissionAction } from "./permission-action";
9
11
  import { MessageResponse, type Question } from "./message-response";
10
12
  import { FailureAction } from "./failure-action";
@@ -40,6 +42,8 @@ const typeIcons: Record<string, React.ReactNode> = {
40
42
  task_completed: <CheckCircle className="h-4 w-4 text-chart-2" aria-hidden="true" />,
41
43
  task_failed: <XCircle className="h-4 w-4 text-destructive" aria-hidden="true" />,
42
44
  budget_alert: <Wallet className="h-4 w-4 text-status-warning" aria-hidden="true" />,
45
+ context_proposal: <Brain className="h-4 w-4 text-chart-4" aria-hidden="true" />,
46
+ context_proposal_batch: <Brain className="h-4 w-4 text-chart-4" aria-hidden="true" />,
43
47
  };
44
48
 
45
49
  const typeLabels: Record<string, string> = {
@@ -48,6 +52,8 @@ const typeLabels: Record<string, string> = {
48
52
  task_completed: "Task completed",
49
53
  task_failed: "Task failed",
50
54
  budget_alert: "Budget alert",
55
+ context_proposal: "Self-learning",
56
+ context_proposal_batch: "Self-learning batch",
51
57
  };
52
58
 
53
59
  function formatToolInput(
@@ -94,12 +100,31 @@ function formatToolInput(
94
100
  );
95
101
  }
96
102
 
103
+ const navigableTypes = new Set(["task_completed", "task_failed", "permission_required", "agent_message"]);
104
+
97
105
  export function NotificationItem({ notification, onUpdated }: NotificationItemProps) {
106
+ const router = useRouter();
98
107
  const [toggling, setToggling] = useState(false);
99
108
  const [dismissing, setDismissing] = useState(false);
109
+ const [expanded, setExpanded] = useState(false);
100
110
  const isUnread = !notification.read;
101
111
  const hasResponse = !!notification.response;
102
112
  const parsedToolInput = parseNotificationToolInput(notification.toolInput);
113
+ const isNavigable = !!notification.taskId && navigableTypes.has(notification.type);
114
+
115
+ async function handleNavigate() {
116
+ if (!isNavigable) return;
117
+ // Mark as read on click-through
118
+ if (isUnread) {
119
+ await fetch(`/api/notifications/${notification.id}`, {
120
+ method: "PATCH",
121
+ headers: { "Content-Type": "application/json" },
122
+ body: JSON.stringify({ read: true }),
123
+ });
124
+ onUpdated();
125
+ }
126
+ router.push(`/tasks/${notification.taskId}`);
127
+ }
103
128
 
104
129
  async function toggleRead() {
105
130
  setToggling(true);
@@ -133,9 +158,17 @@ export function NotificationItem({ notification, onUpdated }: NotificationItemPr
133
158
  isUnread
134
159
  ? "surface-card border-l-4 border-l-primary"
135
160
  : "surface-card-muted"
136
- }`}
161
+ }${isNavigable ? " cursor-pointer hover:bg-accent/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" : ""}`}
137
162
  role="article"
138
163
  aria-label={`${typeLabels[notification.type] ?? "Notification"}: ${notification.title}${isUnread ? " (unread)" : ""}`}
164
+ tabIndex={isNavigable ? 0 : undefined}
165
+ onClick={isNavigable ? handleNavigate : undefined}
166
+ onKeyDown={isNavigable ? (e: React.KeyboardEvent) => {
167
+ if (e.key === "Enter" || e.key === " ") {
168
+ e.preventDefault();
169
+ handleNavigate();
170
+ }
171
+ } : undefined}
139
172
  >
140
173
  <div className="flex items-start gap-3">
141
174
  <div className="mt-0.5">{typeIcons[notification.type]}</div>
@@ -174,9 +207,43 @@ export function NotificationItem({ notification, onUpdated }: NotificationItemPr
174
207
  {notification.body &&
175
208
  notification.type !== "permission_required" &&
176
209
  notification.type !== "agent_message" && (
177
- <p className="text-sm text-muted-foreground line-clamp-3">
178
- {notification.body}
179
- </p>
210
+ <div className="mt-1" onClick={(e) => e.stopPropagation()}>
211
+ {expanded ? (
212
+ <>
213
+ <div className="prose prose-sm dark:prose-invert max-w-none max-h-96 overflow-auto rounded-md border bg-muted/30 p-3">
214
+ <LightMarkdown content={notification.body} textSize="sm" />
215
+ </div>
216
+ <Button
217
+ variant="ghost"
218
+ size="sm"
219
+ className="mt-1 h-6 text-xs text-muted-foreground"
220
+ onClick={(e) => { e.stopPropagation(); setExpanded(false); }}
221
+ >
222
+ <ChevronDown className="h-3 w-3 mr-1" />
223
+ Collapse
224
+ </Button>
225
+ </>
226
+ ) : (
227
+ <>
228
+ <LightMarkdown
229
+ content={notification.body}
230
+ lineClamp={2}
231
+ textSize="sm"
232
+ />
233
+ {notification.body.length > 200 && (
234
+ <Button
235
+ variant="ghost"
236
+ size="sm"
237
+ className="mt-1 h-6 text-xs text-muted-foreground"
238
+ onClick={(e) => { e.stopPropagation(); setExpanded(true); }}
239
+ >
240
+ <ChevronRight className="h-3 w-3 mr-1" />
241
+ Expand
242
+ </Button>
243
+ )}
244
+ </>
245
+ )}
246
+ </div>
180
247
  )}
181
248
 
182
249
  {/* Actions based on type */}
@@ -222,7 +289,7 @@ export function NotificationItem({ notification, onUpdated }: NotificationItemPr
222
289
  )}
223
290
  </p>
224
291
  </div>
225
- <div className="flex flex-col gap-1 shrink-0">
292
+ <div className="flex flex-col gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
226
293
  <Button
227
294
  variant="ghost"
228
295
  size="icon"
@@ -13,6 +13,8 @@ import {
13
13
 
14
14
  import { PermissionResponseActions } from "@/components/notifications/permission-response-actions";
15
15
  import { ContextProposalReview } from "@/components/profiles/context-proposal-review";
16
+ import { BatchProposalReview } from "@/components/notifications/batch-proposal-review";
17
+ import { LightMarkdown } from "@/components/shared/light-markdown";
16
18
  import { Badge } from "@/components/ui/badge";
17
19
  import {
18
20
  Dialog,
@@ -46,6 +48,22 @@ function dedupePendingApprovals(items: PendingApprovalPayload[]) {
46
48
  );
47
49
  }
48
50
 
51
+ function parseBatchToolInput(toolInput: unknown): {
52
+ proposalIds: string[];
53
+ profileIds: string[];
54
+ } {
55
+ try {
56
+ const parsed =
57
+ typeof toolInput === "string" ? JSON.parse(toolInput) : toolInput;
58
+ return {
59
+ proposalIds: Array.isArray(parsed?.proposalIds) ? parsed.proposalIds : [],
60
+ profileIds: Array.isArray(parsed?.profileIds) ? parsed.profileIds : [],
61
+ };
62
+ } catch {
63
+ return { proposalIds: [], profileIds: [] };
64
+ }
65
+ }
66
+
49
67
  function buildContextLabel(payload: PendingApprovalPayload) {
50
68
  if (payload.workflowName && payload.taskTitle) {
51
69
  return `${payload.workflowName} · ${payload.taskTitle}`;
@@ -127,17 +145,31 @@ function PendingApprovalDetail({
127
145
  <p className="mt-2 text-sm text-muted-foreground">
128
146
  {selected.compactSummary}
129
147
  </p>
130
- {selected.body && (
131
- <p className="mt-3 text-sm leading-6 text-muted-foreground">
132
- {selected.body}
133
- </p>
148
+ {selected.body &&
149
+ selected.notificationType !== "context_proposal" &&
150
+ selected.notificationType !== "context_proposal_batch" && (
151
+ <div className="mt-3">
152
+ <LightMarkdown content={selected.body} textSize="sm" />
153
+ </div>
134
154
  )}
135
155
  <p className="mt-3 text-xs text-muted-foreground">
136
156
  Requested {formatTimestamp(selected.createdAt)}
137
157
  </p>
138
158
  </div>
139
159
 
140
- {selected.notificationType === "context_proposal" ? (
160
+ {selected.notificationType === "context_proposal_batch" ? (
161
+ (() => {
162
+ const parsed = parseBatchToolInput(selected.toolInput);
163
+ return (
164
+ <BatchProposalReview
165
+ proposalIds={parsed.proposalIds}
166
+ profileIds={parsed.profileIds}
167
+ body={selected.body ?? ""}
168
+ onResponded={onResponded}
169
+ />
170
+ );
171
+ })()
172
+ ) : selected.notificationType === "context_proposal" ? (
141
173
  <ContextProposalReview
142
174
  notificationId={selected.notificationId}
143
175
  profileId={selected.toolName ?? ""}
@@ -405,7 +437,22 @@ export function PendingApprovalHost() {
405
437
  </div>
406
438
  </button>
407
439
 
408
- {primary.notificationType === "context_proposal" ? (
440
+ {primary.notificationType === "context_proposal_batch" ? (
441
+ <div className="mt-3">
442
+ {(() => {
443
+ const parsed = parseBatchToolInput(primary.toolInput);
444
+ return (
445
+ <BatchProposalReview
446
+ proposalIds={parsed.proposalIds}
447
+ profileIds={parsed.profileIds}
448
+ body={primary.body ?? ""}
449
+ onResponded={() => removeNotification(primary.notificationId)}
450
+ compact
451
+ />
452
+ );
453
+ })()}
454
+ </div>
455
+ ) : primary.notificationType === "context_proposal" ? (
409
456
  <div className="mt-3">
410
457
  <ContextProposalReview
411
458
  notificationId={primary.notificationId}
@@ -474,7 +521,7 @@ export function PendingApprovalHost() {
474
521
  ) : (
475
522
  <Dialog open={detailOpen} onOpenChange={setDetailOpen}>
476
523
  <DialogContent
477
- className="max-w-2xl"
524
+ className="max-w-2xl max-h-[85dvh] flex flex-col"
478
525
  onCloseAutoFocus={(event) => {
479
526
  event.preventDefault();
480
527
  triggerRef.current?.focus();
@@ -487,13 +534,15 @@ export function PendingApprovalHost() {
487
534
  the Inbox first.
488
535
  </DialogDescription>
489
536
  </DialogHeader>
490
- <PendingApprovalDetail
491
- selected={selected}
492
- overflow={overflowItems}
493
- onResponded={() => removeNotification(selected.notificationId)}
494
- onOpenInbox={handleOpenInbox}
495
- onSelect={setSelectedId}
496
- />
537
+ <div className="overflow-y-auto -mx-6 px-6 pb-1">
538
+ <PendingApprovalDetail
539
+ selected={selected}
540
+ overflow={overflowItems}
541
+ onResponded={() => removeNotification(selected.notificationId)}
542
+ onOpenInbox={handleOpenInbox}
543
+ onSelect={setSelectedId}
544
+ />
545
+ </div>
497
546
  </DialogContent>
498
547
  </Dialog>
499
548
  ))}
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { DocSection, AdoptionEntry } from "@/lib/docs/types";
5
+
6
+ interface AdoptionHeatmapProps {
7
+ sections: DocSection[];
8
+ adoption: Record<string, AdoptionEntry>;
9
+ }
10
+
11
+ const depthStyles: Record<AdoptionEntry["depth"], string> = {
12
+ none: "bg-muted border-border/30 text-muted-foreground",
13
+ light: "bg-amber-500/10 border-amber-500/30 text-amber-600 dark:text-amber-400",
14
+ deep: "bg-emerald-500/10 border-emerald-500/30 text-emerald-600 dark:text-emerald-400",
15
+ };
16
+
17
+ const depthLabels: Record<AdoptionEntry["depth"], string> = {
18
+ none: "Not explored",
19
+ light: "Lightly used",
20
+ deep: "Deeply used",
21
+ };
22
+
23
+ export function AdoptionHeatmap({
24
+ sections,
25
+ adoption,
26
+ }: AdoptionHeatmapProps) {
27
+ return (
28
+ <div className="space-y-3">
29
+ <div className="flex items-center justify-between">
30
+ <h3 className="text-base font-medium text-muted-foreground uppercase tracking-wider">
31
+ Feature Adoption
32
+ </h3>
33
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
34
+ <span className="flex items-center gap-1">
35
+ <span className="h-2 w-2 rounded-full bg-muted-foreground/30" />
36
+ Not explored
37
+ </span>
38
+ <span className="flex items-center gap-1">
39
+ <span className="h-2 w-2 rounded-full bg-amber-500" />
40
+ Light
41
+ </span>
42
+ <span className="flex items-center gap-1">
43
+ <span className="h-2 w-2 rounded-full bg-emerald-500" />
44
+ Deep
45
+ </span>
46
+ </div>
47
+ </div>
48
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
49
+ {sections.map((section) => {
50
+ const entry = adoption[section.slug] ?? {
51
+ adopted: false,
52
+ depth: "none" as const,
53
+ };
54
+
55
+ return (
56
+ <Link
57
+ key={section.slug}
58
+ href={`/playbook/${section.slug}`}
59
+ className={`rounded-lg border px-4 py-3 text-sm font-medium transition-colors hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${depthStyles[entry.depth]}`}
60
+ title={depthLabels[entry.depth]}
61
+ >
62
+ {section.title}
63
+ </Link>
64
+ );
65
+ })}
66
+ </div>
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Card, CardHeader, CardContent } from "@/components/ui/card";
6
+ import { Map, Clock, GraduationCap, Briefcase, Zap, Code } from "lucide-react";
7
+ import type {
8
+ DocJourney,
9
+ AdoptionEntry,
10
+ JourneyCompletion,
11
+ } from "@/lib/docs/types";
12
+
13
+ const personaIcons: Record<string, typeof Map> = {
14
+ personal: GraduationCap,
15
+ work: Briefcase,
16
+ "power-user": Zap,
17
+ developer: Code,
18
+ };
19
+
20
+ const difficultyColors: Record<string, string> = {
21
+ beginner: "bg-emerald-500/10 text-emerald-500 border-emerald-500/20",
22
+ intermediate: "bg-amber-500/10 text-amber-500 border-amber-500/20",
23
+ advanced: "bg-rose-500/10 text-rose-500 border-rose-500/20",
24
+ };
25
+
26
+ interface JourneyCardProps {
27
+ journey: DocJourney;
28
+ completion?: JourneyCompletion;
29
+ adoption: Record<string, AdoptionEntry>;
30
+ }
31
+
32
+ export function JourneyCard({
33
+ journey,
34
+ completion,
35
+ adoption,
36
+ }: JourneyCardProps) {
37
+ const Icon = personaIcons[journey.persona] || Map;
38
+ const completed = completion?.completed ?? 0;
39
+ const total = completion?.total ?? journey.sections.length;
40
+ const percentage = completion?.percentage ?? 0;
41
+
42
+ return (
43
+ <Link
44
+ href={`/playbook/${journey.slug}`}
45
+ className="block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xl"
46
+ >
47
+ <Card className="surface-card glass-shimmer group h-full transition-colors hover:border-border hover:bg-accent/50 rounded-xl">
48
+ <CardHeader className="pb-2">
49
+ <div className="flex items-start justify-between gap-2">
50
+ <div className="flex items-center gap-2">
51
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
52
+ <Icon className="h-4 w-4 text-primary" />
53
+ </div>
54
+ <div>
55
+ <h3 className="text-base font-medium group-hover:text-primary transition-colors">
56
+ {journey.title}
57
+ </h3>
58
+ <div className="flex items-center gap-2 mt-0.5">
59
+ <Badge
60
+ variant="outline"
61
+ className={`text-xs ${difficultyColors[journey.difficulty] || ""}`}
62
+ >
63
+ {journey.difficulty}
64
+ </Badge>
65
+ <span className="flex items-center gap-1 text-xs text-muted-foreground">
66
+ <Clock className="h-3 w-3" />
67
+ {journey.stepCount} steps
68
+ </span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </CardHeader>
74
+
75
+ <CardContent className="space-y-3">
76
+ {/* Progress bar — segmented by section */}
77
+ <div className="flex gap-1">
78
+ {journey.sections.map((sectionSlug) => {
79
+ const sectionAdoption = adoption[sectionSlug];
80
+ const isAdopted = sectionAdoption?.adopted === true;
81
+ const depth = sectionAdoption?.depth ?? "none";
82
+
83
+ let barColor = "bg-muted-foreground/20";
84
+ if (depth === "deep") barColor = "bg-emerald-500";
85
+ else if (depth === "light") barColor = "bg-amber-500";
86
+
87
+ return (
88
+ <div
89
+ key={sectionSlug}
90
+ className={`h-2 flex-1 rounded-full ${barColor} transition-colors`}
91
+ title={`${sectionSlug}: ${isAdopted ? "explored" : "not explored"}`}
92
+ />
93
+ );
94
+ })}
95
+ </div>
96
+
97
+ {/* Completion label */}
98
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
99
+ <span>
100
+ {completed} of {total} explored
101
+ </span>
102
+ <span className="font-medium">
103
+ {percentage}%
104
+ </span>
105
+ </div>
106
+ </CardContent>
107
+ </Card>
108
+ </Link>
109
+ );
110
+ }
@@ -0,0 +1,22 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "lucide-react";
3
+
4
+ interface PlaybookActionButtonProps {
5
+ href: string;
6
+ children: React.ReactNode;
7
+ }
8
+
9
+ export function PlaybookActionButton({
10
+ href,
11
+ children,
12
+ }: PlaybookActionButtonProps) {
13
+ return (
14
+ <Link
15
+ href={href}
16
+ className="inline-flex items-center gap-1.5 rounded-full bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 no-underline"
17
+ >
18
+ {children}
19
+ <ArrowRight className="h-3.5 w-3.5" />
20
+ </Link>
21
+ );
22
+ }
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo } from "react";
4
+ import { Search, BookOpen } from "lucide-react";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
7
+ import { EmptyState } from "@/components/shared/empty-state";
8
+ import { PlaybookCard } from "@/components/playbook/playbook-card";
9
+ import { JourneyCard } from "@/components/playbook/journey-card";
10
+ import type {
11
+ DocSection,
12
+ DocJourney,
13
+ AdoptionEntry,
14
+ JourneyCompletion,
15
+ } from "@/lib/docs/types";
16
+
17
+ type CategoryFilter = "all" | "features" | "journeys" | "getting-started";
18
+
19
+ interface PlaybookBrowserProps {
20
+ sections: DocSection[];
21
+ journeys: DocJourney[];
22
+ adoption: Record<string, AdoptionEntry>;
23
+ journeyCompletions: Record<string, JourneyCompletion>;
24
+ }
25
+
26
+ export function PlaybookBrowser({
27
+ sections,
28
+ journeys,
29
+ adoption,
30
+ journeyCompletions,
31
+ }: PlaybookBrowserProps) {
32
+ const [search, setSearch] = useState("");
33
+ const [category, setCategory] = useState<CategoryFilter>("all");
34
+
35
+ const filteredSections = useMemo(() => {
36
+ const q = search.toLowerCase();
37
+ return sections.filter((s) => {
38
+ if (!q) return true;
39
+ return (
40
+ s.title.toLowerCase().includes(q) ||
41
+ s.tags.some((t) => t.toLowerCase().includes(q)) ||
42
+ s.slug.toLowerCase().includes(q)
43
+ );
44
+ });
45
+ }, [sections, search]);
46
+
47
+ const filteredJourneys = useMemo(() => {
48
+ const q = search.toLowerCase();
49
+ return journeys.filter((j) => {
50
+ if (!q) return true;
51
+ return (
52
+ j.title.toLowerCase().includes(q) ||
53
+ j.persona.toLowerCase().includes(q) ||
54
+ j.slug.toLowerCase().includes(q)
55
+ );
56
+ });
57
+ }, [journeys, search]);
58
+
59
+ const showSections = category === "all" || category === "features";
60
+ const showJourneys = category === "all" || category === "journeys";
61
+ const isEmpty =
62
+ (showSections ? filteredSections.length : 0) +
63
+ (showJourneys ? filteredJourneys.length : 0) ===
64
+ 0;
65
+
66
+ return (
67
+ <div className="space-y-4">
68
+ {/* Search + Filter */}
69
+ <div className="surface-panel flex flex-col gap-4 rounded-2xl p-4 sm:flex-row sm:items-center">
70
+ <div className="relative flex-1">
71
+ <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
72
+ <Input
73
+ placeholder="Search docs..."
74
+ value={search}
75
+ onChange={(e) => setSearch(e.target.value)}
76
+ className="surface-control pl-9"
77
+ />
78
+ </div>
79
+ <Tabs
80
+ value={category}
81
+ onValueChange={(v) => setCategory(v as CategoryFilter)}
82
+ >
83
+ <TabsList className="surface-control">
84
+ <TabsTrigger value="all">All</TabsTrigger>
85
+ <TabsTrigger value="features">Features</TabsTrigger>
86
+ <TabsTrigger value="journeys">Journeys</TabsTrigger>
87
+ </TabsList>
88
+ </Tabs>
89
+ </div>
90
+
91
+ {isEmpty ? (
92
+ <EmptyState
93
+ icon={BookOpen}
94
+ heading="No docs found"
95
+ description="Try adjusting your search or filter."
96
+ />
97
+ ) : (
98
+ <div className="space-y-6">
99
+ {/* Journeys */}
100
+ {showJourneys && filteredJourneys.length > 0 && (
101
+ <div className="space-y-3">
102
+ {category === "all" && (
103
+ <h3 className="text-base font-medium text-muted-foreground uppercase tracking-wider">
104
+ Guided Journeys
105
+ </h3>
106
+ )}
107
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
108
+ {filteredJourneys.map((journey) => (
109
+ <JourneyCard
110
+ key={journey.slug}
111
+ journey={journey}
112
+ completion={journeyCompletions[journey.slug]}
113
+ adoption={adoption}
114
+ />
115
+ ))}
116
+ </div>
117
+ </div>
118
+ )}
119
+
120
+ {/* Feature Sections */}
121
+ {showSections && filteredSections.length > 0 && (
122
+ <div className="space-y-3">
123
+ {category === "all" && (
124
+ <h3 className="text-base font-medium text-muted-foreground uppercase tracking-wider">
125
+ Feature Reference
126
+ </h3>
127
+ )}
128
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
129
+ {filteredSections.map((section) => (
130
+ <PlaybookCard
131
+ key={section.slug}
132
+ section={section}
133
+ adoption={adoption[section.slug]}
134
+ />
135
+ ))}
136
+ </div>
137
+ </div>
138
+ )}
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ }