From bc2f60e685571c89f5497f1c1ffb07eac82ff3d9 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Wed, 20 Aug 2025 23:42:31 +0530 Subject: [PATCH] feat: generated images parsed --- lib/core/models/chat_message.dart | 1 + lib/core/services/api_service.dart | 36 +++++++--- .../widgets/assistant_message_widget.dart | 60 ++++++++++++++++ .../widgets/enhanced_image_attachment.dart | 69 ++++++++++++++++++- 4 files changed, 156 insertions(+), 10 deletions(-) diff --git a/lib/core/models/chat_message.dart b/lib/core/models/chat_message.dart index 92eed78..353a42f 100644 --- a/lib/core/models/chat_message.dart +++ b/lib/core/models/chat_message.dart @@ -13,6 +13,7 @@ sealed class ChatMessage with _$ChatMessage { String? model, @Default(false) bool isStreaming, List? attachmentIds, + List>? files, // For generated images Map? metadata, List>? sources, Map? usage, diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 5f10b8c..521fabc 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -27,6 +27,9 @@ class ApiService { // Public getter for dio instance Dio get dio => _dio; + + // Public getter for base URL + String get baseUrl => serverConfig.url; // Callback to notify when auth token becomes invalid void Function()? onAuthTokenInvalid; @@ -718,18 +721,34 @@ class ApiService { role = 'user'; } - // Parse attachments from 'files' field + // Parse attachments and generated images from 'files' field List? attachmentIds; + List>? files; + if (msgData['files'] != null) { final filesList = msgData['files'] as List; - attachmentIds = filesList - .where((file) => file is Map && file['file_id'] != null) - .map((file) => file['file_id'] as String) - .toList(); - - if (attachmentIds.isEmpty) { - attachmentIds = null; + + // Separate user uploads (with file_id) from generated images (with type and url) + final userAttachments = []; + final generatedFiles = >[]; + + for (final file in filesList) { + if (file is Map) { + if (file['file_id'] != null) { + // User uploaded file + userAttachments.add(file['file_id'] as String); + } else if (file['type'] == 'image' && file['url'] != null) { + // Generated image + generatedFiles.add({ + 'type': file['type'], + 'url': file['url'], + }); + } + } } + + attachmentIds = userAttachments.isNotEmpty ? userAttachments : null; + files = generatedFiles.isNotEmpty ? generatedFiles : null; } return ChatMessage( @@ -739,6 +758,7 @@ class ApiService { timestamp: _parseTimestamp(msgData['timestamp']), model: msgData['model'] as String?, attachmentIds: attachmentIds, + files: files, ); } diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index d91cda7..274d561 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -287,6 +287,13 @@ class _AssistantMessageWidgetState extends ConsumerState const SizedBox(height: Spacing.md), ], + // Display generated images from files property + if (widget.message.files != null && + widget.message.files!.isNotEmpty) ...[ + _buildGeneratedImages(), + const SizedBox(height: Spacing.md), + ], + if (widget.isStreaming && (widget.message.content.trim().isEmpty || widget.message.content == '[TYPING_INDICATOR]')) @@ -400,6 +407,59 @@ class _AssistantMessageWidgetState extends ConsumerState } } + Widget _buildGeneratedImages() { + if (widget.message.files == null || widget.message.files!.isEmpty) { + return const SizedBox.shrink(); + } + + // Filter for image files + final imageFiles = widget.message.files! + .where((file) => file['type'] == 'image') + .toList(); + + if (imageFiles.isEmpty) { + return const SizedBox.shrink(); + } + + final imageCount = imageFiles.length; + + // Display generated images using EnhancedImageAttachment for consistency + if (imageCount == 1) { + final imageUrl = imageFiles[0]['url'] as String?; + if (imageUrl == null) return const SizedBox.shrink(); + + return ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: EnhancedImageAttachment( + attachmentId: imageUrl, // Pass URL directly as it handles URLs + isMarkdownFormat: true, + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 400), + ), + ); + } else { + return Wrap( + spacing: Spacing.sm, + runSpacing: Spacing.sm, + children: imageFiles.map((file) { + final imageUrl = file['url'] as String?; + if (imageUrl == null) return const SizedBox.shrink(); + + return ClipRRect( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + child: EnhancedImageAttachment( + attachmentId: imageUrl, // Pass URL directly + isMarkdownFormat: true, + constraints: BoxConstraints( + maxWidth: imageCount == 2 ? 245 : 160, + maxHeight: imageCount == 2 ? 245 : 160, + ), + ), + ); + }).toList(), + ); + } + } + Widget _buildTypingIndicator() { return Consumer( builder: (context, ref, child) { diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index c7ad857..9f68ea5 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../../core/providers/app_providers.dart'; +import '../../auth/providers/unified_auth_providers.dart'; // Global cache for image data to prevent reloading final _globalImageCache = {}; @@ -69,6 +70,32 @@ class _EnhancedImageAttachmentState } return; } + + // Check if this is a relative URL that needs base URL prepending + if (widget.attachmentId.startsWith('/')) { + // This is a relative URL, prepend the base URL + final api = ref.read(apiServiceProvider); + if (api != null) { + final fullUrl = api.baseUrl + widget.attachmentId; + _globalImageCache[widget.attachmentId] = fullUrl; + if (mounted) { + setState(() { + _cachedImageData = fullUrl; + _isLoading = false; + }); + } + return; + } else { + // If API service is not available, show error + if (mounted) { + setState(() { + _errorMessage = 'Unable to load image: API service not available'; + _isLoading = false; + }); + } + return; + } + } final api = ref.read(apiServiceProvider); if (api == null) { @@ -231,9 +258,28 @@ class _EnhancedImageAttachmentState } Widget _buildNetworkImage() { + // Get authentication headers if available + final api = ref.read(apiServiceProvider); + final authToken = ref.read(authTokenProvider3); + final headers = {}; + + // Add auth token from unified auth provider + if (authToken != null && authToken.isNotEmpty) { + headers['Authorization'] = 'Bearer $authToken'; + } else if (api?.serverConfig.apiKey != null && api!.serverConfig.apiKey!.isNotEmpty) { + // Fallback to API key from server config + headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}'; + } + + // Add any custom headers from server config + if (api != null && api.serverConfig.customHeaders.isNotEmpty) { + headers.addAll(api.serverConfig.customHeaders); + } + final imageWidget = CachedNetworkImage( imageUrl: _cachedImageData!, fit: BoxFit.cover, + httpHeaders: headers.isNotEmpty ? headers : null, placeholder: (context, url) => _buildLoadingState(), errorWidget: (context, url, error) { _errorMessage = error.toString(); @@ -312,7 +358,7 @@ class _EnhancedImageAttachmentState } } -class FullScreenImageViewer extends StatelessWidget { +class FullScreenImageViewer extends ConsumerWidget { final String imageData; final String tag; @@ -323,13 +369,32 @@ class FullScreenImageViewer extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { Widget imageWidget; if (imageData.startsWith('http')) { + // Get authentication headers if available + final api = ref.read(apiServiceProvider); + final authToken = ref.read(authTokenProvider3); + final headers = {}; + + // Add auth token from unified auth provider + if (authToken != null && authToken.isNotEmpty) { + headers['Authorization'] = 'Bearer $authToken'; + } else if (api?.serverConfig.apiKey != null && api!.serverConfig.apiKey!.isNotEmpty) { + // Fallback to API key from server config + headers['Authorization'] = 'Bearer ${api.serverConfig.apiKey}'; + } + + // Add any custom headers from server config + if (api != null && api.serverConfig.customHeaders.isNotEmpty) { + headers.addAll(api.serverConfig.customHeaders); + } + imageWidget = CachedNetworkImage( imageUrl: imageData, fit: BoxFit.contain, + httpHeaders: headers.isNotEmpty ? headers : null, placeholder: (context, url) => Center( child: CircularProgressIndicator( color: context.conduitTheme.buttonPrimary,