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