StudyBoost Docs

Feature: AI Learning Tools — Centralized Architecture & Reusable Component System

Metadata

  • Issue ID: FEAT-61
  • Status: Done
  • Owner: danrave1234
  • Related PRs: 61-ai-learning-tools-reusable-component-system -> dev (StudyBoostIO/StudyboostV2#61)

Overview

FEAT-61 completes the AI architecture in three layers:

  1. Backend centralization — a typed, extensible AI tool registry so adding a new AI tool requires no changes to the controller or processor.
  2. Frontend primitivesDocumentPicker, AiPageShell, and the useAiTool hook replace the former AiToolWorkspace monolith.
  3. Gamification foundationTriviaGameTool as the first game-type tool, establishing the pattern for the later game tools (typing, crossword, letter tile, hangman).

It builds on FEAT-60 (which delivered Flashcards, Summary, GeminiGenerationProvider, the AI job queue, and the plan-access guard). FEAT-61 adds the Q&A Tool, the Study Guide Tool, the IAiTool interface + BaseAiTool base class, AiToolRegistryService, the dynamic POST /ai/tools/:toolId controller, the standalone GeminiModule, the frontend primitives, and TriviaGameTool.

Canonical feature doc: docs/features/FEAT-61-AI-Learning-Tools-Reusable-Component-System.md.


Frontend Behavior

Primitive components

ComponentLocationPurpose
AiPageShellcomponents/features/ai/ai-page-shell.tsxPage header wrapper (label, title, description, badges)
DocumentPickercomponents/features/ai/document-picker.tsxUpload + owned-doc picker; ingests docs; fires onDocumentsIngested(ids[])
useAiTool<T>hooks/use-ai-tool.tsGeneric hook: run(body) -> POST /ai/tools/:toolId; exposes isLoading, error, result, reset
AiToolBasecomponents/features/ai/ai-tool-base.tsxLoading / error / rerun / token-usage wrapper

UI states and interactions

  • Loading — while an AI request is in-flight, AiToolBase shows a spinner and disables the trigger.
  • Error — failed requests render a retryable error state with a "Try again" affordance.
  • Document ingestDocumentPicker uploads files via POST /documents/upload, then fires onDocumentsIngested(ids[]); pages can auto-trigger their tool once docs are ingested.
  • Result rendering — each tool page composes a tool-specific result component (SummaryComponent, FlashcardsComponent, QnAComponent, StudyGuideComponent).

Validation rules

  • Q&A pages require a non-empty user_query before the request fires.
  • Tool pages that require a scope (Q&A, study guide) do not trigger until at least one document is ingested.

AI tool pages

RouteTool calledNotes
/ai/document-analyzerPOST /ai/tools/summaryAuto-triggers after doc ingest
/ai/flashcard-builderPOST /ai/tools/flashcardsAuto-triggers after doc ingest
/ai/ask-documentsPOST /ai/tools/qaShows query input after ingest; fires on submit
/ai/quiz-generatorPOST /ai/tools/study-guideAuto-triggers after doc ingest
/ai/trivia-gamePOST /ai/tools/trivia-gameFull game loop: generate -> play -> submit -> scored results

AiToolWorkspace and AiToolTrigger were removed — superseded by the reusable primitives.


Backend Behavior

Tool registry pattern

All AI tools implement IAiTool and extend BaseAiTool. BaseAiTool.run() calls the tool's execute() then deducts one AI credit unless DEV_BYPASS_SUBSCRIPTION=true. Tools are registered in AiModule as AI_TOOL_REGISTRY multi-providers; AiToolRegistryService builds a Map<toolId, IAiTool> injected into AiToolsController and AiProcessingProcessor (no switch statement).

GeminiModule is a standalone module exporting GeminiGenerationProvider, imported by both AiModule and DocumentsModule to avoid circular dependencies. DocumentsService uses GeminiGenerationProvider instead of an inline fetch to Gemini.

Tool DTO formats

ToolRequired body fields
summary{ documentId } OR { scope: { document_ids?, course_id? } }
flashcards{ documentId, count? } OR { scope }
qa{ scope: { document_ids? }, user_query }
study-guide{ scope: { document_ids?, course_id? } }
trivia-game{ documentId, questionCount?, difficulty?, focusTopic? } OR { scope }

Endpoint map

MethodPathAuthDescription
POST/ai/tools/:toolIdAuth + PlanRun any registered AI tool
GET/ai/tools/summary/document/:documentIdAuth + PlanList summaries for a document
GET/ai/tools/flashcards/document/:documentIdAuth + PlanList flashcard sets
GET/ai/tools/flashcards/sets/:setIdAuth + PlanGet a specific flashcard set
GET/ai/tools/study-guide/document/:documentIdAuth + PlanList study guides
GET/ai/tools/study-guide/sets/:guideIdAuth + PlanGet a specific study guide
POST/ai/tools/trivia-game/:sessionId/submitAuth + PlanSubmit answers; returns scored result
GET/ai/tools/trivia-game/:sessionIdAuth + PlanRetrieve a session (questions without answers)

All AI routes carry AiThrottlerGuard at the controller level (20 requests / hour per user bucket) plus cookie auth and plan access.

Business logic and failure modes

  • AiScopeResolverService enforces document/course ownership; users can only resolve chunks from material they own.
  • TriviaGameTool stores generated questions in ai_game_sessions.questions (JSONB) with correctIndex + explanation; the API response strips both, sending only question + options[]. submitAttempt() scores client answers server-side and reveals explanations.
  • Q&A is stateless — no persistence table. Semantic retrieval uses resolveWithEmbeddings() (cosine-similarity ranking, positional fallback).
  • Empty document or empty course scope -> tool returns 422 Unprocessable Entity.
  • Malformed LLM JSON -> tool throws 500 with a safe error message.

Database tables

TableMigrationPurpose
ai_summaries202505030300_add_ai_tools_foundationSummary results
ai_flashcard_sets / ai_flashcards202505030300_add_ai_tools_foundationFlashcard sets and rows
ai_study_guides / ai_study_guide_sections202605050234_add_ai_study_guidesStudy guide metadata + sections
ai_game_sessions / ai_game_attempts202605050327_add_ai_game_tablesTrivia game sessions + attempts

Migrations are hand-written SQL applied via pnpm run migrate:prod.


QA Test Scenarios

Scenario IDDescriptionStepsInputExpected Result
FEAT-61-01Happy path: Q&A with valid scopeOpen /ai/ask-documents, ingest owned doc, submit a questionValid scope.document_ids, user_queryReturns { answer, sources[] } with matching snippets
FEAT-61-02Happy path: generate study guideIngest single owned document, trigger study guideValid documentIdReturns structured guide with >=1 section
FEAT-61-03Happy path: generate trivia gameOpen /ai/trivia-game, pick owned doc, generateValid documentIdSession returned with N questions; correctIndex/explanation absent
FEAT-61-04Validation: Q&A without queryPOST /ai/tools/qa with scope but no user_queryMissing user_query400 Bad Request; no LLM call
FEAT-61-05Validation: trivia submit with wrong answer countSubmit fewer/more answers than questionsMismatched answers[] length400 Bad Request; no attempt row created
FEAT-61-06Backend failure: LLM returns malformed JSONTrigger a tool when Gemini returns non-JSONMalformed LLM response500 with safe error message; no partial persistence
FEAT-61-07Authorization: access a document not ownedPOST any tool with a foreign documentIdForeign documentId403 Forbidden from AiScopeResolverService
FEAT-61-08Unauthorized access: call AI tool without authPOST /ai/tools/summary with no cookiesNo auth cookies401 Unauthorized
FEAT-61-09Rate limit / abuse: exceed AI throttleFire 21 AI tool requests within one hour21st request in the window429 Too Many Requests from AiThrottlerGuard
FEAT-61-10Edge case: empty document scopeTrigger a tool on a document with no ingested chunksDocument with 0 chunks422 Unprocessable Entity
FEAT-61-11Edge case: trivia scoring mixSubmit a mix of correct + incorrect trivia answersPartially correct answers[]Score and explanations returned correctly per answer
FEAT-61-12Plan gating: free-plan user hits a toolFree-plan user POSTs any /ai/tools/* routeFree-plan account403 with upgrade prompt

Edge Cases

  • Empty document (no chunks ingested) -> tool returns 422 Unprocessable Entity.
  • Course with zero documents -> scope resolver returns empty, tool returns 422.
  • LLM returns malformed JSON -> tool throws 500 with a safe error message.
  • Very large document scope -> scope resolver caps total characters at MAX_TOTAL_CHARACTERS.
  • DEV_BYPASS_SUBSCRIPTION=true -> credit deduction skipped; all tools remain functional.
  • AI request in-flight -> AiToolBase shows a loading spinner and disables the trigger.
  • AI request fails -> retryable error state with "Try again".
  • Network failure during DocumentPicker upload -> upload error surfaced; onDocumentsIngested does not fire.

Notes

  • Dependencies: FEAT-60 (GeminiGenerationProvider, job queue, embedding pipeline, plan-access guard), FEAT-31 (CookieAuthGuard), FEAT-39 (PlanAccessGuard, @RequireFeature('AI_CREDIT')).
  • Q&A has no persistence table — answers are stateless by design.
  • Adding a new AI tool requires no controller or processor changes — register it in AiModule.providers + the AI_TOOL_REGISTRY multi-provider factory.
  • The project uses manual SQL migrations + pnpm run migrate:prod; never prisma migrate dev on the shared Neon branch.

Adding a New AI Tool (Checklist)

  1. backend/src/ai/tools/<name>.tool.ts — extend BaseAiTool, set readonly toolId, implement protected execute().
  2. backend/src/ai/dto/generate-<name>.dto.ts — validated DTO.
  3. AiModule — add the tool to providers + the AI_TOOL_REGISTRY multi-provider factory.
  4. Migration (if needed) — write SQL with the required header block, run pnpm run migrate:prod.
  5. frontend/app/(features)/ai/<name>/page.tsx"use client", compose AiPageShell + DocumentPicker + useAiTool + a result component.

No change to dynamic generation routing is required as long as the tool is registered in AiModule.


Security Notes

  • No AI endpoint accepts an arbitrary user_id; identity always comes from cookie auth context.
  • AiScopeResolverService enforces ownership of documents/courses.
  • BaseAiTool.run() deducts one AI credit per successful generation (skipped when DEV_BYPASS_SUBSCRIPTION=true).
  • All tool endpoints are rate-limited at 20 requests / hour via AiThrottlerGuard.