Skip to content

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:

// Tab 1: Completes a step
completeSessionsStep("createForm");
// Triggers: window.dispatchEvent(new StorageEvent(...))

// Tab 2: Listening via useSyncExternalStore
// Receives storage event and re-renders with updated progress