StudyBoost Docs

Feature: AI Chat Tool Conversation Mode & Message Indicators

Metadata

  • Issue ID: FEAT-82
  • Status: Done
  • Owner: danrave1234
  • Related PRs: feature/82-ai-chat-tool-mode -> dev (StudyBoostIO/StudyboostV2#96, closes #82)

Overview

FEAT-82 is a UI-only foundation layered onto the existing AI chat page (frontend/app/(features)/ai/page.tsx). It introduces a text-based AI tool conversation mode as a distinct interaction state from normal chat, so users can tell when the chat is "inside" a tool session (e.g. the Scenario Simulator from FEAT-81, or future quiz / word-search / crossword chat tools).

The PR also ships two related redesigns released together under #96:

  1. /ai toolbox-rail redesign — a persistent right-hand rail so AI tools and games are always visible.
  2. DocumentPicker modal redesign + multi-file uploads — used by every other AI tool page.

There are no backend changes — FEAT-82 is intentionally UI-only. Any future tool registers its own endpoint and calls it from the session handler. Canonical feature doc: docs/features/FEAT-82-ai-chat-tool-conversation-mode.md.


Frontend Behavior

State machine

  • activeToolMode: ToolModeKey | null
  • sessionHandlerRef: Ref<(text: string) => Promise<void> | void> — registered by the active tool.
  • Lifecycle helpers: startToolSession(mode, handler?) and endToolSession().

Tool registry

  • TOOL_MODES registry keyed by tool slug (scenario, quiz, word-search, crossword) defines label / emoji / accent color / input placeholder per tool.
  • ACCENT_CLASSES maps each accent to banner, bubble border, badge, and divider Tailwind classes (light + dark).

UI states and rendering rules

  • Active-session banner — appears only while activeToolMode is set; role="status", accent styling, and an End Session button. End Session clears state, removes the handler, and appends a session-ended divider.
  • Tool message bubbles — any message with toolMode set gets the accent left border. AI-side tool messages render a badge from toolMessageType: prompt -> tool emoji + label, feedback -> "Feedback", summary -> "Summary".
  • Score pills — render when toolScore is present. scoreTone(score, max) returns green (>=80%), amber (>=50%), red below. Default toolMaxScore is 10.
  • Session dividers — first-class role: "divider" messages so they survive the same persistence path as other messages.
  • Input placeholder — follows the active tool's inputPlaceholder.
  • Tool chips — disabled during an active session (aria-disabled, pointer-events-none opacity-50, native tooltip "End current session first"); onClick short-circuits via preventDefault.

Toolbox rail redesign

  • /ai becomes a two-column layout at lg+: chat shell on the left, persistent AI Toolbox rail on the right, grouped into Text Tools, Games, and a Recent placeholder.
  • Tools with inChatMode (Scenario Simulator, Word Search) launch their session inline via startToolSession(mode) instead of navigating away.
  • During an active session the rail dims (grayscale opacity-40) and pins a "SESSION IN PROGRESS" callout with its own End Session button.
  • The chat-shell wrapper is capped at lg:h-[calc(100vh-8rem)]; the messages container scrolls itself rather than bubbling scroll up to the page-level <main>.
  • Below lg, a "Tools" button in the top bar opens a slide-in drawer with the same toolbox.

DocumentPicker modal redesign

  • DocumentPickerDialog — controlled modal wrapper (overlay, click-outside-close, Escape-to-close).
  • DocumentPickerTrigger — bundles a primary CTA button + the dialog, with a render prop for compact "Change document" links.
  • DocumentPickerEmptyState — pre-result card per tool page (icon + title + subtitle + primary CTA).
  • Multi-file uploads — the Upload tab accepts batches; each file gets its own row with an individual remove button, an "Add more files" pill, a total summary line, and Submit ingests files sequentially before returning all ids.
  • DocumentPicker gained a variant: "card" | "embedded" prop so it can be reused inside the modal without double-bordering.

Validation and error handling

  • Existing messages without toolMode render exactly as before; normal sendMessage() is unchanged when activeToolMode is null.
  • Unreadable files in a multi-file batch are skipped with a per-file error; duplicate selections (same name + size) are deduplicated.
  • If a sequential ingest throws, the error is shown but partially-uploaded ids are still returned so work is not lost.

Dev bridge

window.__aiChat is exposed in development for QA and downstream tool integration: startToolSession(mode, handler?), endToolSession(), pushToolMessage(msg).


Backend Behavior

None. FEAT-82 is UI-only — no new endpoints, DTOs, or migrations. Future text-based tools (FEAT-81 onward) register their own endpoint and call it from the session handler registered via sessionHandlerRef.


QA Test Scenarios

Scenario IDDescriptionStepsInputExpected Result
FEAT-82-01Happy path: normal chat unchangedOpen /ai, send a normal message with no tool activePlain chat messageNo banner, no badges; behavior identical to pre-FEAT-82
FEAT-82-02Happy path: start a tool sessionCall window.__aiChat.startToolSession("scenario")mode="scenario"Amber banner "Scenario Simulator Active" + End Session button; a session-start divider is appended
FEAT-82-03Happy path: feedback message with scorePush a feedback message with score 8/10toolMessageType="feedback", toolScore=8Bubble has amber left border, "Feedback" badge, green "Score: 8 / 10" pill
FEAT-82-04Validation: score-tone thresholdsPush feedback messages scored 8/10, 6/10, 3/10Scores 8, 6, 3Pills render green, amber, red respectively
FEAT-82-05Validation: tool chip click during active sessionClick a tool chip while a session is activeChip click eventChip is aria-disabled, no navigation; native tooltip "End current session first"
FEAT-82-06Validation: in-chat tool card during active sessionClick a non-in-chat tool card in the rail while a session runsTool card clickCard rendered as div[aria-disabled], no navigation; rail shows "SESSION IN PROGRESS" callout
FEAT-82-07Backend-path failure: session active, no handler registeredWith a session active but no handler, call sendMessage()Trimmed text inputUser bubble appended with toolMode set; no network call to /ai/chat
FEAT-82-08Backend-path: multi-file ingest partial failureQueue two files where one ingest throwsTwo files, one unreadableError shown; partially-uploaded ids still returned to the calling tool
FEAT-82-09Edge case: End Session with no active modeInvoke endToolSession() when activeToolMode is nullNo active sessionGuarded early-return; no divider created, no error
FEAT-82-10Edge case: multiple consecutive sessionsRun two tool sessions back-to-back in one chatTwo start/end cyclesEach session bounded by its own start/end dividers; accent colors do not bleed
FEAT-82-11Edge case: scroll containment on new messagePush a tool message via pushToolMessageNew message appendedOnly the messages container scrolls; main.scrollTop stays 0
FEAT-82-12Edge case: Escape closes DocumentPicker modalOpen the picker modal, press EscapeEscape keypressModal closes, focus returns to trigger, underlying page state preserved
FEAT-82-13Accessibility: keyboard focus on End SessionTab to the End Session buttonKeyboard focusVisible focus ring; banner announced via role="status" aria-live="polite"

Edge Cases

  • Session active but handler missingsendMessage still appends the user bubble (so input is visible in the transcript) but does nothing else.
  • End Session with no active mode — guarded: endToolSession early-returns from divider creation if mode is null.
  • Unknown toolMode on a persisted message — guarded by the ToolModeKey type at compile time; the dev bridge enforces the typed shape via pushToolMessage(msg: Omit<ChatMessage, "id">).
  • Dark mode — every accent has both light and dark Tailwind classes in ACCENT_CLASSES.
  • Long banner labels — the label is truncate so a long tool name does not push End Session off-screen on mobile.
  • Empty score (toolScore === 0)0 / 10 still renders (red pill) since toolScore !== undefined.
  • Rail clipping when chat content is short — chat-shell wrapper constrained at lg:h-[calc(100vh-8rem)] so the rail's overflow-y-auto scrolls internally.
  • Scroll-into-view bubbling — messages container is scrolled via scrollTo({ top: scrollHeight }); scrollIntoView() is avoided because it walks the ancestor chain.
  • DocumentPicker multi-file — unreadable files are skipped with a per-file error; duplicates deduped; sequential ingest returns partial ids on a mid-batch failure.
  • Embedded variantai-courses uses variant="embedded" so the picker drops its outer border inside the host card.

Notes

  • Feature flag: none — ships unconditionally.
  • Dependencies: FEAT-60 (AI document intelligence), FEAT-61 (reusable AI tool registry, AI page primitives). Blocks/enables FEAT-81 (Scenario Simulator), the first consumer of the conversation mode.
  • Known limitations: UI-only; normal chat behavior is preserved exactly when no tool mode is active. The window.__aiChat bridge is development-only.
  • All accent colors and per-tool copy live in TOOL_MODES + ACCENT_CLASSES — adding a new tool means adding a registry entry, not editing JSX.

Related Pages