stagent 0.1.10 → 0.1.12
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 +58 -27
- package/package.json +3 -3
- 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 -21
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/costs/page.tsx +53 -43
- package/src/app/globals.css +0 -5
- 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/page.tsx +5 -0
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- 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 +223 -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-detail-view.tsx +7 -19
- package/src/components/profiles/profile-form-view.tsx +0 -22
- 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 +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +32 -28
- package/src/lib/agents/learning-session.ts +234 -0
- package/src/lib/agents/pattern-extractor.ts +34 -64
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +66 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- 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 +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- 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 +29 -5
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +4 -2
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -41
- 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__/profile.test.ts +0 -15
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/engine.ts +20 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { Image, ExternalLink } from "lucide-react";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
|
7
|
+
import type { DocSection, AdoptionEntry } from "@/lib/docs/types";
|
|
8
|
+
|
|
9
|
+
interface PlaybookCardProps {
|
|
10
|
+
section: DocSection;
|
|
11
|
+
adoption?: AdoptionEntry;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function adoptionDot(adoption?: AdoptionEntry) {
|
|
15
|
+
if (!adoption || adoption.depth === "none") {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
className="h-2.5 w-2.5 rounded-full bg-muted-foreground/30"
|
|
19
|
+
title="Not explored"
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
if (adoption.depth === "light") {
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
className="h-2.5 w-2.5 rounded-full bg-amber-500"
|
|
27
|
+
title="Lightly explored"
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return (
|
|
32
|
+
<span
|
|
33
|
+
className="h-2.5 w-2.5 rounded-full bg-emerald-500"
|
|
34
|
+
title="Deeply explored"
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function PlaybookCard({ section, adoption }: PlaybookCardProps) {
|
|
40
|
+
const isAppRoute =
|
|
41
|
+
section.route !== "cross-cutting" && section.route.startsWith("/");
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Link
|
|
45
|
+
href={`/playbook/${section.slug}`}
|
|
46
|
+
className="block focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-xl"
|
|
47
|
+
>
|
|
48
|
+
<Card className="surface-card glass-shimmer group h-full transition-colors hover:border-border hover:bg-accent/50 rounded-xl">
|
|
49
|
+
<CardHeader className="pb-2">
|
|
50
|
+
<div className="flex items-start justify-between gap-2">
|
|
51
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
52
|
+
{adoptionDot(adoption)}
|
|
53
|
+
<h3 className="text-base font-medium truncate group-hover:text-primary transition-colors">
|
|
54
|
+
{section.title}
|
|
55
|
+
</h3>
|
|
56
|
+
</div>
|
|
57
|
+
{isAppRoute ? (
|
|
58
|
+
<Badge variant="secondary" className="shrink-0 text-xs">
|
|
59
|
+
{section.route}
|
|
60
|
+
</Badge>
|
|
61
|
+
) : (
|
|
62
|
+
<Badge variant="outline" className="shrink-0 text-xs">
|
|
63
|
+
<ExternalLink className="h-3 w-3 mr-0.5" />
|
|
64
|
+
cross-cutting
|
|
65
|
+
</Badge>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{/* Subtitle: feature count */}
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
{section.features.length} feature
|
|
71
|
+
{section.features.length !== 1 ? "s" : ""} covered
|
|
72
|
+
</p>
|
|
73
|
+
</CardHeader>
|
|
74
|
+
|
|
75
|
+
<CardContent className="space-y-3">
|
|
76
|
+
{/* Tags */}
|
|
77
|
+
<div className="flex flex-wrap gap-1.5">
|
|
78
|
+
{section.tags.slice(0, 4).map((tag) => (
|
|
79
|
+
<Badge key={tag} variant="outline" className="text-xs font-normal">
|
|
80
|
+
{tag}
|
|
81
|
+
</Badge>
|
|
82
|
+
))}
|
|
83
|
+
{section.tags.length > 4 && (
|
|
84
|
+
<span className="text-xs text-muted-foreground">
|
|
85
|
+
+{section.tags.length - 4}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Footer */}
|
|
91
|
+
{section.screengrabCount > 0 && (
|
|
92
|
+
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
93
|
+
<Image className="h-3.5 w-3.5" />
|
|
94
|
+
{section.screengrabCount} screenshot
|
|
95
|
+
{section.screengrabCount > 1 ? "s" : ""}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</CardContent>
|
|
99
|
+
</Card>
|
|
100
|
+
</Link>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { ArrowLeft, ArrowRight, ExternalLink } from "lucide-react";
|
|
8
|
+
import { Button } from "@/components/ui/button";
|
|
9
|
+
import { Badge } from "@/components/ui/badge";
|
|
10
|
+
import { PlaybookToc } from "./playbook-toc";
|
|
11
|
+
import { RelatedDocs } from "./related-docs";
|
|
12
|
+
import type { ParsedDoc, DocSection, AdoptionEntry } from "@/lib/docs/types";
|
|
13
|
+
|
|
14
|
+
interface PlaybookDetailViewProps {
|
|
15
|
+
doc: ParsedDoc;
|
|
16
|
+
relatedSections: DocSection[];
|
|
17
|
+
adoption: Record<string, AdoptionEntry>;
|
|
18
|
+
allSlugs: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Convert a heading text to a slug-style ID */
|
|
22
|
+
function headingId(text: string): string {
|
|
23
|
+
return text
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
26
|
+
.replace(/(^-|-$)/g, "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function PlaybookDetailView({
|
|
30
|
+
doc,
|
|
31
|
+
relatedSections,
|
|
32
|
+
adoption,
|
|
33
|
+
allSlugs,
|
|
34
|
+
}: PlaybookDetailViewProps) {
|
|
35
|
+
const title =
|
|
36
|
+
(doc.frontmatter.title as string) || doc.slug.replace(/-/g, " ");
|
|
37
|
+
const tags = (doc.frontmatter.tags as string[]) || [];
|
|
38
|
+
const route = doc.frontmatter.route as string | undefined;
|
|
39
|
+
const category = doc.frontmatter.category as string | undefined;
|
|
40
|
+
const difficulty = doc.frontmatter.difficulty as string | undefined;
|
|
41
|
+
|
|
42
|
+
// Build a slug lookup from all known docs for rewriting .md links
|
|
43
|
+
const slugSet = useMemo(
|
|
44
|
+
() => new Set(allSlugs),
|
|
45
|
+
[allSlugs]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Custom link renderer for react-markdown */
|
|
49
|
+
function MarkdownLink({
|
|
50
|
+
href,
|
|
51
|
+
children,
|
|
52
|
+
}: {
|
|
53
|
+
href?: string;
|
|
54
|
+
children?: React.ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
if (!href) return <>{children}</>;
|
|
57
|
+
|
|
58
|
+
// Relative .md links → internal playbook links
|
|
59
|
+
if (href.includes(".md") || href.startsWith("./") || href.startsWith("../")) {
|
|
60
|
+
// Strip fragment, .md extension, and any relative path prefixes
|
|
61
|
+
const [pathPart, fragment] = href.split("#");
|
|
62
|
+
const slug = pathPart
|
|
63
|
+
.replace(/\.md$/, "")
|
|
64
|
+
.replace(/^(\.\.?\/)+/, "") // strip leading ./ or ../
|
|
65
|
+
.replace(/^(features|journeys)\//, ""); // strip directory prefix
|
|
66
|
+
if (slugSet.has(slug)) {
|
|
67
|
+
const hashSuffix = fragment ? `#${fragment}` : "";
|
|
68
|
+
return (
|
|
69
|
+
<Link
|
|
70
|
+
href={`/playbook/${slug}${hashSuffix}`}
|
|
71
|
+
className="text-primary underline underline-offset-4 hover:text-primary/80"
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</Link>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// App routes → action buttons
|
|
80
|
+
if (href.startsWith("/") && !href.startsWith("/playbook")) {
|
|
81
|
+
return (
|
|
82
|
+
<Link
|
|
83
|
+
href={href}
|
|
84
|
+
className="inline-flex items-center gap-1.5 rounded-full bg-primary px-3 py-1 text-sm font-medium text-primary-foreground no-underline hover:bg-primary/90 transition-colors"
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
<ArrowRight className="h-3.5 w-3.5" />
|
|
88
|
+
</Link>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// External links
|
|
93
|
+
return (
|
|
94
|
+
<a
|
|
95
|
+
href={href}
|
|
96
|
+
target="_blank"
|
|
97
|
+
rel="noopener noreferrer"
|
|
98
|
+
className="text-primary underline underline-offset-4 hover:text-primary/80 inline-flex items-center gap-0.5"
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
<ExternalLink className="h-3 w-3" />
|
|
102
|
+
</a>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Custom heading renderer that adds IDs for TOC linking */
|
|
107
|
+
function H2({ children }: { children?: React.ReactNode }) {
|
|
108
|
+
const text = typeof children === "string" ? children : String(children);
|
|
109
|
+
return <h2 id={headingId(text)}>{children}</h2>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function H3({ children }: { children?: React.ReactNode }) {
|
|
113
|
+
const text = typeof children === "string" ? children : String(children);
|
|
114
|
+
return <h3 id={headingId(text)}>{children}</h3>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Custom image renderer that resolves relative screengrab paths */
|
|
118
|
+
function MarkdownImage({
|
|
119
|
+
src,
|
|
120
|
+
alt,
|
|
121
|
+
}: {
|
|
122
|
+
src?: string;
|
|
123
|
+
alt?: string;
|
|
124
|
+
}) {
|
|
125
|
+
if (!src) return null;
|
|
126
|
+
|
|
127
|
+
// Resolve ../screengrabs/ paths to /readme/ (images live in public/readme/)
|
|
128
|
+
let resolvedSrc = src;
|
|
129
|
+
if (src.startsWith("../screengrabs/")) {
|
|
130
|
+
resolvedSrc = `/readme/${src.replace("../screengrabs/", "")}`;
|
|
131
|
+
} else if (src.startsWith("./")) {
|
|
132
|
+
resolvedSrc = `/docs/${src.replace("./", "")}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<span className="block my-6">
|
|
137
|
+
<img
|
|
138
|
+
src={resolvedSrc}
|
|
139
|
+
alt={alt || ""}
|
|
140
|
+
className="rounded-xl border border-border/50 max-w-full"
|
|
141
|
+
loading="lazy"
|
|
142
|
+
onError={(e) => {
|
|
143
|
+
(e.target as HTMLImageElement).style.display = "none";
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div className="space-y-6">
|
|
152
|
+
{/* Back nav */}
|
|
153
|
+
<div className="flex items-center gap-4">
|
|
154
|
+
<Button variant="ghost" size="sm" asChild>
|
|
155
|
+
<Link href="/playbook">
|
|
156
|
+
<ArrowLeft className="h-4 w-4 mr-1" />
|
|
157
|
+
Playbook
|
|
158
|
+
</Link>
|
|
159
|
+
</Button>
|
|
160
|
+
{route && route !== "cross-cutting" && (
|
|
161
|
+
<Button variant="outline" size="sm" asChild>
|
|
162
|
+
<Link href={route}>
|
|
163
|
+
Open {title}
|
|
164
|
+
<ArrowRight className="h-3.5 w-3.5 ml-1" />
|
|
165
|
+
</Link>
|
|
166
|
+
</Button>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Title + meta */}
|
|
171
|
+
<div className="space-y-2">
|
|
172
|
+
<h1 className="text-3xl font-bold tracking-tight">{title}</h1>
|
|
173
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
174
|
+
{category && (
|
|
175
|
+
<Badge variant="secondary" className="text-xs">
|
|
176
|
+
{category}
|
|
177
|
+
</Badge>
|
|
178
|
+
)}
|
|
179
|
+
{difficulty && (
|
|
180
|
+
<Badge variant="outline" className="text-xs">
|
|
181
|
+
{difficulty}
|
|
182
|
+
</Badge>
|
|
183
|
+
)}
|
|
184
|
+
{tags.slice(0, 6).map((tag) => (
|
|
185
|
+
<Badge key={tag} variant="outline" className="text-xs font-normal">
|
|
186
|
+
{tag}
|
|
187
|
+
</Badge>
|
|
188
|
+
))}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Content layout: TOC sidebar + prose */}
|
|
193
|
+
<div className="flex gap-8">
|
|
194
|
+
{/* TOC sidebar — hidden on mobile */}
|
|
195
|
+
<aside className="hidden lg:block w-48 shrink-0">
|
|
196
|
+
<div className="sticky top-20">
|
|
197
|
+
<PlaybookToc body={doc.body} />
|
|
198
|
+
</div>
|
|
199
|
+
</aside>
|
|
200
|
+
|
|
201
|
+
{/* Prose content */}
|
|
202
|
+
<div className="flex-1 min-w-0">
|
|
203
|
+
<div className="prose dark:prose-invert max-w-none prose-headings:scroll-mt-20 prose-img:rounded-xl prose-img:border prose-img:border-border/50">
|
|
204
|
+
<ReactMarkdown
|
|
205
|
+
remarkPlugins={[remarkGfm]}
|
|
206
|
+
components={{
|
|
207
|
+
a: MarkdownLink as never,
|
|
208
|
+
h2: H2 as never,
|
|
209
|
+
h3: H3 as never,
|
|
210
|
+
img: MarkdownImage as never,
|
|
211
|
+
}}
|
|
212
|
+
>
|
|
213
|
+
{doc.body}
|
|
214
|
+
</ReactMarkdown>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Related docs */}
|
|
218
|
+
<RelatedDocs sections={relatedSections} adoption={adoption} />
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import {
|
|
5
|
+
BookOpen,
|
|
6
|
+
Sparkles,
|
|
7
|
+
Rocket,
|
|
8
|
+
Crown,
|
|
9
|
+
ArrowRight,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { Badge } from "@/components/ui/badge";
|
|
13
|
+
import { PlaybookBrowser } from "./playbook-browser";
|
|
14
|
+
import { AdoptionHeatmap } from "./adoption-heatmap";
|
|
15
|
+
import type {
|
|
16
|
+
DocManifest,
|
|
17
|
+
UsageStage,
|
|
18
|
+
AdoptionEntry,
|
|
19
|
+
JourneyCompletion,
|
|
20
|
+
} from "@/lib/docs/types";
|
|
21
|
+
|
|
22
|
+
interface PlaybookHomepageProps {
|
|
23
|
+
manifest: DocManifest;
|
|
24
|
+
stage: UsageStage;
|
|
25
|
+
adoption: Record<string, AdoptionEntry>;
|
|
26
|
+
journeyCompletions: Record<string, JourneyCompletion>;
|
|
27
|
+
hasUpdates: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const stageConfig: Record<
|
|
31
|
+
UsageStage,
|
|
32
|
+
{
|
|
33
|
+
icon: typeof BookOpen;
|
|
34
|
+
title: string;
|
|
35
|
+
subtitle: string;
|
|
36
|
+
ctaLabel: string;
|
|
37
|
+
ctaSlug: string;
|
|
38
|
+
}
|
|
39
|
+
> = {
|
|
40
|
+
new: {
|
|
41
|
+
icon: BookOpen,
|
|
42
|
+
title: "Welcome to Stagent",
|
|
43
|
+
subtitle:
|
|
44
|
+
"Your AI agent orchestrator. Start with the getting started guide or explore feature docs at your own pace.",
|
|
45
|
+
ctaLabel: "Getting Started",
|
|
46
|
+
ctaSlug: "getting-started",
|
|
47
|
+
},
|
|
48
|
+
early: {
|
|
49
|
+
icon: Sparkles,
|
|
50
|
+
title: "Keep Building",
|
|
51
|
+
subtitle:
|
|
52
|
+
"You've started creating tasks. Follow a guided journey to unlock more of what Stagent can do.",
|
|
53
|
+
ctaLabel: "Personal Use Guide",
|
|
54
|
+
ctaSlug: "personal-use",
|
|
55
|
+
},
|
|
56
|
+
active: {
|
|
57
|
+
icon: Rocket,
|
|
58
|
+
title: "Level Up",
|
|
59
|
+
subtitle:
|
|
60
|
+
"You're actively using Stagent. Explore advanced workflows, schedules, and multi-agent features.",
|
|
61
|
+
ctaLabel: "Power User Guide",
|
|
62
|
+
ctaSlug: "power-user",
|
|
63
|
+
},
|
|
64
|
+
power: {
|
|
65
|
+
icon: Crown,
|
|
66
|
+
title: "Master Mode",
|
|
67
|
+
subtitle:
|
|
68
|
+
"You've unlocked the full platform. Dive into cross-cutting guides and developer docs.",
|
|
69
|
+
ctaLabel: "Developer Guide",
|
|
70
|
+
ctaSlug: "developer",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export function PlaybookHomepage({
|
|
75
|
+
manifest,
|
|
76
|
+
stage,
|
|
77
|
+
adoption,
|
|
78
|
+
journeyCompletions,
|
|
79
|
+
hasUpdates,
|
|
80
|
+
}: PlaybookHomepageProps) {
|
|
81
|
+
const config = stageConfig[stage];
|
|
82
|
+
const Icon = config.icon;
|
|
83
|
+
|
|
84
|
+
const adoptedCount = Object.values(adoption).filter(
|
|
85
|
+
(a) => a.adopted
|
|
86
|
+
).length;
|
|
87
|
+
const totalSections = manifest.sections.length;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-8">
|
|
91
|
+
{/* Hero */}
|
|
92
|
+
<div className="surface-panel rounded-2xl p-6 md:p-8">
|
|
93
|
+
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
94
|
+
<div className="flex items-start gap-4">
|
|
95
|
+
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
|
96
|
+
<Icon className="h-6 w-6 text-primary" />
|
|
97
|
+
</div>
|
|
98
|
+
<div className="space-y-1">
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<h1 className="text-3xl font-bold tracking-tight">
|
|
101
|
+
{config.title}
|
|
102
|
+
</h1>
|
|
103
|
+
{hasUpdates && (
|
|
104
|
+
<Badge className="bg-primary/10 text-primary text-xs">
|
|
105
|
+
Updated
|
|
106
|
+
</Badge>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
<p className="text-base text-muted-foreground max-w-lg">
|
|
110
|
+
{config.subtitle}
|
|
111
|
+
</p>
|
|
112
|
+
{stage !== "new" && (
|
|
113
|
+
<p className="text-sm text-muted-foreground">
|
|
114
|
+
{adoptedCount} of {totalSections} features explored
|
|
115
|
+
</p>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
<Button asChild>
|
|
120
|
+
<Link href={`/playbook/${config.ctaSlug}`}>
|
|
121
|
+
{config.ctaLabel}
|
|
122
|
+
<ArrowRight className="h-4 w-4 ml-1" />
|
|
123
|
+
</Link>
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Adoption Heatmap — shown for active/power users */}
|
|
129
|
+
{(stage === "active" || stage === "power") && (
|
|
130
|
+
<AdoptionHeatmap sections={manifest.sections} adoption={adoption} />
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Full Browser */}
|
|
134
|
+
<PlaybookBrowser
|
|
135
|
+
sections={manifest.sections}
|
|
136
|
+
journeys={manifest.journeys}
|
|
137
|
+
adoption={adoption}
|
|
138
|
+
journeyCompletions={journeyCompletions}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
interface TocItem {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
level: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PlaybookTocProps {
|
|
12
|
+
body: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Extract ## and ### headings from markdown body */
|
|
16
|
+
function extractHeadings(body: string): TocItem[] {
|
|
17
|
+
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
|
|
18
|
+
const items: TocItem[] = [];
|
|
19
|
+
let match;
|
|
20
|
+
|
|
21
|
+
while ((match = headingRegex.exec(body)) !== null) {
|
|
22
|
+
const level = match[1].length;
|
|
23
|
+
const text = match[2].trim();
|
|
24
|
+
const id = text
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
+
.replace(/(^-|-$)/g, "");
|
|
28
|
+
items.push({ id, text, level });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return items;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function PlaybookToc({ body }: PlaybookTocProps) {
|
|
35
|
+
const headings = extractHeadings(body);
|
|
36
|
+
const [activeId, setActiveId] = useState<string>("");
|
|
37
|
+
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
observerRef.current = new IntersectionObserver(
|
|
41
|
+
(entries) => {
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (entry.isIntersecting) {
|
|
44
|
+
setActiveId(entry.target.id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{ rootMargin: "-80px 0px -60% 0px", threshold: 0 }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const elements = headings
|
|
52
|
+
.map((h) => document.getElementById(h.id))
|
|
53
|
+
.filter(Boolean) as HTMLElement[];
|
|
54
|
+
|
|
55
|
+
for (const el of elements) {
|
|
56
|
+
observerRef.current.observe(el);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return () => observerRef.current?.disconnect();
|
|
60
|
+
}, [headings]);
|
|
61
|
+
|
|
62
|
+
if (headings.length < 2) return null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<nav className="space-y-1" aria-label="Table of contents">
|
|
66
|
+
<h4 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
67
|
+
On this page
|
|
68
|
+
</h4>
|
|
69
|
+
{headings.map((h) => (
|
|
70
|
+
<a
|
|
71
|
+
key={h.id}
|
|
72
|
+
href={`#${h.id}`}
|
|
73
|
+
className={`block text-sm py-0.5 transition-colors hover:text-foreground ${
|
|
74
|
+
h.level === 3 ? "pl-3" : ""
|
|
75
|
+
} ${
|
|
76
|
+
activeId === h.id
|
|
77
|
+
? "text-primary font-medium"
|
|
78
|
+
: "text-muted-foreground"
|
|
79
|
+
}`}
|
|
80
|
+
onClick={(e) => {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
document.getElementById(h.id)?.scrollIntoView({ behavior: "smooth" });
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{h.text}
|
|
86
|
+
</a>
|
|
87
|
+
))}
|
|
88
|
+
</nav>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/** Small "Updated" dot badge for sidebar — shows when docs changed since last visit */
|
|
6
|
+
export function PlaybookUpdatedBadge() {
|
|
7
|
+
const [hasUpdates, setHasUpdates] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
fetch("/api/playbook/status")
|
|
11
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
12
|
+
.then((data) => {
|
|
13
|
+
if (data?.hasUpdates) setHasUpdates(true);
|
|
14
|
+
})
|
|
15
|
+
.catch(() => {});
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
if (!hasUpdates) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<span className="ml-auto flex h-2 w-2 rounded-full bg-primary" aria-label="Updated docs available" />
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PlaybookCard } from "./playbook-card";
|
|
4
|
+
import type { DocSection, AdoptionEntry } from "@/lib/docs/types";
|
|
5
|
+
|
|
6
|
+
interface RelatedDocsProps {
|
|
7
|
+
sections: DocSection[];
|
|
8
|
+
adoption: Record<string, AdoptionEntry>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function RelatedDocs({ sections, adoption }: RelatedDocsProps) {
|
|
12
|
+
if (sections.length === 0) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-3 pt-6 border-t border-border/50">
|
|
16
|
+
<h3 className="text-base font-medium text-muted-foreground uppercase tracking-wider">
|
|
17
|
+
Related Docs
|
|
18
|
+
</h3>
|
|
19
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
20
|
+
{sections.map((section) => (
|
|
21
|
+
<PlaybookCard
|
|
22
|
+
key={section.slug}
|
|
23
|
+
section={section}
|
|
24
|
+
adoption={adoption[section.slug]}
|
|
25
|
+
/>
|
|
26
|
+
))}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|