Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions openspec/changes/lms-ask-doubt/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-17
68 changes: 68 additions & 0 deletions openspec/changes/lms-ask-doubt/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Design: LMS Ask Doubt (AI Assistant)

## Context
The "Ask a Doubt" feature is a dedicated route within the AI Assistant package. It leverages the global design tokens and localization system so the screen feels native to the Cortex shell rather than like a one-off custom surface.

## Goals / Non-Goals

**Goals:**
- Provide a dedicated `AskDoubtScreen`.
- Implement a 2x2 grid of "Quick Suggestions" in the empty chat state.
- Create a floating Chat Input bar with an integrated attachment button, placeholder voice affordance, and prominent send action.
- Keep all user-facing labels, prompts, mock responses, and accessibility copy localized through `packages/core` l10n resources.
- Use existing Cortex semantic colors, typography, spacing, and surfaces instead of hardcoded values.

**Non-Goals:**
- Implementing the "History Drawer" fully if it requires complex backend storage changes in the first sprint (mock history is acceptable).
- Voice recording functionality (placeholder only).

## Decisions

### 1. Screen Architecture
- **Widget**: `AskDoubtScreen` (StatefulWidget or ConsumerWidget).
- **Layout**: `Column` containing a custom `AppHeader` (mimicking the minimal reference header), a scrollable `ListView` for messages, and a fixed `PreferredSize` or `BottomAppBar` equivalent for the input bar.
- **Shell Context**: Ask Doubt currently renders within the existing shell/navigation context reached from the AI Assistant hub rather than introducing a separate shell root.
- **Routing**: The `AskDoubtScreen` is pushed directly from `AIAssistantPage` using `AppRoute` instead of being registered as a root immersive route.
- **Separation**: Ask Doubt UI concerns should be split into focused widgets when a screen file grows beyond lightweight orchestration. The screen should coordinate state and composition, while drawer, menu, empty state, and message-list presentation live in dedicated files.

### 2. Message UI
- **Student Bubbles**: Right-aligned, using a soft neutral grey bubble that is darker than the page background but not near-black, with support for optional image headers.
- **AI Responses**: Left-aligned and rendered without a surrounding bubble/container so the assistant copy reads directly on the page, closer to the reference chat style.
- **Actions**: Bubble action affordances should inherit design-token icon colors and spacing.

### 3. Input System
- **State**: `TextEditingController` for the message.
- **Image Support**: The attachment action currently uses a mock image source and sends an image-backed placeholder message immediately instead of maintaining a local pre-send preview state.
- **Composer**: A single floating pill surface with small left/right/bottom inset so it visually floats above the page background.
- **Keyboard Behavior**: The floating composer must translate above the software keyboard on device instead of remaining fixed under it.
- **Landscape Handling**: In landscape or similarly short viewports, keyboard avoidance must keep the composer visible within the remaining viewport rather than lifting it beyond the visible content area.
- **Dynamic Placement**: Keyboard avoidance should be derived from the actual composer size and the currently visible viewport, not from a fixed estimated height.
- **Focus Behavior**: Repeated taps on the input surface should reliably reopen the software keyboard even after the keyboard was dismissed while focus remained on the field.
- **Tap Detection**: Keyboard reopening should not depend only on high-level gesture recognition for the editable region; the composer should respond reliably even when the text field consumes the gesture.
- **Keyboard Gap**: The floating offset above the keyboard should tighten while the keyboard is visible so the composer does not appear detached from it.
- **Portrait Keyboard Gap**: In portrait, the keyboard-visible gap should collapse to the minimum possible value so the composer feels attached to the keyboard rather than floating above it.
- **Orientation-Specific Gap**: Keyboard-visible spacing should be orientation-aware: near-flush in portrait, but allowed a small buffer in landscape so the composer remains comfortably legible in short viewports.
- **Portrait Inset Tuning**: Portrait keyboard placement may apply a small inset compensation so the composer aligns to the visible keyboard edge rather than to an over-reported system inset.
- **Portrait Tightness**: Portrait tuning should prefer the smallest comfortable gap above the keyboard and can be refined independently from landscape spacing.
- **Portrait Priority**: If portrait still feels visually detached, prefer reducing the gap further rather than reusing the landscape buffer.
- **Send Button**: A circular button within the input field that becomes visually dimmed when no input is present. Empty submissions are ignored by the send handler.
- **Copy**: Placeholder text, semantics labels, and action labels must come from shared localizations.
- **Suggestion Cards**: The four empty-state suggestion cards should keep a stable visual height across rows, even when labels wrap or text scaling is larger.
- **Suggestion Card Alignment**: Empty-state suggestion labels should read centered within each card rather than left-weighted.

### 4. Session Controls
- **History Drawer**: The history drawer should remain a dedicated surface with readable active/inactive states.
- **Chat Menu**: The per-session overflow menu must use a surface and text treatment that preserves readable contrast in both light and dark mode.
- **Pin State Labeling**: The session overflow menu should reflect the current pin state of the session, showing `Unpin` for already pinned chats.
- **Pinned Ordering**: Pinned chats should remain grouped at the top of history even when new unpinned chats are created afterward.

### 5. Animations
- **Slide-up**: Chat bubbles should enter with a subtle slide-up and fade-in.
- **Thinking Indicator**: A localized loading indicator accompanies AI response wait times.

## Risks / Trade-offs

- **Risk**: Keyboard covering the input bar on some Android devices.
- **Trade-off**: Using `AppScroll` vs a standard `ListView` - `ListView` is better for chat history with `reverse: false` and manual scrolling to bottom.
- **Risk**: The current attachment flow is still a mock and does not yet cover real file selection or pre-send preview behavior.
- **Trade-off**: Mock AI responses remain local placeholder content for now, but they should still be localized because they are visible product copy.
30 changes: 30 additions & 0 deletions openspec/changes/lms-ask-doubt/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Proposal: LMS Ask Doubt (AI Assistant)

## Context
The AI Assistant hub serves as the central location for personalized student support. One of the core pillars of this experience is the ability for students to quickly "Ask a Doubt" and receive immediate, context-aware assistance.

## Goals
- **Seamless Entry**: Trigger the doubt workflow from the AI Assistant landing page.
- **Dedicated Chat Route**: Provide a focused Ask Doubt chat environment for exploring doubts.
- **Attachment Placeholder**: Support a lightweight attachment affordance backed by mock image message behavior in the first sprint.
- **Quick Guidance**: Offer suggestion chips (e.g., "Explain Concept", "Solve Problem") to reduce friction for new users.

## What Changes
1. **AIAssistant Navigation**: Update `AIAssistantPage` to navigate to the new `AskDoubtScreen`.
2. **New AskDoubtScreen**: Create a dedicated chat interface in `packages/ai_assistant` that is pushed from the AI Assistant hub.
3. **Chat UI System**: Implement specialized message bubbles for Student and AI roles, incorporating the current premium light-surface styling.
4. **Integrated Input Bar**: A sophisticated input system supporting text, a mock attachment action, and voice (optional placeholder).
5. **History Access**: A slide-out drawer or similar mechanism to access past doubts (as seen in reference).

## Capabilities

### New Capabilities
- `ai-ask-doubt`: A full-featured chat environment for students to interact with the AI study assistant.

### Modified Capabilities
- `ai-assistant-hub`: Enhanced to support navigation and state persistence for active doubt sessions.

## Impact
- `packages/ai_assistant`: Core feature implementation (Screens, Widgets, Providers).
- `packages/core`: Utilization of `AppRoute`, `AppHeader`, and refined `AppText` for chat bubbles.
- `packages/testpress`: Integration of the new route within the main shell.
104 changes: 104 additions & 0 deletions openspec/changes/lms-ask-doubt/specs/ai-ask-doubt/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Specs: LMS Ask Doubt (AI Assistant)

## ADDED Requirements

### Requirement: AI Hub Navigation
The system SHALL navigate from the AI Assistant hub to a dedicated "Ask a Doubt" route.

#### Scenario: User opens Ask a Doubt
- **WHEN** the user taps the "Ask a Doubt" card in the AI Assistant hub
- **THEN** the system SHALL transition to the `AskDoubtScreen`
- **AND** the transition SHALL be smooth (AppRoute default)
- **AND** the Ask Doubt route SHALL render within the existing app shell/navigation context used by the hub flow

### Requirement: Empty State & Suggestions
The system SHALL provide guidance when no messages are present in the current session.

#### Scenario: Initial empty state
- **WHEN** the `AskDoubtScreen` is opened for a new session
- **THEN** it SHALL display a localized empty-state greeting asking how the assistant can help
- **AND** it SHALL show 4 localized quick suggestion cards for concept explanation, problem solving, practice questions, and study tips
- **AND** the quick suggestion cards SHALL maintain consistent row heights when labels wrap or text scaling increases
- **AND** the quick suggestion card labels SHALL be visually centered within each tile
- **WHEN** a user taps a chip
- **THEN** the chip text SHALL be populated into the chat input field (or sent directly)

### Requirement: Multimedia Chat Interface
The system SHALL allow users to send text and images to the AI assistant.

#### Scenario: Sending a text doubt
- **WHEN** the user types a question and taps the "Send" (Up arrow) button
- **THEN** the message SHALL appear as a soft grey student bubble on the right
- **AND** the AI SHALL display a localized thinking indicator
- **AND** a response SHALL appear left-aligned without a surrounding bubble container

#### Scenario: Attaching an image
- **WHEN** the user taps the "Plus" button
- **THEN** the system SHALL create a mock image-backed doubt using the current placeholder attachment flow
- **AND** the attachment flow MAY use a predefined image source instead of a real picker in the current sprint
- **WHEN** the mock attachment is sent
- **THEN** the image SHALL be displayed within the student's message bubble

### Requirement: Visual Design & Feedback
The chat interface SHALL provide visual feedback for all interactive states.

#### Scenario: Typing and Sending
- **WHEN** the input field is empty
- **THEN** the "Send" button SHALL appear visually dimmed
- **AND** empty send attempts SHALL have no effect
- **WHEN** the user is typing
- **THEN** the input SHALL update in real time
- **WHEN** the software keyboard opens on device
- **THEN** the composer SHALL remain visible within the viewport
- **AND** the composer SHALL stay closely aligned to the keyboard edge
- **AND** keyboard spacing MAY be tuned by orientation, with portrait appearing near-flush and landscape allowing a small buffer
- **AND** the composer position SHALL respond to the actual rendered composer height instead of a fixed height assumption
- **WHEN** the user taps the input surface again after dismissing the keyboard
- **THEN** the keyboard SHALL reopen reliably
- **AND** the reopen behavior SHALL remain reliable even when the editable child handles the tap interaction
- **WHEN** a message is sent
- **THEN** the keyboard SHALL remain open (standard chat behavior)
- **AND** the view SHALL auto-scroll to the latest message

### Requirement: Localized and Token-Driven UI
The Ask Doubt implementation SHALL use shared localization resources and Cortex design tokens for user-facing UI.

#### Scenario: Rendering Ask Doubt surfaces and copy
- **WHEN** the Ask Doubt screen renders headers, actions, placeholders, prompts, empty-state labels, or mock assistant responses
- **THEN** those strings SHALL come from shared app localizations
- **AND** visible colors for surfaces, text, overlays, and cursors SHALL come from the design system instead of hardcoded literals

#### Scenario: Rendering session menu in dark mode
- **WHEN** the session overflow menu is opened in dark mode
- **THEN** the menu surface SHALL provide sufficient contrast with the page background
- **AND** all menu actions, not just destructive ones, SHALL remain clearly readable

#### Scenario: Managing a pinned session
- **WHEN** the user opens the overflow menu for a session that is already pinned
- **THEN** the primary pin action SHALL read as `Unpin`
- **AND** pinned sessions SHALL remain above unpinned sessions in the history list even after new chats are created

### Requirement: State Consistency & Race Condition Prevention
The system SHALL ensure that asynchronous AI responses are appended to the most recent session state.

#### Scenario: User clears chat during AI thinking
- **WHEN** the AI is in "Thinking" mode following a user message
- **AND** the user clears the chat session (or modifies the message history) before the AI response is received
- **THEN** the subsequent AI response SHALL NOT revert the chat to the stale state captured before the delay
- **AND** the AI response SHALL be appended to the current state of the session if it still exists
### Requirement: Proper Resource Management
The Ask Doubt implementation SHALL manage lifecycle-dependent resources correctly to prevent memory leaks.

#### Scenario: Using FocusNodes in interactive overlays
- **WHEN** an interactive field like the rename session dialog requires focus
- **THEN** the system SHALL manage the `FocusNode` lifecycle within a `StatefulWidget` or equivalent controller
- **AND** the `FocusNode` SHALL be explicitly disposed of when the parent widget is removed from the tree
- **AND** focus SHALL be requested programmatically when the overlay appears, rather than instantiating new nodes during build

### Requirement: Robust Resource Identifiers
The system SHALL use sufficiently unique identifiers for all internal resources to prevent collisions.

#### Scenario: Generating IDs for sessions and messages
- **WHEN** a new session or message (user/AI/image) is created
- **THEN** its `id` SHALL be generated with sufficient entropy to avoid collisions even when multiple items are created in rapid succession
- **AND** the generation logic SHALL incorporate microsecond-level precision combined with current state metadata (e.g., list length) to ensure uniqueness
50 changes: 50 additions & 0 deletions openspec/changes/lms-ask-doubt/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Tasks: LMS Ask Doubt (AI Assistant)

## 1. Setup and Navigation

- [x] 1.1 Add `AskDoubtScreen` scaffold in `packages/ai_assistant/lib/screens/`
- [x] 1.2 Implement navigation route in `packages/testpress` (if required) or direct push in `AIAssistantPage`
- [x] 1.3 Update `AIQuickActionCard` in `AIAssistantPage` to trigger navigation to `AskDoubtScreen`

## 2. UI Components Implementation

- [x] 2.1 Implement `DoubtHeader`: Minimal header with Back button and Menu icon (as per reference)
- [x] 2.2 Implement `DoubtEmptyState`: "What can I help with?" title and `QuickSuggestionGrid` (2x2)
- [x] 2.3 Implement `MessageBubble`: Customizable widget for Student and AI roles with premium styling
- [x] 2.4 Implement `DoubtInputBar`: Full-width input with attachment (+) button and integrated Send (Up arrow) button

## 3. State and Logic

- [x] 3.1 Create `DoubtSessionProvider` (Riverpod) to manage the list of messages and "Thinking" state
- [x] 3.2 Implement mock response logic: Simulate AI delayed response when a message is sent
- [x] 3.3 Add mock attachment placeholder logic that injects an image-backed user message without full picker integration
- [x] 3.4 Implement auto-scroll behavior for the message list

## 4. Polishing and Verification

- [x] 4.1 Apply premium design tokens (gradients, custom shadows) to match the "AI Hub" aesthetic
- [x] 4.2 Verify Dark Mode compatibility and accessibility (AppSemantics)
- [x] 4.3 Add micro-animations for message entry and a localized loading indicator for thinking state
- [x] 4.4 Replace remaining hardcoded Ask Doubt UI strings and color literals with shared localizations and design tokens
- [x] 4.5 Align the Ask Doubt OpenSpec artifacts with the current floating light-surface implementation
- [x] 4.6 Split oversized Ask Doubt screen concerns into dedicated widgets so the screen stays focused on orchestration
- [x] 4.7 Keep the floating composer visible above the software keyboard on device
- [x] 4.8 Fix session menu contrast so all actions remain readable in dark mode
- [x] 4.9 Make repeated input taps reliably reopen the keyboard and tighten the composer gap above it
- [x] 4.10 Keep empty-state suggestion cards visually consistent under larger text scales and refine the keyboard lift spacing
- [x] 4.11 Make keyboard reopening robust even when the editable field consumes tap gestures
- [x] 4.12 Keep the composer visible in landscape when the keyboard occupies most of the viewport
- [x] 4.13 Base keyboard avoidance on the actual composer size so portrait and landscape use the same dynamic behavior
- [x] 4.14 Keep Ask Doubt as a dedicated pushed route within the existing shell while tuning its local inset handling
- [x] 4.15 Remove the remaining portrait keyboard gap so the composer sits nearly flush above the keyboard
- [x] 4.16 Make keyboard-visible spacing orientation-aware so portrait and landscape can use different gap rules
- [x] 4.17 Fine-tune portrait inset handling without changing landscape spacing
- [x] 4.18 Tighten the portrait-only keyboard gap further without changing landscape spacing
- [x] 4.19 Tighten the portrait-only keyboard gap again without changing landscape spacing
- [x] 4.20 Center the empty-state suggestion card labels within their tiles
- [x] 4.21 Remove the surrounding bubble surface from AI responses while keeping user messages styled as bubbles
- [x] 4.22 Make the history menu reflect pinned state and keep pinned chats above newly created unpinned chats
- [x] 4.23 Soften the student message bubble from near-black to a neutral grey surface
- [x] 4.24 Fix race condition in `DoubtSessionNotifier` during `sendMessage` and `addImageMessage` by using the latest state in async callbacks
- [x] 4.25 Fix memory leak in `AskDoubtOverlays` by moving `FocusNode` management to `_AskDoubtScreenState` and implementing proper disposal
- [x] 4.26 Implement more robust unique ID generation for sessions and messages using microsecond precision and session metadata
49 changes: 48 additions & 1 deletion packages/ai_assistant/lib/models/ai_models.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

class AIRecommendation {
final String id;
final String type;
Expand Down Expand Up @@ -52,3 +51,51 @@ class AIActivity {
this.status,
});
}

enum AIMessageRole { user, assistant }

class AIMessage {
final String id;
final String content;
final AIMessageRole role;
final DateTime timestamp;
final String? imageUrl;

const AIMessage({
required this.id,
required this.content,
required this.role,
required this.timestamp,
this.imageUrl,
});
}

class AIChatSession {
final String id;
final String title;
final List<AIMessage> messages;
final DateTime createdAt;
final bool isPinned;

const AIChatSession({
required this.id,
required this.title,
required this.messages,
required this.createdAt,
this.isPinned = false,
});

AIChatSession copyWith({
String? title,
List<AIMessage>? messages,
bool? isPinned,
}) {
return AIChatSession(
id: id,
title: title ?? this.title,
messages: messages ?? this.messages,
createdAt: createdAt,
isPinned: isPinned ?? this.isPinned,
);
}
}
Loading