feat: folders implementation
This commit is contained in:
@@ -1,41 +1,63 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'folder.freezed.dart';
|
||||
part 'folder.g.dart';
|
||||
|
||||
// Timestamp converter for Unix timestamps
|
||||
class TimestampConverter implements JsonConverter<DateTime, dynamic> {
|
||||
const TimestampConverter();
|
||||
|
||||
@override
|
||||
DateTime fromJson(dynamic json) {
|
||||
if (json is String) {
|
||||
return DateTime.parse(json);
|
||||
} else if (json is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(json * 1000);
|
||||
} else {
|
||||
throw ArgumentError('Invalid date format: $json');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
dynamic toJson(DateTime object) {
|
||||
return object.millisecondsSinceEpoch ~/ 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class Folder with _$Folder {
|
||||
const factory Folder({
|
||||
required String id,
|
||||
required String name,
|
||||
@TimestampConverter() required DateTime createdAt,
|
||||
@TimestampConverter() required DateTime updatedAt,
|
||||
String? parentId,
|
||||
String? userId,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
@Default(false) bool isExpanded,
|
||||
@Default([]) List<String> conversationIds,
|
||||
@Default([]) List<Folder> subfolders,
|
||||
@Default({}) Map<String, dynamic> metadata,
|
||||
Map<String, dynamic>? meta,
|
||||
Map<String, dynamic>? data,
|
||||
Map<String, dynamic>? items,
|
||||
}) = _Folder;
|
||||
|
||||
factory Folder.fromJson(Map<String, dynamic> json) => _$FolderFromJson(json);
|
||||
factory Folder.fromJson(Map<String, dynamic> json) {
|
||||
// Extract conversation IDs from items.chats if available
|
||||
final items = json['items'] as Map<String, dynamic>?;
|
||||
final chats = items?['chats'] as List?;
|
||||
|
||||
// Handle both string IDs and conversation objects
|
||||
final conversationIds = chats?.map((chat) {
|
||||
if (chat is String) {
|
||||
return chat;
|
||||
} else if (chat is Map<String, dynamic>) {
|
||||
return chat['id'] as String? ?? '';
|
||||
}
|
||||
return '';
|
||||
}).where((id) => id.isNotEmpty).toList().cast<String>() ?? <String>[];
|
||||
|
||||
// Handle Unix timestamp conversion
|
||||
DateTime? parseTimestamp(dynamic timestamp) {
|
||||
if (timestamp == null) return null;
|
||||
if (timestamp is int) {
|
||||
return DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
}
|
||||
if (timestamp is String) {
|
||||
return DateTime.parse(timestamp);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the modified JSON with proper field mapping
|
||||
return Folder(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
parentId: json['parent_id'] as String?,
|
||||
userId: json['user_id'] as String?,
|
||||
createdAt: parseTimestamp(json['created_at']),
|
||||
updatedAt: parseTimestamp(json['updated_at']),
|
||||
isExpanded: json['is_expanded'] as bool? ?? false,
|
||||
conversationIds: conversationIds,
|
||||
meta: json['meta'] as Map<String, dynamic>?,
|
||||
data: json['data'] as Map<String, dynamic>?,
|
||||
items: json['items'] as Map<String, dynamic>?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../models/server_config.dart';
|
||||
import '../models/user.dart';
|
||||
import '../models/model.dart';
|
||||
import '../models/conversation.dart';
|
||||
import '../models/folder.dart';
|
||||
import '../models/user_settings.dart';
|
||||
import '../models/folder.dart';
|
||||
import '../models/file_info.dart';
|
||||
@@ -278,7 +279,60 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
||||
foundation.debugPrint(
|
||||
'DEBUG: Successfully fetched ${conversations.length} conversations',
|
||||
);
|
||||
return conversations;
|
||||
|
||||
// Also fetch folder information and update conversations with folder IDs
|
||||
try {
|
||||
final foldersData = await api.getFolders();
|
||||
foundation.debugPrint('DEBUG: Fetched ${foldersData.length} folders for conversation mapping');
|
||||
|
||||
// Parse folder data into Folder objects
|
||||
final folders = foldersData.map((folderData) => Folder.fromJson(folderData)).toList();
|
||||
|
||||
// Create a map of conversation ID to folder ID
|
||||
final conversationToFolder = <String, String>{};
|
||||
for (final folder in folders) {
|
||||
for (final conversationId in folder.conversationIds) {
|
||||
conversationToFolder[conversationId] = folder.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Update conversations with folder IDs and add missing folder conversations
|
||||
final updatedConversations = <Conversation>[];
|
||||
final existingConversationIds = conversations.map((c) => c.id).toSet();
|
||||
|
||||
for (final conversation in conversations) {
|
||||
final folderId = conversationToFolder[conversation.id];
|
||||
if (folderId != null) {
|
||||
updatedConversations.add(conversation.copyWith(folderId: folderId));
|
||||
foundation.debugPrint('DEBUG: Updated conversation ${conversation.id.substring(0, 8)} with folderId: $folderId');
|
||||
} else {
|
||||
updatedConversations.add(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
// Add conversations that are in folders but not in the main list
|
||||
for (final folder in folders) {
|
||||
for (final conversationId in folder.conversationIds) {
|
||||
if (!existingConversationIds.contains(conversationId)) {
|
||||
// Create a minimal conversation object for folder-only conversations
|
||||
// We'll need to fetch the full conversation details
|
||||
try {
|
||||
final fullConversation = await api.getConversation(conversationId);
|
||||
updatedConversations.add(fullConversation.copyWith(folderId: folder.id));
|
||||
foundation.debugPrint('DEBUG: Added folder conversation ${conversationId.substring(0, 8)} from folder ${folder.name}');
|
||||
} catch (e) {
|
||||
foundation.debugPrint('DEBUG: Failed to fetch folder conversation $conversationId: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foundation.debugPrint('DEBUG: Final conversation count: ${updatedConversations.length}');
|
||||
return updatedConversations;
|
||||
} catch (e) {
|
||||
foundation.debugPrint('DEBUG: Failed to fetch folder information: $e');
|
||||
return conversations; // Return original conversations if folder fetch fails
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
foundation.debugPrint('DEBUG: Error fetching conversations: $e');
|
||||
foundation.debugPrint('DEBUG: Stack trace: $stackTrace');
|
||||
@@ -649,13 +703,20 @@ final conversationSuggestionsProvider = FutureProvider<List<String>>((
|
||||
// Folders provider
|
||||
final foldersProvider = FutureProvider<List<Folder>>((ref) async {
|
||||
final api = ref.watch(apiServiceProvider);
|
||||
if (api == null) return [];
|
||||
if (api == null) {
|
||||
foundation.debugPrint('DEBUG: No API service available for folders');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
foundation.debugPrint('DEBUG: Fetching folders from API...');
|
||||
final foldersData = await api.getFolders();
|
||||
return foldersData
|
||||
foundation.debugPrint('DEBUG: Raw folders data: $foldersData');
|
||||
final folders = foldersData
|
||||
.map((folderData) => Folder.fromJson(folderData))
|
||||
.toList();
|
||||
foundation.debugPrint('DEBUG: Parsed ${folders.length} folders');
|
||||
return folders;
|
||||
} catch (e) {
|
||||
foundation.debugPrint('DEBUG: Error fetching folders: $e');
|
||||
return [];
|
||||
|
||||
@@ -404,6 +404,17 @@ class ApiService {
|
||||
// Process regular conversations (excluding pinned and archived ones)
|
||||
for (final chatData in regularChatList) {
|
||||
try {
|
||||
// Debug: Check if conversation has folder_id in raw data
|
||||
if (chatData.containsKey('folder_id') && chatData['folder_id'] != null) {
|
||||
debugPrint('🔍 DEBUG: Found conversation with folder_id in raw data: ${chatData['id']} -> ${chatData['folder_id']}');
|
||||
}
|
||||
|
||||
// Debug: Check what fields are available in the chat data
|
||||
if (regularChatList.indexOf(chatData) == 0) {
|
||||
debugPrint('🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}');
|
||||
debugPrint('🔍 DEBUG: Sample chat data: ${chatData.toString().substring(0, 200)}...');
|
||||
}
|
||||
|
||||
final conversation = _parseOpenWebUIChat(chatData);
|
||||
// Only add if not already added as pinned or archived
|
||||
if (!pinnedIds.contains(conversation.id) &&
|
||||
@@ -477,6 +488,11 @@ class ApiService {
|
||||
final archived = chatData['archived'] as bool? ?? false;
|
||||
final shareId = chatData['share_id'] as String?;
|
||||
final folderId = chatData['folder_id'] as String?;
|
||||
|
||||
// Debug logging for folder assignment
|
||||
if (folderId != null) {
|
||||
debugPrint('🔍 DEBUG: Conversation ${id.substring(0, 8)} has folderId: $folderId');
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Parsed conversation $id: pinned=$pinned, archived=$archived',
|
||||
@@ -929,13 +945,24 @@ class ApiService {
|
||||
|
||||
// Folders
|
||||
Future<List<Map<String, dynamic>>> getFolders() async {
|
||||
debugPrint('DEBUG: Fetching folders');
|
||||
final response = await _dio.get('/api/v1/folders/');
|
||||
final data = response.data;
|
||||
if (data is List) {
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
try {
|
||||
debugPrint('DEBUG: Fetching folders from /api/v1/folders/');
|
||||
final response = await _dio.get('/api/v1/folders/');
|
||||
debugPrint('DEBUG: Folders response status: ${response.statusCode}');
|
||||
debugPrint('DEBUG: Folders response data: ${response.data}');
|
||||
|
||||
final data = response.data;
|
||||
if (data is List) {
|
||||
debugPrint('DEBUG: Found ${data.length} folders');
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
debugPrint('DEBUG: Response data is not a list: ${data.runtimeType}');
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Error in getFolders: $e');
|
||||
rethrow;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createFolder({
|
||||
|
||||
@@ -6,6 +6,7 @@ import '../../features/auth/views/connect_signin_page.dart';
|
||||
import '../../features/settings/views/searchable_settings_page.dart';
|
||||
import '../../features/profile/views/profile_page.dart';
|
||||
import '../../features/files/views/files_page.dart';
|
||||
|
||||
import '../../features/chat/views/conversation_search_page.dart';
|
||||
import '../../shared/widgets/themed_dialogs.dart';
|
||||
|
||||
@@ -221,6 +222,8 @@ class NavigationService {
|
||||
page = const FilesPage();
|
||||
break;
|
||||
|
||||
|
||||
|
||||
case Routes.chatsList:
|
||||
page = const ChatsListPage();
|
||||
break;
|
||||
@@ -246,5 +249,6 @@ class Routes {
|
||||
static const String serverConnection = '/server-connection';
|
||||
static const String search = '/search';
|
||||
static const String files = '/files';
|
||||
|
||||
static const String chatsList = '/chats-list';
|
||||
}
|
||||
|
||||
@@ -105,8 +105,10 @@ class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
|
||||
}
|
||||
|
||||
// Default error UI
|
||||
return Scaffold(
|
||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Scaffold(
|
||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
@@ -145,7 +147,8 @@ class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap child in error handler
|
||||
|
||||
Reference in New Issue
Block a user