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:
/aitoolbox-rail redesign — a persistent right-hand rail so AI tools and games are always visible.- 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 | nullsessionHandlerRef: Ref<(text: string) => Promise<void> | void>— registered by the active tool.- Lifecycle helpers:
startToolSession(mode, handler?)andendToolSession().
Tool registry
TOOL_MODESregistry keyed by tool slug (scenario,quiz,word-search,crossword) defines label / emoji / accent color / input placeholder per tool.ACCENT_CLASSESmaps each accent to banner, bubble border, badge, and divider Tailwind classes (light + dark).
UI states and rendering rules
- Active-session banner — appears only while
activeToolModeis 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
toolModeset gets the accent left border. AI-side tool messages render a badge fromtoolMessageType:prompt-> tool emoji + label,feedback-> "Feedback",summary-> "Summary". - Score pills — render when
toolScoreis present.scoreTone(score, max)returns green (>=80%), amber (>=50%), red below. DefaulttoolMaxScoreis 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");onClickshort-circuits viapreventDefault.
Toolbox rail redesign
/aibecomes a two-column layout atlg+: 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 viastartToolSession(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 arenderprop 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.
DocumentPickergained avariant: "card" | "embedded"prop so it can be reused inside the modal without double-bordering.
Validation and error handling
- Existing messages without
toolModerender exactly as before; normalsendMessage()is unchanged whenactiveToolModeis 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 ID | Description | Steps | Input | Expected Result |
|---|---|---|---|---|
| FEAT-82-01 | Happy path: normal chat unchanged | Open /ai, send a normal message with no tool active | Plain chat message | No banner, no badges; behavior identical to pre-FEAT-82 |
| FEAT-82-02 | Happy path: start a tool session | Call window.__aiChat.startToolSession("scenario") | mode="scenario" | Amber banner "Scenario Simulator Active" + End Session button; a session-start divider is appended |
| FEAT-82-03 | Happy path: feedback message with score | Push a feedback message with score 8/10 | toolMessageType="feedback", toolScore=8 | Bubble has amber left border, "Feedback" badge, green "Score: 8 / 10" pill |
| FEAT-82-04 | Validation: score-tone thresholds | Push feedback messages scored 8/10, 6/10, 3/10 | Scores 8, 6, 3 | Pills render green, amber, red respectively |
| FEAT-82-05 | Validation: tool chip click during active session | Click a tool chip while a session is active | Chip click event | Chip is aria-disabled, no navigation; native tooltip "End current session first" |
| FEAT-82-06 | Validation: in-chat tool card during active session | Click a non-in-chat tool card in the rail while a session runs | Tool card click | Card rendered as div[aria-disabled], no navigation; rail shows "SESSION IN PROGRESS" callout |
| FEAT-82-07 | Backend-path failure: session active, no handler registered | With a session active but no handler, call sendMessage() | Trimmed text input | User bubble appended with toolMode set; no network call to /ai/chat |
| FEAT-82-08 | Backend-path: multi-file ingest partial failure | Queue two files where one ingest throws | Two files, one unreadable | Error shown; partially-uploaded ids still returned to the calling tool |
| FEAT-82-09 | Edge case: End Session with no active mode | Invoke endToolSession() when activeToolMode is null | No active session | Guarded early-return; no divider created, no error |
| FEAT-82-10 | Edge case: multiple consecutive sessions | Run two tool sessions back-to-back in one chat | Two start/end cycles | Each session bounded by its own start/end dividers; accent colors do not bleed |
| FEAT-82-11 | Edge case: scroll containment on new message | Push a tool message via pushToolMessage | New message appended | Only the messages container scrolls; main.scrollTop stays 0 |
| FEAT-82-12 | Edge case: Escape closes DocumentPicker modal | Open the picker modal, press Escape | Escape keypress | Modal closes, focus returns to trigger, underlying page state preserved |
| FEAT-82-13 | Accessibility: keyboard focus on End Session | Tab to the End Session button | Keyboard focus | Visible focus ring; banner announced via role="status" aria-live="polite" |
Edge Cases
- Session active but handler missing —
sendMessagestill appends the user bubble (so input is visible in the transcript) but does nothing else. - End Session with no active mode — guarded:
endToolSessionearly-returns from divider creation ifmodeis null. - Unknown
toolModeon a persisted message — guarded by theToolModeKeytype at compile time; the dev bridge enforces the typed shape viapushToolMessage(msg: Omit<ChatMessage, "id">). - Dark mode — every accent has both light and dark Tailwind classes in
ACCENT_CLASSES. - Long banner labels — the label is
truncateso a long tool name does not push End Session off-screen on mobile. - Empty score (
toolScore === 0) —0 / 10still renders (red pill) sincetoolScore !== undefined. - Rail clipping when chat content is short — chat-shell wrapper constrained at
lg:h-[calc(100vh-8rem)]so the rail'soverflow-y-autoscrolls 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 variant —
ai-coursesusesvariant="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.__aiChatbridge 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
/docs/features/61-ai-learning-tools— AI tool registry foundation/docs/features/89-hangman-game— game whose card lives in the toolbox rail/docs/features/90-letter-tile-game— game whose card lives in the toolbox rail/docs/features/qa-requirements— QA scenario coverage rules/docs/git-convention— branch / commit / PR conventions