stagent 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/cli.js +39 -10
- package/drizzle.config.ts +3 -1
- package/package.json +3 -1
- package/src/app/api/book/bookmarks/route.ts +73 -0
- package/src/app/api/book/progress/route.ts +79 -0
- package/src/app/api/book/regenerate/route.ts +111 -0
- package/src/app/api/book/stage/route.ts +13 -0
- package/src/app/api/chat/conversations/[id]/respond/route.ts +19 -20
- package/src/app/api/chat/conversations/[id]/route.ts +2 -1
- package/src/app/api/documents/[id]/route.ts +34 -2
- package/src/app/api/documents/route.ts +91 -0
- package/src/app/api/settings/runtime/route.ts +46 -0
- package/src/app/book/page.tsx +14 -0
- package/src/app/chat/page.tsx +7 -1
- package/src/app/globals.css +375 -0
- package/src/app/projects/[id]/page.tsx +31 -6
- package/src/app/settings/page.tsx +2 -0
- package/src/app/{playbook → user-guide}/[slug]/page.tsx +12 -2
- package/src/app/{playbook → user-guide}/page.tsx +2 -2
- package/src/app/workflows/[id]/page.tsx +28 -2
- package/src/components/book/book-reader.tsx +801 -0
- package/src/components/book/chapter-generation-bar.tsx +109 -0
- package/src/components/book/content-blocks.tsx +432 -0
- package/src/components/book/path-progress.tsx +33 -0
- package/src/components/book/path-selector.tsx +42 -0
- package/src/components/book/try-it-now.tsx +164 -0
- package/src/components/chat/chat-activity-indicator.tsx +92 -0
- package/src/components/chat/chat-message-list.tsx +3 -0
- package/src/components/chat/chat-message.tsx +22 -6
- package/src/components/chat/chat-permission-request.tsx +5 -1
- package/src/components/chat/chat-question.tsx +3 -0
- package/src/components/chat/chat-shell.tsx +130 -19
- package/src/components/chat/conversation-list.tsx +8 -2
- package/src/components/playbook/adoption-heatmap.tsx +1 -1
- package/src/components/playbook/journey-card.tsx +1 -1
- package/src/components/playbook/playbook-card.tsx +1 -1
- package/src/components/playbook/playbook-detail-view.tsx +15 -5
- package/src/components/playbook/playbook-homepage.tsx +1 -1
- package/src/components/playbook/playbook-updated-badge.tsx +1 -1
- package/src/components/projects/project-detail.tsx +147 -27
- package/src/components/projects/project-form-sheet.tsx +6 -2
- package/src/components/projects/project-list.tsx +1 -1
- package/src/components/settings/runtime-timeout-section.tsx +170 -0
- package/src/components/shared/app-sidebar.tsx +7 -1
- package/src/components/shared/command-palette.tsx +4 -4
- package/src/hooks/use-chapter-generation.ts +255 -0
- package/src/lib/agents/claude-agent.ts +12 -6
- package/src/lib/agents/runtime/claude.ts +29 -3
- package/src/lib/book/chapter-generator.ts +193 -0
- package/src/lib/book/chapter-mapping.ts +91 -0
- package/src/lib/book/content.ts +251 -0
- package/src/lib/book/markdown-parser.ts +317 -0
- package/src/lib/book/reading-paths.ts +82 -0
- package/src/lib/book/types.ts +152 -0
- package/src/lib/book/update-detector.ts +157 -0
- package/src/lib/chat/codex-engine.ts +537 -0
- package/src/lib/chat/context-builder.ts +18 -4
- package/src/lib/chat/engine.ts +116 -39
- package/src/lib/chat/model-discovery.ts +13 -5
- package/src/lib/chat/permission-bridge.ts +14 -2
- package/src/lib/chat/stagent-tools.ts +2 -0
- package/src/lib/chat/system-prompt.ts +16 -1
- package/src/lib/chat/tools/chat-history-tools.ts +177 -0
- package/src/lib/chat/tools/document-tools.ts +204 -0
- package/src/lib/chat/tools/settings-tools.ts +30 -3
- package/src/lib/chat/types.ts +8 -1
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/chat.ts +83 -2
- package/src/lib/data/clear.ts +8 -0
- package/src/lib/db/bootstrap.ts +24 -0
- package/src/lib/db/schema.ts +32 -0
- package/src/lib/docs/types.ts +9 -0
- /package/src/app/api/{playbook → user-guide}/status/route.ts +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { Book, BookChapter, BookPart } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Mapping from chapter ID to markdown filename slug (without ch-N- prefix) */
|
|
4
|
+
const CHAPTER_SLUG_MAP: Record<string, string> = {
|
|
5
|
+
"ch-1": "ch-1-project-management",
|
|
6
|
+
"ch-2": "ch-2-task-execution",
|
|
7
|
+
"ch-3": "ch-3-document-processing",
|
|
8
|
+
"ch-4": "ch-4-workflow-orchestration",
|
|
9
|
+
"ch-5": "ch-5-scheduled-intelligence",
|
|
10
|
+
"ch-6": "ch-6-agent-self-improvement",
|
|
11
|
+
"ch-7": "ch-7-multi-agent-swarms",
|
|
12
|
+
"ch-8": "ch-8-human-in-the-loop",
|
|
13
|
+
"ch-9": "ch-9-the-autonomous-organization",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The three parts of the AI Native book */
|
|
17
|
+
export const PARTS: BookPart[] = [
|
|
18
|
+
{
|
|
19
|
+
number: 1,
|
|
20
|
+
title: "Foundation",
|
|
21
|
+
description: "Operations — from manual processes to AI-assisted automation",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
number: 2,
|
|
25
|
+
title: "Intelligence",
|
|
26
|
+
description: "Workflows & Learning — adaptive systems that improve over time",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
number: 3,
|
|
30
|
+
title: "Autonomy",
|
|
31
|
+
description: "Advanced Patterns — fully delegated business processes",
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Chapter metadata stubs — content is loaded from docs/book/*.md at runtime.
|
|
37
|
+
* These stubs provide the chapter list for navigation and iteration.
|
|
38
|
+
* Sections are populated by tryLoadMarkdownChapter().
|
|
39
|
+
*/
|
|
40
|
+
export const CHAPTERS: BookChapter[] = [
|
|
41
|
+
{
|
|
42
|
+
id: "ch-1",
|
|
43
|
+
number: 1,
|
|
44
|
+
title: "Project Management",
|
|
45
|
+
subtitle: "From Manual Planning to Autonomous Sprint Planning",
|
|
46
|
+
part: PARTS[0],
|
|
47
|
+
readingTime: 12,
|
|
48
|
+
relatedDocs: ["projects", "home-workspace", "dashboard-kanban"],
|
|
49
|
+
relatedJourney: "personal-use",
|
|
50
|
+
sections: [],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "ch-2",
|
|
54
|
+
number: 2,
|
|
55
|
+
title: "Task Execution",
|
|
56
|
+
subtitle: "Single-Agent to Multi-Agent Task Orchestration",
|
|
57
|
+
part: PARTS[0],
|
|
58
|
+
readingTime: 15,
|
|
59
|
+
relatedDocs: ["agent-intelligence", "profiles", "monitoring"],
|
|
60
|
+
relatedJourney: "work-use",
|
|
61
|
+
sections: [],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "ch-3",
|
|
65
|
+
number: 3,
|
|
66
|
+
title: "Document Processing",
|
|
67
|
+
subtitle: "Unstructured Input to Structured Knowledge",
|
|
68
|
+
part: PARTS[0],
|
|
69
|
+
readingTime: 14,
|
|
70
|
+
relatedDocs: ["documents", "shared-components"],
|
|
71
|
+
sections: [],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "ch-4",
|
|
75
|
+
number: 4,
|
|
76
|
+
title: "Workflow Orchestration",
|
|
77
|
+
subtitle: "From Linear Sequences to Adaptive Blueprints",
|
|
78
|
+
part: PARTS[1],
|
|
79
|
+
readingTime: 14,
|
|
80
|
+
relatedDocs: ["workflows", "agent-intelligence"],
|
|
81
|
+
relatedJourney: "power-user",
|
|
82
|
+
sections: [],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "ch-5",
|
|
86
|
+
number: 5,
|
|
87
|
+
title: "Scheduled Intelligence",
|
|
88
|
+
subtitle: "Time-Based Automation and Recurring Intelligence Loops",
|
|
89
|
+
part: PARTS[1],
|
|
90
|
+
readingTime: 11,
|
|
91
|
+
relatedDocs: ["schedules", "monitoring"],
|
|
92
|
+
sections: [],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "ch-6",
|
|
96
|
+
number: 6,
|
|
97
|
+
title: "Agent Self-Improvement",
|
|
98
|
+
subtitle: "Learning from Execution Logs and Feedback",
|
|
99
|
+
part: PARTS[1],
|
|
100
|
+
readingTime: 13,
|
|
101
|
+
relatedDocs: ["agent-intelligence", "profiles"],
|
|
102
|
+
relatedJourney: "developer",
|
|
103
|
+
sections: [],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "ch-7",
|
|
107
|
+
number: 7,
|
|
108
|
+
title: "Multi-Agent Swarms",
|
|
109
|
+
subtitle: "Parallel Execution, Consensus, and Specialization",
|
|
110
|
+
part: PARTS[2],
|
|
111
|
+
readingTime: 16,
|
|
112
|
+
relatedDocs: ["profiles", "agent-intelligence"],
|
|
113
|
+
sections: [],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "ch-8",
|
|
117
|
+
number: 8,
|
|
118
|
+
title: "Human-in-the-Loop",
|
|
119
|
+
subtitle: "Permission Systems and Graceful Escalation",
|
|
120
|
+
part: PARTS[2],
|
|
121
|
+
readingTime: 12,
|
|
122
|
+
relatedDocs: ["inbox-notifications", "tool-permissions", "settings"],
|
|
123
|
+
sections: [],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "ch-9",
|
|
127
|
+
number: 9,
|
|
128
|
+
title: "The Autonomous Organization",
|
|
129
|
+
subtitle: "Fully Delegated Business Processes",
|
|
130
|
+
part: PARTS[2],
|
|
131
|
+
readingTime: 18,
|
|
132
|
+
relatedDocs: ["workflows", "profiles", "schedules"],
|
|
133
|
+
sections: [],
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Try to load a chapter from its markdown file in book/chapters/.
|
|
139
|
+
* Only works server-side (where fs is available). Returns null on client.
|
|
140
|
+
*/
|
|
141
|
+
function tryLoadMarkdownChapter(id: string): BookChapter | null {
|
|
142
|
+
// Only attempt markdown loading on the server
|
|
143
|
+
if (typeof window !== "undefined") return null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const fileSlug = CHAPTER_SLUG_MAP[id];
|
|
147
|
+
if (!fileSlug) return null;
|
|
148
|
+
|
|
149
|
+
// Dynamic require to avoid bundling fs in client builds
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
151
|
+
const { readFileSync, existsSync } = require("fs") as typeof import("fs");
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
153
|
+
const { join } = require("path") as typeof import("path");
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
155
|
+
const { parseMarkdownChapter } = require("./markdown-parser") as { parseMarkdownChapter: (md: string, slug: string) => { sections: Array<{ id: string; title: string; content: import("./types").ContentBlock[] }> } };
|
|
156
|
+
|
|
157
|
+
const filePath = join(process.cwd(), "book", "chapters", `${fileSlug}.md`);
|
|
158
|
+
if (!existsSync(filePath)) return null;
|
|
159
|
+
|
|
160
|
+
const content = readFileSync(filePath, "utf-8");
|
|
161
|
+
|
|
162
|
+
// Parse frontmatter
|
|
163
|
+
const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
164
|
+
const fmMatch = content.match(fmRegex);
|
|
165
|
+
const fmBlock = fmMatch ? fmMatch[1] : "";
|
|
166
|
+
const body = fmMatch ? fmMatch[2] : content;
|
|
167
|
+
|
|
168
|
+
const fm: Record<string, unknown> = {};
|
|
169
|
+
for (const line of fmBlock.split("\n")) {
|
|
170
|
+
const colonIdx = line.indexOf(":");
|
|
171
|
+
if (colonIdx === -1) continue;
|
|
172
|
+
const key = line.slice(0, colonIdx).trim();
|
|
173
|
+
let value: unknown = line.slice(colonIdx + 1).trim();
|
|
174
|
+
if (typeof value === "string" && value.startsWith('"') && value.endsWith('"')) {
|
|
175
|
+
value = value.slice(1, -1);
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
178
|
+
try { value = JSON.parse(value); } catch {
|
|
179
|
+
value = (value as string).slice(1, -1).split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
fm[key] = value;
|
|
183
|
+
}
|
|
184
|
+
const chapterNum = Number(fm.chapter) || 0;
|
|
185
|
+
const partNum = Number(fm.part) || 1;
|
|
186
|
+
const part = PARTS[partNum - 1] || PARTS[0];
|
|
187
|
+
|
|
188
|
+
const { sections } = parseMarkdownChapter(body, id);
|
|
189
|
+
|
|
190
|
+
const relatedDocs = Array.isArray(fm.relatedDocs)
|
|
191
|
+
? (fm.relatedDocs as string[])
|
|
192
|
+
: typeof fm.relatedDocs === "string"
|
|
193
|
+
? [fm.relatedDocs]
|
|
194
|
+
: [];
|
|
195
|
+
|
|
196
|
+
const chapter: BookChapter = {
|
|
197
|
+
id,
|
|
198
|
+
number: chapterNum,
|
|
199
|
+
title: (fm.title as string) || "",
|
|
200
|
+
subtitle: (fm.subtitle as string) || "",
|
|
201
|
+
part,
|
|
202
|
+
sections,
|
|
203
|
+
readingTime: Number(fm.readingTime) || 0,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (relatedDocs.length > 0) chapter.relatedDocs = relatedDocs;
|
|
207
|
+
if (fm.relatedJourney && fm.relatedJourney !== "null") {
|
|
208
|
+
chapter.relatedJourney = fm.relatedJourney as string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return chapter;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Get the full book object */
|
|
218
|
+
export function getBook(): Book {
|
|
219
|
+
const chapters = CHAPTERS.map((ch) => {
|
|
220
|
+
const md = tryLoadMarkdownChapter(ch.id);
|
|
221
|
+
return md || ch;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
title: "AI Native",
|
|
226
|
+
subtitle: "Building Autonomous Business Systems with AI Agents",
|
|
227
|
+
description:
|
|
228
|
+
"A practical guide to building AI-native applications, from single-agent task execution to fully autonomous business processes. Every pattern is demonstrated with working code from Stagent — a tool that built itself.",
|
|
229
|
+
parts: PARTS,
|
|
230
|
+
chapters,
|
|
231
|
+
totalReadingTime: chapters.reduce((sum, ch) => sum + ch.readingTime, 0),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Get a chapter by its ID */
|
|
236
|
+
export function getChapter(id: string): BookChapter | undefined {
|
|
237
|
+
const mdChapter = tryLoadMarkdownChapter(id);
|
|
238
|
+
if (mdChapter) return mdChapter;
|
|
239
|
+
return CHAPTERS.find((ch) => ch.id === id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Get chapters grouped by part */
|
|
243
|
+
export function getChaptersByPart(): Map<number, BookChapter[]> {
|
|
244
|
+
const grouped = new Map<number, BookChapter[]>();
|
|
245
|
+
for (const ch of CHAPTERS) {
|
|
246
|
+
const part = ch.part.number;
|
|
247
|
+
if (!grouped.has(part)) grouped.set(part, []);
|
|
248
|
+
grouped.get(part)!.push(ch);
|
|
249
|
+
}
|
|
250
|
+
return grouped;
|
|
251
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContentBlock,
|
|
3
|
+
TextBlock,
|
|
4
|
+
CodeBlock,
|
|
5
|
+
CalloutBlock,
|
|
6
|
+
ImageBlock,
|
|
7
|
+
InteractiveLinkBlock,
|
|
8
|
+
InteractiveCollapsibleBlock,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
interface ParsedSection {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
content: ContentBlock[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a markdown chapter body (frontmatter already stripped) into structured sections.
|
|
19
|
+
* Uses a line-by-line state machine to identify content blocks.
|
|
20
|
+
*/
|
|
21
|
+
export function parseMarkdownChapter(
|
|
22
|
+
markdown: string,
|
|
23
|
+
chapterSlug: string
|
|
24
|
+
): { sections: ParsedSection[] } {
|
|
25
|
+
const lines = markdown.split("\n");
|
|
26
|
+
const sections: ParsedSection[] = [];
|
|
27
|
+
|
|
28
|
+
// Split by ## headings
|
|
29
|
+
const sectionChunks: Array<{ title: string; lines: string[] }> = [];
|
|
30
|
+
let currentTitle = "Introduction";
|
|
31
|
+
let currentLines: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (line.startsWith("## ")) {
|
|
35
|
+
if (currentLines.length > 0 || sectionChunks.length > 0) {
|
|
36
|
+
sectionChunks.push({ title: currentTitle, lines: currentLines });
|
|
37
|
+
}
|
|
38
|
+
currentTitle = line.slice(3).trim();
|
|
39
|
+
currentLines = [];
|
|
40
|
+
} else {
|
|
41
|
+
currentLines.push(line);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Push the last section
|
|
45
|
+
if (currentLines.length > 0 || sectionChunks.length > 0) {
|
|
46
|
+
sectionChunks.push({ title: currentTitle, lines: currentLines });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If no ## headings were found, treat entire content as one section
|
|
50
|
+
if (sectionChunks.length === 0 && currentLines.length > 0) {
|
|
51
|
+
sectionChunks.push({ title: "Introduction", lines: currentLines });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const chunk of sectionChunks) {
|
|
55
|
+
const sectionId = generateSectionId(chapterSlug, chunk.title);
|
|
56
|
+
const content = parseContentBlocks(chunk.lines);
|
|
57
|
+
sections.push({ id: sectionId, title: chunk.title, content });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { sections };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Convert heading text to a section ID */
|
|
64
|
+
function generateSectionId(chapterSlug: string, title: string): string {
|
|
65
|
+
const slug = title
|
|
66
|
+
.toLowerCase()
|
|
67
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
68
|
+
.replace(/\s+/g, "-")
|
|
69
|
+
.replace(/-+/g, "-")
|
|
70
|
+
.replace(/^-|-$/g, "");
|
|
71
|
+
return `${chapterSlug}-${slug}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parse lines within a section into ContentBlock[] */
|
|
75
|
+
function parseContentBlocks(lines: string[]): ContentBlock[] {
|
|
76
|
+
const blocks: ContentBlock[] = [];
|
|
77
|
+
let textBuffer: string[] = [];
|
|
78
|
+
let i = 0;
|
|
79
|
+
|
|
80
|
+
function flushText() {
|
|
81
|
+
if (textBuffer.length === 0) return;
|
|
82
|
+
const text = textBuffer.join("\n").trim();
|
|
83
|
+
if (text) {
|
|
84
|
+
blocks.push({ type: "text", markdown: text } as TextBlock);
|
|
85
|
+
}
|
|
86
|
+
textBuffer = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
while (i < lines.length) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
|
|
92
|
+
// --- Fenced code block ---
|
|
93
|
+
if (line.trimStart().startsWith("```")) {
|
|
94
|
+
flushText();
|
|
95
|
+
const indent = line.length - line.trimStart().length;
|
|
96
|
+
const lang = line.trimStart().slice(3).trim();
|
|
97
|
+
let filename: string | undefined;
|
|
98
|
+
|
|
99
|
+
// Check if previous line was a filename comment
|
|
100
|
+
if (
|
|
101
|
+
blocks.length > 0 ||
|
|
102
|
+
textBuffer.length > 0
|
|
103
|
+
) {
|
|
104
|
+
// Check the last text buffer line or look back
|
|
105
|
+
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
106
|
+
const filenameMatch = prevLine.match(
|
|
107
|
+
/^<!--\s*filename:\s*(.+?)\s*-->$/
|
|
108
|
+
);
|
|
109
|
+
if (filenameMatch) {
|
|
110
|
+
filename = filenameMatch[1];
|
|
111
|
+
// Remove the filename comment from the last text block if it got flushed
|
|
112
|
+
if (blocks.length > 0 && blocks[blocks.length - 1].type === "text") {
|
|
113
|
+
const lastBlock = blocks[blocks.length - 1] as TextBlock;
|
|
114
|
+
const mdLines = lastBlock.markdown.split("\n");
|
|
115
|
+
if (
|
|
116
|
+
mdLines.length > 0 &&
|
|
117
|
+
mdLines[mdLines.length - 1].match(/^<!--\s*filename:\s*.+\s*-->$/)
|
|
118
|
+
) {
|
|
119
|
+
mdLines.pop();
|
|
120
|
+
const trimmed = mdLines.join("\n").trim();
|
|
121
|
+
if (trimmed) {
|
|
122
|
+
lastBlock.markdown = trimmed;
|
|
123
|
+
} else {
|
|
124
|
+
blocks.pop();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
// Check previous line directly
|
|
131
|
+
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
132
|
+
const filenameMatch = prevLine.match(
|
|
133
|
+
/^<!--\s*filename:\s*(.+?)\s*-->$/
|
|
134
|
+
);
|
|
135
|
+
if (filenameMatch) {
|
|
136
|
+
filename = filenameMatch[1];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const codeLines: string[] = [];
|
|
141
|
+
i++;
|
|
142
|
+
while (i < lines.length) {
|
|
143
|
+
const codeLine = lines[i];
|
|
144
|
+
// Match closing fence at same or less indentation
|
|
145
|
+
if (
|
|
146
|
+
codeLine.trimStart().startsWith("```") &&
|
|
147
|
+
codeLine.trimStart() === "```" &&
|
|
148
|
+
(codeLine.length - codeLine.trimStart().length) <= indent
|
|
149
|
+
) {
|
|
150
|
+
i++;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
codeLines.push(codeLine);
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const block: CodeBlock = {
|
|
158
|
+
type: "code",
|
|
159
|
+
language: lang || "text",
|
|
160
|
+
code: codeLines.join("\n"),
|
|
161
|
+
};
|
|
162
|
+
if (filename) block.filename = filename;
|
|
163
|
+
blocks.push(block);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- Callout blockquote: > [!variant] ---
|
|
168
|
+
const calloutMatch = line.match(
|
|
169
|
+
/^>\s*\[!(tip|warning|info|lesson|authors-note)\]\s*$/
|
|
170
|
+
);
|
|
171
|
+
if (calloutMatch) {
|
|
172
|
+
flushText();
|
|
173
|
+
const variant = calloutMatch[1] as CalloutBlock["variant"];
|
|
174
|
+
const calloutLines: string[] = [];
|
|
175
|
+
let title: string | undefined;
|
|
176
|
+
let imageSrc: string | undefined;
|
|
177
|
+
let imageAlt: string | undefined;
|
|
178
|
+
let defaultCollapsed = false;
|
|
179
|
+
|
|
180
|
+
i++;
|
|
181
|
+
// Check for optional title on first line: > **Title**
|
|
182
|
+
if (i < lines.length) {
|
|
183
|
+
const titleMatch = lines[i].match(/^>\s*\*\*(.+?)\*\*\s*$/);
|
|
184
|
+
if (titleMatch) {
|
|
185
|
+
title = titleMatch[1];
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Collect blockquote content
|
|
191
|
+
while (i < lines.length) {
|
|
192
|
+
const bqLine = lines[i];
|
|
193
|
+
if (bqLine.startsWith("> ") || bqLine === ">") {
|
|
194
|
+
let content = bqLine === ">" ? "" : bqLine.slice(2);
|
|
195
|
+
|
|
196
|
+
// Check for image in callout
|
|
197
|
+
const imgMatch = content.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
198
|
+
if (imgMatch) {
|
|
199
|
+
imageAlt = imgMatch[1];
|
|
200
|
+
imageSrc = imgMatch[2];
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check for collapsed marker
|
|
206
|
+
if (content.trim() === "[collapsed]") {
|
|
207
|
+
defaultCollapsed = true;
|
|
208
|
+
i++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
calloutLines.push(content);
|
|
213
|
+
i++;
|
|
214
|
+
} else if (bqLine.trim() === "") {
|
|
215
|
+
// Blank line ends the blockquote
|
|
216
|
+
i++;
|
|
217
|
+
break;
|
|
218
|
+
} else {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (variant === "authors-note") {
|
|
224
|
+
defaultCollapsed = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const block: CalloutBlock = {
|
|
228
|
+
type: "callout",
|
|
229
|
+
variant,
|
|
230
|
+
markdown: calloutLines.join("\n").trim(),
|
|
231
|
+
};
|
|
232
|
+
if (title) block.title = title;
|
|
233
|
+
if (imageSrc) block.imageSrc = imageSrc;
|
|
234
|
+
if (imageAlt) block.imageAlt = imageAlt;
|
|
235
|
+
if (defaultCollapsed) block.defaultCollapsed = true;
|
|
236
|
+
blocks.push(block);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Image on its own line:  ---
|
|
241
|
+
const imageMatch = line.match(
|
|
242
|
+
/^!\[([^\]]*)\]\((\S+?)(?:\s+"([^"]*)")?\)\s*$/
|
|
243
|
+
);
|
|
244
|
+
if (imageMatch) {
|
|
245
|
+
flushText();
|
|
246
|
+
const block: ImageBlock = {
|
|
247
|
+
type: "image",
|
|
248
|
+
src: imageMatch[2],
|
|
249
|
+
alt: imageMatch[1],
|
|
250
|
+
};
|
|
251
|
+
if (imageMatch[3]) block.caption = imageMatch[3];
|
|
252
|
+
blocks.push(block);
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// --- Interactive link: [Try: label](href) ---
|
|
258
|
+
const tryMatch = line.match(/^\[Try:\s*(.+?)\]\((.+?)\)\s*$/);
|
|
259
|
+
if (tryMatch) {
|
|
260
|
+
flushText();
|
|
261
|
+
blocks.push({
|
|
262
|
+
type: "interactive",
|
|
263
|
+
interactiveType: "link",
|
|
264
|
+
label: `Try: ${tryMatch[1]}`,
|
|
265
|
+
description: "",
|
|
266
|
+
href: tryMatch[2],
|
|
267
|
+
} as InteractiveLinkBlock);
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- Details/collapsible: <details><summary>label</summary> ---
|
|
273
|
+
const detailsMatch = line.match(
|
|
274
|
+
/^<details>\s*<summary>(.+?)<\/summary>\s*$/
|
|
275
|
+
);
|
|
276
|
+
if (detailsMatch) {
|
|
277
|
+
flushText();
|
|
278
|
+
const label = detailsMatch[1];
|
|
279
|
+
const contentLines: string[] = [];
|
|
280
|
+
i++;
|
|
281
|
+
while (i < lines.length) {
|
|
282
|
+
if (lines[i].trim() === "</details>") {
|
|
283
|
+
i++;
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
contentLines.push(lines[i]);
|
|
287
|
+
i++;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
blocks.push({
|
|
291
|
+
type: "interactive",
|
|
292
|
+
interactiveType: "collapsible",
|
|
293
|
+
label,
|
|
294
|
+
markdown: contentLines.join("\n").trim(),
|
|
295
|
+
} as InteractiveCollapsibleBlock);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Filename comment line (will be consumed by next code block) ---
|
|
300
|
+
if (line.match(/^<!--\s*filename:\s*.+\s*-->$/)) {
|
|
301
|
+
// Don't flush yet — peek ahead for a code block
|
|
302
|
+
if (i + 1 < lines.length && lines[i + 1].trimStart().startsWith("```")) {
|
|
303
|
+
// Let the code block handler pick it up
|
|
304
|
+
textBuffer.push(line);
|
|
305
|
+
i++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- Regular text ---
|
|
311
|
+
textBuffer.push(line);
|
|
312
|
+
i++;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
flushText();
|
|
316
|
+
return blocks;
|
|
317
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { UsageStage } from "@/lib/docs/types";
|
|
2
|
+
|
|
3
|
+
export interface ReadingPath {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
persona: string;
|
|
8
|
+
chapterIds: string[];
|
|
9
|
+
usageStage: UsageStage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const READING_PATHS: ReadingPath[] = [
|
|
13
|
+
{
|
|
14
|
+
id: "getting-started",
|
|
15
|
+
name: "Getting Started",
|
|
16
|
+
description: "Essential chapters for new users learning the basics",
|
|
17
|
+
persona: "new",
|
|
18
|
+
chapterIds: ["ch-1", "ch-2", "ch-3"],
|
|
19
|
+
usageStage: "new",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "team-lead",
|
|
23
|
+
name: "Team Lead",
|
|
24
|
+
description: "Chapters focused on team management and workflows",
|
|
25
|
+
persona: "work",
|
|
26
|
+
chapterIds: ["ch-1", "ch-4", "ch-5"],
|
|
27
|
+
usageStage: "early",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "power-user",
|
|
31
|
+
name: "Power User",
|
|
32
|
+
description: "Advanced chapters for active users ready to go deeper",
|
|
33
|
+
persona: "active",
|
|
34
|
+
chapterIds: ["ch-4", "ch-5", "ch-6", "ch-7", "ch-8"],
|
|
35
|
+
usageStage: "active",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "developer",
|
|
39
|
+
name: "Developer",
|
|
40
|
+
description: "Comprehensive path covering the full technical depth",
|
|
41
|
+
persona: "developer",
|
|
42
|
+
chapterIds: ["ch-1", "ch-2", "ch-3", "ch-4", "ch-5", "ch-6", "ch-7", "ch-8", "ch-9"],
|
|
43
|
+
usageStage: "power",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Recommend a reading path based on the user's usage stage */
|
|
48
|
+
export function recommendPath(stage: UsageStage): string {
|
|
49
|
+
switch (stage) {
|
|
50
|
+
case "new":
|
|
51
|
+
return "getting-started";
|
|
52
|
+
case "early":
|
|
53
|
+
return "team-lead";
|
|
54
|
+
case "active":
|
|
55
|
+
return "power-user";
|
|
56
|
+
case "power":
|
|
57
|
+
return "developer";
|
|
58
|
+
default:
|
|
59
|
+
return "getting-started";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get a reading path by ID */
|
|
64
|
+
export function getReadingPath(id: string): ReadingPath | undefined {
|
|
65
|
+
return READING_PATHS.find((p) => p.id === id);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Get the next chapter ID in a path after the current one, or null if at end */
|
|
69
|
+
export function getNextPathChapter(pathId: string, currentChapterId: string): string | null {
|
|
70
|
+
const path = getReadingPath(pathId);
|
|
71
|
+
if (!path) return null;
|
|
72
|
+
const idx = path.chapterIds.indexOf(currentChapterId);
|
|
73
|
+
if (idx === -1 || idx >= path.chapterIds.length - 1) return null;
|
|
74
|
+
return path.chapterIds[idx + 1];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Check if a chapter is part of a reading path */
|
|
78
|
+
export function isChapterInPath(pathId: string, chapterId: string): boolean {
|
|
79
|
+
const path = getReadingPath(pathId);
|
|
80
|
+
if (!path) return false;
|
|
81
|
+
return path.chapterIds.includes(chapterId);
|
|
82
|
+
}
|