This commit is contained in:
Dmitry 2025-11-11 02:55:41 +03:00
parent 7c055f64f3
commit 9414005e2a
1467 changed files with 137138 additions and 7444 deletions

View file

@ -0,0 +1,10 @@
---
description: For testing and debugging
alwaysApply: false
---
When rule is mentioned:
1. Run mnemo_cards_web/complete_auto_debug.sh
2. Wait for it to complete
3. Read and analyse the report in mnemo_cards_web/debug_report
4. Fix the errors
5. Go to step 1

View file

@ -0,0 +1,64 @@
---
alwaysApply: false
---
# Title: Autonomy & Context-Budget Control
# Scope: project
# Objective: Run tasks end-to-end without pausing for questions; never exhaust context; recover state after long runs.
## Operating Mode
- Operate as an IMPLEMENTER, not an advisor. Execute changes directly in the codebase, run tests, and iterate until the task is completed.
- Do NOT ask for confirmation. If information is missing, make the safest reasonable assumption, record it in `workflow_state.md` under **ASSUMPTIONS**, and proceed.
- Only stop when:
1) all acceptance checks pass, or
2) an explicit denylisted command is required, or
3) a hard error cannot be bypassed without human credentials.
## Files for Long-Run State
- Maintain two small, bounded files at repo root:
- `project_config.md`: non-volatile goals, constraints, stack specifics, commands to run, and acceptance criteria.
- `workflow_state.md`: volatile loop state with **PLAN**, **NEXT_ACTIONS**, **ASSUMPTIONS**, **PROGRESS_LOG** (tail ≤ 200 lines), and **OPEN_ISSUES**.
- Always read from `project_config.md` first, then `workflow_state.md`. If absent, create them with reasonable defaults and proceed.
## Autonomous Loop (each cycle)
1) **Read**: Load only the smallest necessary snippets. Never paste whole files into chat.
2) **Plan**: Update `workflow_state.md` → **PLAN** and **NEXT_ACTIONS** (≤ 150 tokens combined).
3) **Act**:
- Apply file edits directly via the editor. Prefer targeted diffs and surgical refactors.
- Run fast checks (format, lint, typecheck, unit tests) as configured in `project_config.md`.
4) **Verify**:
- Parse outputs; extract only the essential signals back into `workflow_state.md` (**PROGRESS_LOG** summary ≤ 100 tokens per step).
- If failures occur, append a brief root-cause hypothesis and loop.
5) **Trim**:
- Aggressively prune prior chat messages by relying on the two state files instead of conversational memory.
- Summarize long logs into bullet points; never paste raw logs > 30 lines.
6) **Repeat** until acceptance criteria met. Then write a concise **DONE** report in `workflow_state.md`.
## Context Budget Rules
- Hard token guardrails:
- Summarize code context instead of inlining it. Reference files and line ranges.
- For large diffs, chunk work by directory or component; complete and verify one chunk before opening the next.
- Never quote third-party library code. Link the package name and version only.
- On every loop, if the visible conversation > ~12 screens, summarize to `workflow_state.md` and clear prior verbose messages by continuing with short references.
## Execution & Safety
- Auto-run commands that are safe and reversible (formatters, linters, unit tests, local dev server).
- If a command appears on the **DENYLIST** (see below), do NOT execute. Instead, write an **INTENT** note in `workflow_state.md` asking for approval, then continue with other work.
- Prefer idempotent scripts and atomic commits to prevent partial breakage.
## Reporting Style
- Communicate progress inside `workflow_state.md` only.
- In chat, respond with a one-paragraph status and the next concrete action, no tutorials.
## DENYLIST (edit per project)
- Global destructive actions: `rm -rf /`, `sudo`, `diskutil eraseDisk`, cloud destroy commands (e.g., `terraform destroy`, `pulumi destroy`, `gcloud compute instances delete` without `--dry-run`), production DB migrations, any command targeting prod environments.
- Network or credentialed actions unless sandboxed.
- Cost-incurring benchmarks or load tests without limits.
## Minimal Acceptance Checklist (default)
- Builds successfully.
- Linters/type checks pass.
- All existing tests pass; new tests cover new behavior.
- README or `project_config.md` updated for any new command, env var, or migration.
Use timeouts when running continous actions like launching or compiling the app.

View file

@ -0,0 +1,4 @@
---
alwaysApply: true
---
mnemo_cards_web_v2 is a web app. It should only run on web! Not mobile or macos. And tests should also only run on web!

View file

@ -0,0 +1,9 @@
---
alwaysApply: true
---
Use yx_state and yx_scope,
Follow clean architecture techique,
You can use ../mnemo_cards app as a design and product features reference, but NEVER copy its architecture.
When you finish with another step:
Write unit tests for all the functionalities
Update PROGESS.md and TODO.md

View file

@ -0,0 +1,45 @@
name: agent-issue-to-pr
on:
issues:
types: [labeled]
jobs:
agent:
if: github.event.label.name == 'agent:do'
runs-on: docker
container: python:3.12
steps:
- uses: actions/checkout@v4
- name: Install Aider
run: pip install --upgrade pip aider-chat
- name: Create branch
id: mkbr
run: |
BR="agent/${{ github.event.issue.number }}"
echo "BR=$BR" >> $GITHUB_ENV
git checkout -b "$BR"
git config user.name "forgejo-actions[bot]"
git config user.email "actions@forgejo.local"
- name: Run agent (write code + tests)
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
aider --yes --message "Issue #${{ github.event.issue.number }} — ${{ github.event.issue.title }}.
Сделай минимальный патч и тесты. CI не трогай." .
git add -A
git commit -m "agent: implement #${{ github.event.issue.number }}" || true
git push -u origin "$BR"
- name: Open PR via API
env:
API: ${{ github.api_url }}
REPO: ${{ github.repository }}
TOKEN: ${{ github.token }}
run: |
curl -sS -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"head":"'"$BR"'","base":"main","title":"Agent PR for #'"${{ github.event.issue.number }}"'","body":"Автогенерация по issue."}' \
"$API/repos/$REPO/pulls"

View file

@ -4,8 +4,8 @@
# This file should be version controlled and should not be manually edited. # This file should be version controlled and should not be manually edited.
version: version:
revision: "6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e" revision: "3e493a3e4d0a5c99fa7da51faae354e95a9a1abe"
channel: "stable" channel: "beta"
project_type: app project_type: app
@ -13,11 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
base_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
- platform: macos - platform: web
create_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e create_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
base_revision: 6c4930c4ac86fb286f30e31d0ec8bffbcbb9953e base_revision: 3e493a3e4d0a5c99fa7da51faae354e95a9a1abe
# User provided section # User provided section

373
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,373 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "funny_letters",
"cwd": "funny_letters",
"request": "launch",
"type": "dart"
},
{
"name": "funny_letters (profile mode)",
"cwd": "funny_letters",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "funny_letters (release mode)",
"cwd": "funny_letters",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "games",
"cwd": "games",
"request": "launch",
"type": "dart"
},
{
"name": "games (profile mode)",
"cwd": "games",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "games (release mode)",
"cwd": "games",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards",
"cwd": "mnemo_cards",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards (profile mode)",
"cwd": "mnemo_cards",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards (release mode)",
"cwd": "mnemo_cards",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards_backend",
"cwd": "mnemo_cards_backend",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_common",
"cwd": "mnemo_cards_common",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_common_backend",
"cwd": "mnemo_cards_common_backend",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_frontend_common",
"cwd": "mnemo_cards_frontend_common",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_frontend_common (profile mode)",
"cwd": "mnemo_cards_frontend_common",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards_frontend_common (release mode)",
"cwd": "mnemo_cards_frontend_common",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards_snake_game",
"cwd": "mnemo_cards_snake_game",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_snake_game (profile mode)",
"cwd": "mnemo_cards_snake_game",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards_snake_game (release mode)",
"cwd": "mnemo_cards_snake_game",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards_telegram_bot",
"cwd": "mnemo_cards_telegram_bot",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_web",
"cwd": "mnemo_cards_web",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_web (profile mode)",
"cwd": "mnemo_cards_web",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards_web (release mode)",
"cwd": "mnemo_cards_web",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "example",
"cwd": "funny_letters/example",
"request": "launch",
"type": "dart"
},
{
"name": "example (profile mode)",
"cwd": "funny_letters/example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "example (release mode)",
"cwd": "funny_letters/example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "ads_example",
"cwd": "games/ads_example",
"request": "launch",
"type": "dart"
},
{
"name": "ads_example (profile mode)",
"cwd": "games/ads_example",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "ads_example (release mode)",
"cwd": "games/ads_example",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards_game_api",
"cwd": "games/mnemo_cards_game_api",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_game_api (profile mode)",
"cwd": "games/mnemo_cards_game_api",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards_game_api (release mode)",
"cwd": "games/mnemo_cards_game_api",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "mnemo_cards_web_bridge",
"cwd": "games/mnemo_cards_web_bridge",
"request": "launch",
"type": "dart"
},
{
"name": "mnemo_cards_web_bridge (profile mode)",
"cwd": "games/mnemo_cards_web_bridge",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "mnemo_cards_web_bridge (release mode)",
"cwd": "games/mnemo_cards_web_bridge",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "bus_word_game",
"cwd": "games/apps/bus_word_game",
"request": "launch",
"type": "dart"
},
{
"name": "bus_word_game (profile mode)",
"cwd": "games/apps/bus_word_game",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "bus_word_game (release mode)",
"cwd": "games/apps/bus_word_game",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "host_app",
"cwd": "games/apps/host_app",
"request": "launch",
"type": "dart"
},
{
"name": "host_app (profile mode)",
"cwd": "games/apps/host_app",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "host_app (release mode)",
"cwd": "games/apps/host_app",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "web_app1",
"cwd": "games/apps/web_app1",
"request": "launch",
"type": "dart"
},
{
"name": "web_app1 (profile mode)",
"cwd": "games/apps/web_app1",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "web_app1 (release mode)",
"cwd": "games/apps/web_app1",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "bridge_core",
"cwd": "games/packages/bridge_core",
"request": "launch",
"type": "dart"
},
{
"name": "bridge_core (profile mode)",
"cwd": "games/packages/bridge_core",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "bridge_core (release mode)",
"cwd": "games/packages/bridge_core",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "payloads_app1",
"cwd": "games/packages/payloads_app1",
"request": "launch",
"type": "dart"
},
{
"name": "payloads_app1 (profile mode)",
"cwd": "games/packages/payloads_app1",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "payloads_app1 (release mode)",
"cwd": "games/packages/payloads_app1",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "payloads_host",
"cwd": "games/packages/payloads_host",
"request": "launch",
"type": "dart"
},
{
"name": "payloads_host (profile mode)",
"cwd": "games/packages/payloads_host",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "payloads_host (release mode)",
"cwd": "games/packages/payloads_host",
"request": "launch",
"type": "dart",
"flutterMode": "release"
},
{
"name": "payloads_shared",
"cwd": "games/packages/payloads_shared",
"request": "launch",
"type": "dart"
},
{
"name": "payloads_shared (profile mode)",
"cwd": "games/packages/payloads_shared",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "payloads_shared (release mode)",
"cwd": "games/packages/payloads_shared",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

219
README.md
View file

@ -1,16 +1,215 @@
# mnemo_cards # Mnemo Cards - Мобильное приложение для изучения языков
Mnemo Полнофункциональная система для изучения языков, включающая мобильное приложение, веб-сервер, телеграм-бот и веб-игры.
## Getting Started ## 🏗️ Структура проекта
This project is a starting point for a Flutter application. Проект состоит из нескольких взаимосвязанных компонентов:
A few resources to get you started if this is your first Flutter project: ### 📱 Мобильное приложение (`mnemo_cards/`)
Основное Flutter приложение для iOS и Android с функциями:
- Изучение языков через карточки
- Интеграция с Firebase (Firestore, Analytics, Crashlytics)
- Система покупок (YooKassa, In-App Purchase)
- Реклама (Yandex Mobile Ads)
- Локальные уведомления
- Интеграция с веб-играми через WebView
- Поддержка Telegram Web App
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) **Технологии:** Flutter, BLoC, Auto Route, Firebase, WebView
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the ### 🌐 Веб-приложение (`mnemo_cards_web/`)
[online documentation](https://docs.flutter.dev/), which offers tutorials, Веб-версия Flutter приложения с функциями:
samples, guidance on mobile development, and a full API reference. - Изучение языков через карточки в браузере
- Интеграция с Firebase (Firestore, Analytics, Crashlytics)
- Адаптивный дизайн для различных устройств
- Синхронизация данных с мобильным приложением
- Поддержка PWA (Progressive Web App)
- Интеграция с веб-играми
**Технологии:** Flutter Web, BLoC, Auto Route, Firebase, PWA
### 🔧 Общие компоненты (`mnemo_cards_common/`)
Общие DTO и утилиты для мобильного приложения:
- Модели данных
- Сетевые утилиты
- JSON сериализация
- Криптографические функции
### 🖥️ Backend сервер (`mnemo_cards_backend/`)
Dart сервер на базе Shelf framework:
- REST API для мобильного приложения
- База данных Isar
- Интеграция с Google APIs
- Система платежей YooKassa
- Периодические задачи
- Swagger документация
**Технологии:** Dart, Shelf, Isar, Google APIs
### 🔧 Backend общие компоненты (`mnemo_cards_common_backend/`)
Общие компоненты для backend:
- Модели данных
- Утилиты для работы с Isar
- Общие бизнес-логика
### 🤖 Telegram бот (`mnemo_cards_telegram_bot/`)
Telegram бот для управления приложением:
- Административные функции
- Распространение APK файлов
- Управление пользователями
- Интеграция с основным backend
**Технологии:** Dart, Teledart, Isar
### 🎮 Веб-игры и WebView Bridge (`games/`)
Система веб-игр с безопасной коммуникацией через WebView:
- **Host приложение** - Flutter приложение-контейнер
- **Web приложения** - Flutter Web игры (bus_word_game, web_app1)
- **Bridge Core** - система безопасной коммуникации
- **Payloads** - типизированные сообщения между приложениями
**Архитектура:**
- Изолированные payload'ы для каждого приложения
- JavaScript bridge для WebView коммуникации
- Типобезопасная обработка сообщений
- Поддержка сложных сценариев (авторизация, quiz, нативные диалоги)
**Технологии:** Flutter Web, WebView, JavaScript Bridge, Melos
## 🚀 Быстрый старт
### Мобильное приложение
```bash
cd mnemo_cards
flutter pub get
flutter run
```
### Веб-приложение
```bash
cd mnemo_cards_web
flutter pub get
flutter run -d chrome
```
### Backend сервер
```bash
cd mnemo_cards_backend
dart pub get
dart run
```
### Telegram бот
```bash
cd mnemo_cards_telegram_bot
dart pub get
dart run
```
### Веб-игры
```bash
cd games
melos bootstrap
cd apps/host_app
flutter run
```
## 🔗 Зависимости между компонентами
```
mnemo_cards
├── mnemo_cards_common (DTO)
├── games/mnemo_cards_web_bridge (WebView интеграция)
└── Firebase/Backend API
mnemo_cards_web
├── mnemo_cards_common (DTO)
├── mnemo_cards_frontend_common (общие компоненты)
├── games/payloads_shared (общие типы)
└── Firebase/Backend API
mnemo_cards_backend
├── mnemo_cards_common (общие DTO)
├── mnemo_cards_common_backend (backend утилиты)
└── Isar Database
mnemo_cards_telegram_bot
├── mnemo_cards_common_backend
└── Backend API
games/
├── bridge_core (WebView коммуникация)
├── payloads_shared (общие типы)
└── apps/ (веб-приложения)
```
## 📋 Основные функции
### Мобильное приложение
- 📚 Изучение языков через карточки
- 🎮 Интеграция с веб-играми
- 💳 Система покупок и подписок
- 📊 Аналитика и статистика
- 🔔 Push уведомления
- 🌐 Telegram Web App поддержка
### Веб-приложение
- 📚 Изучение языков через карточки в браузере
- 🎮 Интеграция с веб-играми
- 💳 Система покупок и подписок
- 📊 Аналитика и статистика
- 🔄 Синхронизация с мобильным приложением
- 📱 PWA поддержка для установки на устройство
### Backend
- 🔐 Аутентификация и авторизация
- 💾 Управление данными пользователей
- 💰 Обработка платежей
- 📈 Аналитика и метрики
- 🔄 Периодические задачи
- 📚 API для мобильного приложения
### Telegram бот
- 👨‍💼 Административные функции
- 📱 Распространение APK
- 👥 Управление пользователями
- 📊 Мониторинг системы
### Веб-игры
- 🎯 Интерактивные игры для изучения языков
- 🔗 Безопасная интеграция с мобильным приложением
- 📊 Отслеживание прогресса
- 🎨 Современный UI/UX
## 🛠️ Технологический стек
- **Frontend:** Flutter, Flutter Web
- **Backend:** Dart, Shelf, Isar
- **База данных:** Isar (локальная), Firestore (облачная)
- **Аутентификация:** Firebase Auth, Google Sign-In
- **Платежи:** YooKassa, In-App Purchase
- **Аналитика:** Firebase Analytics, Crashlytics
- **Реклама:** Yandex Mobile Ads
- **Уведомления:** Firebase Messaging, Local Notifications
- **Коммуникация:** WebView Bridge, REST API
- **Мониторинг:** Telegram Bot
## 📱 Поддерживаемые платформы
- **iOS** - нативное приложение
- **Android** - нативное приложение
- **Web** - Flutter Web приложение и игры
- **Telegram** - Web App интеграция
## 🔒 Безопасность
- Шифрование данных
- Безопасная WebView коммуникация
- Защита от root/jailbreak
- Валидация платежей
- Изоляция payload'ов в веб-играх
## 📄 Лицензия
Проект является приватным и не предназначен для публикации.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,175 @@
# mnemo_cards_chat
A Flutter package providing chat functionality with LLM integration for mnemo_cards applications.
## Features
- 💬 **Text Chat**: Send and receive text messages with LLM
- 🎤 **Audio Messages**: Record and playback voice messages
- 🔄 **Reactive State**: State management with yx_state
- 🏗️ **Clean Architecture**: Modular design with dependency injection
- 🔌 **Extensible**: Abstract ChatRepository interface for different backends
## Installation
Add to your `pubspec.yaml`:
```yaml
dependencies:
mnemo_cards_chat:
path: packages/mnemo_cards_chat
```
## Usage
### Basic Setup
```dart
import 'package:mnemo_cards_chat/mnemo_cards_chat.dart';
// 1. Implement ChatRepository for your backend
class MyChatRepository implements ChatRepository {
// Implement all ChatRepository methods
// e.g., sendTextMessage, getChatMessages, etc.
}
// 2. Create chat service
final chatService = ChatService(chatRepository: MyChatRepository());
// 3. Create state manager
final chatStateManager = ChatStateManager(chatService: chatService);
// 4. Use in your UI
// See ChatStateManager for available methods
```
### Dependency Injection with yx_scope
```dart
import 'package:yx_scope/yx_scope.dart';
import 'package:mnemo_cards_chat/mnemo_cards_chat.dart';
class ChatModule extends ScopeModule<UserScopeContainer> {
ChatModule(super.container);
// Chat repository adapter
late final chatRepositoryDep = dep(
() => ChatRepositoryAdapter(container.httpRepository),
);
// Chat service
late final chatServiceDep = dep(
() => ChatService(chatRepository: chatRepositoryDep.get),
);
// Chat state manager
late final chatStateManagerDep = dep(
() => ChatStateManager(chatService: chatServiceDep.get),
);
ChatStateManager get chatStateManager => chatStateManagerDep.get;
}
```
### State Management
```dart
// Initialize chat session
await chatStateManager.initializeChat('session_id');
// Send text message
await chatStateManager.sendTextMessage('Hello, world!');
// Send audio message
final audioData = Uint8List.fromList([...]);
await chatStateManager.sendAudioMessage(audioData, Duration(seconds: 5));
// Listen to state changes
chatStateManager.addListener(() {
final state = chatStateManager.state;
if (state is ChatStateLoaded) {
// Handle loaded state
print('Messages: ${state.messages.length}');
} else if (state is ChatStateError) {
// Handle error
print('Error: ${state.message}');
}
});
```
## Architecture
### Core Components
- **`ChatService`**: Business logic and API orchestration
- **`ChatStateManager`**: Reactive state management
- **`ChatRepository`**: Abstract interface for backend communication
- **Models**: Data structures for messages, sessions, and participants
### State Types
```dart
// Loading state
const ChatStateLoading()
// Loaded state with session data
ChatStateLoaded(
session: session,
messages: messages,
isSendingMessage: false,
errorMessage: null,
)
// Error state
ChatStateError('Failed to load chat')
```
## Models
### ChatMessage
Union type for different message types:
- `ChatMessage.text(TextMessage)` - Text messages
- `ChatMessage.audio(AudioMessage)` - Audio messages
### ChatParticipant
Represents chat participants:
- `ChatParticipant.user()` - Human users
- `ChatParticipant.assistant()` - AI assistants
### ChatSession
Represents chat conversation sessions with metadata.
## API Integration
Implement `ChatRepository` to connect with your backend:
```dart
abstract class ChatRepository {
Future<ChatBasicSession> createChatSession(CreateChatSessionRequest request);
Future<List<ChatBasicSession>> getChatSessions({int limit, String? afterSessionId});
Future<ChatBasicSession> getChatSession(String sessionId);
Future<ChatMessageResponse> sendTextMessage(SendTextMessageRequest request);
Future<ChatMessageResponse> sendAudioMessage(String sessionId, dynamic audioData, Duration duration, {String? fileName, String? mimeType});
Future<List<ChatMessageResponse>> getChatMessages(String sessionId, {int limit, String? beforeMessageId});
Future<ChatBasicSession> updateChatSession(String sessionId, Map<String, dynamic> updates);
Future<void> deleteChatSession(String sessionId);
}
```
## Testing
Run tests:
```bash
flutter test
```
## Contributing
1. Follow the existing code style
2. Add tests for new features
3. Update documentation
4. Ensure all tests pass
## License
This package is part of the mnemo_cards project.

View file

@ -0,0 +1,61 @@
include: package:flutter_lints/flutter.yaml
analyzer:
plugins:
- custom_lint
exclude:
- '**/*.g.dart'
- '**/*.freezed.dart'
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
# Treat missing required parameters as errors
missing_required_param: error
# Treat missing returns as errors
missing_return: error
# Treat invalid assignments as errors
invalid_assignment: error
linter:
rules:
# Basic rules
avoid_print: true
prefer_const_constructors: true
prefer_const_literals_to_create_immutables: true
prefer_final_fields: true
unnecessary_this: true
sort_child_properties_last: true
use_key_in_widget_constructors: true
# Type safety rules - NO DYNAMIC!
avoid_dynamic_calls: true
avoid_type_to_string: true
implicit_call_tearoffs: true
# Additional type safety
always_declare_return_types: true
always_specify_types: false # Too verbose, but we have strict-inference
type_annotate_public_apis: true
# Code quality
always_use_package_imports: true
avoid_empty_else: true
avoid_relative_lib_imports: true
avoid_slow_async_io: true
cancel_subscriptions: true
close_sinks: true
no_adjacent_strings_in_list: true
unnecessary_statements: true
# Style
prefer_single_quotes: true
require_trailing_commas: true
# Documentation
package_api_docs: true
public_member_api_docs: false # Can enable for stricter docs

View file

@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1 @@
[{"family":"MaterialIcons","fonts":[{"asset":"fonts/MaterialIcons-Regular.otf"}]}]

Binary file not shown.

View file

@ -0,0 +1 @@
{"format-version":[1,0,0],"native-assets":{}}

View file

@ -0,0 +1,37 @@
/// Chat module for mnemo_cards applications
///
/// Provides chat functionality with LLM integration supporting both text and audio messages.
///
/// ## Features
/// - Text message chat with LLM
/// - Audio message recording and playback
/// - Session management
/// - Reactive state management with yx_state
/// - Clean architecture with yx_scope DI
///
/// ## Usage
/// ```dart
/// import 'package:mnemo_cards_chat/mnemo_cards_chat.dart';
///
/// // Register chat module in your scope
/// final scope = UserScopeContainer()
/// ..add(ChatModule(chatConfig: chatConfig));
/// ```
library mnemo_cards_chat;
export 'src/domain/models/chat_basic.dart';
export 'src/domain/models/chat_message.dart';
export 'src/domain/models/chat_session.dart';
export 'src/domain/models/chat_models.dart';
export 'src/domain/services/chat_repository.dart';
export 'src/domain/services/chat_service.dart';
export 'src/domain/state/chat_state_manager.dart';
// Re-export common types from chat_session for convenience
export 'src/domain/models/chat_session.dart' show
ChatSession,
ChatSessionStatus,
UpdateChatSessionRequest;
// Re-export common types from chat_api_simple for convenience
export 'src/domain/models/chat_api_simple.dart';

View file

@ -0,0 +1,67 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_api_simple.freezed.dart';
part 'chat_api_simple.g.dart';
/// Request to send text message to chat
@freezed
class SendTextMessageRequest with _$SendTextMessageRequest {
const factory SendTextMessageRequest({
required String sessionId,
required String content,
}) = _SendTextMessageRequest;
factory SendTextMessageRequest.fromJson(Map<String, dynamic> json) =>
_$SendTextMessageRequestFromJson(json);
}
/// Response from chat API containing assistant's message
@freezed
class ChatMessageResponse with _$ChatMessageResponse {
const factory ChatMessageResponse({
required String messageId,
required String sessionId,
required String content,
required String senderId,
required DateTime timestamp,
@Default('text') String messageType,
}) = _ChatMessageResponse;
factory ChatMessageResponse.fromJson(Map<String, dynamic> json) =>
_$ChatMessageResponseFromJson(json);
}
/// Request to get chat messages for a session
@freezed
class GetChatMessagesRequest with _$GetChatMessagesRequest {
const factory GetChatMessagesRequest({
required String sessionId,
@Default(50) int limit,
}) = _GetChatMessagesRequest;
factory GetChatMessagesRequest.fromJson(Map<String, dynamic> json) =>
_$GetChatMessagesRequestFromJson(json);
}
/// Response containing list of chat messages
@freezed
class ChatMessagesResponse with _$ChatMessagesResponse {
const factory ChatMessagesResponse({
required List<ChatMessageResponse> messages,
@Default(false) bool hasMore,
}) = _ChatMessagesResponse;
factory ChatMessagesResponse.fromJson(Map<String, dynamic> json) =>
_$ChatMessagesResponseFromJson(json);
}
/// Request to create new chat session
@freezed
class CreateChatSessionRequest with _$CreateChatSessionRequest {
const factory CreateChatSessionRequest({
required String title,
}) = _CreateChatSessionRequest;
factory CreateChatSessionRequest.fromJson(Map<String, dynamic> json) =>
_$CreateChatSessionRequestFromJson(json);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_api_simple.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SendTextMessageRequestImpl _$$SendTextMessageRequestImplFromJson(
Map<String, dynamic> json,
) => _$SendTextMessageRequestImpl(
sessionId: json['sessionId'] as String,
content: json['content'] as String,
);
Map<String, dynamic> _$$SendTextMessageRequestImplToJson(
_$SendTextMessageRequestImpl instance,
) => <String, dynamic>{
'sessionId': instance.sessionId,
'content': instance.content,
};
_$ChatMessageResponseImpl _$$ChatMessageResponseImplFromJson(
Map<String, dynamic> json,
) => _$ChatMessageResponseImpl(
messageId: json['messageId'] as String,
sessionId: json['sessionId'] as String,
content: json['content'] as String,
senderId: json['senderId'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
messageType: json['messageType'] as String? ?? 'text',
);
Map<String, dynamic> _$$ChatMessageResponseImplToJson(
_$ChatMessageResponseImpl instance,
) => <String, dynamic>{
'messageId': instance.messageId,
'sessionId': instance.sessionId,
'content': instance.content,
'senderId': instance.senderId,
'timestamp': instance.timestamp.toIso8601String(),
'messageType': instance.messageType,
};
_$GetChatMessagesRequestImpl _$$GetChatMessagesRequestImplFromJson(
Map<String, dynamic> json,
) => _$GetChatMessagesRequestImpl(
sessionId: json['sessionId'] as String,
limit: (json['limit'] as num?)?.toInt() ?? 50,
);
Map<String, dynamic> _$$GetChatMessagesRequestImplToJson(
_$GetChatMessagesRequestImpl instance,
) => <String, dynamic>{
'sessionId': instance.sessionId,
'limit': instance.limit,
};
_$ChatMessagesResponseImpl _$$ChatMessagesResponseImplFromJson(
Map<String, dynamic> json,
) => _$ChatMessagesResponseImpl(
messages: (json['messages'] as List<dynamic>)
.map((e) => ChatMessageResponse.fromJson(e as Map<String, dynamic>))
.toList(),
hasMore: json['hasMore'] as bool? ?? false,
);
Map<String, dynamic> _$$ChatMessagesResponseImplToJson(
_$ChatMessagesResponseImpl instance,
) => <String, dynamic>{
'messages': instance.messages,
'hasMore': instance.hasMore,
};
_$CreateChatSessionRequestImpl _$$CreateChatSessionRequestImplFromJson(
Map<String, dynamic> json,
) => _$CreateChatSessionRequestImpl(title: json['title'] as String);
Map<String, dynamic> _$$CreateChatSessionRequestImplToJson(
_$CreateChatSessionRequestImpl instance,
) => <String, dynamic>{'title': instance.title};

View file

@ -0,0 +1,54 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_basic.freezed.dart';
part 'chat_basic.g.dart';
/// Basic chat message for initial implementation
@freezed
class ChatBasicMessage with _$ChatBasicMessage {
const factory ChatBasicMessage({
required String id,
required String sessionId,
required String content,
required String senderId,
required String senderName,
required DateTime timestamp,
@Default('text') String messageType,
String? audioUrl,
@Default(0) int durationMs,
int? fileSize,
}) = _ChatBasicMessage;
factory ChatBasicMessage.fromJson(Map<String, dynamic> json) =>
_$ChatBasicMessageFromJson(json);
}
/// Basic chat session for initial implementation
@freezed
class ChatBasicSession with _$ChatBasicSession {
const factory ChatBasicSession({
required String id,
required String userId,
required String title,
required DateTime createdAt,
required DateTime updatedAt,
@Default('active') String status,
String? description,
@Default(0) int messageCount,
}) = _ChatBasicSession;
factory ChatBasicSession.fromJson(Map<String, dynamic> json) =>
_$ChatBasicSessionFromJson(json);
}
/// Request to send text message
@freezed
class SendTextMessageBasicRequest with _$SendTextMessageBasicRequest {
const factory SendTextMessageBasicRequest({
required String sessionId,
required String content,
}) = _SendTextMessageBasicRequest;
factory SendTextMessageBasicRequest.fromJson(Map<String, dynamic> json) =>
_$SendTextMessageBasicRequestFromJson(json);
}

View file

@ -0,0 +1,897 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_basic.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
ChatBasicMessage _$ChatBasicMessageFromJson(Map<String, dynamic> json) {
return _ChatBasicMessage.fromJson(json);
}
/// @nodoc
mixin _$ChatBasicMessage {
String get id => throw _privateConstructorUsedError;
String get sessionId => throw _privateConstructorUsedError;
String get content => throw _privateConstructorUsedError;
String get senderId => throw _privateConstructorUsedError;
String get senderName => throw _privateConstructorUsedError;
DateTime get timestamp => throw _privateConstructorUsedError;
String get messageType => throw _privateConstructorUsedError;
String? get audioUrl => throw _privateConstructorUsedError;
int get durationMs => throw _privateConstructorUsedError;
int? get fileSize => throw _privateConstructorUsedError;
/// Serializes this ChatBasicMessage to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ChatBasicMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatBasicMessageCopyWith<ChatBasicMessage> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatBasicMessageCopyWith<$Res> {
factory $ChatBasicMessageCopyWith(
ChatBasicMessage value,
$Res Function(ChatBasicMessage) then,
) = _$ChatBasicMessageCopyWithImpl<$Res, ChatBasicMessage>;
@useResult
$Res call({
String id,
String sessionId,
String content,
String senderId,
String senderName,
DateTime timestamp,
String messageType,
String? audioUrl,
int durationMs,
int? fileSize,
});
}
/// @nodoc
class _$ChatBasicMessageCopyWithImpl<$Res, $Val extends ChatBasicMessage>
implements $ChatBasicMessageCopyWith<$Res> {
_$ChatBasicMessageCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatBasicMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? content = null,
Object? senderId = null,
Object? senderName = null,
Object? timestamp = null,
Object? messageType = null,
Object? audioUrl = freezed,
Object? durationMs = null,
Object? fileSize = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
senderId: null == senderId
? _value.senderId
: senderId // ignore: cast_nullable_to_non_nullable
as String,
senderName: null == senderName
? _value.senderName
: senderName // ignore: cast_nullable_to_non_nullable
as String,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,
messageType: null == messageType
? _value.messageType
: messageType // ignore: cast_nullable_to_non_nullable
as String,
audioUrl: freezed == audioUrl
? _value.audioUrl
: audioUrl // ignore: cast_nullable_to_non_nullable
as String?,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
fileSize: freezed == fileSize
? _value.fileSize
: fileSize // ignore: cast_nullable_to_non_nullable
as int?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ChatBasicMessageImplCopyWith<$Res>
implements $ChatBasicMessageCopyWith<$Res> {
factory _$$ChatBasicMessageImplCopyWith(
_$ChatBasicMessageImpl value,
$Res Function(_$ChatBasicMessageImpl) then,
) = __$$ChatBasicMessageImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String sessionId,
String content,
String senderId,
String senderName,
DateTime timestamp,
String messageType,
String? audioUrl,
int durationMs,
int? fileSize,
});
}
/// @nodoc
class __$$ChatBasicMessageImplCopyWithImpl<$Res>
extends _$ChatBasicMessageCopyWithImpl<$Res, _$ChatBasicMessageImpl>
implements _$$ChatBasicMessageImplCopyWith<$Res> {
__$$ChatBasicMessageImplCopyWithImpl(
_$ChatBasicMessageImpl _value,
$Res Function(_$ChatBasicMessageImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatBasicMessage
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? sessionId = null,
Object? content = null,
Object? senderId = null,
Object? senderName = null,
Object? timestamp = null,
Object? messageType = null,
Object? audioUrl = freezed,
Object? durationMs = null,
Object? fileSize = freezed,
}) {
return _then(
_$ChatBasicMessageImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
senderId: null == senderId
? _value.senderId
: senderId // ignore: cast_nullable_to_non_nullable
as String,
senderName: null == senderName
? _value.senderName
: senderName // ignore: cast_nullable_to_non_nullable
as String,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as DateTime,
messageType: null == messageType
? _value.messageType
: messageType // ignore: cast_nullable_to_non_nullable
as String,
audioUrl: freezed == audioUrl
? _value.audioUrl
: audioUrl // ignore: cast_nullable_to_non_nullable
as String?,
durationMs: null == durationMs
? _value.durationMs
: durationMs // ignore: cast_nullable_to_non_nullable
as int,
fileSize: freezed == fileSize
? _value.fileSize
: fileSize // ignore: cast_nullable_to_non_nullable
as int?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$ChatBasicMessageImpl implements _ChatBasicMessage {
const _$ChatBasicMessageImpl({
required this.id,
required this.sessionId,
required this.content,
required this.senderId,
required this.senderName,
required this.timestamp,
this.messageType = 'text',
this.audioUrl,
this.durationMs = 0,
this.fileSize,
});
factory _$ChatBasicMessageImpl.fromJson(Map<String, dynamic> json) =>
_$$ChatBasicMessageImplFromJson(json);
@override
final String id;
@override
final String sessionId;
@override
final String content;
@override
final String senderId;
@override
final String senderName;
@override
final DateTime timestamp;
@override
@JsonKey()
final String messageType;
@override
final String? audioUrl;
@override
@JsonKey()
final int durationMs;
@override
final int? fileSize;
@override
String toString() {
return 'ChatBasicMessage(id: $id, sessionId: $sessionId, content: $content, senderId: $senderId, senderName: $senderName, timestamp: $timestamp, messageType: $messageType, audioUrl: $audioUrl, durationMs: $durationMs, fileSize: $fileSize)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatBasicMessageImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.sessionId, sessionId) ||
other.sessionId == sessionId) &&
(identical(other.content, content) || other.content == content) &&
(identical(other.senderId, senderId) ||
other.senderId == senderId) &&
(identical(other.senderName, senderName) ||
other.senderName == senderName) &&
(identical(other.timestamp, timestamp) ||
other.timestamp == timestamp) &&
(identical(other.messageType, messageType) ||
other.messageType == messageType) &&
(identical(other.audioUrl, audioUrl) ||
other.audioUrl == audioUrl) &&
(identical(other.durationMs, durationMs) ||
other.durationMs == durationMs) &&
(identical(other.fileSize, fileSize) ||
other.fileSize == fileSize));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
sessionId,
content,
senderId,
senderName,
timestamp,
messageType,
audioUrl,
durationMs,
fileSize,
);
/// Create a copy of ChatBasicMessage
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatBasicMessageImplCopyWith<_$ChatBasicMessageImpl> get copyWith =>
__$$ChatBasicMessageImplCopyWithImpl<_$ChatBasicMessageImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$ChatBasicMessageImplToJson(this);
}
}
abstract class _ChatBasicMessage implements ChatBasicMessage {
const factory _ChatBasicMessage({
required final String id,
required final String sessionId,
required final String content,
required final String senderId,
required final String senderName,
required final DateTime timestamp,
final String messageType,
final String? audioUrl,
final int durationMs,
final int? fileSize,
}) = _$ChatBasicMessageImpl;
factory _ChatBasicMessage.fromJson(Map<String, dynamic> json) =
_$ChatBasicMessageImpl.fromJson;
@override
String get id;
@override
String get sessionId;
@override
String get content;
@override
String get senderId;
@override
String get senderName;
@override
DateTime get timestamp;
@override
String get messageType;
@override
String? get audioUrl;
@override
int get durationMs;
@override
int? get fileSize;
/// Create a copy of ChatBasicMessage
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatBasicMessageImplCopyWith<_$ChatBasicMessageImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ChatBasicSession _$ChatBasicSessionFromJson(Map<String, dynamic> json) {
return _ChatBasicSession.fromJson(json);
}
/// @nodoc
mixin _$ChatBasicSession {
String get id => throw _privateConstructorUsedError;
String get userId => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
String get status => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
int get messageCount => throw _privateConstructorUsedError;
/// Serializes this ChatBasicSession to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ChatBasicSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatBasicSessionCopyWith<ChatBasicSession> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatBasicSessionCopyWith<$Res> {
factory $ChatBasicSessionCopyWith(
ChatBasicSession value,
$Res Function(ChatBasicSession) then,
) = _$ChatBasicSessionCopyWithImpl<$Res, ChatBasicSession>;
@useResult
$Res call({
String id,
String userId,
String title,
DateTime createdAt,
DateTime updatedAt,
String status,
String? description,
int messageCount,
});
}
/// @nodoc
class _$ChatBasicSessionCopyWithImpl<$Res, $Val extends ChatBasicSession>
implements $ChatBasicSessionCopyWith<$Res> {
_$ChatBasicSessionCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatBasicSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? userId = null,
Object? title = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? status = null,
Object? description = freezed,
Object? messageCount = null,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
messageCount: null == messageCount
? _value.messageCount
: messageCount // ignore: cast_nullable_to_non_nullable
as int,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ChatBasicSessionImplCopyWith<$Res>
implements $ChatBasicSessionCopyWith<$Res> {
factory _$$ChatBasicSessionImplCopyWith(
_$ChatBasicSessionImpl value,
$Res Function(_$ChatBasicSessionImpl) then,
) = __$$ChatBasicSessionImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String userId,
String title,
DateTime createdAt,
DateTime updatedAt,
String status,
String? description,
int messageCount,
});
}
/// @nodoc
class __$$ChatBasicSessionImplCopyWithImpl<$Res>
extends _$ChatBasicSessionCopyWithImpl<$Res, _$ChatBasicSessionImpl>
implements _$$ChatBasicSessionImplCopyWith<$Res> {
__$$ChatBasicSessionImplCopyWithImpl(
_$ChatBasicSessionImpl _value,
$Res Function(_$ChatBasicSessionImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatBasicSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? userId = null,
Object? title = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? status = null,
Object? description = freezed,
Object? messageCount = null,
}) {
return _then(
_$ChatBasicSessionImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
messageCount: null == messageCount
? _value.messageCount
: messageCount // ignore: cast_nullable_to_non_nullable
as int,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$ChatBasicSessionImpl implements _ChatBasicSession {
const _$ChatBasicSessionImpl({
required this.id,
required this.userId,
required this.title,
required this.createdAt,
required this.updatedAt,
this.status = 'active',
this.description,
this.messageCount = 0,
});
factory _$ChatBasicSessionImpl.fromJson(Map<String, dynamic> json) =>
_$$ChatBasicSessionImplFromJson(json);
@override
final String id;
@override
final String userId;
@override
final String title;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
@JsonKey()
final String status;
@override
final String? description;
@override
@JsonKey()
final int messageCount;
@override
String toString() {
return 'ChatBasicSession(id: $id, userId: $userId, title: $title, createdAt: $createdAt, updatedAt: $updatedAt, status: $status, description: $description, messageCount: $messageCount)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatBasicSessionImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.userId, userId) || other.userId == userId) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.status, status) || other.status == status) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.messageCount, messageCount) ||
other.messageCount == messageCount));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
userId,
title,
createdAt,
updatedAt,
status,
description,
messageCount,
);
/// Create a copy of ChatBasicSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatBasicSessionImplCopyWith<_$ChatBasicSessionImpl> get copyWith =>
__$$ChatBasicSessionImplCopyWithImpl<_$ChatBasicSessionImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$ChatBasicSessionImplToJson(this);
}
}
abstract class _ChatBasicSession implements ChatBasicSession {
const factory _ChatBasicSession({
required final String id,
required final String userId,
required final String title,
required final DateTime createdAt,
required final DateTime updatedAt,
final String status,
final String? description,
final int messageCount,
}) = _$ChatBasicSessionImpl;
factory _ChatBasicSession.fromJson(Map<String, dynamic> json) =
_$ChatBasicSessionImpl.fromJson;
@override
String get id;
@override
String get userId;
@override
String get title;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
String get status;
@override
String? get description;
@override
int get messageCount;
/// Create a copy of ChatBasicSession
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatBasicSessionImplCopyWith<_$ChatBasicSessionImpl> get copyWith =>
throw _privateConstructorUsedError;
}
SendTextMessageBasicRequest _$SendTextMessageBasicRequestFromJson(
Map<String, dynamic> json,
) {
return _SendTextMessageBasicRequest.fromJson(json);
}
/// @nodoc
mixin _$SendTextMessageBasicRequest {
String get sessionId => throw _privateConstructorUsedError;
String get content => throw _privateConstructorUsedError;
/// Serializes this SendTextMessageBasicRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of SendTextMessageBasicRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$SendTextMessageBasicRequestCopyWith<SendTextMessageBasicRequest>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SendTextMessageBasicRequestCopyWith<$Res> {
factory $SendTextMessageBasicRequestCopyWith(
SendTextMessageBasicRequest value,
$Res Function(SendTextMessageBasicRequest) then,
) =
_$SendTextMessageBasicRequestCopyWithImpl<
$Res,
SendTextMessageBasicRequest
>;
@useResult
$Res call({String sessionId, String content});
}
/// @nodoc
class _$SendTextMessageBasicRequestCopyWithImpl<
$Res,
$Val extends SendTextMessageBasicRequest
>
implements $SendTextMessageBasicRequestCopyWith<$Res> {
_$SendTextMessageBasicRequestCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of SendTextMessageBasicRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? sessionId = null, Object? content = null}) {
return _then(
_value.copyWith(
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$SendTextMessageBasicRequestImplCopyWith<$Res>
implements $SendTextMessageBasicRequestCopyWith<$Res> {
factory _$$SendTextMessageBasicRequestImplCopyWith(
_$SendTextMessageBasicRequestImpl value,
$Res Function(_$SendTextMessageBasicRequestImpl) then,
) = __$$SendTextMessageBasicRequestImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String sessionId, String content});
}
/// @nodoc
class __$$SendTextMessageBasicRequestImplCopyWithImpl<$Res>
extends
_$SendTextMessageBasicRequestCopyWithImpl<
$Res,
_$SendTextMessageBasicRequestImpl
>
implements _$$SendTextMessageBasicRequestImplCopyWith<$Res> {
__$$SendTextMessageBasicRequestImplCopyWithImpl(
_$SendTextMessageBasicRequestImpl _value,
$Res Function(_$SendTextMessageBasicRequestImpl) _then,
) : super(_value, _then);
/// Create a copy of SendTextMessageBasicRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? sessionId = null, Object? content = null}) {
return _then(
_$SendTextMessageBasicRequestImpl(
sessionId: null == sessionId
? _value.sessionId
: sessionId // ignore: cast_nullable_to_non_nullable
as String,
content: null == content
? _value.content
: content // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$SendTextMessageBasicRequestImpl
implements _SendTextMessageBasicRequest {
const _$SendTextMessageBasicRequestImpl({
required this.sessionId,
required this.content,
});
factory _$SendTextMessageBasicRequestImpl.fromJson(
Map<String, dynamic> json,
) => _$$SendTextMessageBasicRequestImplFromJson(json);
@override
final String sessionId;
@override
final String content;
@override
String toString() {
return 'SendTextMessageBasicRequest(sessionId: $sessionId, content: $content)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SendTextMessageBasicRequestImpl &&
(identical(other.sessionId, sessionId) ||
other.sessionId == sessionId) &&
(identical(other.content, content) || other.content == content));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, sessionId, content);
/// Create a copy of SendTextMessageBasicRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$SendTextMessageBasicRequestImplCopyWith<_$SendTextMessageBasicRequestImpl>
get copyWith =>
__$$SendTextMessageBasicRequestImplCopyWithImpl<
_$SendTextMessageBasicRequestImpl
>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SendTextMessageBasicRequestImplToJson(this);
}
}
abstract class _SendTextMessageBasicRequest
implements SendTextMessageBasicRequest {
const factory _SendTextMessageBasicRequest({
required final String sessionId,
required final String content,
}) = _$SendTextMessageBasicRequestImpl;
factory _SendTextMessageBasicRequest.fromJson(Map<String, dynamic> json) =
_$SendTextMessageBasicRequestImpl.fromJson;
@override
String get sessionId;
@override
String get content;
/// Create a copy of SendTextMessageBasicRequest
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$SendTextMessageBasicRequestImplCopyWith<_$SendTextMessageBasicRequestImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,77 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_basic.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ChatBasicMessageImpl _$$ChatBasicMessageImplFromJson(
Map<String, dynamic> json,
) => _$ChatBasicMessageImpl(
id: json['id'] as String,
sessionId: json['sessionId'] as String,
content: json['content'] as String,
senderId: json['senderId'] as String,
senderName: json['senderName'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
messageType: json['messageType'] as String? ?? 'text',
audioUrl: json['audioUrl'] as String?,
durationMs: (json['durationMs'] as num?)?.toInt() ?? 0,
fileSize: (json['fileSize'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ChatBasicMessageImplToJson(
_$ChatBasicMessageImpl instance,
) => <String, dynamic>{
'id': instance.id,
'sessionId': instance.sessionId,
'content': instance.content,
'senderId': instance.senderId,
'senderName': instance.senderName,
'timestamp': instance.timestamp.toIso8601String(),
'messageType': instance.messageType,
'audioUrl': instance.audioUrl,
'durationMs': instance.durationMs,
'fileSize': instance.fileSize,
};
_$ChatBasicSessionImpl _$$ChatBasicSessionImplFromJson(
Map<String, dynamic> json,
) => _$ChatBasicSessionImpl(
id: json['id'] as String,
userId: json['userId'] as String,
title: json['title'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
status: json['status'] as String? ?? 'active',
description: json['description'] as String?,
messageCount: (json['messageCount'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$$ChatBasicSessionImplToJson(
_$ChatBasicSessionImpl instance,
) => <String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'title': instance.title,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
'status': instance.status,
'description': instance.description,
'messageCount': instance.messageCount,
};
_$SendTextMessageBasicRequestImpl _$$SendTextMessageBasicRequestImplFromJson(
Map<String, dynamic> json,
) => _$SendTextMessageBasicRequestImpl(
sessionId: json['sessionId'] as String,
content: json['content'] as String,
);
Map<String, dynamic> _$$SendTextMessageBasicRequestImplToJson(
_$SendTextMessageBasicRequestImpl instance,
) => <String, dynamic>{
'sessionId': instance.sessionId,
'content': instance.content,
};

View file

@ -0,0 +1,107 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_message.freezed.dart';
part 'chat_message.g.dart';
/// Base class for all chat messages
@freezed
class ChatMessage with _$ChatMessage {
const factory ChatMessage.text(TextMessage message) = ChatMessageText;
const factory ChatMessage.audio(AudioMessage message) = ChatMessageAudio;
factory ChatMessage.fromJson(Map<String, dynamic> json) =>
_$ChatMessageFromJson(json);
}
/// Text message in chat
@freezed
class TextMessage with _$TextMessage {
const factory TextMessage({
required String id,
required String sessionId,
required String content,
required ChatParticipant sender,
required DateTime timestamp,
@Default(MessageStatus.sent) MessageStatus status,
String? metadata,
}) = _TextMessage;
factory TextMessage.fromJson(Map<String, dynamic> json) =>
_$TextMessageFromJson(json);
}
/// Audio message in chat
@freezed
class AudioMessage with _$AudioMessage {
const factory AudioMessage({
required String id,
required String sessionId,
required String audioUrl,
required ChatParticipant sender,
required DateTime timestamp,
required Duration duration,
required int fileSize, // in bytes
@Default(MessageStatus.sent) MessageStatus status,
String? transcription, // optional text transcription
String? metadata,
}) = _AudioMessage;
factory AudioMessage.fromJson(Map<String, dynamic> json) =>
_$AudioMessageFromJson(json);
}
/// Participant in chat conversation
@freezed
class ChatParticipant with _$ChatParticipant {
const factory ChatParticipant.user({
required String id,
required String name,
String? avatarUrl,
}) = ChatParticipantUser;
const factory ChatParticipant.assistant({
required String id,
required String name,
String? avatarUrl,
String? model, // LLM model name
}) = ChatParticipantAssistant;
factory ChatParticipant.fromJson(Map<String, dynamic> json) =>
_$ChatParticipantFromJson(json);
}
/// Message delivery status
enum MessageStatus {
@JsonValue('sending')
sending,
@JsonValue('sent')
sent,
@JsonValue('delivered')
delivered,
@JsonValue('error')
error,
}
/// Extension for MessageStatus with display properties
extension MessageStatusExtension on MessageStatus {
String get displayName {
switch (this) {
case MessageStatus.sending:
return 'Отправляется...';
case MessageStatus.sent:
return 'Отправлено';
case MessageStatus.delivered:
return 'Доставлено';
case MessageStatus.error:
return 'Ошибка';
}
}
bool get isError => this == MessageStatus.error;
bool get isSending => this == MessageStatus.sending;
bool get isDelivered => this == MessageStatus.delivered || this == MessageStatus.sent;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,134 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_message.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ChatMessageTextImpl _$$ChatMessageTextImplFromJson(
Map<String, dynamic> json,
) => _$ChatMessageTextImpl(
TextMessage.fromJson(json['message'] as Map<String, dynamic>),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$ChatMessageTextImplToJson(
_$ChatMessageTextImpl instance,
) => <String, dynamic>{
'message': instance.message,
'runtimeType': instance.$type,
};
_$ChatMessageAudioImpl _$$ChatMessageAudioImplFromJson(
Map<String, dynamic> json,
) => _$ChatMessageAudioImpl(
AudioMessage.fromJson(json['message'] as Map<String, dynamic>),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$ChatMessageAudioImplToJson(
_$ChatMessageAudioImpl instance,
) => <String, dynamic>{
'message': instance.message,
'runtimeType': instance.$type,
};
_$TextMessageImpl _$$TextMessageImplFromJson(Map<String, dynamic> json) =>
_$TextMessageImpl(
id: json['id'] as String,
sessionId: json['sessionId'] as String,
content: json['content'] as String,
sender: ChatParticipant.fromJson(json['sender'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
status:
$enumDecodeNullable(_$MessageStatusEnumMap, json['status']) ??
MessageStatus.sent,
metadata: json['metadata'] as String?,
);
Map<String, dynamic> _$$TextMessageImplToJson(_$TextMessageImpl instance) =>
<String, dynamic>{
'id': instance.id,
'sessionId': instance.sessionId,
'content': instance.content,
'sender': instance.sender,
'timestamp': instance.timestamp.toIso8601String(),
'status': _$MessageStatusEnumMap[instance.status]!,
'metadata': instance.metadata,
};
const _$MessageStatusEnumMap = {
MessageStatus.sending: 'sending',
MessageStatus.sent: 'sent',
MessageStatus.delivered: 'delivered',
MessageStatus.error: 'error',
};
_$AudioMessageImpl _$$AudioMessageImplFromJson(Map<String, dynamic> json) =>
_$AudioMessageImpl(
id: json['id'] as String,
sessionId: json['sessionId'] as String,
audioUrl: json['audioUrl'] as String,
sender: ChatParticipant.fromJson(json['sender'] as Map<String, dynamic>),
timestamp: DateTime.parse(json['timestamp'] as String),
duration: Duration(microseconds: (json['duration'] as num).toInt()),
fileSize: (json['fileSize'] as num).toInt(),
status:
$enumDecodeNullable(_$MessageStatusEnumMap, json['status']) ??
MessageStatus.sent,
transcription: json['transcription'] as String?,
metadata: json['metadata'] as String?,
);
Map<String, dynamic> _$$AudioMessageImplToJson(_$AudioMessageImpl instance) =>
<String, dynamic>{
'id': instance.id,
'sessionId': instance.sessionId,
'audioUrl': instance.audioUrl,
'sender': instance.sender,
'timestamp': instance.timestamp.toIso8601String(),
'duration': instance.duration.inMicroseconds,
'fileSize': instance.fileSize,
'status': _$MessageStatusEnumMap[instance.status]!,
'transcription': instance.transcription,
'metadata': instance.metadata,
};
_$ChatParticipantUserImpl _$$ChatParticipantUserImplFromJson(
Map<String, dynamic> json,
) => _$ChatParticipantUserImpl(
id: json['id'] as String,
name: json['name'] as String,
avatarUrl: json['avatarUrl'] as String?,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$ChatParticipantUserImplToJson(
_$ChatParticipantUserImpl instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'avatarUrl': instance.avatarUrl,
'runtimeType': instance.$type,
};
_$ChatParticipantAssistantImpl _$$ChatParticipantAssistantImplFromJson(
Map<String, dynamic> json,
) => _$ChatParticipantAssistantImpl(
id: json['id'] as String,
name: json['name'] as String,
avatarUrl: json['avatarUrl'] as String?,
model: json['model'] as String?,
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$$ChatParticipantAssistantImplToJson(
_$ChatParticipantAssistantImpl instance,
) => <String, dynamic>{
'id': instance.id,
'name': instance.name,
'avatarUrl': instance.avatarUrl,
'model': instance.model,
'runtimeType': instance.$type,
};

View file

@ -0,0 +1,114 @@
/// Non-serializable chat models and utilities
/// These models contain binary data or platform-specific types that cannot be JSON serialized
import 'dart:typed_data';
/// Request to send audio message to chat with binary data
class SendAudioMessageData {
const SendAudioMessageData({
required this.sessionId,
required this.audioData,
required this.duration,
this.fileName,
this.mimeType,
this.transcription,
this.metadata,
});
final String sessionId;
final Uint8List audioData; // Binary audio data
final Duration duration;
final String? fileName;
final String? mimeType; // e.g., 'audio/webm', 'audio/wav'
final String? transcription;
final String? metadata;
/// Create from web audio recording (Blob/ArrayBuffer)
factory SendAudioMessageData.fromWebAudio({
required String sessionId,
required dynamic audioBlob, // Web Blob or ArrayBuffer
required Duration duration,
String? transcription,
String? metadata,
}) {
// Convert to Uint8List (implementation depends on web_audio_api or similar)
final audioData = Uint8List(0); // Placeholder - actual conversion needed
return SendAudioMessageData(
sessionId: sessionId,
audioData: audioData,
duration: duration,
fileName: 'recording.webm',
mimeType: 'audio/webm',
transcription: transcription,
metadata: metadata,
);
}
/// Get file size in bytes
int get fileSize => audioData.length;
/// Validate audio data
bool get isValid => audioData.isNotEmpty && duration.inMilliseconds > 0;
}
/// Audio recording state
enum AudioRecordingState {
idle,
recording,
paused,
stopped,
processing,
error,
}
/// Audio playback state
enum AudioPlaybackState {
idle,
loading,
playing,
paused,
stopped,
error,
}
/// Audio recording session information
class AudioRecordingSession {
AudioRecordingSession({
required this.startTime,
this.endTime,
this.duration = Duration.zero,
});
final DateTime startTime;
DateTime? endTime;
Duration duration;
bool get isActive => endTime == null;
bool get isCompleted => endTime != null;
void stop() {
endTime = DateTime.now();
duration = endTime!.difference(startTime);
}
}
/// Audio playback information
class AudioPlaybackInfo {
const AudioPlaybackInfo({
required this.currentTime,
required this.totalDuration,
required this.isPlaying,
required this.volume,
});
final Duration currentTime;
final Duration totalDuration;
final bool isPlaying;
final double volume; // 0.0 to 1.0
double get progress => totalDuration.inMilliseconds > 0
? currentTime.inMilliseconds / totalDuration.inMilliseconds
: 0.0;
bool get isCompleted => currentTime >= totalDuration;
}

View file

@ -0,0 +1,69 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_session.freezed.dart';
part 'chat_session.g.dart';
/// Chat session containing conversation between user and assistant
@freezed
class ChatSession with _$ChatSession {
const factory ChatSession({
required String id,
required String userId,
required String title,
required DateTime createdAt,
required DateTime updatedAt,
@Default(ChatSessionStatus.active) ChatSessionStatus status,
String? description,
@Default([]) List<String> tags,
@Default(0) int messageCount,
String? lastMessagePreview,
DateTime? lastMessageAt,
}) = _ChatSession;
factory ChatSession.fromJson(Map<String, dynamic> json) =>
_$ChatSessionFromJson(json);
}
/// Status of chat session
enum ChatSessionStatus {
@JsonValue('active')
active,
@JsonValue('archived')
archived,
@JsonValue('deleted')
deleted,
}
/// Extension for ChatSessionStatus
extension ChatSessionStatusExtension on ChatSessionStatus {
String get displayName {
switch (this) {
case ChatSessionStatus.active:
return 'Активный';
case ChatSessionStatus.archived:
return 'Архивирован';
case ChatSessionStatus.deleted:
return 'Удален';
}
}
bool get isActive => this == ChatSessionStatus.active;
bool get isArchived => this == ChatSessionStatus.archived;
bool get isDeleted => this == ChatSessionStatus.deleted;
}
/// DTO for updating chat session
@freezed
class UpdateChatSessionRequest with _$UpdateChatSessionRequest {
const factory UpdateChatSessionRequest({
String? title,
String? description,
List<String>? tags,
ChatSessionStatus? status,
}) = _UpdateChatSessionRequest;
factory UpdateChatSessionRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateChatSessionRequestFromJson(json);
}

View file

@ -0,0 +1,657 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_session.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
ChatSession _$ChatSessionFromJson(Map<String, dynamic> json) {
return _ChatSession.fromJson(json);
}
/// @nodoc
mixin _$ChatSession {
String get id => throw _privateConstructorUsedError;
String get userId => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
DateTime get createdAt => throw _privateConstructorUsedError;
DateTime get updatedAt => throw _privateConstructorUsedError;
ChatSessionStatus get status => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
List<String> get tags => throw _privateConstructorUsedError;
int get messageCount => throw _privateConstructorUsedError;
String? get lastMessagePreview => throw _privateConstructorUsedError;
DateTime? get lastMessageAt => throw _privateConstructorUsedError;
/// Serializes this ChatSession to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ChatSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatSessionCopyWith<ChatSession> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatSessionCopyWith<$Res> {
factory $ChatSessionCopyWith(
ChatSession value,
$Res Function(ChatSession) then,
) = _$ChatSessionCopyWithImpl<$Res, ChatSession>;
@useResult
$Res call({
String id,
String userId,
String title,
DateTime createdAt,
DateTime updatedAt,
ChatSessionStatus status,
String? description,
List<String> tags,
int messageCount,
String? lastMessagePreview,
DateTime? lastMessageAt,
});
}
/// @nodoc
class _$ChatSessionCopyWithImpl<$Res, $Val extends ChatSession>
implements $ChatSessionCopyWith<$Res> {
_$ChatSessionCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? userId = null,
Object? title = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? status = null,
Object? description = freezed,
Object? tags = null,
Object? messageCount = null,
Object? lastMessagePreview = freezed,
Object? lastMessageAt = freezed,
}) {
return _then(
_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as ChatSessionStatus,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
messageCount: null == messageCount
? _value.messageCount
: messageCount // ignore: cast_nullable_to_non_nullable
as int,
lastMessagePreview: freezed == lastMessagePreview
? _value.lastMessagePreview
: lastMessagePreview // ignore: cast_nullable_to_non_nullable
as String?,
lastMessageAt: freezed == lastMessageAt
? _value.lastMessageAt
: lastMessageAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ChatSessionImplCopyWith<$Res>
implements $ChatSessionCopyWith<$Res> {
factory _$$ChatSessionImplCopyWith(
_$ChatSessionImpl value,
$Res Function(_$ChatSessionImpl) then,
) = __$$ChatSessionImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String id,
String userId,
String title,
DateTime createdAt,
DateTime updatedAt,
ChatSessionStatus status,
String? description,
List<String> tags,
int messageCount,
String? lastMessagePreview,
DateTime? lastMessageAt,
});
}
/// @nodoc
class __$$ChatSessionImplCopyWithImpl<$Res>
extends _$ChatSessionCopyWithImpl<$Res, _$ChatSessionImpl>
implements _$$ChatSessionImplCopyWith<$Res> {
__$$ChatSessionImplCopyWithImpl(
_$ChatSessionImpl _value,
$Res Function(_$ChatSessionImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatSession
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? userId = null,
Object? title = null,
Object? createdAt = null,
Object? updatedAt = null,
Object? status = null,
Object? description = freezed,
Object? tags = null,
Object? messageCount = null,
Object? lastMessagePreview = freezed,
Object? lastMessageAt = freezed,
}) {
return _then(
_$ChatSessionImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
createdAt: null == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,
updatedAt: null == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,
status: null == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as ChatSessionStatus,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
messageCount: null == messageCount
? _value.messageCount
: messageCount // ignore: cast_nullable_to_non_nullable
as int,
lastMessagePreview: freezed == lastMessagePreview
? _value.lastMessagePreview
: lastMessagePreview // ignore: cast_nullable_to_non_nullable
as String?,
lastMessageAt: freezed == lastMessageAt
? _value.lastMessageAt
: lastMessageAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$ChatSessionImpl implements _ChatSession {
const _$ChatSessionImpl({
required this.id,
required this.userId,
required this.title,
required this.createdAt,
required this.updatedAt,
this.status = ChatSessionStatus.active,
this.description,
final List<String> tags = const [],
this.messageCount = 0,
this.lastMessagePreview,
this.lastMessageAt,
}) : _tags = tags;
factory _$ChatSessionImpl.fromJson(Map<String, dynamic> json) =>
_$$ChatSessionImplFromJson(json);
@override
final String id;
@override
final String userId;
@override
final String title;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
@override
@JsonKey()
final ChatSessionStatus status;
@override
final String? description;
final List<String> _tags;
@override
@JsonKey()
List<String> get tags {
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tags);
}
@override
@JsonKey()
final int messageCount;
@override
final String? lastMessagePreview;
@override
final DateTime? lastMessageAt;
@override
String toString() {
return 'ChatSession(id: $id, userId: $userId, title: $title, createdAt: $createdAt, updatedAt: $updatedAt, status: $status, description: $description, tags: $tags, messageCount: $messageCount, lastMessagePreview: $lastMessagePreview, lastMessageAt: $lastMessageAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatSessionImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.userId, userId) || other.userId == userId) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt) &&
(identical(other.status, status) || other.status == status) &&
(identical(other.description, description) ||
other.description == description) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
(identical(other.messageCount, messageCount) ||
other.messageCount == messageCount) &&
(identical(other.lastMessagePreview, lastMessagePreview) ||
other.lastMessagePreview == lastMessagePreview) &&
(identical(other.lastMessageAt, lastMessageAt) ||
other.lastMessageAt == lastMessageAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
userId,
title,
createdAt,
updatedAt,
status,
description,
const DeepCollectionEquality().hash(_tags),
messageCount,
lastMessagePreview,
lastMessageAt,
);
/// Create a copy of ChatSession
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatSessionImplCopyWith<_$ChatSessionImpl> get copyWith =>
__$$ChatSessionImplCopyWithImpl<_$ChatSessionImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ChatSessionImplToJson(this);
}
}
abstract class _ChatSession implements ChatSession {
const factory _ChatSession({
required final String id,
required final String userId,
required final String title,
required final DateTime createdAt,
required final DateTime updatedAt,
final ChatSessionStatus status,
final String? description,
final List<String> tags,
final int messageCount,
final String? lastMessagePreview,
final DateTime? lastMessageAt,
}) = _$ChatSessionImpl;
factory _ChatSession.fromJson(Map<String, dynamic> json) =
_$ChatSessionImpl.fromJson;
@override
String get id;
@override
String get userId;
@override
String get title;
@override
DateTime get createdAt;
@override
DateTime get updatedAt;
@override
ChatSessionStatus get status;
@override
String? get description;
@override
List<String> get tags;
@override
int get messageCount;
@override
String? get lastMessagePreview;
@override
DateTime? get lastMessageAt;
/// Create a copy of ChatSession
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatSessionImplCopyWith<_$ChatSessionImpl> get copyWith =>
throw _privateConstructorUsedError;
}
UpdateChatSessionRequest _$UpdateChatSessionRequestFromJson(
Map<String, dynamic> json,
) {
return _UpdateChatSessionRequest.fromJson(json);
}
/// @nodoc
mixin _$UpdateChatSessionRequest {
String? get title => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
List<String>? get tags => throw _privateConstructorUsedError;
ChatSessionStatus? get status => throw _privateConstructorUsedError;
/// Serializes this UpdateChatSessionRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of UpdateChatSessionRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UpdateChatSessionRequestCopyWith<UpdateChatSessionRequest> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UpdateChatSessionRequestCopyWith<$Res> {
factory $UpdateChatSessionRequestCopyWith(
UpdateChatSessionRequest value,
$Res Function(UpdateChatSessionRequest) then,
) = _$UpdateChatSessionRequestCopyWithImpl<$Res, UpdateChatSessionRequest>;
@useResult
$Res call({
String? title,
String? description,
List<String>? tags,
ChatSessionStatus? status,
});
}
/// @nodoc
class _$UpdateChatSessionRequestCopyWithImpl<
$Res,
$Val extends UpdateChatSessionRequest
>
implements $UpdateChatSessionRequestCopyWith<$Res> {
_$UpdateChatSessionRequestCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UpdateChatSessionRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? title = freezed,
Object? description = freezed,
Object? tags = freezed,
Object? status = freezed,
}) {
return _then(
_value.copyWith(
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
status: freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as ChatSessionStatus?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$UpdateChatSessionRequestImplCopyWith<$Res>
implements $UpdateChatSessionRequestCopyWith<$Res> {
factory _$$UpdateChatSessionRequestImplCopyWith(
_$UpdateChatSessionRequestImpl value,
$Res Function(_$UpdateChatSessionRequestImpl) then,
) = __$$UpdateChatSessionRequestImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String? title,
String? description,
List<String>? tags,
ChatSessionStatus? status,
});
}
/// @nodoc
class __$$UpdateChatSessionRequestImplCopyWithImpl<$Res>
extends
_$UpdateChatSessionRequestCopyWithImpl<
$Res,
_$UpdateChatSessionRequestImpl
>
implements _$$UpdateChatSessionRequestImplCopyWith<$Res> {
__$$UpdateChatSessionRequestImplCopyWithImpl(
_$UpdateChatSessionRequestImpl _value,
$Res Function(_$UpdateChatSessionRequestImpl) _then,
) : super(_value, _then);
/// Create a copy of UpdateChatSessionRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? title = freezed,
Object? description = freezed,
Object? tags = freezed,
Object? status = freezed,
}) {
return _then(
_$UpdateChatSessionRequestImpl(
title: freezed == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
tags: freezed == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>?,
status: freezed == status
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as ChatSessionStatus?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$UpdateChatSessionRequestImpl implements _UpdateChatSessionRequest {
const _$UpdateChatSessionRequestImpl({
this.title,
this.description,
final List<String>? tags,
this.status,
}) : _tags = tags;
factory _$UpdateChatSessionRequestImpl.fromJson(Map<String, dynamic> json) =>
_$$UpdateChatSessionRequestImplFromJson(json);
@override
final String? title;
@override
final String? description;
final List<String>? _tags;
@override
List<String>? get tags {
final value = _tags;
if (value == null) return null;
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
final ChatSessionStatus? status;
@override
String toString() {
return 'UpdateChatSessionRequest(title: $title, description: $description, tags: $tags, status: $status)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UpdateChatSessionRequestImpl &&
(identical(other.title, title) || other.title == title) &&
(identical(other.description, description) ||
other.description == description) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
(identical(other.status, status) || other.status == status));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
title,
description,
const DeepCollectionEquality().hash(_tags),
status,
);
/// Create a copy of UpdateChatSessionRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UpdateChatSessionRequestImplCopyWith<_$UpdateChatSessionRequestImpl>
get copyWith =>
__$$UpdateChatSessionRequestImplCopyWithImpl<
_$UpdateChatSessionRequestImpl
>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UpdateChatSessionRequestImplToJson(this);
}
}
abstract class _UpdateChatSessionRequest implements UpdateChatSessionRequest {
const factory _UpdateChatSessionRequest({
final String? title,
final String? description,
final List<String>? tags,
final ChatSessionStatus? status,
}) = _$UpdateChatSessionRequestImpl;
factory _UpdateChatSessionRequest.fromJson(Map<String, dynamic> json) =
_$UpdateChatSessionRequestImpl.fromJson;
@override
String? get title;
@override
String? get description;
@override
List<String>? get tags;
@override
ChatSessionStatus? get status;
/// Create a copy of UpdateChatSessionRequest
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UpdateChatSessionRequestImplCopyWith<_$UpdateChatSessionRequestImpl>
get copyWith => throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,67 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_session.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ChatSessionImpl _$$ChatSessionImplFromJson(Map<String, dynamic> json) =>
_$ChatSessionImpl(
id: json['id'] as String,
userId: json['userId'] as String,
title: json['title'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
status:
$enumDecodeNullable(_$ChatSessionStatusEnumMap, json['status']) ??
ChatSessionStatus.active,
description: json['description'] as String?,
tags:
(json['tags'] as List<dynamic>?)?.map((e) => e as String).toList() ??
const [],
messageCount: (json['messageCount'] as num?)?.toInt() ?? 0,
lastMessagePreview: json['lastMessagePreview'] as String?,
lastMessageAt: json['lastMessageAt'] == null
? null
: DateTime.parse(json['lastMessageAt'] as String),
);
Map<String, dynamic> _$$ChatSessionImplToJson(_$ChatSessionImpl instance) =>
<String, dynamic>{
'id': instance.id,
'userId': instance.userId,
'title': instance.title,
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
'status': _$ChatSessionStatusEnumMap[instance.status]!,
'description': instance.description,
'tags': instance.tags,
'messageCount': instance.messageCount,
'lastMessagePreview': instance.lastMessagePreview,
'lastMessageAt': instance.lastMessageAt?.toIso8601String(),
};
const _$ChatSessionStatusEnumMap = {
ChatSessionStatus.active: 'active',
ChatSessionStatus.archived: 'archived',
ChatSessionStatus.deleted: 'deleted',
};
_$UpdateChatSessionRequestImpl _$$UpdateChatSessionRequestImplFromJson(
Map<String, dynamic> json,
) => _$UpdateChatSessionRequestImpl(
title: json['title'] as String?,
description: json['description'] as String?,
tags: (json['tags'] as List<dynamic>?)?.map((e) => e as String).toList(),
status: $enumDecodeNullable(_$ChatSessionStatusEnumMap, json['status']),
);
Map<String, dynamic> _$$UpdateChatSessionRequestImplToJson(
_$UpdateChatSessionRequestImpl instance,
) => <String, dynamic>{
'title': instance.title,
'description': instance.description,
'tags': instance.tags,
'status': _$ChatSessionStatusEnumMap[instance.status],
};

View file

@ -0,0 +1,47 @@
import '../models/chat_api_simple.dart';
import '../models/chat_basic.dart';
/// Abstract repository interface for chat operations
///
/// This allows the chat module to be decoupled from specific HTTP implementations
abstract class ChatRepository {
/// Create new chat session
Future<ChatBasicSession> createChatSession(CreateChatSessionRequest request);
/// Get user's chat sessions
Future<List<ChatBasicSession>> getChatSessions({
int limit,
String? afterSessionId,
});
/// Get specific chat session
Future<ChatBasicSession> getChatSession(String sessionId);
/// Send text message
Future<ChatMessageResponse> sendTextMessage(SendTextMessageRequest request);
/// Send audio message (multipart/form-data)
Future<ChatMessageResponse> sendAudioMessage(
String sessionId,
dynamic audioData,
Duration duration, {
String? fileName,
String? mimeType,
});
/// Get messages for chat session
Future<List<ChatMessageResponse>> getChatMessages(
String sessionId, {
int limit,
String? beforeMessageId,
});
/// Update chat session
Future<ChatBasicSession> updateChatSession(
String sessionId,
Map<String, dynamic> updates,
);
/// Delete chat session
Future<void> deleteChatSession(String sessionId);
}

View file

@ -0,0 +1,135 @@
import 'dart:developer';
import '../models/chat_api_simple.dart';
import '../models/chat_basic.dart';
import 'chat_repository.dart';
/// Service for chat functionality
class ChatService {
ChatService({
required ChatRepository chatRepository,
}) : _chatRepository = chatRepository;
final ChatRepository _chatRepository;
/// Create new chat session
Future<ChatBasicSession> createSession({
required String title,
String? description,
}) async {
log('Creating new chat session: $title', name: 'ChatService');
final request = CreateChatSessionRequest(title: title);
final session = await _chatRepository.createChatSession(request);
log('Chat session created: ${session.id}', name: 'ChatService');
return session;
}
/// Get chat session by ID
Future<ChatBasicSession> getSession(String sessionId) async {
log('Getting chat session: $sessionId', name: 'ChatService');
final session = await _chatRepository.getChatSession(sessionId);
log('Chat session loaded: ${session.title}', name: 'ChatService');
return session;
}
/// Get user's chat sessions
Future<List<ChatBasicSession>> getSessions({
int limit = 20,
String? afterSessionId,
}) async {
log('Getting chat sessions (limit: $limit)', name: 'ChatService');
final sessions = await _chatRepository.getChatSessions(
limit: limit,
afterSessionId: afterSessionId,
);
log('Loaded ${sessions.length} chat sessions', name: 'ChatService');
return sessions;
}
/// Send text message
Future<ChatMessageResponse> sendTextMessage(
String sessionId,
String content,
) async {
log('Sending text message to session: $sessionId', name: 'ChatService');
final request = SendTextMessageRequest(
sessionId: sessionId,
content: content,
);
final response = await _chatRepository.sendTextMessage(request);
log('Text message sent successfully', name: 'ChatService');
return response;
}
/// Send audio message
Future<ChatMessageResponse> sendAudioMessage(
String sessionId,
dynamic audioData,
Duration duration, {
String? transcription,
String? fileName,
String? mimeType,
}) async {
log('Sending audio message to session: $sessionId', name: 'ChatService');
final response = await _chatRepository.sendAudioMessage(
sessionId,
audioData,
duration,
fileName: fileName,
mimeType: mimeType,
);
log('Audio message sent successfully', name: 'ChatService');
return response;
}
/// Get messages for session
Future<List<ChatMessageResponse>> getMessages(
String sessionId, {
int limit = 50,
String? beforeMessageId,
}) async {
log('Getting messages for session: $sessionId (limit: $limit)', name: 'ChatService');
final messages = await _chatRepository.getChatMessages(
sessionId,
limit: limit,
beforeMessageId: beforeMessageId,
);
log('Loaded ${messages.length} messages', name: 'ChatService');
return messages;
}
/// Update chat session
Future<ChatBasicSession> updateSession(
String sessionId,
Map<String, dynamic> updates,
) async {
log('Updating chat session: $sessionId', name: 'ChatService');
final session = await _chatRepository.updateChatSession(sessionId, updates);
log('Chat session updated: ${session.title}', name: 'ChatService');
return session;
}
/// Delete chat session
Future<void> deleteSession(String sessionId) async {
log('Deleting chat session: $sessionId', name: 'ChatService');
await _chatRepository.deleteChatSession(sessionId);
log('Chat session deleted', name: 'ChatService');
}
}

View file

@ -0,0 +1,216 @@
import 'dart:async';
import 'dart:developer';
import 'package:yx_state/yx_state.dart';
import '../models/chat_basic.dart';
import '../models/chat_message.dart';
import '../services/chat_service.dart';
/// State for chat functionality
class ChatState {
const ChatState._();
const factory ChatState.loading() = ChatStateLoading;
const factory ChatState.loaded({
required ChatBasicSession session,
required List<ChatMessage> messages,
bool isSendingMessage,
bool isLoadingMore,
bool hasMoreMessages,
String? errorMessage,
}) = ChatStateLoaded;
const factory ChatState.error(String message) = ChatStateError;
}
/// Loading state
class ChatStateLoading extends ChatState {
const ChatStateLoading() : super._();
}
/// Loaded state with session and messages
class ChatStateLoaded extends ChatState {
const ChatStateLoaded({
required this.session,
required this.messages,
this.isSendingMessage = false,
this.isLoadingMore = false,
this.hasMoreMessages = true,
this.errorMessage,
}) : super._();
final ChatBasicSession session;
final List<ChatMessage> messages;
final bool isSendingMessage;
final bool isLoadingMore;
final bool hasMoreMessages;
final String? errorMessage;
ChatStateLoaded copyWith({
ChatBasicSession? session,
List<ChatMessage>? messages,
bool? isSendingMessage,
bool? isLoadingMore,
bool? hasMoreMessages,
String? errorMessage,
}) {
return ChatStateLoaded(
session: session ?? this.session,
messages: messages ?? this.messages,
isSendingMessage: isSendingMessage ?? this.isSendingMessage,
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
hasMoreMessages: hasMoreMessages ?? this.hasMoreMessages,
errorMessage: errorMessage ?? this.errorMessage,
);
}
}
/// Error state
class ChatStateError extends ChatState {
const ChatStateError(this.message) : super._();
final String message;
}
/// State manager for chat functionality
class ChatStateManager extends StateManager<ChatState> {
ChatStateManager({
required ChatService chatService,
}) : _chatService = chatService,
super(const ChatStateLoading());
final ChatService _chatService;
ChatBasicSession? _currentSession;
final List<ChatMessage> _messages = [];
/// Initialize chat with session
Future<void> initializeChat(String sessionId) => handle((emit) async {
log('Initializing chat for session: $sessionId', name: 'ChatStateManager');
emit(const ChatStateLoading());
try {
// Load session info and messages
final session = await _chatService.getSession(sessionId);
final messages = await _chatService.getMessages(sessionId, limit: 50);
_currentSession = session;
_messages.clear();
// Convert ChatMessageResponse to ChatMessage - simplified for now
// In real implementation, this conversion would be more complex
emit(ChatStateLoaded(
session: session,
messages: List.unmodifiable(_messages),
hasMoreMessages: messages.length >= 50,
));
log('Chat initialized with ${messages.length} messages', name: 'ChatStateManager');
} catch (e, s) {
log(
'Error initializing chat',
error: e,
stackTrace: s,
name: 'ChatStateManager',
);
emit(ChatStateError('Failed to load chat: ${e.toString()}'));
}
});
/// Send text message
Future<void> sendTextMessage(String content) => handle((emit) async {
final currentState = state;
if (currentState is! ChatStateLoaded || _currentSession == null) {
log('Cannot send message: chat not initialized', name: 'ChatStateManager');
return;
}
// Add sending state
emit(currentState.copyWith(isSendingMessage: true, errorMessage: null));
try {
// Send to server
final response = await _chatService.sendTextMessage(_currentSession!.id, content);
// Add assistant response
// Simplified - in real implementation would convert response to ChatMessage
emit(currentState.copyWith(
isSendingMessage: false,
));
log('Message sent successfully', name: 'ChatStateManager');
} catch (e, s) {
log(
'Error sending message',
error: e,
stackTrace: s,
name: 'ChatStateManager',
);
emit(currentState.copyWith(
isSendingMessage: false,
errorMessage: 'Failed to send message: ${e.toString()}',
));
}
});
/// Send audio message
Future<void> sendAudioMessage(
dynamic audioData,
Duration duration, {
String? transcription,
}) => handle((emit) async {
final currentState = state;
if (currentState is! ChatStateLoaded || _currentSession == null) {
log('Cannot send audio message: chat not initialized', name: 'ChatStateManager');
return;
}
// Add sending state
emit(currentState.copyWith(isSendingMessage: true, errorMessage: null));
try {
// Send to server
final response = await _chatService.sendAudioMessage(
_currentSession!.id,
audioData,
duration,
transcription: transcription,
);
emit(currentState.copyWith(
isSendingMessage: false,
));
log('Audio message sent successfully', name: 'ChatStateManager');
} catch (e, s) {
log(
'Error sending audio message',
error: e,
stackTrace: s,
name: 'ChatStateManager',
);
emit(currentState.copyWith(
isSendingMessage: false,
errorMessage: 'Failed to send audio message: ${e.toString()}',
));
}
});
/// Clear error message
void clearError() => handle((emit) async {
final currentState = state;
if (currentState is ChatStateLoaded) {
emit(currentState.copyWith(errorMessage: null));
}
});
/// Get current session
ChatBasicSession? get currentSession => _currentSession;
/// Get messages count
int get messagesCount => _messages.length;
}

View file

@ -0,0 +1,629 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
source: hosted
version: "7.7.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
url: "https://pub.dev"
source: hosted
version: "8.12.0"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243"
url: "https://pub.dev"
source: hosted
version: "4.11.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
dio:
dependency: "direct main"
description:
name: dio
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.9.0"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
http:
dependency: transitive
description:
name: http
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.5.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
name: js
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
json_annotation:
dependency: "direct main"
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
json_serializable:
dependency: "direct dev"
description:
name: json_serializable
sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c
url: "https://pub.dev"
source: hosted
version: "6.9.5"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev"
source: hosted
version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: "direct main"
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mocktail:
dependency: "direct dev"
description:
name: mocktail
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_helper:
dependency: transitive
description:
name: source_helper
sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca
url: "https://pub.dev"
source: hosted
version: "1.3.7"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a"
url: "https://pub.dev"
source: hosted
version: "1.1.4"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yx_scope:
dependency: "direct main"
description:
name: yx_scope
sha256: "9ba98b442261596311363bf7361622e5ccc67189705b8d042ca23c9de366f8bf"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
yx_state:
dependency: "direct main"
description:
name: yx_state
sha256: "13ab50d3875686f65058cb9abdfbb1725e9d15ea99ade51b1f3a5f93727a8eaa"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"

View file

@ -0,0 +1,43 @@
name: mnemo_cards_chat
description: "Chat module for mnemo_cards applications with LLM integration"
version: 1.0.0
publish_to: 'none'
environment:
sdk: ^3.9.2
dependencies:
flutter:
sdk: flutter
# YX Framework for DI and state management
yx_scope: ^1.1.2
yx_state: ^1.0.0
# HTTP client
dio: ^5.3.3
# Immutable data models
freezed_annotation: ^2.4.1
json_annotation: ^4.7.0
# Common utilities
meta: ^1.8.0
dev_dependencies:
flutter_test:
sdk: flutter
# Code generation
build_runner: ^2.4.13
freezed: ^2.4.5
json_serializable: ^6.8.0
# Linting
flutter_lints: ^6.0.0
# Testing utilities
mocktail: ^1.0.3
flutter:
uses-material-design: true

View file

@ -0,0 +1,359 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mnemo_cards_chat/domain/models/chat_message.dart';
void main() {
group('ChatMessage', () {
group('MessageStatus', () {
test('has correct display names', () {
expect(MessageStatus.sending.displayName, 'Отправляется...');
expect(MessageStatus.sent.displayName, 'Отправлено');
expect(MessageStatus.delivered.displayName, 'Доставлено');
expect(MessageStatus.error.displayName, 'Ошибка');
});
test('has correct status checks', () {
expect(MessageStatus.sending.isSending, isTrue);
expect(MessageStatus.sent.isSending, isFalse);
expect(MessageStatus.delivered.isSending, isFalse);
expect(MessageStatus.error.isSending, isFalse);
expect(MessageStatus.error.isError, isTrue);
expect(MessageStatus.sent.isError, isFalse);
expect(MessageStatus.sent.isDelivered, isTrue);
expect(MessageStatus.delivered.isDelivered, isTrue);
expect(MessageStatus.sending.isDelivered, isFalse);
expect(MessageStatus.error.isDelivered, isFalse);
});
});
group('ChatParticipant', () {
test('creates user participant correctly', () {
final participant = ChatParticipant.user(
id: 'user_123',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
);
expect(participant, isA<ChatParticipantUser>());
final user = participant as ChatParticipantUser;
expect(user.id, 'user_123');
expect(user.name, 'John Doe');
expect(user.avatarUrl, 'https://example.com/avatar.jpg');
});
test('creates assistant participant correctly', () {
final participant = ChatParticipant.assistant(
id: 'assistant_123',
name: 'AI Assistant',
avatarUrl: 'https://example.com/bot.jpg',
model: 'gpt-4',
);
expect(participant, isA<ChatParticipantAssistant>());
final assistant = participant as ChatParticipantAssistant;
expect(assistant.id, 'assistant_123');
expect(assistant.name, 'AI Assistant');
expect(assistant.avatarUrl, 'https://example.com/bot.jpg');
expect(assistant.model, 'gpt-4');
});
test('user participant supports optional avatar', () {
final participant = ChatParticipant.user(
id: 'user_123',
name: 'John Doe',
);
final user = participant as ChatParticipantUser;
expect(user.avatarUrl, isNull);
});
test('assistant participant supports optional model', () {
final participant = ChatParticipant.assistant(
id: 'assistant_123',
name: 'AI Assistant',
);
final assistant = participant as ChatParticipantAssistant;
expect(assistant.model, isNull);
});
});
group('TextMessage', () {
test('creates text message correctly', () {
final timestamp = DateTime.now();
final sender = ChatParticipant.user(id: 'user_123', name: 'User');
final message = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello, world!',
sender: sender,
timestamp: timestamp,
status: MessageStatus.sent,
metadata: {'source': 'test'},
);
expect(message.id, 'msg_123');
expect(message.sessionId, 'session_123');
expect(message.content, 'Hello, world!');
expect(message.sender, sender);
expect(message.timestamp, timestamp);
expect(message.status, MessageStatus.sent);
expect(message.metadata, {'source': 'test'});
});
test('supports optional metadata', () {
final message = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
);
expect(message.metadata, isNull);
});
test('has default status', () {
final message = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
);
expect(message.status, MessageStatus.sent);
});
});
group('AudioMessage', () {
test('creates audio message correctly', () {
final timestamp = DateTime.now();
final sender = ChatParticipant.user(id: 'user_123', name: 'User');
const duration = Duration(seconds: 30);
final message = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.mp3',
sender: sender,
timestamp: timestamp,
duration: duration,
fileSize: 1024000,
status: MessageStatus.sent,
transcription: 'Hello from audio',
metadata: {'format': 'mp3'},
);
expect(message.id, 'msg_123');
expect(message.sessionId, 'session_123');
expect(message.audioUrl, 'https://example.com/audio.mp3');
expect(message.sender, sender);
expect(message.timestamp, timestamp);
expect(message.duration, duration);
expect(message.fileSize, 1024000);
expect(message.status, MessageStatus.sent);
expect(message.transcription, 'Hello from audio');
expect(message.metadata, {'format': 'mp3'});
});
test('supports optional transcription and metadata', () {
const duration = Duration(seconds: 10);
final message = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.wav',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
duration: duration,
fileSize: 512000,
);
expect(message.transcription, isNull);
expect(message.metadata, isNull);
});
test('has default status', () {
const duration = Duration(seconds: 5);
final message = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.webm',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
duration: duration,
fileSize: 256000,
);
expect(message.status, MessageStatus.sent);
});
});
group('ChatMessage union', () {
test('creates text message variant', () {
final textMessage = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
);
final chatMessage = ChatMessage.text(textMessage);
expect(chatMessage, isA<ChatMessageText>());
});
test('creates audio message variant', () {
const duration = Duration(seconds: 10);
final audioMessage = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.mp3',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.now(),
duration: duration,
fileSize: 1024000,
);
final chatMessage = ChatMessage.audio(audioMessage);
expect(chatMessage, isA<ChatMessageAudio>());
});
});
group('JSON serialization', () {
test('ChatParticipant serializes and deserializes correctly', () {
final user = ChatParticipant.user(
id: 'user_123',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
);
final json = user.toJson();
final deserialized = ChatParticipant.fromJson(json);
expect(deserialized, equals(user));
});
test('TextMessage serializes and deserializes correctly', () {
final message = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello, world!',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
status: MessageStatus.sent,
metadata: {'source': 'test'},
);
final json = message.toJson();
final deserialized = TextMessage.fromJson(json);
expect(deserialized, equals(message));
});
test('AudioMessage serializes and deserializes correctly', () {
final message = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.mp3',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
duration: const Duration(seconds: 30),
fileSize: 1024000,
status: MessageStatus.sent,
transcription: 'Hello from audio',
);
final json = message.toJson();
final deserialized = AudioMessage.fromJson(json);
expect(deserialized, equals(message));
});
});
group('Equality and hashCode', () {
test('TextMessage equality works correctly', () {
final message1 = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
);
final message2 = TextMessage(
id: 'msg_123',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
);
final message3 = TextMessage(
id: 'msg_456',
sessionId: 'session_123',
content: 'Hello',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
);
expect(message1, equals(message2));
expect(message1, isNot(equals(message3)));
expect(message1.hashCode, equals(message2.hashCode));
expect(message1.hashCode, isNot(equals(message3.hashCode)));
});
test('AudioMessage equality works correctly', () {
const duration = Duration(seconds: 30);
final message1 = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.mp3',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
duration: duration,
fileSize: 1024000,
);
final message2 = AudioMessage(
id: 'msg_123',
sessionId: 'session_123',
audioUrl: 'https://example.com/audio.mp3',
sender: ChatParticipant.user(id: 'user_123', name: 'User'),
timestamp: DateTime.parse('2024-01-01T12:00:00Z'),
duration: duration,
fileSize: 1024000,
);
expect(message1, equals(message2));
expect(message1.hashCode, equals(message2.hashCode));
});
test('ChatParticipant equality works correctly', () {
final user1 = ChatParticipant.user(
id: 'user_123',
name: 'John',
avatarUrl: 'https://example.com/avatar.jpg',
);
final user2 = ChatParticipant.user(
id: 'user_123',
name: 'John',
avatarUrl: 'https://example.com/avatar.jpg',
);
final user3 = ChatParticipant.user(
id: 'user_456',
name: 'John',
avatarUrl: 'https://example.com/avatar.jpg',
);
expect(user1, equals(user2));
expect(user1, isNot(equals(user3)));
expect(user1.hashCode, equals(user2.hashCode));
});
});
});
}

View file

@ -0,0 +1,255 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mnemo_cards_chat/domain/models/chat_session.dart';
void main() {
group('ChatSession', () {
group('ChatSessionStatus', () {
test('has correct display names', () {
expect(ChatSessionStatus.active.displayName, 'Активный');
expect(ChatSessionStatus.archived.displayName, 'Архивирован');
expect(ChatSessionStatus.deleted.displayName, 'Удален');
});
test('has correct status checks', () {
expect(ChatSessionStatus.active.isActive, isTrue);
expect(ChatSessionStatus.archived.isActive, isFalse);
expect(ChatSessionStatus.deleted.isActive, isFalse);
expect(ChatSessionStatus.archived.isArchived, isTrue);
expect(ChatSessionStatus.active.isArchived, isFalse);
expect(ChatSessionStatus.deleted.isArchived, isFalse);
expect(ChatSessionStatus.deleted.isDeleted, isTrue);
expect(ChatSessionStatus.active.isDeleted, isFalse);
expect(ChatSessionStatus.archived.isDeleted, isFalse);
});
});
group('ChatSession', () {
test('creates chat session correctly', () {
final createdAt = DateTime.parse('2024-01-01T10:00:00Z');
final updatedAt = DateTime.parse('2024-01-01T11:00:00Z');
final session = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Test Chat Session',
createdAt: createdAt,
updatedAt: updatedAt,
status: ChatSessionStatus.active,
description: 'A test chat session',
tags: ['test', 'ai'],
messageCount: 42,
lastMessagePreview: 'Hello, how are you?',
lastMessageAt: DateTime.parse('2024-01-01T11:30:00Z'),
);
expect(session.id, 'session_123');
expect(session.userId, 'user_456');
expect(session.title, 'Test Chat Session');
expect(session.createdAt, createdAt);
expect(session.updatedAt, updatedAt);
expect(session.status, ChatSessionStatus.active);
expect(session.description, 'A test chat session');
expect(session.tags, ['test', 'ai']);
expect(session.messageCount, 42);
expect(session.lastMessagePreview, 'Hello, how are you?');
expect(session.lastMessageAt, DateTime.parse('2024-01-01T11:30:00Z'));
});
test('supports optional fields', () {
final session = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Minimal Session',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(session.description, isNull);
expect(session.tags, isEmpty);
expect(session.messageCount, 0);
expect(session.lastMessagePreview, isNull);
expect(session.lastMessageAt, isNull);
expect(session.status, ChatSessionStatus.active);
});
test('has default values', () {
final session = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Test',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
expect(session.status, ChatSessionStatus.active);
expect(session.tags, isEmpty);
expect(session.messageCount, 0);
});
});
group('CreateChatSessionRequest', () {
test('creates request correctly', () {
final request = CreateChatSessionRequest(
title: 'New Chat Session',
description: 'A new chat',
tags: ['ai', 'assistant'],
);
expect(request.title, 'New Chat Session');
expect(request.description, 'A new chat');
expect(request.tags, ['ai', 'assistant']);
});
test('supports optional fields', () {
final request = CreateChatSessionRequest(title: 'Simple Chat');
expect(request.title, 'Simple Chat');
expect(request.description, isNull);
expect(request.tags, isEmpty);
});
});
group('UpdateChatSessionRequest', () {
test('creates update request correctly', () {
final request = UpdateChatSessionRequest(
title: 'Updated Title',
description: 'Updated description',
tags: ['updated'],
status: ChatSessionStatus.archived,
);
expect(request.title, 'Updated Title');
expect(request.description, 'Updated description');
expect(request.tags, ['updated']);
expect(request.status, ChatSessionStatus.archived);
});
test('supports partial updates', () {
final request = UpdateChatSessionRequest(
title: 'New Title',
// Other fields null for partial update
);
expect(request.title, 'New Title');
expect(request.description, isNull);
expect(request.tags, isNull);
expect(request.status, isNull);
});
});
group('JSON serialization', () {
test('ChatSession serializes and deserializes correctly', () {
final session = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Test Session',
createdAt: DateTime.parse('2024-01-01T10:00:00Z'),
updatedAt: DateTime.parse('2024-01-01T11:00:00Z'),
status: ChatSessionStatus.active,
description: 'Test description',
tags: ['test'],
messageCount: 5,
lastMessagePreview: 'Hello',
lastMessageAt: DateTime.parse('2024-01-01T11:30:00Z'),
);
final json = session.toJson();
final deserialized = ChatSession.fromJson(json);
expect(deserialized, equals(session));
});
test('CreateChatSessionRequest serializes and deserializes correctly', () {
final request = CreateChatSessionRequest(
title: 'New Session',
description: 'Description',
tags: ['tag1', 'tag2'],
);
final json = request.toJson();
final deserialized = CreateChatSessionRequest.fromJson(json);
expect(deserialized, equals(request));
});
test('UpdateChatSessionRequest serializes and deserializes correctly', () {
final request = UpdateChatSessionRequest(
title: 'Updated',
status: ChatSessionStatus.archived,
);
final json = request.toJson();
final deserialized = UpdateChatSessionRequest.fromJson(json);
expect(deserialized, equals(request));
});
});
group('Equality and hashCode', () {
test('ChatSession equality works correctly', () {
final session1 = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Test',
createdAt: DateTime.parse('2024-01-01T10:00:00Z'),
updatedAt: DateTime.parse('2024-01-01T11:00:00Z'),
status: ChatSessionStatus.active,
description: 'Desc',
tags: ['tag'],
messageCount: 1,
);
final session2 = ChatSession(
id: 'session_123',
userId: 'user_456',
title: 'Test',
createdAt: DateTime.parse('2024-01-01T10:00:00Z'),
updatedAt: DateTime.parse('2024-01-01T11:00:00Z'),
status: ChatSessionStatus.active,
description: 'Desc',
tags: ['tag'],
messageCount: 1,
);
final session3 = ChatSession(
id: 'session_456',
userId: 'user_456',
title: 'Test',
createdAt: DateTime.parse('2024-01-01T10:00:00Z'),
updatedAt: DateTime.parse('2024-01-01T11:00:00Z'),
);
expect(session1, equals(session2));
expect(session1, isNot(equals(session3)));
expect(session1.hashCode, equals(session2.hashCode));
expect(session1.hashCode, isNot(equals(session3.hashCode)));
});
test('CreateChatSessionRequest equality works correctly', () {
final request1 = CreateChatSessionRequest(
title: 'Test',
description: 'Desc',
tags: ['tag'],
);
final request2 = CreateChatSessionRequest(
title: 'Test',
description: 'Desc',
tags: ['tag'],
);
final request3 = CreateChatSessionRequest(
title: 'Different',
description: 'Desc',
tags: ['tag'],
);
expect(request1, equals(request2));
expect(request1, isNot(equals(request3)));
expect(request1.hashCode, equals(request2.hashCode));
});
});
});
}

View file

@ -0,0 +1,345 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:mnemo_cards_chat/mnemo_cards_chat.dart';
// Mock classes
class MockChatRepository extends Mock implements ChatRepository {}
// Fallback values for mocktail
class CreateChatSessionRequestFake extends Fake implements CreateChatSessionRequest {}
class SendTextMessageRequestFake extends Fake implements SendTextMessageRequest {}
void main() {
setUpAll(() {
registerFallbackValue(CreateChatSessionRequestFake());
registerFallbackValue(SendTextMessageRequestFake());
});
group('ChatService', () {
late ChatService chatService;
late MockChatRepository mockChatRepository;
setUp(() {
mockChatRepository = MockChatRepository();
chatService = ChatService(chatRepository: mockChatRepository);
});
test('can be instantiated', () {
expect(chatService, isNotNull);
expect(chatService, isA<ChatService>());
});
test('has proper constructor signature', () {
expect(
() => ChatService(httpRepository: mockChatRepository),
returnsNormally,
);
});
group('createSession', () {
test('creates session successfully', () async {
const title = 'Test Chat';
const sessionId = 'session_123';
final expectedSession = ChatBasicSession(
id: sessionId,
userId: 'user_123',
title: title,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
when(() => mockChatRepository.createChatSession(any()))
.thenAnswer((_) async => expectedSession);
final result = await chatService.createSession(title: title);
expect(result, equals(expectedSession));
verify(() => mockChatRepository.createChatSession(
any(that: isA<CreateChatSessionRequest>()
.having((r) => r.title, 'title', title))
)).called(1);
});
test('creates session with description', () async {
const title = 'Test Chat';
const description = 'Test Description';
when(() => mockChatRepository.createChatSession(any()))
.thenAnswer((_) async => ChatBasicSession(
id: 'session_123',
userId: 'user_123',
title: title,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
));
await chatService.createSession(title: title, description: description);
verify(() => mockChatRepository.createChatSession(
any(that: isA<CreateChatSessionRequest>()
.having((r) => r.title, 'title', title))
)).called(1);
});
});
group('getSession', () {
test('gets session successfully', () async {
const sessionId = 'session_123';
final expectedSession = ChatBasicSession(
id: sessionId,
userId: 'user_123',
title: 'Test Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
when(() => mockChatRepository.getChatSession(sessionId))
.thenAnswer((_) async => expectedSession);
final result = await chatService.getSession(sessionId);
expect(result, equals(expectedSession));
verify(() => mockChatRepository.getChatSession(sessionId)).called(1);
});
});
group('getSessions', () {
test('gets sessions with default parameters', () async {
final sessions = [
ChatBasicSession(
id: 'session_1',
userId: 'user_123',
title: 'Chat 1',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
];
when(() => mockChatRepository.getChatSessions(limit: 20, afterSessionId: null))
.thenAnswer((_) async => sessions);
final result = await chatService.getSessions();
expect(result, equals(sessions));
verify(() => mockChatRepository.getChatSessions(limit: 20, afterSessionId: null)).called(1);
});
test('gets sessions with custom parameters', () async {
const limit = 10;
const afterSessionId = 'session_123';
final sessions = <ChatBasicSession>[];
when(() => mockChatRepository.getChatSessions(limit: limit, afterSessionId: afterSessionId))
.thenAnswer((_) async => sessions);
final result = await chatService.getSessions(
limit: limit,
afterSessionId: afterSessionId,
);
expect(result, equals(sessions));
verify(() => mockChatRepository.getChatSessions(
limit: limit,
afterSessionId: afterSessionId,
)).called(1);
});
});
group('sendTextMessage', () {
test('sends text message successfully', () async {
const sessionId = 'session_123';
const content = 'Hello, world!';
final expectedResponse = ChatMessageResponse(
messageId: 'msg_123',
sessionId: sessionId,
content: 'Assistant response',
senderId: 'assistant',
timestamp: DateTime.now(),
);
when(() => mockChatRepository.sendTextMessage(any()))
.thenAnswer((_) async => expectedResponse);
final result = await chatService.sendTextMessage(sessionId, content);
expect(result, equals(expectedResponse));
verify(() => mockChatRepository.sendTextMessage(
any(that: isA<SendTextMessageRequest>()
.having((r) => r.sessionId, 'sessionId', sessionId)
.having((r) => r.content, 'content', content))
)).called(1);
});
});
group('sendAudioMessage', () {
test('sends audio message successfully', () async {
const sessionId = 'session_123';
final audioData = Uint8List(1024);
const duration = Duration(seconds: 5);
final expectedResponse = ChatMessageResponse(
messageId: 'msg_123',
sessionId: sessionId,
content: 'Audio processed',
senderId: 'assistant',
timestamp: DateTime.now(),
);
when(() => mockChatRepository.sendAudioMessage(
sessionId,
audioData,
duration,
fileName: null,
mimeType: null,
)).thenAnswer((_) async => expectedResponse);
final result = await chatService.sendAudioMessage(
sessionId,
audioData,
duration,
);
expect(result, equals(expectedResponse));
verify(() => mockChatRepository.sendAudioMessage(
sessionId,
audioData,
duration,
fileName: null,
mimeType: null,
)).called(1);
});
test('sends audio message with metadata', () async {
const sessionId = 'session_123';
final audioData = Uint8List(2048);
const duration = Duration(seconds: 10);
const transcription = 'Hello world';
const fileName = 'recording.webm';
const mimeType = 'audio/webm';
when(() => mockChatRepository.sendAudioMessage(
sessionId,
audioData,
duration,
fileName: fileName,
mimeType: mimeType,
)).thenAnswer((_) async => ChatMessageResponse(
messageId: 'msg_123',
sessionId: sessionId,
content: 'Response',
senderId: 'assistant',
timestamp: DateTime.now(),
));
await chatService.sendAudioMessage(
sessionId,
audioData,
duration,
transcription: transcription,
fileName: fileName,
mimeType: mimeType,
);
verify(() => mockChatRepository.sendAudioMessage(
sessionId,
audioData,
duration,
fileName: fileName,
mimeType: mimeType,
)).called(1);
});
});
group('getMessages', () {
test('gets messages with default parameters', () async {
const sessionId = 'session_123';
final messages = [
ChatMessageResponse(
messageId: 'msg_1',
sessionId: sessionId,
content: 'Hello',
senderId: 'user',
timestamp: DateTime.now(),
),
];
when(() => mockChatRepository.getChatMessages(
sessionId,
limit: 50,
beforeMessageId: null,
)).thenAnswer((_) async => messages);
final result = await chatService.getMessages(sessionId);
expect(result, equals(messages));
verify(() => mockChatRepository.getChatMessages(
sessionId,
limit: 50,
beforeMessageId: null,
)).called(1);
});
test('gets messages with custom parameters', () async {
const sessionId = 'session_123';
const limit = 20;
const beforeMessageId = 'msg_123';
final messages = <ChatMessageResponse>[];
when(() => mockChatRepository.getChatMessages(
sessionId,
limit: limit,
beforeMessageId: beforeMessageId,
)).thenAnswer((_) async => messages);
final result = await chatService.getMessages(
sessionId,
limit: limit,
beforeMessageId: beforeMessageId,
);
expect(result, equals(messages));
verify(() => mockChatRepository.getChatMessages(
sessionId,
limit: limit,
beforeMessageId: beforeMessageId,
)).called(1);
});
});
group('updateSession', () {
test('updates session successfully', () async {
const sessionId = 'session_123';
final updates = {'title': 'New Title'};
final expectedSession = ChatBasicSession(
id: sessionId,
userId: 'user_123',
title: 'New Title',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
when(() => mockChatRepository.updateChatSession(sessionId, updates))
.thenAnswer((_) async => expectedSession);
final result = await chatService.updateSession(sessionId, updates);
expect(result, equals(expectedSession));
verify(() => mockChatRepository.updateChatSession(sessionId, updates)).called(1);
});
});
group('deleteSession', () {
test('deletes session successfully', () async {
const sessionId = 'session_123';
when(() => mockChatRepository.deleteChatSession(sessionId))
.thenAnswer((_) async => {});
await chatService.deleteSession(sessionId);
verify(() => mockChatRepository.deleteChatSession(sessionId)).called(1);
});
});
});
}

View file

@ -0,0 +1,449 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:mnemo_cards_chat/domain/models/chat_api_simple.dart';
import 'package:mnemo_cards_chat/domain/models/chat_basic.dart';
import 'package:mnemo_cards_chat/domain/models/chat_message.dart';
import 'package:mnemo_cards_chat/domain/services/chat_service.dart';
import 'package:mnemo_cards_chat/domain/state/chat_state_manager.dart';
// Mock classes
class MockChatService extends Mock implements ChatService {}
void main() {
group('ChatStateManager', () {
late ChatStateManager stateManager;
late MockChatService mockChatService;
setUp(() {
mockChatService = MockChatService();
stateManager = ChatStateManager(chatService: mockChatService);
});
test('should initialize with loading state', () {
expect(stateManager.state, equals(const ChatState.loading()));
});
test('can be instantiated with proper dependencies', () {
expect(stateManager, isNotNull);
expect(stateManager, isA<ChatStateManager>());
});
test('has proper constructor signature', () {
expect(
() => ChatStateManager(chatService: mockChatService),
returnsNormally,
);
});
test('state is accessible', () {
expect(stateManager.state, isA<ChatState>());
});
test('has initializeChat method', () {
expect(stateManager.initializeChat, isA<Function>());
});
test('has sendTextMessage method', () {
expect(stateManager.sendTextMessage, isA<Function>());
});
test('has sendAudioMessage method', () {
expect(stateManager.sendAudioMessage, isA<Function>());
});
test('has loadMoreMessages method', () {
expect(stateManager.loadMoreMessages, isA<Function>());
});
test('has clearError method', () {
expect(stateManager.clearError, isA<Function>());
});
test('has currentSession getter', () {
expect(stateManager.currentSession, isNull);
});
test('has messagesCount getter', () {
expect(stateManager.messagesCount, equals(0));
});
group('initializeChat', () {
test('initializes chat successfully', () async {
const sessionId = 'session_123';
final session = ChatBasicSession(
id: sessionId,
userId: 'user_123',
title: 'Test Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
final messages = [
ChatMessageResponse(
messageId: 'msg_1',
sessionId: sessionId,
content: 'Hello',
senderId: 'user',
timestamp: DateTime.now(),
),
];
when(() => mockChatService.getSession(sessionId))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(sessionId, limit: 50))
.thenAnswer((_) async => messages);
await stateManager.initializeChat(sessionId);
expect(stateManager.state, isA<ChatStateLoaded>());
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.session, equals(session));
expect(loadedState.messages.length, equals(1));
expect(loadedState.isSendingMessage, isFalse);
expect(loadedState.hasMoreMessages, isFalse);
expect(stateManager.currentSession, equals(session));
expect(stateManager.messagesCount, equals(1));
verify(() => mockChatService.getSession(sessionId)).called(1);
verify(() => mockChatService.getMessages(sessionId, limit: 50)).called(1);
});
test('handles initialization error', () async {
const sessionId = 'session_123';
final error = Exception('Network error');
when(() => mockChatService.getSession(sessionId))
.thenThrow(error);
await stateManager.initializeChat(sessionId);
expect(stateManager.state, isA<ChatStateError>());
final errorState = stateManager.state as ChatStateError;
expect(errorState.message, contains('Failed to load chat'));
expect(stateManager.currentSession, isNull);
});
});
group('sendTextMessage', () {
late ChatBasicSession session;
setUp(() {
session = ChatBasicSession(
id: 'session_123',
userId: 'user_123',
title: 'Test Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
});
test('sends text message successfully when chat is initialized', () async {
// First initialize chat
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Now send message
const content = 'Hello, world!';
final response = ChatMessageResponse(
messageId: 'msg_123',
sessionId: session.id,
content: 'Assistant response',
senderId: 'assistant',
timestamp: DateTime.now(),
);
when(() => mockChatService.sendTextMessage(session.id, content))
.thenAnswer((_) async => response);
await stateManager.sendTextMessage(content);
expect(stateManager.state, isA<ChatStateLoaded>());
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.messages.length, equals(2)); // User + Assistant messages
expect(loadedState.isSendingMessage, isFalse);
// Check user message
final userMessage = loadedState.messages[0] as TextMessage;
expect(userMessage.sender.id, equals('current_user'));
expect(userMessage.content, equals(content));
expect(userMessage.status, equals(MessageStatus.sent));
// Check assistant message
final assistantMessage = loadedState.messages[1] as TextMessage;
expect(assistantMessage.sender.id, equals('assistant'));
expect(assistantMessage.content, equals('Assistant response'));
verify(() => mockChatService.sendTextMessage(session.id, content)).called(1);
});
test('handles send text message error', () async {
// Initialize chat first
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Send message with error
const content = 'Hello';
final error = Exception('Network error');
when(() => mockChatService.sendTextMessage(session.id, content))
.thenThrow(error);
await stateManager.sendTextMessage(content);
expect(stateManager.state, isA<ChatStateLoaded>());
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.isSendingMessage, isFalse);
expect(loadedState.errorMessage, contains('Failed to send message'));
expect(loadedState.messages.length, equals(1)); // Only user message
// Check that user message has error status
final userMessage = loadedState.messages[0] as AudioMessage;
expect(userMessage.status, equals(MessageStatus.error));
});
test('does nothing if chat not initialized', () async {
await stateManager.sendTextMessage('Hello');
verifyNever(() => mockChatService.sendTextMessage(any(), any()));
});
});
group('sendAudioMessage', () {
late ChatBasicSession session;
setUp(() {
session = ChatBasicSession(
id: 'session_123',
userId: 'user_123',
title: 'Test Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
});
test('sends audio message successfully', () async {
// Initialize chat
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Send audio message
final audioData = Uint8List(1024);
const duration = Duration(seconds: 5);
final response = ChatMessageResponse(
messageId: 'msg_123',
sessionId: session.id,
content: 'Audio processed',
senderId: 'assistant',
timestamp: DateTime.now(),
audioUrl: 'https://example.com/audio.mp3',
durationMs: 5000,
fileSize: 1024,
);
when(() => mockChatService.sendAudioMessage(
session.id,
audioData,
duration,
transcription: null,
)).thenAnswer((_) async => response);
await stateManager.sendAudioMessage(audioData, duration);
expect(stateManager.state, isA<ChatStateLoaded>());
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.messages.length, equals(2)); // User + Assistant messages
// Check user audio message
final userMessage = loadedState.messages[0] as AudioMessage;
expect(userMessage.sender.id, equals('current_user'));
expect(userMessage.duration, equals(duration));
expect(userMessage.status, equals(MessageStatus.sent));
// Check assistant message
final assistantMessage = loadedState.messages[1] as TextMessage;
expect(assistantMessage.sender.id, equals('assistant'));
expect(assistantMessage.content, equals('Audio processed'));
verify(() => mockChatService.sendAudioMessage(
session.id,
audioData,
duration,
transcription: null,
)).called(1);
});
test('sends audio message with transcription', () async {
// Initialize chat
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Send audio with transcription
final audioData = Uint8List(2048);
const duration = Duration(seconds: 3);
const transcription = 'Hello from audio';
when(() => mockChatService.sendAudioMessage(
session.id,
audioData,
duration,
transcription: transcription,
)).thenAnswer((_) async => ChatMessageResponse(
messageId: 'msg_123',
sessionId: session.id,
content: 'Response',
senderId: 'assistant',
timestamp: DateTime.now(),
));
await stateManager.sendAudioMessage(audioData, duration, transcription: transcription);
verify(() => mockChatService.sendAudioMessage(
session.id,
audioData,
duration,
transcription: transcription,
)).called(1);
});
test('does nothing if chat not initialized', () async {
final audioData = Uint8List(1024);
await stateManager.sendAudioMessage(audioData, const Duration(seconds: 1));
verifyNever(() => mockChatService.sendAudioMessage(any(), any(), any()));
});
});
group('loadMoreMessages', () {
late ChatBasicSession session;
setUp(() {
session = ChatBasicSession(
id: 'session_123',
userId: 'user_123',
title: 'Test Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
});
test('loads more messages successfully', () async {
// Initialize with one message
final initialMessages = [
ChatMessageResponse(
messageId: 'msg_1',
sessionId: session.id,
content: 'Initial message',
senderId: 'user',
timestamp: DateTime.now(),
),
];
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: 50))
.thenAnswer((_) async => initialMessages);
await stateManager.initializeChat(session.id);
// Load more messages
final moreMessages = [
ChatMessageResponse(
messageId: 'msg_0',
sessionId: session.id,
content: 'Older message',
senderId: 'assistant',
timestamp: DateTime.now().subtract(const Duration(hours: 1)),
),
];
when(() => mockChatService.getMessages(
session.id,
limit: 50,
beforeMessageId: 'msg_1',
)).thenAnswer((_) async => moreMessages);
await stateManager.loadMoreMessages();
expect(stateManager.state, isA<ChatStateLoaded>());
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.messages.length, equals(2));
expect(loadedState.isLoadingMore, isFalse);
expect(loadedState.hasMoreMessages, isFalse); // Only 1 more message loaded
// Messages should be in chronological order (oldest first)
expect(loadedState.messages[0].maybeMap(
text: (msg) => msg.content,
audio: (msg) => msg.transcription,
), equals('Older message'));
});
test('does nothing if already loading more', () async {
// Initialize chat
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Start loading more
stateManager.state = (stateManager.state as ChatStateLoaded).copyWith(isLoadingMore: true);
await stateManager.loadMoreMessages();
verifyNever(() => mockChatService.getMessages(any(), limit: any(named: 'limit'), beforeMessageId: any(named: 'beforeMessageId')));
});
test('does nothing if no more messages', () async {
// Initialize chat
when(() => mockChatService.getSession(any()))
.thenAnswer((_) async => session);
when(() => mockChatService.getMessages(any(), limit: any(named: 'limit')))
.thenAnswer((_) async => []);
await stateManager.initializeChat(session.id);
// Set hasMoreMessages to false
stateManager.state = (stateManager.state as ChatStateLoaded).copyWith(hasMoreMessages: false);
await stateManager.loadMoreMessages();
verifyNever(() => mockChatService.getMessages(any(), limit: any(named: 'limit'), beforeMessageId: any(named: 'beforeMessageId')));
});
});
group('clearError', () {
test('clears error message from loaded state', () {
stateManager.state = ChatState.loaded(
session: ChatBasicSession(
id: 'session_123',
userId: 'user_123',
title: 'Test',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
messages: [],
errorMessage: 'Some error',
);
stateManager.clearError();
final loadedState = stateManager.state as ChatStateLoaded;
expect(loadedState.errorMessage, isNull);
});
test('does nothing for non-loaded states', () {
stateManager.clearError();
expect(stateManager.state, isA<ChatState>()); // Should not crash
});
});
});
}

View file

@ -0,0 +1,34 @@
🎯 ИТОГОВЫЙ ОТЧЕТ ПОЛНОЙ АВТОМАТИЧЕСКОЙ СИСТЕМЫ
============================================
🕐 Время выполнения: Thu Sep 4 22:39:20 MSK 2025
🆔 ID сессии: 20250904_223911
⏱️ Время мониторинга: 30 секунд
🌐 Браузер: chrome
📋 РЕЗУЛЬТАТЫ ЭТАПОВ:
🔍 Этап 1 (Отладка): ❌ Ошибки
🔧 Этап 2 (Исправление): ❌ Ошибки
✅ Этап 3 (Валидация): ❌ Ошибки
🧪 Этап 4 (Тестирование): ❌ Ошибки
📊 ФИНАЛЬНАЯ СТАТИСТИКА ПРОБЛЕМ:
📋 Всего проблем: 10370
❌ Ошибок: 9881
⚠️ Предупреждений: 215
💡 РЕКОМЕНДАЦИИ:
🔧 Требуются дополнительные исправления
📋 Проверьте логи ошибок
🔄 Запустите систему повторно
🧪 Настройте автоматические тесты
📊 Интегрируйте в CI/CD pipeline
📁 СОЗДАННЫЕ ФАЙЛЫ:
📋 Отчеты отладки: debug_report/error_report_*.txt
🔧 Отчеты исправлений: debug_report/smart_fix_report_*.txt
📊 Анализ: debug_report/flutter_analysis*.txt
🧪 Тестирование: debug_report/build_test.log
📝 Логи: debug_report/auto_fix_*.log, debug_report/smart_fix_*.log

File diff suppressed because it is too large Load diff

43
funny_letters/.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
funny_letters/.metadata Normal file
View file

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "b25305a8832cfc6ba632a7f87ad455e319dccce8"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
- platform: web
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

25
funny_letters/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "funny_letters",
"request": "launch",
"type": "dart"
},
{
"name": "funny_letters (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "funny_letters (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

Binary file not shown.

16
funny_letters/README.md Normal file
View file

@ -0,0 +1,16 @@
# funny_letters
Funny letters
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,8 +1,5 @@
plugins { plugins {
id "com.android.application" id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android" id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin" id "dev.flutter.flutter-gradle-plugin"
} }
@ -25,15 +22,8 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace "com.cinnabarflower.mnemo_cards" namespace "com.example.funny_letters"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
@ -52,26 +42,20 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.cinnabarflower.mnemo_cards" applicationId "com.example.funny_letters"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 25 minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
} }
} }
} }

View file

@ -1,13 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:label="Mnemo cards" android:label="funny_letters"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"> android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -35,9 +30,4 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>
</manifest> </manifest>

View file

@ -1,4 +1,4 @@
package com.cinnabarflower.mnemo_cards package com.example.funny_letters
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View file

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 544 B

View file

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View file

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

View file

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View file

@ -3,8 +3,5 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<application
android:networkSecurityConfig="@xml/network_security_config"
/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
</manifest> </manifest>

View file

@ -1,16 +1,11 @@
buildscript { buildscript {
ext.kotlin_version = '1.8.20' ext.kotlin_version = '1.7.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
// START: FlutterFire Configuration
classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
// END: FlutterFire Configuration
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View file

@ -1,3 +1,3 @@
org.gradle.jvmargs=-Xmx4608M org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View file

@ -0,0 +1,29 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
plugins {
id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
}
include ":app"

Binary file not shown.

BIN
funny_letters/assets/go.mp3 Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Some files were not shown because too many files have changed in this diff Show more