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