| 1 | import 'dart:async'; |
| 2 | |
| 3 | import 'package:flutter_riverpod/flutter_riverpod.dart'; |
| 4 | import 'package:localmind/core/models/enums.dart'; |
| 5 | import 'package:localmind/core/providers/app_providers.dart'; |
| 6 | import 'package:localmind/features/conversations/providers/conversation_providers.dart' |
| 7 | as conv; |
| 8 | import 'package:localmind/features/voice_mode/providers/voice_mode_provider.dart'; |
| 9 | import 'chat_notifier.dart'; |
| 10 | import 'chat_params_providers.dart'; |
| 11 | import 'chat_service_providers.dart'; |
| 12 | import 'model_selection_providers.dart'; |
| 13 | |
| 14 | final smartRepliesProvider = FutureProvider<List<String>>((ref) async { |
| 15 | final isStreaming = ref.watch(chatProvider.select((s) => s.isStreaming)); |
| 16 | final settings = ref.watch(settingsProvider); |
| 17 | final voiceState = ref.watch(voiceModeProvider); |
| 18 | final target = ref.watch(activeChatTargetProvider); |
| 19 | final chatParams = ref.watch(chatParamsProvider); |
| 20 | final chatService = ref.watch(chatServiceProvider); |
| 21 | |
| 22 | if (isStreaming) return []; |
| 23 | if (voiceState.isActive || voiceState.phase != VoiceModePhase.idle) return []; |
| 24 | |
| 25 | final messages = ref.watch(chatProvider.select((s) => s.messages)); |
| 26 | if (messages.isEmpty) return []; |
| 27 | |
| 28 | final lastMessage = messages.last; |
| 29 | if (lastMessage.role != MessageRole.assistant || |
| 30 | lastMessage.status != MessageStatus.complete) { |
| 31 | return []; |
| 32 | } |
| 33 | |
| 34 | // Only watch the smart replies cache fields of the active conversation to avoid |
| 35 | // invalidating and restarting this provider when conversation title or stats change. |
| 36 | final cachedData = ref.watch( |
| 37 | conv.activeConversationProvider.select((c) { |
| 38 | if (c == null) return null; |
| 39 | return ( |
| 40 | id: c.id, |
| 41 | smartRepliesLastMessageId: c.smartRepliesLastMessageId, |
| 42 | smartReplies: c.smartReplies, |
| 43 | ); |
| 44 | }), |
| 45 | ); |
| 46 | |
| 47 | if (cachedData != null && |
| 48 | cachedData.smartRepliesLastMessageId == lastMessage.id && |
| 49 | cachedData.smartReplies != null && |
| 50 | cachedData.smartReplies!.isNotEmpty) { |
| 51 | return cachedData.smartReplies!; |
| 52 | } |
| 53 | |
| 54 | // If AI title generation is running for this conversation, wait for it first |
| 55 | // so LLM inference executes sequentially without colliding on local models. |
| 56 | final chatNotifier = ref.read(chatProvider.notifier); |
| 57 | await chatNotifier.waitForTitleGeneration(lastMessage.conversationId); |
| 58 | |
| 59 | if (!ref.mounted) return []; |
| 60 | if (ref.read(chatProvider.select((s) => s.isStreaming))) return []; |
| 61 | |
| 62 | List<String> suggestions = []; |
| 63 | |
| 64 | if (settings.smartReplyEnabled) { |
| 65 | final server = target.server; |
| 66 | final modelId = target.effectiveModelId; |
| 67 | |
| 68 | if (server != null && chatService != null && modelId != null) { |
| 69 | final service = ref.read(smartReplyServiceProvider); |
| 70 | suggestions = await service.suggestRepliesWithLLM( |
| 71 | chatService: chatService, |
| 72 | server: server, |
| 73 | modelId: modelId, |
| 74 | messages: messages, |
| 75 | params: chatParams, |
| 76 | personaSystemPrompt: settings.smartRepliesUsePersona |
| 77 | ? chatParams.systemPrompt |
| 78 | : null, |
| 79 | ); |
| 80 | if (!ref.mounted) return []; |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | if (suggestions.isEmpty) { |
| 85 | if (!ref.mounted) return []; |
| 86 | final service = ref.read(smartReplyServiceProvider); |
| 87 | suggestions = service.getFallbackReplies(lastMessage.content); |
| 88 | } |
| 89 | |
| 90 | final convId = cachedData?.id ?? lastMessage.conversationId; |
| 91 | if (suggestions.isNotEmpty) { |
| 92 | Future.microtask(() { |
| 93 | if (ref.mounted) { |
| 94 | ref |
| 95 | .read(conv.conversationsProvider.notifier) |
| 96 | .updateSmartReplies(convId, suggestions, lastMessage.id); |
| 97 | } |
| 98 | }); |
| 99 | } |
| 100 | |
| 101 | return suggestions; |
| 102 | }); |