From aed135c5d4f79e8b7c0b1520914f85aae36f82e5 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 26 Aug 2025 21:19:06 +0530 Subject: [PATCH] refactor: enhance server-side search and user bubble overflow --- lib/core/providers/app_providers.dart | 65 ++++++++++++++-- lib/core/services/api_service.dart | 25 ++++++- lib/features/chat/views/chat_page.dart | 62 ++++++++++++++++ .../chat/widgets/user_message_bubble.dart | 74 ++++++++++--------- 4 files changed, 180 insertions(+), 46 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 0784579..660e151 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -743,21 +743,72 @@ final serverSearchProvider = FutureProvider.family, String>(( foundation.debugPrint('DEBUG: Performing server-side search for: "$query"'); // Use the new server-side search API - final searchResult = await api.searchChats( + final chatHits = await api.searchChats( query: query.trim(), archived: false, // Only search non-archived conversations limit: 50, sortBy: 'updated_at', sortOrder: 'desc', ); + // chatHits is already List + final List conversations = List.of(chatHits); - // Extract conversations from search result - final List conversationsData = searchResult['conversations'] ?? []; + // Perform message-level search and merge chat hits + try { + final messageHits = await api.searchMessages( + query: query.trim(), + limit: 100, + ); - // Convert to Conversation objects - final List conversations = conversationsData.map((data) { - return Conversation.fromJson(data as Map); - }).toList(); + // Build a set of conversation IDs already present from chat search + final existingIds = conversations.map((c) => c.id).toSet(); + + // Extract chat ids from message hits (supporting multiple key casings) + final messageChatIds = {}; + for (final hit in messageHits) { + final chatId = + (hit['chat_id'] ?? hit['chatId'] ?? hit['chatID']) as String?; + if (chatId != null && chatId.isNotEmpty) { + messageChatIds.add(chatId); + } + } + + // Determine which chat ids we still need to fetch + final idsToFetch = messageChatIds + .where((id) => !existingIds.contains(id)) + .toList(); + + // Fetch conversations for those ids in parallel (cap to avoid overload) + const maxFetch = 50; + final fetchList = idsToFetch.take(maxFetch).toList(); + if (fetchList.isNotEmpty) { + foundation.debugPrint( + 'DEBUG: Fetching ${fetchList.length} conversations from message hits', + ); + final fetched = await Future.wait( + fetchList.map((id) async { + try { + return await api.getConversation(id); + } catch (_) { + return null; + } + }), + ); + + // Merge fetched conversations + for (final conv in fetched) { + if (conv != null && !existingIds.contains(conv.id)) { + conversations.add(conv); + existingIds.add(conv.id); + } + } + + // Optional: sort by updated date desc to keep results consistent + conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); + } + } catch (e) { + foundation.debugPrint('DEBUG: Message-level search failed: $e'); + } foundation.debugPrint( 'DEBUG: Server search returned ${conversations.length} results', diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 12b5e59..f2401c6 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -3164,7 +3164,7 @@ class ApiService { } /// Advanced search for chats and messages - Future> searchChats({ + Future> searchChats({ String? query, String? userId, String? model, @@ -3181,7 +3181,8 @@ class ApiService { }) async { debugPrint('DEBUG: Searching chats with query: $query'); final queryParams = {}; - if (query != null) queryParams['q'] = query; + // OpenAPI expects 'text' for this endpoint; keep extras if server tolerates them + if (query != null) queryParams['text'] = query; if (userId != null) queryParams['user_id'] = userId; if (model != null) queryParams['model'] = model; if (tag != null) queryParams['tag'] = tag; @@ -3199,7 +3200,25 @@ class ApiService { '/api/v1/chats/search', queryParameters: queryParams, ); - return response.data as Map; + final data = response.data; + // The endpoint can return a List[ChatTitleIdResponse] or a map. + // Normalize to a List using our safe parser. + if (data is List) { + return data + .whereType>() + .map((e) => _parseOpenWebUIChat(e)) + .toList(); + } + if (data is Map) { + final list = (data['conversations'] ?? data['items'] ?? data['results']); + if (list is List) { + return list + .whereType>() + .map((e) => _parseOpenWebUIChat(e)) + .toList(); + } + } + return []; } /// Search within messages content diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 2e46152..ff3cef0 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1054,6 +1054,37 @@ class _ChatPageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ + Opacity( + opacity: 0.0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.xs, + vertical: Spacing.xxs, + ), + decoration: BoxDecoration( + color: context + .conduitTheme + .surfaceBackground + .withValues(alpha: 0.3), + borderRadius: BorderRadius.circular( + AppBorderRadius.badge, + ), + border: Border.all( + color: + context.conduitTheme.dividerColor, + width: BorderWidth.thin, + ), + ), + child: Icon( + Platform.isIOS + ? CupertinoIcons.chevron_down + : Icons.keyboard_arrow_down, + color: context.conduitTheme.iconSecondary, + size: IconSize.small, + ), + ), + ), + const SizedBox(width: Spacing.xs), Flexible( child: Text( _formatModelDisplayName(selectedModel.name), @@ -1151,6 +1182,37 @@ class _ChatPageState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ + Opacity( + opacity: 0.0, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.xs, + vertical: Spacing.xxs, + ), + decoration: BoxDecoration( + color: context + .conduitTheme + .surfaceBackground + .withValues(alpha: 0.3), + borderRadius: BorderRadius.circular( + AppBorderRadius.badge, + ), + border: Border.all( + color: + context.conduitTheme.dividerColor, + width: BorderWidth.thin, + ), + ), + child: Icon( + Platform.isIOS + ? CupertinoIcons.chevron_down + : Icons.keyboard_arrow_down, + color: context.conduitTheme.iconSecondary, + size: IconSize.small, + ), + ), + ), + const SizedBox(width: Spacing.xs), Flexible( child: Text( 'Choose Model', diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 4ce768a..8739ef7 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -427,7 +427,7 @@ class _UserMessageBubbleState extends ConsumerState child: Container( width: double.infinity, margin: const EdgeInsets.only( - bottom: Spacing.sm, + bottom: Spacing.md, left: Spacing.xxxl, right: Spacing.xs, ), @@ -444,47 +444,49 @@ class _UserMessageBubbleState extends ConsumerState Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.82, - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.chatBubblePadding, - vertical: Spacing.sm, + Flexible( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.82, ), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - context.conduitTheme.chatBubbleUser.withValues( - alpha: 0.95, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.chatBubblePadding, + vertical: Spacing.sm, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + context.conduitTheme.chatBubbleUser + .withValues(alpha: 0.95), + context.conduitTheme.chatBubbleUser, + ], + ), + borderRadius: BorderRadius.circular( + AppBorderRadius.messageBubble, + ), + border: Border.all( + color: + context.conduitTheme.chatBubbleUserBorder, + width: BorderWidth.regular, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.08), + blurRadius: 4, + offset: const Offset(0, 2), ), - context.conduitTheme.chatBubbleUser, ], ), - borderRadius: BorderRadius.circular( - AppBorderRadius.messageBubble, - ), - border: Border.all( - color: context.conduitTheme.chatBubbleUserBorder, - width: BorderWidth.regular, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.08), - blurRadius: 4, - offset: const Offset(0, 2), + child: Text( + widget.message.content, + style: AppTypography.chatMessageStyle.copyWith( + color: context.conduitTheme.chatBubbleUserText, ), - ], - ), - child: Text( - widget.message.content, - style: AppTypography.chatMessageStyle.copyWith( - color: context.conduitTheme.chatBubbleUserText, + softWrap: true, ), - softWrap: true, ), ), ),