From 7ab1ec3acfa879003c9783bef6eb0e2747a3a32a Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Mon, 22 Sep 2025 23:17:23 +0530 Subject: [PATCH] fix: image and files previews on the web --- lib/core/services/api_service.dart | 79 ++++---- .../chat/providers/chat_providers.dart | 127 +++++++++---- .../widgets/assistant_message_widget.dart | 88 +++++++-- .../chat/widgets/user_message_bubble.dart | 173 +++++++++++++----- openwebui-src | 2 +- 5 files changed, 336 insertions(+), 133 deletions(-) diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 1233e10..df67b30 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -788,24 +788,44 @@ class ApiService { if (effectiveFiles != null) { final filesList = effectiveFiles as List; - // Separate user uploads (with file_id) from generated images (with type and url) + // Handle different file formats from OpenWebUI final userAttachments = []; - final generatedFiles = >[]; + final allFiles = >[]; for (final file in filesList) { if (file is Map) { if (file['file_id'] != null) { - // User uploaded file + // User uploaded file with file_id (legacy format) 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']}); + } else if (file['type'] != null && file['url'] != null) { + // File with type and url (OpenWebUI format) + final fileMap = { + 'type': file['type'], + 'url': file['url'], + }; + + // Add optional fields if present + if (file['name'] != null) fileMap['name'] = file['name']; + if (file['size'] != null) fileMap['size'] = file['size']; + + allFiles.add(fileMap); + + // If this is a user-uploaded file (URL contains file ID), also extract the ID + final url = file['url'] as String; + if (url.contains('/api/v1/files/') && url.contains('/content')) { + final fileIdMatch = RegExp( + r'/api/v1/files/([^/]+)/content', + ).firstMatch(url); + if (fileIdMatch != null) { + userAttachments.add(fileIdMatch.group(1)!); + } + } } } } attachmentIds = userAttachments.isNotEmpty ? userAttachments : null; - files = generatedFiles.isNotEmpty ? generatedFiles : null; + files = allFiles.isNotEmpty ? allFiles : null; } return ChatMessage( @@ -1084,8 +1104,8 @@ class ApiService { return _parseFullOpenWebUIChat(responseData); } - // Update conversation with full chat data including all messages - Future updateConversationWithMessages( + // Sync conversation messages to ensure WebUI can load conversation history + Future syncConversationMessages( String conversationId, List messages, { String? title, @@ -1093,7 +1113,7 @@ class ApiService { String? systemPrompt, }) async { debugPrint( - 'DEBUG: Updating conversation $conversationId with ${messages.length} messages', + 'DEBUG: Syncing conversation $conversationId with ${messages.length} messages', ); // Build messages map and array in OpenWebUI format @@ -1105,21 +1125,11 @@ class ApiService { for (final msg in messages) { final messageId = msg.id; - // Build message for messages map (history.messages) - final List> combinedFilesMap = []; - if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) { - for (final id in msg.attachmentIds!) { - if (id.startsWith('data:') || id.startsWith('http')) { - combinedFilesMap.add({'type': 'image', 'url': id}); - } else { - combinedFilesMap.add({'file_id': id}); - } - } - } - if (msg.files != null && msg.files!.isNotEmpty) { - combinedFilesMap.addAll(msg.files!); - } + // Use the properly formatted files array for WebUI display + // The msg.files array already contains all attachments in the correct format + final List> combinedFilesMap = msg.files ?? []; + // Build message for messages map (history.messages) messagesMap[messageId] = { 'id': messageId, 'parentId': previousId, @@ -1141,21 +1151,10 @@ class ApiService { (messagesMap[previousId]['childrenIds'] as List).add(messageId); } - // Build message for messages array - final List> combinedFilesArray = []; - if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) { - for (final id in msg.attachmentIds!) { - if (id.startsWith('data:') || id.startsWith('http')) { - combinedFilesArray.add({'type': 'image', 'url': id}); - } else { - combinedFilesArray.add({'file_id': id}); - } - } - } - if (msg.files != null && msg.files!.isNotEmpty) { - combinedFilesArray.addAll(msg.files!); - } + // Use the same properly formatted files array for messages array + final List> combinedFilesArray = msg.files ?? []; + // Build message for messages array messagesArray.add({ 'id': messageId, 'parentId': previousId, @@ -1193,12 +1192,12 @@ class ApiService { }, }; - debugPrint('DEBUG: Updating chat with OpenWebUI format data using POST'); + debugPrint('DEBUG: Syncing chat with OpenWebUI format data using POST'); // OpenWebUI uses POST not PUT for updating chats await _dio.post('/api/v1/chats/$conversationId', data: chatData); - DebugLogger.log('Update conversation response received successfully'); + DebugLogger.log('Sync conversation response received successfully'); } Future updateConversation( diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index ef372f5..892cd45 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -525,9 +525,6 @@ Future _preseedAssistantAndPersist( required String modelId, String? systemPrompt, }) async { - final api = ref.read(apiServiceProvider); - final activeConv = ref.read(activeConversationProvider); - // Choose id: reuse existing if provided, else create new final String assistantMessageId = (existingAssistantId != null && existingAssistantId.isNotEmpty) @@ -564,22 +561,26 @@ Future _preseedAssistantAndPersist( } catch (_) {} } - // Persist the skeleton to the server so the web client sees a correct chain + // Sync conversation state to ensure WebUI can load conversation history try { + final api = ref.read(apiServiceProvider); + final activeConv = ref.read(activeConversationProvider); if (api != null && activeConv != null) { final resolvedSystemPrompt = (systemPrompt != null && systemPrompt.trim().isNotEmpty) ? systemPrompt.trim() : activeConv.systemPrompt; final current = ref.read(chatMessagesProvider); - await api.updateConversationWithMessages( + await api.syncConversationMessages( activeConv.id, current, model: modelId, systemPrompt: resolvedSystemPrompt, ); } - } catch (_) {} + } catch (_) { + // Non-critical - continue if sync fails + } return assistantMessageId; } @@ -708,6 +709,47 @@ bool validateFileCount(int currentCount, int newFilesCount, int? maxCount) { return (currentCount + newFilesCount) <= maxCount; } +// Helper function to build files array from attachment IDs +Future>?> _buildFilesArrayFromAttachments( + dynamic api, + List attachmentIds, +) async { + final filesArray = >[]; + + for (final attachmentId in attachmentIds) { + try { + final fileInfo = await api.getFileInfo(attachmentId); + final fileName = fileInfo['filename'] ?? fileInfo['name'] ?? 'Unknown'; + final fileSize = fileInfo['size']; + + // Check if it's an image + final ext = fileName.toLowerCase().split('.').last; + final isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext); + + // Add all files to the files array for WebUI display + // Note: This is for storage/display, not for API message sending + filesArray.add({ + 'type': isImage ? 'image' : 'file', + 'id': attachmentId, // Required for RAG system to lookup file content + 'url': '/api/v1/files/$attachmentId/content', + 'name': fileName, + if (fileSize != null) 'size': fileSize, + }); + } catch (_) { + // If we can't get file info, assume it's a non-image file + // Images should be handled in the content array anyway + filesArray.add({ + 'type': 'file', + 'id': attachmentId, // Required for RAG system to lookup file content + 'url': '/api/v1/files/$attachmentId/content', + 'name': 'Unknown', + }); + } + } + + return filesArray.isNotEmpty ? filesArray : null; +} + // Helper function to get file content as base64 Future _getFileAsBase64(dynamic api, String fileId) async { // Check if this is already a data URL (for images) @@ -758,44 +800,57 @@ Future> _buildMessagePayloadWithAttachments({ required List attachmentIds, }) async { final List> contentArray = []; - final List> nonImageFiles = []; if (cleanedText.isNotEmpty) { contentArray.add({'type': 'text', 'text': cleanedText}); } + // Collect all files in OpenWebUI format for the files array + final allFiles = >[]; + for (final attachmentId in attachmentIds) { try { + final fileInfo = await api.getFileInfo(attachmentId); + final fileName = fileInfo['filename'] ?? fileInfo['name'] ?? 'Unknown'; + final fileSize = fileInfo['size']; + final base64Data = await _getFileAsBase64(api, attachmentId); if (base64Data != null) { + // This is an image file - add to content array only if (base64Data.startsWith('data:')) { contentArray.add({ 'type': 'image_url', 'image_url': {'url': base64Data}, }); } else { - if (!attachmentId.startsWith('data:')) { - final fileInfo = await api.getFileInfo(attachmentId); - final fileName = fileInfo['filename'] ?? ''; - final ext = fileName.toLowerCase().split('.').last; - - String mimeType = 'image/png'; - if (ext == 'jpg' || ext == 'jpeg') { - mimeType = 'image/jpeg'; - } else if (ext == 'gif') { - mimeType = 'image/gif'; - } else if (ext == 'webp') { - mimeType = 'image/webp'; - } - - contentArray.add({ - 'type': 'image_url', - 'image_url': {'url': 'data:$mimeType;base64,$base64Data'}, - }); + final ext = fileName.toLowerCase().split('.').last; + String mimeType = 'image/png'; + if (ext == 'jpg' || ext == 'jpeg') { + mimeType = 'image/jpeg'; + } else if (ext == 'gif') { + mimeType = 'image/gif'; + } else if (ext == 'webp') { + mimeType = 'image/webp'; } + + final dataUrl = 'data:$mimeType;base64,$base64Data'; + contentArray.add({ + 'type': 'image_url', + 'image_url': {'url': dataUrl}, + }); } + + // Note: Images are handled in content array above, no need to duplicate in files array + // This prevents duplicate display in the WebUI } else { - nonImageFiles.add({'id': attachmentId, 'type': 'file'}); + // This is a non-image file + allFiles.add({ + 'type': 'file', + 'id': attachmentId, // Required for RAG system to lookup file content + 'url': '/api/v1/files/$attachmentId/content', + 'name': fileName, + if (fileSize != null) 'size': fileSize, + }); } } catch (_) { // Swallow and continue to keep regeneration robust @@ -806,8 +861,8 @@ Future> _buildMessagePayloadWithAttachments({ 'role': role, 'content': contentArray.isNotEmpty ? contentArray : cleanedText, }; - if (nonImageFiles.isNotEmpty) { - messageMap['files'] = nonImageFiles; + if (allFiles.isNotEmpty) { + messageMap['files'] = allFiles; } return messageMap; } @@ -1221,6 +1276,13 @@ Future _sendMessageInternal( var activeConversation = ref.read(activeConversationProvider); // Create user message first + List>? userFiles; + if (attachments != null && + attachments.isNotEmpty && + !reviewerMode && + api != null) { + userFiles = await _buildFilesArrayFromAttachments(api, attachments); + } final userMessage = ChatMessage( id: const Uuid().v4(), @@ -1229,6 +1291,7 @@ Future _sendMessageInternal( timestamp: DateTime.now(), model: selectedModel.id, attachmentIds: attachments, + files: userFiles, ); if (activeConversation == null) { @@ -1450,19 +1513,21 @@ Future _sendMessageInternal( ); ref.read(chatMessagesProvider.notifier).addMessage(assistantPlaceholder); - // Persist skeleton chain to server so web can load correct history + // Sync conversation state to ensure WebUI can load conversation history try { final activeConvForSeed = ref.read(activeConversationProvider); if (activeConvForSeed != null) { final msgsForSeed = ref.read(chatMessagesProvider); - await api.updateConversationWithMessages( + await api.syncConversationMessages( activeConvForSeed.id, msgsForSeed, model: selectedModel.id, systemPrompt: effectiveSystemPrompt, ); } - } catch (_) {} + } catch (_) { + // Non-critical - continue if sync fails + } // Use the model's actual supported parameters if available final supportedParams = selectedModel.supportedParameters ?? diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index b3be7d1..5a1db02 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -561,17 +561,14 @@ class _AssistantMessageWidgetState extends ConsumerState child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Display attachments (images use EnhancedImageAttachment; non-images use card) - if (widget.message.attachmentIds != null && - widget.message.attachmentIds!.isNotEmpty) ...[ - _buildAttachmentItems(), - const SizedBox(height: Spacing.md), - ], - - // Display generated images from files property - OUTSIDE AnimatedSwitcher to prevent fade issues + // Display attachments - prioritize files array over attachmentIds to avoid duplication if (widget.message.files != null && widget.message.files!.isNotEmpty) ...[ - _buildGeneratedImages(), + _buildFilesFromArray(), + const SizedBox(height: Spacing.md), + ] else if (widget.message.attachmentIds != null && + widget.message.attachmentIds!.isNotEmpty) ...[ + _buildAttachmentItems(), const SizedBox(height: Spacing.md), ], @@ -767,30 +764,57 @@ class _AssistantMessageWidgetState extends ConsumerState ); } - Widget _buildGeneratedImages() { + Widget _buildFilesFromArray() { if (widget.message.files == null || widget.message.files!.isEmpty) { return const SizedBox.shrink(); } - // Filter for image files - final imageFiles = widget.message.files! + final allFiles = widget.message.files!; + + // Separate images and non-image files + final imageFiles = allFiles .where((file) => file['type'] == 'image') .toList(); + final nonImageFiles = allFiles + .where((file) => file['type'] != 'image') + .toList(); - if (imageFiles.isEmpty) { + final widgets = []; + + // Add images first + if (imageFiles.isNotEmpty) { + widgets.add(_buildImagesFromFiles(imageFiles)); + } + + // Add non-image files + if (nonImageFiles.isNotEmpty) { + if (widgets.isNotEmpty) { + widgets.add(const SizedBox(height: Spacing.sm)); + } + widgets.add(_buildNonImageFiles(nonImageFiles)); + } + + if (widgets.isEmpty) { return const SizedBox.shrink(); } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: widgets, + ); + } + + Widget _buildImagesFromFiles(List imageFiles) { final imageCount = imageFiles.length; - // Display generated images using EnhancedImageAttachment for consistency + // Display images using EnhancedImageAttachment for consistency // Use AnimatedSwitcher for smooth transitions return AnimatedSwitcher( duration: const Duration(milliseconds: 300), switchInCurve: Curves.easeInOut, child: imageCount == 1 ? Container( - key: ValueKey('gen_single_${imageFiles[0]['url']}'), + key: ValueKey('file_single_${imageFiles[0]['url']}'), child: Builder( builder: (context) { final imageUrl = imageFiles[0]['url'] as String?; @@ -812,7 +836,7 @@ class _AssistantMessageWidgetState extends ConsumerState ) : Wrap( key: ValueKey( - 'gen_multi_${imageFiles.map((f) => f['url']).join('_')}', + 'file_multi_${imageFiles.map((f) => f['url']).join('_')}', ), spacing: Spacing.sm, runSpacing: Spacing.sm, @@ -836,6 +860,38 @@ class _AssistantMessageWidgetState extends ConsumerState ); } + Widget _buildNonImageFiles(List nonImageFiles) { + return Wrap( + spacing: Spacing.sm, + runSpacing: Spacing.sm, + children: nonImageFiles.map((file) { + final fileUrl = file['url'] as String?; + + if (fileUrl == null) return const SizedBox.shrink(); + + // Extract file ID from URL if it's in the format /api/v1/files/{id}/content + String attachmentId = fileUrl; + if (fileUrl.contains('/api/v1/files/') && + fileUrl.contains('/content')) { + final fileIdMatch = RegExp( + r'/api/v1/files/([^/]+)/content', + ).firstMatch(fileUrl); + if (fileIdMatch != null) { + attachmentId = fileIdMatch.group(1)!; + } + } + + return EnhancedAttachment( + key: ValueKey('file_attachment_$attachmentId'), + attachmentId: attachmentId, + isMarkdownFormat: true, + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 100), + disableAnimation: widget.isStreaming, + ); + }).toList(), + ); + } + Widget _buildTypingIndicator() { return Consumer( builder: (context, ref, child) { diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 5c389c8..8e3b16e 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -64,7 +64,9 @@ class _UserMessageBubbleState extends ConsumerState duration: AnimationDuration.messageSlide, vsync: this, ); - _editController = TextEditingController(text: widget.message?.content ?? ''); + _editController = TextEditingController( + text: widget.message?.content ?? '', + ); } Widget _buildUserAttachmentImages() { @@ -88,23 +90,50 @@ class _UserMessageBubbleState extends ConsumerState return const SizedBox.shrink(); } - final imageFiles = widget.message.files! + final allFiles = widget.message.files!; + + // Separate images and non-image files + final imageFiles = allFiles .where( (file) => file is Map && file['type'] == 'image' && file['url'] != null, ) .toList(); + final nonImageFiles = allFiles + .where( + (file) => + file is Map && file['type'] != 'image' && file['url'] != null, + ) + .toList(); - if (imageFiles.isEmpty) { + final widgets = []; + + // Add images first + if (imageFiles.isNotEmpty) { + widgets.add( + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + switchInCurve: Curves.easeInOut, + child: _buildFileImageLayout(imageFiles, imageFiles.length), + ), + ); + } + + // Add non-image files + if (nonImageFiles.isNotEmpty) { + if (widgets.isNotEmpty) { + widgets.add(const SizedBox(height: Spacing.xs)); + } + widgets.add(_buildUserNonImageFiles(nonImageFiles)); + } + + if (widgets.isEmpty) { return const SizedBox.shrink(); } - final imageCount = imageFiles.length; - - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - child: _buildFileImageLayout(imageFiles, imageCount), + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: widgets, ); } @@ -394,6 +423,47 @@ class _UserMessageBubbleState extends ConsumerState } } + Widget _buildUserNonImageFiles(List nonImageFiles) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: Wrap( + alignment: WrapAlignment.end, + spacing: Spacing.xs, + runSpacing: Spacing.xs, + children: nonImageFiles.map((file) { + final fileUrl = file['url'] as String?; + + if (fileUrl == null) return const SizedBox.shrink(); + + // Extract file ID from URL if it's in the format /api/v1/files/{id}/content + String attachmentId = fileUrl; + if (fileUrl.contains('/api/v1/files/') && + fileUrl.contains('/content')) { + final fileIdMatch = RegExp( + r'/api/v1/files/([^/]+)/content', + ).firstMatch(fileUrl); + if (fileIdMatch != null) { + attachmentId = fileIdMatch.group(1)!; + } + } + + return EnhancedAttachment( + key: ValueKey('user_file_attachment_$attachmentId'), + attachmentId: attachmentId, + isMarkdownFormat: false, + isUserMessage: true, + constraints: const BoxConstraints(maxWidth: 280, maxHeight: 80), + disableAnimation: widget.isStreaming, + ); + }).toList(), + ), + ), + ], + ); + } + // Assistant-only helpers removed; this widget renders only user bubbles. @override @@ -429,14 +499,14 @@ class _UserMessageBubbleState extends ConsumerState widget.message.attachmentIds != null && widget.message.attachmentIds!.isNotEmpty; final hasText = widget.message.content.isNotEmpty; - final hasGeneratedImages = + final hasFilesFromArray = widget.message.files != null && - (widget.message.files as List).any( - (f) => f is Map && f['type'] == 'image' && f['url'] != null, - ); + (widget.message.files as List).any((f) => f is Map && f['url'] != null); // Prefer input/textPrimary colors during inline editing to avoid low contrast final inlineEditTextColor = context.conduitTheme.textPrimary; - final inlineEditFill = context.conduitTheme.surfaceContainer.withValues(alpha: 0.92); + final inlineEditFill = context.conduitTheme.surfaceContainer.withValues( + alpha: 0.92, + ); return GestureDetector( onLongPress: () => _toggleActions(), @@ -452,8 +522,12 @@ class _UserMessageBubbleState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.end, children: [ // Display images outside and above the text bubble (iMessage style) - if (hasImages) ...[_buildUserAttachmentImages()], - if (hasGeneratedImages) ...[_buildUserFileImages()], + // Prioritize files array over attachmentIds to avoid duplication + if (hasFilesFromArray) ...[ + _buildUserFileImages(), + ] else if (hasImages) ...[ + _buildUserAttachmentImages(), + ], // Display text bubble if there's text content if (hasText) const SizedBox(height: Spacing.xs), @@ -504,9 +578,14 @@ class _UserMessageBubbleState extends ConsumerState child: DecoratedBox( decoration: BoxDecoration( color: inlineEditFill, - borderRadius: BorderRadius.circular(AppBorderRadius.sm), + borderRadius: BorderRadius.circular( + AppBorderRadius.sm, + ), border: Border.all( - color: context.conduitTheme.inputBorderFocused.withValues(alpha: 0.6), + color: context + .conduitTheme + .inputBorderFocused + .withValues(alpha: 0.6), width: BorderWidth.thin, ), ), @@ -520,38 +599,41 @@ class _UserMessageBubbleState extends ConsumerState controller: _editController, maxLines: null, padding: EdgeInsets.zero, - autofillHints: - const [], + autofillHints: const [], style: AppTypography .chatMessageStyle .copyWith( - color: inlineEditTextColor, - ), - decoration: const BoxDecoration(), + color: + inlineEditTextColor, + ), + decoration: + const BoxDecoration(), cursorColor: context - .conduitTheme.buttonPrimary, + .conduitTheme + .buttonPrimary, onSubmitted: (_) => _saveInlineEdit(), ) : TextField( controller: _editController, maxLines: null, - autofillHints: - const [], + autofillHints: const [], style: AppTypography .chatMessageStyle .copyWith( - color: inlineEditTextColor, - ), + color: + inlineEditTextColor, + ), decoration: const InputDecoration( - isCollapsed: true, - border: InputBorder.none, - contentPadding: - EdgeInsets.zero, - ), + isCollapsed: true, + border: InputBorder.none, + contentPadding: + EdgeInsets.zero, + ), cursorColor: context - .conduitTheme.buttonPrimary, + .conduitTheme + .buttonPrimary, onSubmitted: (_) => _saveInlineEdit(), ), @@ -562,18 +644,19 @@ class _UserMessageBubbleState extends ConsumerState widget.message.content, style: AppTypography.chatMessageStyle .copyWith( - color: context - .conduitTheme.chatBubbleUserText, - ), + color: context + .conduitTheme + .chatBubbleUserText, + ), softWrap: true, textAlign: TextAlign.left, textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - leadingDistribution: - TextLeadingDistribution.even, - ), + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + leadingDistribution: + TextLeadingDistribution.even, + ), ), ), ), @@ -633,8 +716,7 @@ class _UserMessageBubbleState extends ConsumerState ), ] else ...[ _buildActionButton( - icon: - Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_outlined, + icon: Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_outlined, label: AppLocalizations.of(context)!.edit, onTap: widget.onEdit ?? _startInlineEdit, ), @@ -693,7 +775,8 @@ class _UserMessageBubbleState extends ConsumerState // Enqueue edited text as a new message final activeConv = ref.read(activeConversationProvider); - final List? attachments = (widget.message.attachmentIds != null && + final List? attachments = + (widget.message.attachmentIds != null && (widget.message.attachmentIds as List).isNotEmpty) ? List.from(widget.message.attachmentIds as List) : null; diff --git a/openwebui-src b/openwebui-src index 2407d9b..6bc5d33 160000 --- a/openwebui-src +++ b/openwebui-src @@ -1 +1 @@ -Subproject commit 2407d9b905978d68619bdce4021e424046ec8df9 +Subproject commit 6bc5d331a27c5106f492213510a763effa316faf