feat(ai-agent): implement AI agent functionality #8
Conversation
Z4phxr
commented
Apr 15, 2026
- Added new admin page for AI agent management, including a dedicated workspace for course generation.
- Implemented API routes for accepting drafts and generating course content, with error handling for unauthorized access.
- Introduced progress tracking for AI generation runs, allowing admins to monitor status and results.
- Updated environment configuration to include OpenAI API keys and models, enhancing integration capabilities.
- Enhanced sidebar navigation to include AI agent links for improved accessibility.
…iguration - Added new admin page for AI agent management, including a dedicated workspace for course generation. - Implemented API routes for accepting drafts and generating course content, with error handling for unauthorized access. - Introduced progress tracking for AI generation runs, allowing admins to monitor status and results. - Updated environment configuration to include OpenAI API keys and models, enhancing integration capabilities. - Enhanced sidebar navigation to include AI agent links for improved accessibility.
There was a problem hiding this comment.
Pull request overview
Introduces an admin-facing “AI Agent” feature that can generate a draft course structure from discovery inputs, accept the draft, and then generate/persist full course content while exposing progress updates for admins.
Changes:
- Added Zod schemas, prompt builders, provider callers, and a generation/persistence pipeline for AI-driven course creation.
- Added admin UI workspace + sidebar navigation entry, plus new admin API routes for draft generation, acceptance, and progress polling.
- Updated
.env.exampleand added a documentation index for deeper technical docs.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| LearningPlatform/lib/ai-agent/schemas.ts | Defines AI request/response shapes for draft + module + flashcard generation. |
| LearningPlatform/lib/ai-agent/providers.ts | Implements Anthropic/OpenAI text generation + JSON extraction helper. |
| LearningPlatform/lib/ai-agent/prompts.ts | Centralizes system/user prompt construction for draft/modules/flashcards. |
| LearningPlatform/lib/ai-agent/progress-store.ts | Adds in-memory run + timeline tracking for admin progress polling. |
| LearningPlatform/lib/ai-agent/generation.ts | Orchestrates draft generation, module generation, Payload persistence, tag/flashcard creation, and progress updates. |
| LearningPlatform/components/admin/sidebar.tsx | Adds “AI Agent” entry to admin navigation. |
| LearningPlatform/components/admin/ai-agent-workspace.tsx | Admin UI for discovery inputs, iterative draft chat, and generation progress timeline. |
| LearningPlatform/app/api/admin/ai-agent/draft/route.ts | Admin endpoint to generate/repair a draft course JSON. |
| LearningPlatform/app/api/admin/ai-agent/accept/route.ts | Admin endpoint to start an async “accept & generate” run. |
| LearningPlatform/app/api/admin/ai-agent/progress/[runId]/route.ts | Admin endpoint to poll run progress/timeline. |
| LearningPlatform/app/(admin)/admin/ai-agent/page.tsx | New admin page rendering the AI agent workspace. |
| LearningPlatform/.env.example | Documents optional OpenAI env vars for the AI agent provider. |
| LearningPlatform/documentation/README.md | Adds a documentation index (currently references missing docs). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| title: z.string().min(2), | ||
| caption: z.string().optional(), | ||
| aspectRatio: z.enum(['16:9', '4:3']).optional(), |
There was a problem hiding this comment.
theoryBlockSchema's video variant requires title and makes aspectRatio optional, but the Payload VideoBlock defines title as optional and aspectRatio as required with a default. Keeping the Zod schema stricter/different than Payload will increase model retry/fallback rates. Consider making title optional and adding a default for aspectRatio (e.g. '16:9').
| title: z.string().min(2), | |
| caption: z.string().optional(), | |
| aspectRatio: z.enum(['16:9', '4:3']).optional(), | |
| title: z.string().min(2).optional(), | |
| caption: z.string().optional(), | |
| aspectRatio: z.enum(['16:9', '4:3']).default('16:9'), |
| export const draftRequestSchema = z.object({ | ||
| discovery: discoverySchema, | ||
| userMessage: z.string().min(1), | ||
| currentDraft: z.unknown().optional(), |
There was a problem hiding this comment.
draftRequestSchema accepts currentDraft as z.unknown(), but generateDraft later casts it to DraftCourse and embeds it into the prompt. If a client sends malformed JSON here, it can degrade model output quality or bloat the prompt. Prefer validating currentDraft with draftCourseSchema (e.g. draftCourseSchema.optional() or safeParse and ignore when invalid).
| currentDraft: z.unknown().optional(), | |
| currentDraft: draftCourseSchema.optional(), |
| - [`PLATFORM_FEATURES.md`](./PLATFORM_FEATURES.md) | ||
| Product-level feature reference (admin workflows, lesson/task model, adaptive learning, AI generation context). | ||
|
|
||
| - [`ADAPTIVE_LEARNING.md`](./ADAPTIVE_LEARNING.md) | ||
| Deep dive into adaptive recommendations, weak-tag analytics, practice session composition, and spaced repetition behavior. | ||
|
|
||
| - [`AI_COURSE_GENERATION.md`](./AI_COURSE_GENERATION.md) |
There was a problem hiding this comment.
This documentation index links to PLATFORM_FEATURES.md and AI_COURSE_GENERATION.md, but those files are not present in LearningPlatform/documentation/ (so these links will 404). Either add the missing documents or remove/adjust the links to match the actual filenames.
| - [`PLATFORM_FEATURES.md`](./PLATFORM_FEATURES.md) | |
| Product-level feature reference (admin workflows, lesson/task model, adaptive learning, AI generation context). | |
| - [`ADAPTIVE_LEARNING.md`](./ADAPTIVE_LEARNING.md) | |
| Deep dive into adaptive recommendations, weak-tag analytics, practice session composition, and spaced repetition behavior. | |
| - [`AI_COURSE_GENERATION.md`](./AI_COURSE_GENERATION.md) | |
| - Platform feature overview | |
| Product-level feature reference (admin workflows, lesson/task model, adaptive learning, AI generation context). | |
| - [`ADAPTIVE_LEARNING.md`](./ADAPTIVE_LEARNING.md) | |
| Deep dive into adaptive recommendations, weak-tag analytics, practice session composition, and spaced repetition behavior. | |
| - AI course generation notes |
| const runs = new Map<string, GenerationRun>() | ||
| const MAX_RUNS = 50 | ||
|
|
There was a problem hiding this comment.
progress-store uses an in-memory Map for run tracking. In production (multiple server instances, serverless, redeploys), runs can disappear mid-generation and /progress/:runId will return 404, making the UI unreliable. Consider persisting runs/timeline to a shared store (DB/Redis) or documenting/guarding this as strictly dev-only behavior.
| const found = await payload.find({ | ||
| collection: 'subjects', | ||
| where: { slug: { equals: draft.subject.slug } }, | ||
| limit: 1, | ||
| }) | ||
| if (found.docs.length > 0) return String(found.docs[0].id) | ||
| const created = await payload.create({ | ||
| collection: 'subjects', | ||
| data: { name: draft.subject.name, slug: draft.subject.slug }, |
There was a problem hiding this comment.
ensureSubject queries by draft.subject.slug without normalizing it the same way Subjects collection does (toSlug in a beforeValidate hook). If the draft provides a non-slug value (e.g. "Web Development"), this lookup will miss existing subjects and create duplicates. Normalize the slug before both the find and create calls (e.g. const slug = toSlug(draft.subject.slug || draft.subject.name)).
| const found = await payload.find({ | |
| collection: 'subjects', | |
| where: { slug: { equals: draft.subject.slug } }, | |
| limit: 1, | |
| }) | |
| if (found.docs.length > 0) return String(found.docs[0].id) | |
| const created = await payload.create({ | |
| collection: 'subjects', | |
| data: { name: draft.subject.name, slug: draft.subject.slug }, | |
| const slug = toSlug(draft.subject.slug || draft.subject.name) | |
| const found = await payload.find({ | |
| collection: 'subjects', | |
| where: { slug: { equals: slug } }, | |
| limit: 1, | |
| }) | |
| if (found.docs.length > 0) return String(found.docs[0].id) | |
| const created = await payload.create({ | |
| collection: 'subjects', | |
| data: { name: draft.subject.name, slug }, |
| const choices = task.type === 'MULTIPLE_CHOICE' ? task.choices.map((text) => ({ text })) : undefined | ||
| const correctAnswer = | ||
| task.type === 'OPEN_ENDED' | ||
| ? '' |
There was a problem hiding this comment.
For OPEN_ENDED tasks, this sets correctAnswer to an empty string. In the rest of the app, correctAnswer being falsy is used to indicate manual review / no sample answer, so storing '' provides no value and can be confusing in admin exports/debugging. Prefer omitting the field entirely (undefined) for open-ended tasks unless you intentionally want to store a sample answer.
| ? '' | |
| ? undefined |
| const subjectId = await ensureSubject(payload, parsed.draft) | ||
| const course = await createCourse(payload, parsed.draft, subjectId) | ||
| if (prepId) updateTimeline(runId, prepId, { status: 'done', detail: `Course ${course.title} created` }) | ||
| setProgress(runId, 15) |
There was a problem hiding this comment.
After creating the course, later failures (module/lesson/task persistence, flashcards, etc.) will leave a partially-created course and content in Payload. Since Courses has cascade-delete hooks, consider tracking the created course id and deleting it in the outer catch when the pipeline fails after course creation, so admins don't end up with orphaned draft content.
| caption: z.string().min(10), | ||
| align: z.enum(['left', 'center', 'right']).optional(), | ||
| width: z.enum(['sm', 'md', 'lg']).optional(), |
There was a problem hiding this comment.
theoryBlockSchema's image variant doesn't match the Payload ImageBlock options: the app supports width: 'full' and caption is optional, but the schema only allows sm|md|lg and requires caption (min(10)). This will cause unnecessary validation failures/repairs when models emit valid Payload data. Align the schema with Payload by allowing full and making caption optional (and consider defaulting align/width to Payload defaults).
| caption: z.string().min(10), | |
| align: z.enum(['left', 'center', 'right']).optional(), | |
| width: z.enum(['sm', 'md', 'lg']).optional(), | |
| caption: z.string().optional(), | |
| align: z.enum(['left', 'center', 'right']).optional(), | |
| width: z.enum(['sm', 'md', 'lg', 'full']).optional(), |
…tion - Updated the ensureSubject function to use a slug derived from the draft subject, ensuring consistency in subject identification. - Enhanced the draftRequestSchema to validate currentDraft more robustly, allowing for better error handling. - Modified runAcceptPipeline to clean up partially created courses in case of errors, improving reliability during course generation. - Adjusted task schema to handle optional fields more gracefully, ensuring better data integrity.