Korai Docs
Quiz

Quiz Store

Zustand store managing quiz feature state

Quiz Store

The quiz store is a Zustand store that manages all state for the Quiz Generator feature. It handles video URLs, video metadata, quiz questions, user answers, AI model selection, UI states, and usage tracking.

Store Implementation

import { create } from 'zustand';

export interface QuizQuestion {
  id: string;
  question: string;
  options: string[];
  correctAnswer: number;
  explanation?: string;
}

export interface UserAnswer {
  questionId: string;
  selectedOption: number;
}

export interface VideoDetails {
  id: string;
  title: string;
  description: string;
  thumbnails: {
    maxres?: { url: string };
    high?: { url: string };
    medium?: { url: string };
  };
  channelTitle: string;
  publishedAt: string;
  duration: number;
  viewCount: number;
  likeCount: number;
}

export interface UsageInfo {
  limit: number;
  remaining: number;
  reset: number;
  used: number;
}

interface QuizState {
  videoUrl: string;
  videoDetails: VideoDetails | null;
  quiz: QuizQuestion[];
  userAnswers: UserAnswer[];
  selectedLLM: string;
  numQuestions: string;
  showFullDescription: boolean;
  isLoading: boolean;
  isGenerating: boolean;
  isSubmitted: boolean;
  isSuccess: boolean;
  score: number;
  hasCopied: boolean;
  usage: UsageInfo | null;
}

interface QuizActions {
  setVideoUrl: (url: string) => void;
  setVideoDetails: (details: VideoDetails | null) => void;
  setQuiz: (quiz: QuizQuestion[]) => void;
  setUserAnswers: (answers: UserAnswer[]) => void;
  setSelectedLLM: (model: string) => void;
  setNumQuestions: (num: string) => void;
  setShowFullDescription: (show: boolean) => void;
  setIsLoading: (loading: boolean) => void;
  setIsGenerating: (generating: boolean) => void;
  setIsSubmitted: (submitted: boolean) => void;
  setIsSuccess: (success: boolean) => void;
  setScore: (score: number) => void;
  setHasCopied: (copied: boolean) => void;
  setUsage: (usage: UsageInfo | null) => void;
  updateUserAnswer: (questionId: string, selectedOption: number) => void;
  resetQuiz: () => void;
  reset: () => void;
}

const initialState: QuizState = {
  videoUrl: '',
  videoDetails: null,
  quiz: [],
  userAnswers: [],
  selectedLLM: 'gemini-2.5-flash',
  numQuestions: '5',
  showFullDescription: false,
  isLoading: false,
  isGenerating: false,
  isSubmitted: false,
  isSuccess: false,
  score: 0,
  hasCopied: false,
  usage: null
};

export const useQuizStore = create<QuizState & QuizActions>((set) => ({
  ...initialState,
  setVideoUrl: (url) => set({ videoUrl: url }),
  setVideoDetails: (details) => set({ videoDetails: details }),
  setQuiz: (quiz) => set({ quiz }),
  setUserAnswers: (answers) => set({ userAnswers: answers }),
  setSelectedLLM: (model) => set({ selectedLLM: model }),
  setNumQuestions: (num) => set({ numQuestions: num }),
  setShowFullDescription: (show) => set({ showFullDescription: show }),
  setIsLoading: (loading) => set({ isLoading: loading }),
  setIsGenerating: (generating) => set({ isGenerating: generating }),
  setIsSubmitted: (submitted) => set({ isSubmitted: submitted }),
  setIsSuccess: (success) => set({ isSuccess: success }),
  setScore: (score) => set({ score }),
  setHasCopied: (copied) => set({ hasCopied: copied }),
  setUsage: (usage) => set({ usage }),
  updateUserAnswer: (questionId, selectedOption) =>
    set((state) => ({
      userAnswers: state.userAnswers.map((answer) =>
        answer.questionId === questionId
          ? { ...answer, selectedOption }
          : answer
      )
    })),
  resetQuiz: () =>
    set({
      userAnswers: initialState.userAnswers,
      isSubmitted: false,
      score: 0
    }),
  reset: () => set(initialState)
}));

State Properties

videoUrl: string

Stores the YouTube video URL entered by the user.

Initial Value: ''

Usage: Updated from input field, used for API requests

videoDetails: VideoDetails | null

Contains video metadata including ID, title, description, thumbnails, channel info, statistics, and publish date.

Initial Value: null

Set After: Successful fetch from /api/videoDetail

quiz: QuizQuestion[]

Array of generated quiz questions. Each question has an ID, question text, 4 options, correct answer index, and optional explanation.

Initial Value: []

Populated From: /api/quiz endpoint response after AI generation

Example Data:

[
  {
    id: "q1",
    question: "What is the main topic of the video?",
    options: [
      "Programming basics",
      "Machine learning",
      "Web development",
      "Data science"
    ],
    correctAnswer: 2,
    explanation: "The video focuses on modern web development techniques."
  }
]

userAnswers: UserAnswer[]

Array tracking user's selected answer for each question. Initialized with -1 for unanswered questions.

Initial Value: []

Structure: One entry per quiz question

Example Data:

[
  { questionId: "q1", selectedOption: 2 },
  { questionId: "q2", selectedOption: -1 },  // Not answered
  { questionId: "q3", selectedOption: 0 }
]

selectedLLM: string

AI model identifier for quiz generation.

Initial Value: 'gemini-2.5-flash'

Options: Currently supports Gemini 2.5 Flash model

numQuestions: string

Number of quiz questions to generate. Stored as string for select component compatibility.

Initial Value: '5'

Valid Range: '2' to '10'

showFullDescription: boolean

Toggle state for expanding/collapsing video description.

Initial Value: false

Usage: Controls line-clamp CSS class on description

isLoading: boolean

Loading state during video and transcript fetching.

Initial Value: false

Set to true: When fetching video details and transcript

Set to false: When fetch completes or errors

isGenerating: boolean

Loading state during AI quiz generation.

Initial Value: false

Set to true: When generating quiz from transcript

Set to false: When generation completes or errors

Usage: Shows quiz skeleton and updates button label to "Generating..."

isSubmitted: boolean

Whether the quiz has been submitted by the user.

Initial Value: false

Set to true: When user clicks "Submit Quiz" button

Effect:

  • Locks answer selection
  • Shows score badge
  • Displays color-coded correct/incorrect indicators
  • Shows explanations
  • Enables retry button

isSuccess: boolean

Success state for UI feedback animation.

Initial Value: false

Flow:

  1. Set to true after successful quiz generation
  2. Auto-reset to false after 4 seconds

score: number

Number of correct answers after quiz submission.

Initial Value: 0

Calculated: By comparing userAnswers with quiz[].correctAnswer

Displayed: As badge "Score: X/Y" in quiz header

hasCopied: boolean

Temporary state for copy button feedback.

Initial Value: false

Flow:

  1. Set to true when copy succeeds
  2. Changes copy icon to checkmark
  3. Auto-reset to false after 2 seconds

usage: UsageInfo | null

API usage tracking information.

Initial Value: null

Structure:

{
  limit: 100,      // Total allowed requests
  remaining: 75,   // Requests remaining
  reset: 1696636800000,  // Unix timestamp
  used: 25         // Requests used
}

Actions

setVideoUrl(url: string)

Updates the video URL from user input.

Called From: Input field onChange handler

setVideoDetails(details: VideoDetails | null)

Stores video metadata after API fetch.

Called From: useFetchVideoAndTranscript hook

setQuiz(quiz: QuizQuestion[])

Stores generated quiz questions.

Called From: useGenerateQuiz hook after parsing AI response

setUserAnswers(answers: UserAnswer[])

Sets the complete user answers array. Used for initialization and reset.

Called From:

  • useGenerateQuiz - Initialize empty answers
  • useQuizActions.handleRetry - Reset to empty answers

setSelectedLLM(model: string)

Updates the AI model selection.

Called From: LLM selector dropdown (if implemented)

setNumQuestions(num: string)

Updates the number of questions to generate.

Called From: Select component onValueChange

setShowFullDescription(show: boolean)

Toggles video description expansion.

Called From: Show more/less button click handler

setIsLoading(loading: boolean)

Controls loading state for video/transcript fetch.

Called From: useFetchVideoAndTranscript hook

setIsGenerating(generating: boolean)

Controls loading state for quiz generation.

Called From: useGenerateQuiz hook

setIsSubmitted(submitted: boolean)

Marks quiz as submitted.

Called From:

  • useQuizActions.handleQuizSubmit - Set to true
  • useQuizActions.handleRetry - Set to false

setIsSuccess(success: boolean)

Controls success feedback animation.

Called From: useFetchVideoAndTranscript hook

setScore(score: number)

Updates the quiz score.

Called From:

  • useQuizActions.handleQuizSubmit - Calculate and set score
  • useQuizActions.handleRetry - Reset to 0

setHasCopied(copied: boolean)

Controls copy button feedback.

Called From: useQuizActions.handleCopy

setUsage(usage: UsageInfo | null)

Updates API usage information.

Called From: useFetchUsage hook

updateUserAnswer(questionId: string, selectedOption: number)

Updates a single user answer while preserving others.

Implementation:

updateUserAnswer: (questionId, selectedOption) =>
  set((state) => ({
    userAnswers: state.userAnswers.map((answer) =>
      answer.questionId === questionId
        ? { ...answer, selectedOption }
        : answer
    )
  }))

Called From: useQuizActions.handleAnswerSelect when user clicks an option

Process: Maps through answers array, updates matching question, returns new array

resetQuiz()

Resets quiz-specific state for retrying without clearing video details.

Resets:

  • userAnswers to initial empty array
  • isSubmitted to false
  • score to 0

Preserves:

  • Video details
  • Quiz questions
  • Video URL

Called From: useQuizActions.handleRetry

reset()

Resets entire store to initial state.

Called From: Navigation away from feature or complete reset

Store Patterns

Computed Values

The store doesn't include computed values. These are calculated in components:

Quiz Completion Check:

const allAnswered = userAnswers.every((answer) => answer.selectedOption !== -1);

Individual Answer Correctness:

const isCorrect = userAnswers.find(
  (answer) => answer.questionId === question.id
)?.selectedOption === question.correctAnswer;

State Organization

The store is organized into logical sections:

  1. Input State: videoUrl, numQuestions, selectedLLM
  2. Data State: videoDetails, quiz, userAnswers, usage
  3. UI State: showFullDescription, hasCopied
  4. Loading State: isLoading, isGenerating, isSubmitted, isSuccess
  5. Result State: score

This separation makes it easy to understand what each part of the state represents and when it should be updated.