import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../data/models/conversation.dart'; import '../data/models/message.dart'; import 'api_client_provider.dart'; final conversationsProvider = AsyncNotifierProvider>( ConversationsNotifier.new); class ConversationsNotifier extends AsyncNotifier> { @override Future> build() async { return ref.watch(chatRepositoryProvider).getConversations(); } Future create(String title) async { final conv = await ref.read(chatRepositoryProvider).createConversation(title); state = AsyncData([conv, ...state.valueOrNull ?? []]); return conv; } Future delete(int id) async { await ref.read(chatRepositoryProvider).deleteConversation(id); state = AsyncData([ for (final c in state.valueOrNull ?? []) if (c.id != id) c, ]); } } final messagesProvider = AsyncNotifierProvider.family, int>(MessagesNotifier.new); class MessagesNotifier extends FamilyAsyncNotifier, int> { bool _isStreaming = false; bool get isStreaming => _isStreaming; @override Future> build(int arg) async { return ref.watch(chatRepositoryProvider).getMessages(arg); } Future sendMessage(String content) async { final conversationId = arg; final repo = ref.read(chatRepositoryProvider); // Optimistically add the user message. final userMsg = Message( conversationId: conversationId, role: MessageRole.user, content: content, ); state = AsyncData([...state.valueOrNull ?? [], userMsg]); // Add an empty assistant placeholder. final placeholder = Message( conversationId: conversationId, role: MessageRole.assistant, content: '', status: 'generating', ); state = AsyncData([...state.requireValue, placeholder]); _isStreaming = true; try { // Step 1: POST the message (fires background generation on server). await repo.sendMessage(conversationId, content); // Step 2: Stream chunks and update the placeholder in place. await for (final chunk in repo.streamGeneration(conversationId)) { final msgs = state.requireValue; final last = msgs.last.copyWith(content: msgs.last.content + chunk); state = AsyncData([...msgs.sublist(0, msgs.length - 1), last]); } // Mark the assistant message as complete. final msgs = state.requireValue; final last = msgs.last.copyWith(status: 'complete'); state = AsyncData([...msgs.sublist(0, msgs.length - 1), last]); } catch (e) { // Remove the placeholder on error so the user can retry. state = AsyncData([ for (final m in state.valueOrNull ?? []) if (m.status != 'generating') m, ]); rethrow; } finally { _isStreaming = false; } } } final isStreamingProvider = Provider.family((ref, convId) { final notifier = ref.watch(messagesProvider(convId).notifier); return notifier.isStreaming; });