feat(chat): add support for custom HTTP headers in image attachments

This commit is contained in:
cogwheel0
2025-11-29 10:57:06 +05:30
parent 4c5f12919f
commit aefd4dfa2c
4 changed files with 90 additions and 5 deletions

View File

@@ -272,6 +272,10 @@ Map<String, dynamic> _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<Map<String, dynamic>> _parseStatusHistoryField(dynamic raw) {
return const <Map<String, dynamic>>[];
}
Map<String, String>? _coerceStringMap(dynamic raw) {
if (raw is Map) {
final result = <String, String>{};
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<String> _coerceStringList(dynamic raw) {
if (raw is List) {
return raw

View File

@@ -1063,6 +1063,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
),
disableAnimation:
false, // Keep animations enabled to prevent black display
httpHeaders: _headersForFile(imageFiles[0]),
);
},
),
@@ -1087,12 +1088,31 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
),
disableAnimation:
false, // Keep animations enabled to prevent black display
httpHeaders: _headersForFile(file),
);
}).toList(),
),
);
}
Map<String, String>? _headersForFile(dynamic file) {
if (file is! Map) return null;
final rawHeaders = file['headers'];
if (rawHeaders is! Map) return null;
final result = <String, String>{};
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<dynamic> nonImageFiles) {
return Wrap(
spacing: Spacing.sm,

View File

@@ -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('<svg');
}
Map<String, String>? _mergeHeaders(
Map<String, String>? defaults,
Map<String, String>? 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<String, String>? 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<String, String>? 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<List<int>>(
imageData,
options: dio.Options(
responseType: dio.ResponseType.bytes,
headers: headers.isNotEmpty ? headers : null,
headers: mergedHeaders,
),
);
final data = response.data;

View File

@@ -150,6 +150,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: 350,
),
disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(imageFiles[0]),
),
),
),
@@ -191,6 +192,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: 180,
),
disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(entry.value),
),
),
),
@@ -232,6 +234,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
maxHeight: imageCount == 3 ? 135 : 90,
),
disableAnimation: widget.isStreaming,
httpHeaders: _headersForFile(file),
),
),
);
@@ -401,6 +404,24 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
);
}
Map<String, String>? _headersForFile(dynamic file) {
if (file is! Map) return null;
final rawHeaders = file['headers'];
if (rawHeaders is! Map) return null;
final result = <String, String>{};
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