A self-paced onboarding track that introduces a new employee to every KB theme in breadth (not depth), so they grasp how Respellion works day to day and week to week. Offered as a CTA inside the Dashboard "New here?" explainer card; always available regardless of enrollment. Design: - Theme is the trackable unit; the 5 "days" are a read-time presentation grouping, so re-chunking never loses progress. Completion is stored per theme in onboarding_completions. - Per-theme overview generated lazily on first open (fast-tier emit_onboarding_overview tool), cached in onboarding_overviews keyed by theme + a topics_fingerprint that triggers regeneration when the theme's topic set changes. - Reachable via /onboarding-track using the existing skipEnrollmentGate prop, decoupled from the 26-week curriculum (distinct from /onboarding, the enrollment page). Backend: - pb_migrations/1781200000_created_onboarding.js: two collections with authenticated-only rules and unique indexes; TEXT team_member_id (no relation) per the post-#18/#27 convention. Mirrored in scripts/setup-pb-collections.mjs. - src/lib/onboardingService.js: pure helpers (orderThemes, distributeThemesIntoDays, computeTopicsFingerprint, computeOnboardingProgress, buildOnboardingPlan) + generation + I/O. - db.js onboarding helpers use pb.filter() bindings (theme is free text). - LLM tool + Zod schema + registry + simulation stub. Frontend: - src/pages/OnboardingTrack.jsx (day list, per-theme overview, completion banner, progress ring/day bar). - Dashboard "New here?" card CTA + X/5-days progress chip (hidden when the KB has no themes). Docs: data-model, generation-spec (§D), frontend-spec updated. Verified: 22 new unit tests (npm test 134/134), eslint clean on changed files, npm run build OK, PocketBase v0.30.4 boot applies the migration (collections + unique indexes + authed rules confirmed), and a backend contract check (upsert idempotency, unique-index guard, special-char theme filtering). Closes #30 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
267 lines
7.7 KiB
JavaScript
267 lines
7.7 KiB
JavaScript
/**
|
|
* Zod schemas for every structured LLM output the platform consumes.
|
|
*
|
|
* Field names mirror what callers already produce — do not rename them
|
|
* without migrating the corresponding service module.
|
|
*/
|
|
|
|
import { z } from 'zod';
|
|
|
|
const topicTypeEnum = z.enum(['concept', 'role', 'process']);
|
|
const relationTypeStrict = z.enum(['related_to', 'depends_on', 'part_of', 'executed_by']);
|
|
const learningRelevanceEnum = z.enum(['core', 'standard', 'peripheral', 'exclude']);
|
|
|
|
const extractionTopicSchema = z.object({
|
|
id: z.string().min(1),
|
|
label: z.string().min(1),
|
|
type: topicTypeEnum,
|
|
description: z.string().min(1),
|
|
learning_relevance: learningRelevanceEnum,
|
|
});
|
|
|
|
const extractionRelationSchema = z.object({
|
|
source: z.string().min(1),
|
|
target: z.string().min(1),
|
|
type: relationTypeStrict,
|
|
});
|
|
|
|
export const extractionResultSchema = z.object({
|
|
topics: z.array(extractionTopicSchema),
|
|
relations: z.array(extractionRelationSchema),
|
|
});
|
|
|
|
const curriculumWeekSchema = z.object({
|
|
week_number: z.number().int().min(1).max(26),
|
|
theme: z.string().min(1),
|
|
topic_ids: z.array(z.string().min(1)).min(1),
|
|
estimated_duration: z.number().int().min(15).max(45),
|
|
week_rationale: z.string().min(1),
|
|
});
|
|
|
|
export const curriculumScheduleSchema = z.object({
|
|
weeks: z.array(curriculumWeekSchema).length(26),
|
|
});
|
|
|
|
const topicEnrichmentSchemaDef = z.object({
|
|
id: z.string().min(1),
|
|
theme: z.string().min(1),
|
|
complexity_weight: z.number().int().min(1).max(5),
|
|
difficulty: z.enum(['introductory', 'intermediate', 'advanced']),
|
|
});
|
|
|
|
export const topicEnrichmentSchema = z.object({
|
|
topics: z.array(topicEnrichmentSchemaDef).min(1),
|
|
});
|
|
|
|
|
|
const articleSectionSchema = z.object({
|
|
heading: z.string().min(1),
|
|
body: z.string().min(1),
|
|
});
|
|
|
|
const articleBodySchema = z.object({
|
|
title: z.string().min(1),
|
|
intro: z.string().min(1),
|
|
sections: z.array(articleSectionSchema).min(1),
|
|
keyTakeaways: z.array(z.string().min(1)).min(1),
|
|
});
|
|
|
|
export const learningArticleSchema = z.object({
|
|
article: articleBodySchema,
|
|
});
|
|
|
|
const slideSchema = z.object({
|
|
title: z.string().min(1),
|
|
bullets: z.array(z.string().min(1)).min(1),
|
|
speakerNote: z.string().min(1),
|
|
});
|
|
|
|
export const learningSlidesSchema = z.object({
|
|
slides: z.array(slideSchema).min(1),
|
|
});
|
|
|
|
const infographicStatSchema = z.object({
|
|
value: z.string().min(1),
|
|
label: z.string().min(1),
|
|
icon: z.string().min(1),
|
|
});
|
|
|
|
const infographicStepSchema = z.object({
|
|
number: z.number().int().min(1),
|
|
title: z.string().min(1),
|
|
description: z.string().min(1),
|
|
icon: z.string().min(1),
|
|
});
|
|
|
|
const infographicBodySchema = z.object({
|
|
headline: z.string().min(1),
|
|
tagline: z.string().min(1),
|
|
stats: z.array(infographicStatSchema).min(1),
|
|
steps: z.array(infographicStepSchema).min(1),
|
|
quote: z.string().min(1),
|
|
colorTheme: z.string().min(1),
|
|
});
|
|
|
|
export const learningInfographicSchema = z.object({
|
|
infographic: infographicBodySchema,
|
|
});
|
|
|
|
export const learningAllSchema = z.object({
|
|
article: articleBodySchema,
|
|
slides: z.array(slideSchema).min(1),
|
|
infographic: infographicBodySchema,
|
|
});
|
|
|
|
const quizDifficultyEnum = z.enum(['easy', 'medium', 'hard']).catch('medium');
|
|
|
|
const quizQuestionSchema = z.object({
|
|
id: z.string().min(1).catch(`gen-${Math.random().toString(36).slice(2, 8)}`),
|
|
question: z.string().min(1),
|
|
topicLabel: z.string().min(1).catch('General'),
|
|
options: z.array(z.string().min(1)).length(4),
|
|
correctIndex: z.preprocess(
|
|
(v) => (typeof v === 'number' ? Math.round(v) : v),
|
|
z.number().int().min(0).max(3),
|
|
),
|
|
explanation: z.string().min(1).catch('See the correct answer above.'),
|
|
difficulty: quizDifficultyEnum,
|
|
});
|
|
|
|
export const quizQuestionsSchema = z.object({
|
|
questions: z.preprocess(
|
|
(v) => {
|
|
// If the model returned an array directly, wrap it
|
|
if (Array.isArray(v)) return v;
|
|
// If it returned a single object, wrap in array
|
|
if (v && typeof v === 'object' && !Array.isArray(v)) return [v];
|
|
return v;
|
|
},
|
|
z.array(quizQuestionSchema).min(1),
|
|
),
|
|
});
|
|
|
|
export const customTopicSchema = z.object({
|
|
label: z.string().min(1),
|
|
type: topicTypeEnum,
|
|
description: z.string().min(1),
|
|
});
|
|
|
|
const mergeActionSchema = z.object({
|
|
keepId: z.string().min(1),
|
|
deleteId: z.string().min(1),
|
|
});
|
|
|
|
const newRelationSchema = z.object({
|
|
source: z.string().min(1),
|
|
target: z.string().min(1),
|
|
type: relationTypeStrict,
|
|
});
|
|
|
|
const relevanceUpdateSchema = z.object({
|
|
id: z.string().min(1),
|
|
learning_relevance: learningRelevanceEnum,
|
|
});
|
|
|
|
export const graphActionsSchema = z.object({
|
|
merges: z.array(mergeActionSchema).optional().default([]),
|
|
deletions: z.array(z.string().min(1)).optional().default([]),
|
|
newRelations: z.array(newRelationSchema).optional().default([]),
|
|
relevanceUpdates: z.array(relevanceUpdateSchema).optional().default([]),
|
|
});
|
|
|
|
const deltaTopicSchema = z.object({
|
|
id: z.string().min(1),
|
|
label: z.string().min(1),
|
|
type: topicTypeEnum,
|
|
description: z.string().min(1),
|
|
});
|
|
|
|
const deltaRelationSchema = z.object({
|
|
source: z.string().min(1),
|
|
target: z.string().min(1),
|
|
type: relationTypeStrict,
|
|
});
|
|
|
|
export const proposeGraphDeltaSchema = z.object({
|
|
reason: z.string().min(1),
|
|
topics: z.array(deltaTopicSchema).max(3).optional(),
|
|
relations: z.array(deltaRelationSchema).max(5).optional(),
|
|
});
|
|
|
|
// ── Article patch operation schemas (Phase 2.4) ──────────────────────────────
|
|
|
|
export const setIntroPatchSchema = z.object({
|
|
intro: z.string().min(1),
|
|
});
|
|
|
|
export const setSectionPatchSchema = z.object({
|
|
heading: z.string().min(1),
|
|
body: z.string().min(1),
|
|
});
|
|
|
|
export const addSectionPatchSchema = z.object({
|
|
heading: z.string().min(1),
|
|
body: z.string().min(1),
|
|
position: z.enum(['start', 'end']),
|
|
});
|
|
|
|
export const removeSectionPatchSchema = z.object({
|
|
heading: z.string().min(1),
|
|
});
|
|
|
|
export const replaceTakeawaysPatchSchema = z.object({
|
|
items: z.array(z.string().min(1)).min(1),
|
|
});
|
|
|
|
// ── Theme session schema ─────────────────────────────────────────────────────
|
|
|
|
const themeSessionTopicSection = z.object({
|
|
topic_id: z.string().min(1),
|
|
label: z.string().min(1),
|
|
summary: z.string().min(1),
|
|
});
|
|
|
|
export const themeSessionSchema = z.object({
|
|
title: z.string().min(1),
|
|
intro: z.string().min(1),
|
|
topicSections: z.array(themeSessionTopicSection).min(1),
|
|
connections: z.string().min(1),
|
|
keyTakeaways: z.array(z.string().min(1)).min(3),
|
|
});
|
|
|
|
export const onboardingOverviewSchema = z.object({
|
|
title: z.string().min(1),
|
|
what_it_is: z.string().min(1),
|
|
why_it_matters: z.string().min(1),
|
|
key_points: z.array(z.string().min(1)).min(3).max(5),
|
|
topics_covered: z
|
|
.array(z.object({ topic_id: z.string().min(1), label: z.string().min(1) }))
|
|
.min(1),
|
|
});
|
|
|
|
/**
|
|
* Registry mapping known tool names to their input schemas. `callLLM`
|
|
* consults this when the caller does not pass an explicit `toolSchemas`
|
|
* override.
|
|
*/
|
|
export const toolSchemaRegistry = {
|
|
emit_knowledge_graph: extractionResultSchema,
|
|
emit_curriculum_schedule: curriculumScheduleSchema,
|
|
emit_topic_enrichment: topicEnrichmentSchema,
|
|
emit_learning_article: learningArticleSchema,
|
|
emit_learning_slides: learningSlidesSchema,
|
|
emit_learning_infographic: learningInfographicSchema,
|
|
emit_learning_all: learningAllSchema,
|
|
emit_quiz_questions: quizQuestionsSchema,
|
|
emit_custom_topic: customTopicSchema,
|
|
emit_graph_actions: graphActionsSchema,
|
|
propose_graph_delta: proposeGraphDeltaSchema,
|
|
set_intro: setIntroPatchSchema,
|
|
set_section: setSectionPatchSchema,
|
|
add_section: addSectionPatchSchema,
|
|
remove_section: removeSectionPatchSchema,
|
|
replace_takeaways: replaceTakeawaysPatchSchema,
|
|
emit_theme_session: themeSessionSchema,
|
|
emit_onboarding_overview: onboardingOverviewSchema,
|
|
};
|