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.
- package/README.md +74 -49
- package/package.json +3 -2
- package/public/readme/cost-usage-list.png +0 -0
- package/public/readme/dashboard-bulk-select.png +0 -0
- package/public/readme/dashboard-card-edit.png +0 -0
- package/public/readme/dashboard-create-form-ai-applied.png +0 -0
- package/public/readme/dashboard-create-form-ai-assist.png +0 -0
- package/public/readme/dashboard-create-form-empty.png +0 -0
- package/public/readme/dashboard-create-form-filled.png +0 -0
- package/public/readme/dashboard-filtered.png +0 -0
- package/public/readme/dashboard-list.png +0 -0
- package/public/readme/dashboard-workflow-confirm.png +0 -0
- package/public/readme/home-below-fold.png +0 -0
- package/public/readme/home-list.png +0 -0
- package/public/readme/inbox-list.png +0 -0
- package/public/readme/playbook-list.png +0 -0
- package/public/readme/profiles-list.png +0 -0
- package/public/readme/settings-list.png +0 -0
- package/public/readme/workflows-list.png +0 -0
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -20
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/api/tasks/[id]/route.ts +54 -3
- package/src/app/api/workflows/[id]/route.ts +43 -4
- package/src/app/api/workflows/[id]/status/route.ts +70 -2
- package/src/app/api/workflows/from-assist/route.ts +6 -32
- package/src/app/costs/page.tsx +53 -43
- package/src/app/dashboard/page.tsx +59 -21
- package/src/app/documents/[id]/page.tsx +10 -8
- package/src/app/globals.css +11 -0
- package/src/app/page.tsx +60 -3
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/[id]/page.tsx +22 -2
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/dashboard/greeting.tsx +3 -1
- package/src/components/dashboard/priority-queue.tsx +58 -9
- package/src/components/dashboard/stats-cards.tsx +16 -2
- package/src/components/documents/document-chip-bar.tsx +183 -0
- package/src/components/documents/document-content-renderer.tsx +146 -0
- package/src/components/documents/document-detail-view.tsx +16 -239
- package/src/components/documents/image-zoom-view.tsx +60 -0
- package/src/components/documents/smart-extracted-text.tsx +47 -0
- package/src/components/documents/utils.ts +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/inbox-list.tsx +4 -5
- package/src/components/notifications/notification-item.tsx +73 -6
- package/src/components/notifications/pending-approval-host.tsx +63 -14
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +225 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-browser.tsx +1 -0
- package/src/components/profiles/profile-card.tsx +16 -8
- package/src/components/profiles/profile-detail-view.tsx +12 -4
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +4 -2
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
- package/src/components/tasks/ai-assist-panel.tsx +108 -78
- package/src/components/tasks/content-preview.tsx +2 -1
- package/src/components/tasks/kanban-board.tsx +57 -5
- package/src/components/tasks/kanban-column.tsx +34 -23
- package/src/components/tasks/task-bento-cell.tsx +50 -0
- package/src/components/tasks/task-bento-grid.tsx +155 -0
- package/src/components/tasks/task-card.tsx +14 -16
- package/src/components/tasks/task-chip-bar.tsx +207 -0
- package/src/components/tasks/task-detail-view.tsx +42 -190
- package/src/components/tasks/task-result-renderer.tsx +33 -0
- package/src/components/workflows/blueprint-gallery.tsx +19 -12
- package/src/components/workflows/blueprint-preview.tsx +8 -1
- package/src/components/workflows/loop-status-view.tsx +2 -4
- package/src/components/workflows/swarm-dashboard.tsx +2 -3
- package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
- package/src/components/workflows/workflow-full-output.tsx +80 -0
- package/src/components/workflows/workflow-kanban-card.tsx +121 -0
- package/src/components/workflows/workflow-list.tsx +47 -42
- package/src/components/workflows/workflow-status-view.tsx +163 -16
- package/src/lib/agents/learned-context.ts +27 -15
- package/src/lib/agents/learning-session.ts +354 -0
- package/src/lib/agents/pattern-extractor.ts +19 -0
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/constants/card-icons.tsx +202 -0
- package/src/lib/constants/prose-styles.ts +7 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +3 -0
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +107 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/documents/context-builder.ts +41 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/queries/chart-data.ts +20 -1
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +2 -2
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +1 -1
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -95
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/engine.ts +75 -61
- package/src/lib/workflows/types.ts +2 -0
- package/tsconfig.json +2 -1
- 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
|
-
<
|
|
178
|
-
{
|
|
179
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 === "
|
|
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 === "
|
|
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
|
-
<
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
}
|