feat(conversations): Improve conversation and folder fetching with concurrent requests
This commit is contained in:
@@ -984,198 +984,196 @@ class Conversations extends _$Conversations {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
DebugLogger.log('fetch-start', scope: 'conversations');
|
DebugLogger.log('fetch-start', scope: 'conversations');
|
||||||
final conversations = await api.getConversations();
|
final conversationsFuture = api.getConversations();
|
||||||
|
final foldersFuture = api.getFolders().catchError((error, stackTrace) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'folders-fetch-failed',
|
||||||
|
scope: 'conversations',
|
||||||
|
error: error,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
return <Map<String, dynamic>>[];
|
||||||
|
});
|
||||||
|
|
||||||
|
final results = await Future.wait<dynamic>([
|
||||||
|
conversationsFuture,
|
||||||
|
foldersFuture,
|
||||||
|
]);
|
||||||
|
final conversations = results[0] as List<Conversation>;
|
||||||
|
final foldersData = results[1] as List<Map<String, dynamic>>;
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'fetch-ok',
|
'fetch-ok',
|
||||||
scope: 'conversations',
|
scope: 'conversations',
|
||||||
data: {'count': conversations.length},
|
data: {'count': conversations.length},
|
||||||
);
|
);
|
||||||
|
DebugLogger.log(
|
||||||
|
'folders-fetched',
|
||||||
|
scope: 'conversations',
|
||||||
|
data: {'count': foldersData.length},
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
final folders = foldersData
|
||||||
final foldersData = await api.getFolders();
|
.map((folderData) => Folder.fromJson(folderData))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final conversationToFolder = <String, String>{};
|
||||||
|
for (final folder in folders) {
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'folders-fetched',
|
'folder',
|
||||||
scope: 'conversations',
|
scope: 'conversations/map',
|
||||||
data: {'count': foldersData.length},
|
data: {
|
||||||
|
'id': folder.id,
|
||||||
|
'name': folder.name,
|
||||||
|
'count': folder.conversationIds.length,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
for (final conversationId in folder.conversationIds) {
|
||||||
final folders = foldersData
|
conversationToFolder[conversationId] = folder.id;
|
||||||
.map((folderData) => Folder.fromJson(folderData))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final conversationToFolder = <String, String>{};
|
|
||||||
for (final folder in folders) {
|
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'folder',
|
'map',
|
||||||
scope: 'conversations/map',
|
scope: 'conversations/map',
|
||||||
data: {
|
data: {'conversationId': conversationId, 'folderId': folder.id},
|
||||||
'id': folder.id,
|
|
||||||
'name': folder.name,
|
|
||||||
'count': folder.conversationIds.length,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
for (final conversationId in folder.conversationIds) {
|
|
||||||
conversationToFolder[conversationId] = folder.id;
|
|
||||||
DebugLogger.log(
|
|
||||||
'map',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {'conversationId': conversationId, 'folderId': folder.id},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final conversationMap = <String, Conversation>{};
|
final conversationMap = <String, Conversation>{};
|
||||||
|
|
||||||
for (final conversation in conversations) {
|
for (final conversation in conversations) {
|
||||||
final explicitFolderId = conversation.folderId;
|
final explicitFolderId = conversation.folderId;
|
||||||
final mappedFolderId = conversationToFolder[conversation.id];
|
final mappedFolderId = conversationToFolder[conversation.id];
|
||||||
final folderIdToUse = explicitFolderId ?? mappedFolderId;
|
final folderIdToUse = explicitFolderId ?? mappedFolderId;
|
||||||
if (folderIdToUse != null) {
|
if (folderIdToUse != null) {
|
||||||
conversationMap[conversation.id] = conversation.copyWith(
|
conversationMap[conversation.id] = conversation.copyWith(
|
||||||
folderId: folderIdToUse,
|
folderId: folderIdToUse,
|
||||||
);
|
);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'update-folder',
|
'update-folder',
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {
|
|
||||||
'conversationId': conversation.id,
|
|
||||||
'folderId': folderIdToUse,
|
|
||||||
'explicit': explicitFolderId != null,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
conversationMap[conversation.id] = conversation;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final existingIds = conversationMap.keys.toSet();
|
|
||||||
final missingInBase = conversationToFolder.keys
|
|
||||||
.where((id) => !existingIds.contains(id))
|
|
||||||
.toList();
|
|
||||||
if (missingInBase.isNotEmpty) {
|
|
||||||
DebugLogger.warning(
|
|
||||||
'missing-in-base',
|
|
||||||
scope: 'conversations/map',
|
scope: 'conversations/map',
|
||||||
data: {
|
data: {
|
||||||
'count': missingInBase.length,
|
'conversationId': conversation.id,
|
||||||
'preview': missingInBase.take(5).toList(),
|
'folderId': folderIdToUse,
|
||||||
|
'explicit': explicitFolderId != null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
DebugLogger.log('folders-synced', scope: 'conversations/map');
|
conversationMap[conversation.id] = conversation;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final folder in folders) {
|
|
||||||
final missingIds = folder.conversationIds
|
|
||||||
.where((id) => !existingIds.contains(id))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final hasKnownConversations = conversationMap.values.any(
|
|
||||||
(conversation) => conversation.folderId == folder.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
final shouldFetchFolder =
|
|
||||||
missingIds.isNotEmpty ||
|
|
||||||
(!hasKnownConversations && folder.conversationIds.isEmpty);
|
|
||||||
|
|
||||||
List<Conversation> folderConvs = const [];
|
|
||||||
if (shouldFetchFolder) {
|
|
||||||
try {
|
|
||||||
folderConvs = await api.getConversationsInFolder(folder.id);
|
|
||||||
DebugLogger.log(
|
|
||||||
'folder-sync',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {
|
|
||||||
'folderId': folder.id,
|
|
||||||
'fetched': folderConvs.length,
|
|
||||||
'missingIds': missingIds.length,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'folder-fetch-failed',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
error: e,
|
|
||||||
data: {'folderId': folder.id},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final fetchedMap = {for (final c in folderConvs) c.id: c};
|
|
||||||
|
|
||||||
for (final convId in missingIds) {
|
|
||||||
final fetched = fetchedMap[convId];
|
|
||||||
if (fetched != null) {
|
|
||||||
final toAdd = fetched.folderId == null
|
|
||||||
? fetched.copyWith(folderId: folder.id)
|
|
||||||
: fetched;
|
|
||||||
conversationMap[toAdd.id] = toAdd;
|
|
||||||
existingIds.add(toAdd.id);
|
|
||||||
DebugLogger.log(
|
|
||||||
'add-missing',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {'conversationId': toAdd.id, 'folderId': folder.id},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final placeholder = Conversation(
|
|
||||||
id: convId,
|
|
||||||
title: 'Chat',
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
messages: const [],
|
|
||||||
folderId: folder.id,
|
|
||||||
);
|
|
||||||
conversationMap[convId] = placeholder;
|
|
||||||
existingIds.add(convId);
|
|
||||||
DebugLogger.log(
|
|
||||||
'add-placeholder',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {'conversationId': convId, 'folderId': folder.id},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folderConvs.isNotEmpty && folder.conversationIds.isEmpty) {
|
|
||||||
for (final conv in folderConvs) {
|
|
||||||
final toAdd = conv.folderId == null
|
|
||||||
? conv.copyWith(folderId: folder.id)
|
|
||||||
: conv;
|
|
||||||
conversationMap[toAdd.id] = toAdd;
|
|
||||||
existingIds.add(toAdd.id);
|
|
||||||
DebugLogger.log(
|
|
||||||
'add-folder-fetch',
|
|
||||||
scope: 'conversations/map',
|
|
||||||
data: {'conversationId': toAdd.id, 'folderId': folder.id},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final sortedConversations = _sortByUpdatedAt(
|
|
||||||
conversationMap.values.toList(),
|
|
||||||
);
|
|
||||||
DebugLogger.log(
|
|
||||||
'sort',
|
|
||||||
scope: 'conversations',
|
|
||||||
data: {'source': 'folder-sync'},
|
|
||||||
);
|
|
||||||
_updateCacheTimestamp(DateTime.now());
|
|
||||||
return sortedConversations;
|
|
||||||
} catch (e) {
|
|
||||||
DebugLogger.error(
|
|
||||||
'folders-fetch-failed',
|
|
||||||
scope: 'conversations',
|
|
||||||
error: e,
|
|
||||||
);
|
|
||||||
final sorted = _sortByUpdatedAt(conversations.toList());
|
|
||||||
DebugLogger.log(
|
|
||||||
'sort',
|
|
||||||
scope: 'conversations',
|
|
||||||
data: {'source': 'fallback'},
|
|
||||||
);
|
|
||||||
_updateCacheTimestamp(DateTime.now());
|
|
||||||
return sorted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final existingIds = conversationMap.keys.toSet();
|
||||||
|
final missingInBase = conversationToFolder.keys
|
||||||
|
.where((id) => !existingIds.contains(id))
|
||||||
|
.toList();
|
||||||
|
if (missingInBase.isNotEmpty) {
|
||||||
|
DebugLogger.warning(
|
||||||
|
'missing-in-base',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
data: {
|
||||||
|
'count': missingInBase.length,
|
||||||
|
'preview': missingInBase.take(5).toList(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
DebugLogger.log('folders-synced', scope: 'conversations/map');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final folder in folders) {
|
||||||
|
final missingIds = folder.conversationIds
|
||||||
|
.where((id) => !existingIds.contains(id))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final hasKnownConversations = conversationMap.values.any(
|
||||||
|
(conversation) => conversation.folderId == folder.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final shouldFetchFolder =
|
||||||
|
missingIds.isNotEmpty ||
|
||||||
|
(!hasKnownConversations && folder.conversationIds.isEmpty);
|
||||||
|
|
||||||
|
List<Conversation> folderConvs = const [];
|
||||||
|
if (shouldFetchFolder) {
|
||||||
|
try {
|
||||||
|
folderConvs = await api.getFolderConversationSummaries(folder.id);
|
||||||
|
DebugLogger.log(
|
||||||
|
'folder-sync',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
data: {
|
||||||
|
'folderId': folder.id,
|
||||||
|
'fetched': folderConvs.length,
|
||||||
|
'missingIds': missingIds.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'folder-fetch-failed',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
error: e,
|
||||||
|
data: {'folderId': folder.id},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final fetchedMap = {for (final c in folderConvs) c.id: c};
|
||||||
|
|
||||||
|
for (final convId in missingIds) {
|
||||||
|
final fetched = fetchedMap[convId];
|
||||||
|
if (fetched != null) {
|
||||||
|
final toAdd = fetched.folderId == null
|
||||||
|
? fetched.copyWith(folderId: folder.id)
|
||||||
|
: fetched;
|
||||||
|
conversationMap[toAdd.id] = toAdd;
|
||||||
|
existingIds.add(toAdd.id);
|
||||||
|
DebugLogger.log(
|
||||||
|
'add-missing',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
data: {'conversationId': toAdd.id, 'folderId': folder.id},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final placeholder = Conversation(
|
||||||
|
id: convId,
|
||||||
|
title: 'Chat',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
messages: const [],
|
||||||
|
folderId: folder.id,
|
||||||
|
);
|
||||||
|
conversationMap[convId] = placeholder;
|
||||||
|
existingIds.add(convId);
|
||||||
|
DebugLogger.log(
|
||||||
|
'add-placeholder',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
data: {'conversationId': convId, 'folderId': folder.id},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folderConvs.isNotEmpty && folder.conversationIds.isEmpty) {
|
||||||
|
for (final conv in folderConvs) {
|
||||||
|
final toAdd = conv.folderId == null
|
||||||
|
? conv.copyWith(folderId: folder.id)
|
||||||
|
: conv;
|
||||||
|
conversationMap[toAdd.id] = toAdd;
|
||||||
|
existingIds.add(toAdd.id);
|
||||||
|
DebugLogger.log(
|
||||||
|
'add-folder-fetch',
|
||||||
|
scope: 'conversations/map',
|
||||||
|
data: {'conversationId': toAdd.id, 'folderId': folder.id},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortedConversations = _sortByUpdatedAt(
|
||||||
|
conversationMap.values.toList(),
|
||||||
|
);
|
||||||
|
DebugLogger.log(
|
||||||
|
'sort',
|
||||||
|
scope: 'conversations',
|
||||||
|
data: {'source': 'folder-sync'},
|
||||||
|
);
|
||||||
|
_updateCacheTimestamp(DateTime.now());
|
||||||
|
return sortedConversations;
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
DebugLogger.error(
|
DebugLogger.error(
|
||||||
'fetch-failed',
|
'fetch-failed',
|
||||||
|
|||||||
@@ -409,6 +409,15 @@ class ApiService {
|
|||||||
|
|
||||||
// Conversations - Updated to use correct OpenWebUI API
|
// Conversations - Updated to use correct OpenWebUI API
|
||||||
Future<List<Conversation>> getConversations({int? limit, int? skip}) async {
|
Future<List<Conversation>> getConversations({int? limit, int? skip}) async {
|
||||||
|
final pinnedFuture = _fetchChatCollection(
|
||||||
|
'/api/v1/chats/pinned',
|
||||||
|
debugLabel: 'pinned chats',
|
||||||
|
);
|
||||||
|
final archivedFuture = _fetchChatCollection(
|
||||||
|
'/api/v1/chats/archived',
|
||||||
|
debugLabel: 'archived chats',
|
||||||
|
);
|
||||||
|
|
||||||
List<dynamic> allRegularChats = [];
|
List<dynamic> allRegularChats = [];
|
||||||
|
|
||||||
if (limit == null) {
|
if (limit == null) {
|
||||||
@@ -422,7 +431,11 @@ class ApiService {
|
|||||||
while (true) {
|
while (true) {
|
||||||
final response = await _dio.get(
|
final response = await _dio.get(
|
||||||
'/api/v1/chats/',
|
'/api/v1/chats/',
|
||||||
queryParameters: {'page': currentPage},
|
queryParameters: {
|
||||||
|
'page': currentPage,
|
||||||
|
'include_folders': true,
|
||||||
|
'include_pinned': true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.data is! List) {
|
if (response.data is! List) {
|
||||||
@@ -454,14 +467,21 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Original single page fetch
|
// Original single page fetch
|
||||||
|
final pageQuery = <String, dynamic>{
|
||||||
|
'include_folders': true,
|
||||||
|
'include_pinned': true,
|
||||||
|
};
|
||||||
|
if (limit > 0) {
|
||||||
|
pageQuery['page'] = (((skip ?? 0) / limit).floor() + 1).clamp(
|
||||||
|
1,
|
||||||
|
1 << 30,
|
||||||
|
);
|
||||||
|
}
|
||||||
final regularResponse = await _dio.get(
|
final regularResponse = await _dio.get(
|
||||||
'/api/v1/chats/',
|
'/api/v1/chats/',
|
||||||
// Convert skip/limit to 1-based page index expected by OpenWebUI.
|
// Convert skip/limit to 1-based page index expected by OpenWebUI.
|
||||||
// Example: skip=0 => page=1, skip=limit => page=2, etc.
|
// Example: skip=0 => page=1, skip=limit => page=2, etc.
|
||||||
queryParameters: {
|
queryParameters: pageQuery,
|
||||||
if (limit > 0)
|
|
||||||
'page': (((skip ?? 0) / limit).floor() + 1).clamp(1, 1 << 30),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (regularResponse.data is! List) {
|
if (regularResponse.data is! List) {
|
||||||
@@ -473,14 +493,12 @@ class ApiService {
|
|||||||
allRegularChats = regularResponse.data as List;
|
allRegularChats = regularResponse.data as List;
|
||||||
}
|
}
|
||||||
|
|
||||||
final pinnedChatList = await _fetchChatCollection(
|
final pinnedAndArchived = await Future.wait<List<dynamic>>([
|
||||||
'/api/v1/chats/pinned',
|
pinnedFuture,
|
||||||
debugLabel: 'pinned chats',
|
archivedFuture,
|
||||||
);
|
]);
|
||||||
final archivedChatList = await _fetchChatCollection(
|
final pinnedChatList = pinnedAndArchived[0];
|
||||||
'/api/v1/chats/all/archived',
|
final archivedChatList = pinnedAndArchived[1];
|
||||||
debugLabel: 'archived chats',
|
|
||||||
);
|
|
||||||
final regularChatList = allRegularChats;
|
final regularChatList = allRegularChats;
|
||||||
|
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
@@ -1035,17 +1053,21 @@ class ApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Conversation>> getConversationsInFolder(String folderId) async {
|
Future<List<Conversation>> getFolderConversationSummaries(
|
||||||
_traceApi('Fetching conversations in folder: $folderId');
|
String folderId,
|
||||||
final response = await _dio.get('/api/v1/chats/folder/$folderId');
|
) async {
|
||||||
|
_traceApi('Fetching conversation summaries in folder: $folderId');
|
||||||
|
final response = await _dio.get('/api/v1/chats/folder/$folderId/list');
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
if (data is List) {
|
if (data is! List) {
|
||||||
return _parseConversationSummaryList(
|
return const [];
|
||||||
data,
|
|
||||||
debugLabel: 'parse_folder_$folderId',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return [];
|
final normalized = data
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(parseConversationSummary)
|
||||||
|
.map(Conversation.fromJson)
|
||||||
|
.toList(growable: false);
|
||||||
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
|
|||||||
Reference in New Issue
Block a user