feat(folders): Improve folder management and parsing logic
This commit is contained in:
@@ -890,6 +890,10 @@ void refreshConversationsCache(dynamic ref, {bool includeFolders = false}) {
|
||||
ref.read(_conversationsCacheTimestampProvider.notifier).set(null);
|
||||
final notifier = ref.read(conversationsProvider.notifier);
|
||||
unawaited(notifier.refresh(includeFolders: includeFolders));
|
||||
if (includeFolders) {
|
||||
final foldersNotifier = ref.read(foldersProvider.notifier);
|
||||
unawaited(foldersNotifier.refresh());
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation providers - Now using correct OpenWebUI API with caching and
|
||||
@@ -918,7 +922,7 @@ class Conversations extends _$Conversations {
|
||||
_updateCacheTimestamp(null);
|
||||
state = AsyncData<List<Conversation>>(<Conversation>[]);
|
||||
if (includeFolders) {
|
||||
ref.invalidate(foldersProvider);
|
||||
unawaited(ref.read(foldersProvider.notifier).refresh());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -926,7 +930,7 @@ class Conversations extends _$Conversations {
|
||||
if (ref.read(reviewerModeProvider)) {
|
||||
state = AsyncData<List<Conversation>>(_demoConversations());
|
||||
if (includeFolders) {
|
||||
ref.invalidate(foldersProvider);
|
||||
unawaited(ref.read(foldersProvider.notifier).refresh());
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -935,7 +939,7 @@ class Conversations extends _$Conversations {
|
||||
if (!ref.mounted) return;
|
||||
state = result;
|
||||
if (includeFolders) {
|
||||
ref.invalidate(foldersProvider);
|
||||
unawaited(ref.read(foldersProvider.notifier).refresh());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1823,18 +1827,68 @@ final webSearchAvailableProvider = Provider<bool>((ref) {
|
||||
|
||||
// Folders provider
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<List<Folder>> folders(Ref ref) async {
|
||||
// Protected: require authentication
|
||||
if (!ref.read(isAuthenticatedProvider2)) {
|
||||
class Folders extends _$Folders {
|
||||
@override
|
||||
Future<List<Folder>> build() async {
|
||||
if (!ref.watch(isAuthenticatedProvider2)) {
|
||||
DebugLogger.log('skip-unauthed', scope: 'folders');
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
final api = ref.watch(apiServiceProvider);
|
||||
if (api == null) {
|
||||
DebugLogger.warning('api-missing', scope: 'folders');
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
return _load(api);
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
if (!ref.read(isAuthenticatedProvider2)) {
|
||||
state = const AsyncData<List<Folder>>([]);
|
||||
return;
|
||||
}
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) {
|
||||
state = const AsyncData<List<Folder>>([]);
|
||||
return;
|
||||
}
|
||||
final result = await AsyncValue.guard(() => _load(api));
|
||||
if (!ref.mounted) return;
|
||||
state = result;
|
||||
}
|
||||
|
||||
void upsertFolder(Folder folder) {
|
||||
final current = state.asData?.value ?? const <Folder>[];
|
||||
final updated = <Folder>[...current];
|
||||
final index = updated.indexWhere((existing) => existing.id == folder.id);
|
||||
if (index >= 0) {
|
||||
updated[index] = folder;
|
||||
} else {
|
||||
updated.add(folder);
|
||||
}
|
||||
state = AsyncData<List<Folder>>(_sort(updated));
|
||||
}
|
||||
|
||||
void updateFolder(String id, Folder Function(Folder folder) transform) {
|
||||
final current = state.asData?.value;
|
||||
if (current == null) return;
|
||||
final index = current.indexWhere((folder) => folder.id == id);
|
||||
if (index < 0) return;
|
||||
final updated = <Folder>[...current];
|
||||
updated[index] = transform(updated[index]);
|
||||
state = AsyncData<List<Folder>>(_sort(updated));
|
||||
}
|
||||
|
||||
void removeFolder(String id) {
|
||||
final current = state.asData?.value;
|
||||
if (current == null) return;
|
||||
final updated = current
|
||||
.where((folder) => folder.id != id)
|
||||
.toList(growable: true);
|
||||
state = AsyncData<List<Folder>>(_sort(updated));
|
||||
}
|
||||
|
||||
Future<List<Folder>> _load(ApiService api) async {
|
||||
try {
|
||||
final foldersData = await api.getFolders();
|
||||
final folders = foldersData
|
||||
@@ -1845,10 +1899,22 @@ Future<List<Folder>> folders(Ref ref) async {
|
||||
scope: 'folders',
|
||||
data: {'count': folders.length},
|
||||
);
|
||||
return folders;
|
||||
} catch (e) {
|
||||
DebugLogger.error('fetch-failed', scope: 'folders', error: e);
|
||||
return [];
|
||||
return _sort(folders);
|
||||
} catch (e, stackTrace) {
|
||||
DebugLogger.error(
|
||||
'fetch-failed',
|
||||
scope: 'folders',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
List<Folder> _sort(List<Folder> input) {
|
||||
final sorted = [...input];
|
||||
sorted.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
return List<Folder>.unmodifiable(sorted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3003,23 +3003,23 @@ class ApiService {
|
||||
);
|
||||
final data = response.data;
|
||||
// The endpoint can return a List[ChatTitleIdResponse] or a map.
|
||||
// Normalize to a List<Conversation> using our safe parser.
|
||||
// Normalize to a List<Conversation> using our isolate parser.
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map((e) => Conversation.fromJson(parseConversationSummary(e)))
|
||||
.toList();
|
||||
return _parseConversationSummaryList(
|
||||
data,
|
||||
debugLabel: 'parse_search_direct',
|
||||
);
|
||||
}
|
||||
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) => Conversation.fromJson(parseConversationSummary(e)))
|
||||
.toList();
|
||||
return _parseConversationSummaryList(
|
||||
list,
|
||||
debugLabel: 'parse_search_wrapped',
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Conversation>[];
|
||||
return const <Conversation>[];
|
||||
}
|
||||
|
||||
/// Search within messages content (capability-safe)
|
||||
|
||||
@@ -322,9 +322,13 @@ class OptimizedStorageService {
|
||||
|
||||
Future<void> saveLocalConversations(List<Conversation> conversations) async {
|
||||
try {
|
||||
final serialized = conversations
|
||||
final jsonReady = conversations
|
||||
.map((conversation) => conversation.toJson())
|
||||
.toList();
|
||||
final serialized = await _workerManager
|
||||
.schedule<Map<String, dynamic>, String>(_encodeConversationsWorker, {
|
||||
'conversations': jsonReady,
|
||||
}, debugLabel: 'encode_local_conversations');
|
||||
await _cachesBox.put(_localConversationsKey, serialized);
|
||||
DebugLogger.log(
|
||||
'Saved ${conversations.length} local conversations',
|
||||
@@ -478,3 +482,15 @@ List<Map<String, dynamic>> _decodeStoredConversationsWorker(
|
||||
|
||||
return <Map<String, dynamic>>[];
|
||||
}
|
||||
|
||||
String _encodeConversationsWorker(Map<String, dynamic> payload) {
|
||||
final raw = payload['conversations'];
|
||||
if (raw is List) {
|
||||
return jsonEncode(raw);
|
||||
}
|
||||
if (raw is String) {
|
||||
// Already encoded.
|
||||
return raw;
|
||||
}
|
||||
return jsonEncode([]);
|
||||
}
|
||||
|
||||
@@ -770,8 +770,10 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
try {
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.createFolder(name: name);
|
||||
final created = await api.createFolder(name: name);
|
||||
final folder = Folder.fromJson(Map<String, dynamic>.from(created));
|
||||
HapticFeedback.lightImpact();
|
||||
ref.read(foldersProvider.notifier).upsertFolder(folder);
|
||||
refreshConversationsCache(ref, includeFolders: true);
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted) return;
|
||||
@@ -1094,6 +1096,13 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.updateFolder(folderId, name: newName);
|
||||
HapticFeedback.selectionClick();
|
||||
ref
|
||||
.read(foldersProvider.notifier)
|
||||
.updateFolder(
|
||||
folderId,
|
||||
(folder) =>
|
||||
folder.copyWith(name: newName, updatedAt: DateTime.now()),
|
||||
);
|
||||
refreshConversationsCache(ref, includeFolders: true);
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted) return;
|
||||
@@ -1129,6 +1138,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
if (api == null) throw Exception('No API service');
|
||||
await api.deleteFolder(folderId);
|
||||
HapticFeedback.mediumImpact();
|
||||
ref.read(foldersProvider.notifier).removeFolder(folderId);
|
||||
refreshConversationsCache(ref, includeFolders: true);
|
||||
} catch (e, stackTrace) {
|
||||
if (!mounted) return;
|
||||
|
||||
Reference in New Issue
Block a user