feat: folders implementation
This commit is contained in:
@@ -1,41 +1,63 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'folder.freezed.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
|
@freezed
|
||||||
sealed class Folder with _$Folder {
|
sealed class Folder with _$Folder {
|
||||||
const factory Folder({
|
const factory Folder({
|
||||||
required String id,
|
required String id,
|
||||||
required String name,
|
required String name,
|
||||||
@TimestampConverter() required DateTime createdAt,
|
|
||||||
@TimestampConverter() required DateTime updatedAt,
|
|
||||||
String? parentId,
|
String? parentId,
|
||||||
|
String? userId,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
@Default(false) bool isExpanded,
|
||||||
@Default([]) List<String> conversationIds,
|
@Default([]) List<String> conversationIds,
|
||||||
@Default([]) List<Folder> subfolders,
|
Map<String, dynamic>? meta,
|
||||||
@Default({}) Map<String, dynamic> metadata,
|
Map<String, dynamic>? data,
|
||||||
|
Map<String, dynamic>? items,
|
||||||
}) = _Folder;
|
}) = _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/user.dart';
|
||||||
import '../models/model.dart';
|
import '../models/model.dart';
|
||||||
import '../models/conversation.dart';
|
import '../models/conversation.dart';
|
||||||
|
import '../models/folder.dart';
|
||||||
import '../models/user_settings.dart';
|
import '../models/user_settings.dart';
|
||||||
import '../models/folder.dart';
|
import '../models/folder.dart';
|
||||||
import '../models/file_info.dart';
|
import '../models/file_info.dart';
|
||||||
@@ -278,7 +279,60 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
|
|||||||
foundation.debugPrint(
|
foundation.debugPrint(
|
||||||
'DEBUG: Successfully fetched ${conversations.length} conversations',
|
'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) {
|
} catch (e, stackTrace) {
|
||||||
foundation.debugPrint('DEBUG: Error fetching conversations: $e');
|
foundation.debugPrint('DEBUG: Error fetching conversations: $e');
|
||||||
foundation.debugPrint('DEBUG: Stack trace: $stackTrace');
|
foundation.debugPrint('DEBUG: Stack trace: $stackTrace');
|
||||||
@@ -649,13 +703,20 @@ final conversationSuggestionsProvider = FutureProvider<List<String>>((
|
|||||||
// Folders provider
|
// Folders provider
|
||||||
final foldersProvider = FutureProvider<List<Folder>>((ref) async {
|
final foldersProvider = FutureProvider<List<Folder>>((ref) async {
|
||||||
final api = ref.watch(apiServiceProvider);
|
final api = ref.watch(apiServiceProvider);
|
||||||
if (api == null) return [];
|
if (api == null) {
|
||||||
|
foundation.debugPrint('DEBUG: No API service available for folders');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
foundation.debugPrint('DEBUG: Fetching folders from API...');
|
||||||
final foldersData = await api.getFolders();
|
final foldersData = await api.getFolders();
|
||||||
return foldersData
|
foundation.debugPrint('DEBUG: Raw folders data: $foldersData');
|
||||||
|
final folders = foldersData
|
||||||
.map((folderData) => Folder.fromJson(folderData))
|
.map((folderData) => Folder.fromJson(folderData))
|
||||||
.toList();
|
.toList();
|
||||||
|
foundation.debugPrint('DEBUG: Parsed ${folders.length} folders');
|
||||||
|
return folders;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
foundation.debugPrint('DEBUG: Error fetching folders: $e');
|
foundation.debugPrint('DEBUG: Error fetching folders: $e');
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -404,6 +404,17 @@ class ApiService {
|
|||||||
// Process regular conversations (excluding pinned and archived ones)
|
// Process regular conversations (excluding pinned and archived ones)
|
||||||
for (final chatData in regularChatList) {
|
for (final chatData in regularChatList) {
|
||||||
try {
|
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);
|
final conversation = _parseOpenWebUIChat(chatData);
|
||||||
// Only add if not already added as pinned or archived
|
// Only add if not already added as pinned or archived
|
||||||
if (!pinnedIds.contains(conversation.id) &&
|
if (!pinnedIds.contains(conversation.id) &&
|
||||||
@@ -478,6 +489,11 @@ class ApiService {
|
|||||||
final shareId = chatData['share_id'] as String?;
|
final shareId = chatData['share_id'] as String?;
|
||||||
final folderId = chatData['folder_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(
|
debugPrint(
|
||||||
'DEBUG: Parsed conversation $id: pinned=$pinned, archived=$archived',
|
'DEBUG: Parsed conversation $id: pinned=$pinned, archived=$archived',
|
||||||
);
|
);
|
||||||
@@ -929,14 +945,25 @@ class ApiService {
|
|||||||
|
|
||||||
// Folders
|
// Folders
|
||||||
Future<List<Map<String, dynamic>>> getFolders() async {
|
Future<List<Map<String, dynamic>>> getFolders() async {
|
||||||
debugPrint('DEBUG: Fetching folders');
|
try {
|
||||||
|
debugPrint('DEBUG: Fetching folders from /api/v1/folders/');
|
||||||
final response = await _dio.get('/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;
|
final data = response.data;
|
||||||
if (data is List) {
|
if (data is List) {
|
||||||
|
debugPrint('DEBUG: Found ${data.length} folders');
|
||||||
return data.cast<Map<String, dynamic>>();
|
return data.cast<Map<String, dynamic>>();
|
||||||
}
|
} else {
|
||||||
|
debugPrint('DEBUG: Response data is not a list: ${data.runtimeType}');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('DEBUG: Error in getFolders: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> createFolder({
|
Future<Map<String, dynamic>> createFolder({
|
||||||
required String name,
|
required String name,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../features/auth/views/connect_signin_page.dart';
|
|||||||
import '../../features/settings/views/searchable_settings_page.dart';
|
import '../../features/settings/views/searchable_settings_page.dart';
|
||||||
import '../../features/profile/views/profile_page.dart';
|
import '../../features/profile/views/profile_page.dart';
|
||||||
import '../../features/files/views/files_page.dart';
|
import '../../features/files/views/files_page.dart';
|
||||||
|
|
||||||
import '../../features/chat/views/conversation_search_page.dart';
|
import '../../features/chat/views/conversation_search_page.dart';
|
||||||
import '../../shared/widgets/themed_dialogs.dart';
|
import '../../shared/widgets/themed_dialogs.dart';
|
||||||
|
|
||||||
@@ -221,6 +222,8 @@ class NavigationService {
|
|||||||
page = const FilesPage();
|
page = const FilesPage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
case Routes.chatsList:
|
case Routes.chatsList:
|
||||||
page = const ChatsListPage();
|
page = const ChatsListPage();
|
||||||
break;
|
break;
|
||||||
@@ -246,5 +249,6 @@ class Routes {
|
|||||||
static const String serverConnection = '/server-connection';
|
static const String serverConnection = '/server-connection';
|
||||||
static const String search = '/search';
|
static const String search = '/search';
|
||||||
static const String files = '/files';
|
static const String files = '/files';
|
||||||
|
|
||||||
static const String chatsList = '/chats-list';
|
static const String chatsList = '/chats-list';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default error UI
|
// Default error UI
|
||||||
return Scaffold(
|
return Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Scaffold(
|
||||||
backgroundColor: context.conduitTheme.surfaceBackground,
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -145,6 +147,7 @@ class _ErrorBoundaryState extends ConsumerState<ErrorBoundary> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -546,6 +546,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
_navigateToFiles();
|
_navigateToFiles();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
Platform.isIOS ? CupertinoIcons.person : Icons.person_outline,
|
Platform.isIOS ? CupertinoIcons.person : Icons.person_outline,
|
||||||
@@ -582,6 +583,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
).push(MaterialPageRoute(builder: (context) => const FilesPage()));
|
).push(MaterialPageRoute(builder: (context) => const FilesPage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void _navigateToProfile() {
|
void _navigateToProfile() {
|
||||||
Navigator.of(
|
Navigator.of(
|
||||||
context,
|
context,
|
||||||
@@ -1229,6 +1232,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
},
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -1239,6 +1243,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: Spacing.xs),
|
const SizedBox(width: Spacing.xs),
|
||||||
@@ -1618,25 +1623,7 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Header
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: Spacing.sm),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Choose Model',
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.conduitTheme.textPrimary,
|
|
||||||
fontSize: AppTypography.headlineMedium,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Removed capabilities legend to reduce icon noise
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Search field
|
// Search field
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../../core/models/model.dart';
|
|||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../shared/theme/theme_extensions.dart';
|
import '../../../shared/theme/theme_extensions.dart';
|
||||||
import '../../../shared/theme/app_theme.dart';
|
import '../../../shared/theme/app_theme.dart';
|
||||||
|
import '../../../shared/widgets/conduit_components.dart';
|
||||||
|
|
||||||
class ModelSelectorPage extends ConsumerStatefulWidget {
|
class ModelSelectorPage extends ConsumerStatefulWidget {
|
||||||
const ModelSelectorPage({super.key});
|
const ModelSelectorPage({super.key});
|
||||||
@@ -53,17 +54,27 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final modelsAsync = ref.watch(modelsProvider);
|
final modelsAsync = ref.watch(modelsProvider);
|
||||||
final selectedModel = ref.watch(selectedModelProvider);
|
final selectedModel = ref.watch(selectedModelProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Select Model'),
|
backgroundColor: context.conduitTheme.surfaceBackground,
|
||||||
leading: IconButton(
|
elevation: Elevation.none,
|
||||||
icon: Icon(Platform.isIOS ? CupertinoIcons.back : Icons.arrow_back),
|
scrolledUnderElevation: Elevation.none,
|
||||||
|
leading: ConduitIconButton(
|
||||||
|
icon: Platform.isIOS
|
||||||
|
? CupertinoIcons.back
|
||||||
|
: Icons.arrow_back_rounded,
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
),
|
),
|
||||||
|
title: Text(
|
||||||
|
'Select Model',
|
||||||
|
style: AppTypography.headlineMediumStyle.copyWith(
|
||||||
|
color: context.conduitTheme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -71,10 +82,10 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(Spacing.md),
|
padding: const EdgeInsets.all(Spacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.scaffoldBackgroundColor,
|
color: context.conduitTheme.surfaceBackground,
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: theme.dividerColor.withValues(alpha: 0.1),
|
color: context.conduitTheme.dividerColor.withValues(alpha: 0.1),
|
||||||
width: BorderWidth.regular,
|
width: BorderWidth.regular,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -96,27 +107,22 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.cube_box
|
? CupertinoIcons.cube_box
|
||||||
: Icons.view_in_ar,
|
: Icons.view_in_ar,
|
||||||
size: 64,
|
size: IconSize.xxl,
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.iconSecondary,
|
||||||
alpha: 0.3,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: Spacing.lg),
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
Text(
|
Text(
|
||||||
'No models available',
|
'No models available',
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.textPrimary,
|
||||||
alpha: 0.6,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
Text(
|
Text(
|
||||||
'Please check your Open-WebUI configuration',
|
'Please check your Open-WebUI configuration',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: AppTypography.bodyMediumStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.textSecondary,
|
||||||
alpha: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -132,28 +138,23 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
Icon(
|
Icon(
|
||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.search
|
? CupertinoIcons.search
|
||||||
: Icons.search_off,
|
: Icons.search_rounded,
|
||||||
size: 64,
|
size: IconSize.xxl,
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.iconSecondary,
|
||||||
alpha: 0.3,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: Spacing.lg),
|
||||||
const SizedBox(height: Spacing.md),
|
|
||||||
Text(
|
Text(
|
||||||
'No models found',
|
'No models found',
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.textPrimary,
|
||||||
alpha: 0.6,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
Text(
|
Text(
|
||||||
'Try searching with different keywords',
|
'Try searching with different keywords',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: AppTypography.bodyMediumStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.textSecondary,
|
||||||
alpha: 0.5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -183,9 +184,10 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
group.title!,
|
group.title!,
|
||||||
style: theme.textTheme.titleSmall?.copyWith(
|
style: AppTypography.labelStyle.copyWith(
|
||||||
color: theme.colorScheme.primary,
|
color: context.conduitTheme.textSecondary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -206,7 +208,13 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
context.conduitTheme.buttonPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
error: (error, _) => Center(
|
error: (error, _) => Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -214,32 +222,48 @@ class _ModelSelectorPageState extends ConsumerState<ModelSelectorPage> {
|
|||||||
Icon(
|
Icon(
|
||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.exclamationmark_triangle
|
? CupertinoIcons.exclamationmark_triangle
|
||||||
: Icons.error_outline,
|
: Icons.error_rounded,
|
||||||
size: 48,
|
size: IconSize.xxl,
|
||||||
color: theme.colorScheme.error,
|
color: context.conduitTheme.error,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.lg),
|
||||||
Text(
|
Text(
|
||||||
'Failed to load models',
|
'Failed to load models',
|
||||||
style: theme.textTheme.titleMedium,
|
style: AppTypography.headlineSmallStyle.copyWith(
|
||||||
|
color: context.conduitTheme.textPrimary,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.sm),
|
const SizedBox(height: Spacing.sm),
|
||||||
Text(
|
Text(
|
||||||
error.toString(),
|
'Please try again later',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: AppTypography.bodyMediumStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(
|
color: context.conduitTheme.textSecondary,
|
||||||
alpha: 0.6,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: Spacing.lg),
|
const SizedBox(height: Spacing.xl),
|
||||||
ElevatedButton.icon(
|
ElevatedButton(
|
||||||
onPressed: () => ref.refresh(modelsProvider),
|
onPressed: () => ref.refresh(modelsProvider),
|
||||||
icon: Icon(
|
style: ElevatedButton.styleFrom(
|
||||||
Platform.isIOS ? CupertinoIcons.refresh : Icons.refresh,
|
backgroundColor: context.conduitTheme.buttonPrimary,
|
||||||
|
foregroundColor: context.conduitTheme.buttonPrimaryText,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.buttonPadding,
|
||||||
|
vertical: Spacing.md,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(AppBorderRadius.button),
|
||||||
|
),
|
||||||
|
elevation: Elevation.none,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Retry',
|
||||||
|
style: AppTypography.labelStyle.copyWith(
|
||||||
|
color: context.conduitTheme.buttonPrimaryText,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
label: const Text('Retry'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -339,20 +363,18 @@ class ModelTile extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
elevation: isSelected ? 2 : 0,
|
elevation: isSelected ? 2 : 0,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? theme.colorScheme.primary.withValues(alpha: 0.1)
|
? context.conduitTheme.buttonPrimary.withValues(alpha: 0.1)
|
||||||
: null,
|
: context.conduitTheme.cardBackground,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
borderRadius: BorderRadius.circular(AppBorderRadius.md),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? theme.colorScheme.primary
|
? context.conduitTheme.buttonPrimary
|
||||||
: theme.dividerColor.withValues(alpha: 0.3),
|
: context.conduitTheme.dividerColor.withValues(alpha: 0.3),
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -369,9 +391,11 @@ class ModelTile extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
model.name,
|
model.name,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: AppTypography.bodyLargeStyle.copyWith(
|
||||||
fontWeight: isSelected ? FontWeight.w600 : null,
|
fontWeight: FontWeight.w600,
|
||||||
color: isSelected ? theme.colorScheme.primary : null,
|
color: isSelected
|
||||||
|
? context.conduitTheme.buttonPrimary
|
||||||
|
: context.conduitTheme.textPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -380,7 +404,7 @@ class ModelTile extends StatelessWidget {
|
|||||||
Platform.isIOS
|
Platform.isIOS
|
||||||
? CupertinoIcons.checkmark_circle_fill
|
? CupertinoIcons.checkmark_circle_fill
|
||||||
: Icons.check_circle,
|
: Icons.check_circle,
|
||||||
color: theme.colorScheme.primary,
|
color: context.conduitTheme.buttonPrimary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -388,8 +412,8 @@ class ModelTile extends StatelessWidget {
|
|||||||
const SizedBox(height: Spacing.xs),
|
const SizedBox(height: Spacing.xs),
|
||||||
Text(
|
Text(
|
||||||
model.description!,
|
model.description!,
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: AppTypography.bodySmallStyle.copyWith(
|
||||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
color: context.conduitTheme.textSecondary,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -303,7 +303,10 @@ class _ModernMessageBubbleState extends ConsumerState<ModernMessageBubble>
|
|||||||
left: Spacing.xxxl,
|
left: Spacing.xxxl,
|
||||||
right: Spacing.xs,
|
right: Spacing.xs,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
@@ -352,19 +355,21 @@ class _ModernMessageBubbleState extends ConsumerState<ModernMessageBubble>
|
|||||||
context.conduitTheme.chatBubbleUserText,
|
context.conduitTheme.chatBubbleUserText,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
// Action buttons for user messages
|
// Action buttons below the message bubble
|
||||||
if (_showActions) ...[
|
if (_showActions) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.sm),
|
||||||
_buildUserActionButtons(),
|
_buildUserActionButtons(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.animate()
|
.animate()
|
||||||
.fadeIn(duration: AnimationDuration.messageAppear)
|
.fadeIn(duration: AnimationDuration.messageAppear)
|
||||||
@@ -461,18 +466,18 @@ class _ModernMessageBubbleState extends ConsumerState<ModernMessageBubble>
|
|||||||
] else
|
] else
|
||||||
// Fallback: show empty state for non-streaming empty messages
|
// Fallback: show empty state for non-streaming empty messages
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons below the message content
|
||||||
if (_showActions) ...[
|
if (_showActions) ...[
|
||||||
const SizedBox(height: Spacing.md),
|
const SizedBox(height: Spacing.sm),
|
||||||
_buildActionButtons(),
|
_buildActionButtons(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.animate()
|
.animate()
|
||||||
.fadeIn(duration: AnimationDuration.messageAppear)
|
.fadeIn(duration: AnimationDuration.messageAppear)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -483,8 +483,10 @@ class ConduitEmptyState extends StatelessWidget {
|
|||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg),
|
padding: EdgeInsets.all(isCompact ? Spacing.md : Spacing.lg),
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md,
|
width: isCompact ? IconSize.xxl : IconSize.xxl + Spacing.md,
|
||||||
@@ -515,6 +517,8 @@ class ConduitEmptyState extends StatelessWidget {
|
|||||||
color: context.conduitTheme.textSecondary,
|
color: context.conduitTheme.textSecondary,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: isCompact ? 2 : null,
|
||||||
|
overflow: isCompact ? TextOverflow.ellipsis : null,
|
||||||
),
|
),
|
||||||
if (action != null) ...[
|
if (action != null) ...[
|
||||||
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
|
SizedBox(height: isCompact ? Spacing.md : Spacing.lg),
|
||||||
@@ -523,6 +527,7 @@ class ConduitEmptyState extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user