refactor: enhance server-side search and user bubble overflow
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,47 +444,49 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user