diff --git a/lib/core/services/conversation_parsing.dart b/lib/core/services/conversation_parsing.dart index 4cf2c62..c36d6ce 100644 --- a/lib/core/services/conversation_parsing.dart +++ b/lib/core/services/conversation_parsing.dart @@ -272,6 +272,10 @@ Map _parseOpenWebUIMessageToJson( }; if (entry['name'] != null) fileMap['name'] = entry['name']; if (entry['size'] != null) fileMap['size'] = entry['size']; + final headers = _coerceStringMap(entry['headers']); + if (headers != null && headers.isNotEmpty) { + fileMap['headers'] = headers; + } allFiles.add(fileMap); final url = entry['url'].toString(); @@ -388,6 +392,24 @@ List> _parseStatusHistoryField(dynamic raw) { return const >[]; } +Map? _coerceStringMap(dynamic raw) { + if (raw is Map) { + final result = {}; + raw.forEach((key, value) { + final keyString = key?.toString(); + final valueString = value?.toString(); + if (keyString != null && + keyString.isNotEmpty && + valueString != null && + valueString.isNotEmpty) { + result[keyString] = valueString; + } + }); + return result.isEmpty ? null : result; + } + return null; +} + List _coerceStringList(dynamic raw) { if (raw is List) { return raw diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index 570ee3f..f1e3dd7 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -1063,6 +1063,7 @@ class _AssistantMessageWidgetState extends ConsumerState ), disableAnimation: false, // Keep animations enabled to prevent black display + httpHeaders: _headersForFile(imageFiles[0]), ); }, ), @@ -1087,12 +1088,31 @@ class _AssistantMessageWidgetState extends ConsumerState ), disableAnimation: false, // Keep animations enabled to prevent black display + httpHeaders: _headersForFile(file), ); }).toList(), ), ); } + Map? _headersForFile(dynamic file) { + if (file is! Map) return null; + final rawHeaders = file['headers']; + if (rawHeaders is! Map) return null; + final result = {}; + rawHeaders.forEach((key, value) { + final keyString = key?.toString(); + final valueString = value?.toString(); + if (keyString != null && + keyString.isNotEmpty && + valueString != null && + valueString.isNotEmpty) { + result[keyString] = valueString; + } + }); + return result.isEmpty ? null : result; + } + Widget _buildNonImageFiles(List nonImageFiles) { return Wrap( spacing: Spacing.sm, diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index 2cf22f3..94359e4 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -51,7 +51,9 @@ bool _isSvgUrl(String url) { // Check for .svg file extension (with or without query string) final queryIndex = lowerUrl.indexOf('?'); - final pathPart = queryIndex >= 0 ? lowerUrl.substring(0, queryIndex) : lowerUrl; + final pathPart = queryIndex >= 0 + ? lowerUrl.substring(0, queryIndex) + : lowerUrl; if (pathPart.endsWith('.svg')) return true; // Check for SVG MIME type in query parameters only (not in path) @@ -75,6 +77,17 @@ bool _isSvgBytes(Uint8List bytes) { return header.toLowerCase().contains('? _mergeHeaders( + Map? defaults, + Map? overrides, +) { + if ((defaults == null || defaults.isEmpty) && + (overrides == null || overrides.isEmpty)) { + return null; + } + return {...?defaults, ...?overrides}; +} + class EnhancedImageAttachment extends ConsumerStatefulWidget { final String attachmentId; final bool isMarkdownFormat; @@ -82,6 +95,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget { final BoxConstraints? constraints; final bool isUserMessage; final bool disableAnimation; + final Map? httpHeaders; const EnhancedImageAttachment({ super.key, @@ -91,6 +105,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget { this.constraints, this.isUserMessage = false, this.disableAnimation = false, + this.httpHeaders, }); @override @@ -483,7 +498,8 @@ class _EnhancedImageAttachmentState Widget _buildNetworkImage() { // Get authentication headers if available - final headers = buildImageHeadersFromWidgetRef(ref); + final defaultHeaders = buildImageHeadersFromWidgetRef(ref); + final headers = _mergeHeaders(defaultHeaders, widget.httpHeaders); final cacheManager = ref.watch(selfSignedImageCacheManagerProvider); final imageWidget = CachedNetworkImage( @@ -516,7 +532,8 @@ class _EnhancedImageAttachmentState Widget _buildNetworkSvg() { // Get authentication headers if available - final headers = buildImageHeadersFromWidgetRef(ref); + final defaultHeaders = buildImageHeadersFromWidgetRef(ref); + final headers = _mergeHeaders(defaultHeaders, widget.httpHeaders); final svgWidget = SvgPicture.network( _cachedImageData!, @@ -647,6 +664,7 @@ class _EnhancedImageAttachmentState imageData: _cachedImageData!, tag: _heroTag, isSvg: _isSvg, + customHeaders: widget.httpHeaders, ), ), ); @@ -657,12 +675,14 @@ class FullScreenImageViewer extends ConsumerWidget { final String imageData; final String tag; final bool isSvg; + final Map? customHeaders; const FullScreenImageViewer({ super.key, required this.imageData, required this.tag, this.isSvg = false, + this.customHeaders, }); @override @@ -671,7 +691,8 @@ class FullScreenImageViewer extends ConsumerWidget { if (imageData.startsWith('http')) { // Get authentication headers if available - final headers = buildImageHeadersFromWidgetRef(ref); + final defaultHeaders = buildImageHeadersFromWidgetRef(ref); + final headers = _mergeHeaders(defaultHeaders, customHeaders); if (isSvg || _isSvgUrl(imageData)) { imageWidget = SvgPicture.network( @@ -818,13 +839,14 @@ class FullScreenImageViewer extends ConsumerWidget { if (api != null && api.serverConfig.customHeaders.isNotEmpty) { headers.addAll(api.serverConfig.customHeaders); } + final mergedHeaders = _mergeHeaders(headers, customHeaders); final client = api?.dio ?? dio.Dio(); final response = await client.get>( imageData, options: dio.Options( responseType: dio.ResponseType.bytes, - headers: headers.isNotEmpty ? headers : null, + headers: mergedHeaders, ), ); final data = response.data; diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 896d35c..3848265 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -150,6 +150,7 @@ class _UserMessageBubbleState extends ConsumerState { maxHeight: 350, ), disableAnimation: widget.isStreaming, + httpHeaders: _headersForFile(imageFiles[0]), ), ), ), @@ -191,6 +192,7 @@ class _UserMessageBubbleState extends ConsumerState { maxHeight: 180, ), disableAnimation: widget.isStreaming, + httpHeaders: _headersForFile(entry.value), ), ), ), @@ -232,6 +234,7 @@ class _UserMessageBubbleState extends ConsumerState { maxHeight: imageCount == 3 ? 135 : 90, ), disableAnimation: widget.isStreaming, + httpHeaders: _headersForFile(file), ), ), ); @@ -401,6 +404,24 @@ class _UserMessageBubbleState extends ConsumerState { ); } + Map? _headersForFile(dynamic file) { + if (file is! Map) return null; + final rawHeaders = file['headers']; + if (rawHeaders is! Map) return null; + final result = {}; + rawHeaders.forEach((key, value) { + final keyString = key?.toString(); + final valueString = value?.toString(); + if (keyString != null && + keyString.isNotEmpty && + valueString != null && + valueString.isNotEmpty) { + result[keyString] = valueString; + } + }); + return result.isEmpty ? null : result; + } + // Assistant-only helpers removed; this widget renders only user bubbles. @override