Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# LearnKit AI — Roadmap

LearnKit AI is an open-source TypeScript engine for embedding role-aware AI learning paths in any product. Apache-2.0, no hosted SaaS, no paid tier.

---

## v0 — Foundation ✅

Core engine, React component, and public website shipped.

### Packages
- [x] `@learnkit-ai/schemas` — Zod schemas and inferred TypeScript types
- [x] `@learnkit-ai/core` — `generateLearningPath()`, pure deterministic, no LLM
- [x] `@learnkit-ai/react` — `<LearningPath />`, `<LessonCard />`, `<AIGuide />`, `useLearnKit()`

### Content
- [x] Role-specific 4-week curricula: Product Manager, Software Engineer, Designer, Data Analyst, Marketer, Founder, Operations, Researcher
- [x] 8 supported tools: Claude, ChatGPT, Cursor, Copilot, Midjourney, Notion AI, Perplexity, Gemini
- [x] `level` field (`beginner` / `intermediate` / `advanced`) adjusts lesson pacing

### Web
- [x] Landing page (`/`)
- [x] Interactive demo (`/demo`) with role, tools, goal, level, and optional company context
- [x] Docs (`/docs`) with full API reference
- [x] `/roles/[slug]` and `/tools/[slug]` SEO pages
- [x] `/teams` page (OSS integration guide)
- [x] Blog (`/blog`) — pedagogy and engineering posts
- [x] Custom 404, OG image, sitemap, robots.txt

### Infrastructure
- [x] pnpm workspaces + Turborepo
- [x] tsup: ESM + CJS + `.d.ts` for all packages
- [x] Vitest: unit tests for core + React components
- [x] GitHub Actions CI
- [x] `examples/nextjs-basic`

---

## v1 — Depth (planned)

### Engine
- [ ] `companyContext` field used in lesson personalisation (currently accepted but not applied)
- [ ] More supported tools (target: 20+), including Windsurf, Replit, Linear, Figma AI, v0
- [ ] More roles: Sales, Customer Success, Legal, Finance, Executive
- [ ] `progress` field in `LearningPath` to track completed lessons
- [ ] Lesson prerequisite graph — reorder-aware sequencing
- [ ] `generateLessonContent(lesson)` — returns full lesson body, exercises, and rubric

### React package
- [ ] `<ProgressTracker />` component — persist lesson state to localStorage
- [ ] `<LessonDetail />` — renders full lesson body returned by `generateLessonContent()`
- [ ] `light` theme variant (in addition to `warm`, `midnight`, `technical`)
- [ ] Headless mode: all components accept `renderItem` render-prop overrides

### Web
- [ ] `/changelog` page — versioned release notes
- [ ] `/compare` pages — e.g. "Claude vs ChatGPT for engineers"
- [ ] `/guides/[slug]` — long-form integration guides
- [ ] Per-role `opengraph-image` for `/roles/[slug]` pages

### Developer experience
- [ ] `@learnkit-ai/cli` — `npx @learnkit-ai/cli generate` outputs a JSON learning path
- [ ] VS Code extension — sidebar learning path panel
- [ ] Storybook for `@learnkit-ai/react` components

---

## v2 — Scale (exploratory)

These are directions worth exploring, not commitments.

- Multi-language support: lessons in Spanish, French, German, Portuguese
- Team progress aggregation: local-first, no server required (IndexedDB + export)
- Embeddable widget (`<script>` tag, no build step required)
- Plugin API: third-party lesson packs and tool definitions

---

## What will never be in LearnKit AI

- A hosted SaaS platform or paid tier
- A backend server, database, or auth system
- Real LLM API calls inside the engine
- Telemetry, analytics, or phone-home behaviour
- Team dashboards, HR reporting, SSO, or SCIM

The engine is a library. Embedding it in a SaaS product is the point — but that SaaS is yours to build, not ours to host.

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). The best first issues are tagged [`good first issue`](https://github.com/learnkit-ai/learnkit/labels/good%20first%20issue) on GitHub.
7 changes: 6 additions & 1 deletion apps/web/src/components/demo/CurriculumView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,23 @@ export function CurriculumView({
tools,
goal,
role,
level = 'beginner',
companyContext,
onLessonClick,
}: {
tools: string[];
goal?: string;
role?: string;
level?: 'beginner' | 'intermediate' | 'advanced';
companyContext?: string;
onLessonClick: () => void;
}) {
const path = generateLearningPath({
role: role ?? 'Product Manager',
tools: tools.length > 0 ? tools : ['Claude'],
goal: goal ?? 'Ship something useful this Friday',
level: 'beginner',
level,
...(companyContext ? { companyContext } : {}),
});

const totalHours = Math.round(path.totalMinutes / 60);
Expand Down
118 changes: 118 additions & 0 deletions apps/web/src/components/demo/DemoFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,23 @@ const GOAL_SAMPLES = [
'Stop hallucinated answers in our customer support bot',
];

const LEVELS = [
{ value: 'beginner', label: 'Beginner', hint: 'New to AI tools' },
{ value: 'intermediate', label: 'Intermediate', hint: 'Use them daily' },
{ value: 'advanced', label: 'Advanced', hint: 'Build with APIs' },
] as const;

type Level = 'beginner' | 'intermediate' | 'advanced';

export function DemoFlow() {
const [step, setStep] = useState(0);
const [role, setRole] = useState('Product Manager');
const [tools, setTools] = useState<string[]>(['Claude', 'Cursor']);
const [goal, setGoal] = useState('');
const [typedGoal, setTypedGoal] = useState('');
const [level, setLevel] = useState<Level>('beginner');
const [companyContext, setCompanyContext] = useState('');
const [showContext, setShowContext] = useState(false);
const [genProgress, setGenProgress] = useState(0);

// Auto-type goal placeholder
Expand Down Expand Up @@ -88,6 +99,9 @@ export function DemoFlow() {
setStep(0);
setGoal('');
setTypedGoal('');
setLevel('beginner');
setCompanyContext('');
setShowContext(false);
};

return (
Expand Down Expand Up @@ -380,6 +394,108 @@ export function DemoFlow() {
</Chip>
))}
</div>

{/* Level picker */}
<div style={{ maxWidth: 720, width: '100%', marginTop: 28 }}>
<div
style={{
fontSize: 12,
fontFamily: 'var(--mono)',
color: 'var(--muted)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: 10,
}}
>
Your current level
</div>
<div style={{ display: 'flex', gap: 10 }}>
{LEVELS.map(({ value, label, hint }) => (
<button
key={value}
onClick={() => setLevel(value)}
style={{
flex: 1,
padding: '14px 16px',
borderRadius: 12,
background: level === value ? 'var(--ink)' : 'var(--surface)',
color: level === value ? 'var(--paper)' : 'var(--ink)',
border: `1.5px solid ${level === value ? 'var(--ink)' : 'var(--rule)'}`,
cursor: 'pointer',
textAlign: 'left',
transition: 'all .15s ease',
}}
>
<div style={{ fontSize: 15, fontWeight: 500, fontFamily: 'var(--serif)', letterSpacing: '-0.01em' }}>
{label}
</div>
<div style={{ fontSize: 11, color: level === value ? 'rgba(244,239,227,0.65)' : 'var(--muted)', fontFamily: 'var(--mono)', marginTop: 3 }}>
{hint}
</div>
</button>
))}
</div>
</div>

{/* Optional company context */}
<div style={{ maxWidth: 720, width: '100%', marginTop: 16 }}>
{!showContext ? (
<button
onClick={() => setShowContext(true)}
style={{
background: 'none',
border: 'none',
color: 'var(--muted)',
fontSize: 12,
fontFamily: 'var(--mono)',
cursor: 'pointer',
padding: 0,
textDecoration: 'underline',
textUnderlineOffset: 3,
}}
>
+ Add company context (optional)
</button>
) : (
<div>
<div
style={{
fontSize: 12,
fontFamily: 'var(--mono)',
color: 'var(--muted)',
textTransform: 'uppercase',
letterSpacing: '0.1em',
marginBottom: 8,
}}
>
Company context <span style={{ textTransform: 'none', letterSpacing: 0 }}>(max 500 chars)</span>
</div>
<textarea
value={companyContext}
onChange={(e) => setCompanyContext(e.target.value.slice(0, 500))}
placeholder="e.g. B2B SaaS, 40-person team, Python + React stack, ships weekly"
style={{
width: '100%',
minHeight: 80,
padding: '12px 16px',
fontFamily: 'var(--mono)',
fontSize: 13,
lineHeight: 1.5,
background: 'var(--surface)',
border: '1.5px solid var(--rule-strong)',
borderRadius: 10,
resize: 'none',
color: 'var(--ink)',
outline: 'none',
}}
/>
<div style={{ fontSize: 11, color: 'var(--muted)', fontFamily: 'var(--mono)', marginTop: 4 }}>
{companyContext.length}/500
</div>
</div>
)}
</div>

<FlowFooter
onBack={() => setStep(1)}
onNext={advance}
Expand Down Expand Up @@ -420,6 +536,8 @@ export function DemoFlow() {
tools={tools}
role={role}
goal={goal || typedGoal}
level={level}
companyContext={companyContext || undefined}
onLessonClick={() => setStep(5)}
/>
<FlowFooter
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/lib/blog-posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,58 @@ export const BLOG_POSTS: BlogPost[] = [
'One detail worth knowing: the path is fully generated client-side. There is no network call in generateLearningPath(). That means it works in SSR, in edge functions, in offline mode. It also means you can pregenerate paths for your most common role/level combinations and cache them at build time — a pattern we use on /demo to make the initial render instant.',
],
},
{
slug: 'theming-learnkit-css-custom-properties',
title: 'Theming LearnKit AI: CSS custom properties as a first-class API',
description:
'How @learnkit-ai/react ships three themes with zero Tailwind dependency — and how to drop in your own brand colors in four lines of CSS.',
date: '2026-05-22',
readMin: 5,
category: 'Engineering',
excerpt:
'No Tailwind in @learnkit-ai/react — just CSS custom properties. Override four variables and the entire component tree picks up your brand colors.',
body: [
'Most component libraries that ship theming require you to configure a design token system, install a CSS preprocessor, or wrap your app in a theme provider. We wanted something simpler: drop in four lines of CSS and your product\'s colors work everywhere.',
'@learnkit-ai/react has no Tailwind dependency. Every style is expressed as an inline CSS value or a CSS custom property reference — things like var(--accent), var(--paper), var(--ink). The three built-in themes (warm, midnight, technical) are just different values for those same twelve variables, applied as inline styles on the root component element.',
'To match your brand, you do not need to fork the package. Pass theme="warm" and add a stylesheet that overrides the root variables: :root { --accent: #7C3AED; --paper: #FAFAF9; --ink: #1A1523; --ink-soft: #5B5070; }. The full component tree — cards, progress bars, the AI Guide widget — inherits your values immediately.',
'The decision to avoid Tailwind was deliberate. Tailwind requires a build step tuned to the host project\'s content paths. A library that ships Tailwind classes leaks its build config into every project that installs it. CSS custom properties are a browser primitive — they compose without configuration.',
'If you want to ship your own named theme, the pattern is simple: export a theme object that maps the twelve variable names to your values, pass it as the theme prop, and the component applies them as a style attribute. We will document the full theme object shape in the v1 docs.',
],
},
{
slug: 'role-based-learning-paths-the-design-decisions',
title: 'Role-based learning paths: the design decisions behind the curricula',
description:
'Why we built eight role-specific 4-week curricula instead of a generic AI training track, and what we learned from the first iteration of each.',
date: '2026-05-22',
readMin: 7,
category: 'Pedagogy',
excerpt:
'Eight roles, eight 4-week curricula, zero shared lessons between them. Here is why role specificity matters and what we got wrong in the first draft.',
body: [
'The initial design for LearnKit AI was a single shared curriculum: four weeks of foundational AI skills that any professional could follow. We scrapped it after the second pilot. The problem was not quality — the content was fine. The problem was relevance. A PM and a software engineer sitting through the same "building a research agent" lesson are in completely different contexts. The PM needs to scope the agent to a workflow they own. The engineer needs to write the eval harness. A single lesson cannot do both well.',
'So we built eight curricula — one for each supported role — with no shared lessons. Product Managers start with discovery briefs and PRDs before ever writing an agent. Software Engineers start with prompt architecture and eval patterns on day one. Designers start with generating and critiquing copy, not writing code.',
'The lesson structure is consistent: three lessons per week, with the third always a project. Projects are the unit of completion that actually matters. They are not exercises — they are deliverables your team could use. The week-three project for a PM is a go-to-market brief. For an Operations specialist, it is a weekly reporting prompt they can run every Monday. For a Researcher, it is a literature synthesis they would file in their research archive.',
'The hardest role to design for was Founder. Founders span every function — they are their own PM, their own engineer, their own marketer in the early days. We resisted the temptation to build a generalist curriculum. Instead, the Founder track focuses on the three AI use cases that are most asymmetric for a solo operator: strategic stress-testing (using AI to argue against your own plan), voice-consistent writing at scale, and rapid market validation from public signals.',
'None of the eight curricula are finished. They will not be in v1 either. The right approach is to ship opinionated first drafts, collect signal from what people actually build in week-three projects, and iterate. If you fork the repo and find that one role\'s curriculum is weak, a PR is exactly the right response.',
],
},
{
slug: 'the-companycontext-field',
title: 'The companyContext field: personalising learning paths for your stack',
description:
'An explanation of the optional companyContext input field, why it exists, and how it will be used to personalise lesson summaries in v1.',
date: '2026-05-22',
readMin: 4,
category: 'Engineering',
excerpt:
'companyContext is in the public API schema today but does not yet change the output. Here is what it will do in v1 and why we shipped the field first.',
body: [
'LearningPathInput has an optional companyContext field: a string of up to 500 characters describing the learner\'s team, stack, and context. You can pass it today — the schema accepts it and validates it. It does not yet change the generated path. That is intentional, and this post explains why.',
'We shipped the field before the feature for two reasons. First, it locks in the API shape before any consuming code depends on a version without it. If we added it in v1, every product that had already shipped LearnKit AI would need to handle a schema change. By shipping it in v0, all valid v0 inputs are valid v1 inputs — the only change is that the engine starts doing something with a field it was already accepting.',
'Second, we needed to decide what "using companyContext" actually means before we built it. The obvious design is string interpolation: embed the context into lesson summary templates wherever there is a gap. The better design is semantic: parse the context for signals (team size, stack, industry, pace) and use those signals to weight lesson selection, adjust project scope, and tune the language of summaries.',
'In v1, companyContext will be parsed for four signals: tech stack keywords (to prefer lessons that use tools the team already has), pace signals ("ships weekly" versus "quarterly releases"), industry signals (regulated industries get different compliance framing), and team size signals ("40-person team" versus "solo founder"). The lesson templates will expose hooks for each signal so the interpolation is structured, not arbitrary.',
'If you are building on LearnKit AI today, pass companyContext. It will be silently ignored for now. In v1, it will start doing something useful — and your integration will not need to change.',
],
},
];
22 changes: 22 additions & 0 deletions packages/core/src/__tests__/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,26 @@ describe('generateLearningPath', () => {
}
}
});

it.each([
'Marketer',
'Founder',
'Operations',
'Researcher',
])('generates a valid four-week path for %s', (role) => {
const path = generateLearningPath({ ...SAMPLE, role });
expect(path.weeks).toHaveLength(4);
for (const w of path.weeks) {
expect(w.lessons.length).toBeGreaterThan(0);
}
});

it('generates different paths for different levels', () => {
const roles = ['Marketer', 'Founder', 'Operations', 'Researcher'];
for (const role of roles) {
const beg = generateLearningPath({ ...SAMPLE, role, level: 'beginner' });
const adv = generateLearningPath({ ...SAMPLE, role, level: 'advanced' });
expect(adv.totalMinutes).toBeLessThan(beg.totalMinutes);
}
});
});
Loading
Loading