diff --git a/app/assets/images/ai_bot.png b/app/assets/images/ai_bot.png new file mode 100644 index 000000000..9a01d8818 Binary files /dev/null and b/app/assets/images/ai_bot.png differ diff --git a/openspec/changes/redesign-ai-tab/.openspec.yaml b/openspec/changes/redesign-ai-tab/.openspec.yaml new file mode 100644 index 000000000..f774115be --- /dev/null +++ b/openspec/changes/redesign-ai-tab/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-20 diff --git a/openspec/changes/redesign-ai-tab/design.md b/openspec/changes/redesign-ai-tab/design.md new file mode 100644 index 000000000..214bd9792 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/design.md @@ -0,0 +1,40 @@ +## Context + +See [proposal.md](proposal.md) for background and motivation. The AI tab is being revamped to feature a mock conversational chatbot companion instead of outdated static content. + +## Goals / Non-Goals + +**Goals:** +- Nested navigation subroutes for immersive chat (`/ai/chat`) and history (`/ai/history`). +- Immersive chatbot interface with simulated typewriter responses and auto-scrolling. +- Compliant touch targets and accessibility semantics across all new interactive widgets. +- Mock chat sessions and messages stored in-memory. + +**Non-Goals:** +- Real network HTTP data sync or production backend integration. +- Offline database persistence (Drift/SQLite caching layers). + +## Decisions + +### 1. Navigation Shell & Subroutes +Configure `/ai/chat` and `/ai/history` as child routes nested under the primary `/ai` route in `ai_routes.dart`, using `rootNavigatorKey` as the `parentNavigatorKey` to hide the main bottom navigation bar during chat interactions. +- **Alternatives Considered:** Registering them as separate root-level routes, which breaks logical hierarchy and tab grouping. + +### 2. Accessibility & Target Sizing (WCAG 2.5.5) +Refactor custom interactive elements (such as the send button in the composer) to use `AppIconButton` (which natively bakes in 48x48dp dimensions and semantics) rather than small raw `GestureDetector` buttons. Wrap other interactive items (like cards and text links) with `AppSemantics.button()` or configure layout padding to maintain a 48dp target. +- **Alternatives Considered:** Relying on basic 36x36 dp icons, which violates WCAG accessibility requirements. + +### 3. Animation and Motion Tokens +Utilize design tokens (`design.motion.*`, `design.motion.normal`, etc.) and verify `MotionPreferences.shouldAnimate(context)` in all custom animations (like the typing indicator), rather than hardcoding durations or curves. +- **Alternatives Considered:** Using hardcoded `Curves.easeInOut` and `Duration(...)` values, which bypasses system preference settings. + +### 4. In-Memory Mock Data Layer +Utilize static mock structures (`AiChatSession` and `AiChatMessage`) in `ai_chat_mock_data.dart` to simulate local session fetching and modification. +- **Alternatives Considered:** Building a full Repository/Drift DB pipeline, which is out of scope for this UI redesign experiment. + +## Risks / Trade-offs + +- **[Risk]** The scroll controller might be disposed before the post-frame callback runs in `_scrollToBottom()`, leading to assertion errors. + - **Mitigation:** Ensure all accesses to `_scrollController` inside post-frame callbacks are guarded with `if (!mounted) return;`. +- **[Risk]** Heavy custom typewriter states leading to UI lag on low-end devices. + - **Mitigation:** Run timer intervals at a lightweight 30ms and check user motion preferences to skip animations if needed. diff --git a/openspec/changes/redesign-ai-tab/proposal.md b/openspec/changes/redesign-ai-tab/proposal.md new file mode 100644 index 000000000..f0affd2e6 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/proposal.md @@ -0,0 +1,44 @@ +## Why + +The current AI screen displays placeholder greetings, quick action cards (such as "Ask Doubt"), and recent help sections that are outdated. They need to be replaced with an interactive mock AI study companion chatbot experience to support a mock UI redesign experiment. + +## What Changes + +- **AiScreen Revamp:** Clean up the UI of `AiScreen` by removing the greeting, quick actions card, and recent help list. Replace them with a welcome section prompting users to start a new chat, and a "Recent chats" list linking to past sessions. +- **Immersive Chat Screen:** Create a new `AiChatImmersiveScreen` providing a full conversational mock AI chat interface, incorporating a typing animation, typewriter message generation, and auto-scroll behaviors. +- **Chat History Screen:** Create `AiChatHistoryScreen` to display a scrollable list of all past mock chat sessions with humanized timestamps. +- **AI Composer Widget:** Add a reusable `AiComposer` widget with text input and media attachment buttons. +- **Rich Markdown Styling:** Enhance `AppMarkdown` to support rich text formatting (custom blockquotes, inline/block code, table styles) for rendered AI responses. +- **Mock Data Layer:** Establish standard structures (`AiChatMessage`, `AiChatSession`) and lists of pre-populated mock chat sessions in `ai_chat_mock_data.dart`. +- **Navigation Integration:** Update `AiRoutes` to register child routes (`/ai/chat`, `/ai/history`) and change the tab navigation icon to `LucideIcons.bot`. +- **Localization cleanup:** Clean up outdated localization entries associated with the old AI screen from `app_en.arb` (and other locale arb files). + +## Capabilities + +### New Capabilities + +- `ai-immersive-chat`: Provides a full conversational chat interface with mock AI responses and typing indicators. +- `ai-chat-history`: Displays a list of recent and archived chat sessions with formatted timestamps. +- `ai-composer`: A dedicated input widget for typing messages and triggering actions. + +### Modified Capabilities + +- `ai-screen`: Revamped root screen focusing on onboarding and navigation to immersive experiences. +- `app-markdown`: Enriched styling support for markdown elements. +- `navigation`: Added child routes for chat and history screens. + +## Impact + +- **UI Screens & Widgets:** + - Modified `packages/core/lib/screens/ai_screen.dart` (revamped welcome/history UI). + - Created `packages/core/lib/screens/ai_chat_immersive_screen.dart` (immersive chat UI). + - Created `packages/core/lib/screens/ai_chat_history_screen.dart` (chat history UI). + - Created `packages/core/lib/widgets/ai_composer.dart` (composer input). + - Modified `packages/core/lib/widgets/app_markdown.dart` (improved markdown rendering). +- **Data Layer:** + - Created `packages/core/lib/data/ai_chat_mock_data.dart` (mock chat sessions and model definitions). +- **Navigation Router:** + - Modified `packages/testpress/lib/navigation/routes/ai_routes.dart` (nested chat/history subroutes). + - Modified `packages/testpress/lib/navigation/app_router.dart` (AI tab icon update to bot icon). +- **Localization:** + - Modified `packages/core/lib/l10n/app_en.arb` (and other locale arb files) to remove outdated keys and prepare for new strings. diff --git a/openspec/changes/redesign-ai-tab/specs/ai-chat-history/spec.md b/openspec/changes/redesign-ai-tab/specs/ai-chat-history/spec.md new file mode 100644 index 000000000..28310cb7b --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/ai-chat-history/spec.md @@ -0,0 +1,12 @@ +## Purpose + +Displays a list of recent and archived chat sessions with formatted timestamps. + +## ADDED Requirements + +### Requirement: Chat Session History List +The system SHALL list previous chat sessions sorted by modification time. + +#### Scenario: View chat history +- **WHEN** the user views the chat history list +- **THEN** the system displays all past sessions with their title and relative last active timestamp. diff --git a/openspec/changes/redesign-ai-tab/specs/ai-composer/spec.md b/openspec/changes/redesign-ai-tab/specs/ai-composer/spec.md new file mode 100644 index 000000000..726b7abc0 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/ai-composer/spec.md @@ -0,0 +1,12 @@ +## Purpose + +A dedicated input widget for typing messages and triggering actions. + +## ADDED Requirements + +### Requirement: Text and Media Input Composer +The system SHALL allow the user to type text messages and trigger attachment actions. + +#### Scenario: User types message +- **WHEN** the user inputs text and taps send +- **THEN** the composer submits the text and resets. diff --git a/openspec/changes/redesign-ai-tab/specs/ai-immersive-chat/spec.md b/openspec/changes/redesign-ai-tab/specs/ai-immersive-chat/spec.md new file mode 100644 index 000000000..3edd06380 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/ai-immersive-chat/spec.md @@ -0,0 +1,12 @@ +## Purpose + +Provides a full conversational chat interface with mock AI responses and typewriter typing indicators. + +## ADDED Requirements + +### Requirement: Interactive conversational chat flow +The system SHALL display the message history and allow the user to send new messages, resulting in typewriter-animated mock AI responses. + +#### Scenario: User sends a message +- **WHEN** the user submits a message in the immersive chat +- **THEN** the message is appended to the list, a bouncing typing indicator is briefly displayed, and a mock AI response is printed character by character. diff --git a/openspec/changes/redesign-ai-tab/specs/ai-screen/spec.md b/openspec/changes/redesign-ai-tab/specs/ai-screen/spec.md new file mode 100644 index 000000000..e65cd150e --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/ai-screen/spec.md @@ -0,0 +1,12 @@ +## Purpose + +Revamped root screen focusing on onboarding and navigation to immersive experiences. + +## ADDED Requirements + +### Requirement: Onboarding Welcome Screen +The system SHALL display an AI onboarding welcome screen with an action to start a new chat. + +#### Scenario: Start new chat from welcome screen +- **WHEN** the user taps the start new chat button +- **THEN** the system navigates the user to the immersive chat interface. diff --git a/openspec/changes/redesign-ai-tab/specs/app-markdown/spec.md b/openspec/changes/redesign-ai-tab/specs/app-markdown/spec.md new file mode 100644 index 000000000..c9aece7b4 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/app-markdown/spec.md @@ -0,0 +1,12 @@ +## Purpose + +Enriched styling support for markdown elements inside the application. + +## ADDED Requirements + +### Requirement: Rich Text Markdown Rendering +The system SHALL render markdown text including custom blockquotes, inline/block code, and tables. + +#### Scenario: Display rich text response +- **WHEN** the message contains markdown code blocks or tables +- **THEN** the system displays them with standard code styling and table borders. diff --git a/openspec/changes/redesign-ai-tab/specs/navigation/spec.md b/openspec/changes/redesign-ai-tab/specs/navigation/spec.md new file mode 100644 index 000000000..9afb7c3ed --- /dev/null +++ b/openspec/changes/redesign-ai-tab/specs/navigation/spec.md @@ -0,0 +1,8 @@ +## ADDED Requirements + +### Requirement: Subpage Back Button Navigation +All nested screens pushed onto the root tab navigation stack SHALL show a back button in the header. + +#### Scenario: User navigates to immersive chat +- **WHEN** the user is viewing the immersive chat screen +- **THEN** the header displays a back button to return to the AI root screen diff --git a/openspec/changes/redesign-ai-tab/tasks.md b/openspec/changes/redesign-ai-tab/tasks.md new file mode 100644 index 000000000..f44667e89 --- /dev/null +++ b/openspec/changes/redesign-ai-tab/tasks.md @@ -0,0 +1,30 @@ +## 1. Setup and Routing + +- [x] 1.1 Add the `ai_bot.png` asset to the app assets directory. +- [x] 1.2 Update `packages/testpress/lib/navigation/routes/ai_routes.dart` to add nested routes for `chat` (`AiChatImmersiveScreen`) and `history` (`AiChatHistoryScreen`). +- [x] 1.3 Update `packages/testpress/lib/navigation/app_router.dart` to change the AI tab icon to `LucideIcons.bot`. +- [x] 1.4 Export the new screens and widgets in `packages/core/lib/core.dart`. + +## 2. Mock Data Layer + +- [x] 2.1 Create `packages/core/lib/data/ai_chat_mock_data.dart` containing model definitions for `AiChatMessage` and `AiChatSession`, along with a list of pre-populated sessions. + +## 3. UI Redesign Implementation & Screen Fixes + +- [x] 3.1 Implement the welcome layout, history card, and "Start new chat" action in `packages/core/lib/screens/ai_screen.dart`. +- [x] 3.2 Refactor `packages/core/lib/screens/ai_screen.dart` to use localized strings and wrap interactive text button targets in `AppSemantics.button()`, ensuring compliance with WCAG 2.5.5 touch target size (48x48dp). +- [x] 3.3 Create the list-based chat history UI in `packages/core/lib/screens/ai_chat_history_screen.dart`. +- [x] 3.4 Implement accessibility semantics (`AppSemantics.scrollableList` and `AppSemantics.button`) and localization in `packages/core/lib/screens/ai_chat_history_screen.dart`. +- [x] 3.5 Create the conversation view with simulated typewriter responses in `packages/core/lib/screens/ai_chat_immersive_screen.dart`. +- [x] 3.6 Implement design motion tokens, history button semantics, motion preference checks, and `!mounted` guards in `packages/core/lib/screens/ai_chat_immersive_screen.dart`. + +## 4. UI Composer & Markdown Polish + +- [x] 4.1 Create the composer input text and attachment triggers layout in `packages/core/lib/widgets/ai_composer.dart`. +- [x] 4.2 Implement the Send button in `packages/core/lib/widgets/ai_composer.dart` using `AppIconButton` to meet the WCAG 48x48dp touch target requirement. +- [x] 4.3 Refactor `packages/core/lib/widgets/app_markdown.dart` to support rich layout styling using the design token parameters. + +## 5. Localization Setup + +- [x] 5.1 Add all user-visible English strings (e.g. "Your AI study companion", "Start new chat", "Recent chats", "View All", "New Chat", "How can I help you today?", "Ask anything...", and attachment labels) to `packages/core/lib/l10n/app_en.arb` and remove unused legacy keys. +- [x] 5.2 Re-generate localized helper files using the localization code generator script or build command. diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart index 5dabd7919..2fd07ce19 100644 --- a/packages/core/lib/core.dart +++ b/packages/core/lib/core.dart @@ -49,12 +49,15 @@ export 'widgets/bookmark_folders_sheet.dart'; export 'widgets/app_toast.dart'; export 'widgets/session_expired_dialog.dart'; export 'widgets/app_confirmation_dialog.dart'; +export 'widgets/ai_composer.dart'; // Shell export 'shell/app_shell.dart'; // Screens export 'screens/ai_screen.dart'; +export 'screens/ai_chat_immersive_screen.dart'; +export 'screens/ai_chat_history_screen.dart'; export 'screens/bp_elearn_my_results_screen.dart'; // Navigation diff --git a/packages/core/lib/data/ai_chat_mock_data.dart b/packages/core/lib/data/ai_chat_mock_data.dart new file mode 100644 index 000000000..8a6b09362 --- /dev/null +++ b/packages/core/lib/data/ai_chat_mock_data.dart @@ -0,0 +1,196 @@ +enum MessageRole { user, ai } + +class AiChatMessage { + final String content; + final DateTime timestamp; + final MessageRole role; + + const AiChatMessage({ + required this.content, + required this.timestamp, + required this.role, + }); +} + +class AiChatSession { + final String id; + final String title; + final DateTime createdAt; + final DateTime modifiedAt; + final List messages; + + const AiChatSession({ + required this.id, + required this.title, + required this.createdAt, + required this.modifiedAt, + required this.messages, + }); +} + +final List mockChatSessions = [ + AiChatSession( + id: '1', + title: 'Quantum Computing Intro', + createdAt: DateTime.now().subtract(const Duration(hours: 2)), + modifiedAt: DateTime.now().subtract(const Duration(minutes: 5)), + messages: [ + AiChatMessage( + content: 'Explain quantum computing in simple terms.', + timestamp: DateTime.now().subtract(const Duration(minutes: 6)), + role: MessageRole.user, + ), + AiChatMessage( + content: '''## What is Quantum Computing? + +Quantum computing is a fundamentally different way of processing information — one that harnesses the strange laws of **quantum mechanics**. + +### Classical vs Quantum + +| Feature | Classical Computer | Quantum Computer | +|---|---|---| +| Basic unit | Bit (0 or 1) | Qubit (0, 1, or both) | +| Processing | Sequential / parallel | Massively parallel | +| Best for | General tasks | Optimization, simulation | + +### Key Concepts + +- **Superposition** — A qubit can be 0 and 1 *at the same time*, until observed. +- **Entanglement** — Two qubits can be linked so that the state of one instantly affects the other, regardless of distance. +- **Interference** — Quantum algorithms amplify correct answers and cancel wrong ones. + +> Think of it like this: a classical computer tries every door one by one. A quantum computer tries *all doors simultaneously*.''', + timestamp: DateTime.now().subtract(const Duration(minutes: 6)), + role: MessageRole.ai, + ), + AiChatMessage( + content: 'Give me an example of its real-world application.', + timestamp: DateTime.now().subtract(const Duration(minutes: 5)), + role: MessageRole.user, + ), + AiChatMessage( + content: '''## Real-World Applications of Quantum Computing + +Here are some of the most promising areas where quantum computing is already making an impact: + +### 1. 🧬 Drug Discovery +Quantum computers can simulate molecular interactions at the atomic level — something classical computers struggle with exponentially. Companies like **IBM** and **Google** are partnering with pharma to model protein folding and drug binding. + +### 2. 🔐 Cryptography +Quantum algorithms like **Shor's algorithm** can theoretically break RSA encryption, which is driving the field of *post-quantum cryptography* to build quantum-safe protocols. + +### 3. 📦 Supply Chain Optimization +Quantum annealing (used by **D-Wave**) solves complex optimization problems — like minimizing shipping routes across thousands of variables — far faster than classical solvers. + +### 4. 🌦️ Climate Modelling +Simulating atmospheric chemistry at quantum scale enables more accurate climate predictions and helps design better **carbon capture** materials. + +--- + +**Bottom line:** Quantum computing won't replace classical computing — it will *complement* it for specific hard problems.''', + timestamp: DateTime.now().subtract(const Duration(minutes: 5)), + role: MessageRole.ai, + ), + ], + ), + AiChatSession( + id: '2', + title: 'Flutter Monorepo', + createdAt: DateTime.now().subtract(const Duration(days: 1)), + modifiedAt: DateTime.now().subtract(const Duration(days: 1)), + messages: [ + AiChatMessage( + content: 'What is Flutter monorepo?', + timestamp: DateTime.now().subtract(const Duration(days: 1)), + role: MessageRole.user, + ), + AiChatMessage( + content: '''## Flutter Monorepo Architecture + +A **Flutter monorepo** is a single Git repository that contains multiple related packages and applications — all managed together. + +### Why Use a Monorepo? + +- ✅ **Shared code** — Design systems, utilities, and domain logic live in one place +- ✅ **Atomic commits** — A single PR can update multiple packages consistently +- ✅ **Simplified dependency management** — No version mismatch between internal packages +- ✅ **Easier refactoring** — Rename a widget across 5 packages in one go + +### Typical Structure + +``` +my_app/ +├── app/ # Consumer shell (thin) +├── packages/ +│ ├── core/ # Design system, primitives +│ ├── courses/ # Courses domain +│ └── exams/ # Exams domain +└── melos.yaml # Monorepo tooling config +``` + +### Tooling + +The most common tool is **Melos**, which provides: +- `melos run build` — run scripts across packages +- `melos bootstrap` — link local packages together +- `melos publish` — publish packages to pub.dev + +> 🔑 The key principle: **packages are feature domains, not layers**. Each package owns its data, logic, and UI for one vertical slice.''', + timestamp: DateTime.now().subtract(const Duration(days: 1)), + role: MessageRole.ai, + ), + ], + ), + AiChatSession( + id: '3', + title: 'Cosine Similarity vs Dot Product', + createdAt: DateTime.now().subtract(const Duration(days: 14)), + modifiedAt: DateTime.now().subtract(const Duration(days: 14)), + messages: [ + AiChatMessage( + content: 'Comparison between cosine similarity and dot product', + timestamp: DateTime.now().subtract(const Duration(days: 14)), + role: MessageRole.user, + ), + AiChatMessage( + content: '''## Cosine Similarity vs Dot Product + +Both are ways to measure the **relationship between two vectors**, but they capture different things. + +### Definitions + +**Dot Product** +``` +A · B = |A| × |B| × cos(θ) +``` +It measures both the *direction* and the *magnitude* of two vectors. + +**Cosine Similarity** +``` +cos(θ) = (A · B) / (|A| × |B|) +``` +It normalizes the dot product, so only *direction* matters — not magnitude. + +--- + +### Comparison Table + +| Property | Dot Product | Cosine Similarity | +|---|---|---| +| Range | −∞ to +∞ | −1 to +1 | +| Sensitive to magnitude? | ✅ Yes | ❌ No | +| Normalized vectors | Equivalent | Equivalent | +| Use case | Raw similarity score | Semantic / text similarity | + +### When to Use Which? + +- Use **dot product** when vector magnitude carries meaning (e.g. recommendation scores, attention weights in transformers). +- Use **cosine similarity** when you only care about *direction* — e.g. comparing document embeddings regardless of length. + +> 💡 **In practice**: if your embeddings are already L2-normalized (unit vectors), the dot product *equals* cosine similarity. This is why vector databases like Pinecone and Weaviate often use dot product internally for speed.''', + timestamp: DateTime.now().subtract(const Duration(days: 14)), + role: MessageRole.ai, + ), + ], + ), +]; diff --git a/packages/core/lib/generated/l10n/app_localizations.dart b/packages/core/lib/generated/l10n/app_localizations.dart index 3c7a9c93e..764cca0a5 100644 --- a/packages/core/lib/generated/l10n/app_localizations.dart +++ b/packages/core/lib/generated/l10n/app_localizations.dart @@ -1775,6 +1775,12 @@ abstract class AppLocalizations { /// **'Open lesson: {title}'** String openDetailedLesson(String title); + /// No description provided for @openChatSession. + /// + /// In en, this message translates to: + /// **'Open chat: {title}'** + String openChatSession(String title); + /// No description provided for @videoLessonTabNotes. /// /// In en, this message translates to: @@ -5186,87 +5192,9 @@ abstract class AppLocalizations { /// No description provided for @aiSupportTitle. /// /// In en, this message translates to: - /// **'AI Support'** + /// **'AI Chat'** String get aiSupportTitle; - /// No description provided for @aiSupportGreeting. - /// - /// In en, this message translates to: - /// **'Hi {userName} 👋'** - String aiSupportGreeting(String userName); - - /// No description provided for @aiSupportQuickActions. - /// - /// In en, this message translates to: - /// **'QUICK ACTIONS'** - String get aiSupportQuickActions; - - /// No description provided for @aiSupportAskDoubtTitle. - /// - /// In en, this message translates to: - /// **'Ask a Doubt'** - String get aiSupportAskDoubtTitle; - - /// No description provided for @aiSupportAskDoubtSubtitle. - /// - /// In en, this message translates to: - /// **'Snap, upload or type your question'** - String get aiSupportAskDoubtSubtitle; - - /// No description provided for @aiSupportAskNowButton. - /// - /// In en, this message translates to: - /// **'Ask Now'** - String get aiSupportAskNowButton; - - /// No description provided for @aiSupportAiExamTitle. - /// - /// In en, this message translates to: - /// **'AI Exam'** - String get aiSupportAiExamTitle; - - /// No description provided for @aiSupportAiExamSubtitle. - /// - /// In en, this message translates to: - /// **'Create AI practice exams based on weak topics or chapters'** - String get aiSupportAiExamSubtitle; - - /// No description provided for @aiSupportCreateAiExamButton. - /// - /// In en, this message translates to: - /// **'Create AI Exam'** - String get aiSupportCreateAiExamButton; - - /// No description provided for @aiSupportRecentHelp. - /// - /// In en, this message translates to: - /// **'RECENT HELP'** - String get aiSupportRecentHelp; - - /// No description provided for @aiSupportViewAll. - /// - /// In en, this message translates to: - /// **'View All'** - String get aiSupportViewAll; - - /// No description provided for @aiSupportNoRecentDoubts. - /// - /// In en, this message translates to: - /// **'No recent AI doubts.'** - String get aiSupportNoRecentDoubts; - - /// No description provided for @aiSupportStatusAnswered. - /// - /// In en, this message translates to: - /// **'Answered'** - String get aiSupportStatusAnswered; - - /// No description provided for @aiSupportStatusProcessing. - /// - /// In en, this message translates to: - /// **'Processing'** - String get aiSupportStatusProcessing; - /// No description provided for @aiSupportAskingAi. /// /// In en, this message translates to: @@ -5728,6 +5656,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Meeting link is missing'** String get teamsMissingJoinLink; + + /// No description provided for @aiStudyCompanionTitle. + /// + /// In en, this message translates to: + /// **'Your AI study companion'** + String get aiStudyCompanionTitle; + + /// No description provided for @aiWelcomeSubtitle. + /// + /// In en, this message translates to: + /// **'Ask questions, understand concepts,\nand learn faster.'** + String get aiWelcomeSubtitle; + + /// No description provided for @aiStartNewChat. + /// + /// In en, this message translates to: + /// **'Start new chat'** + String get aiStartNewChat; + + /// No description provided for @aiRecentChatsHeader. + /// + /// In en, this message translates to: + /// **'Recent chats'** + String get aiRecentChatsHeader; + + /// No description provided for @aiViewAllRecentChats. + /// + /// In en, this message translates to: + /// **'View All'** + String get aiViewAllRecentChats; + + /// No description provided for @aiNewChatHeader. + /// + /// In en, this message translates to: + /// **'New Chat'** + String get aiNewChatHeader; + + /// No description provided for @aiComposerGreeting. + /// + /// In en, this message translates to: + /// **'How can I help you today?'** + String get aiComposerGreeting; + + /// No description provided for @aiChatHistoryTitle. + /// + /// In en, this message translates to: + /// **'Chat History'** + String get aiChatHistoryTitle; + + /// No description provided for @aiComposerPlaceholder. + /// + /// In en, this message translates to: + /// **'Ask anything...'** + String get aiComposerPlaceholder; + + /// No description provided for @aiComposerAttachImage. + /// + /// In en, this message translates to: + /// **'Attach image'** + String get aiComposerAttachImage; + + /// No description provided for @aiComposerTakePhoto. + /// + /// In en, this message translates to: + /// **'Take photo'** + String get aiComposerTakePhoto; + + /// No description provided for @aiComposerSendMessage. + /// + /// In en, this message translates to: + /// **'Send message'** + String get aiComposerSendMessage; } class _AppLocalizationsDelegate diff --git a/packages/core/lib/generated/l10n/app_localizations_ar.dart b/packages/core/lib/generated/l10n/app_localizations_ar.dart index 5d770f988..366a14100 100644 --- a/packages/core/lib/generated/l10n/app_localizations_ar.dart +++ b/packages/core/lib/generated/l10n/app_localizations_ar.dart @@ -933,6 +933,11 @@ class AppLocalizationsAr extends AppLocalizations { return 'افتح الدرس: $title'; } + @override + String openChatSession(String title) { + return 'افتح الدردشة: $title'; + } + @override String get videoLessonTabNotes => 'ملاحظات'; @@ -2877,49 +2882,7 @@ class AppLocalizationsAr extends AppLocalizations { 'هل أنت متأكد أنك تريد حذف هذا الامتحان المنزل؟'; @override - String get aiSupportTitle => 'دعم الذكاء الاصطناعي'; - - @override - String aiSupportGreeting(String userName) { - return 'مرحباً $userName 👋'; - } - - @override - String get aiSupportQuickActions => 'إجراءات سريعة'; - - @override - String get aiSupportAskDoubtTitle => 'اسأل شكاً'; - - @override - String get aiSupportAskDoubtSubtitle => 'التقط أو حمّل أو اكتب سؤالك'; - - @override - String get aiSupportAskNowButton => 'اسأل الآن'; - - @override - String get aiSupportAiExamTitle => 'امتحان الذكاء الاصطناعي'; - - @override - String get aiSupportAiExamSubtitle => - 'قم بإنشاء امتحانات تدريبية بناءً على المواضيع الضعيفة'; - - @override - String get aiSupportCreateAiExamButton => 'إنشاء امتحان'; - - @override - String get aiSupportRecentHelp => 'المساعدة الأخيرة'; - - @override - String get aiSupportViewAll => 'عرض الكل'; - - @override - String get aiSupportNoRecentDoubts => 'لا توجد شكوك حديثة.'; - - @override - String get aiSupportStatusAnswered => 'تم الرد'; - - @override - String get aiSupportStatusProcessing => 'قيد المعالجة'; + String get aiSupportTitle => 'دردشة الذكاء الاصطناعي'; @override String get aiSupportAskingAi => 'جاري سؤال الذكاء الاصطناعي'; @@ -3168,4 +3131,41 @@ class AppLocalizationsAr extends AppLocalizations { @override String get teamsMissingJoinLink => 'رابط الاجتماع مفقود'; + + @override + String get aiStudyCompanionTitle => 'رفيق الدراسة بالذكاء الاصطناعي الخاص بك'; + + @override + String get aiWelcomeSubtitle => + 'اسأل أسئلة، وافهم المفاهيم،\nوتعلم بشكل أسرع.'; + + @override + String get aiStartNewChat => 'بدء محادثة جديدة'; + + @override + String get aiRecentChatsHeader => 'المحادثات الأخيرة'; + + @override + String get aiViewAllRecentChats => 'عرض الكل'; + + @override + String get aiNewChatHeader => 'محادثة جديدة'; + + @override + String get aiComposerGreeting => 'كيف يمكنني مساعدتك اليوم؟'; + + @override + String get aiChatHistoryTitle => 'سجل المحادثات'; + + @override + String get aiComposerPlaceholder => 'اسأل عن أي شيء...'; + + @override + String get aiComposerAttachImage => 'إرفاق صورة'; + + @override + String get aiComposerTakePhoto => 'التقاط صورة'; + + @override + String get aiComposerSendMessage => 'إرسال رسالة'; } diff --git a/packages/core/lib/generated/l10n/app_localizations_en.dart b/packages/core/lib/generated/l10n/app_localizations_en.dart index 170e6947c..b41ce529f 100644 --- a/packages/core/lib/generated/l10n/app_localizations_en.dart +++ b/packages/core/lib/generated/l10n/app_localizations_en.dart @@ -936,6 +936,11 @@ class AppLocalizationsEn extends AppLocalizations { return 'Open lesson: $title'; } + @override + String openChatSession(String title) { + return 'Open chat: $title'; + } + @override String get videoLessonTabNotes => 'Notes'; @@ -2874,49 +2879,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Are you sure you want to delete this downloaded exam?'; @override - String get aiSupportTitle => 'AI Support'; - - @override - String aiSupportGreeting(String userName) { - return 'Hi $userName 👋'; - } - - @override - String get aiSupportQuickActions => 'QUICK ACTIONS'; - - @override - String get aiSupportAskDoubtTitle => 'Ask a Doubt'; - - @override - String get aiSupportAskDoubtSubtitle => 'Snap, upload or type your question'; - - @override - String get aiSupportAskNowButton => 'Ask Now'; - - @override - String get aiSupportAiExamTitle => 'AI Exam'; - - @override - String get aiSupportAiExamSubtitle => - 'Create AI practice exams based on weak topics or chapters'; - - @override - String get aiSupportCreateAiExamButton => 'Create AI Exam'; - - @override - String get aiSupportRecentHelp => 'RECENT HELP'; - - @override - String get aiSupportViewAll => 'View All'; - - @override - String get aiSupportNoRecentDoubts => 'No recent AI doubts.'; - - @override - String get aiSupportStatusAnswered => 'Answered'; - - @override - String get aiSupportStatusProcessing => 'Processing'; + String get aiSupportTitle => 'AI Chat'; @override String get aiSupportAskingAi => 'ASKING AI'; @@ -3168,4 +3131,41 @@ class AppLocalizationsEn extends AppLocalizations { @override String get teamsMissingJoinLink => 'Meeting link is missing'; + + @override + String get aiStudyCompanionTitle => 'Your AI study companion'; + + @override + String get aiWelcomeSubtitle => + 'Ask questions, understand concepts,\nand learn faster.'; + + @override + String get aiStartNewChat => 'Start new chat'; + + @override + String get aiRecentChatsHeader => 'Recent chats'; + + @override + String get aiViewAllRecentChats => 'View All'; + + @override + String get aiNewChatHeader => 'New Chat'; + + @override + String get aiComposerGreeting => 'How can I help you today?'; + + @override + String get aiChatHistoryTitle => 'Chat History'; + + @override + String get aiComposerPlaceholder => 'Ask anything...'; + + @override + String get aiComposerAttachImage => 'Attach image'; + + @override + String get aiComposerTakePhoto => 'Take photo'; + + @override + String get aiComposerSendMessage => 'Send message'; } diff --git a/packages/core/lib/generated/l10n/app_localizations_ml.dart b/packages/core/lib/generated/l10n/app_localizations_ml.dart index 9a5944273..26db30a9b 100644 --- a/packages/core/lib/generated/l10n/app_localizations_ml.dart +++ b/packages/core/lib/generated/l10n/app_localizations_ml.dart @@ -945,6 +945,11 @@ class AppLocalizationsMl extends AppLocalizations { return 'പാഠം തുറക്കുക: $title'; } + @override + String openChatSession(String title) { + return 'ചാറ്റ് തുറക്കുക: $title'; + } + @override String get videoLessonTabNotes => 'കുറിപ്പുകൾ'; @@ -2923,50 +2928,7 @@ class AppLocalizationsMl extends AppLocalizations { 'ഈ ഡൗൺലോഡ് ചെയ്ത പരീക്ഷ ഇല്ലാതാക്കണമെന്ന് ഉറപ്പാണോ?'; @override - String get aiSupportTitle => 'AI പിന്തുണ'; - - @override - String aiSupportGreeting(String userName) { - return 'നമസ്കാരം $userName 👋'; - } - - @override - String get aiSupportQuickActions => 'പെട്ടെന്നുള്ള പ്രവർത്തനങ്ങൾ'; - - @override - String get aiSupportAskDoubtTitle => 'സംശയം ചോദിക്കുക'; - - @override - String get aiSupportAskDoubtSubtitle => - 'ചോദ്യം ഫോട്ടോ എടുക്കുകയോ അപ്‌ലോഡ് ചെയ്യുകയോ ടൈപ്പ് ചെയ്യുകയോ ചെയ്യുക'; - - @override - String get aiSupportAskNowButton => 'ഇപ്പോൾ ചോദിക്കുക'; - - @override - String get aiSupportAiExamTitle => 'AI പരീക്ഷ'; - - @override - String get aiSupportAiExamSubtitle => - 'ദുർബലമായ വിഷയങ്ങളെ അടിസ്ഥാനമാക്കി AI പരീക്ഷകൾ സൃഷ്ടിക്കുക'; - - @override - String get aiSupportCreateAiExamButton => 'AI പരീക്ഷ സൃഷ്ടിക്കുക'; - - @override - String get aiSupportRecentHelp => 'സമീപകാല സഹായം'; - - @override - String get aiSupportViewAll => 'എല്ലാം കാണുക'; - - @override - String get aiSupportNoRecentDoubts => 'സമീപകാല AI സംശയങ്ങളൊന്നുമില്ല.'; - - @override - String get aiSupportStatusAnswered => 'മറുപടി നൽകി'; - - @override - String get aiSupportStatusProcessing => 'പ്രോസസ്സ് ചെയ്യുന്നു'; + String get aiSupportTitle => 'AI ചാറ്റ്'; @override String get aiSupportAskingAi => 'AI-യോട് ചോദിക്കുന്നു'; @@ -3218,8 +3180,45 @@ class AppLocalizationsMl extends AppLocalizations { 'ക്യാമറ, മൈക്രോഫോൺ ആക്സസ് ആവശ്യമാണ്. ചേരുന്നതിന് ക്രമീകരണങ്ങളിൽ അവ പ്രവർത്തനക്ഷമമാക്കുക.'; @override - String get teamsJoinMeetingLoading => 'മീറ്റിംഗിൽ ചേരുന്നു...'; + String get teamsJoinMeetingLoading => 'മീറ്റിംഗിൻ ചേരുന്നു...'; @override String get teamsMissingJoinLink => 'മീറ്റിംഗ് ലിങ്ക് ലഭ്യമല്ല'; + + @override + String get aiStudyCompanionTitle => 'നിങ്ങളുടെ AI പഠന സഹായി'; + + @override + String get aiWelcomeSubtitle => + 'ചോദ്യങ്ങൾ ചോദിക്കുക, ആശയങ്ങൾ മനസ്സിലാക്കുക,\nകൂടുതൽ വേഗത്തിൽ പഠിക്കുക.'; + + @override + String get aiStartNewChat => 'പുതിയ ചാറ്റ് ആരംഭിക്കുക'; + + @override + String get aiRecentChatsHeader => 'സമീപകാല ചാറ്റുകൾ'; + + @override + String get aiViewAllRecentChats => 'എല്ലാം കാണുക'; + + @override + String get aiNewChatHeader => 'പുതിയ ചാറ്റ്'; + + @override + String get aiComposerGreeting => 'ഇന്ന് ഞാൻ നിങ്ങളെ എങ്ങനെ സഹായിക്കണം?'; + + @override + String get aiChatHistoryTitle => 'ചാറ്റ് ചരിത്രം'; + + @override + String get aiComposerPlaceholder => 'എന്തും ചോദിക്കൂ...'; + + @override + String get aiComposerAttachImage => 'ചിത്രം ചേർക്കുക'; + + @override + String get aiComposerTakePhoto => 'ഫോട്ടോ എടുക്കുക'; + + @override + String get aiComposerSendMessage => 'സന്ദേശം അയക്കുക'; } diff --git a/packages/core/lib/generated/l10n/app_localizations_ta.dart b/packages/core/lib/generated/l10n/app_localizations_ta.dart index 3adfb16b6..93651a00f 100644 --- a/packages/core/lib/generated/l10n/app_localizations_ta.dart +++ b/packages/core/lib/generated/l10n/app_localizations_ta.dart @@ -949,6 +949,11 @@ class AppLocalizationsTa extends AppLocalizations { return 'பாடத்தைத் திற: $title'; } + @override + String openChatSession(String title) { + return 'அரட்டையைத் திற: $title'; + } + @override String get videoLessonTabNotes => 'குறிப்புகள்'; @@ -2918,50 +2923,7 @@ class AppLocalizationsTa extends AppLocalizations { 'பதிவிறக்கம் செய்யப்பட்ட இந்த தேர்வை அழிக்க வேண்டுமா?'; @override - String get aiSupportTitle => 'AI ஆதரவு'; - - @override - String aiSupportGreeting(String userName) { - return 'வணக்கம் $userName 👋'; - } - - @override - String get aiSupportQuickActions => 'விரைவான செயல்கள்'; - - @override - String get aiSupportAskDoubtTitle => 'சந்தேகம் கேளுங்கள்'; - - @override - String get aiSupportAskDoubtSubtitle => - 'உங்கள் கேள்வியைப் படமெடுக்கவும், பதிவேற்றவும் அல்லது தட்டச்சு செய்யவும்'; - - @override - String get aiSupportAskNowButton => 'இப்போது கேளுங்கள்'; - - @override - String get aiSupportAiExamTitle => 'AI தேர்வு'; - - @override - String get aiSupportAiExamSubtitle => - 'பலவீனமான தலைப்புகள் அல்லது அத்தியாயங்களின் அடிப்படையில் AI பயிற்சி தேர்வுகளை உருவாக்கவும்'; - - @override - String get aiSupportCreateAiExamButton => 'AI தேர்வை உருவாக்கு'; - - @override - String get aiSupportRecentHelp => 'சமீபத்திய உதவி'; - - @override - String get aiSupportViewAll => 'அனைத்தையும் காண்க'; - - @override - String get aiSupportNoRecentDoubts => 'சமீபத்திய AI சந்தேகங்கள் இல்லை.'; - - @override - String get aiSupportStatusAnswered => 'பதிலளிக்கப்பட்டது'; - - @override - String get aiSupportStatusProcessing => 'செயலாக்கப்படுகிறது'; + String get aiSupportTitle => 'AI அரட்டை'; @override String get aiSupportAskingAi => 'AI-இடம் கேட்கிறது'; @@ -3218,4 +3180,42 @@ class AppLocalizationsTa extends AppLocalizations { @override String get teamsMissingJoinLink => 'சந்திப்பு இணைப்பு காணவில்லை'; + + @override + String get aiStudyCompanionTitle => 'உங்கள் AI கற்றல் துணை'; + + @override + String get aiWelcomeSubtitle => + 'கேள்விகள் கேளுங்கள், கருத்துக்களைப் புரிந்து கொள்ளுங்கள்,\nமேலும் வேகமாக கற்றுக்கொள்ளுங்கள்.'; + + @override + String get aiStartNewChat => 'புதிய அரட்டையைத் தொடங்கு'; + + @override + String get aiRecentChatsHeader => 'சமீபத்திய அரட்டைகள்'; + + @override + String get aiViewAllRecentChats => 'அனைத்தையும் காட்டு'; + + @override + String get aiNewChatHeader => 'புதிய அரட்டை'; + + @override + String get aiComposerGreeting => + 'இன்று நான் உங்களுக்கு எவ்வாறு உதவ முடியும்?'; + + @override + String get aiChatHistoryTitle => 'அரட்டை வரலாறு'; + + @override + String get aiComposerPlaceholder => 'எது வேண்டுமானாலும் கேளுங்கள்...'; + + @override + String get aiComposerAttachImage => 'படத்தை இணைக்கவும்'; + + @override + String get aiComposerTakePhoto => 'புகைப்படம் எடுக்கவும்'; + + @override + String get aiComposerSendMessage => 'செய்தி அனுப்பவும்'; } diff --git a/packages/core/lib/l10n/app_ar.arb b/packages/core/lib/l10n/app_ar.arb index 0bb1f2939..56f70400f 100644 --- a/packages/core/lib/l10n/app_ar.arb +++ b/packages/core/lib/l10n/app_ar.arb @@ -306,6 +306,7 @@ "navigationPrevious": "الدرس السابق", "navigationNext": "الدرس التالي", "openDetailedLesson": "افتح الدرس: {title}", + "openChatSession": "افتح الدردشة: {title}", "videoLessonTabNotes": "ملاحظات", "videoLessonTabTranscript": "النص", "videoLessonSyncToVideo": "مزامنة مع الفيديو", @@ -1092,27 +1093,7 @@ "deleteExamTitle": "هل تريد حذف الامتحان؟", "deleteExamConfirmationMessage": "هل أنت متأكد أنك تريد حذف هذا الامتحان المنزل؟", - "aiSupportTitle": "دعم الذكاء الاصطناعي", - "aiSupportGreeting": "مرحباً {userName} 👋", - "@aiSupportGreeting": { - "placeholders": { - "userName": { - "type": "String" - } - } - }, - "aiSupportQuickActions": "إجراءات سريعة", - "aiSupportAskDoubtTitle": "اسأل شكاً", - "aiSupportAskDoubtSubtitle": "التقط أو حمّل أو اكتب سؤالك", - "aiSupportAskNowButton": "اسأل الآن", - "aiSupportAiExamTitle": "امتحان الذكاء الاصطناعي", - "aiSupportAiExamSubtitle": "قم بإنشاء امتحانات تدريبية بناءً على المواضيع الضعيفة", - "aiSupportCreateAiExamButton": "إنشاء امتحان", - "aiSupportRecentHelp": "المساعدة الأخيرة", - "aiSupportViewAll": "عرض الكل", - "aiSupportNoRecentDoubts": "لا توجد شكوك حديثة.", - "aiSupportStatusAnswered": "تم الرد", - "aiSupportStatusProcessing": "قيد المعالجة", + "aiSupportTitle": "دردشة الذكاء الاصطناعي", "aiSupportAskingAi": "جاري سؤال الذكاء الاصطناعي", "contentAccessEnded": "لقد انتهى وصولك إلى هذا المحتوى!", "accessExpired": "انتهت صلاحية الوصول", @@ -1206,5 +1187,17 @@ "liveStreamJoinFailed": "فشل الانضمام إلى الاجتماع. يُرجى المحاولة مرة أخرى.", "teamsPermissionRequired": "يلزم الوصول إلى الكاميرا والميكروفون. قم بتمكينها في الإعدادات للانضمام.", "teamsJoinMeetingLoading": "جارِ الانضمام إلى الاجتماع...", - "teamsMissingJoinLink": "رابط الاجتماع مفقود" + "teamsMissingJoinLink": "رابط الاجتماع مفقود", + "aiStudyCompanionTitle": "رفيق الدراسة بالذكاء الاصطناعي الخاص بك", + "aiWelcomeSubtitle": "اسأل أسئلة، وافهم المفاهيم،\nوتعلم بشكل أسرع.", + "aiStartNewChat": "بدء محادثة جديدة", + "aiRecentChatsHeader": "المحادثات الأخيرة", + "aiViewAllRecentChats": "عرض الكل", + "aiNewChatHeader": "محادثة جديدة", + "aiComposerGreeting": "كيف يمكنني مساعدتك اليوم؟", + "aiChatHistoryTitle": "سجل المحادثات", + "aiComposerPlaceholder": "اسأل عن أي شيء...", + "aiComposerAttachImage": "إرفاق صورة", + "aiComposerTakePhoto": "التقاط صورة", + "aiComposerSendMessage": "إرسال رسالة" } diff --git a/packages/core/lib/l10n/app_en.arb b/packages/core/lib/l10n/app_en.arb index b83fb7723..f921d5e4c 100644 --- a/packages/core/lib/l10n/app_en.arb +++ b/packages/core/lib/l10n/app_en.arb @@ -454,6 +454,14 @@ } } }, + "openChatSession": "Open chat: {title}", + "@openChatSession": { + "placeholders": { + "title": { + "type": "String" + } + } + }, "videoLessonTabNotes": "Notes", "videoLessonTabTranscript": "Transcript", "videoLessonSyncToVideo": "Sync to Video", @@ -1429,27 +1437,7 @@ "deleteExamTitle": "Delete Exam?", "deleteExamConfirmationMessage": "Are you sure you want to delete this downloaded exam?", - "aiSupportTitle": "AI Support", - "aiSupportGreeting": "Hi {userName} 👋", - "@aiSupportGreeting": { - "placeholders": { - "userName": { - "type": "String" - } - } - }, - "aiSupportQuickActions": "QUICK ACTIONS", - "aiSupportAskDoubtTitle": "Ask a Doubt", - "aiSupportAskDoubtSubtitle": "Snap, upload or type your question", - "aiSupportAskNowButton": "Ask Now", - "aiSupportAiExamTitle": "AI Exam", - "aiSupportAiExamSubtitle": "Create AI practice exams based on weak topics or chapters", - "aiSupportCreateAiExamButton": "Create AI Exam", - "aiSupportRecentHelp": "RECENT HELP", - "aiSupportViewAll": "View All", - "aiSupportNoRecentDoubts": "No recent AI doubts.", - "aiSupportStatusAnswered": "Answered", - "aiSupportStatusProcessing": "Processing", + "aiSupportTitle": "AI Chat", "aiSupportAskingAi": "ASKING AI", "contentAccessEnded": "Your access to this content has ended!", "accessExpired": "Access expired", @@ -1566,5 +1554,17 @@ "liveStreamJoinFailed": "Failed to join the meeting. Please try again.", "teamsPermissionRequired": "Camera and microphone access is required. Enable them in Settings to join.", "teamsJoinMeetingLoading": "Joining meeting...", - "teamsMissingJoinLink": "Meeting link is missing" + "teamsMissingJoinLink": "Meeting link is missing", + "aiStudyCompanionTitle": "Your AI study companion", + "aiWelcomeSubtitle": "Ask questions, understand concepts,\nand learn faster.", + "aiStartNewChat": "Start new chat", + "aiRecentChatsHeader": "Recent chats", + "aiViewAllRecentChats": "View All", + "aiNewChatHeader": "New Chat", + "aiComposerGreeting": "How can I help you today?", + "aiChatHistoryTitle": "Chat History", + "aiComposerPlaceholder": "Ask anything...", + "aiComposerAttachImage": "Attach image", + "aiComposerTakePhoto": "Take photo", + "aiComposerSendMessage": "Send message" } diff --git a/packages/core/lib/l10n/app_ml.arb b/packages/core/lib/l10n/app_ml.arb index 3c8aba578..e07a96d71 100644 --- a/packages/core/lib/l10n/app_ml.arb +++ b/packages/core/lib/l10n/app_ml.arb @@ -306,6 +306,7 @@ "navigationPrevious": "മുൻപത്തെ പാഠം", "navigationNext": "അടുത്ത പാഠം", "openDetailedLesson": "പാഠം തുറക്കുക: {title}", + "openChatSession": "ചാറ്റ് തുറക്കുക: {title}", "videoLessonTabNotes": "കുറിപ്പുകൾ", "videoLessonTabTranscript": "ട്രാൻസ്ക്രിപ്റ്റ്", "videoLessonSyncToVideo": "വീഡിയോയുമായി സമന്വയിപ്പിക്കുക", @@ -1092,27 +1093,7 @@ "deleteExamTitle": "പരീക്ഷ ഇല്ലാതാക്കണോ?", "deleteExamConfirmationMessage": "ഈ ഡൗൺലോഡ് ചെയ്ത പരീക്ഷ ഇല്ലാതാക്കണമെന്ന് ഉറപ്പാണോ?", - "aiSupportTitle": "AI പിന്തുണ", - "aiSupportGreeting": "നമസ്കാരം {userName} 👋", - "@aiSupportGreeting": { - "placeholders": { - "userName": { - "type": "String" - } - } - }, - "aiSupportQuickActions": "പെട്ടെന്നുള്ള പ്രവർത്തനങ്ങൾ", - "aiSupportAskDoubtTitle": "സംശയം ചോദിക്കുക", - "aiSupportAskDoubtSubtitle": "ചോദ്യം ഫോട്ടോ എടുക്കുകയോ അപ്‌ലോഡ് ചെയ്യുകയോ ടൈപ്പ് ചെയ്യുകയോ ചെയ്യുക", - "aiSupportAskNowButton": "ഇപ്പോൾ ചോദിക്കുക", - "aiSupportAiExamTitle": "AI പരീക്ഷ", - "aiSupportAiExamSubtitle": "ദുർബലമായ വിഷയങ്ങളെ അടിസ്ഥാനമാക്കി AI പരീക്ഷകൾ സൃഷ്ടിക്കുക", - "aiSupportCreateAiExamButton": "AI പരീക്ഷ സൃഷ്ടിക്കുക", - "aiSupportRecentHelp": "സമീപകാല സഹായം", - "aiSupportViewAll": "എല്ലാം കാണുക", - "aiSupportNoRecentDoubts": "സമീപകാല AI സംശയങ്ങളൊന്നുമില്ല.", - "aiSupportStatusAnswered": "മറുപടി നൽകി", - "aiSupportStatusProcessing": "പ്രോസസ്സ് ചെയ്യുന്നു", + "aiSupportTitle": "AI ചാറ്റ്", "aiSupportAskingAi": "AI-യോട് ചോദിക്കുന്നു", "contentAccessEnded": "ഈ ഉള്ളടക്കത്തിലേക്കുള്ള നിങ്ങളുടെ ആക്‌സസ് അവസാനിച്ചു!", "accessExpired": "ആക്‌സസ് കാലഹരണപ്പെട്ടു", @@ -1205,6 +1186,18 @@ "liveStreamStatusLive": "ലൈവ്", "liveStreamJoinFailed": "മീറ്റിംഗിൽ ചേരുന്നതിൽ പരാജയപ്പെട്ടു. വീണ്ടും ശ്രമിക്കുക.", "teamsPermissionRequired": "ക്യാമറ, മൈക്രോഫോൺ ആക്സസ് ആവശ്യമാണ്. ചേരുന്നതിന് ക്രമീകരണങ്ങളിൽ അവ പ്രവർത്തനക്ഷമമാക്കുക.", - "teamsJoinMeetingLoading": "മീറ്റിംഗിൽ ചേരുന്നു...", - "teamsMissingJoinLink": "മീറ്റിംഗ് ലിങ്ക് ലഭ്യമല്ല" + "teamsJoinMeetingLoading": "മീറ്റിംഗിൻ ചേരുന്നു...", + "teamsMissingJoinLink": "മീറ്റിംഗ് ലിങ്ക് ലഭ്യമല്ല", + "aiStudyCompanionTitle": "നിങ്ങളുടെ AI പഠന സഹായി", + "aiWelcomeSubtitle": "ചോദ്യങ്ങൾ ചോദിക്കുക, ആശയങ്ങൾ മനസ്സിലാക്കുക,\nകൂടുതൽ വേഗത്തിൽ പഠിക്കുക.", + "aiStartNewChat": "പുതിയ ചാറ്റ് ആരംഭിക്കുക", + "aiRecentChatsHeader": "സമീപകാല ചാറ്റുകൾ", + "aiViewAllRecentChats": "എല്ലാം കാണുക", + "aiNewChatHeader": "പുതിയ ചാറ്റ്", + "aiComposerGreeting": "ഇന്ന് ഞാൻ നിങ്ങളെ എങ്ങനെ സഹായിക്കണം?", + "aiChatHistoryTitle": "ചാറ്റ് ചരിത്രം", + "aiComposerPlaceholder": "എന്തും ചോദിക്കൂ...", + "aiComposerAttachImage": "ചിത്രം ചേർക്കുക", + "aiComposerTakePhoto": "ഫോട്ടോ എടുക്കുക", + "aiComposerSendMessage": "സന്ദേശം അയക്കുക" } diff --git a/packages/core/lib/l10n/app_ta.arb b/packages/core/lib/l10n/app_ta.arb index 8afa1b987..c6cae3d95 100644 --- a/packages/core/lib/l10n/app_ta.arb +++ b/packages/core/lib/l10n/app_ta.arb @@ -447,6 +447,14 @@ } } }, + "openChatSession": "அரட்டையைத் திற: {title}", + "@openChatSession": { + "placeholders": { + "title": { + "type": "String" + } + } + }, "videoLessonTabNotes": "குறிப்புகள்", "videoLessonTabTranscript": "டிரான்ஸ்கிரிப்ட்", "videoLessonSyncToVideo": "வீடியோவுடன் ஒத்திசை", @@ -1326,27 +1334,7 @@ "deleteExamTitle": "தேர்வை அழிக்க வேண்டுமா?", "deleteExamConfirmationMessage": "பதிவிறக்கம் செய்யப்பட்ட இந்த தேர்வை அழிக்க வேண்டுமா?", - "aiSupportTitle": "AI ஆதரவு", - "aiSupportGreeting": "வணக்கம் {userName} 👋", - "@aiSupportGreeting": { - "placeholders": { - "userName": { - "type": "String" - } - } - }, - "aiSupportQuickActions": "விரைவான செயல்கள்", - "aiSupportAskDoubtTitle": "சந்தேகம் கேளுங்கள்", - "aiSupportAskDoubtSubtitle": "உங்கள் கேள்வியைப் படமெடுக்கவும், பதிவேற்றவும் அல்லது தட்டச்சு செய்யவும்", - "aiSupportAskNowButton": "இப்போது கேளுங்கள்", - "aiSupportAiExamTitle": "AI தேர்வு", - "aiSupportAiExamSubtitle": "பலவீனமான தலைப்புகள் அல்லது அத்தியாயங்களின் அடிப்படையில் AI பயிற்சி தேர்வுகளை உருவாக்கவும்", - "aiSupportCreateAiExamButton": "AI தேர்வை உருவாக்கு", - "aiSupportRecentHelp": "சமீபத்திய உதவி", - "aiSupportViewAll": "அனைத்தையும் காண்க", - "aiSupportNoRecentDoubts": "சமீபத்திய AI சந்தேகங்கள் இல்லை.", - "aiSupportStatusAnswered": "பதிலளிக்கப்பட்டது", - "aiSupportStatusProcessing": "செயலாக்கப்படுகிறது", + "aiSupportTitle": "AI அரட்டை", "aiSupportAskingAi": "AI-இடம் கேட்கிறது", "contentAccessEnded": "இந்த உள்ளடக்கத்திற்கான உங்கள் அணுகல் முடிந்துவிட்டது!", "accessExpired": "அணுகல் காலாவதியானது", @@ -1440,5 +1428,17 @@ "liveStreamJoinFailed": "கூட்டத்தில் சேர முடியவில்லை. மீண்டும் முயற்சிக்கவும்.", "teamsPermissionRequired": "சேர, கேமரா மற்றும் மைக்ரோஃபோன் அணுகல் தேவை. அமைப்புகளில் அவற்றை இயக்கவும்.", "teamsJoinMeetingLoading": "கூட்டத்தில் சேர்கிறோம்...", - "teamsMissingJoinLink": "சந்திப்பு இணைப்பு காணவில்லை" + "teamsMissingJoinLink": "சந்திப்பு இணைப்பு காணவில்லை", + "aiStudyCompanionTitle": "உங்கள் AI கற்றல் துணை", + "aiWelcomeSubtitle": "கேள்விகள் கேளுங்கள், கருத்துக்களைப் புரிந்து கொள்ளுங்கள்,\nமேலும் வேகமாக கற்றுக்கொள்ளுங்கள்.", + "aiStartNewChat": "புதிய அரட்டையைத் தொடங்கு", + "aiRecentChatsHeader": "சமீபத்திய அரட்டைகள்", + "aiViewAllRecentChats": "அனைத்தையும் காட்டு", + "aiNewChatHeader": "புதிய அரட்டை", + "aiComposerGreeting": "இன்று நான் உங்களுக்கு எவ்வாறு உதவ முடியும்?", + "aiChatHistoryTitle": "அரட்டை வரலாறு", + "aiComposerPlaceholder": "எது வேண்டுமானாலும் கேளுங்கள்...", + "aiComposerAttachImage": "படத்தை இணைக்கவும்", + "aiComposerTakePhoto": "புகைப்படம் எடுக்கவும்", + "aiComposerSendMessage": "செய்தி அனுப்பவும்" } diff --git a/packages/core/lib/screens/ai_chat_history_screen.dart b/packages/core/lib/screens/ai_chat_history_screen.dart new file mode 100644 index 000000000..9e0c1dc86 --- /dev/null +++ b/packages/core/lib/screens/ai_chat_history_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../core.dart'; +import '../data/ai_chat_mock_data.dart'; + +class AiChatHistoryScreen extends ConsumerWidget { + const AiChatHistoryScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final design = Design.of(context); + final l10n = L10n.of(context); + + return Container( + color: design.colors.card, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppHeader( + title: l10n.aiChatHistoryTitle, + leading: AppBackButton(onTap: () => context.pop()), + ), + Expanded( + child: AppSemantics.scrollableList( + itemCount: mockChatSessions.length, + label: l10n.aiChatHistoryTitle, + child: AppScroll( + padding: EdgeInsets.symmetric( + horizontal: design.spacing.md, + vertical: design.spacing.lg, + ), + children: [ + ...mockChatSessions.map((session) { + final isLast = session == mockChatSessions.last; + final lastHumanMessage = session.messages + .lastWhere((msg) => msg.role == MessageRole.user) + .content; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _HistoryItem( + sessionId: session.id, + title: session.title, + lastMessage: lastHumanMessage, + timestamp: DateFormatter.formatTimeAgo( + session.modifiedAt, + ), + ), + if (!isLast) SizedBox(height: design.spacing.xs), + ], + ); + }), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _HistoryItem extends StatelessWidget { + const _HistoryItem({ + required this.sessionId, + required this.title, + this.lastMessage, + required this.timestamp, + }); + + final String sessionId; + final String title; + final String? lastMessage; + final String timestamp; + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); + + return AppSemantics.button( + label: l10n.openChatSession(title), + onTap: () => context.push('/ai/chat?id=$sessionId'), + child: AppCard( + padding: EdgeInsets.all(design.spacing.md), + onTap: () => context.push('/ai/chat?id=$sessionId'), + child: Row( + children: [ + SizedBox(width: design.spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.cardTitle( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: design.spacing.xs), + if (lastMessage != null) ...[ + AppText.cardSubtitle( + lastMessage!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: design.spacing.xs), + ], + AppText.cardCaption(timestamp), + ], + ), + ), + SizedBox(width: design.spacing.xs), + Icon( + LucideIcons.chevronRight, + color: design.colors.textTertiary, + size: 18.0, + ), + ], + ), + ), + ); + } +} diff --git a/packages/core/lib/screens/ai_chat_immersive_screen.dart b/packages/core/lib/screens/ai_chat_immersive_screen.dart new file mode 100644 index 000000000..2daa72c94 --- /dev/null +++ b/packages/core/lib/screens/ai_chat_immersive_screen.dart @@ -0,0 +1,384 @@ +import 'dart:async'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../core.dart'; +import '../data/ai_chat_mock_data.dart'; + +class AiChatImmersiveScreen extends ConsumerStatefulWidget { + const AiChatImmersiveScreen({super.key}); + + @override + ConsumerState createState() => + _AiChatImmersiveScreenState(); +} + +class _AiChatImmersiveScreenState extends ConsumerState { + late final ScrollController _scrollController; + final List _messages = []; + bool _initialized = false; + bool _isTyping = false; + Timer? _typewriterTimer; + + @override + void initState() { + super.initState(); + _scrollController = ScrollController(); + } + + @override + void dispose() { + _scrollController.dispose(); + _typewriterTimer?.cancel(); + super.dispose(); + } + + void _scrollToBottom({bool animate = true}) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (_scrollController.hasClients) { + final design = Design.of(context); + final shouldAnimate = + animate && MotionPreferences.shouldAnimate(context); + if (shouldAnimate) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: design.motion.normal, + curve: design.motion.easeOut, + ); + } else { + _scrollController.jumpTo(_scrollController.position.maxScrollExtent); + } + } + }); + } + + void _onSendMessage(String text) { + setState(() { + _messages.add( + AiChatMessage( + content: text, + timestamp: DateTime.now(), + role: MessageRole.user, + ), + ); + _isTyping = true; + }); + _scrollToBottom(); + + final design = Design.of(context); + final shouldAnimate = MotionPreferences.shouldAnimate(context); + + const reply = + "I am an **AI assistant** designed for:\n\n- 🙋‍♂️ **Answering doubts** in real-time\n- 🧠 **Clearing concepts** with examples\n- 🚀 **Learning faster** and more efficiently"; + + if (!shouldAnimate) { + setState(() { + _isTyping = false; + _messages.add( + AiChatMessage( + content: reply, + timestamp: DateTime.now(), + role: MessageRole.ai, + ), + ); + }); + _scrollToBottom(); + return; + } + + // Show 3 dots bouncing for 1500ms + final delay = design.motion.verySlow * 2.5; + Future.delayed(delay, () { + if (!mounted) return; + setState(() { + _isTyping = false; + _messages.add( + AiChatMessage( + content: '', + timestamp: DateTime.now(), + role: MessageRole.ai, + ), + ); + }); + _scrollToBottom(); + + final replyChars = reply.characters; + int charIndex = 0; + final interval = design.motion.fast ~/ 5; // 30ms character typing speed + _typewriterTimer = Timer.periodic(interval, (timer) { + if (!mounted) { + timer.cancel(); + return; + } + if (charIndex < replyChars.length) { + charIndex++; + setState(() { + final lastIndex = _messages.length - 1; + _messages[lastIndex] = AiChatMessage( + content: replyChars.take(charIndex).toString(), + timestamp: _messages[lastIndex].timestamp, + role: MessageRole.ai, + ); + }); + _scrollToBottom(); + } else { + timer.cancel(); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); + + // Load initial mock messages once + if (!_initialized) { + final state = GoRouterState.of(context); + final sessionId = state.uri.queryParameters['id']; + if (sessionId != null) { + final session = mockChatSessions.firstWhere( + (s) => s.id == sessionId, + orElse: () => mockChatSessions.first, + ); + _messages.addAll(session.messages); + } + _initialized = true; + _scrollToBottom(animate: false); + } + + final hasMessages = _messages.isNotEmpty || _isTyping; + final state = GoRouterState.of(context); + final sessionId = state.uri.queryParameters['id']; + final session = sessionId != null + ? mockChatSessions.firstWhere( + (s) => s.id == sessionId, + orElse: () => mockChatSessions.first, + ) + : null; + + final headerTitle = session != null ? session.title : l10n.aiNewChatHeader; + + return Container( + color: design.colors.card, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppHeader( + title: headerTitle, + leading: AppBackButton(onTap: () => context.pop()), + actions: [ + AppSemantics.button( + label: l10n.aiChatHistoryTitle, + onTap: () => context.push('/ai/history'), + child: AppFocusable( + onTap: () => context.push('/ai/history'), + child: Padding( + padding: const EdgeInsets.only( + top: 6, + ), // Optical alignment matching AppBackButton + child: Icon( + LucideIcons.history, + color: design.colors.textSecondary, + size: design.iconSize.action, + ), + ), + ), + ), + ], + ), + Expanded( + child: hasMessages + ? AppSemantics.scrollableList( + itemCount: _messages.length, + label: l10n.aiSupportTitle, + child: AppScroll( + controller: _scrollController, + padding: EdgeInsets.symmetric( + horizontal: design.spacing.screenPadding, + vertical: design.spacing.lg, + ), + children: [ + ..._messages.map((message) { + final isUser = message.role == MessageRole.user; + if (isUser) { + return Align( + alignment: Alignment.centerRight, + child: LayoutBuilder( + builder: (context, constraints) { + return Container( + margin: EdgeInsets.only( + bottom: design.spacing.md, + ), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.75, + ), + padding: EdgeInsets.all(design.spacing.sm), + decoration: BoxDecoration( + color: design.colors.primary, + borderRadius: BorderRadius.circular(16.0), + ), + child: AppText.bodySmall( + message.content, + color: design.colors.textInverse, + ), + ); + }, + ), + ); + } else { + return Container( + margin: EdgeInsets.only( + bottom: design.spacing.lg, + ), + child: AppMarkdown(data: message.content), + ); + } + }), + if (_isTyping) + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only( + bottom: design.spacing.lg, + ), + child: const _TypingIndicator(), + ), + ), + ], + ), + ) + : Center( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric( + horizontal: design.spacing.md, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + LucideIcons.bot, + size: 64.0, + color: design.colors.primary, + ), + SizedBox(height: design.spacing.md), + AppText.title( + l10n.aiComposerGreeting, + textAlign: TextAlign.center, + ), + SizedBox(height: design.spacing.lg), + AiComposer(onSend: _onSendMessage), + ], + ), + ), + ), + ), + if (hasMessages) + SafeArea( + top: false, + left: false, + right: false, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: design.spacing.md, + vertical: design.spacing.lg, + ), + child: AiComposer(onSend: _onSendMessage), + ), + ), + ], + ), + ); + } +} + +class _TypingIndicator extends StatefulWidget { + const _TypingIndicator(); + + @override + State<_TypingIndicator> createState() => _TypingIndicatorState(); +} + +class _TypingIndicatorState extends State<_TypingIndicator> + with TickerProviderStateMixin { + late List _controllers; + late List> _animations; + bool _animationStarted = false; + + @override + void initState() { + super.initState(); + _controllers = List.generate(3, (index) { + return AnimationController(vsync: this); + }); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final design = Design.of(context); + + for (var controller in _controllers) { + controller.duration = design.motion.normal; + } + + _animations = _controllers.map((controller) { + return Tween(begin: 0.0, end: -8.0).animate( + CurvedAnimation(parent: controller, curve: design.motion.easeInOut), + ); + }).toList(); + + if (!_animationStarted) { + _animationStarted = true; + _startAnimation(); + } + } + + void _startAnimation() async { + if (!MotionPreferences.shouldAnimate(context)) return; + final design = Design.of(context); + for (int i = 0; i < 3; i++) { + if (!mounted) return; + _controllers[i].repeat(reverse: true); + await Future.delayed(design.motion.fast); + } + } + + @override + void dispose() { + for (var controller in _controllers) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final isAnimated = MotionPreferences.shouldAnimate(context); + return Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (index) { + return AnimatedBuilder( + animation: _animations[index], + builder: (context, child) { + final yOffset = isAnimated ? _animations[index].value : 0.0; + return Transform.translate( + offset: Offset(0, yOffset), + child: Container( + width: 8.0, + height: 8.0, + margin: EdgeInsets.symmetric(horizontal: design.spacing.xs / 2), + decoration: BoxDecoration( + color: design.colors.textSecondary, + shape: BoxShape.circle, + ), + ), + ); + }, + ); + }), + ); + } +} diff --git a/packages/core/lib/screens/ai_screen.dart b/packages/core/lib/screens/ai_screen.dart index b9fe4a240..21271b0c7 100644 --- a/packages/core/lib/screens/ai_screen.dart +++ b/packages/core/lib/screens/ai_screen.dart @@ -1,29 +1,18 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core.dart'; -import '../data/data.dart'; +import '../data/ai_chat_mock_data.dart'; class AiScreen extends ConsumerWidget { - final VoidCallback onAskAiPressed; - final VoidCallback onViewAllDoubtsPressed; - final void Function(String doubtId) onDoubtTapped; - - const AiScreen({ - super.key, - required this.onAskAiPressed, - required this.onViewAllDoubtsPressed, - required this.onDoubtTapped, - }); + const AiScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final design = Design.of(context); final l10n = L10n.of(context); - final user = ref.watch(userProvider).valueOrNull; - final userName = user?.name; return Container( - color: design.colors.surface, + color: design.colors.card, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -31,16 +20,13 @@ class AiScreen extends ConsumerWidget { Expanded( child: AppScroll( padding: EdgeInsets.symmetric( - horizontal: design.spacing.screenPadding, + horizontal: design.spacing.md, vertical: design.spacing.lg, ), children: [ - _buildGreeting(design, l10n, userName), - SizedBox(height: design.spacing.xl), - _buildQuickActions(context, design, l10n), - SizedBox(height: design.spacing.xl), - _buildRecentHelp(context, ref, design, l10n), - SizedBox(height: design.spacing.xl), + const _WelcomeSection(), + SizedBox(height: design.spacing.md), + const _RecentHelpSection(), ], ), ), @@ -48,303 +34,187 @@ class AiScreen extends ConsumerWidget { ), ); } +} + +class _WelcomeSection extends StatelessWidget { + const _WelcomeSection(); + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); - Widget _buildGreeting( - DesignConfig design, - AppLocalizations l10n, - String? userName, - ) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - LucideIcons.sparkles, - color: design.colors.accent1, - size: design.iconSize.md, - ), - SizedBox(width: design.spacing.sm), - AppText.headline( - l10n.aiSupportGreeting(userName ?? ''), - color: design.colors.textPrimary, + Align( + alignment: Alignment.topCenter, + child: SizedBox( + height: 120.0, + child: OverflowBox( + maxHeight: 250.0, + minHeight: 250.0, + child: Image.asset( + 'assets/images/ai_bot.png', + width: 250.0, + height: 250.0, + ), ), - ], + ), ), - ], - ); - } - - Widget _buildQuickActions( - BuildContext context, - DesignConfig design, - AppLocalizations l10n, - ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppText.labelBold( - l10n.aiSupportQuickActions, + SizedBox(height: design.spacing.sm), + AppSemantics.header( + label: l10n.aiStudyCompanionTitle, + child: AppText.title( + l10n.aiStudyCompanionTitle, + textAlign: TextAlign.center, + ), + ), + SizedBox(height: design.spacing.sm), + AppText.bodySmall( + l10n.aiWelcomeSubtitle, + textAlign: TextAlign.center, color: design.colors.textSecondary, ), SizedBox(height: design.spacing.md), - - _buildQuickActionCard( - design: design, - accentColor: design.colors.accent4, - cardIcon: LucideIcons.messageCircle, - title: l10n.aiSupportAskDoubtTitle, - subtitle: l10n.aiSupportAskDoubtSubtitle, - buttonLabel: l10n.aiSupportAskNowButton, - buttonIcon: LucideIcons.send, - onPressed: onAskAiPressed, + AppButton.primary( + label: l10n.aiStartNewChat, + onPressed: () => context.push('/ai/chat'), ), ], ); } +} - Widget _buildQuickActionCard({ - required DesignConfig design, - required Color accentColor, - required IconData cardIcon, - required String title, - required String subtitle, - required String buttonLabel, - required IconData buttonIcon, - VoidCallback? onPressed, - }) { - return Container( - width: double.infinity, - padding: EdgeInsets.all(design.spacing.lg), - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(design.radius.xl), - border: Border.all(color: accentColor.withValues(alpha: 0.3)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.all(design.spacing.md), - decoration: BoxDecoration( - color: accentColor.withValues(alpha: 0.15), - shape: BoxShape.circle, +class _HistoryCard extends StatelessWidget { + const _HistoryCard({ + required this.sessionId, + required this.title, + this.lastMessage, + required this.timestamp, + }); + + final String sessionId; + final String title; + final String? lastMessage; + final String timestamp; + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); + + return AppSemantics.button( + label: l10n.openChatSession(title), + onTap: () => context.push('/ai/chat?id=$sessionId'), + child: AppCard( + padding: EdgeInsets.all(design.spacing.md), + onTap: () => context.push('/ai/chat?id=$sessionId'), + child: Row( + children: [ + SizedBox(width: design.spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.cardTitle( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: design.spacing.xs), + if (lastMessage != null) ...[ + AppText.cardSubtitle( + lastMessage!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: design.spacing.xs), + ], + AppText.cardCaption(timestamp), + ], + ), ), - child: Icon(cardIcon, color: accentColor, size: design.iconSize.lg), - ), - SizedBox(width: design.spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppText.cardTitle(title), - SizedBox(height: design.spacing.xs), - AppText.cardSubtitle(subtitle), - SizedBox(height: design.spacing.md), - AppButton( - label: buttonLabel, - backgroundColor: accentColor, - foregroundColor: design.colors.textInverse, - leading: Icon(buttonIcon, size: design.iconSize.sm), - onPressed: onPressed, - ), - ], + SizedBox(width: design.spacing.xs), + Icon( + LucideIcons.chevronRight, + color: design.colors.textTertiary, + size: 18.0, ), - ), - ], + ], + ), ), ); } +} - Widget _buildRecentHelp( - BuildContext context, - WidgetRef ref, - DesignConfig design, - AppLocalizations l10n, - ) { - final recentDoubtsAsync = ref.watch(recentAiDoubtsProvider); +class _RecentHelpSection extends StatelessWidget { + const _RecentHelpSection(); + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - AppText.labelBold( - l10n.aiSupportRecentHelp, - color: design.colors.textSecondary, + AppSemantics.header( + label: l10n.aiRecentChatsHeader, + child: AppText.title(l10n.aiRecentChatsHeader), ), AppSemantics.button( - label: l10n.aiSupportViewAll, - onTap: onViewAllDoubtsPressed, - child: AppFocusable( - onTap: onViewAllDoubtsPressed, - child: AppText.labelSmall( - l10n.aiSupportViewAll, - color: design.colors.primary, + label: l10n.aiViewAllRecentChats, + onTap: () => context.push('/ai/history'), + child: GestureDetector( + onTap: () => context.push('/ai/history'), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: design.spacing.sm, + vertical: design.spacing.md, + ), + child: AppText.labelBold( + l10n.aiViewAllRecentChats, + color: design.colors.primary, + ), ), ), ), ], ), - SizedBox(height: design.spacing.md), - recentDoubtsAsync.when( - data: (doubtsList) { - final doubts = doubtsList.take(3).toList(); - if (doubts.isEmpty) { - return Padding( - padding: EdgeInsets.symmetric(vertical: design.spacing.xxxl), - child: Center( - child: AppText.body( - l10n.aiSupportNoRecentDoubts, - color: design.colors.textSecondary, + SizedBox(height: design.spacing.sm), + AppSemantics.scrollableList( + itemCount: mockChatSessions.length, + label: l10n.aiRecentChatsHeader, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: mockChatSessions.map((session) { + final isLast = session == mockChatSessions.last; + final lastHumanMessage = session.messages + .lastWhere((msg) => msg.role == MessageRole.user) + .content; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _HistoryCard( + sessionId: session.id, + title: session.title, + lastMessage: lastHumanMessage, + timestamp: DateFormatter.formatTimeAgo(session.modifiedAt), ), - ), + if (!isLast) SizedBox(height: design.spacing.xs), + ], ); - } - - return Column( - children: doubts.map((doubt) { - IconData statusIcon; - Color statusColor; - Color statusBg; - String statusText; - - switch (doubt.status) { - case DoubtStatus.resolved: - case DoubtStatus.closed: - statusIcon = LucideIcons.checkCircle2; - statusColor = design.statusColors.completed.foreground; - statusBg = design.statusColors.completed.background; - statusText = l10n.aiSupportStatusAnswered; - break; - case DoubtStatus.active: - case DoubtStatus.pending: - statusIcon = LucideIcons.loader; - statusColor = design.statusColors.upcoming.foreground; - statusBg = design.statusColors.upcoming.background; - statusText = l10n.aiSupportStatusProcessing; - break; - } - - return Padding( - padding: EdgeInsets.only(bottom: design.spacing.md), - child: AppSemantics.button( - label: doubt.title, - onTap: () => onDoubtTapped(doubt.id), - child: AppFocusable( - onTap: () => onDoubtTapped(doubt.id), - child: _buildHelpCard( - design: design, - icon: LucideIcons.messageCircleQuestionMark, - iconColor: design.colors.accent2, - title: doubt.title, - timestamp: doubt.createdHumanized ?? '', - statusText: statusText, - statusColor: statusColor, - statusBg: statusBg, - statusIcon: statusIcon, - ), - ), - ), - ); - }).toList(), - ); - }, - loading: () => const Center(child: AppLoadingIndicator()), - error: (_, _) => const SizedBox(), + }).toList(), + ), ), ], ); } - - Widget _buildHelpCard({ - required DesignConfig design, - required IconData icon, - required Color iconColor, - required String title, - required String timestamp, - required String statusText, - required Color statusColor, - required Color statusBg, - required IconData statusIcon, - }) { - return AppCard( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.all(design.spacing.sm), - decoration: BoxDecoration( - color: design.colors.surface, - borderRadius: BorderRadius.circular(design.radius.md), - ), - child: Icon(icon, color: iconColor, size: design.iconSize.md), - ), - SizedBox(width: design.spacing.md), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: AppText.body( - title, - color: design.colors.textPrimary, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - ), - SizedBox(width: design.spacing.sm), - Container( - padding: EdgeInsets.symmetric( - horizontal: design.spacing.sm, - vertical: design.spacing.xs, - ), - decoration: BoxDecoration( - color: statusBg, - borderRadius: BorderRadius.circular(design.radius.full), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - statusIcon, - size: design.iconSize.xs, - color: statusColor, - ), - SizedBox(width: 4), - AppText.labelSmall(statusText, color: statusColor), - ], - ), - ), - ], - ), - SizedBox(height: design.spacing.sm), - Row( - children: [ - Icon( - LucideIcons.clock, - size: design.iconSize.xs, - color: design.colors.textTertiary, - ), - SizedBox(width: 4), - AppText.caption( - timestamp, - color: design.colors.textTertiary, - ), - ], - ), - ], - ), - ), - ], - ), - ); - } } diff --git a/packages/core/lib/widgets/ai_composer.dart b/packages/core/lib/widgets/ai_composer.dart new file mode 100644 index 000000000..be590d0bb --- /dev/null +++ b/packages/core/lib/widgets/ai_composer.dart @@ -0,0 +1,155 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../core.dart'; + +class AiComposer extends ConsumerStatefulWidget { + const AiComposer({super.key, this.onSend}); + + final ValueChanged? onSend; + + @override + ConsumerState createState() => _AiComposerState(); +} + +class _AiComposerState extends ConsumerState { + late final TextEditingController _controller; + late final FocusNode _focusNode; + late final ScrollController _textScrollController; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _focusNode = FocusNode(); + _textScrollController = ScrollController(); + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + _textScrollController.dispose(); + super.dispose(); + } + + void _submit() { + final text = _controller.text.trim(); + if (text.isNotEmpty) { + widget.onSend?.call(text); + _controller.clear(); + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + final design = Design.of(context); + final l10n = L10n.of(context); + + return Container( + decoration: BoxDecoration( + color: design.colors.card, + borderRadius: design.radius.card, + border: Border.all(color: design.colors.border), + ), + padding: EdgeInsets.only( + left: design.spacing.sm, + right: design.spacing.sm, + top: design.spacing.sm, + bottom: design.spacing.xs, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: design.spacing.xs), + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 100.0), + child: RawScrollbar( + controller: _textScrollController, + thumbColor: design.colors.textTertiary.withValues(alpha: 0.5), + thickness: 4.0, + radius: const Radius.circular(2.0), + thumbVisibility: true, + child: Stack( + children: [ + if (_controller.text.isEmpty) + AppText.body( + l10n.aiComposerPlaceholder, + color: design.colors.textTertiary, + ), + EditableText( + controller: _controller, + focusNode: _focusNode, + scrollController: _textScrollController, + style: design.typography.body.copyWith( + color: design.colors.textPrimary, + ), + cursorColor: design.colors.primary, + backgroundCursorColor: const Color(0xFF000000), + keyboardType: TextInputType.multiline, + maxLines: null, + onChanged: (text) { + setState(() {}); + }, + ), + ], + ), + ), + ), + ), + SizedBox(height: design.spacing.xs / 2), + Row( + children: [ + AppIconButton( + icon: LucideIcons.image, + onTap: () {}, + accessibilityLabel: l10n.aiComposerAttachImage, + color: design.colors.textSecondary, + size: 20, + ), + AppIconButton( + icon: LucideIcons.camera, + onTap: () {}, + accessibilityLabel: l10n.aiComposerTakePhoto, + color: design.colors.textSecondary, + size: 20, + ), + const Spacer(), + AppSemantics.button( + label: l10n.aiComposerSendMessage, + onTap: _submit, + child: GestureDetector( + onTap: _submit, + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: 48.0, + height: 48.0, + child: Center( + child: Container( + width: 36.0, + height: 36.0, + decoration: BoxDecoration( + color: design.colors.primary, + shape: BoxShape.circle, + ), + child: Center( + child: Icon( + LucideIcons.send, + color: design.colors.textInverse, + size: 16.0, + ), + ), + ), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/core/lib/widgets/app_markdown.dart b/packages/core/lib/widgets/app_markdown.dart index 67d7a1816..902f3bd7f 100644 --- a/packages/core/lib/widgets/app_markdown.dart +++ b/packages/core/lib/widgets/app_markdown.dart @@ -23,7 +23,6 @@ class AppMarkdown extends StatelessWidget { final design = Design.of(context); final baseStyle = design.typography.body.copyWith( - color: design.colors.textSecondary, fontSize: 14, height: 1.5, ); @@ -32,10 +31,9 @@ class AppMarkdown extends StatelessWidget { data: data, selectable: selectable, onTapLink: onTapLink, - styleSheet: MarkdownStyleSheet.light().copyWith( + styleSheet: MarkdownStyleSheet.light(baseStyle: baseStyle).copyWith( textStyle: baseStyle, paragraphStyle: baseStyle, - linkStyle: baseStyle.copyWith(color: design.colors.accent2), h1Style: design.typography.headline.copyWith( color: design.colors.textPrimary, ), @@ -46,25 +44,56 @@ class AppMarkdown extends StatelessWidget { color: design.colors.textPrimary, fontWeight: FontWeight.w700, ), + h4Style: design.typography.body.copyWith( + color: design.colors.textPrimary, + ), + h5Style: baseStyle.copyWith(color: design.colors.textPrimary), + h6Style: baseStyle.copyWith(color: design.colors.textTertiary), + boldStyle: baseStyle.copyWith( + fontWeight: FontWeight.w700, + color: design.colors.textPrimary, + ), + italicStyle: baseStyle.copyWith(fontStyle: FontStyle.italic), + linkStyle: baseStyle.copyWith( + color: design.colors.accent2, + decoration: TextDecoration.none, + ), listBulletStyle: baseStyle.copyWith(color: design.colors.textTertiary), blockSpacing: design.spacing.md, listIndent: design.spacing.lg, + blockquoteDecoration: BoxDecoration( + color: design.colors.primary.withValues(alpha: 0.06), + border: Border( + left: BorderSide(color: design.colors.primary, width: 3.0), + ), + ), blockquotePadding: EdgeInsets.symmetric( horizontal: design.spacing.md, vertical: design.spacing.sm, ), - codeBlockPadding: EdgeInsets.all(design.spacing.sm), - tableCellPadding: EdgeInsets.all(design.spacing.sm), + blockquoteStyle: baseStyle.copyWith( + color: design.colors.textSecondary, + fontStyle: FontStyle.italic, + ), inlineCodeStyle: baseStyle.copyWith( - backgroundColor: design.colors.divider.withValues(alpha: 0.1), + backgroundColor: design.colors.divider.withValues(alpha: 0.15), fontFamily: 'monospace', fontSize: 13, color: design.colors.accent2, ), codeBlockDecoration: BoxDecoration( - color: design.colors.divider.withValues(alpha: 0.1), + color: design.colors.divider.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(design.spacing.sm), ), + codeBlockPadding: EdgeInsets.all(design.spacing.md), + tableHeaderStyle: baseStyle.copyWith( + color: design.colors.textPrimary, + fontWeight: FontWeight.w700, + ), + tableCellStyle: baseStyle, + tableCellPadding: EdgeInsets.all(design.spacing.sm), + tableHeaderDecoration: BoxDecoration(color: design.colors.surface), + tableBorder: TableBorder.all(color: design.colors.border, width: 1.0), ), ); } diff --git a/packages/courses/lib/widgets/lesson_detail/ai_tab.dart b/packages/courses/lib/widgets/lesson_detail/ai_tab.dart index 304516e45..70826edf3 100644 --- a/packages/courses/lib/widgets/lesson_detail/ai_tab.dart +++ b/packages/courses/lib/widgets/lesson_detail/ai_tab.dart @@ -161,6 +161,7 @@ class _AITabState extends ConsumerState void _scrollToBottom(DesignConfig design) { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; if (_scrollController.hasClients) { _scrollController.animateTo( _scrollController.position.maxScrollExtent, @@ -186,7 +187,7 @@ class _AITabState extends ConsumerState Expanded( child: AppSemantics.scrollableList( itemCount: _messages.length, - label: 'AI Chat Messages', + label: l10n.aiSupportTitle, child: ListView.builder( controller: _scrollController, padding: EdgeInsets.all(design.spacing.md), diff --git a/packages/testpress/lib/navigation/app_router.dart b/packages/testpress/lib/navigation/app_router.dart index 3dc4af454..24c521d79 100644 --- a/packages/testpress/lib/navigation/app_router.dart +++ b/packages/testpress/lib/navigation/app_router.dart @@ -54,7 +54,7 @@ enum NavTab { home('/home', 'Home', LucideIcons.home), study('/study', 'Study', LucideIcons.bookOpen), exams('/exams', 'Exam', LucideIcons.fileText), - ai('/ai', 'AI', LucideIcons.sparkles), + ai('/ai', 'AI', LucideIcons.bot), store('/store', 'Store', LucideIcons.store), info('/info', 'Info', LucideIcons.squarePlay), profile('/profile', 'Profile', LucideIcons.user); diff --git a/packages/testpress/lib/navigation/routes/ai_routes.dart b/packages/testpress/lib/navigation/routes/ai_routes.dart index 977846a33..7723b8c51 100644 --- a/packages/testpress/lib/navigation/routes/ai_routes.dart +++ b/packages/testpress/lib/navigation/routes/ai_routes.dart @@ -6,14 +6,21 @@ class AiRoutes { GoRoute( name: 'ai', path: '/ai', - builder: (context, state) => AiScreen( - onAskAiPressed: () => - context.push('/home/discussions/doubts/ask?isAskAi=true'), - onViewAllDoubtsPressed: () => - context.push('/home/discussions/doubts?filter=ai'), - onDoubtTapped: (doubtId) => - context.push('/home/discussions/doubts/$doubtId'), - ), + builder: (context, state) => const AiScreen(), + routes: [ + GoRoute( + name: 'ai_chat', + path: 'chat', + parentNavigatorKey: rootNavigatorKey, + builder: (context, state) => const AiChatImmersiveScreen(), + ), + GoRoute( + name: 'ai_history', + path: 'history', + parentNavigatorKey: rootNavigatorKey, + builder: (context, state) => const AiChatHistoryScreen(), + ), + ], ), ]; }