@tduncan / Cortana / commits / 25a208a

feat: implement smart reply service, update conversation entity for persistence, and refactor chat input UI components

abdulmominsakib committed May 20, 2026 at 13:37 UTC 25a208adbb0289f38d0a78ab1599bf928c8e36a6
26 files changed +688 -114
lib/core/providers/app_providers.dart
+2
@@ -96,6 +96,8 @@ class SettingsNotifier extends Notifier<AppSettings> {
96 _update(state.copyWith(kittenTtsModelVariant: value));
97 void setAutoSpeakEnabled(bool value) =>
98 _update(state.copyWith(autoSpeakEnabled: value));
99 + void setSmartReplyEnabled(bool value) =>
100 + _update(state.copyWith(smartReplyEnabled: value));
101 void setLocaleCode(String? value) =>
102 _update(state.copyWith(localeCode: value));
103
lib/core/storage/entities.dart
+12
@@ -180,6 +180,8 @@ class ConversationEntity {
180 int? maxTokens;
181 int? contextLength;
182 bool? mcpEnabled;
183 + String? smartRepliesJson;
184 + String? smartRepliesLastMessageId;
185
186 @Backlink()
187 final messages = ToMany<MessageEntity>();
@@ -202,6 +204,8 @@ class ConversationEntity {
204 this.maxTokens,
205 this.contextLength,
206 this.mcpEnabled,
207 + this.smartRepliesJson,
208 + this.smartRepliesLastMessageId,
209 });
210
211 factory ConversationEntity.fromDomain(Conversation conversation) {
@@ -222,6 +226,10 @@ class ConversationEntity {
226 maxTokens: conversation.maxTokens,
227 contextLength: conversation.contextLength,
228 mcpEnabled: conversation.mcpEnabled,
229 + smartRepliesJson: conversation.smartReplies != null
230 + ? jsonEncode(conversation.smartReplies)
231 + : null,
232 + smartRepliesLastMessageId: conversation.smartRepliesLastMessageId,
233 );
234 }
235
@@ -243,6 +251,10 @@ class ConversationEntity {
251 maxTokens: maxTokens,
252 contextLength: contextLength,
253 mcpEnabled: mcpEnabled,
254 + smartReplies: smartRepliesJson != null
255 + ? List<String>.from(jsonDecode(smartRepliesJson!))
256 + : null,
257 + smartRepliesLastMessageId: smartRepliesLastMessageId,
258 );
259 }
260
lib/features/chat/data/smart_reply_service.dart new
+333
@@ -0,0 +1,333 @@
1 +import 'dart:async';
2 +import 'dart:convert';
3 +
4 +import '../../../core/logger/app_logger.dart';
5 +import '../../../core/models/enums.dart';
6 +import '../../servers/data/models/server.dart';
7 +import 'chat_service.dart';
8 +import 'models/chat_parameters.dart';
9 +import 'models/message.dart';
10 +
11 +class SmartReplyService {
12 + Future<List<String>> suggestRepliesWithLLM({
13 + required ChatService chatService,
14 + required Server server,
15 + required String modelId,
16 + required List<Message> messages,
17 + required ChatParameters params,
18 + }) async {
19 + if (messages.isEmpty) return [];
20 +
21 + final lastMessage = messages.last;
22 +
23 + // Construct the prompt message for generating suggestions
24 + final promptMessage = Message(
25 + id: 'smart-reply-prompt',
26 + conversationId: lastMessage.conversationId,
27 + role: MessageRole.user,
28 + content: 'Based on the conversation history above, suggest 3 short, natural, context-appropriate reply options that the user (human) might want to send next. '
29 + 'Return them strictly as a JSON array of strings, for example: ["Reply 1", "Reply 2", "Reply 3"]. '
30 + 'Each reply should be brief (under 8 words). Return ONLY the raw JSON array, with no markdown code block formatting (do not wrap in ```json), explanations, or extra text.',
31 + createdAt: DateTime.now(),
32 + );
33 +
34 + final apiMessages = [
35 + ...messages,
36 + promptMessage,
37 + ];
38 +
39 + // Use low temperature and small tokens for fast, cheap, and deterministic suggestions
40 + final suggestionParams = params.copyWith(
41 + temperature: 0.2,
42 + maxTokens: 128,
43 + systemPrompt: 'You are a smart reply assistant. Your job is to output exactly a JSON array containing 3 suggested short replies for the user. Do not output anything other than the JSON array.',
44 + );
45 +
46 + try {
47 + final completer = Completer<String>();
48 + String accumulatedContent = '';
49 +
50 + final stream = chatService.sendMessage(
51 + server: server,
52 + modelId: modelId,
53 + messages: apiMessages,
54 + params: suggestionParams,
55 + );
56 +
57 + StreamSubscription? subscription;
58 + subscription = stream.listen(
59 + (response) {
60 + if (response.type == ChatResponseType.message && response.content != null) {
61 + accumulatedContent += response.content!;
62 + } else if (response.type == ChatResponseType.done) {
63 + subscription?.cancel();
64 + if (!completer.isCompleted) {
65 + completer.complete(accumulatedContent);
66 + }
67 + } else if (response.type == ChatResponseType.error) {
68 + subscription?.cancel();
69 + if (!completer.isCompleted) {
70 + completer.completeError(response.content ?? 'Unknown streaming error');
71 + }
72 + }
73 + },
74 + onError: (err) {
75 + subscription?.cancel();
76 + if (!completer.isCompleted) {
77 + completer.completeError(err);
78 + }
79 + },
80 + onDone: () {
81 + subscription?.cancel();
82 + if (!completer.isCompleted) {
83 + completer.complete(accumulatedContent);
84 + }
85 + },
86 + cancelOnError: true,
87 + );
88 +
89 + final rawResponse = await completer.future.timeout(
90 + const Duration(seconds: 15),
91 + onTimeout: () {
92 + subscription?.cancel();
93 + return accumulatedContent;
94 + },
95 + );
96 +
97 + return _parseSuggestions(rawResponse);
98 + } catch (e) {
99 + Log.warning('LLM SmartReply generation failed: $e');
100 + return [];
101 + }
102 + }
103 +
104 + List<String> _parseSuggestions(String text) {
105 + final cleaned = text.trim();
106 +
107 + // 1. Try parsing as JSON array
108 + try {
109 + final startIdx = cleaned.indexOf('[');
110 + final endIdx = cleaned.lastIndexOf(']');
111 + if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {
112 + final jsonStr = cleaned.substring(startIdx, endIdx + 1);
113 + final decoded = jsonDecode(jsonStr);
114 + if (decoded is List) {
115 + return decoded
116 + .map((e) => e.toString().trim())
117 + .where((s) => s.isNotEmpty)
118 + .toList();
119 + }
120 + }
121 + } catch (_) {
122 + // Ignore JSON parsing failures and fall back to line-by-line parsing
123 + }
124 +
125 + // 2. Fallback: Line-based parsing (numbered list, bullet points, etc.)
126 + final suggestions = <String>[];
127 + final lines = cleaned.split('\n');
128 + for (var line in lines) {
129 + line = line.trim();
130 + if (line.isEmpty) continue;
131 +
132 + // Remove numbered list prefixes like "1. ", "2)", etc.
133 + // Also remove bullet points like "- ", "* "
134 + line = line.replaceFirst(RegExp(r'^(\d+\.\s*|\d+\)\s*|[-*+]\s*)'), '');
135 + line = line.trim();
136 +
137 + // Strip surrounding quotes if present
138 + if (line.startsWith('"') && line.endsWith('"')) {
139 + line = line.substring(1, line.length - 1).trim();
140 + } else if (line.startsWith("'") && line.endsWith("'")) {
141 + line = line.substring(1, line.length - 1).trim();
142 + }
143 +
144 + if (line.isNotEmpty && line.length < 100) {
145 + suggestions.add(line);
146 + }
147 + }
148 + return suggestions.take(3).toList();
149 + }
150 +
151 + List<String> getFallbackReplies(String content) {
152 + final cleanContent = content.trim();
153 + final lowerContent = cleanContent.toLowerCase();
154 +
155 + // 1. Greetings & Welcomes
156 + if (lowerContent.contains('hello') ||
157 + lowerContent.contains('hi') ||
158 + lowerContent.contains('hey') ||
159 + lowerContent.contains('greetings') ||
160 + lowerContent.contains('good morning') ||
161 + lowerContent.contains('good afternoon') ||
162 + lowerContent.contains('good evening') ||
163 + lowerContent.contains('welcome')) {
164 + return [
165 + 'Hello! How are you?',
166 + 'I have a question',
167 + 'Let\'s get started!',
168 + ];
169 + }
170 +
171 + // 2. Gratitude & Farewells
172 + if (lowerContent.contains('thank you') ||
173 + lowerContent.contains('thanks') ||
174 + lowerContent.contains('great help') ||
175 + lowerContent.contains('solved') ||
176 + lowerContent.contains('goodbye') ||
177 + lowerContent.contains('bye') ||
178 + lowerContent.contains('see you')) {
179 + return [
180 + 'You\'re welcome!',
181 + 'Thanks for your help!',
182 + 'Have a great day!',
183 + ];
184 + }
185 +
186 + // 3. Code & Programming
187 + if (lowerContent.contains('```') ||
188 + lowerContent.contains('function') ||
189 + lowerContent.contains('class ') ||
190 + lowerContent.contains('import ') ||
191 + lowerContent.contains('const ') ||
192 + lowerContent.contains('def ') ||
193 + lowerContent.contains('fn ') ||
194 + lowerContent.contains('public static') ||
195 + lowerContent.contains('std::') ||
196 + lowerContent.contains('package:')) {
197 + return [
198 + 'Explain this code',
199 + 'How can I improve this?',
200 + 'Add error handling',
201 + 'Write tests for this',
202 + ];
203 + }
204 +
205 + // 4. Debugging & Errors
206 + if (lowerContent.contains('error') ||
207 + lowerContent.contains('exception') ||
208 + lowerContent.contains('failed') ||
209 + lowerContent.contains('crash') ||
210 + lowerContent.contains('bug') ||
211 + lowerContent.contains('issue') ||
212 + lowerContent.contains('problem') ||
213 + lowerContent.contains('broken') ||
214 + lowerContent.contains('debug') ||
215 + lowerContent.contains('stacktrace')) {
216 + return [
217 + 'How do I fix this error?',
218 + 'What causes this issue?',
219 + 'Show me a fix',
220 + 'How to prevent this?',
221 + ];
222 + }
223 +
224 + // 5. Lists & Steps
225 + final hasNumberedList = RegExp(r'\b\d+\.\s').hasMatch(cleanContent);
226 + final hasBulletList = RegExp(r'^[\s]*[-*+]\s', multiLine: true).hasMatch(cleanContent);
227 + if (hasNumberedList ||
228 + hasBulletList ||
229 + lowerContent.contains('step 1') ||
230 + lowerContent.contains('firstly') ||
231 + lowerContent.contains('secondly') ||
232 + lowerContent.contains('finally') ||
233 + lowerContent.contains('steps:')) {
234 + return [
235 + 'Can you elaborate on step 1?',
236 + 'What is the next step?',
237 + 'Give me a summary',
238 + 'Can you simplify this?',
239 + ];
240 + }
241 +
242 + // 6. Questions asked by the assistant
243 + if (cleanContent.endsWith('?')) {
244 + final isYesNoQuestion = lowerContent.contains('do you') ||
245 + lowerContent.contains('can you') ||
246 + lowerContent.contains('is it') ||
247 + lowerContent.contains('are you') ||
248 + lowerContent.contains('would you') ||
249 + lowerContent.contains('should we');
250 + if (isYesNoQuestion) {
251 + return [
252 + 'Yes, please.',
253 + 'No, thank you.',
254 + 'Can you explain more?',
255 + ];
256 + }
257 + return [
258 + 'Sure, tell me more.',
259 + 'I\'m not sure, what do you think?',
260 + 'Can you give me an example?',
261 + ];
262 + }
263 +
264 + // 7. Writing & Content Creation
265 + if (lowerContent.contains('write') ||
266 + lowerContent.contains('essay') ||
267 + lowerContent.contains('poem') ||
268 + lowerContent.contains('story') ||
269 + lowerContent.contains('summarize') ||
270 + lowerContent.contains('draft') ||
271 + lowerContent.contains('paragraph') ||
272 + lowerContent.contains('grammar') ||
273 + lowerContent.contains('article') ||
274 + lowerContent.contains('translation') ||
275 + lowerContent.contains('translate')) {
276 + return [
277 + 'Can you make it shorter?',
278 + 'Make it more professional',
279 + 'Can you change the tone?',
280 + 'Check for grammatical errors',
281 + ];
282 + }
283 +
284 + // 8. Data & Tables
285 + if (lowerContent.contains('data') ||
286 + lowerContent.contains('json') ||
287 + lowerContent.contains('csv') ||
288 + lowerContent.contains('table') ||
289 + lowerContent.contains('chart') ||
290 + lowerContent.contains('database') ||
291 + lowerContent.contains('sql') ||
292 + lowerContent.contains('format') ||
293 + lowerContent.contains('query') ||
294 + lowerContent.contains('analytics')) {
295 + return [
296 + 'Format this as a markdown table',
297 + 'Convert this to JSON',
298 + 'How do I query this?',
299 + 'Summarize key insights',
300 + ];
301 + }
302 +
303 + // 9. Planning & Ideas
304 + if (lowerContent.contains('plan') ||
305 + lowerContent.contains('idea') ||
306 + lowerContent.contains('brainstorm') ||
307 + lowerContent.contains('suggest') ||
308 + lowerContent.contains('recommend') ||
309 + lowerContent.contains('options') ||
310 + lowerContent.contains('alternatives') ||
311 + lowerContent.contains('strategies') ||
312 + lowerContent.contains('tips')) {
313 + return [
314 + 'Give me more options',
315 + 'Which one do you recommend?',
316 + 'What are the pros and cons?',
317 + 'Help me choose between them',
318 + ];
319 + }
320 +
321 + // 10. Default General suggestions
322 + return [
323 + 'Tell me more',
324 + 'Give me an example',
325 + 'Can you explain this simpler?',
326 + 'What are the alternatives?',
327 + ];
328 + }
329 +
330 + void reset() {}
331 +
332 + void dispose() {}
333 +}
lib/features/chat/providers/chat_providers.dart
+59 -45
@@ -23,6 +23,7 @@ import '../../conversations/data/models/conversation.dart';
23 import '../data/chat_service.dart';
24 import '../data/models/chat_parameters.dart';
25 import '../data/models/message.dart';
26 +import '../data/smart_reply_service.dart';
27 import '../../../core/providers/chat_background_service_provider.dart';
28
29 final selectedModelProvider =
@@ -245,55 +246,66 @@ final chatServiceProvider = Provider<ChatService?>((ref) {
246 return ChatService.forServer(server.type, ref.read(dioProvider));
247 });
248
248 -final smartRepliesProvider = Provider<List<String>>((ref) {
249 +final smartReplyServiceProvider = Provider<SmartReplyService>((ref) {
250 + final service = SmartReplyService();
251 + ref.onDispose(() => service.dispose());
252 + return service;
253 +});
254 +
255 +final smartRepliesProvider = FutureProvider<List<String>>((ref) async {
256 final chatState = ref.watch(chatProvider);
257 final isStreaming = ref.watch(isStreamingProvider);
258 + final settings = ref.watch(settingsProvider);
259
252 - if (chatState.messages.length < 2 || isStreaming) return [];
260 + if (chatState.messages.isEmpty || isStreaming) return [];
261
254 - final lastAssistant = chatState.messages.reversed.firstWhere(
255 - (m) =>
256 - m.role == MessageRole.assistant && m.status == MessageStatus.complete,
257 - orElse: () => chatState.messages.last,
258 - );
259 - if (lastAssistant.role != MessageRole.assistant) return [];
260 -
261 - final content = lastAssistant.content.toLowerCase();
262 - final suggestions = <String>[];
263 -
264 - if (content.contains('```') ||
265 - content.contains('function') ||
266 - content.contains('class ') ||
267 - content.contains('import ')) {
268 - suggestions.addAll([
269 - 'Explain this code',
270 - 'How can I improve this?',
271 - 'Add error handling',
272 - 'Write tests for this',
273 - ]);
274 - } else if (content.contains('step') ||
275 - content.contains('first') ||
276 - content.contains('then')) {
277 - suggestions.addAll([
278 - 'Can you elaborate on step 1?',
279 - 'What if I get stuck?',
280 - 'Give me a summary',
281 - ]);
282 - } else if (content.contains('error') ||
283 - content.contains('problem') ||
284 - content.contains('issue')) {
285 - suggestions.addAll([
286 - 'Show me a fix',
287 - 'What else could cause this?',
288 - 'How to prevent this?',
289 - ]);
290 - } else {
291 - suggestions.addAll([
292 - 'Tell me more',
293 - 'Give me an example',
294 - 'Summarize this',
295 - 'What are the alternatives?',
296 - ]);
262 + final lastMessage = chatState.messages.last;
263 + if (lastMessage.role != MessageRole.assistant ||
264 + lastMessage.status != MessageStatus.complete) {
265 + return [];
266 + }
267 +
268 + // Load saved replies if they exist for the current last message
269 + final activeConv = ref.watch(conv.activeConversationProvider);
270 + if (activeConv != null &&
271 + activeConv.smartRepliesLastMessageId == lastMessage.id &&
272 + activeConv.smartReplies != null &&
273 + activeConv.smartReplies!.isNotEmpty) {
274 + return activeConv.smartReplies!;
275 + }
276 +
277 + List<String> suggestions = [];
278 +
279 + if (settings.smartReplyEnabled) {
280 + final server = ref.watch(activeServerProvider);
281 + final selectedModel = ref.watch(selectedModelProvider);
282 + final chatParams = ref.watch(chatParamsProvider);
283 + final chatService = ref.watch(chatServiceProvider);
284 +
285 + if (server != null && chatService != null && selectedModel != null) {
286 + final service = ref.read(smartReplyServiceProvider);
287 + suggestions = await service.suggestRepliesWithLLM(
288 + chatService: chatService,
289 + server: server,
290 + modelId: selectedModel.id,
291 + messages: chatState.messages,
292 + params: chatParams,
293 + );
294 + }
295 + }
296 +
297 + if (suggestions.isEmpty) {
298 + final service = ref.read(smartReplyServiceProvider);
299 + suggestions = service.getFallbackReplies(lastMessage.content);
300 + }
301 +
302 + // Save the replies to the conversation database
303 + if (activeConv != null && suggestions.isNotEmpty) {
304 + Future.microtask(() {
305 + ref
306 + .read(conv.conversationsProvider.notifier)
307 + .updateSmartReplies(activeConv.id, suggestions, lastMessage.id);
308 + });
309 }
310
311 return suggestions;
@@ -361,6 +373,7 @@ class ChatNotifier extends Notifier<ChatState> {
373 Future<void> loadConversation(Conversation conversation) async {
374 await cancelStream();
375 _currentConversationId = conversation.id;
376 + ref.read(smartReplyServiceProvider).reset();
377 state = state.copyWith(
378 isLoading: true,
379 errorMessage: null,
@@ -436,6 +449,7 @@ class ChatNotifier extends Notifier<ChatState> {
449 Future<void> startNewConversation() async {
450 await cancelStream();
451 _currentConversationId = null;
452 + ref.read(smartReplyServiceProvider).reset();
453 state = const ChatState();
454 ref
455 .read(conv.activeConversationProvider.notifier)
lib/features/chat/views/chat_screen.dart
+112 -52
@@ -1,3 +1,5 @@
1 +import 'dart:ui';
2 +
3 import 'package:flutter/material.dart';
4 import 'package:flutter/services.dart';
5 import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -36,6 +38,7 @@ class ChatScreen extends ConsumerStatefulWidget {
38
39 class _ChatScreenState extends ConsumerState<ChatScreen> {
40 final ScrollController _scrollController = ScrollController();
41 + final FocusNode _inputFocusNode = FocusNode();
42
43 List<String> _quickPrompts(AppLocalizations l10n) => [
44 l10n.quick_write,
@@ -47,11 +50,18 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
50 @override
51 void initState() {
52 super.initState();
53 + _inputFocusNode.addListener(_onFocusChanged);
54 + }
55 +
56 + void _onFocusChanged() {
57 + setState(() {});
58 }
59
60 @override
61 void dispose() {
62 _scrollController.dispose();
63 + _inputFocusNode.removeListener(_onFocusChanged);
64 + _inputFocusNode.dispose();
65 super.dispose();
66 }
67
@@ -184,7 +194,9 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
194 : Icons.smart_toy_outlined,
195 ),
196 title: Text(
187 - persona != null ? l10n.change_persona : l10n.set_persona,
197 + persona != null
198 + ? l10n.change_persona
199 + : l10n.set_persona,
200 ),
201 contentPadding: EdgeInsets.zero,
202 ),
@@ -283,33 +295,60 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
295 },
296 hasSmartReplies:
297 !chatState.isStreaming &&
286 - ref.read(smartRepliesProvider).isNotEmpty,
298 + MediaQuery.of(context).viewInsets.bottom == 0 &&
299 + (ref
300 + .watch(smartRepliesProvider)
301 + .asData
302 + ?.value
303 + .isNotEmpty ??
304 + false),
305 ),
306
307 Positioned(
308 bottom: 0,
309 left: 0,
310 right: 0,
293 - child: Stack(
294 - children: [
295 - if (!chatState.isStreaming)
296 - _SmartReplyChips(
297 - onSend: (message) {
298 - ref.read(chatProvider.notifier).sendMessage(message);
311 + child: Container(
312 + decoration: BoxDecoration(
313 + gradient: LinearGradient(
314 + begin: Alignment.topCenter,
315 + end: Alignment.bottomCenter,
316 + colors: [
317 + theme.scaffoldBackgroundColor.withValues(alpha: 0.1),
318 + theme.scaffoldBackgroundColor.withValues(alpha: 0.8),
319 + theme.scaffoldBackgroundColor,
320 + ],
321 + stops: const [0.0, 0.7, 1.0],
322 + ),
323 + ),
324 + child: Column(
325 + mainAxisSize: MainAxisSize.min,
326 + children: [
327 + if (!chatState.isStreaming &&
328 + MediaQuery.of(context).viewInsets.bottom == 0) ...[
329 + const SizedBox(height: 4),
330 + _SmartReplyChips(
331 + onSend: (message) {
332 + ref
333 + .read(chatProvider.notifier)
334 + .sendMessage(message);
335 + },
336 + ),
337 + ],
338 + ChatInputBar(
339 + focusNode: _inputFocusNode,
340 + isStreaming: chatState.isStreaming,
341 + onSend: (message, {attachments}) {
342 + ref
343 + .read(chatProvider.notifier)
344 + .sendMessage(message, attachments: attachments);
345 + },
346 + onStop: () {
347 + ref.read(chatProvider.notifier).cancelStream();
348 },
349 ),
301 - ChatInputBar(
302 - isStreaming: chatState.isStreaming,
303 - onSend: (message, {attachments}) {
304 - ref
305 - .read(chatProvider.notifier)
306 - .sendMessage(message, attachments: attachments);
307 - },
308 - onStop: () {
309 - ref.read(chatProvider.notifier).cancelStream();
310 - },
311 - ),
312 - ],
350 + ],
351 + ),
352 ),
353 ),
354 ],
@@ -664,9 +703,7 @@ class _ConnectionBanner extends StatelessWidget {
703 const SizedBox(width: 8),
704 Expanded(
705 child: Text(
667 - isError
668 - ? l10n.connection_error
669 - : l10n.disconnected,
706 + isError ? l10n.connection_error : l10n.disconnected,
707 style: TextStyle(
708 fontSize: 13,
709 color: isError ? Colors.red : Colors.orange[700],
@@ -1150,7 +1187,7 @@ class _MessageList extends StatelessWidget {
1187 cacheExtent: 1000,
1188 padding: EdgeInsets.only(
1189 top: 16,
1153 - bottom: 120 + (hasSmartReplies ? 56 : 0),
1190 + bottom: 120 + (hasSmartReplies ? 64 : 0),
1191 ),
1192 itemCount:
1193 allMessages.length +
@@ -1264,7 +1301,8 @@ class _SmartReplyChipsState extends ConsumerState<_SmartReplyChips>
1301
1302 @override
1303 Widget build(BuildContext context) {
1267 - final suggestions = ref.watch(smartRepliesProvider);
1304 + final suggestionsAsync = ref.watch(smartRepliesProvider);
1305 + final suggestions = suggestionsAsync.asData?.value ?? [];
1306
1307 if (suggestions.isEmpty) {
1308 _previousSuggestions = [];
@@ -1282,7 +1320,8 @@ class _SmartReplyChipsState extends ConsumerState<_SmartReplyChips>
1320 child: SizedBox(
1321 height: 40,
1322 child: ListView.separated(
1285 - padding: EdgeInsetsDirectional.only(start: 8),
1323 + padding: const EdgeInsets.symmetric(horizontal: 20),
1324 + clipBehavior: Clip.none,
1325 scrollDirection: Axis.horizontal,
1326 itemCount: suggestions.length,
1327 separatorBuilder: (_, _) => const SizedBox(width: 6),
@@ -1306,31 +1345,49 @@ class _SmartReplyChipsState extends ConsumerState<_SmartReplyChips>
1345 ),
1346 );
1347 },
1309 - child: GestureDetector(
1310 - onTap: () => widget.onSend(label),
1311 - child: Container(
1312 - padding: const EdgeInsets.symmetric(
1313 - horizontal: 12,
1314 - vertical: 6,
1315 - ),
1316 - decoration: BoxDecoration(
1317 - color: isDark
1318 - ? const Color(0xFF2A2A2A)
1319 - : const Color(0xFFF0F0F0),
1320 - borderRadius: BorderRadius.circular(16),
1321 - border: Border.all(
1322 - color: isDark
1323 - ? const Color(0xFF3A3A3A)
1324 - : const Color(0xFFE0E0E0),
1325 - ),
1326 - ),
1327 - child: Text(
1328 - label,
1329 - style: TextStyle(
1330 - fontSize: 13,
1331 - color: isDark
1332 - ? const Color(0xFFCCCCCC)
1333 - : const Color(0xFF444444),
1348 + child: Center(
1349 + child: GestureDetector(
1350 + onTap: () => widget.onSend(label),
1351 + child: ClipRRect(
1352 + borderRadius: BorderRadius.circular(20),
1353 + child: BackdropFilter(
1354 + filter: ImageFilter.blur(sigmaX: 12, sigmaY: 12),
1355 + child: Container(
1356 + padding: const EdgeInsets.symmetric(
1357 + horizontal: 12,
1358 + vertical: 6,
1359 + ),
1360 + decoration: BoxDecoration(
1361 + gradient: LinearGradient(
1362 + begin: Alignment.topLeft,
1363 + end: Alignment.bottomRight,
1364 + colors: isDark
1365 + ? [
1366 + Colors.white.withValues(alpha: 0.10),
1367 + Colors.white.withValues(alpha: 0.05),
1368 + ]
1369 + : [
1370 + Colors.white.withValues(alpha: 0.70),
1371 + Colors.white.withValues(alpha: 0.40),
1372 + ],
1373 + ),
1374 + borderRadius: BorderRadius.circular(20),
1375 + border: Border.all(
1376 + color: isDark
1377 + ? Colors.white.withValues(alpha: 0.18)
1378 + : Colors.white.withValues(alpha: 0.80),
1379 + width: 1.0,
1380 + ),
1381 + ),
1382 + child: Text(
1383 + label,
1384 + style: TextStyle(
1385 + fontSize: 13,
1386 + fontWeight: FontWeight.w500,
1387 + color: theme.colorScheme.onSurface,
1388 + ),
1389 + ),
1390 + ),
1391 ),
1392 ),
1393 ),
@@ -1460,7 +1517,10 @@ Error: $errorMessage
1517 crossAxisAlignment: CrossAxisAlignment.start,
1518 mainAxisSize: MainAxisSize.min,
1519 children: [
1463 - _DebugRow(label: debugL10n.conversation_id, value: conversation.id),
1520 + _DebugRow(
1521 + label: debugL10n.conversation_id,
1522 + value: conversation.id,
1523 + ),
1524 _DebugRow(
1525 label: debugL10n.created_at,
1526 value: conversation.createdAt.toIso8601String(),
lib/features/chat/views/components/chat_input_bar.dart
+27 -13
@@ -18,6 +18,7 @@ class ChatInputBar extends ConsumerStatefulWidget {
18 this.onAttach,
19 this.enabled = true,
20 this.isStreaming = false,
21 + this.focusNode,
22 });
23
24 final void Function(String message, {List<File>? attachments}) onSend;
@@ -25,6 +26,7 @@ class ChatInputBar extends ConsumerStatefulWidget {
26 final void Function(List<File> attachments)? onAttach;
27 final bool enabled;
28 final bool isStreaming;
29 + final FocusNode? focusNode;
30
31 @override
32 ConsumerState<ChatInputBar> createState() => _ChatInputBarState();
@@ -33,7 +35,7 @@ class ChatInputBar extends ConsumerStatefulWidget {
35 class _ChatInputBarState extends ConsumerState<ChatInputBar>
36 with SingleTickerProviderStateMixin {
37 final _controller = TextEditingController();
36 - final _focusNode = FocusNode();
38 + late final FocusNode _focusNode;
39 final List<File> _attachedFiles = [];
40 late AnimationController _sendButtonAnimController;
41 late Animation<double> _sendButtonScale;
@@ -41,6 +43,7 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
43 @override
44 void initState() {
45 super.initState();
46 + _focusNode = widget.focusNode ?? FocusNode();
47 _sendButtonAnimController = AnimationController(
48 duration: const Duration(milliseconds: 200),
49 vsync: this,
@@ -53,7 +56,9 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
56 @override
57 void dispose() {
58 _controller.dispose();
56 - _focusNode.dispose();
59 + if (widget.focusNode == null) {
60 + _focusNode.dispose();
61 + }
62 _sendButtonAnimController.dispose();
63 super.dispose();
64 }
@@ -90,10 +95,14 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
95 widget.onStop();
96 }
97
93 - Widget _buildActionButton(bool canSend, bool isDark) {
98 + Widget _buildActionButton(bool canSend, ThemeData theme) {
99 final l10n = AppLocalizations.of(context)!;
95 - final backgroundColor = isDark ? Colors.white : Colors.black;
96 - final iconColor = isDark ? Colors.black : Colors.white;
100 + final backgroundColor = widget.isStreaming
101 + ? Colors.red
102 + : theme.colorScheme.primary;
103 + final iconColor = widget.isStreaming
104 + ? Colors.white
105 + : theme.colorScheme.onPrimary;
106
107 return GestureDetector(
108 onTapDown: canSend ? (_) => _sendButtonAnimController.forward() : null,
@@ -157,11 +166,12 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
166 !widget.isStreaming;
167
168 return SafeArea(
169 + top: false,
170 child: Container(
161 - margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
171 + margin: const EdgeInsets.only(left: 20, right: 20, top: 8, bottom: 16),
172 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
173 decoration: BoxDecoration(
164 - color: isDark ? const Color(0xFF1C1C1C) : Colors.white,
174 + color: theme.colorScheme.surface,
175 borderRadius: BorderRadius.circular(40),
176 boxShadow: [
177 BoxShadow(
@@ -171,7 +181,7 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
181 ),
182 ],
183 border: Border.all(
174 - color: isDark ? const Color(0xFF2E2E2E) : const Color(0xFFE5E5E5),
184 + color: theme.colorScheme.outline,
185 width: 1.2,
186 ),
187 ),
@@ -252,7 +262,7 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
262 IconButton(
263 icon: HugeIcon(
264 icon: HugeIcons.strokeRoundedPlusSign,
255 - color: isDark ? Colors.white70 : Colors.black87,
265 + color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
266 size: 22,
267 ),
268 onPressed: isConnected ? _handleAttach : null,
@@ -273,23 +283,27 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
283 },
284 style: TextStyle(
285 fontSize: 15,
276 - color: isDark ? Colors.white : Colors.black87,
286 + color: theme.colorScheme.onSurface,
287 ),
288 decoration: InputDecoration(
289 + filled: false,
290 + border: InputBorder.none,
291 + enabledBorder: InputBorder.none,
292 + focusedBorder: InputBorder.none,
293 + disabledBorder: InputBorder.none,
294 hintText: l10n.chat_input_hint,
295 hintStyle: TextStyle(
281 - color: isDark ? Colors.white38 : Colors.black38,
296 + color: theme.colorScheme.onSurface.withValues(alpha: 0.38),
297 fontSize: 15,
298 fontWeight: FontWeight.w400,
299 ),
285 - border: InputBorder.none,
300 isDense: true,
301 contentPadding: const EdgeInsets.symmetric(vertical: 12),
302 ),
303 ),
304 ),
305 const SizedBox(width: 8),
292 - _buildActionButton(canSend, isDark),
306 + _buildActionButton(canSend, theme),
307 ],
308 ),
309 ],
lib/features/conversations/data/models/conversation.dart
+12
@@ -15,6 +15,8 @@ class Conversation {
15 final int? maxTokens;
16 final int? contextLength;
17 final bool? mcpEnabled;
18 + final List<String>? smartReplies;
19 + final String? smartRepliesLastMessageId;
20
21 Conversation({
22 required this.id,
@@ -33,6 +35,8 @@ class Conversation {
35 this.maxTokens,
36 this.contextLength,
37 this.mcpEnabled,
38 + this.smartReplies,
39 + this.smartRepliesLastMessageId,
40 });
41
42 Conversation copyWith({
@@ -59,6 +63,10 @@ class Conversation {
63 bool clearContextLength = false,
64 bool? mcpEnabled,
65 bool clearMcpEnabled = false,
66 + List<String>? smartReplies,
67 + bool clearSmartReplies = false,
68 + String? smartRepliesLastMessageId,
69 + bool clearSmartRepliesLastMessageId = false,
70 }) {
71 return Conversation(
72 id: id ?? this.id,
@@ -79,6 +87,10 @@ class Conversation {
87 contextLength:
88 clearContextLength ? null : (contextLength ?? this.contextLength),
89 mcpEnabled: clearMcpEnabled ? null : (mcpEnabled ?? this.mcpEnabled),
90 + smartReplies: clearSmartReplies ? null : (smartReplies ?? this.smartReplies),
91 + smartRepliesLastMessageId: clearSmartRepliesLastMessageId
92 + ? null
93 + : (smartRepliesLastMessageId ?? this.smartRepliesLastMessageId),
94 );
95 }
96
lib/features/conversations/providers/conversation_providers.dart
+31
@@ -279,6 +279,37 @@ class ConversationsNotifier extends AsyncNotifier<List<Conversation>> {
279 }
280 }
281
282 + Future<void> updateSmartReplies(
283 + String id,
284 + List<String> replies,
285 + String lastMessageId,
286 + ) async {
287 + final db = ref.read(databaseProvider);
288 + final conversations = state.value ?? [];
289 + final existingIndex = conversations.indexWhere((c) => c.id == id);
290 + if (existingIndex != -1) {
291 + final existing = conversations[existingIndex];
292 + final updated = existing.copyWith(
293 + smartReplies: replies,
294 + smartRepliesLastMessageId: lastMessageId,
295 + );
296 +
297 + final query = db.conversationBox
298 + .query(ConversationEntity_.id.equals(id))
299 + .build();
300 + final existingEntity = query.findFirst();
301 + query.close();
302 +
303 + final entity = ConversationEntity.fromDomain(updated);
304 + if (existingEntity != null) {
305 + entity.internalId = existingEntity.internalId;
306 + }
307 + db.conversationBox.put(entity);
308 +
309 + state = AsyncData(await _loadAll());
310 + }
311 + }
312 +
313 Future<void> deleteAll() async {
314 final db = ref.read(databaseProvider);
315 db.messageBox.removeAll();
lib/features/settings/data/models/app_settings.dart
+6
@@ -36,6 +36,7 @@ class AppSettings {
36 final double ttsSpeed;
37 final KittenTtsModelVariant kittenTtsModelVariant;
38 final bool autoSpeakEnabled;
39 + final bool smartReplyEnabled;
40 final String? localeCode;
41
42 AppSettings({
@@ -65,6 +66,7 @@ class AppSettings {
66 this.ttsSpeed = 1.0,
67 this.kittenTtsModelVariant = KittenTtsModelVariant.nanoInt8,
68 this.autoSpeakEnabled = false,
69 + this.smartReplyEnabled = true,
70 this.localeCode,
71 });
72
@@ -95,6 +97,7 @@ class AppSettings {
97 double? ttsSpeed,
98 KittenTtsModelVariant? kittenTtsModelVariant,
99 bool? autoSpeakEnabled,
100 + bool? smartReplyEnabled,
101 Object? localeCode = _unset,
102 }) {
103 return AppSettings(
@@ -130,6 +133,7 @@ class AppSettings {
133 kittenTtsModelVariant:
134 kittenTtsModelVariant ?? this.kittenTtsModelVariant,
135 autoSpeakEnabled: autoSpeakEnabled ?? this.autoSpeakEnabled,
136 + smartReplyEnabled: smartReplyEnabled ?? this.smartReplyEnabled,
137 localeCode: identical(localeCode, _unset)
138 ? this.localeCode
139 : localeCode as String?,
@@ -164,6 +168,7 @@ class AppSettings {
168 'ttsSpeed': ttsSpeed,
169 'kittenTtsModelVariant': kittenTtsModelVariant.name,
170 'autoSpeakEnabled': autoSpeakEnabled,
171 + 'smartReplyEnabled': smartReplyEnabled,
172 'localeCode': localeCode,
173 };
174 }
@@ -196,6 +201,7 @@ class AppSettings {
201 ttsSpeed: map['ttsSpeed']?.toDouble() ?? 1.0,
202 kittenTtsModelVariant: _parseKittenVariant(map['kittenTtsModelVariant']),
203 autoSpeakEnabled: map['autoSpeakEnabled'] ?? false,
204 + smartReplyEnabled: map['smartReplyEnabled'] ?? true,
205 localeCode: map['localeCode'] as String?,
206 );
207 }
lib/features/settings/views/settings_screen.dart
+7
@@ -162,6 +162,13 @@ class SettingsViews extends ConsumerWidget {
162 ),
163 const Divider(height: 32),
164 _SectionHeader(title: l10n.settings_on_device),
165 + _ToggleSetting(
166 + label: l10n.enable_smart_reply,
167 + value: settings.smartReplyEnabled,
168 + onChanged: (v) =>
169 + ref.read(settingsProvider.notifier).setSmartReplyEnabled(v),
170 + isDark: isDark,
171 + ),
172 Padding(
173 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
174 child: OutlinedButton.icon(
lib/l10n/app_ar.arb
+2
@@ -281,6 +281,8 @@
281
282 "manage_on_device_models": "إدارة النماذج المحلية",
283
284 + "enable_smart_reply": "الردود الذكية المحلية",
285 +
286 "streaming_responses": "الردود المتدفقة",
287
288 "auto_generate_titles": "إنشاء العناوين تلقائياً",
lib/l10n/app_bn.arb
+1
@@ -174,6 +174,7 @@
174 "loading_model": "{modelId} লোড করা হচ্ছে...",
175 "loading_model_error": "ত্রুটি: {error}",
176 "manage_on_device_models": "অন-ডিভাইস মডেল পরিচালনা করুন",
177 + "enable_smart_reply": "অন-ডিভাইস স্মার্ট রিপ্লাই",
178 "manage_servers": "সার্ভার পরিচালনা করুন",
179 "manage_tts_models": "টিটিএস (TTS) মডেল পরিচালনা করুন",
180 "max_tokens": "সর্বোচ্চ টোকেন",
lib/l10n/app_en.arb
+5
@@ -785,6 +785,11 @@
785 "description": "Button to manage on-device models"
786 },
787
788 + "enable_smart_reply": "On-Device Smart Replies",
789 + "@enable_smart_reply": {
790 + "description": "Toggle for on-device smart reply suggestions"
791 + },
792 +
793 "streaming_responses": "Streaming Responses",
794 "@streaming_responses": {
795 "description": "Toggle for streaming responses"
lib/l10n/app_es.arb
+1
@@ -174,6 +174,7 @@
174 "loading_model": "Cargando {modelId}...",
175 "loading_model_error": "Error: {error}",
176 "manage_on_device_models": "Gestionar modelos locales",
177 + "enable_smart_reply": "Respuestas inteligentes locales",
178 "manage_servers": "Gestionar servidores",
179 "manage_tts_models": "Gestionar modelos TTS",
180 "max_tokens": "Máximo de tokens",
lib/l10n/app_hi.arb
+1
@@ -174,6 +174,7 @@
174 "loading_model": "{modelId} लोड किया जा रहा है...",
175 "loading_model_error": "त्रुटि: {error}",
176 "manage_on_device_models": "ऑन-डिवाइस मॉडल प्रबंधित करें",
177 + "enable_smart_reply": "ऑन-डिवाइस स्मार्ट रिप्लाइ",
178 "manage_servers": "सर्वर प्रबंधित करें",
179 "manage_tts_models": "TTS मॉडल प्रबंधित करें",
180 "max_tokens": "अधिकतम टोकन",
lib/l10n/app_localizations.dart
+6
@@ -946,6 +946,12 @@ abstract class AppLocalizations {
946 /// **'Manage On-Device Models'**
947 String get manage_on_device_models;
948
949 + /// Toggle for on-device smart reply suggestions
950 + ///
951 + /// In en, this message translates to:
952 + /// **'On-Device Smart Replies'**
953 + String get enable_smart_reply;
954 +
955 /// Toggle for streaming responses
956 ///
957 /// In en, this message translates to:
lib/l10n/app_localizations_ar.dart
+3
@@ -455,6 +455,9 @@ class AppLocalizationsAr extends AppLocalizations {
455 @override
456 String get manage_on_device_models => 'إدارة النماذج المحلية';
457
458 + @override
459 + String get enable_smart_reply => 'الردود الذكية المحلية';
460 +
461 @override
462 String get streaming_responses => 'الردود المتدفقة';
463
lib/l10n/app_localizations_bn.dart
+3
@@ -458,6 +458,9 @@ class AppLocalizationsBn extends AppLocalizations {
458 @override
459 String get manage_on_device_models => 'অন-ডিভাইস মডেল পরিচালনা করুন';
460
461 + @override
462 + String get enable_smart_reply => 'অন-ডিভাইস স্মার্ট রিপ্লাই';
463 +
464 @override
465 String get streaming_responses => 'স্ট্রিমিং প্রতিক্রিয়া';
466
lib/l10n/app_localizations_en.dart
+3
@@ -456,6 +456,9 @@ class AppLocalizationsEn extends AppLocalizations {
456 @override
457 String get manage_on_device_models => 'Manage On-Device Models';
458
459 + @override
460 + String get enable_smart_reply => 'On-Device Smart Replies';
461 +
462 @override
463 String get streaming_responses => 'Streaming Responses';
464
lib/l10n/app_localizations_es.dart
+3
@@ -457,6 +457,9 @@ class AppLocalizationsEs extends AppLocalizations {
457 @override
458 String get manage_on_device_models => 'Gestionar modelos locales';
459
460 + @override
461 + String get enable_smart_reply => 'Respuestas inteligentes locales';
462 +
463 @override
464 String get streaming_responses => 'Respuestas en tiempo real (streaming)';
465
lib/l10n/app_localizations_hi.dart
+3
@@ -458,6 +458,9 @@ class AppLocalizationsHi extends AppLocalizations {
458 @override
459 String get manage_on_device_models => 'ऑन-डिवाइस मॉडल प्रबंधित करें';
460
461 + @override
462 + String get enable_smart_reply => 'ऑन-डिवाइस स्मार्ट रिप्लाइ';
463 +
464 @override
465 String get streaming_responses => 'प्रतिक्रियाएं स्ट्रीम करें';
466
lib/l10n/app_localizations_zh.dart
+3
@@ -452,6 +452,9 @@ class AppLocalizationsZh extends AppLocalizations {
452 @override
453 String get manage_on_device_models => '管理设备端模型';
454
455 + @override
456 + String get enable_smart_reply => '设备端智能回复';
457 +
458 @override
459 String get streaming_responses => '流式响应';
460
lib/l10n/app_zh.arb
+1
@@ -174,6 +174,7 @@
174 "loading_model": "正在加载 {modelId}...",
175 "loading_model_error": "错误: {error}",
176 "manage_on_device_models": "管理设备端模型",
177 + "enable_smart_reply": "设备端智能回复",
178 "manage_servers": "管理服务器",
179 "manage_tts_models": "管理 TTS 模型",
180 "max_tokens": "最大 Token 数",
lib/objectbox-model.json
+11 -1
@@ -5,7 +5,7 @@
5 "entities": [
6 {
7 "id": "1:6060443518060866028",
8 - "lastPropertyId": "17:8783340423572117942",
8 + "lastPropertyId": "19:2437473986237603139",
9 "name": "ConversationEntity",
10 "properties": [
11 {
@@ -95,6 +95,16 @@
95 "id": "17:8783340423572117942",
96 "name": "mcpEnabled",
97 "type": 1
98 + },
99 + {
100 + "id": "18:2260684969168742247",
101 + "name": "smartRepliesJson",
102 + "type": 9
103 + },
104 + {
105 + "id": "19:2437473986237603139",
106 + "name": "smartRepliesLastMessageId",
107 + "type": 9
108 }
109 ],
110 "relations": []
lib/objectbox.g.dart
+40 -2
@@ -22,7 +22,7 @@ final _entities = <obx_int.ModelEntity>[
22 obx_int.ModelEntity(
23 id: const obx_int.IdUid(1, 6060443518060866028),
24 name: 'ConversationEntity',
25 - lastPropertyId: const obx_int.IdUid(17, 8783340423572117942),
25 + lastPropertyId: const obx_int.IdUid(19, 2437473986237603139),
26 flags: 0,
27 properties: <obx_int.ModelProperty>[
28 obx_int.ModelProperty(
@@ -128,6 +128,18 @@ final _entities = <obx_int.ModelEntity>[
128 type: 1,
129 flags: 0,
130 ),
131 + obx_int.ModelProperty(
132 + id: const obx_int.IdUid(18, 2260684969168742247),
133 + name: 'smartRepliesJson',
134 + type: 9,
135 + flags: 0,
136 + ),
137 + obx_int.ModelProperty(
138 + id: const obx_int.IdUid(19, 2437473986237603139),
139 + name: 'smartRepliesLastMessageId',
140 + type: 9,
141 + flags: 0,
142 + ),
143 ],
144 relations: <obx_int.ModelRelation>[],
145 backlinks: <obx_int.ModelBacklink>[
@@ -506,7 +518,14 @@ obx_int.ModelDefinition getObjectBoxModel() {
518 final systemPromptOffset = object.systemPrompt == null
519 ? null
520 : fbb.writeString(object.systemPrompt!);
509 - fbb.startTable(18);
521 + final smartRepliesJsonOffset = object.smartRepliesJson == null
522 + ? null
523 + : fbb.writeString(object.smartRepliesJson!);
524 + final smartRepliesLastMessageIdOffset =
525 + object.smartRepliesLastMessageId == null
526 + ? null
527 + : fbb.writeString(object.smartRepliesLastMessageId!);
528 + fbb.startTable(20);
529 fbb.addInt64(0, object.internalId);
530 fbb.addOffset(1, idOffset);
531 fbb.addOffset(2, titleOffset);
@@ -524,6 +543,8 @@ obx_int.ModelDefinition getObjectBoxModel() {
543 fbb.addInt64(14, object.maxTokens);
544 fbb.addInt64(15, object.contextLength);
545 fbb.addBool(16, object.mcpEnabled);
546 + fbb.addOffset(17, smartRepliesJsonOffset);
547 + fbb.addOffset(18, smartRepliesLastMessageIdOffset);
548 fbb.finish(fbb.endTable());
549 return object.internalId;
550 },
@@ -600,6 +621,12 @@ obx_int.ModelDefinition getObjectBoxModel() {
621 rootOffset,
622 36,
623 );
624 + final smartRepliesJsonParam = const fb.StringReader(
625 + asciiOptimization: true,
626 + ).vTableGetNullable(buffer, rootOffset, 38);
627 + final smartRepliesLastMessageIdParam = const fb.StringReader(
628 + asciiOptimization: true,
629 + ).vTableGetNullable(buffer, rootOffset, 40);
630 final object = ConversationEntity(
631 internalId: internalIdParam,
632 id: idParam,
@@ -618,6 +645,8 @@ obx_int.ModelDefinition getObjectBoxModel() {
645 maxTokens: maxTokensParam,
646 contextLength: contextLengthParam,
647 mcpEnabled: mcpEnabledParam,
648 + smartRepliesJson: smartRepliesJsonParam,
649 + smartRepliesLastMessageId: smartRepliesLastMessageIdParam,
650 );
651 obx_int.InternalToManyAccess.setRelInfo<ConversationEntity>(
652 object.messages,
@@ -1071,6 +1100,15 @@ class ConversationEntity_ {
1100 _entities[0].properties[16],
1101 );
1102
1103 + /// See [ConversationEntity.smartRepliesJson].
1104 + static final smartRepliesJson = obx.QueryStringProperty<ConversationEntity>(
1105 + _entities[0].properties[17],
1106 + );
1107 +
1108 + /// See [ConversationEntity.smartRepliesLastMessageId].
1109 + static final smartRepliesLastMessageId =
1110 + obx.QueryStringProperty<ConversationEntity>(_entities[0].properties[18]);
1111 +
1112 /// see [ConversationEntity.messages]
1113 static final messages =
1114 obx.QueryBacklinkToMany<MessageEntity, ConversationEntity>(
pubspec.yaml
+1 -1
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
16 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
17 # In Windows, build-name is used as the major, minor, and patch parts
18 # of the product and file versions while build-number is used as the build suffix.
19 -version: 1.2.4+23
19 +version: 1.2.6+25
20
21 environment:
22 sdk: ^3.11.0