Tutorial & Onboarding System¶
Local-only tutorial progression system for guiding users through features.
Architecture¶
The tutorial system uses: - localStorage for persistence (no backend) - useSyncExternalStore for React state sync - storage events for cross-tab updates - Toast notifications for step completion
Tutorial Configuration¶
lib/tutorial.ts
Pages¶
export const TUTORIAL_PAGES = {
sessions: "sessions",
context: "context",
training: "training"
} as const;
Steps¶
export const TUTORIAL_STEPS = {
sessions: {
createForm: {
id: "createForm",
title: "Create a Form Template",
description: "Define the schema for logging during sessions"
},
createSession: {
id: "createSession",
title: "Create a Session",
description: "Start a new game or practice session"
},
logEvents: {
id: "logEvents",
title: "Log Events",
description: "Record plays and observations"
},
embedContext: {
id: "embedContext",
title: "Embed Timeline",
description: "Convert session data to AI context"
}
},
context: {
uploadFiles: {
id: "uploadFiles",
title: "Upload Files",
description: "Add documents, videos, and images"
},
searchEmbeddings: {
id: "searchEmbeddings",
title: "Search Embeddings",
description: "Find relevant context"
}
},
training: {
buildContext: {
id: "buildContext",
title: "Build Context",
description: "Accumulate training data"
},
configureJob: {
id: "configureJob",
title: "Configure Training",
description: "Set up training parameters"
},
runTraining: {
id: "runTraining",
title: "Run Training",
description: "Start the training job"
},
deployAdapter: {
id: "deployAdapter",
title: "Deploy Adapter",
description: "Use the trained model"
}
}
};
Completing Steps¶
Helper Functions¶
// lib/tutorial.ts
export function completeTutorialStep({
pageId,
stepId,
tutorialPath,
category,
stepNumber,
stepTitle
}: CompleteTutorialStepParams) {
// Save to localStorage
const key = `${pageId}-tutorial-progress`;
const current = JSON.parse(localStorage.getItem(key) || "{}");
current[stepId] = true;
localStorage.setItem(key, JSON.stringify(current));
// Show toast notification
toast.success(`Step ${stepNumber} Complete!`, {
description: stepTitle,
action: {
label: "View Progress",
onClick: () => window.location.href = tutorialPath
}
});
}
// Convenience wrappers
export function completeSessionsStep(stepId: string) {
const step = TUTORIAL_STEPS.sessions[stepId];
completeTutorialStep({
pageId: TUTORIAL_PAGES.sessions,
stepId,
tutorialPath: "/portal/[slug]/[teamID]/sessions",
category: "Sessions",
stepNumber: getStepNumber(stepId),
stepTitle: step.title
});
}
export function completeContextStep(stepId: string) { /* ... */ }
export function completeTrainingStep(stepId: string) { /* ... */ }
Usage in Components¶
// After creating a session form
const handleSubmit = async (data) => {
await createForm(data);
completeSessionsStep("createForm");
};
// After uploading files
const handleUpload = async (files) => {
await uploadFiles(files);
completeContextStep("uploadFiles");
};
Reading Progress¶
useTutorialProgress Hook¶
hooks/use-tutorial-progress.ts
export function useTutorialProgress(pageId: string, stepIds: string[]) {
const getSnapshot = () => {
const stored = localStorage.getItem(`${pageId}-tutorial-progress`);
return stored || "{}";
};
const subscribe = (callback: () => void) => {
const handler = (e: StorageEvent) => {
if (e.key === `${pageId}-tutorial-progress`) {
callback();
}
};
window.addEventListener("storage", handler);
return () => window.removeEventListener("storage", handler);
};
const progressString = useSyncExternalStore(
subscribe,
getSnapshot,
() => "{}" // Server snapshot
);
const progress = JSON.parse(progressString);
const completeStep = (stepId: string) => {
const current = JSON.parse(getSnapshot());
current[stepId] = true;
localStorage.setItem(`${pageId}-tutorial-progress`, JSON.stringify(current));
// Trigger re-render
window.dispatchEvent(new StorageEvent("storage", {
key: `${pageId}-tutorial-progress`
}));
};
const resetProgress = () => {
localStorage.removeItem(`${pageId}-tutorial-progress`);
window.dispatchEvent(new StorageEvent("storage", {
key: `${pageId}-tutorial-progress`
}));
};
const isStepCompleted = (stepId: string) => Boolean(progress[stepId]);
return { progress, completeStep, resetProgress, isStepCompleted };
}
Usage in Pages¶
// Sessions tutorial page
function SessionsTutorialPage() {
const stepIds = ["createForm", "createSession", "logEvents", "embedContext"];
const { progress, isStepCompleted } = useTutorialProgress("sessions", stepIds);
const completedCount = stepIds.filter(isStepCompleted).length;
const totalSteps = stepIds.length;
return (
<div>
<TutorialProgressHeader
completed={completedCount}
total={totalSteps}
/>
<TutorialCardsSection
steps={stepIds.map(id => ({
...TUTORIAL_STEPS.sessions[id],
isCompleted: isStepCompleted(id)
}))}
/>
</div>
);
}
Tutorial UI Components¶
TutorialProgressHeader¶
components/org/tutorial-progress-header.tsx
Progress bar showing completion status:
<TutorialProgressHeader completed={2} total={4} />
// Renders: "2 of 4 steps completed" with progress bar
TutorialCardsSection¶
components/org/tutorial-cards-section.tsx
Grid of tutorial step cards:
<TutorialCardsSection steps={[
{
id: "createForm",
title: "Create a Form Template",
description: "Define logging schema",
icon: <FileIcon />,
estimatedTime: "5 min",
isCompleted: true,
actionLabel: "Create Form",
actionHref: "/sessions/forms/new"
},
// ...
]} />
TutorialCard¶
components/cards/tutorial-card.tsx
Individual step card:
<TutorialCard
icon={<UploadIcon />}
title="Upload Files"
description="Add documents, videos, and images to build context"
estimatedTime="2 min"
isCompleted={progress.uploadFiles}
actionLabel="Upload"
onAction={() => router.push("/context/upload")}
/>
Tutorial Pages¶
Each feature area has a tutorial overview page:
Sessions Tutorial¶
/portal/[slug]/[teamID]/sessions
Steps: 1. Create Form Template 2. Create Session 3. Log Events 4. Embed Timeline
Context Tutorial¶
/portal/[slug]/[teamID]/context
Steps: 1. Upload Files 2. Search Embeddings
Training Tutorial¶
/portal/[slug]/[teamID]/training
Steps: 1. Build Context 2. Configure Training 3. Run Training 4. Deploy Adapter
Storage Schema¶
localStorage:
├── sessions-tutorial-progress: {
│ "createForm": true,
│ "createSession": true,
│ "logEvents": false,
│ "embedContext": false
│ }
├── context-tutorial-progress: {
│ "uploadFiles": true,
│ "searchEmbeddings": false
│ }
└── training-tutorial-progress: {
"buildContext": false,
"configureJob": false,
"runTraining": false,
"deployAdapter": false
}
Cross-Tab Sync¶
When a step is completed in one tab, other tabs update automatically: