refactor: enhance server-side search and user bubble overflow

This commit is contained in:
cogwheel0
2025-08-26 21:19:06 +05:30
parent 598227489d
commit aed135c5d4
4 changed files with 180 additions and 46 deletions

View File

@@ -743,21 +743,72 @@ final serverSearchProvider = FutureProvider.family<List<Conversation>, 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<Conversation>
final List<Conversation> conversations = List.of(chatHits);
// Extract conversations from search result
final List<dynamic> 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<Conversation> conversations = conversationsData.map((data) {
return Conversation.fromJson(data as Map<String, dynamic>);
}).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 = <String>{};
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',

View File

@@ -3164,7 +3164,7 @@ class ApiService {
}
/// Advanced search for chats and messages
Future<Map<String, dynamic>> searchChats({
Future<List<Conversation>> searchChats({
String? query,
String? userId,
String? model,
@@ -3181,7 +3181,8 @@ class ApiService {
}) async {
debugPrint('DEBUG: Searching chats with query: $query');
final queryParams = <String, dynamic>{};
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<String, dynamic>;
final data = response.data;
// The endpoint can return a List[ChatTitleIdResponse] or a map.
// Normalize to a List<Conversation> using our safe parser.
if (data is List) {
return data
.whereType<Map<String, dynamic>>()
.map((e) => _parseOpenWebUIChat(e))
.toList();
}
if (data is Map<String, dynamic>) {
final list = (data['conversations'] ?? data['items'] ?? data['results']);
if (list is List) {
return list
.whereType<Map<String, dynamic>>()
.map((e) => _parseOpenWebUIChat(e))
.toList();
}
}
return <Conversation>[];
}
/// Search within messages content

View File

@@ -1054,6 +1054,37 @@ class _ChatPageState extends ConsumerState<ChatPage> {
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<ChatPage> {
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',

View File

@@ -427,7 +427,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
child: Container(
width: double.infinity,
margin: const EdgeInsets.only(
bottom: Spacing.sm,
bottom: Spacing.md,
left: Spacing.xxxl,
right: Spacing.xs,
),
@@ -444,7 +444,8 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ConstrainedBox(
Flexible(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.82,
),
@@ -458,9 +459,8 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
context.conduitTheme.chatBubbleUser.withValues(
alpha: 0.95,
),
context.conduitTheme.chatBubbleUser
.withValues(alpha: 0.95),
context.conduitTheme.chatBubbleUser,
],
),
@@ -468,7 +468,8 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
AppBorderRadius.messageBubble,
),
border: Border.all(
color: context.conduitTheme.chatBubbleUserBorder,
color:
context.conduitTheme.chatBubbleUserBorder,
width: BorderWidth.regular,
),
boxShadow: [
@@ -488,6 +489,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
),
),
),
),
],
),
if (hasText) const SizedBox(height: Spacing.xs),