Skip to content

Commit b04ecdf

Browse files
ouiliameclaude
andcommitted
docs: add Academy learning surface
Adds the Academy section to the docs: video-first lessons (self-hosted MP4 on Vercel Blob), organized into Workflows, Agents, Tables, Files, and Knowledge Bases, each linking back to the reference docs. Lessons use a course layout (hero video with chapter seek, "what you'll learn", block diagrams). Docs only — no runtime or auth changes. The content may move to a separate CMS or its own site (academy.sim.ai) later; the docs are a starting point. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e1c3c7f commit b04ecdf

24 files changed

Lines changed: 1451 additions & 4 deletions

apps/docs/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ next-env.d.ts
3838
# Fumadocs
3939
/.source/
4040
.plans/
41+
42+
# fumadocs generates .source dirs anywhere a source.config sits
43+
**/.source/

apps/docs/app/[lang]/[[...slug]]/page.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
7575
}
7676
const isOpenAPI = '_openapi' in data && data._openapi != null
7777
const isApiReference = slug?.some((s) => s === 'api-reference') ?? false
78+
// Academy lessons are video-first: drop the "On this page" TOC and go full
79+
// width so the lesson hero/video gets the room (chapters live in-page instead).
80+
const isAcademy = slug?.[0] === 'academy'
7881

7982
const pageTreeRecord = source.pageTree as Record<string, Root>
8083
const pageTree = pageTreeRecord[lang] ?? pageTreeRecord.en ?? Object.values(pageTreeRecord)[0]
@@ -197,18 +200,18 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l
197200
/>
198201
<DocsPage
199202
toc={data.toc}
200-
full={data.full}
203+
full={data.full || isAcademy}
201204
breadcrumb={{
202205
enabled: false,
203206
}}
204207
tableOfContent={{
205208
style: 'clerk',
206-
enabled: true,
209+
enabled: !isAcademy,
207210
single: false,
208211
}}
209212
tableOfContentPopover={{
210213
style: 'clerk',
211-
enabled: true,
214+
enabled: !isAcademy,
212215
}}
213216
footer={{
214217
enabled: true,

apps/docs/components/navbar/navbar.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ const NAV_TABS = [
1313
{
1414
label: 'Documentation',
1515
href: '/introduction',
16-
match: (p: string) => !p.includes('/api-reference'),
16+
match: (p: string) => !p.includes('/api-reference') && !p.includes('/academy'),
17+
external: false,
18+
},
19+
{
20+
label: 'Academy',
21+
href: '/academy',
22+
match: (p: string) => p.includes('/academy'),
1723
external: false,
1824
},
1925
{
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { BookOpen, Check, CirclePlay, Clock } from 'lucide-react'
2+
import { cn } from '@/lib/utils'
3+
4+
interface Lesson {
5+
title: string
6+
/** e.g. "4:12". Omit to show "View" instead. */
7+
duration?: string
8+
/** Highlights the current lesson. */
9+
active?: boolean
10+
/** Renders a completed checkmark. */
11+
done?: boolean
12+
}
13+
14+
interface CourseProgressProps {
15+
/** Course name shown as the panel heading. */
16+
course: string
17+
lessons: Lesson[]
18+
/** e.g. "Approx. 18 min". */
19+
durationLabel?: string
20+
/** Completion percentage 0–100. */
21+
progress?: number
22+
className?: string
23+
}
24+
25+
/** Right-rail course panel: lesson count, duration, progress bar, and lesson list. */
26+
export function CourseProgress({
27+
course,
28+
lessons,
29+
durationLabel,
30+
progress = 0,
31+
className,
32+
}: CourseProgressProps) {
33+
return (
34+
<aside className={cn('rounded-xl border border-fd-border bg-fd-card/40 p-5', className)}>
35+
<h2 className='mt-0 mb-3 font-semibold text-fd-foreground text-lg'>{course}</h2>
36+
37+
<div className='flex flex-wrap items-center gap-2 border-fd-border border-b pb-4 text-fd-muted-foreground text-xs'>
38+
<span className='inline-flex items-center gap-1.5 rounded-md border border-fd-border px-2 py-1'>
39+
<BookOpen className='size-3.5' />
40+
{lessons.length} lessons
41+
</span>
42+
{durationLabel && (
43+
<span className='inline-flex items-center gap-1.5 rounded-md border border-fd-border px-2 py-1'>
44+
<Clock className='size-3.5' />
45+
{durationLabel}
46+
</span>
47+
)}
48+
</div>
49+
50+
<div className='py-4'>
51+
<div className='mb-2 flex items-center justify-between text-sm'>
52+
<span className='text-fd-foreground'>Your progress</span>
53+
<span className='text-fd-muted-foreground'>{Math.min(100, Math.max(0, progress))}%</span>
54+
</div>
55+
<div className='h-1.5 w-full overflow-hidden rounded-full bg-fd-muted'>
56+
<div
57+
className='h-full rounded-full bg-[#33c482] transition-all'
58+
style={{ width: `${Math.min(100, Math.max(0, progress))}%` }}
59+
/>
60+
</div>
61+
</div>
62+
63+
<ul className='m-0 flex list-none flex-col gap-0.5 p-0'>
64+
{lessons.map((lesson) => (
65+
<li
66+
key={lesson.title}
67+
className={cn(
68+
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm',
69+
lesson.active
70+
? 'bg-fd-accent text-fd-foreground'
71+
: 'text-fd-muted-foreground hover:bg-fd-accent/50'
72+
)}
73+
>
74+
{lesson.done ? (
75+
<Check className='size-4 shrink-0 text-[#33c482]' />
76+
) : (
77+
<CirclePlay
78+
className={cn('size-4 shrink-0', lesson.active && 'text-fd-foreground')}
79+
/>
80+
)}
81+
<span className={cn('flex-1 truncate', lesson.active && 'font-medium')}>
82+
{lesson.title}
83+
</span>
84+
<span className='shrink-0 text-fd-muted-foreground text-xs tabular-nums'>
85+
{lesson.duration ?? 'View'}
86+
</span>
87+
</li>
88+
))}
89+
</ul>
90+
</aside>
91+
)
92+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client'
2+
3+
import { CirclePlay } from 'lucide-react'
4+
import { cn } from '@/lib/utils'
5+
6+
/** Parse a chapter timestamp ("M:SS" or "H:MM:SS") into seconds. */
7+
function parseTime(time: string): number {
8+
const parts = time.split(':').map(Number)
9+
if (parts.some(Number.isNaN)) return 0
10+
return parts.reduce((acc, n) => acc * 60 + n, 0)
11+
}
12+
13+
interface Chapter {
14+
/** Chapter label. */
15+
title: string
16+
/** Timestamp, e.g. "0:45". */
17+
time?: string
18+
}
19+
20+
interface VideoChaptersProps {
21+
/** Panel heading. Defaults to "Chapters". */
22+
title?: string
23+
chapters: Chapter[]
24+
className?: string
25+
}
26+
27+
/**
28+
* Right-rail panel listing the current video's chapters, styled to match the
29+
* Academy's course panels. Rows are skip-to controls; they activate once the
30+
* lesson's video is recorded.
31+
*/
32+
export function VideoChapters({ title = 'Chapters', chapters, className }: VideoChaptersProps) {
33+
return (
34+
<aside
35+
className={cn('not-prose rounded-xl border border-fd-border bg-fd-card/40 p-5', className)}
36+
>
37+
<h2 className='mt-0 mb-3 font-semibold text-fd-foreground text-lg'>{title}</h2>
38+
<ul className='m-0 flex list-none flex-col gap-0.5 p-0'>
39+
{chapters.map((chapter) => (
40+
<li key={chapter.title}>
41+
<button
42+
type='button'
43+
disabled={chapter.time == null}
44+
onClick={() => {
45+
if (chapter.time == null) return
46+
window.dispatchEvent(
47+
new CustomEvent('academy:seek', { detail: { time: parseTime(chapter.time) } })
48+
)
49+
}}
50+
className='flex w-full cursor-pointer items-start gap-2.5 rounded-lg px-2.5 py-2 text-left text-fd-muted-foreground text-sm transition-colors hover:bg-fd-accent/50 disabled:cursor-default disabled:hover:bg-transparent'
51+
>
52+
<CirclePlay className='mt-0.5 size-4 shrink-0' />
53+
<span className='min-w-0 flex-1 break-words'>{chapter.title}</span>
54+
{chapter.time && (
55+
<span className='mt-0.5 shrink-0 text-fd-muted-foreground text-xs tabular-nums'>
56+
{chapter.time}
57+
</span>
58+
)}
59+
</button>
60+
</li>
61+
))}
62+
</ul>
63+
</aside>
64+
)
65+
}

0 commit comments

Comments
 (0)