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:
- Backend centralization — a typed, extensible AI tool registry so adding a new AI tool requires no changes to the controller or processor.
- Frontend primitives —
DocumentPicker,AiPageShell, and theuseAiToolhook replace the formerAiToolWorkspacemonolith. - Gamification foundation —
TriviaGameToolas 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
| Component | Location | Purpose |
|---|---|---|
AiPageShell | components/features/ai/ai-page-shell.tsx | Page header wrapper (label, title, description, badges) |
DocumentPicker | components/features/ai/document-picker.tsx | Upload + owned-doc picker; ingests docs; fires onDocumentsIngested(ids[]) |
useAiTool<T> | hooks/use-ai-tool.ts | Generic hook: run(body) -> POST /ai/tools/:toolId; exposes isLoading, error, result, reset |
AiToolBase | components/features/ai/ai-tool-base.tsx | Loading / error / rerun / token-usage wrapper |
UI states and interactions
- Loading — while an AI request is in-flight,
AiToolBaseshows a spinner and disables the trigger. - Error — failed requests render a retryable error state with a "Try again" affordance.
- Document ingest —
DocumentPickeruploads files viaPOST /documents/upload, then firesonDocumentsIngested(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_querybefore 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
| Route | Tool called | Notes |
|---|---|---|
/ai/document-analyzer | POST /ai/tools/summary | Auto-triggers after doc ingest |
/ai/flashcard-builder | POST /ai/tools/flashcards | Auto-triggers after doc ingest |
/ai/ask-documents | POST /ai/tools/qa | Shows query input after ingest; fires on submit |
/ai/quiz-generator | POST /ai/tools/study-guide | Auto-triggers after doc ingest |
/ai/trivia-game | POST /ai/tools/trivia-game | Full 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
| Tool | Required 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
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /ai/tools/:toolId | Auth + Plan | Run any registered AI tool |
GET | /ai/tools/summary/document/:documentId | Auth + Plan | List summaries for a document |
GET | /ai/tools/flashcards/document/:documentId | Auth + Plan | List flashcard sets |
GET | /ai/tools/flashcards/sets/:setId | Auth + Plan | Get a specific flashcard set |
GET | /ai/tools/study-guide/document/:documentId | Auth + Plan | List study guides |
GET | /ai/tools/study-guide/sets/:guideId | Auth + Plan | Get a specific study guide |
POST | /ai/tools/trivia-game/:sessionId/submit | Auth + Plan | Submit answers; returns scored result |
GET | /ai/tools/trivia-game/:sessionId | Auth + Plan | Retrieve 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
AiScopeResolverServiceenforces document/course ownership; users can only resolve chunks from material they own.TriviaGameToolstores generated questions inai_game_sessions.questions(JSONB) withcorrectIndex+explanation; the API response strips both, sending onlyquestion+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
500with a safe error message.
Database tables
| Table | Migration | Purpose |
|---|---|---|
ai_summaries | 202505030300_add_ai_tools_foundation | Summary results |
ai_flashcard_sets / ai_flashcards | 202505030300_add_ai_tools_foundation | Flashcard sets and rows |
ai_study_guides / ai_study_guide_sections | 202605050234_add_ai_study_guides | Study guide metadata + sections |
ai_game_sessions / ai_game_attempts | 202605050327_add_ai_game_tables | Trivia game sessions + attempts |
Migrations are hand-written SQL applied via pnpm run migrate:prod.
QA Test Scenarios
| Scenario ID | Description | Steps | Input | Expected Result |
|---|---|---|---|---|
| FEAT-61-01 | Happy path: Q&A with valid scope | Open /ai/ask-documents, ingest owned doc, submit a question | Valid scope.document_ids, user_query | Returns { answer, sources[] } with matching snippets |
| FEAT-61-02 | Happy path: generate study guide | Ingest single owned document, trigger study guide | Valid documentId | Returns structured guide with >=1 section |
| FEAT-61-03 | Happy path: generate trivia game | Open /ai/trivia-game, pick owned doc, generate | Valid documentId | Session returned with N questions; correctIndex/explanation absent |
| FEAT-61-04 | Validation: Q&A without query | POST /ai/tools/qa with scope but no user_query | Missing user_query | 400 Bad Request; no LLM call |
| FEAT-61-05 | Validation: trivia submit with wrong answer count | Submit fewer/more answers than questions | Mismatched answers[] length | 400 Bad Request; no attempt row created |
| FEAT-61-06 | Backend failure: LLM returns malformed JSON | Trigger a tool when Gemini returns non-JSON | Malformed LLM response | 500 with safe error message; no partial persistence |
| FEAT-61-07 | Authorization: access a document not owned | POST any tool with a foreign documentId | Foreign documentId | 403 Forbidden from AiScopeResolverService |
| FEAT-61-08 | Unauthorized access: call AI tool without auth | POST /ai/tools/summary with no cookies | No auth cookies | 401 Unauthorized |
| FEAT-61-09 | Rate limit / abuse: exceed AI throttle | Fire 21 AI tool requests within one hour | 21st request in the window | 429 Too Many Requests from AiThrottlerGuard |
| FEAT-61-10 | Edge case: empty document scope | Trigger a tool on a document with no ingested chunks | Document with 0 chunks | 422 Unprocessable Entity |
| FEAT-61-11 | Edge case: trivia scoring mix | Submit a mix of correct + incorrect trivia answers | Partially correct answers[] | Score and explanations returned correctly per answer |
| FEAT-61-12 | Plan gating: free-plan user hits a tool | Free-plan user POSTs any /ai/tools/* route | Free-plan account | 403 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
500with 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 ->
AiToolBaseshows a loading spinner and disables the trigger. - AI request fails -> retryable error state with "Try again".
- Network failure during
DocumentPickerupload -> upload error surfaced;onDocumentsIngesteddoes 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+ theAI_TOOL_REGISTRYmulti-provider factory. - The project uses manual SQL migrations +
pnpm run migrate:prod; neverprisma migrate devon the shared Neon branch.
Adding a New AI Tool (Checklist)
backend/src/ai/tools/<name>.tool.ts— extendBaseAiTool, setreadonly toolId, implementprotected execute().backend/src/ai/dto/generate-<name>.dto.ts— validated DTO.AiModule— add the tool toproviders+ theAI_TOOL_REGISTRYmulti-provider factory.- Migration (if needed) — write SQL with the required header block, run
pnpm run migrate:prod. frontend/app/(features)/ai/<name>/page.tsx—"use client", composeAiPageShell+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. AiScopeResolverServiceenforces ownership of documents/courses.BaseAiTool.run()deducts one AI credit per successful generation (skipped whenDEV_BYPASS_SUBSCRIPTION=true).- All tool endpoints are rate-limited at 20 requests / hour via
AiThrottlerGuard.