main
dart 102 lines 3.45 KB
Raw
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 });