From d2af55c5aa2cf3b9f234964e3a6810a608bc3256 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:08:44 +0530 Subject: [PATCH] feat: unified tools and search modal --- .gitignore | 1 + .../chat/widgets/modern_chat_input.dart | 215 ++------------- .../tools/widgets/unified_tools_modal.dart | 249 ++++++++++++++++++ 3 files changed, 267 insertions(+), 198 deletions(-) create mode 100644 lib/features/tools/widgets/unified_tools_modal.dart diff --git a/.gitignore b/.gitignore index f976826..b939929 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ .swiftpm/ migrate_working_dir/ AGENTS.md +flutter_*.png # IntelliJ related *.iml diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index a517b49..a80a00e 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1,6 +1,5 @@ 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'; @@ -8,7 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import 'dart:async'; import '../providers/chat_providers.dart'; -import '../../tools/widgets/tool_selector.dart'; +import '../../tools/widgets/unified_tools_modal.dart'; import '../../tools/providers/tools_providers.dart'; import '../../../shared/utils/platform_utils.dart'; @@ -49,7 +48,6 @@ class _ModernChatInputState extends ConsumerState late AnimationController _pulseController; Timer? _blurCollapseTimer; bool _hasAutoFocusedOnce = false; - bool _showToolSelector = false; @override void initState() { @@ -168,11 +166,7 @@ class _ModernChatInputState extends ConsumerState PlatformUtils.lightHaptic(); widget.onSendMessage(text); _controller.clear(); - setState(() { - _showToolSelector = false; - }); - // Clear selected tools after sending - ref.read(selectedToolIdsProvider.notifier).state = []; + // Keep tools and web search enabled for the conversation // Keep input expanded and focused for better UX - don't dismiss keyboard // KeyboardUtils.dismissKeyboard(context); // _setExpanded(false); @@ -212,9 +206,6 @@ class _ModernChatInputState extends ConsumerState child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Web search status indicator - _buildWebSearchStatusIndicator(), - // Main input area with unified 2-row design Container( clipBehavior: Clip.antiAlias, @@ -354,14 +345,6 @@ class _ModernChatInputState extends ConsumerState // Expanded bottom row with additional options if (_isExpanded) ...[ - // Tool selector - if (_showToolSelector) - const Padding( - padding: EdgeInsets.symmetric( - horizontal: Spacing.inputPadding, - ), - child: ToolSelector(), - ), Container( padding: const EdgeInsets.only( left: Spacing.inputPadding, @@ -385,19 +368,17 @@ class _ModernChatInputState extends ConsumerState icon: Icons.build, onTap: widget.enabled ? () { - setState(() { - _showToolSelector = !_showToolSelector; - }); + _showUnifiedToolsModal(); } : null, tooltip: 'Tools', - isActive: _showToolSelector || ref.watch(selectedToolIdsProvider).isNotEmpty, + isActive: + ref + .watch(selectedToolIdsProvider) + .isNotEmpty || + ref.watch(webSearchEnabledProvider), ), - const SizedBox(width: Spacing.sm), - Flexible( - child: Center(child: _buildResearchToggle()), - ), - const SizedBox(width: Spacing.md), + const Spacer(), // Microphone button: call provided callback for premium voice UI _buildRoundButton( icon: Platform.isIOS @@ -584,176 +565,6 @@ class _ModernChatInputState extends ConsumerState ); } - Widget _buildResearchToggle() { - final webSearchEnabled = ref.watch( - webSearchEnabledProvider.select((enabled) => enabled), - ); - - 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.info.withValues( - alpha: Alpha.buttonHover, - ) - : context.conduitTheme.surfaceBackground.withValues( - alpha: Alpha.subtle, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.xl), - border: Border.all( - color: webSearchEnabled - ? context.conduitTheme.info - : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.subtle, - ), - width: BorderWidth.regular, - ), - ), - 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 - ? Colors.white - : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.strong, - )) - : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.disabled, - ), - ), - ), - 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, - ), - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _buildWebSearchStatusIndicator() { - final webSearchEnabled = ref.watch( - webSearchEnabledProvider.select((enabled) => enabled), - ); - - if (!webSearchEnabled) return const SizedBox.shrink(); - - return AnimatedContainer( - duration: AnimationDuration.fast, - curve: Curves.easeInOut, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - margin: const EdgeInsets.only( - left: Spacing.md, - right: Spacing.md, - bottom: Spacing.xs, - ), - decoration: BoxDecoration( - color: context.conduitTheme.info.withValues( - alpha: Alpha.badgeBackground, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.badge), - border: Border.all( - color: context.conduitTheme.info.withValues(alpha: Alpha.subtle), - width: BorderWidth.regular, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Platform.isIOS ? CupertinoIcons.globe : Icons.travel_explore, - size: IconSize.small, - color: context.conduitTheme.info, - ), - const SizedBox(width: Spacing.xs), - Text( - '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, - ), - ), - ], - ), - ); - } - void _showAttachmentOptions() { showModalBottomSheet( context: context, @@ -822,6 +633,14 @@ class _ModernChatInputState extends ConsumerState ); } + void _showUnifiedToolsModal() { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (context) => const UnifiedToolsModal(), + ); + } + Widget _buildAttachmentOption({ required IconData icon, required String label, diff --git a/lib/features/tools/widgets/unified_tools_modal.dart b/lib/features/tools/widgets/unified_tools_modal.dart new file mode 100644 index 0000000..76a3d91 --- /dev/null +++ b/lib/features/tools/widgets/unified_tools_modal.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:io' show Platform; +import '../../../shared/theme/theme_extensions.dart'; +import '../../chat/providers/chat_providers.dart'; +import '../providers/tools_providers.dart'; + +class UnifiedToolsModal extends ConsumerStatefulWidget { + const UnifiedToolsModal({super.key}); + + @override + ConsumerState createState() => _UnifiedToolsModalState(); +} + +class _UnifiedToolsModalState extends ConsumerState { + @override + Widget build(BuildContext context) { + final webSearchEnabled = ref.watch(webSearchEnabledProvider); + final selectedToolIds = ref.watch(selectedToolIdsProvider); + final toolsAsync = ref.watch(toolsListProvider); + + return Container( + decoration: BoxDecoration( + color: context.conduitTheme.surfaceBackground, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppBorderRadius.bottomSheet), + ), + boxShadow: ConduitShadows.modal, + ), + padding: const EdgeInsets.all(Spacing.bottomSheetPadding), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Handle bar + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: context.conduitTheme.textPrimary.withValues( + alpha: Alpha.medium, + ), + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: Spacing.lg), + + // Title + Text( + 'Tools & Search', + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + ), + ), + const SizedBox(height: Spacing.lg), + + // Web Search Toggle + _buildWebSearchToggle(webSearchEnabled), + const SizedBox(height: Spacing.md), + + // Tools Section + Container( + width: double.infinity, + padding: const EdgeInsets.all(Spacing.md), + decoration: BoxDecoration( + color: context.conduitTheme.cardBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: context.conduitTheme.cardBorder, + width: BorderWidth.regular, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Available Tools', + style: AppTypography.labelStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.sm), + toolsAsync.when( + data: (tools) { + if (tools.isEmpty) { + return Text( + 'No tools available', + style: AppTypography.bodySmallStyle.copyWith( + color: context.conduitTheme.textSecondary, + ), + ); + } + + return Wrap( + spacing: Spacing.sm, + runSpacing: Spacing.sm, + children: tools.map((tool) { + final isSelected = selectedToolIds.contains(tool.id); + return FilterChip( + label: Text( + tool.name, + style: TextStyle( + color: isSelected + ? context.conduitTheme.buttonPrimaryText + : context.conduitTheme.textPrimary, + ), + ), + selected: isSelected, + onSelected: (selected) { + HapticFeedback.lightImpact(); + final currentIds = ref.read( + selectedToolIdsProvider, + ); + if (selected) { + ref.read(selectedToolIdsProvider.notifier).state = + [...currentIds, tool.id]; + } else { + ref + .read(selectedToolIdsProvider.notifier) + .state = currentIds + .where((id) => id != tool.id) + .toList(); + } + }, + avatar: Icon( + Icons.build, + size: IconSize.small, + color: isSelected + ? context.conduitTheme.buttonPrimaryText + : context.conduitTheme.textSecondary, + ), + backgroundColor: + context.conduitTheme.surfaceBackground, + selectedColor: context.conduitTheme.buttonPrimary, + showCheckmark: false, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppBorderRadius.md, + ), + side: BorderSide( + color: isSelected + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.cardBorder, + ), + ), + ); + }).toList(), + ); + }, + loading: () => const Center( + child: SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), + error: (error, stack) => Text( + 'Failed to load tools', + style: AppTypography.bodySmallStyle.copyWith( + color: context.conduitTheme.error, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildWebSearchToggle(bool webSearchEnabled) { + return GestureDetector( + onTap: () { + HapticFeedback.lightImpact(); + ref.read(webSearchEnabledProvider.notifier).state = !webSearchEnabled; + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(Spacing.md), + decoration: BoxDecoration( + color: webSearchEnabled + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.cardBackground, + borderRadius: BorderRadius.circular(AppBorderRadius.md), + border: Border.all( + color: webSearchEnabled + ? context.conduitTheme.buttonPrimary + : context.conduitTheme.cardBorder, + width: BorderWidth.regular, + ), + ), + child: Row( + children: [ + Icon( + webSearchEnabled + ? (Platform.isIOS ? CupertinoIcons.globe : Icons.public) + : (Platform.isIOS ? CupertinoIcons.search : Icons.search), + size: IconSize.medium, + color: webSearchEnabled + ? context.conduitTheme.buttonPrimaryText + : context.conduitTheme.textPrimary.withValues( + alpha: Alpha.strong, + ), + ), + const SizedBox(width: Spacing.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Web Search', + style: AppTypography.labelStyle.copyWith( + color: webSearchEnabled + ? context.conduitTheme.buttonPrimaryText + : context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + ), + ), + Text( + webSearchEnabled + ? 'I can search the internet for information' + : 'Enable to search the web for answers', + style: AppTypography.captionStyle.copyWith( + color: webSearchEnabled + ? context.conduitTheme.buttonPrimaryText.withValues( + alpha: Alpha.strong, + ) + : context.conduitTheme.textSecondary, + ), + ), + ], + ), + ), + Icon( + webSearchEnabled ? Icons.toggle_on : Icons.toggle_off, + size: IconSize.large, + color: webSearchEnabled + ? context.conduitTheme.buttonPrimaryText + : context.conduitTheme.textSecondary, + ), + ], + ), + ), + ); + } +}