From 6cea654b882c8717e651f2ca8f72c01c9cdae69a Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:41:55 +0530 Subject: [PATCH] refactor: unused files --- lib/core/services/navigation_service.dart | 124 +-- .../services/conversation_search_service.dart | 397 ---------- .../chat/views/conversation_search_page.dart | 370 --------- .../chat/views/model_selector_page.dart | 490 ------------ .../widgets/conversation_search_widget.dart | 739 ------------------ lib/features/tools/widgets/tool_selector.dart | 61 -- 6 files changed, 20 insertions(+), 2161 deletions(-) delete mode 100644 lib/features/chat/services/conversation_search_service.dart delete mode 100644 lib/features/chat/views/conversation_search_page.dart delete mode 100644 lib/features/chat/views/model_selector_page.dart delete mode 100644 lib/features/chat/widgets/conversation_search_widget.dart delete mode 100644 lib/features/tools/widgets/tool_selector.dart diff --git a/lib/core/services/navigation_service.dart b/lib/core/services/navigation_service.dart index 0c587a2..c4d1806 100644 --- a/lib/core/services/navigation_service.dart +++ b/lib/core/services/navigation_service.dart @@ -1,19 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // ThemedDialogs handles theming; no direct use of extensions here -import '../../features/chat/views/chat_page.dart'; import '../../features/auth/views/connect_signin_page.dart'; - -import '../../features/profile/views/profile_page.dart'; +import '../../features/chat/views/chat_page.dart'; import '../../features/files/views/files_page.dart'; - -import '../../features/chat/views/conversation_search_page.dart'; +import '../../features/navigation/views/chats_list_page.dart'; +import '../../features/profile/views/profile_page.dart'; import '../../shared/widgets/themed_dialogs.dart'; -import '../../features/navigation/views/chats_list_page.dart'; - -/// Centralized navigation service to handle all routing logic -/// Prevents navigation stack issues and memory leaks +/// Service for handling navigation throughout the app class NavigationService { static final GlobalKey navigatorKey = GlobalKey(); @@ -21,87 +15,33 @@ class NavigationService { static NavigatorState? get navigator => navigatorKey.currentState; static BuildContext? get context => navigatorKey.currentContext; - // Navigation stack tracking for analytics and debugging static final List _navigationStack = []; + static String? _currentRoute; + + /// Get current route + static String? get currentRoute => _currentRoute; + + /// Get navigation stack static List get navigationStack => List.unmodifiable(_navigationStack); - // Prevent duplicate navigation - static String? _currentRoute; - static bool _isNavigating = false; - static DateTime? _lastNavigationTime; - - /// Navigate to a named route with optional arguments - static Future navigateTo( - String routeName, { - Object? arguments, - bool replace = false, - bool clearStack = false, - }) async { - // Only block if we're already navigating to the exact same route - // Allow navigation to different routes even if currently navigating - if (_isNavigating && _currentRoute == routeName) { - debugPrint('Navigation blocked: Already navigating to same route'); - return null; - } - - // Prevent rapid successive navigation attempts - final now = DateTime.now(); - if (_lastNavigationTime != null && - now.difference(_lastNavigationTime!).inMilliseconds < 300) { - debugPrint('Navigation blocked: Too rapid navigation attempts'); - return null; - } - - _isNavigating = true; - - try { - // Add haptic feedback for navigation - HapticFeedback.lightImpact(); - - // Track navigation - if (!replace && !clearStack) { - _navigationStack.add(routeName); - } + /// Navigate to a specific route + static Future navigateTo(String routeName) async { + if (_currentRoute != routeName) { + _navigationStack.add(routeName); _currentRoute = routeName; - - if (clearStack) { - _navigationStack.clear(); - _navigationStack.add(routeName); - return await navigator?.pushNamedAndRemoveUntil( - routeName, - (route) => false, - arguments: arguments, - ); - } else if (replace) { - if (_navigationStack.isNotEmpty) { - _navigationStack.removeLast(); - } - _navigationStack.add(routeName); - return await navigator?.pushReplacementNamed( - routeName, - arguments: arguments, - ); - } else { - return await navigator?.pushNamed(routeName, arguments: arguments); - } - } catch (e) { - debugPrint('Navigation error: $e'); - rethrow; - } finally { - _isNavigating = false; - _lastNavigationTime = DateTime.now(); } } /// Navigate back with optional result static void goBack([T? result]) { if (navigator?.canPop() == true) { - HapticFeedback.lightImpact(); if (_navigationStack.isNotEmpty) { _navigationStack.removeLast(); } - _currentRoute = _navigationStack.isEmpty ? null : _navigationStack.last; + _currentRoute = _navigationStack.isNotEmpty + ? _navigationStack.last + : null; navigator?.pop(result); } } @@ -132,24 +72,16 @@ class NavigationService { return result; } - // Removed tabbed main navigation - /// Navigate to chat - static Future navigateToChat({String? conversationId}) { - return navigateTo( - Routes.chat, - arguments: {'conversationId': conversationId}, - replace: true, - ); + static Future navigateToChat() { + return navigateTo(Routes.chat); } /// Navigate to login static Future navigateToLogin() { - return navigateTo(Routes.login, clearStack: true); + return navigateTo(Routes.login); } - - /// Navigate to profile static Future navigateToProfile() { return navigateTo(Routes.profile); @@ -160,11 +92,6 @@ class NavigationService { return navigateTo(Routes.serverConnection); } - /// Navigate to search - static Future navigateToSearch() { - return navigateTo(Routes.search); - } - /// Navigate to chats list static Future navigateToChatsList() { return navigateTo(Routes.chatsList); @@ -199,8 +126,6 @@ class NavigationService { page = const ConnectAndSignInPage(); break; - - case Routes.profile: page = const ProfilePage(); break; @@ -209,16 +134,10 @@ class NavigationService { page = const ConnectAndSignInPage(); break; - case Routes.search: - page = const ConversationSearchPage(); - break; - case Routes.files: page = const FilesPage(); break; - - case Routes.chatsList: page = const ChatsListPage(); break; @@ -239,11 +158,8 @@ class NavigationService { class Routes { static const String chat = '/chat'; static const String login = '/login'; - static const String profile = '/profile'; static const String serverConnection = '/server-connection'; - static const String search = '/search'; static const String files = '/files'; - static const String chatsList = '/chats-list'; } diff --git a/lib/features/chat/services/conversation_search_service.dart b/lib/features/chat/services/conversation_search_service.dart deleted file mode 100644 index 3535eca..0000000 --- a/lib/features/chat/services/conversation_search_service.dart +++ /dev/null @@ -1,397 +0,0 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../core/models/conversation.dart'; -import '../../../core/models/chat_message.dart'; - -/// Advanced conversation search service with multiple search strategies -class ConversationSearchService { - static const int maxResults = 50; - static const int contextLines = 2; // Lines before/after match for context - - /// Search through conversations with various criteria - Future searchConversations({ - required List conversations, - required String query, - ConversationSearchOptions options = const ConversationSearchOptions(), - }) async { - if (query.trim().isEmpty) { - return ConversationSearchResults.empty(); - } - - final normalizedQuery = query.toLowerCase().trim(); - final results = []; - - // Search through each conversation - for (final conversation in conversations) { - final matches = await _searchInConversation( - conversation: conversation, - query: normalizedQuery, - options: options, - ); - results.addAll(matches); - } - - // Sort results by relevance and date - results.sort((a, b) { - // First by relevance score (higher is better) - final relevanceCompare = b.relevanceScore.compareTo(a.relevanceScore); - if (relevanceCompare != 0) return relevanceCompare; - - // Then by date (newer first) - return b.timestamp.compareTo(a.timestamp); - }); - - // Limit results - final limitedResults = results.take(maxResults).toList(); - - return ConversationSearchResults( - query: query, - results: limitedResults, - totalMatches: results.length, - searchDuration: DateTime.now().difference(DateTime.now()), - ); - } - - /// Search within a single conversation - Future> _searchInConversation({ - required Conversation conversation, - required String query, - required ConversationSearchOptions options, - }) async { - final matches = []; - - // Search in conversation title - if (options.searchTitles && _containsQuery(conversation.title, query)) { - matches.add( - ConversationSearchMatch( - conversationId: conversation.id, - conversationTitle: conversation.title, - matchType: SearchMatchType.title, - snippet: conversation.title, - highlightedSnippet: _highlightQuery(conversation.title, query), - relevanceScore: _calculateTitleRelevance(conversation.title, query), - timestamp: conversation.updatedAt, - ), - ); - } - - // Search in messages - if (options.searchMessages) { - final messageMatches = await _searchInMessages( - conversation: conversation, - query: query, - options: options, - ); - matches.addAll(messageMatches); - } - - // Search in tags - if (options.searchTags) { - for (final tag in conversation.tags) { - if (_containsQuery(tag, query)) { - matches.add( - ConversationSearchMatch( - conversationId: conversation.id, - conversationTitle: conversation.title, - matchType: SearchMatchType.tag, - snippet: tag, - highlightedSnippet: _highlightQuery(tag, query), - relevanceScore: _calculateTagRelevance(tag, query), - timestamp: conversation.updatedAt, - additionalInfo: {'tag': tag}, - ), - ); - } - } - } - - return matches; - } - - /// Search within messages of a conversation - Future> _searchInMessages({ - required Conversation conversation, - required String query, - required ConversationSearchOptions options, - }) async { - final matches = []; - - for (int i = 0; i < conversation.messages.length; i++) { - final message = conversation.messages[i]; - - // Skip system messages if not enabled - if (!options.includeSystemMessages && message.role == 'system') { - continue; - } - - // Filter by role if specified - if (options.roleFilter != null && message.role != options.roleFilter) { - continue; - } - - // Check if message contains query - if (_containsQuery(message.content, query)) { - final snippet = _extractSnippet(message.content, query); - final contextMessages = _getContextMessages(conversation.messages, i); - - matches.add( - ConversationSearchMatch( - conversationId: conversation.id, - conversationTitle: conversation.title, - messageId: message.id, - matchType: SearchMatchType.message, - snippet: snippet, - highlightedSnippet: _highlightQuery(snippet, query), - relevanceScore: _calculateMessageRelevance(message.content, query), - timestamp: message.timestamp, - messageRole: message.role, - messageIndex: i, - contextMessages: contextMessages, - ), - ); - } - } - - return matches; - } - - /// Extract relevant snippet around the query match - String _extractSnippet(String content, String query) { - const maxSnippetLength = 200; - final queryIndex = content.toLowerCase().indexOf(query); - - if (queryIndex == -1) { - return content.substring(0, maxSnippetLength.clamp(0, content.length)); - } - - // Calculate snippet bounds - final start = (queryIndex - 50).clamp(0, content.length); - final end = (queryIndex + query.length + 50).clamp(0, content.length); - - String snippet = content.substring(start, end); - - // Add ellipsis if needed - if (start > 0) snippet = '...$snippet'; - if (end < content.length) snippet = '$snippet...'; - - return snippet; - } - - /// Get context messages around a matched message - List _getContextMessages(List messages, int index) { - final start = (index - contextLines).clamp(0, messages.length); - final end = (index + contextLines + 1).clamp(0, messages.length); - return messages.sublist(start, end); - } - - /// Highlight query matches in text - String _highlightQuery(String text, String query) { - if (query.isEmpty) return text; - - final regex = RegExp(RegExp.escape(query), caseSensitive: false); - return text.replaceAllMapped(regex, (match) { - return '${match.group(0)}'; - }); - } - - /// Check if text contains the query - bool _containsQuery(String text, String query) { - return text.toLowerCase().contains(query); - } - - /// Calculate relevance score for title matches - double _calculateTitleRelevance(String title, String query) { - final titleLower = title.toLowerCase(); - final queryLower = query.toLowerCase(); - - // Exact match gets highest score - if (titleLower == queryLower) return 100.0; - - // Title starts with query gets high score - if (titleLower.startsWith(queryLower)) return 90.0; - - // Title contains query as whole word gets medium score - if (RegExp( - r'\b' + RegExp.escape(queryLower) + r'\b', - ).hasMatch(titleLower)) { - return 70.0; - } - - // Partial match gets lower score - return 50.0; - } - - /// Calculate relevance score for message matches - double _calculateMessageRelevance(String content, String query) { - final contentLower = content.toLowerCase(); - final queryLower = query.toLowerCase(); - - // Count occurrences - final occurrences = queryLower.allMatches(contentLower).length; - - // Base score for containing the query - double score = 30.0; - - // Bonus for multiple occurrences - score += (occurrences - 1) * 10.0; - - // Bonus for whole word matches - if (RegExp( - r'\b' + RegExp.escape(queryLower) + r'\b', - ).hasMatch(contentLower)) { - score += 20.0; - } - - // Penalty for very long messages (relevance dilution) - if (content.length > 1000) { - score *= 0.8; - } - - return score.clamp(0.0, 100.0); - } - - /// Calculate relevance score for tag matches - double _calculateTagRelevance(String tag, String query) { - final tagLower = tag.toLowerCase(); - final queryLower = query.toLowerCase(); - - // Exact match gets highest score - if (tagLower == queryLower) return 80.0; - - // Tag starts with query gets high score - if (tagLower.startsWith(queryLower)) return 70.0; - - // Partial match gets medium score - return 50.0; - } -} - -/// Search options for conversation search -@immutable -class ConversationSearchOptions { - final bool searchTitles; - final bool searchMessages; - final bool searchTags; - final bool includeSystemMessages; - final String? roleFilter; // 'user', 'assistant', 'system' - final DateTime? dateFrom; - final DateTime? dateTo; - final bool caseSensitive; - - const ConversationSearchOptions({ - this.searchTitles = true, - this.searchMessages = true, - this.searchTags = true, - this.includeSystemMessages = false, - this.roleFilter, - this.dateFrom, - this.dateTo, - this.caseSensitive = false, - }); - - ConversationSearchOptions copyWith({ - bool? searchTitles, - bool? searchMessages, - bool? searchTags, - bool? includeSystemMessages, - String? roleFilter, - DateTime? dateFrom, - DateTime? dateTo, - bool? caseSensitive, - }) { - return ConversationSearchOptions( - searchTitles: searchTitles ?? this.searchTitles, - searchMessages: searchMessages ?? this.searchMessages, - searchTags: searchTags ?? this.searchTags, - includeSystemMessages: - includeSystemMessages ?? this.includeSystemMessages, - roleFilter: roleFilter ?? this.roleFilter, - dateFrom: dateFrom ?? this.dateFrom, - dateTo: dateTo ?? this.dateTo, - caseSensitive: caseSensitive ?? this.caseSensitive, - ); - } -} - -/// Search results container -@immutable -class ConversationSearchResults { - final String query; - final List results; - final int totalMatches; - final Duration searchDuration; - - const ConversationSearchResults({ - required this.query, - required this.results, - required this.totalMatches, - required this.searchDuration, - }); - - factory ConversationSearchResults.empty() { - return ConversationSearchResults( - query: '', - results: const [], - totalMatches: 0, - searchDuration: Duration.zero, - ); - } - - bool get isEmpty => results.isEmpty; - bool get isNotEmpty => results.isNotEmpty; - int get length => results.length; -} - -/// Individual search match -@immutable -class ConversationSearchMatch { - final String conversationId; - final String conversationTitle; - final String? messageId; - final SearchMatchType matchType; - final String snippet; - final String highlightedSnippet; - final double relevanceScore; - final DateTime timestamp; - final String? messageRole; - final int? messageIndex; - final List? contextMessages; - final Map? additionalInfo; - - const ConversationSearchMatch({ - required this.conversationId, - required this.conversationTitle, - this.messageId, - required this.matchType, - required this.snippet, - required this.highlightedSnippet, - required this.relevanceScore, - required this.timestamp, - this.messageRole, - this.messageIndex, - this.contextMessages, - this.additionalInfo, - }); -} - -/// Types of search matches -enum SearchMatchType { title, message, tag } - -/// Provider for conversation search service -final conversationSearchServiceProvider = Provider(( - ref, -) { - return ConversationSearchService(); -}); - -/// Provider for search results -final conversationSearchResultsProvider = - StateProvider((ref) { - return null; - }); - -/// Provider for search options -final searchOptionsProvider = StateProvider((ref) { - return const ConversationSearchOptions(); -}); diff --git a/lib/features/chat/views/conversation_search_page.dart b/lib/features/chat/views/conversation_search_page.dart deleted file mode 100644 index d7cc921..0000000 --- a/lib/features/chat/views/conversation_search_page.dart +++ /dev/null @@ -1,370 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io' show Platform; -import '../../../shared/utils/platform_utils.dart'; -import '../widgets/conversation_search_widget.dart'; -import '../../../core/providers/app_providers.dart'; -import '../providers/chat_providers.dart'; -import 'chat_page.dart'; - -/// Dedicated page for conversation search functionality -class ConversationSearchPage extends ConsumerStatefulWidget { - const ConversationSearchPage({super.key}); - - @override - ConsumerState createState() => - _ConversationSearchPageState(); -} - -class _ConversationSearchPageState - extends ConsumerState { - @override - Widget build(BuildContext context) { - final conduitTheme = context.conduitTheme; - - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: _buildAppBar(context, conduitTheme), - body: ConversationSearchWidget( - onResultTap: _onSearchResultTap, - showFilters: true, - ), - ); - } - - PreferredSizeWidget _buildAppBar( - BuildContext context, - ConduitThemeExtension theme, - ) { - if (Platform.isIOS) { - return CupertinoNavigationBar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - border: Border(bottom: BorderSide(color: theme.cardBorder, width: 0.5)), - leading: CupertinoNavigationBarBackButton( - color: context.conduitTheme.textPrimary, - onPressed: () => Navigator.of(context).pop(), - ), - middle: Text( - 'Search Conversations', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.bodyLarge, - fontWeight: FontWeight.w600, - ), - ), - ); - } - - return AppBar( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - elevation: Elevation.none, - title: Text( - 'Search Conversations', - style: TextStyle( - color: context.conduitTheme.textPrimary, - fontSize: AppTypography.headlineMedium, - fontWeight: FontWeight.w600, - ), - ), - leading: IconButton( - icon: Icon(Icons.arrow_back, color: context.conduitTheme.textPrimary), - onPressed: () => Navigator.of(context).pop(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1), - child: Container(height: 1, color: theme.cardBorder), - ), - ); - } - - void _onSearchResultTap(String conversationId, String? messageId) { - PlatformUtils.lightHaptic(); - - // Set the active conversation - final conversationsAsync = ref.read(conversationsProvider); - conversationsAsync.whenData((conversations) { - final conversation = conversations.firstWhere( - (c) => c.id == conversationId, - orElse: () => throw Exception('Conversation not found'), - ); - - // Set active conversation - ref.read(activeConversationProvider.notifier).state = conversation; - - // Navigate back to chat - Navigator.of(context).pop(); - - // If we have a specific message, navigate to it and highlight it - if (messageId != null) { - // Use a custom navigation approach with message highlighting - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => - ChatPageWithHighlight(messageIdToHighlight: messageId), - ), - ); - } - }); - } -} - -/// Chat page wrapper that highlights a specific message -class ChatPageWithHighlight extends ConsumerStatefulWidget { - final String messageIdToHighlight; - - const ChatPageWithHighlight({super.key, required this.messageIdToHighlight}); - - @override - ConsumerState createState() => - _ChatPageWithHighlightState(); -} - -class _ChatPageWithHighlightState extends ConsumerState { - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - - // Schedule highlighting after the widget is built - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToAndHighlightMessage(); - }); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - void _scrollToAndHighlightMessage() async { - try { - final messages = ref.read(chatMessagesProvider); - final messageIndex = messages.indexWhere( - (msg) => msg.id == widget.messageIdToHighlight, - ); - - if (messageIndex >= 0 && _scrollController.hasClients) { - // Calculate the approximate position (assuming 100px per message) - final targetOffset = messageIndex * 100.0; - - // Scroll to the message - await _scrollController.animateTo( - targetOffset, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - - // Show a highlight indicator - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Found message'), - duration: const Duration(seconds: 2), - backgroundColor: context.conduitTheme.buttonPrimary, - ), - ); - } - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Message not found'), - backgroundColor: context.conduitTheme.error, - ), - ); - } - } - } - - @override - Widget build(BuildContext context) { - return const ChatPage(); - } -} - -/// Search icon button for app bars -class ConversationSearchButton extends ConsumerWidget { - final VoidCallback? onPressed; - - const ConversationSearchButton({super.key, this.onPressed}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return IconButton( - icon: Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search, - color: context.conduitTheme.iconPrimary.withValues(alpha: 0.8), - size: IconSize.lg, - ), - onPressed: - onPressed ?? - () { - PlatformUtils.lightHaptic(); - Navigator.of(context).push( - Platform.isIOS - ? CupertinoPageRoute( - builder: (context) => const ConversationSearchPage(), - ) - : MaterialPageRoute( - builder: (context) => const ConversationSearchPage(), - ), - ); - }, - tooltip: 'Search conversations', - ); - } -} - -/// Quick search overlay that can be shown from any page -class QuickSearchOverlay extends ConsumerStatefulWidget { - final VoidCallback? onDismiss; - - const QuickSearchOverlay({super.key, this.onDismiss}); - - @override - ConsumerState createState() => _QuickSearchOverlayState(); -} - -class _QuickSearchOverlayState extends ConsumerState - with SingleTickerProviderStateMixin { - late AnimationController _animationController; - late Animation _fadeAnimation; - late Animation _slideAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - - _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeOut), - ); - - _slideAnimation = - Tween(begin: const Offset(0, -1), end: Offset.zero).animate( - CurvedAnimation(parent: _animationController, curve: Curves.easeOut), - ); - - _animationController.forward(); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - - Future _dismiss() async { - await _animationController.reverse(); - widget.onDismiss?.call(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _animationController, - builder: (context, child) { - return Stack( - children: [ - // Backdrop - GestureDetector( - onTap: _dismiss, - child: Container( - color: context.conduitTheme.surfaceBackground.withValues( - alpha: 0.7 * _fadeAnimation.value, - ), - ), - ), - - // Search panel - SlideTransition( - position: _slideAnimation, - child: FadeTransition( - opacity: _fadeAnimation, - child: Container( - height: MediaQuery.of(context).size.height * 0.8, - margin: const EdgeInsets.only(top: Spacing.xxxl + Spacing.md), - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - borderRadius: const BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - ), - child: Column( - children: [ - // Handle bar - Container( - margin: const EdgeInsets.only(top: Spacing.sm), - width: 40, - height: 4, - decoration: BoxDecoration( - color: context.conduitTheme.textPrimary.withValues( - alpha: 0.3, - ), - borderRadius: BorderRadius.circular( - AppBorderRadius.xs, - ), - ), - ), - - // Search content - Expanded( - child: ConversationSearchWidget( - onResultTap: (conversationId, messageId) { - _onSearchResultTap(conversationId, messageId); - _dismiss(); - }, - showFilters: false, // Simplified for overlay - ), - ), - ], - ), - ), - ), - ), - ], - ); - }, - ); - } - - void _onSearchResultTap(String conversationId, String? messageId) { - // Same logic as the search page - final conversationsAsync = ref.read(conversationsProvider); - conversationsAsync.whenData((conversations) { - final conversation = conversations.firstWhere( - (c) => c.id == conversationId, - orElse: () => throw Exception('Conversation not found'), - ); - - ref.read(activeConversationProvider.notifier).state = conversation; - - if (messageId != null) { - debugPrint( - 'Navigate to message: $messageId in conversation: $conversationId', - ); - } - }); - } -} - -/// Show quick search overlay -void showQuickSearch(BuildContext context) { - showGeneralDialog( - context: context, - barrierColor: Colors.transparent, - barrierDismissible: true, - transitionDuration: const Duration(milliseconds: 300), - pageBuilder: (context, animation, secondaryAnimation) { - return QuickSearchOverlay(onDismiss: () => Navigator.of(context).pop()); - }, - ); -} diff --git a/lib/features/chat/views/model_selector_page.dart b/lib/features/chat/views/model_selector_page.dart deleted file mode 100644 index 22fd517..0000000 --- a/lib/features/chat/views/model_selector_page.dart +++ /dev/null @@ -1,490 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:io' show Platform; -import '../../../core/models/model.dart'; -import '../../../core/providers/app_providers.dart'; -import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/theme/app_theme.dart'; -import '../../../shared/widgets/conduit_components.dart'; - -class ModelSelectorPage extends ConsumerStatefulWidget { - const ModelSelectorPage({super.key}); - - @override - ConsumerState createState() => _ModelSelectorPageState(); -} - -class _ModelSelectorPageState extends ConsumerState { - final TextEditingController _searchController = TextEditingController(); - final FocusNode _searchFocusNode = FocusNode(); - String _searchQuery = ''; - - @override - void initState() { - super.initState(); - _searchController.addListener(_onSearchChanged); - } - - @override - void dispose() { - _searchController.removeListener(_onSearchChanged); - _searchController.dispose(); - _searchFocusNode.dispose(); - super.dispose(); - } - - void _onSearchChanged() { - setState(() { - _searchQuery = _searchController.text; - }); - } - - List _filterModels(List models) { - if (_searchQuery.isEmpty) { - return models; - } - - final query = _searchQuery.toLowerCase(); - return models.where((model) { - return model.name.toLowerCase().contains(query) || - (model.description?.toLowerCase().contains(query) ?? false); - }).toList(); - } - - @override - Widget build(BuildContext context) { - final modelsAsync = ref.watch(modelsProvider); - final selectedModel = ref.watch(selectedModelProvider); - - return Scaffold( - appBar: AppBar( - backgroundColor: context.conduitTheme.surfaceBackground, - elevation: Elevation.none, - scrolledUnderElevation: Elevation.none, - leading: ConduitIconButton( - icon: Platform.isIOS - ? CupertinoIcons.back - : Icons.arrow_back_rounded, - onPressed: () => Navigator.pop(context), - ), - title: Text( - 'Select Model', - style: AppTypography.headlineMediumStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - ), - body: Column( - children: [ - // Search bar - Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: context.conduitTheme.surfaceBackground, - border: Border( - bottom: BorderSide( - color: context.conduitTheme.dividerColor.withValues(alpha: 0.1), - width: BorderWidth.regular, - ), - ), - ), - child: _buildSearchField(), - ), - // Models list - Expanded( - child: modelsAsync.when( - data: (models) { - final filteredModels = _filterModels(models); - - if (models.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.cube_box - : Icons.view_in_ar, - size: IconSize.xxl, - color: context.conduitTheme.iconSecondary, - ), - const SizedBox(height: Spacing.lg), - Text( - 'No models available', - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Please check your Open-WebUI configuration', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - ], - ), - ); - } - - if (filteredModels.isEmpty && _searchQuery.isNotEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.search - : Icons.search_rounded, - size: IconSize.xxl, - color: context.conduitTheme.iconSecondary, - ), - const SizedBox(height: Spacing.lg), - Text( - 'No models found', - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Try searching with different keywords', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - ), - ], - ), - ); - } - - // Group models by category if needed - final groupedModels = _groupModels(filteredModels); - - return ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: groupedModels.length, - itemBuilder: (context, index) { - final group = groupedModels[index]; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (group.title != null) ...[ - Padding( - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.md, - Spacing.md, - Spacing.sm, - ), - child: Text( - group.title!, - style: AppTypography.labelStyle.copyWith( - color: context.conduitTheme.textSecondary, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - ), - ), - ), - ], - ...group.models.map( - (model) => ModelTile( - model: model, - isSelected: selectedModel?.id == model.id, - onTap: () { - ref.read(selectedModelProvider.notifier).state = - model; - ref.read(isManualModelSelectionProvider.notifier).state = true; - Navigator.pop(context); - }, - ), - ), - ], - ); - }, - ); - }, - loading: () => Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation( - context.conduitTheme.buttonPrimary, - ), - ), - ), - error: (error, _) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Platform.isIOS - ? CupertinoIcons.exclamationmark_triangle - : Icons.error_rounded, - size: IconSize.xxl, - color: context.conduitTheme.error, - ), - const SizedBox(height: Spacing.lg), - Text( - 'Failed to load models', - style: AppTypography.headlineSmallStyle.copyWith( - color: context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: Spacing.sm), - Text( - 'Please try again later', - style: AppTypography.bodyMediumStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: Spacing.xl), - ElevatedButton( - onPressed: () => ref.refresh(modelsProvider), - style: ElevatedButton.styleFrom( - backgroundColor: context.conduitTheme.buttonPrimary, - foregroundColor: context.conduitTheme.buttonPrimaryText, - padding: const EdgeInsets.symmetric( - horizontal: Spacing.buttonPadding, - vertical: Spacing.md, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.button), - ), - elevation: Elevation.none, - ), - child: Text( - 'Retry', - style: AppTypography.labelStyle.copyWith( - color: context.conduitTheme.buttonPrimaryText, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildSearchField() { - return Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - context.conduitTheme.inputBackground.withValues(alpha: 0.6), - context.conduitTheme.inputBackground.withValues(alpha: 0.3), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - border: Border.all( - color: context.conduitTheme.inputBorder.withValues(alpha: 0.3), - width: BorderWidth.thin, - ), - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocusNode, - style: TextStyle( - color: context.conduitTheme.inputText, - fontSize: AppTypography.bodyMedium, - ), - decoration: InputDecoration( - hintText: 'Search models...', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder.withValues(alpha: 0.8), - fontSize: AppTypography.bodyMedium, - ), - prefixIcon: Icon( - Platform.isIOS ? CupertinoIcons.search : Icons.search, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: Icon( - Platform.isIOS - ? CupertinoIcons.clear_circled_solid - : Icons.clear, - color: context.conduitTheme.iconSecondary, - size: IconSize.md, - ), - onPressed: () { - _searchController.clear(); - _searchFocusNode.unfocus(); - }, - ) - : null, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - ), - ), - ); - } - - List _groupModels(List models) { - // For now, just return all models in one group - // In the future, we can group by provider, capability, etc. - return [ModelGroup(title: null, models: models)]; - } -} - -class ModelGroup { - final String? title; - final List models; - - ModelGroup({required this.title, required this.models}); -} - -class ModelTile extends StatelessWidget { - final Model model; - final bool isSelected; - final VoidCallback onTap; - - const ModelTile({ - super.key, - required this.model, - required this.isSelected, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - elevation: isSelected ? 2 : 0, - color: isSelected - ? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1) - : context.conduitTheme.cardBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppBorderRadius.md), - side: BorderSide( - color: isSelected - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.dividerColor.withValues(alpha: 0.3), - width: isSelected ? 2 : 1, - ), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - child: Padding( - padding: const EdgeInsets.all(Spacing.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( - model.name, - style: AppTypography.bodyLargeStyle.copyWith( - fontWeight: FontWeight.w600, - color: isSelected - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.textPrimary, - ), - ), - ), - if (isSelected) - Icon( - Platform.isIOS - ? CupertinoIcons.checkmark_circle_fill - : Icons.check_circle, - color: context.conduitTheme.buttonPrimary, - ), - ], - ), - if (model.description != null) ...[ - const SizedBox(height: Spacing.xs), - Text( - model.description!, - style: AppTypography.bodySmallStyle.copyWith( - color: context.conduitTheme.textSecondary, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - const SizedBox(height: Spacing.sm), - Wrap( - spacing: 8, - children: [ - if (model.isMultimodal) - _buildCapabilityChip( - context, - icon: Platform.isIOS ? CupertinoIcons.photo : Icons.image, - label: 'Multimodal', - color: AppTheme.info, - ), - if (model.supportsStreaming) - _buildCapabilityChip( - context, - icon: Platform.isIOS - ? CupertinoIcons.bolt - : Icons.flash_on, - label: 'Streaming', - color: AppTheme.warning, - ), - if (model.supportsRAG) - _buildCapabilityChip( - context, - icon: Platform.isIOS - ? CupertinoIcons.doc_text - : Icons.description, - label: 'RAG', - color: AppTheme.success, - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _buildCapabilityChip( - BuildContext context, { - required IconData icon, - required String label, - required Color color, - }) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 14, color: color), - const SizedBox(width: Spacing.xs), - Text( - label, - style: TextStyle( - fontSize: AppTypography.labelMedium, - color: color, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } -} diff --git a/lib/features/chat/widgets/conversation_search_widget.dart b/lib/features/chat/widgets/conversation_search_widget.dart deleted file mode 100644 index a12171f..0000000 --- a/lib/features/chat/widgets/conversation_search_widget.dart +++ /dev/null @@ -1,739 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../../shared/theme/app_theme.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'dart:io' show Platform; -import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/widgets/loading_states.dart'; -import '../../../shared/widgets/empty_states.dart'; - -import '../../../shared/utils/platform_utils.dart'; -import '../services/conversation_search_service.dart'; -import '../../../core/providers/app_providers.dart'; -import '../../../core/utils/debug_logger.dart'; - -/// Advanced conversation search widget with filters and results -class ConversationSearchWidget extends ConsumerStatefulWidget { - final Function(String conversationId, String? messageId)? onResultTap; - final bool showFilters; - - const ConversationSearchWidget({ - super.key, - this.onResultTap, - this.showFilters = true, - }); - - @override - ConsumerState createState() => - _ConversationSearchWidgetState(); -} - -class _ConversationSearchWidgetState - extends ConsumerState { - final TextEditingController _searchController = TextEditingController(); - final FocusNode _searchFocus = FocusNode(); - bool _isSearching = false; - bool _showFilters = false; - - @override - void initState() { - super.initState(); - _searchController.addListener(_onSearchChanged); - } - - @override - void dispose() { - _searchController.removeListener(_onSearchChanged); - _searchController.dispose(); - _searchFocus.dispose(); - super.dispose(); - } - - void _onSearchChanged() { - final query = _searchController.text.trim(); - ref.read(searchQueryProvider.notifier).state = query; - - if (query.isNotEmpty) { - _performSearch(query); - } else { - ref.read(conversationSearchResultsProvider.notifier).state = null; - } - } - - Future _performSearch(String query) async { - if (_isSearching) return; - - setState(() { - _isSearching = true; - }); - - try { - final searchService = ref.read(conversationSearchServiceProvider); - final conversations = ref - .read(conversationsProvider) - .when( - data: (data) => data, - loading: () => [], - error: (_, _) => [], - ); - - final options = ref.read(searchOptionsProvider); - - final results = await searchService.searchConversations( - conversations: conversations.cast(), - query: query, - options: options, - ); - - ref.read(conversationSearchResultsProvider.notifier).state = results; - } catch (e) { - DebugLogger.error('Search error', e); - } finally { - if (mounted) { - setState(() { - _isSearching = false; - }); - } - } - } - - @override - Widget build(BuildContext context) { - final conduitTheme = context.conduitTheme; - final searchResults = ref.watch(conversationSearchResultsProvider); - - return Column( - children: [ - // Search header - Container( - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: conduitTheme.cardBackground, - border: Border( - bottom: BorderSide( - color: conduitTheme.cardBorder, - width: BorderWidth.regular, - ), - ), - ), - child: Column( - children: [ - // Search input - Row( - children: [ - Expanded( - child: Container( - decoration: BoxDecoration( - color: conduitTheme.inputBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: _searchFocus.hasFocus - ? conduitTheme.inputBorderFocused - : conduitTheme.inputBorder, - width: BorderWidth.regular, - ), - ), - child: TextField( - controller: _searchController, - focusNode: _searchFocus, - decoration: InputDecoration( - hintText: 'Search conversations...', - hintStyle: TextStyle( - color: context.conduitTheme.inputPlaceholder, - fontSize: AppTypography.bodyLarge, - ), - prefixIcon: Icon( - Platform.isIOS - ? CupertinoIcons.search - : Icons.search, - color: context.conduitTheme.iconSecondary, - size: AppTypography.headlineMedium, - ), - suffixIcon: _isSearching - ? Padding( - padding: const EdgeInsets.all(Spacing.md), - child: ConduitLoading.inline( - size: Spacing.md, - ), - ) - : _searchController.text.isNotEmpty - ? IconButton( - icon: Icon( - Platform.isIOS - ? CupertinoIcons.clear - : Icons.clear, - color: context.conduitTheme.iconSecondary, - size: AppTypography.headlineMedium, - ), - onPressed: () { - _searchController.clear(); - _searchFocus.unfocus(); - }, - ) - : null, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - ), - style: TextStyle( - color: context.conduitTheme.inputText, - fontSize: AppTypography.bodyLarge, - ), - onSubmitted: (_) => _searchFocus.unfocus(), - ), - ), - ), - - // Filter toggle - if (widget.showFilters) ...[ - const SizedBox(width: Spacing.xs), - GestureDetector( - onTap: () { - PlatformUtils.lightHaptic(); - setState(() { - _showFilters = !_showFilters; - }); - }, - child: Container( - width: Spacing.xxl + Spacing.xs, - height: Spacing.xxl + Spacing.xs, - decoration: BoxDecoration( - color: _showFilters - ? AppTheme.neutral50.withValues(alpha: 0.2) - : Colors.transparent, - borderRadius: BorderRadius.circular( - AppBorderRadius.md, - ), - border: Border.all( - color: _showFilters - ? AppTheme.neutral50.withValues(alpha: 0.3) - : conduitTheme.inputBorder, - width: BorderWidth.regular, - ), - ), - child: Icon( - Platform.isIOS - ? CupertinoIcons.slider_horizontal_3 - : Icons.tune, - color: AppTheme.neutral50.withValues(alpha: 0.8), - size: AppTypography.headlineMedium, - ), - ), - ), - ], - ], - ), - - // Search filters - if (_showFilters && widget.showFilters) - _buildSearchFilters(conduitTheme), - ], - ), - ), - - // Search results - Expanded(child: _buildSearchResults(conduitTheme, searchResults)), - ], - ); - } - - Widget _buildSearchFilters(ConduitThemeExtension theme) { - final options = ref.watch(searchOptionsProvider); - - return Container( - margin: const EdgeInsets.only(top: Spacing.md), - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: AppTheme.neutral50.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all(color: theme.cardBorder, width: BorderWidth.regular), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Search in:', - style: theme.bodySmall?.copyWith( - fontWeight: FontWeight.w600, - color: AppTheme.neutral50.withValues(alpha: 0.8), - ), - ), - const SizedBox(height: Spacing.xs), - - // Search scope toggles - Wrap( - spacing: Spacing.md, - runSpacing: Spacing.sm, - children: [ - _buildFilterToggle( - 'Titles', - options.searchTitles, - (value) => - _updateSearchOptions(options.copyWith(searchTitles: value)), - ), - _buildFilterToggle( - 'Messages', - options.searchMessages, - (value) => _updateSearchOptions( - options.copyWith(searchMessages: value), - ), - ), - _buildFilterToggle( - 'Tags', - options.searchTags, - (value) => - _updateSearchOptions(options.copyWith(searchTags: value)), - ), - ], - ), - - const SizedBox(height: Spacing.md), - - Text( - 'Message type:', - style: theme.bodySmall?.copyWith( - fontWeight: FontWeight.w600, - color: AppTheme.neutral50.withValues(alpha: 0.8), - ), - ), - const SizedBox(height: Spacing.xs), - - // Role filter - Wrap( - spacing: Spacing.md, - runSpacing: Spacing.sm, - children: [ - _buildFilterChip( - 'All', - options.roleFilter == null, - () => _updateSearchOptions(options.copyWith(roleFilter: null)), - ), - _buildFilterChip( - 'My messages', - options.roleFilter == 'user', - () => - _updateSearchOptions(options.copyWith(roleFilter: 'user')), - ), - _buildFilterChip( - 'AI messages', - options.roleFilter == 'assistant', - () => _updateSearchOptions( - options.copyWith(roleFilter: 'assistant'), - ), - ), - ], - ), - ], - ), - ).animate().slideY( - begin: -0.5, - end: 0, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut, - ); - } - - Widget _buildFilterToggle( - String label, - bool value, - Function(bool) onChanged, - ) { - return GestureDetector( - onTap: () { - PlatformUtils.selectionHaptic(); - onChanged(!value); - }, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: AppTypography.headlineMedium, - height: AppTypography.headlineMedium, - decoration: BoxDecoration( - color: value ? AppTheme.brandPrimary : Colors.transparent, - borderRadius: BorderRadius.circular(AppBorderRadius.xs), - border: Border.all( - color: value - ? AppTheme.brandPrimary - : AppTheme.neutral50.withValues(alpha: 0.3), - width: BorderWidth.regular, - ), - ), - child: value - ? const Icon( - Icons.check, - color: AppTheme.neutral50, - size: AppTypography.labelLarge, - ) - : null, - ), - const SizedBox(width: Spacing.sm), - Text( - label, - style: TextStyle( - color: AppTheme.neutral50.withValues(alpha: 0.8), - fontSize: AppTypography.labelLarge, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ); - } - - Widget _buildFilterChip(String label, bool isActive, VoidCallback onTap) { - return GestureDetector( - onTap: () { - PlatformUtils.selectionHaptic(); - onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xs, - vertical: Spacing.xs + Spacing.xxs, - ), - decoration: BoxDecoration( - color: isActive - ? AppTheme.brandPrimary.withValues(alpha: 0.2) - : Colors.transparent, - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - border: Border.all( - color: isActive - ? AppTheme.brandPrimary - : AppTheme.neutral50.withValues(alpha: 0.3), - width: BorderWidth.regular, - ), - ), - child: Text( - label, - style: TextStyle( - color: isActive - ? AppTheme.brandPrimary - : AppTheme.neutral50.withValues(alpha: 0.8), - fontSize: AppTypography.labelMedium, - fontWeight: FontWeight.w500, - ), - ), - ), - ); - } - - void _updateSearchOptions(ConversationSearchOptions newOptions) { - ref.read(searchOptionsProvider.notifier).state = newOptions; - - // Re-search with new options if we have a query - final query = _searchController.text.trim(); - if (query.isNotEmpty) { - _performSearch(query); - } - } - - Widget _buildSearchResults( - ConduitThemeExtension theme, - ConversationSearchResults? results, - ) { - if (_searchController.text.trim().isEmpty) { - return _buildSearchPrompt(theme); - } - - if (results == null) { - return Center(child: ConduitLoading.primary()); - } - - if (results.isEmpty) { - return SearchEmptyState( - query: results.query, - onClearSearch: () { - _searchController.clear(); - _searchFocus.unfocus(); - }, - ); - } - - return Column( - children: [ - // Results header - Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - decoration: BoxDecoration( - color: AppTheme.neutral50.withValues(alpha: 0.05), - border: Border( - bottom: BorderSide( - color: theme.cardBorder, - width: BorderWidth.regular, - ), - ), - ), - child: Row( - children: [ - Text( - '${results.length} of ${results.totalMatches} results', - style: theme.bodySmall?.copyWith( - color: AppTheme.neutral50.withValues(alpha: 0.7), - ), - ), - const Spacer(), - Text( - '${results.searchDuration.inMilliseconds}ms', - style: theme.bodySmall?.copyWith( - color: AppTheme.neutral50.withValues(alpha: 0.5), - ), - ), - ], - ), - ), - - // Results list - Expanded( - child: ListView.builder( - itemCount: results.length, - itemBuilder: (context, index) { - final match = results.results[index]; - return _buildSearchResultItem(theme, match, index); - }, - ), - ), - ], - ); - } - - Widget _buildSearchPrompt(ConduitThemeExtension theme) { - return ConduitEmptyState( - title: 'Search your conversations', - subtitle: 'Find messages, titles, and tags across all your conversations', - icon: Platform.isIOS ? CupertinoIcons.search : Icons.search, - ); - } - - Widget _buildSearchResultItem( - ConduitThemeExtension theme, - ConversationSearchMatch match, - int index, - ) { - return GestureDetector( - onTap: () { - PlatformUtils.lightHaptic(); - widget.onResultTap?.call(match.conversationId, match.messageId); - }, - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: Spacing.md, - vertical: Spacing.xs, - ), - padding: const EdgeInsets.all(Spacing.md), - decoration: BoxDecoration( - color: theme.cardBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.md), - border: Border.all( - color: theme.cardBorder, - width: BorderWidth.regular, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header with conversation title and match type - Row( - children: [ - Expanded( - child: Text( - match.conversationTitle, - style: theme.headingSmall?.copyWith( - fontSize: AppTypography.bodyLarge, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: Spacing.sm), - _buildMatchTypeBadge(match.matchType), - ], - ), - - const SizedBox(height: Spacing.sm), - - // Snippet with highlighted text - _buildHighlightedSnippet(theme, match.highlightedSnippet), - - const SizedBox(height: Spacing.sm), - - // Footer with metadata - Row( - children: [ - if (match.messageRole != null) ...[ - _buildRoleBadge(match.messageRole!), - const SizedBox(width: Spacing.sm), - ], - Text( - _formatTimestamp(match.timestamp), - style: theme.caption, - ), - const Spacer(), - Text( - '${match.relevanceScore.round()}% match', - style: theme.caption?.copyWith( - color: AppTheme.brandPrimary, - ), - ), - ], - ), - ], - ), - ), - ) - .animate(delay: Duration(milliseconds: index * 50)) - .fadeIn(duration: const Duration(milliseconds: 200)) - .slideX(begin: 0.3, end: 0); - } - - Widget _buildMatchTypeBadge(SearchMatchType type) { - Color color; - String label; - - switch (type) { - case SearchMatchType.title: - color = AppTheme.info; - label = 'Title'; - break; - case SearchMatchType.message: - color = AppTheme.success; - label = 'Message'; - break; - case SearchMatchType.tag: - color = AppTheme.warning; - label = 'Tag'; - break; - } - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.sm, - vertical: Spacing.xxs, - ), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(AppBorderRadius.md), - ), - child: Text( - label, - style: TextStyle( - color: color, - fontSize: AppTypography.labelSmall, - fontWeight: FontWeight.w600, - ), - ), - ); - } - - Widget _buildRoleBadge(String role) { - Color color; - String label; - - switch (role) { - case 'user': - color = AppTheme.brandPrimary; - label = 'You'; - break; - case 'assistant': - color = AppTheme.success; - label = 'AI'; - break; - case 'system': - color = AppTheme.warning; - label = 'System'; - break; - default: - color = AppTheme.neutral400; - label = role; - } - - return Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.xs + Spacing.xxs, - vertical: Spacing.xxs, - ), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(AppBorderRadius.sm), - ), - child: Text( - label, - style: TextStyle( - color: color, - fontSize: AppTypography.labelSmall, - fontWeight: FontWeight.w500, - ), - ), - ); - } - - Widget _buildHighlightedSnippet( - ConduitThemeExtension theme, - String highlightedText, - ) { - // Simple implementation - in a real app you'd want proper HTML parsing - final parts = highlightedText.split(''); - final spans = []; - - for (int i = 0; i < parts.length; i++) { - final part = parts[i]; - if (i == 0) { - spans.add(TextSpan(text: part)); - } else { - final markParts = part.split(''); - if (markParts.length >= 2) { - // Highlighted part - spans.add( - TextSpan( - text: markParts[0], - style: TextStyle( - backgroundColor: AppTheme.brandPrimary.withValues(alpha: 0.3), - color: AppTheme.neutral50, - fontWeight: FontWeight.w600, - ), - ), - ); - // Rest of the text - spans.add(TextSpan(text: markParts.sublist(1).join(''))); - } else { - spans.add(TextSpan(text: part)); - } - } - } - - return RichText( - text: TextSpan( - style: theme.bodyMedium?.copyWith( - color: AppTheme.neutral50.withValues(alpha: 0.8), - height: 1.4, - ), - children: spans, - ), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ); - } - - String _formatTimestamp(DateTime timestamp) { - final now = DateTime.now(); - final diff = now.difference(timestamp); - - if (diff.inDays > 7) { - return '${timestamp.day}/${timestamp.month}/${timestamp.year}'; - } else if (diff.inDays > 0) { - return '${diff.inDays}d ago'; - } else if (diff.inHours > 0) { - return '${diff.inHours}h ago'; - } else if (diff.inMinutes > 0) { - return '${diff.inMinutes}m ago'; - } else { - return 'Just now'; - } - } -} diff --git a/lib/features/tools/widgets/tool_selector.dart b/lib/features/tools/widgets/tool_selector.dart deleted file mode 100644 index a7bfa6f..0000000 --- a/lib/features/tools/widgets/tool_selector.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:conduit/features/tools/providers/tools_providers.dart'; - -class ToolSelector extends ConsumerWidget { - const ToolSelector({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final toolsAsync = ref.watch(toolsListProvider); - final selectedIds = ref.watch(selectedToolIdsProvider); - final theme = Theme.of(context); - - return toolsAsync.when( - data: (tools) { - if (tools.isEmpty) { - return const SizedBox.shrink(); - } - - return Container( - height: 40, - margin: const EdgeInsets.symmetric(vertical: 8), - child: ListView.separated( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 12), - itemCount: tools.length, - separatorBuilder: (context, index) => const SizedBox(width: 8), - itemBuilder: (context, index) { - final tool = tools[index]; - final isSelected = selectedIds.contains(tool.id); - - return FilterChip( - label: Text(tool.name), - selected: isSelected, - onSelected: (_) { - final currentIds = ref.read(selectedToolIdsProvider); - if (isSelected) { - ref.read(selectedToolIdsProvider.notifier).state = - currentIds.where((id) => id != tool.id).toList(); - } else { - ref.read(selectedToolIdsProvider.notifier).state = - [...currentIds, tool.id]; - } - }, - avatar: Icon( - Icons.build, - size: 16, - color: isSelected - ? theme.colorScheme.onSecondaryContainer - : theme.colorScheme.onSurfaceVariant, - ), - ); - }, - ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (error, stack) => const SizedBox.shrink(), - ); - } -} \ No newline at end of file