diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index e2d15dc..566fce8 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -2470,6 +2470,19 @@ class ApiService { data['chat_id'] = conversationId; } + // Add web search flag if enabled + if (enableWebSearch) { + data['web_search'] = true; + // Also add it in features for compatibility + data['features'] = { + 'web_search': true, + 'image_generation': false, + 'code_interpreter': false, + 'memory': false, + }; + debugPrint('DEBUG: Web search enabled in SSE request'); + } + // Don't add session_id or id - they break SSE streaming! // The server falls back to task-based async when these are present diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 070bda3..b1dc75e 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -163,6 +163,18 @@ class ChatMessagesNotifier extends StateNotifier> { lastMessage.copyWith(content: content), ]; } + + void updateLastMessageWithFunction(ChatMessage Function(ChatMessage) updater) { + if (state.isEmpty) return; + + final lastMessage = state.last; + if (lastMessage.role != 'assistant') return; + + state = [ + ...state.sublist(0, state.length - 1), + updater(lastMessage), + ]; + } void appendToLastMessage(String content) { debugPrint('DEBUG: appendToLastMessage called with: "$content"'); @@ -778,6 +790,9 @@ Future _sendMessageInternal( // Check if web search is enabled for API final webSearchEnabled = ref.read(webSearchEnabledProvider); + + // Debug log to track web search state + debugPrint('DEBUG: Web search toggle state: $webSearchEnabled'); // No need for function calling tools since we're using retrieval directly final tools = >[]; @@ -979,10 +994,49 @@ Future _sendMessageInternal( }, ); + // Track web search status + bool isSearching = false; + final streamSubscription = persistentController.stream.listen( (chunk) { debugPrint('DEBUG: Received stream chunk: "$chunk"'); - ref.read(chatMessagesProvider.notifier).appendToLastMessage(chunk); + + // Check for web search indicators in the stream + if (webSearchEnabled && !isSearching) { + // Check if this is the start of web search + if (chunk.contains('[SEARCHING]') || + chunk.contains('Searching the web') || + chunk.contains('web search')) { + isSearching = true; + // Update the message to show search status + ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction( + (message) => message.copyWith( + content: '🔍 Searching the web...', + metadata: {'webSearchActive': true}, + ), + ); + return; // Don't append this chunk + } + } + + // Check if web search is complete + if (isSearching && (chunk.contains('[/SEARCHING]') || + chunk.contains('Search complete'))) { + isSearching = false; + // Clear the search status message + ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction( + (message) => message.copyWith( + content: '', + metadata: {'webSearchActive': false}, + ), + ); + return; // Don't append this chunk + } + + // Regular content - append to message + if (!chunk.contains('[SEARCHING]') && !chunk.contains('[/SEARCHING]')) { + ref.read(chatMessagesProvider.notifier).appendToLastMessage(chunk); + } }, onDone: () async { diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 4f34d7e..b83acb0 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; import '../../../shared/theme/theme_extensions.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -558,64 +559,76 @@ class _ModernChatInputState extends ConsumerState webSearchEnabledProvider.select((enabled) => enabled), ); - return GestureDetector( - onTap: widget.enabled - ? () { - ref.read(webSearchEnabledProvider.notifier).state = - !webSearchEnabled; - } - : null, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.sm, - ), - decoration: BoxDecoration( - color: webSearchEnabled - ? context.conduitTheme.textPrimary.withValues( - alpha: Alpha.buttonHover, - ) - : context.conduitTheme.surfaceBackground.withValues( - alpha: Alpha.subtle, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.xl), - border: Border.all( + return AnimatedContainer( + duration: AnimationDuration.fast, + curve: Curves.easeInOut, + child: GestureDetector( + onTap: widget.enabled + ? () { + // Toggle web search with haptic feedback + HapticFeedback.lightImpact(); + ref.read(webSearchEnabledProvider.notifier).state = + !webSearchEnabled; + + // Show a snackbar to confirm the toggle + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + webSearchEnabled + ? 'Web search disabled' + : 'Web search enabled - I can now search the internet', + style: TextStyle( + color: context.conduitTheme.textPrimary, + ), + ), + backgroundColor: context.conduitTheme.surfaceBackground, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + margin: const EdgeInsets.all(Spacing.md), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), + ), + ); + } + : null, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.sm, + ), + decoration: BoxDecoration( color: webSearchEnabled - ? context.conduitTheme.textPrimary.withValues( - alpha: Alpha.buttonHover + Alpha.subtle, + ? context.conduitTheme.info.withValues( + alpha: Alpha.buttonHover, ) - : context.conduitTheme.textPrimary.withValues( + : context.conduitTheme.surfaceBackground.withValues( alpha: Alpha.subtle, ), - width: BorderWidth.regular, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.travel_explore, - size: IconSize.small, - color: widget.enabled - ? (webSearchEnabled - ? context.conduitTheme.textPrimary - : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.strong, - )) + borderRadius: BorderRadius.circular(AppBorderRadius.xl), + border: Border.all( + color: webSearchEnabled + ? context.conduitTheme.info : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.disabled, + alpha: Alpha.subtle, ), + width: BorderWidth.regular, ), - const SizedBox(width: Spacing.sm), - Flexible( - child: Text( - 'Search', - style: TextStyle( - fontSize: AppTypography.bodySmall, - fontWeight: FontWeight.w500, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: AnimationDuration.fast, + child: Icon( + webSearchEnabled + ? (Platform.isIOS ? CupertinoIcons.globe : Icons.public) + : (Platform.isIOS ? CupertinoIcons.search : Icons.search), + key: ValueKey(webSearchEnabled), + size: IconSize.small, color: widget.enabled ? (webSearchEnabled - ? context.conduitTheme.textPrimary + ? Colors.white : context.conduitTheme.textPrimary.withValues( alpha: Alpha.strong, )) @@ -624,8 +637,27 @@ class _ModernChatInputState extends ConsumerState ), ), ), - ), - ], + const SizedBox(width: Spacing.sm), + Flexible( + child: Text( + webSearchEnabled ? 'Web' : 'Search', + style: TextStyle( + fontSize: AppTypography.bodySmall, + fontWeight: webSearchEnabled ? FontWeight.w600 : FontWeight.w500, + color: widget.enabled + ? (webSearchEnabled + ? Colors.white + : context.conduitTheme.textPrimary.withValues( + alpha: Alpha.strong, + )) + : context.conduitTheme.textPrimary.withValues( + alpha: Alpha.disabled, + ), + ), + ), + ), + ], + ), ), ), ); @@ -638,7 +670,9 @@ class _ModernChatInputState extends ConsumerState if (!webSearchEnabled) return const SizedBox.shrink(); - return Container( + return AnimatedContainer( + duration: AnimationDuration.fast, + curve: Curves.easeInOut, padding: const EdgeInsets.symmetric( horizontal: Spacing.md, vertical: Spacing.xs, @@ -662,15 +696,27 @@ class _ModernChatInputState extends ConsumerState mainAxisSize: MainAxisSize.min, children: [ Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.travel_explore, + Platform.isIOS ? CupertinoIcons.globe : Icons.travel_explore, size: IconSize.small, color: context.conduitTheme.info, ), const SizedBox(width: Spacing.xs), Text( - 'Web search on', + 'Web search enabled', style: AppTypography.captionStyle.copyWith( color: context.conduitTheme.info, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: Spacing.xs), + GestureDetector( + onTap: () { + ref.read(webSearchEnabledProvider.notifier).state = false; + }, + child: Icon( + Icons.close, + size: 12, + color: context.conduitTheme.info, ), ), ],