From 42ef62d5657d8789da44c1acbd7d132cf8309247 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Sat, 1 Nov 2025 15:15:38 +0530 Subject: [PATCH] feat(api): Improve file and knowledge base data parsing with normalization --- lib/core/providers/app_providers.dart | 161 +++++++++++++++++++++----- lib/core/services/api_service.dart | 60 ++++++++-- 2 files changed, 184 insertions(+), 37 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index ba0f3b2..cd331a4 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -1920,21 +1920,73 @@ class Folders extends _$Folders { // Files provider @Riverpod(keepAlive: true) -Future> userFiles(Ref ref) async { - // Protected: require authentication - if (!ref.read(isAuthenticatedProvider2)) { - DebugLogger.log('skip-unauthed', scope: 'files'); - return []; +class UserFiles extends _$UserFiles { + @override + Future> build() async { + if (!ref.watch(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'files'); + return const []; + } + final api = ref.watch(apiServiceProvider); + if (api == null) return const []; + return _load(api); } - final api = ref.watch(apiServiceProvider); - if (api == null) return []; - try { - final filesData = await api.getUserFiles(); - return filesData.map((fileData) => FileInfo.fromJson(fileData)).toList(); - } catch (e) { - DebugLogger.error('files-failed', scope: 'files', error: e); - return []; + Future refresh() async { + if (!ref.read(isAuthenticatedProvider2)) { + state = const AsyncData>([]); + return; + } + final api = ref.read(apiServiceProvider); + if (api == null) { + state = const AsyncData>([]); + return; + } + final result = await AsyncValue.guard(() => _load(api)); + if (!ref.mounted) return; + state = result; + } + + void upsert(FileInfo file) { + final current = state.asData?.value ?? const []; + final updated = [...current]; + final index = updated.indexWhere((existing) => existing.id == file.id); + if (index >= 0) { + updated[index] = file; + } else { + updated.add(file); + } + state = AsyncData>(_sort(updated)); + } + + void remove(String id) { + final current = state.asData?.value; + if (current == null) return; + final updated = current + .where((file) => file.id != id) + .toList(growable: true); + state = AsyncData>(_sort(updated)); + } + + Future> _load(ApiService api) async { + try { + final files = await api.getUserFiles(); + return _sort(files); + } catch (e, stackTrace) { + DebugLogger.error( + 'files-failed', + scope: 'files', + error: e, + stackTrace: stackTrace, + ); + return const []; + } + } + + List _sort(List input) { + final sorted = [...input]; + sorted.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); + return List.unmodifiable(sorted); } } @@ -1964,21 +2016,75 @@ Future fileContent(Ref ref, String fileId) async { // Knowledge Base providers @Riverpod(keepAlive: true) -Future> knowledgeBases(Ref ref) async { - // Protected: require authentication - if (!ref.read(isAuthenticatedProvider2)) { - DebugLogger.log('skip-unauthed', scope: 'knowledge'); - return []; +class KnowledgeBases extends _$KnowledgeBases { + @override + Future> build() async { + if (!ref.watch(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'knowledge'); + return const []; + } + final api = ref.watch(apiServiceProvider); + if (api == null) return const []; + return _load(api); } - final api = ref.watch(apiServiceProvider); - if (api == null) return []; - try { - final kbData = await api.getKnowledgeBases(); - return kbData.map((data) => KnowledgeBase.fromJson(data)).toList(); - } catch (e) { - DebugLogger.error('knowledge-bases-failed', scope: 'knowledge', error: e); - return []; + Future refresh() async { + if (!ref.read(isAuthenticatedProvider2)) { + state = const AsyncData>([]); + return; + } + final api = ref.read(apiServiceProvider); + if (api == null) { + state = const AsyncData>([]); + return; + } + final result = await AsyncValue.guard(() => _load(api)); + if (!ref.mounted) return; + state = result; + } + + void upsert(KnowledgeBase knowledgeBase) { + final current = state.asData?.value ?? const []; + final updated = [...current]; + final index = updated.indexWhere( + (existing) => existing.id == knowledgeBase.id, + ); + if (index >= 0) { + updated[index] = knowledgeBase; + } else { + updated.add(knowledgeBase); + } + state = AsyncData>(_sort(updated)); + } + + void remove(String id) { + final current = state.asData?.value; + if (current == null) return; + final updated = current + .where((knowledgeBase) => knowledgeBase.id != id) + .toList(growable: true); + state = AsyncData>(_sort(updated)); + } + + Future> _load(ApiService api) async { + try { + final knowledgeBases = await api.getKnowledgeBases(); + return _sort(knowledgeBases); + } catch (e, stackTrace) { + DebugLogger.error( + 'knowledge-bases-failed', + scope: 'knowledge', + error: e, + stackTrace: stackTrace, + ); + return const []; + } + } + + List _sort(List input) { + final sorted = [...input]; + sorted.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); + return List.unmodifiable(sorted); } } @@ -1993,8 +2099,7 @@ Future> knowledgeBaseItems(Ref ref, String kbId) async { if (api == null) return []; try { - final itemsData = await api.getKnowledgeBaseItems(kbId); - return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList(); + return await api.getKnowledgeBaseItems(kbId); } catch (e) { DebugLogger.error('knowledge-items-failed', scope: 'knowledge', error: e); return []; diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index b588f7b..d010765 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -13,6 +13,8 @@ import '../models/user.dart'; import '../models/model.dart'; import '../models/conversation.dart'; import '../models/chat_message.dart'; +import '../models/file_info.dart'; +import '../models/knowledge_base.dart'; import '../auth/api_auth_interceptor.dart'; import '../error/api_error_interceptor.dart'; // Tool-call details are parsed in the UI layer to render collapsible blocks @@ -931,6 +933,18 @@ class ApiService { .toList(growable: false); } + Future>> _normalizeList( + List raw, { + required String debugLabel, + }) { + return _workerManager + .schedule, List>>( + _normalizeMapListWorker, + {'list': raw}, + debugLabel: debugLabel, + ); + } + // Tools - Check available tools on server Future>> getAvailableTools() async { _traceApi('Fetching available tools'); @@ -1117,14 +1131,18 @@ class ApiService { return response.data as Map; } - Future>> getUserFiles() async { + Future> getUserFiles() async { _traceApi('Fetching user files'); final response = await _dio.get('/api/v1/files/'); final data = response.data; if (data is List) { - return data.cast>(); + final normalized = await _normalizeList( + data, + debugLabel: 'parse_file_list', + ); + return normalized.map(FileInfo.fromJson).toList(growable: false); } - return []; + return const []; } // Enhanced File Operations @@ -1262,14 +1280,18 @@ class ApiService { } // Knowledge Base - Future>> getKnowledgeBases() async { + Future> getKnowledgeBases() async { _traceApi('Fetching knowledge bases'); final response = await _dio.get('/api/v1/knowledge/'); final data = response.data; if (data is List) { - return data.cast>(); + final normalized = await _normalizeList( + data, + debugLabel: 'parse_knowledge_bases', + ); + return normalized.map(KnowledgeBase.fromJson).toList(growable: false); } - return []; + return const []; } Future> createKnowledgeBase({ @@ -1304,16 +1326,20 @@ class ApiService { await _dio.delete('/api/v1/knowledge/$id'); } - Future>> getKnowledgeBaseItems( + Future> getKnowledgeBaseItems( String knowledgeBaseId, ) async { _traceApi('Fetching knowledge base items: $knowledgeBaseId'); final response = await _dio.get('/api/v1/knowledge/$knowledgeBaseId/items'); final data = response.data; if (data is List) { - return data.cast>(); + final normalized = await _normalizeList( + data, + debugLabel: 'parse_kb_items', + ); + return normalized.map(KnowledgeBaseItem.fromJson).toList(growable: false); } - return []; + return const []; } Future> addKnowledgeBaseItem( @@ -3268,3 +3294,19 @@ class ApiService { // Legacy streaming wrapper methods removed } + +List> _normalizeMapListWorker( + Map payload, +) { + final raw = payload['list']; + if (raw is! List) { + return const >[]; + } + final normalized = >[]; + for (final entry in raw) { + if (entry is Map) { + normalized.add(Map.from(entry)); + } + } + return normalized; +}