@tduncan / Cortana / commits / 131adb4

refactor: consolidate chat configuration and MCP settings into a new ChatSettingsSheet and provider structure

abdulmominsakib committed Apr 28, 2026 at 07:23 UTC 131adb4aa1c3d23987f06f40d06ce33e9ca3dc13
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 -}