EdTech Platform

Assessments & Flashcard Editor

Built a full-featured question editor from scratch — empowering teachers with AI capabilities to create, improve, validate, and bulk-manage assessments and flashcard decks at scale.

Vue 3Composition APITypeScriptPiniaAI IntegrationDrag & DropDesign System

THE PROBLEM

Teachers creating quizzes and flashcard decks faced a fragmented experience — the legacy editor was tightly coupled, hard to extend with new question types, and offered no AI assistance. Every question had to be manually written, validated, and formatted.

Adding new question types (comprehension, drag-and-drop, equation) required changes across multiple disconnected files. Inline editing wasn't possible, so even small text fixes required opening a full editor page. The result: low publish rates, high error rates, and frustrated educators.

+1.5pp

Increase in publish rate after launch

15+ Types

Question types supported (MCQ, MSQ, flashcard, comprehension, etc.)

0 → 1

Built from scratch with AI capabilities

THE EDITOR

Main Editor ViewQuestion card with inline editing, AI actions bar, and recommendation sidebar
Question Types Gallery
AI Question Generation

COMPONENT ARCHITECTURE

A layered component tree with a page-level container delegating to a core editor component, which orchestrates headers, question cards, sidebars, and modals.

ActivityEditorRoute

Page-level route container

EditorHeader

Top navigation bar

LoadingState

Skeleton placeholder

SimpleAssessmentEditCore

Core editor — orchestrates everything

FloatingHeader

Sticky header on scroll (Intersection Observer)

StaticHeader

Always visible header + AI quick actions

EmptyState

Zero-questions state

QuestionTypesGallery

Question type picker (gallery grid)

QuestionCard

Per-question wrapper

BaseQuestionCard

Reusable renderer with inline editing

QuestionCardHeader

Number, points, time badges

RecommendationPanelAI

AI-recommended questions sidebar

QuickEditQuestion

Modal for inline flashcard editing

TimeOrPointModal

Bulk/single time & points

AIQuizConvertSliderAI

AI conversion sidebar

STATE MANAGEMENT

Central Pinia store with supporting stores for AI, modals, and teleport overlays. State machine modes govern the editor lifecycle.

Central Editor Store (Pinia)

Single source of truth for quiz data, questions, validation, and edit mode

Editor Modes

IDLEQUIZ_LOADINGQUIZ_LOADEDQUESTION_LOADEDTELEPORTING_QUESTIONTELEPORTING_QUIZ

Key State

  • Quiz / Flashcard deck object
  • Questions list (flat, includes children)
  • Active question being edited
  • Bulk selection IDs
  • Validation errors
  • Validation + pre-save pipeline

Computed

  • Questions indexed by ID
  • Is flashcard activity type
  • Total points sum
  • Current draft version ID
  • Is question valid

Actions

  • Load quiz, set active question
  • Insert / delete / bulk delete questions
  • Inline update (text, options, answer)
  • Toggle edit mode
  • Batch update question records

AI Store

AI recommendation state, premium question tracking

Modal Store

Quick edit modal, confirmation dialog management

Teleport Store

Question import/search overlay state

COMPOSABLE DECOMPOSITION

Business logic decomposed into focused composables, instantiated in the core editor component and distributed to children via Vue's provide/inject pattern.

Provide / Inject Contract

Core editor provides composable instances — child components inject only what they need

questionManagement

QuestionCard, BaseQuestionCard

timePointManagement

QuestionCard

questionUpdates

QuestionCard, BaseQuestionCard

countableQuestions

QuestionCard

questionNumberMap

QuestionCard

katex

Math rendering

isMobile

Layout adaptations

useQuestionManagement

CRUD operations on questions
Comprehension question parent/child handling
Create new question with analytics tracking
Duplicate question via API + insert clone
Open edit mode or quick-edit modal (flashcards)
Delete with comprehension-awareness

useQuestionUpdates

Inline editing of MCQ/MSQ text, options, and answers directly on the card
Runs validation + pre-save pipeline before API call
Determines if inline edit is available (MCQ/MSQ, no math)
Batch update text + options + answer in one operation
Silent save — no loading state, no page navigation

useTimePointManagement

Per-question and bulk time configuration
Per-question and bulk points configuration
Modal-based UI for single or all-question updates
Dispatches to correct update path based on context

useDragAndDrop

SortableJS via @vueuse/integrations
Creates synced sortable reference from parent questions
Normalizes comprehension children order on reorder
Flattens parent+children for backend API call
Manages drag state (isDragging, dragOverIndex)

useProgressiveQuestionLoading

Performance optimization for 50+ question lists
First batch: 20 questions rendered immediately
Subsequent batches: 30 questions at 50ms intervals
Returns computed subset that grows progressively
Only activates above configurable threshold

useAIRecommendations

Sidebar panel with AI-recommended questions
Drag-to-add: user drags from panel into question list
Search and filter recommended questions
API call to add question on drop
Auto-scroll to newly added question

DATA FLOWS

Question Lifecycle

User clicks "Add Question"

Create question

Analytics fired, API call to create question

Store updates

insertQuestionsInQuiz() — reactive state update

Computed reflow

parentQuestions → sortableQuestions → questionsToRender

QuestionCard rendered

Vue re-renders with the new question card

User clicks "Edit" — routing decision

Flashcard + Desktop

Quick-edit modal

Otherwise

Full editor page navigation

Save

Pipeline runs: validation → pre-save → API call → store update

Inline Edit Flow (MCQ/MSQ)

User edits option text on card

Event emitted

@update:option from BaseQuestionCard

Change detection

Check if text actually changed — skip no-ops

Pipeline setup

Initialize validation + pre-save pipeline for question

Silent save

Update store → toggle edit mode → run pipeline → API call (no loading state)

Store reconciled

API response → batch update question records

Drag & Drop Reorder

User grabs drag handle

SortableJS onStart

isDragging = true, UI enters drag mode

User drops at new position

SortableJS reorders the sortable array

Normalize comprehension children

Flatten parent+children order for backend consistency

Persist

reorderQuestions() API call — backend persists new order

AI Recommendation Drag-to-Add

User drags from recommendation panel

Drag over question list

Blue drop zone appears — visual affordance

Drop event

Parse JSON from dataTransfer, extract question data

Add to quiz

API call to add question to quiz

Store update + scroll

Store updated reactively — auto-scroll to newly added question

RESPONSIVE STRATEGY

ViewportQuestions ColumnSidebar
Desktop + Sidebar Open
Columns 1–8 (8 span)Columns 9–12 (4 span, recommendations visible)
Desktop + Sidebar Closed
Columns 3–10 (8 span, centered)Column 12 (1 span, collapsed toggle)
Mobile / Tablet
Columns 1–12 (full width)Hidden

FloatingHeader

Triggered by Intersection Observer. Only mounts the sticky header when the static header scrolls out of viewport — avoids unnecessary DOM and layout calculations.

12-Column Grid

Uses a shared Columns/Column grid system with responsive breakpoints. Headers switch between floating and static variants based on scroll position.

PERFORMANCE OPTIMIZATIONS

TechniqueWhereImpact
Progressive LoadingQuestion listRenders first 20, batches rest at 30 per 50ms — prevents frame drops on 50+ question quizzes
Async ComponentsTime/Point modaldefineAsyncComponent — lazy-loaded modals, zero cost until opened
Throttled DragDrag-over handler100ms throttle on drag position updates — smooth 60fps during reorder
Intersection ObserverFloating headerAvoids rendering sticky header when static header is visible
Debounced HoverAdd-question line250ms delay before showing "add question" between cards — prevents flicker
Conditional MountingRecommendations, nudgesv-if guards on heavy sections — unmounted until needed

FEATURE-FLAGGED ROLLOUT

Editor Revamp

Switches between the new editor (SimpleAssessmentEdit) and legacy editor — gradual rollout with instant rollback

Flashcard Mode

Hides recommendation panel, uses quick-edit modal instead of full-page editor, changes button labels

Question Type Gallery

Shows rich gallery grid for question type picking vs. simple empty state

AI Quick Actions

Enables AI enhancement buttons in headers — generate, improve, validate questions

PACKAGE ARCHITECTURE

The editor spans multiple packages in a monorepo, with clear boundaries between AI features, shared editor logic, domain types, and the design system.

AI Package

Entry point — routes, page components, AI-specific composables

Shared Editor Library

  • Central Pinia store
  • Editor composables & actions
  • Validation pipeline
  • HTTP API services
  • Modal & teleport stores

Editor Shared Components

  • RecommendationPanel (AI)
  • QuestionTypesGallery
  • QuickEditQuestion modal
  • TimeOrPointUpdateModal
  • AI worksheet suggestions

Shared Domains

Question types, schemas, AI interfaces, editor constants

Design System

Reusable UI components, tokens, grid system

SUPPORTED QUESTION TYPES

MCQ

Multiple choice

MSQ

Multi-select

Flashcard

Term & definition

Comprehension

Passage + children

Fill in Blank

Typed answer

Open Ended

Free response

Poll

Survey question

Match

Pair matching

Reorder

Sequence ordering

Draw

Drawing response

Graphing

Graph plotting

DnD Image

Drag & drop on image

Hotspot

Click on image

Equation

Math equation

Classification

Category sorting

KEY DECISIONS

Provide/Inject over Props Drilling

The component tree is 4+ levels deep. Composable instances needed in QuestionCard and BaseQuestionCard without threading through every intermediate component. Vue's provide/inject gives typed, reactive access with zero prop noise.

Composable Decomposition over Monolithic Component

The core editor would be 1000+ lines as a single component. Decomposing into 6 focused composables (question management, updates, time/points, drag-drop, progressive loading, AI recommendations) keeps each unit testable and under 200 lines.

Progressive Loading over Virtualization

Virtual scrolling breaks SortableJS drag-and-drop (items outside viewport unmount). Progressive loading was chosen: render first 20 immediately, batch the rest at 30 per 50ms. Keeps all items in DOM for drag reorder while avoiding initial frame drops.

Feature-Flagged Rollout with Instant Rollback

The editor is mission-critical for content creation. Shipping behind a feature flag allowed gradual rollout (5% → 25% → 100%) with instant rollback to the legacy editor if regressions appeared. No deploy required to revert.

Silent Inline Save over Modal-Based Editing

For MCQ/MSQ questions, opening a full editor page for a typo fix killed the publish flow. Inline editing with silent save (no loading spinner, no navigation) reduced friction and directly contributed to the +1.5pp publish rate lift.

Back to all Projects

Built with Vue 3, Composition API, Pinia, TypeScript, and SortableJS