main
dart 435 lines 17 KB
Raw
1 import 'dart:convert';
2
3 import 'package:flutter/material.dart';
4 import 'package:flutter_gemma/flutter_gemma.dart';
5
6 import '../../../../core/models/enums.dart';
7 import '../../../../features/chat/data/models/mcp_integration.dart';
8 import '../../../../features/chat/utils/image_upload_utils.dart';
9 import '../../../../features/tts/data/kitten_tts_model.dart';
10
11 enum SyntaxThemeName { light, dark }
12
13 class AppSettings {
14 static const Object _unset = Object();
15
16 final double temperature;
17 final double topP;
18 final int maxTokens;
19 final int contextLength;
20 final ThemeMode themeMode;
21 final double fontSize;
22 final bool showSystemMessages;
23 final bool hapticFeedbackEnabled;
24 final bool sendOnEnter;
25 final String? defaultServerId;
26 final bool showDataIndicator;
27 final bool autoGenerateTitle;
28 final bool streamingEnabled;
29 final String? defaultPersonaId;
30 final String? defaultModelId;
31 final String? defaultModelServerId;
32 final bool hasCompletedOnboarding;
33 final bool hasAskedForNotifications;
34 final bool mcpEnabled;
35 final bool newChatMcpEnabled;
36 final SyntaxThemeName codeThemeDark;
37 final SyntaxThemeName codeThemeLight;
38 final PreferredBackend preferredBackend;
39 final EngineId ttsEngine;
40 final String? ttsVoiceId;
41 final double ttsSpeed;
42 final KittenTtsModelVariant kittenTtsModelVariant;
43 final bool autoSpeakEnabled;
44 final bool conciseVoiceResponsesEnabled;
45 final bool ttsProcessMarkdown;
46 final int ttsSkipSeconds;
47 final bool smartReplyEnabled;
48 final bool aiUserResponseEnabled;
49 final String? localeCode;
50 final String? huggingFaceToken;
51 final bool unloadModelsBeforeLoad;
52 final bool tempChatKeyboardIncognito;
53 final bool resumeLastChat;
54 final bool imageCompressionEnabled;
55 final ImageCompressionLevel imageCompressionLevel;
56 final bool smartRepliesUsePersona;
57 final bool keepPersonaOnNewChat;
58 final bool roleSwapButtonEnabled;
59 final bool showSystemMessagesInChat;
60 final bool calendarToolsEnabled;
61 final bool locationToolsEnabled;
62 final bool autoCollapseThinking;
63
64 /// Whether `temperature` / `top_p` are sent to remote APIs. Some providers
65 /// (e.g. reasoning models) reject them (#81).
66 final bool sendTemperature;
67 final bool sendTopP;
68
69 /// Fallback system prompt used when a chat has neither its own prompt nor
70 /// a persona.
71 final String defaultSystemPrompt;
72 final List<McpIntegration> savedMcpIntegrations;
73
74 AppSettings({
75 this.temperature = 0.7,
76 this.topP = 0.9,
77 this.maxTokens = 2048,
78 this.contextLength = 4096,
79 this.themeMode = ThemeMode.dark,
80 this.fontSize = 16.0,
81 this.showSystemMessages = false,
82 this.hapticFeedbackEnabled = true,
83 this.sendOnEnter = false,
84 this.defaultServerId,
85 this.showDataIndicator = true,
86 this.autoGenerateTitle = true,
87 this.streamingEnabled = true,
88 this.defaultPersonaId,
89 this.defaultModelId,
90 this.defaultModelServerId,
91 this.hasCompletedOnboarding = false,
92 this.hasAskedForNotifications = false,
93 this.mcpEnabled = false,
94 this.newChatMcpEnabled = false,
95 this.codeThemeDark = SyntaxThemeName.dark,
96 this.codeThemeLight = SyntaxThemeName.light,
97 this.preferredBackend = PreferredBackend.cpu,
98 this.ttsEngine = EngineId.system,
99 this.ttsVoiceId,
100 this.ttsSpeed = 1.0,
101 this.kittenTtsModelVariant = KittenTtsModelVariant.nanoInt8,
102 this.autoSpeakEnabled = false,
103 this.conciseVoiceResponsesEnabled = true,
104 this.ttsProcessMarkdown = true,
105 this.ttsSkipSeconds = 10,
106 this.smartReplyEnabled = true,
107 this.aiUserResponseEnabled = false,
108 this.localeCode,
109 this.huggingFaceToken,
110 this.unloadModelsBeforeLoad = false,
111 this.tempChatKeyboardIncognito = true,
112 this.resumeLastChat = true,
113 this.imageCompressionEnabled = false,
114 this.imageCompressionLevel = ImageCompressionLevel.medium,
115 this.smartRepliesUsePersona = false,
116 this.keepPersonaOnNewChat = false,
117 this.roleSwapButtonEnabled = false,
118 this.showSystemMessagesInChat = true,
119 this.calendarToolsEnabled = false,
120 this.locationToolsEnabled = false,
121 this.autoCollapseThinking = false,
122 this.sendTemperature = true,
123 this.sendTopP = true,
124 this.defaultSystemPrompt = '',
125 this.savedMcpIntegrations = const [],
126 });
127
128 AppSettings copyWith({
129 double? temperature,
130 double? topP,
131 int? maxTokens,
132 int? contextLength,
133 ThemeMode? themeMode,
134 double? fontSize,
135 bool? showSystemMessages,
136 bool? hapticFeedbackEnabled,
137 bool? sendOnEnter,
138 String? defaultServerId,
139 bool? showDataIndicator,
140 bool? autoGenerateTitle,
141 bool? streamingEnabled,
142 String? defaultPersonaId,
143 Object? defaultModelId = _unset,
144 Object? defaultModelServerId = _unset,
145 bool? hasCompletedOnboarding,
146 bool? hasAskedForNotifications,
147 bool? mcpEnabled,
148 bool? newChatMcpEnabled,
149 SyntaxThemeName? codeThemeDark,
150 SyntaxThemeName? codeThemeLight,
151 PreferredBackend? preferredBackend,
152 EngineId? ttsEngine,
153 Object? ttsVoiceId = _unset,
154 double? ttsSpeed,
155 KittenTtsModelVariant? kittenTtsModelVariant,
156 bool? autoSpeakEnabled,
157 bool? conciseVoiceResponsesEnabled,
158 bool? ttsProcessMarkdown,
159 int? ttsSkipSeconds,
160 bool? smartReplyEnabled,
161 bool? aiUserResponseEnabled,
162 Object? localeCode = _unset,
163 Object? huggingFaceToken = _unset,
164 bool? unloadModelsBeforeLoad,
165 bool? tempChatKeyboardIncognito,
166 bool? resumeLastChat,
167 bool? imageCompressionEnabled,
168 ImageCompressionLevel? imageCompressionLevel,
169 bool? smartRepliesUsePersona,
170 bool? keepPersonaOnNewChat,
171 bool? roleSwapButtonEnabled,
172 bool? showSystemMessagesInChat,
173 bool? calendarToolsEnabled,
174 bool? locationToolsEnabled,
175 bool? autoCollapseThinking,
176 bool? sendTemperature,
177 bool? sendTopP,
178 String? defaultSystemPrompt,
179 List<McpIntegration>? savedMcpIntegrations,
180 }) {
181 return AppSettings(
182 temperature: temperature ?? this.temperature,
183 topP: topP ?? this.topP,
184 maxTokens: maxTokens ?? this.maxTokens,
185 contextLength: contextLength ?? this.contextLength,
186 themeMode: themeMode ?? this.themeMode,
187 fontSize: fontSize ?? this.fontSize,
188 showSystemMessages: showSystemMessages ?? this.showSystemMessages,
189 hapticFeedbackEnabled:
190 hapticFeedbackEnabled ?? this.hapticFeedbackEnabled,
191 sendOnEnter: sendOnEnter ?? this.sendOnEnter,
192 defaultServerId: defaultServerId ?? this.defaultServerId,
193 showDataIndicator: showDataIndicator ?? this.showDataIndicator,
194 autoGenerateTitle: autoGenerateTitle ?? this.autoGenerateTitle,
195 streamingEnabled: streamingEnabled ?? this.streamingEnabled,
196 defaultPersonaId: defaultPersonaId ?? this.defaultPersonaId,
197 defaultModelId: identical(defaultModelId, _unset)
198 ? this.defaultModelId
199 : defaultModelId as String?,
200 defaultModelServerId: identical(defaultModelServerId, _unset)
201 ? this.defaultModelServerId
202 : defaultModelServerId as String?,
203 hasCompletedOnboarding:
204 hasCompletedOnboarding ?? this.hasCompletedOnboarding,
205 hasAskedForNotifications:
206 hasAskedForNotifications ?? this.hasAskedForNotifications,
207 mcpEnabled: mcpEnabled ?? this.mcpEnabled,
208 newChatMcpEnabled: newChatMcpEnabled ?? this.newChatMcpEnabled,
209 codeThemeDark: codeThemeDark ?? this.codeThemeDark,
210 codeThemeLight: codeThemeLight ?? this.codeThemeLight,
211 preferredBackend: preferredBackend ?? this.preferredBackend,
212 ttsEngine: ttsEngine ?? this.ttsEngine,
213 ttsVoiceId: identical(ttsVoiceId, _unset)
214 ? this.ttsVoiceId
215 : ttsVoiceId as String?,
216 ttsSpeed: ttsSpeed ?? this.ttsSpeed,
217 kittenTtsModelVariant:
218 kittenTtsModelVariant ?? this.kittenTtsModelVariant,
219 autoSpeakEnabled: autoSpeakEnabled ?? this.autoSpeakEnabled,
220 conciseVoiceResponsesEnabled:
221 conciseVoiceResponsesEnabled ?? this.conciseVoiceResponsesEnabled,
222 ttsProcessMarkdown: ttsProcessMarkdown ?? this.ttsProcessMarkdown,
223 ttsSkipSeconds: ttsSkipSeconds ?? this.ttsSkipSeconds,
224 smartReplyEnabled: smartReplyEnabled ?? this.smartReplyEnabled,
225 aiUserResponseEnabled:
226 aiUserResponseEnabled ?? this.aiUserResponseEnabled,
227 localeCode: identical(localeCode, _unset)
228 ? this.localeCode
229 : localeCode as String?,
230 huggingFaceToken: identical(huggingFaceToken, _unset)
231 ? this.huggingFaceToken
232 : huggingFaceToken as String?,
233 unloadModelsBeforeLoad:
234 unloadModelsBeforeLoad ?? this.unloadModelsBeforeLoad,
235 tempChatKeyboardIncognito:
236 tempChatKeyboardIncognito ?? this.tempChatKeyboardIncognito,
237 resumeLastChat: resumeLastChat ?? this.resumeLastChat,
238 imageCompressionEnabled:
239 imageCompressionEnabled ?? this.imageCompressionEnabled,
240 imageCompressionLevel:
241 imageCompressionLevel ?? this.imageCompressionLevel,
242 smartRepliesUsePersona:
243 smartRepliesUsePersona ?? this.smartRepliesUsePersona,
244 keepPersonaOnNewChat: keepPersonaOnNewChat ?? this.keepPersonaOnNewChat,
245 roleSwapButtonEnabled:
246 roleSwapButtonEnabled ?? this.roleSwapButtonEnabled,
247 showSystemMessagesInChat:
248 showSystemMessagesInChat ?? this.showSystemMessagesInChat,
249 calendarToolsEnabled: calendarToolsEnabled ?? this.calendarToolsEnabled,
250 locationToolsEnabled: locationToolsEnabled ?? this.locationToolsEnabled,
251 autoCollapseThinking: autoCollapseThinking ?? this.autoCollapseThinking,
252 sendTemperature: sendTemperature ?? this.sendTemperature,
253 sendTopP: sendTopP ?? this.sendTopP,
254 defaultSystemPrompt: defaultSystemPrompt ?? this.defaultSystemPrompt,
255 savedMcpIntegrations: savedMcpIntegrations ?? this.savedMcpIntegrations,
256 );
257 }
258
259 Map<String, dynamic> toMap() {
260 return {
261 'temperature': temperature,
262 'topP': topP,
263 'maxTokens': maxTokens,
264 'contextLength': contextLength,
265 'themeMode': themeMode.index,
266 'fontSize': fontSize,
267 'showSystemMessages': showSystemMessages,
268 'hapticFeedbackEnabled': hapticFeedbackEnabled,
269 'sendOnEnter': sendOnEnter,
270 'defaultServerId': defaultServerId,
271 'showDataIndicator': showDataIndicator,
272 'autoGenerateTitle': autoGenerateTitle,
273 'streamingEnabled': streamingEnabled,
274 'defaultPersonaId': defaultPersonaId,
275 'defaultModelId': defaultModelId,
276 'defaultModelServerId': defaultModelServerId,
277 'hasCompletedOnboarding': hasCompletedOnboarding,
278 'hasAskedForNotifications': hasAskedForNotifications,
279 'mcpEnabled': mcpEnabled,
280 'newChatMcpEnabled': newChatMcpEnabled,
281 'codeThemeDark': codeThemeDark.index,
282 'codeThemeLight': codeThemeLight.index,
283 'preferredBackend': preferredBackend.index,
284 'ttsEngine': ttsEngine.name,
285 'ttsVoiceId': ttsVoiceId,
286 'ttsSpeed': ttsSpeed,
287 'kittenTtsModelVariant': kittenTtsModelVariant.name,
288 'autoSpeakEnabled': autoSpeakEnabled,
289 'conciseVoiceResponsesEnabled': conciseVoiceResponsesEnabled,
290 'ttsProcessMarkdown': ttsProcessMarkdown,
291 'ttsSkipSeconds': ttsSkipSeconds,
292 'smartReplyEnabled': smartReplyEnabled,
293 'aiUserResponseEnabled': aiUserResponseEnabled,
294 'localeCode': localeCode,
295 'huggingFaceToken': huggingFaceToken,
296 'unloadModelsBeforeLoad': unloadModelsBeforeLoad,
297 'tempChatKeyboardIncognito': tempChatKeyboardIncognito,
298 'resumeLastChat': resumeLastChat,
299 'imageCompressionEnabled': imageCompressionEnabled,
300 'imageCompressionLevel': imageCompressionLevel.name,
301 'smartRepliesUsePersona': smartRepliesUsePersona,
302 'keepPersonaOnNewChat': keepPersonaOnNewChat,
303 'roleSwapButtonEnabled': roleSwapButtonEnabled,
304 'showSystemMessagesInChat': showSystemMessagesInChat,
305 'calendarToolsEnabled': calendarToolsEnabled,
306 'locationToolsEnabled': locationToolsEnabled,
307 'autoCollapseThinking': autoCollapseThinking,
308 'sendTemperature': sendTemperature,
309 'sendTopP': sendTopP,
310 'defaultSystemPrompt': defaultSystemPrompt,
311 'savedMcpIntegrations': savedMcpIntegrations
312 .map((i) => i.toJson())
313 .toList(),
314 };
315 }
316
317 factory AppSettings.fromMap(Map<String, dynamic> map) {
318 return AppSettings(
319 temperature: map['temperature']?.toDouble() ?? 0.7,
320 topP: map['topP']?.toDouble() ?? 0.9,
321 maxTokens: map['maxTokens']?.toInt() ?? 2048,
322 contextLength: map['contextLength']?.toInt() ?? 4096,
323 themeMode: ThemeMode.values[map['themeMode'] ?? 2],
324 fontSize: map['fontSize']?.toDouble() ?? 16.0,
325 showSystemMessages: map['showSystemMessages'] ?? false,
326 hapticFeedbackEnabled: map['hapticFeedbackEnabled'] ?? true,
327 sendOnEnter: map['sendOnEnter'] ?? false,
328 defaultServerId: map['defaultServerId'],
329 showDataIndicator: map['showDataIndicator'] ?? true,
330 autoGenerateTitle: map['autoGenerateTitle'] ?? true,
331 streamingEnabled: map['streamingEnabled'] ?? true,
332 defaultPersonaId: map['defaultPersonaId'],
333 defaultModelId: map['defaultModelId'] as String?,
334 defaultModelServerId: map['defaultModelServerId'] as String?,
335 hasCompletedOnboarding: map['hasCompletedOnboarding'] ?? false,
336 hasAskedForNotifications: map['hasAskedForNotifications'] ?? false,
337 mcpEnabled: map['mcpEnabled'] ?? false,
338 newChatMcpEnabled: map['newChatMcpEnabled'] ?? false,
339 codeThemeDark: SyntaxThemeName.values[map['codeThemeDark'] ?? 0],
340 codeThemeLight: SyntaxThemeName.values[map['codeThemeLight'] ?? 1],
341 preferredBackend: _parsePreferredBackend(map['preferredBackend']),
342 ttsEngine: _parseEngine(map['ttsEngine']),
343 ttsVoiceId: map['ttsVoiceId'],
344 ttsSpeed: map['ttsSpeed']?.toDouble() ?? 1.0,
345 kittenTtsModelVariant: _parseKittenVariant(map['kittenTtsModelVariant']),
346 autoSpeakEnabled: map['autoSpeakEnabled'] ?? false,
347 conciseVoiceResponsesEnabled: map['conciseVoiceResponsesEnabled'] ?? true,
348 ttsProcessMarkdown: map['ttsProcessMarkdown'] ?? true,
349 ttsSkipSeconds: _parseTtsSkipSeconds(map['ttsSkipSeconds']),
350 smartReplyEnabled: map['smartReplyEnabled'] ?? true,
351 aiUserResponseEnabled: map['aiUserResponseEnabled'] ?? false,
352 localeCode: map['localeCode'] as String?,
353 huggingFaceToken: map['huggingFaceToken'] as String?,
354 unloadModelsBeforeLoad: map['unloadModelsBeforeLoad'] ?? false,
355 tempChatKeyboardIncognito: map['tempChatKeyboardIncognito'] ?? true,
356 resumeLastChat: map['resumeLastChat'] ?? true,
357 imageCompressionEnabled: map['imageCompressionEnabled'] ?? false,
358 imageCompressionLevel: _parseImageCompressionLevel(
359 map['imageCompressionLevel'],
360 ),
361 smartRepliesUsePersona: map['smartRepliesUsePersona'] ?? false,
362 keepPersonaOnNewChat: map['keepPersonaOnNewChat'] ?? false,
363 roleSwapButtonEnabled: map['roleSwapButtonEnabled'] ?? false,
364 showSystemMessagesInChat: map['showSystemMessagesInChat'] ?? true,
365 calendarToolsEnabled: map['calendarToolsEnabled'] ?? false,
366 locationToolsEnabled: map['locationToolsEnabled'] ?? false,
367 autoCollapseThinking: map['autoCollapseThinking'] ?? false,
368 sendTemperature: map['sendTemperature'] ?? true,
369 sendTopP: map['sendTopP'] ?? true,
370 defaultSystemPrompt: map['defaultSystemPrompt'] as String? ?? '',
371 savedMcpIntegrations: _parseSavedMcpIntegrations(
372 map['savedMcpIntegrations'],
373 ),
374 );
375 }
376
377 static List<McpIntegration> _parseSavedMcpIntegrations(dynamic value) {
378 if (value is List) {
379 return value
380 .whereType<Map<String, dynamic>>()
381 .map((m) => McpIntegration.fromJson(m))
382 .toList();
383 }
384 return const [];
385 }
386
387 static ImageCompressionLevel _parseImageCompressionLevel(dynamic value) {
388 if (value is String) {
389 try {
390 return ImageCompressionLevel.values.byName(value);
391 } catch (_) {}
392 }
393 return ImageCompressionLevel.medium;
394 }
395
396 static PreferredBackend _parsePreferredBackend(dynamic value) {
397 if (value is int && value < PreferredBackend.values.length) {
398 return PreferredBackend.values[value];
399 }
400 // Handle migration from old LiteLmBackendType (had cpu=0, gpu=1, npu=2)
401 // Map npu (2) to gpu since flutter_gemma only has cpu/gpu
402 if (value is int && value == 2) return PreferredBackend.gpu;
403 return PreferredBackend.cpu;
404 }
405
406 static EngineId _parseEngine(dynamic value) {
407 if (value is String) {
408 return engineIdFromString(value);
409 }
410 if (value is int && value >= 0 && value < EngineId.values.length) {
411 return EngineId.values[value];
412 }
413 return EngineId.system;
414 }
415
416 static KittenTtsModelVariant _parseKittenVariant(dynamic value) {
417 if (value is String) {
418 try {
419 return KittenTtsModelVariant.values.byName(value);
420 } catch (_) {}
421 }
422 return KittenTtsModelVariant.nanoInt8;
423 }
424
425 static int _parseTtsSkipSeconds(dynamic value) {
426 const allowed = {5, 10, 15, 30};
427 if (value is int && allowed.contains(value)) return value;
428 return 10;
429 }
430
431 String toJson() => json.encode(toMap());
432
433 factory AppSettings.fromJson(String source) =>
434 AppSettings.fromMap(json.decode(source));
435 }