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.
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
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.
Page-level route container
Top navigation bar
Skeleton placeholder
Core editor — orchestrates everything
Sticky header on scroll (Intersection Observer)
Always visible header + AI quick actions
Zero-questions state
Question type picker (gallery grid)
Per-question wrapper
Reusable renderer with inline editing
Number, points, time badges
AI-recommended questions sidebar
Modal for inline flashcard editing
Bulk/single time & points
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
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
useQuestionUpdates
useTimePointManagement
useDragAndDrop
useProgressiveQuestionLoading
useAIRecommendations
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
| Viewport | Questions Column | Sidebar |
|---|---|---|
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
| Technique | Where | Impact |
|---|---|---|
| Progressive Loading | Question list | Renders first 20, batches rest at 30 per 50ms — prevents frame drops on 50+ question quizzes |
| Async Components | Time/Point modal | defineAsyncComponent — lazy-loaded modals, zero cost until opened |
| Throttled Drag | Drag-over handler | 100ms throttle on drag position updates — smooth 60fps during reorder |
| Intersection Observer | Floating header | Avoids rendering sticky header when static header is visible |
| Debounced Hover | Add-question line | 250ms delay before showing "add question" between cards — prevents flicker |
| Conditional Mounting | Recommendations, nudges | v-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.
Built with Vue 3, Composition API, Pinia, TypeScript, and SortableJS