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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions workout-logger/lib/main.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,8 @@ import 'package:provider/provider.dart';

import 'services/storage_service.dart';
import 'services/ml_service.dart';
import 'services/gemini_service.dart';
import 'services/ai/gemini_ai_service.dart';
import 'services/ai/coach_tool_service.dart';
import 'services/health_connect_service.dart';
import 'services/interfaces/storage_service_interface.dart';
import 'services/interfaces/ml_service_interface.dart';
Expand All@@ -21,6 +22,7 @@ import 'services/managers/program_manager.dart';
import 'services/managers/history_manager.dart';
import 'services/managers/health_sync_manager.dart';
import 'services/managers/pr_manager.dart';
import 'services/managers/conversation_manager.dart';
import 'theme/app_theme.dart';
import 'screens/home_screen.dart';
import 'screens/onboarding_screen.dart';
Expand DownExpand Up@@ -62,7 +64,10 @@ class WorkoutLoggerApp extends StatelessWidget {
static final HistoryManager _historyManager =
HistoryManager(_storageService, healthSyncManager: _healthSyncManager);
static final PRManager _prManager = PRManager(_storageService);
static final GeminiService _geminiService = GeminiService();
static final GeminiAiService _geminiService =
GeminiAiService(storage: _storageService);
static final ConversationManager _conversationManager =
ConversationManager(_storageService);

const WorkoutLoggerApp({super.key});

Expand All@@ -89,7 +94,15 @@ class WorkoutLoggerApp extends StatelessWidget {
// Provided as ChangeNotifier so HistoryScreen rebuilds on sync badge changes.
ChangeNotifierProvider<HistoryManager>.value(value: _historyManager),
ChangeNotifierProvider<PRManager>.value(value: _prManager),
ChangeNotifierProvider<GeminiService>.value(value: _geminiService),
// GeminiAiService is the single AI backend instance. It's a ChangeNotifier
// (settings UI watches isConfigured/model), so it's provided as such.
// Consumers that should depend on the abstraction (the coach ViewModel,
// program generator) receive it typed as IAiService at construction —
// the future firebase_ai swap point — without a separate provider.
ChangeNotifierProvider<GeminiAiService>.value(value: _geminiService),
ChangeNotifierProvider<ConversationManager>.value(
value: _conversationManager,
),
// WorkoutProvider receives dependencies via constructor injection
ChangeNotifierProvider(
create: (_) => WorkoutProvider(
Expand All@@ -99,6 +112,13 @@ class WorkoutLoggerApp extends StatelessWidget {
programManager: _programManager,
),
),
// CoachToolService backs AI tool calls; reads from WorkoutProvider + PRManager.
Provider<CoachToolService>(
create: (ctx) => CoachToolService(
ctx.read<WorkoutProvider>(),
ctx.read<PRManager>(),
),
),
],
child: MaterialApp(
title: 'Workout Logger',
Expand DownExpand Up@@ -135,12 +155,17 @@ class _AppInitializerState extends State<AppInitializer> {
final historyManager = context.read<HistoryManager>();
final prManager = context.read<PRManager>();
final api = context.read<ApiService>();
final gemini = context.read<GeminiService>();
final gemini = context.read<GeminiAiService>();

try {
await provider.init();
await settings.init();
gemini.init(settings.geminiApiKey, model: settings.geminiModel);
try {
await gemini.loadUsage();
} catch (e, st) {
debugPrint('gemini.loadUsage failed: $e\n$st');
}
await historyManager.loadSessions();
await prManager.load();
await prManager.backfillFromSessions(historyManager.sessions);
Expand Down
98 changes: 98 additions & 0 deletions workout-logger/lib/models/models.dart
Original file line numberDiff line numberDiff line change
Expand Up@@ -812,3 +812,101 @@ class TrainingProgram {
createdAt == _sentinel ? this.createdAt : createdAt as DateTime,
);
}

// ==================== AI Coach Chat ====================

/// A single message in an AI coach conversation.
class ChatMessage {
final String id;
final String role; // 'user' | 'model'
final String text;
final DateTime timestamp;

ChatMessage({
String? id,
required this.role,
required this.text,
DateTime? timestamp,
}) : id = id ?? _uuid.v4(),
timestamp = timestamp ?? DateTime.now();

Map<String, dynamic> toJson() => {
'id': id,
'role': role,
'text': text,
'timestamp': timestamp.toIso8601String(),
};

factory ChatMessage.fromJson(Map<String, dynamic> json) => ChatMessage(
id: json['id'] as String?,
role: json['role'] as String,
text: json['text'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
);

ChatMessage copyWith({
Object? role = _sentinel,
Object? text = _sentinel,
Object? timestamp = _sentinel,
}) => ChatMessage(
id: id,
role: role == _sentinel ? this.role : role as String,
text: text == _sentinel ? this.text : text as String,
timestamp: timestamp == _sentinel ? this.timestamp : timestamp as DateTime,
);
}

/// A persisted AI coach conversation: an ordered list of [ChatMessage]s.
class Conversation {
final String id;
final String title;
final DateTime createdAt;
final DateTime updatedAt;
final List<ChatMessage> messages;

Conversation({
String? id,
required this.title,
DateTime? createdAt,
DateTime? updatedAt,
List<ChatMessage>? messages,
}) : id = id ?? _uuid.v4(),
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? createdAt ?? DateTime.now(),
messages = messages ?? const [];

Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
'messages': messages.map((m) => m.toJson()).toList(),
};

factory Conversation.fromJson(Map<String, dynamic> json) => Conversation(
id: json['id'] as String?,
title: json['title'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: DateTime.parse(json['createdAt'] as String),
messages: (json['messages'] as List)
.map((m) => ChatMessage.fromJson(m as Map<String, dynamic>))
.toList(),
);

Conversation copyWith({
Object? title = _sentinel,
Object? createdAt = _sentinel,
Object? updatedAt = _sentinel,
Object? messages = _sentinel,
}) => Conversation(
id: id,
title: title == _sentinel ? this.title : title as String,
createdAt: createdAt == _sentinel ? this.createdAt : createdAt as DateTime,
updatedAt: updatedAt == _sentinel ? this.updatedAt : updatedAt as DateTime,
messages: messages == _sentinel
? this.messages
: messages as List<ChatMessage>,
);
}
Loading
Loading