5 files changed
+609
-748
lib/features/chat/providers/chat_mcp_providers.dart
new
+62
@@ -0,0 +1,62 @@
1
+import 'package:flutter_riverpod/flutter_riverpod.dart';
2
+import '../../../core/providers/app_providers.dart';
3
+import '../data/models/mcp_integration.dart';
4
+
5
+export '../data/models/mcp_integration.dart';
6
+
7
+class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
8
+ @override
9
+ ChatMcpConfig build() {
10
+ final settings = ref.watch(settingsProvider);
11
+ return ChatMcpConfig(enabled: settings.mcpEnabled);
12
+ }
13
+
14
+ void setConfig(ChatMcpConfig config) {
15
+ state = config;
16
+ }
17
+
18
+ void addIntegration(McpIntegration integration) {
19
+ state = state.copyWith(
20
+ integrations: [...state.integrations, integration],
21
+ activeMcpServers:
22
+ integration.serverLabel != null && integration.serverUrl != null
23
+ ? {
24
+ ...state.activeMcpServers,
25
+ integration.serverLabel!: integration.serverUrl!,
26
+ }
27
+ : state.activeMcpServers,
28
+ );
29
+ }
30
+
31
+ void removeIntegration(int index) {
32
+ final integration = state.integrations[index];
33
+ final newIntegrations = List<McpIntegration>.from(state.integrations)
34
+ ..removeAt(index);
35
+ final newServers = Map<String, String>.from(state.activeMcpServers);
36
+ if (integration.serverLabel != null) {
37
+ newServers.remove(integration.serverLabel);
38
+ }
39
+ state = state.copyWith(
40
+ integrations: newIntegrations,
41
+ activeMcpServers: newServers,
42
+ );
43
+ }
44
+
45
+ void toggleAutoExecute() {
46
+ state = state.copyWith(autoExecuteTools: !state.autoExecuteTools);
47
+ }
48
+
49
+ void toggleEnabled() {
50
+ state = state.copyWith(enabled: !state.enabled);
51
+ }
52
+
53
+ void clearAll() {
54
+ final settings = ref.read(settingsProvider);
55
+ state = ChatMcpConfig(enabled: settings.mcpEnabled);
56
+ }
57
+}
58
+
59
+final chatMcpConfigProvider =
60
+ NotifierProvider<ChatMcpConfigNotifier, ChatMcpConfig>(() {
61
+ return ChatMcpConfigNotifier();
62
+ });
lib/features/chat/views/chat_screen.dart
+35
-25
@@ -14,8 +14,8 @@ import '../data/models/message.dart';
14
import '../providers/chat_providers.dart';
15
import 'components/chat_bubble.dart';
16
import 'components/chat_input_bar.dart';
17
-import 'components/chat_filter_sheet.dart';
18
-import 'components/mcp_config_sheet.dart';
17
+import '../providers/chat_mcp_providers.dart';
18
+import 'components/chat_settings_sheet.dart';
19
import 'components/notification_permission_banner.dart';
20
import '../../conversations/data/models/conversation.dart';
21
import 'package:localmind/features/conversations/providers/conversation_providers.dart'
@@ -126,32 +126,42 @@ class _ChatScreenState extends ConsumerState<ChatScreen> {
126
builder: (context, ref, child) {
127
final settings = ref.watch(settingsProvider);
128
final mcpConfig = ref.watch(chatMcpConfigProvider);
129
- final isEnabled = settings.mcpEnabled && mcpConfig.enabled;
130
- return IconButton(
131
- icon: Icon(
132
- isEnabled
133
- ? Icons.extension
134
- : Icons.extension_off_outlined,
135
- color: isEnabled
136
- ? theme.colorScheme.primary
137
- : (isDark
138
- ? const Color(0xFF666666)
139
- : const Color(0xFF999999)),
140
- ),
141
- tooltip: isEnabled ? 'MCP Enabled' : 'MCP Disabled',
142
- onPressed: () => showMcpConfigSheet(context),
129
+ final isMcpEnabled = settings.mcpEnabled && mcpConfig.enabled;
130
+
131
+ return Stack(
132
+ children: [
133
+ IconButton(
134
+ icon: HugeIcon(
135
+ icon: HugeIcons.strokeRoundedFilterHorizontal,
136
+ size: 24,
137
+ color: isDark ? Colors.white70 : Colors.black87,
138
+ ),
139
+ onPressed: () => showChatSettingsSheet(
140
+ context,
141
+ initialTab: 'parameters',
142
+ ),
143
+ tooltip: 'Chat Parameters',
144
+ ),
145
+ Positioned(
146
+ top: 4,
147
+ right: 0,
148
+ child: Container(
149
+ padding: const EdgeInsets.all(2),
150
+ decoration: BoxDecoration(
151
+ color: isMcpEnabled ? Colors.green : Colors.grey,
152
+ shape: BoxShape.circle,
153
+ ),
154
+ child: const HugeIcon(
155
+ icon: HugeIcons.strokeRoundedTools,
156
+ size: 10,
157
+ color: Colors.white,
158
+ ),
159
+ ),
160
+ ),
161
+ ],
162
);
163
},
164
),
146
- IconButton(
147
- icon: HugeIcon(
148
- icon: HugeIcons.strokeRoundedFilterHorizontal,
149
- size: 24,
150
- color: isDark ? Colors.white70 : Colors.black87,
151
- ),
152
- onPressed: () => showChatFilterSheet(context),
153
- tooltip: 'Chat Parameters',
154
- ),
165
PopupMenuButton<String>(
166
icon: const Icon(Icons.more_vert),
167
onSelected: (value) => _handleMenuAction(value, context),
lib/features/chat/views/components/chat_filter_sheet.dart
deleted
-385
@@ -1,385 +0,0 @@
1
-import 'package:flutter/material.dart';
2
-import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import 'package:hugeicons/hugeicons.dart';
4
-import '../../../../core/providers/app_providers.dart';
5
-import '../../../conversations/providers/conversation_providers.dart' as conv;
6
-
7
-class ChatFilterSheet extends ConsumerWidget {
8
- const ChatFilterSheet({super.key});
9
-
10
- @override
11
- Widget build(BuildContext context, WidgetRef ref) {
12
- final settings = ref.watch(settingsProvider);
13
- final activeConv = ref.watch(conv.activeConversationProvider);
14
- final theme = Theme.of(context);
15
- final isDark = theme.brightness == Brightness.dark;
16
-
17
- // Use per-conversation values if set, otherwise fall back to app defaults
18
- final temperature = activeConv?.temperature ?? settings.temperature;
19
- final topP = activeConv?.topP ?? settings.topP;
20
- final maxTokens = activeConv?.maxTokens ?? settings.maxTokens;
21
- final contextLength = activeConv?.contextLength ?? settings.contextLength;
22
-
23
- final hasOverrides = activeConv?.temperature != null ||
24
- activeConv?.topP != null ||
25
- activeConv?.maxTokens != null ||
26
- activeConv?.contextLength != null;
27
-
28
- return Container(
29
- padding: const EdgeInsets.all(16),
30
- decoration: BoxDecoration(
31
- color: isDark ? const Color(0xFF1F1F1F) : Colors.white,
32
- borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
33
- ),
34
- child: SafeArea(
35
- child: Column(
36
- mainAxisSize: MainAxisSize.min,
37
- crossAxisAlignment: CrossAxisAlignment.start,
38
- children: [
39
- Center(
40
- child: Container(
41
- width: 40,
42
- height: 4,
43
- decoration: BoxDecoration(
44
- color: isDark
45
- ? const Color(0xFF3A3A3A)
46
- : const Color(0xFFE5E5E5),
47
- borderRadius: BorderRadius.circular(2),
48
- ),
49
- ),
50
- ),
51
- const SizedBox(height: 16),
52
- Row(
53
- children: [
54
- HugeIcon(
55
- icon: HugeIcons.strokeRoundedFilterHorizontal,
56
- size: 24,
57
- color: theme.colorScheme.primary,
58
- ),
59
- const SizedBox(width: 8),
60
- Text(
61
- 'Chat Parameters',
62
- style: TextStyle(
63
- fontSize: 18,
64
- fontWeight: FontWeight.bold,
65
- color: isDark ? Colors.white : Colors.black,
66
- ),
67
- ),
68
- const Spacer(),
69
- if (hasOverrides)
70
- TextButton.icon(
71
- onPressed: () => _resetToDefaults(ref, activeConv?.id),
72
- icon: const Icon(Icons.restore, size: 16),
73
- label: const Text('Reset'),
74
- ),
75
- ],
76
- ),
77
- const SizedBox(height: 8),
78
- Text(
79
- 'These parameters control how the model responds. Tap Reset to use app defaults.',
80
- style: TextStyle(
81
- fontSize: 12,
82
- color: isDark
83
- ? const Color(0xFF666666)
84
- : const Color(0xFF999999),
85
- ),
86
- ),
87
- const SizedBox(height: 16),
88
- Flexible(
89
- child: SingleChildScrollView(
90
- child: Column(
91
- mainAxisSize: MainAxisSize.min,
92
- children: [
93
- _SliderParam(
94
- label: 'Temperature',
95
- value: temperature,
96
- min: 0.0,
97
- max: 2.0,
98
- divisions: 20,
99
- description:
100
- 'Controls randomness. Lower = focused, Higher = creative.',
101
- onChanged: (v) => _updateParam(
102
- ref,
103
- activeConv?.id,
104
- temperature: v,
105
- ),
106
- isDark: isDark,
107
- ),
108
- _SliderParam(
109
- label: 'Top P',
110
- value: topP,
111
- min: 0.0,
112
- max: 1.0,
113
- divisions: 10,
114
- description: 'Nucleus sampling threshold. Lower = more focused.',
115
- onChanged: (v) => _updateParam(
116
- ref,
117
- activeConv?.id,
118
- topP: v,
119
- ),
120
- isDark: isDark,
121
- ),
122
- _IntParam(
123
- label: 'Max Tokens',
124
- value: maxTokens,
125
- description: 'Maximum response length in tokens.',
126
- onChanged: (v) => _updateParam(
127
- ref,
128
- activeConv?.id,
129
- maxTokens: v,
130
- ),
131
- isDark: isDark,
132
- ),
133
- _IntParam(
134
- label: 'Context Length',
135
- value: contextLength,
136
- description: 'Conversation history window in tokens.',
137
- onChanged: (v) => _updateParam(
138
- ref,
139
- activeConv?.id,
140
- contextLength: v,
141
- ),
142
- isDark: isDark,
143
- ),
144
- ],
145
- ),
146
- ),
147
- ),
148
- const SizedBox(height: 16),
149
- ],
150
- ),
151
- ),
152
- );
153
- }
154
-
155
- void _updateParam(
156
- WidgetRef ref,
157
- String? conversationId, {
158
- double? temperature,
159
- double? topP,
160
- int? maxTokens,
161
- int? contextLength,
162
- }) {
163
- if (conversationId == null) {
164
- // No active conversation — update app-level defaults instead
165
- final notifier = ref.read(settingsProvider.notifier);
166
- if (temperature != null) notifier.setTemperature(temperature);
167
- if (topP != null) notifier.setTopP(topP);
168
- if (maxTokens != null) notifier.setMaxTokens(maxTokens);
169
- if (contextLength != null) notifier.setContextLength(contextLength);
170
- return;
171
- }
172
-
173
- ref.read(conv.conversationsProvider.notifier).updateChatParams(
174
- conversationId,
175
- temperature: temperature,
176
- topP: topP,
177
- maxTokens: maxTokens,
178
- contextLength: contextLength,
179
- );
180
- }
181
-
182
- void _resetToDefaults(WidgetRef ref, String? conversationId) {
183
- if (conversationId == null) return;
184
- ref.read(conv.conversationsProvider.notifier).updateChatParams(
185
- conversationId,
186
- clearTemperature: true,
187
- clearTopP: true,
188
- clearMaxTokens: true,
189
- clearContextLength: true,
190
- );
191
- }
192
-}
193
-
194
-class _SliderParam extends StatelessWidget {
195
- const _SliderParam({
196
- required this.label,
197
- required this.value,
198
- required this.min,
199
- required this.max,
200
- required this.divisions,
201
- required this.description,
202
- required this.onChanged,
203
- required this.isDark,
204
- });
205
-
206
- final String label;
207
- final double value;
208
- final double min;
209
- final double max;
210
- final int divisions;
211
- final String description;
212
- final ValueChanged<double> onChanged;
213
- final bool isDark;
214
-
215
- @override
216
- Widget build(BuildContext context) {
217
- return Padding(
218
- padding: const EdgeInsets.symmetric(vertical: 8),
219
- child: Column(
220
- crossAxisAlignment: CrossAxisAlignment.start,
221
- children: [
222
- Row(
223
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
224
- children: [
225
- Text(
226
- label,
227
- style: TextStyle(
228
- fontSize: 14,
229
- fontWeight: FontWeight.w500,
230
- color: isDark ? Colors.white : Colors.black,
231
- ),
232
- ),
233
- Text(
234
- value.toStringAsFixed(2),
235
- style: TextStyle(
236
- fontSize: 14,
237
- fontWeight: FontWeight.w600,
238
- color: isDark
239
- ? const Color(0xFF3B82F6)
240
- : const Color(0xFF2563EB),
241
- ),
242
- ),
243
- ],
244
- ),
245
- SliderTheme(
246
- data: SliderThemeData(
247
- trackHeight: 4,
248
- thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8),
249
- overlayShape: const RoundSliderOverlayShape(overlayRadius: 16),
250
- activeTrackColor: isDark
251
- ? const Color(0xFF3B82F6)
252
- : const Color(0xFF2563EB),
253
- thumbColor: isDark
254
- ? const Color(0xFF3B82F6)
255
- : const Color(0xFF2563EB),
256
- inactiveTrackColor: isDark
257
- ? const Color(0xFF3A3A3A)
258
- : const Color(0xFFE5E5E5),
259
- ),
260
- child: Slider(
261
- value: value.clamp(min, max),
262
- min: min,
263
- max: max,
264
- divisions: divisions,
265
- onChanged: onChanged,
266
- ),
267
- ),
268
- Text(
269
- description,
270
- style: TextStyle(
271
- fontSize: 12,
272
- color: isDark
273
- ? const Color(0xFF666666)
274
- : const Color(0xFF999999),
275
- ),
276
- ),
277
- ],
278
- ),
279
- );
280
- }
281
-}
282
-
283
-class _IntParam extends StatefulWidget {
284
- const _IntParam({
285
- required this.label,
286
- required this.value,
287
- required this.description,
288
- required this.onChanged,
289
- required this.isDark,
290
- });
291
-
292
- final String label;
293
- final int value;
294
- final String description;
295
- final ValueChanged<int> onChanged;
296
- final bool isDark;
297
-
298
- @override
299
- State<_IntParam> createState() => _IntParamState();
300
-}
301
-
302
-class _IntParamState extends State<_IntParam> {
303
- late TextEditingController _controller;
304
-
305
- @override
306
- void initState() {
307
- super.initState();
308
- _controller = TextEditingController(text: widget.value.toString());
309
- }
310
-
311
- @override
312
- void didUpdateWidget(_IntParam old) {
313
- super.didUpdateWidget(old);
314
- if (old.value != widget.value) {
315
- _controller.text = widget.value.toString();
316
- }
317
- }
318
-
319
- @override
320
- void dispose() {
321
- _controller.dispose();
322
- super.dispose();
323
- }
324
-
325
- @override
326
- Widget build(BuildContext context) {
327
- return Padding(
328
- padding: const EdgeInsets.symmetric(vertical: 8),
329
- child: Column(
330
- crossAxisAlignment: CrossAxisAlignment.start,
331
- children: [
332
- Text(
333
- widget.label,
334
- style: TextStyle(
335
- fontSize: 14,
336
- fontWeight: FontWeight.w500,
337
- color: widget.isDark ? Colors.white : Colors.black,
338
- ),
339
- ),
340
- const SizedBox(height: 8),
341
- SizedBox(
342
- width: 120,
343
- child: TextField(
344
- controller: _controller,
345
- keyboardType: TextInputType.number,
346
- decoration: InputDecoration(
347
- isDense: true,
348
- contentPadding: const EdgeInsets.symmetric(
349
- horizontal: 12,
350
- vertical: 10,
351
- ),
352
- border: OutlineInputBorder(
353
- borderRadius: BorderRadius.circular(8),
354
- ),
355
- ),
356
- onChanged: (text) {
357
- final val = int.tryParse(text);
358
- if (val != null && val > 0) widget.onChanged(val);
359
- },
360
- ),
361
- ),
362
- const SizedBox(height: 4),
363
- Text(
364
- widget.description,
365
- style: TextStyle(
366
- fontSize: 12,
367
- color: widget.isDark
368
- ? const Color(0xFF666666)
369
- : const Color(0xFF999999),
370
- ),
371
- ),
372
- ],
373
- ),
374
- );
375
- }
376
-}
377
-
378
-void showChatFilterSheet(BuildContext context) {
379
- showModalBottomSheet(
380
- context: context,
381
- isScrollControlled: true,
382
- backgroundColor: Colors.transparent,
383
- builder: (context) => const ChatFilterSheet(),
384
- );
385
-}
lib/features/chat/views/components/chat_settings_sheet.dart
new
+512
@@ -0,0 +1,512 @@
1
+import 'package:flutter/material.dart';
2
+import 'package:flutter_riverpod/flutter_riverpod.dart';
3
+import 'package:shadcn_ui/shadcn_ui.dart';
4
+import 'package:hugeicons/hugeicons.dart';
5
+import '../../../../core/providers/app_providers.dart';
6
+import '../../../conversations/providers/conversation_providers.dart' as conv;
7
+import '../../providers/chat_mcp_providers.dart';
8
+
9
+class ChatSettingsSheet extends ConsumerStatefulWidget {
10
+ const ChatSettingsSheet({super.key, this.initialTab = 'parameters'});
11
+
12
+ final String initialTab;
13
+
14
+ @override
15
+ ConsumerState<ChatSettingsSheet> createState() => _ChatSettingsSheetState();
16
+}
17
+
18
+class _ChatSettingsSheetState extends ConsumerState<ChatSettingsSheet> {
19
+ final _serverLabelController = TextEditingController();
20
+ final _serverUrlController = TextEditingController();
21
+
22
+ @override
23
+ void dispose() {
24
+ _serverLabelController.dispose();
25
+ _serverUrlController.dispose();
26
+ super.dispose();
27
+ }
28
+
29
+ @override
30
+ Widget build(BuildContext context) {
31
+ final settings = ref.watch(settingsProvider);
32
+ final activeConv = ref.watch(conv.activeConversationProvider);
33
+ final mcpConfig = ref.watch(chatMcpConfigProvider);
34
+ // final theme = ShadTheme.of(context);
35
+ final isDark = Theme.of(context).brightness == Brightness.dark;
36
+
37
+ // Parameters
38
+ final temperature = activeConv?.temperature ?? settings.temperature;
39
+ final topP = activeConv?.topP ?? settings.topP;
40
+ final maxTokens = activeConv?.maxTokens ?? settings.maxTokens;
41
+ final contextLength = activeConv?.contextLength ?? settings.contextLength;
42
+
43
+ final hasOverrides = activeConv?.temperature != null ||
44
+ activeConv?.topP != null ||
45
+ activeConv?.maxTokens != null ||
46
+ activeConv?.contextLength != null;
47
+
48
+ final isGloballyEnabled = settings.mcpEnabled;
49
+
50
+ return Container(
51
+ padding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
52
+ decoration: BoxDecoration(
53
+ color: isDark ? const Color(0xFF121212) : Colors.white,
54
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
55
+ boxShadow: [
56
+ BoxShadow(
57
+ color: Colors.black.withValues(alpha: 0.2),
58
+ blurRadius: 10,
59
+ offset: const Offset(0, -2),
60
+ ),
61
+ ],
62
+ ),
63
+ child: SafeArea(
64
+ child: Column(
65
+ mainAxisSize: MainAxisSize.min,
66
+ crossAxisAlignment: CrossAxisAlignment.start,
67
+ children: [
68
+ Center(
69
+ child: Container(
70
+ width: 32,
71
+ height: 4,
72
+ decoration: BoxDecoration(
73
+ color: isDark
74
+ ? const Color(0xFF2A2A2A)
75
+ : const Color(0xFFE5E5E5),
76
+ borderRadius: BorderRadius.circular(2),
77
+ ),
78
+ ),
79
+ ),
80
+ const SizedBox(height: 16),
81
+ Row(
82
+ children: [
83
+ Text(
84
+ 'Chat Settings',
85
+ style: TextStyle(
86
+ fontSize: 20,
87
+ fontWeight: FontWeight.bold,
88
+ color: isDark ? Colors.white : Colors.black,
89
+ ),
90
+ ),
91
+ const Spacer(),
92
+ if (hasOverrides)
93
+ ShadButton.ghost(
94
+ onPressed: () => _resetToDefaults(ref, activeConv?.id),
95
+ leading: const Icon(Icons.restore, size: 16),
96
+ child: const Text('Reset Defaults'),
97
+ ),
98
+ ],
99
+ ),
100
+ const SizedBox(height: 20),
101
+ ShadTabs<String>(
102
+ value: widget.initialTab,
103
+ tabs: [
104
+ ShadTab(
105
+ value: 'parameters',
106
+ content: _buildParametersTab(
107
+ context,
108
+ temperature,
109
+ topP,
110
+ maxTokens,
111
+ contextLength,
112
+ activeConv?.id,
113
+ isDark,
114
+ ),
115
+ child: const Row(
116
+ children: [
117
+ HugeIcon(icon: HugeIcons.strokeRoundedSlidersHorizontal, size: 16),
118
+ SizedBox(width: 8),
119
+ Text('Parameters'),
120
+ ],
121
+ ),
122
+ ),
123
+ ShadTab(
124
+ value: 'mcp',
125
+ content: _buildMcpTab(
126
+ context,
127
+ mcpConfig,
128
+ isGloballyEnabled,
129
+ isDark,
130
+ ),
131
+ child: const Row(
132
+ children: [
133
+ HugeIcon(icon: HugeIcons.strokeRoundedPuzzle, size: 16),
134
+ SizedBox(width: 8),
135
+ Text('MCP'),
136
+ ],
137
+ ),
138
+ ),
139
+ ],
140
+ ),
141
+ ],
142
+ ),
143
+ ),
144
+ );
145
+ }
146
+
147
+ Widget _buildParametersTab(
148
+ BuildContext context,
149
+ double temperature,
150
+ double topP,
151
+ int maxTokens,
152
+ int contextLength,
153
+ String? conversationId,
154
+ bool isDark,
155
+ ) {
156
+ return Padding(
157
+ padding: const EdgeInsets.symmetric(vertical: 16),
158
+ child: Column(
159
+ children: [
160
+ _ParamSlider(
161
+ label: 'Temperature',
162
+ value: temperature,
163
+ min: 0,
164
+ max: 2,
165
+ divisions: 20,
166
+ description: 'Controls randomness: Higher = Creative, Lower = Focused',
167
+ onChanged: (v) => _updateParam(ref, conversationId, temperature: v),
168
+ isDark: isDark,
169
+ ),
170
+ const SizedBox(height: 24),
171
+ _ParamSlider(
172
+ label: 'Top P',
173
+ value: topP,
174
+ min: 0,
175
+ max: 1,
176
+ divisions: 10,
177
+ description: 'Nucleus sampling threshold',
178
+ onChanged: (v) => _updateParam(ref, conversationId, topP: v),
179
+ isDark: isDark,
180
+ ),
181
+ const SizedBox(height: 24),
182
+ Row(
183
+ children: [
184
+ Expanded(
185
+ child: _ParamInput(
186
+ label: 'Max Tokens',
187
+ value: maxTokens,
188
+ description: 'Response limit',
189
+ onChanged: (v) => _updateParam(ref, conversationId, maxTokens: v),
190
+ isDark: isDark,
191
+ ),
192
+ ),
193
+ const SizedBox(width: 16),
194
+ Expanded(
195
+ child: _ParamInput(
196
+ label: 'Context Length',
197
+ value: contextLength,
198
+ description: 'History window',
199
+ onChanged: (v) => _updateParam(ref, conversationId, contextLength: v),
200
+ isDark: isDark,
201
+ ),
202
+ ),
203
+ ],
204
+ ),
205
+ ],
206
+ ),
207
+ );
208
+ }
209
+
210
+ Widget _buildMcpTab(
211
+ BuildContext context,
212
+ ChatMcpConfig mcpConfig,
213
+ bool isGloballyEnabled,
214
+ bool isDark,
215
+ ) {
216
+ final theme = ShadTheme.of(context);
217
+ return Padding(
218
+ padding: const EdgeInsets.symmetric(vertical: 16),
219
+ child: Column(
220
+ crossAxisAlignment: CrossAxisAlignment.start,
221
+ children: [
222
+ if (!isGloballyEnabled)
223
+ Container(
224
+ margin: const EdgeInsets.only(bottom: 16),
225
+ padding: const EdgeInsets.all(12),
226
+ decoration: BoxDecoration(
227
+ color: Colors.orange.withValues(alpha: 0.1),
228
+ borderRadius: BorderRadius.circular(12),
229
+ border: Border.all(color: Colors.orange.withValues(alpha: 0.2)),
230
+ ),
231
+ child: Row(
232
+ children: [
233
+ const Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 20),
234
+ const SizedBox(width: 12),
235
+ const Expanded(
236
+ child: Text(
237
+ 'MCP is disabled globally. Enable it in Settings to use these features.',
238
+ style: TextStyle(fontSize: 13, color: Colors.orange),
239
+ ),
240
+ ),
241
+ ],
242
+ ),
243
+ ),
244
+ ShadSwitch(
245
+ value: mcpConfig.enabled,
246
+ onChanged: isGloballyEnabled
247
+ ? (v) => ref.read(chatMcpConfigProvider.notifier).toggleEnabled()
248
+ : null,
249
+ label: const Text('Enable MCP for this chat'),
250
+ ),
251
+ const SizedBox(height: 16),
252
+ if (mcpConfig.enabled && isGloballyEnabled) ...[
253
+ ShadSwitch(
254
+ value: mcpConfig.autoExecuteTools,
255
+ onChanged: (v) =>
256
+ ref.read(chatMcpConfigProvider.notifier).toggleAutoExecute(),
257
+ label: const Text('Auto-execute tools'),
258
+ ),
259
+ const SizedBox(height: 24),
260
+ Text(
261
+ 'Add Ephemeral MCP Server',
262
+ style: theme.textTheme.list,
263
+ ),
264
+ const SizedBox(height: 12),
265
+ Row(
266
+ children: [
267
+ Expanded(
268
+ child: ShadInput(
269
+ controller: _serverLabelController,
270
+ placeholder: const Text('Label'),
271
+ ),
272
+ ),
273
+ const SizedBox(width: 8),
274
+ Expanded(
275
+ flex: 2,
276
+ child: ShadInput(
277
+ controller: _serverUrlController,
278
+ placeholder: const Text('URL (https://...)'),
279
+ keyboardType: TextInputType.url,
280
+ ),
281
+ ),
282
+ const SizedBox(width: 8),
283
+ ShadIconButton(
284
+ icon: const Icon(Icons.add),
285
+ onPressed: _addServer,
286
+ ),
287
+ ],
288
+ ),
289
+ const SizedBox(height: 24),
290
+ if (mcpConfig.integrations.isNotEmpty) ...[
291
+ Text(
292
+ 'Active Integrations',
293
+ style: theme.textTheme.list,
294
+ ),
295
+ const SizedBox(height: 12),
296
+ ConstrainedBox(
297
+ constraints: const BoxConstraints(maxHeight: 200),
298
+ child: ListView.builder(
299
+ shrinkWrap: true,
300
+ itemCount: mcpConfig.integrations.length,
301
+ itemBuilder: (context, index) {
302
+ final integration = mcpConfig.integrations[index];
303
+ return Container(
304
+ margin: const EdgeInsets.only(bottom: 8),
305
+ decoration: BoxDecoration(
306
+ color: isDark ? const Color(0xFF1E1E1E) : const Color(0xFFF9FAFB),
307
+ borderRadius: BorderRadius.circular(12),
308
+ border: Border.all(
309
+ color: isDark ? const Color(0xFF2A2A2A) : const Color(0xFFE5E7EB),
310
+ ),
311
+ ),
312
+ child: ListTile(
313
+ dense: true,
314
+ leading: const HugeIcon(icon: HugeIcons.strokeRoundedPuzzle, size: 18),
315
+ title: Text(
316
+ integration.serverLabel ?? integration.pluginId ?? '',
317
+ style: const TextStyle(fontWeight: FontWeight.w600),
318
+ ),
319
+ subtitle: Text(
320
+ integration.serverUrl ?? '',
321
+ style: const TextStyle(fontSize: 11),
322
+ ),
323
+ trailing: ShadIconButton.ghost(
324
+ icon: const Icon(Icons.delete_outline, size: 18, color: Colors.red),
325
+ onPressed: () => ref
326
+ .read(chatMcpConfigProvider.notifier)
327
+ .removeIntegration(index),
328
+ ),
329
+ ),
330
+ );
331
+ },
332
+ ),
333
+ ),
334
+ ],
335
+ ],
336
+ ],
337
+ ),
338
+ );
339
+ }
340
+
341
+ void _addServer() {
342
+ final label = _serverLabelController.text.trim();
343
+ final url = _serverUrlController.text.trim();
344
+ if (label.isEmpty || url.isEmpty) return;
345
+
346
+ final integration = McpIntegration(
347
+ type: McpIntegrationType.ephemeralMcp,
348
+ serverLabel: label,
349
+ serverUrl: url,
350
+ );
351
+
352
+ ref.read(chatMcpConfigProvider.notifier).addIntegration(integration);
353
+
354
+ _serverLabelController.clear();
355
+ _serverUrlController.clear();
356
+ FocusScope.of(context).unfocus();
357
+ }
358
+
359
+ void _updateParam(
360
+ WidgetRef ref,
361
+ String? conversationId, {
362
+ double? temperature,
363
+ double? topP,
364
+ int? maxTokens,
365
+ int? contextLength,
366
+ }) {
367
+ if (conversationId == null) {
368
+ final notifier = ref.read(settingsProvider.notifier);
369
+ if (temperature != null) notifier.setTemperature(temperature);
370
+ if (topP != null) notifier.setTopP(topP);
371
+ if (maxTokens != null) notifier.setMaxTokens(maxTokens);
372
+ if (contextLength != null) notifier.setContextLength(contextLength);
373
+ return;
374
+ }
375
+
376
+ ref.read(conv.conversationsProvider.notifier).updateChatParams(
377
+ conversationId,
378
+ temperature: temperature,
379
+ topP: topP,
380
+ maxTokens: maxTokens,
381
+ contextLength: contextLength,
382
+ );
383
+ }
384
+
385
+ void _resetToDefaults(WidgetRef ref, String? conversationId) {
386
+ if (conversationId == null) return;
387
+ ref.read(conv.conversationsProvider.notifier).updateChatParams(
388
+ conversationId,
389
+ clearTemperature: true,
390
+ clearTopP: true,
391
+ clearMaxTokens: true,
392
+ clearContextLength: true,
393
+ );
394
+ }
395
+}
396
+
397
+class _ParamSlider extends StatelessWidget {
398
+ const _ParamSlider({
399
+ required this.label,
400
+ required this.value,
401
+ required this.min,
402
+ required this.max,
403
+ required this.divisions,
404
+ required this.description,
405
+ required this.onChanged,
406
+ required this.isDark,
407
+ });
408
+
409
+ final String label;
410
+ final double value;
411
+ final double min;
412
+ final double max;
413
+ final int divisions;
414
+ final String description;
415
+ final ValueChanged<double> onChanged;
416
+ final bool isDark;
417
+
418
+ @override
419
+ Widget build(BuildContext context) {
420
+ return Column(
421
+ crossAxisAlignment: CrossAxisAlignment.start,
422
+ children: [
423
+ Row(
424
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
425
+ children: [
426
+ Text(
427
+ label,
428
+ style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
429
+ ),
430
+ Text(
431
+ value.toStringAsFixed(2),
432
+ style: TextStyle(
433
+ fontWeight: FontWeight.bold,
434
+ color: Theme.of(context).colorScheme.primary,
435
+ ),
436
+ ),
437
+ ],
438
+ ),
439
+ const SizedBox(height: 8),
440
+ ShadSlider(
441
+ initialValue: value,
442
+ min: min,
443
+ max: max,
444
+ onChanged: onChanged,
445
+ ),
446
+ const SizedBox(height: 4),
447
+ Text(
448
+ description,
449
+ style: TextStyle(
450
+ fontSize: 12,
451
+ color: isDark ? Colors.white54 : Colors.black54,
452
+ ),
453
+ ),
454
+ ],
455
+ );
456
+ }
457
+}
458
+
459
+class _ParamInput extends StatelessWidget {
460
+ const _ParamInput({
461
+ required this.label,
462
+ required this.value,
463
+ required this.description,
464
+ required this.onChanged,
465
+ required this.isDark,
466
+ });
467
+
468
+ final String label;
469
+ final int value;
470
+ final String description;
471
+ final ValueChanged<int> onChanged;
472
+ final bool isDark;
473
+
474
+ @override
475
+ Widget build(BuildContext context) {
476
+ return Column(
477
+ crossAxisAlignment: CrossAxisAlignment.start,
478
+ children: [
479
+ Text(
480
+ label,
481
+ style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
482
+ ),
483
+ const SizedBox(height: 8),
484
+ ShadInput(
485
+ initialValue: value.toString(),
486
+ keyboardType: TextInputType.number,
487
+ onChanged: (v) {
488
+ final val = int.tryParse(v);
489
+ if (val != null) onChanged(val);
490
+ },
491
+ ),
492
+ const SizedBox(height: 4),
493
+ Text(
494
+ description,
495
+ style: TextStyle(
496
+ fontSize: 11,
497
+ color: isDark ? Colors.white54 : Colors.black54,
498
+ ),
499
+ ),
500
+ ],
501
+ );
502
+ }
503
+}
504
+
505
+void showChatSettingsSheet(BuildContext context, {String initialTab = 'parameters'}) {
506
+ showModalBottomSheet(
507
+ context: context,
508
+ isScrollControlled: true,
509
+ backgroundColor: Colors.transparent,
510
+ builder: (context) => ChatSettingsSheet(initialTab: initialTab),
511
+ );
512
+}
lib/features/chat/views/components/mcp_config_sheet.dart
deleted
-338
@@ -1,338 +0,0 @@
1
-import 'package:flutter/material.dart';
2
-import 'package:flutter_riverpod/flutter_riverpod.dart';
3
-import '../../../../core/providers/app_providers.dart';
4
-import '../../data/models/mcp_integration.dart';
5
-
6
-class ChatMcpConfigNotifier extends Notifier<ChatMcpConfig> {
7
- @override
8
- ChatMcpConfig build() {
9
- final settings = ref.watch(settingsProvider);
10
- return ChatMcpConfig(enabled: settings.mcpEnabled);
11
- }
12
-
13
- void setConfig(ChatMcpConfig config) {
14
- state = config;
15
- }
16
-
17
- void addIntegration(McpIntegration integration) {
18
- state = state.copyWith(
19
- integrations: [...state.integrations, integration],
20
- activeMcpServers:
21
- integration.serverLabel != null && integration.serverUrl != null
22
- ? {
23
- ...state.activeMcpServers,
24
- integration.serverLabel!: integration.serverUrl!,
25
- }
26
- : state.activeMcpServers,
27
- );
28
- }
29
-
30
- void removeIntegration(int index) {
31
- final integration = state.integrations[index];
32
- final newIntegrations = List<McpIntegration>.from(state.integrations)
33
- ..removeAt(index);
34
- final newServers = Map<String, String>.from(state.activeMcpServers);
35
- if (integration.serverLabel != null) {
36
- newServers.remove(integration.serverLabel);
37
- }
38
- state = state.copyWith(
39
- integrations: newIntegrations,
40
- activeMcpServers: newServers,
41
- );
42
- }
43
-
44
- void toggleAutoExecute() {
45
- state = state.copyWith(autoExecuteTools: !state.autoExecuteTools);
46
- }
47
-
48
- void toggleEnabled() {
49
- state = state.copyWith(enabled: !state.enabled);
50
- }
51
-
52
- void clearAll() {
53
- final settings = ref.read(settingsProvider);
54
- state = ChatMcpConfig(enabled: settings.mcpEnabled);
55
- }
56
-}
57
-
58
-final chatMcpConfigProvider =
59
- NotifierProvider<ChatMcpConfigNotifier, ChatMcpConfig>(() {
60
- return ChatMcpConfigNotifier();
61
- });
62
-
63
-class McpConfigSheet extends ConsumerStatefulWidget {
64
- const McpConfigSheet({super.key});
65
-
66
- @override
67
- ConsumerState<McpConfigSheet> createState() => _McpConfigSheetState();
68
-}
69
-
70
-class _McpConfigSheetState extends ConsumerState<McpConfigSheet> {
71
- final _serverLabelController = TextEditingController();
72
- final _serverUrlController = TextEditingController();
73
-
74
- @override
75
- void dispose() {
76
- _serverLabelController.dispose();
77
- _serverUrlController.dispose();
78
- super.dispose();
79
- }
80
-
81
- @override
82
- Widget build(BuildContext context) {
83
- final config = ref.watch(chatMcpConfigProvider);
84
- final settings = ref.watch(settingsProvider);
85
- final theme = Theme.of(context);
86
- final isDark = theme.brightness == Brightness.dark;
87
- final isGloballyEnabled = settings.mcpEnabled;
88
-
89
- return Container(
90
- padding: const EdgeInsets.all(16),
91
- decoration: BoxDecoration(
92
- color: isDark ? const Color(0xFF1F1F1F) : Colors.white,
93
- borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
94
- ),
95
- child: Column(
96
- crossAxisAlignment: CrossAxisAlignment.start,
97
- mainAxisSize: MainAxisSize.min,
98
- children: [
99
- Center(
100
- child: Container(
101
- width: 40,
102
- height: 4,
103
- decoration: BoxDecoration(
104
- color: isDark
105
- ? const Color(0xFF3A3A3A)
106
- : const Color(0xFFE5E5E5),
107
- borderRadius: BorderRadius.circular(2),
108
- ),
109
- ),
110
- ),
111
- const SizedBox(height: 16),
112
- Row(
113
- children: [
114
- Icon(
115
- Icons.extension,
116
- color: isGloballyEnabled
117
- ? theme.colorScheme.primary
118
- : (isDark
119
- ? const Color(0xFF666666)
120
- : const Color(0xFF999999)),
121
- ),
122
- const SizedBox(width: 8),
123
- Text(
124
- 'MCP Servers',
125
- style: TextStyle(
126
- fontSize: 18,
127
- fontWeight: FontWeight.bold,
128
- color: isDark ? Colors.white : Colors.black,
129
- ),
130
- ),
131
- ],
132
- ),
133
- if (!isGloballyEnabled) ...[
134
- const SizedBox(height: 12),
135
- Container(
136
- padding: const EdgeInsets.all(12),
137
- decoration: BoxDecoration(
138
- color: isDark
139
- ? Colors.orange.withValues(alpha: 0.1)
140
- : Colors.orange.withValues(alpha: 0.1),
141
- borderRadius: BorderRadius.circular(8),
142
- border: Border.all(color: Colors.orange.withValues(alpha: 0.3)),
143
- ),
144
- child: Row(
145
- children: [
146
- const Icon(
147
- Icons.info_outline,
148
- color: Colors.orange,
149
- size: 20,
150
- ),
151
- const SizedBox(width: 8),
152
- Expanded(
153
- child: Text(
154
- 'MCP is disabled. Enable it in Settings to use MCP servers.',
155
- style: TextStyle(
156
- fontSize: 13,
157
- color: isDark ? Colors.orange[200] : Colors.orange[800],
158
- ),
159
- ),
160
- ),
161
- ],
162
- ),
163
- ),
164
- ],
165
- const SizedBox(height: 16),
166
- SwitchListTile(
167
- title: Text(
168
- 'Enable MCP for this chat',
169
- style: TextStyle(color: isDark ? Colors.white : Colors.black),
170
- ),
171
- subtitle: Text(
172
- isGloballyEnabled
173
- ? 'Toggle MCP functionality for this session'
174
- : 'MCP is disabled globally in Settings',
175
- style: TextStyle(
176
- fontSize: 12,
177
- color: isDark
178
- ? const Color(0xFF9CA3AF)
179
- : const Color(0xFF6B7280),
180
- ),
181
- ),
182
- value: config.enabled,
183
- onChanged: isGloballyEnabled
184
- ? (value) {
185
- ref.read(chatMcpConfigProvider.notifier).toggleEnabled();
186
- }
187
- : null,
188
- ),
189
- if (config.enabled && isGloballyEnabled) ...[
190
- const Divider(),
191
- SwitchListTile(
192
- title: Text(
193
- 'Auto-execute tools',
194
- style: TextStyle(color: isDark ? Colors.white : Colors.black),
195
- ),
196
- subtitle: Text(
197
- 'Automatically run MCP tools without asking',
198
- style: TextStyle(
199
- fontSize: 12,
200
- color: isDark
201
- ? const Color(0xFF9CA3AF)
202
- : const Color(0xFF6B7280),
203
- ),
204
- ),
205
- value: config.autoExecuteTools,
206
- onChanged: (value) {
207
- ref.read(chatMcpConfigProvider.notifier).toggleAutoExecute();
208
- },
209
- ),
210
- const Divider(),
211
- Text(
212
- 'Add Ephemeral MCP Server',
213
- style: TextStyle(
214
- fontSize: 14,
215
- fontWeight: FontWeight.w600,
216
- color: isDark ? Colors.white : Colors.black,
217
- ),
218
- ),
219
- const SizedBox(height: 8),
220
- TextField(
221
- controller: _serverLabelController,
222
- decoration: InputDecoration(
223
- labelText: 'Server Label',
224
- hintText: 'e.g., huggingface',
225
- filled: true,
226
- fillColor: isDark
227
- ? const Color(0xFF2A2A2A)
228
- : const Color(0xFFF5F5F5),
229
- ),
230
- style: TextStyle(color: isDark ? Colors.white : Colors.black),
231
- ),
232
- const SizedBox(height: 8),
233
- TextField(
234
- controller: _serverUrlController,
235
- decoration: InputDecoration(
236
- labelText: 'Server URL',
237
- hintText: 'e.g., https://huggingface.co/mcp',
238
- filled: true,
239
- fillColor: isDark
240
- ? const Color(0xFF2A2A2A)
241
- : const Color(0xFFF5F5F5),
242
- ),
243
- keyboardType: TextInputType.url,
244
- style: TextStyle(color: isDark ? Colors.white : Colors.black),
245
- ),
246
- const SizedBox(height: 8),
247
- SizedBox(
248
- width: double.infinity,
249
- child: ElevatedButton.icon(
250
- onPressed: _addServer,
251
- icon: const Icon(Icons.add),
252
- label: const Text('Add Server'),
253
- ),
254
- ),
255
- if (config.integrations.isNotEmpty) ...[
256
- const Divider(),
257
- Text(
258
- 'Active Integrations',
259
- style: TextStyle(
260
- fontSize: 14,
261
- fontWeight: FontWeight.w600,
262
- color: isDark ? Colors.white : Colors.black,
263
- ),
264
- ),
265
- const SizedBox(height: 8),
266
- ConstrainedBox(
267
- constraints: const BoxConstraints(maxHeight: 200),
268
- child: ListView.builder(
269
- shrinkWrap: true,
270
- itemCount: config.integrations.length,
271
- itemBuilder: (context, index) {
272
- final integration = config.integrations[index];
273
- return ListTile(
274
- leading: Icon(
275
- Icons.extension,
276
- color: theme.colorScheme.primary,
277
- ),
278
- title: Text(
279
- integration.serverLabel ?? integration.pluginId ?? '',
280
- style: TextStyle(
281
- color: isDark ? Colors.white : Colors.black,
282
- ),
283
- ),
284
- subtitle: Text(
285
- integration.serverUrl ?? integration.pluginId ?? '',
286
- style: TextStyle(
287
- fontSize: 12,
288
- color: isDark
289
- ? const Color(0xFF9CA3AF)
290
- : const Color(0xFF6B7280),
291
- ),
292
- ),
293
- trailing: IconButton(
294
- icon: const Icon(Icons.delete_outline),
295
- onPressed: () {
296
- ref
297
- .read(chatMcpConfigProvider.notifier)
298
- .removeIntegration(index);
299
- },
300
- ),
301
- );
302
- },
303
- ),
304
- ),
305
- ],
306
- ],
307
- const SizedBox(height: 16),
308
- ],
309
- ),
310
- );
311
- }
312
-
313
- void _addServer() {
314
- final label = _serverLabelController.text.trim();
315
- final url = _serverUrlController.text.trim();
316
- if (label.isEmpty || url.isEmpty) return;
317
-
318
- final integration = McpIntegration(
319
- type: McpIntegrationType.ephemeralMcp,
320
- serverLabel: label,
321
- serverUrl: url,
322
- );
323
-
324
- ref.read(chatMcpConfigProvider.notifier).addIntegration(integration);
325
-
326
- _serverLabelController.clear();
327
- _serverUrlController.clear();
328
- }
329
-}
330
-
331
-void showMcpConfigSheet(BuildContext context) {
332
- showModalBottomSheet(
333
- context: context,
334
- isScrollControlled: true,
335
- backgroundColor: Colors.transparent,
336
- builder: (context) => const McpConfigSheet(),
337
- );
338
-}