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"');
|
foundation.debugPrint('DEBUG: Performing server-side search for: "$query"');
|
||||||
|
|
||||||
// Use the new server-side search API
|
// Use the new server-side search API
|
||||||
final searchResult = await api.searchChats(
|
final chatHits = await api.searchChats(
|
||||||
query: query.trim(),
|
query: query.trim(),
|
||||||
archived: false, // Only search non-archived conversations
|
archived: false, // Only search non-archived conversations
|
||||||
limit: 50,
|
limit: 50,
|
||||||
sortBy: 'updated_at',
|
sortBy: 'updated_at',
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
);
|
);
|
||||||
|
// chatHits is already List<Conversation>
|
||||||
|
final List<Conversation> conversations = List.of(chatHits);
|
||||||
|
|
||||||
// Extract conversations from search result
|
// Perform message-level search and merge chat hits
|
||||||
final List<dynamic> conversationsData = searchResult['conversations'] ?? [];
|
try {
|
||||||
|
final messageHits = await api.searchMessages(
|
||||||
|
query: query.trim(),
|
||||||
|
limit: 100,
|
||||||
|
);
|
||||||
|
|
||||||
// Convert to Conversation objects
|
// Build a set of conversation IDs already present from chat search
|
||||||
final List<Conversation> conversations = conversationsData.map((data) {
|
final existingIds = conversations.map((c) => c.id).toSet();
|
||||||
return Conversation.fromJson(data as Map<String, dynamic>);
|
|
||||||
}).toList();
|
// 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(
|
foundation.debugPrint(
|
||||||
'DEBUG: Server search returned ${conversations.length} results',
|
'DEBUG: Server search returned ${conversations.length} results',
|
||||||
|
|||||||
@@ -3164,7 +3164,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Advanced search for chats and messages
|
/// Advanced search for chats and messages
|
||||||
Future<Map<String, dynamic>> searchChats({
|
Future<List<Conversation>> searchChats({
|
||||||
String? query,
|
String? query,
|
||||||
String? userId,
|
String? userId,
|
||||||
String? model,
|
String? model,
|
||||||
@@ -3181,7 +3181,8 @@ class ApiService {
|
|||||||
}) async {
|
}) async {
|
||||||
debugPrint('DEBUG: Searching chats with query: $query');
|
debugPrint('DEBUG: Searching chats with query: $query');
|
||||||
final queryParams = <String, dynamic>{};
|
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 (userId != null) queryParams['user_id'] = userId;
|
||||||
if (model != null) queryParams['model'] = model;
|
if (model != null) queryParams['model'] = model;
|
||||||
if (tag != null) queryParams['tag'] = tag;
|
if (tag != null) queryParams['tag'] = tag;
|
||||||
@@ -3199,7 +3200,25 @@ class ApiService {
|
|||||||
'/api/v1/chats/search',
|
'/api/v1/chats/search',
|
||||||
queryParameters: queryParams,
|
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
|
/// Search within messages content
|
||||||
|
|||||||
@@ -1054,6 +1054,37 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
_formatModelDisplayName(selectedModel.name),
|
_formatModelDisplayName(selectedModel.name),
|
||||||
@@ -1151,6 +1182,37 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
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(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Choose Model',
|
'Choose Model',
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
margin: const EdgeInsets.only(
|
margin: const EdgeInsets.only(
|
||||||
bottom: Spacing.sm,
|
bottom: Spacing.md,
|
||||||
left: Spacing.xxxl,
|
left: Spacing.xxxl,
|
||||||
right: Spacing.xs,
|
right: Spacing.xs,
|
||||||
),
|
),
|
||||||
@@ -444,47 +444,49 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble>
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
ConstrainedBox(
|
Flexible(
|
||||||
constraints: BoxConstraints(
|
child: ConstrainedBox(
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.82,
|
constraints: BoxConstraints(
|
||||||
),
|
maxWidth: MediaQuery.of(context).size.width * 0.82,
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: Spacing.chatBubblePadding,
|
|
||||||
vertical: Spacing.sm,
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
gradient: LinearGradient(
|
padding: const EdgeInsets.symmetric(
|
||||||
begin: Alignment.topLeft,
|
horizontal: Spacing.chatBubblePadding,
|
||||||
end: Alignment.bottomRight,
|
vertical: Spacing.sm,
|
||||||
colors: [
|
),
|
||||||
context.conduitTheme.chatBubbleUser.withValues(
|
decoration: BoxDecoration(
|
||||||
alpha: 0.95,
|
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(
|
child: Text(
|
||||||
AppBorderRadius.messageBubble,
|
widget.message.content,
|
||||||
),
|
style: AppTypography.chatMessageStyle.copyWith(
|
||||||
border: Border.all(
|
color: context.conduitTheme.chatBubbleUserText,
|
||||||
color: context.conduitTheme.chatBubbleUserBorder,
|
|
||||||
width: BorderWidth.regular,
|
|
||||||
),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withValues(alpha: 0.08),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
),
|
||||||
],
|
softWrap: true,
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
widget.message.content,
|
|
||||||
style: AppTypography.chatMessageStyle.copyWith(
|
|
||||||
color: context.conduitTheme.chatBubbleUserText,
|
|
||||||
),
|
),
|
||||||
softWrap: true,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user