feat(chat): add support for custom HTTP headers in image attachments
This commit is contained in:
@@ -272,6 +272,10 @@ Map<String, dynamic> _parseOpenWebUIMessageToJson(
|
|||||||
};
|
};
|
||||||
if (entry['name'] != null) fileMap['name'] = entry['name'];
|
if (entry['name'] != null) fileMap['name'] = entry['name'];
|
||||||
if (entry['size'] != null) fileMap['size'] = entry['size'];
|
if (entry['size'] != null) fileMap['size'] = entry['size'];
|
||||||
|
final headers = _coerceStringMap(entry['headers']);
|
||||||
|
if (headers != null && headers.isNotEmpty) {
|
||||||
|
fileMap['headers'] = headers;
|
||||||
|
}
|
||||||
allFiles.add(fileMap);
|
allFiles.add(fileMap);
|
||||||
|
|
||||||
final url = entry['url'].toString();
|
final url = entry['url'].toString();
|
||||||
@@ -388,6 +392,24 @@ List<Map<String, dynamic>> _parseStatusHistoryField(dynamic raw) {
|
|||||||
return const <Map<String, dynamic>>[];
|
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) {
|
List<String> _coerceStringList(dynamic raw) {
|
||||||
if (raw is List) {
|
if (raw is List) {
|
||||||
return raw
|
return raw
|
||||||
|
|||||||
@@ -1063,6 +1063,7 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
disableAnimation:
|
disableAnimation:
|
||||||
false, // Keep animations enabled to prevent black display
|
false, // Keep animations enabled to prevent black display
|
||||||
|
httpHeaders: _headersForFile(imageFiles[0]),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -1087,12 +1088,31 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
|
|||||||
),
|
),
|
||||||
disableAnimation:
|
disableAnimation:
|
||||||
false, // Keep animations enabled to prevent black display
|
false, // Keep animations enabled to prevent black display
|
||||||
|
httpHeaders: _headersForFile(file),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).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) {
|
Widget _buildNonImageFiles(List<dynamic> nonImageFiles) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: Spacing.sm,
|
spacing: Spacing.sm,
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ bool _isSvgUrl(String url) {
|
|||||||
|
|
||||||
// Check for .svg file extension (with or without query string)
|
// Check for .svg file extension (with or without query string)
|
||||||
final queryIndex = lowerUrl.indexOf('?');
|
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;
|
if (pathPart.endsWith('.svg')) return true;
|
||||||
|
|
||||||
// Check for SVG MIME type in query parameters only (not in path)
|
// 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');
|
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 {
|
class EnhancedImageAttachment extends ConsumerStatefulWidget {
|
||||||
final String attachmentId;
|
final String attachmentId;
|
||||||
final bool isMarkdownFormat;
|
final bool isMarkdownFormat;
|
||||||
@@ -82,6 +95,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget {
|
|||||||
final BoxConstraints? constraints;
|
final BoxConstraints? constraints;
|
||||||
final bool isUserMessage;
|
final bool isUserMessage;
|
||||||
final bool disableAnimation;
|
final bool disableAnimation;
|
||||||
|
final Map<String, String>? httpHeaders;
|
||||||
|
|
||||||
const EnhancedImageAttachment({
|
const EnhancedImageAttachment({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -91,6 +105,7 @@ class EnhancedImageAttachment extends ConsumerStatefulWidget {
|
|||||||
this.constraints,
|
this.constraints,
|
||||||
this.isUserMessage = false,
|
this.isUserMessage = false,
|
||||||
this.disableAnimation = false,
|
this.disableAnimation = false,
|
||||||
|
this.httpHeaders,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -483,7 +498,8 @@ class _EnhancedImageAttachmentState
|
|||||||
|
|
||||||
Widget _buildNetworkImage() {
|
Widget _buildNetworkImage() {
|
||||||
// Get authentication headers if available
|
// 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 cacheManager = ref.watch(selfSignedImageCacheManagerProvider);
|
||||||
final imageWidget = CachedNetworkImage(
|
final imageWidget = CachedNetworkImage(
|
||||||
@@ -516,7 +532,8 @@ class _EnhancedImageAttachmentState
|
|||||||
|
|
||||||
Widget _buildNetworkSvg() {
|
Widget _buildNetworkSvg() {
|
||||||
// Get authentication headers if available
|
// Get authentication headers if available
|
||||||
final headers = buildImageHeadersFromWidgetRef(ref);
|
final defaultHeaders = buildImageHeadersFromWidgetRef(ref);
|
||||||
|
final headers = _mergeHeaders(defaultHeaders, widget.httpHeaders);
|
||||||
|
|
||||||
final svgWidget = SvgPicture.network(
|
final svgWidget = SvgPicture.network(
|
||||||
_cachedImageData!,
|
_cachedImageData!,
|
||||||
@@ -647,6 +664,7 @@ class _EnhancedImageAttachmentState
|
|||||||
imageData: _cachedImageData!,
|
imageData: _cachedImageData!,
|
||||||
tag: _heroTag,
|
tag: _heroTag,
|
||||||
isSvg: _isSvg,
|
isSvg: _isSvg,
|
||||||
|
customHeaders: widget.httpHeaders,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -657,12 +675,14 @@ class FullScreenImageViewer extends ConsumerWidget {
|
|||||||
final String imageData;
|
final String imageData;
|
||||||
final String tag;
|
final String tag;
|
||||||
final bool isSvg;
|
final bool isSvg;
|
||||||
|
final Map<String, String>? customHeaders;
|
||||||
|
|
||||||
const FullScreenImageViewer({
|
const FullScreenImageViewer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.imageData,
|
required this.imageData,
|
||||||
required this.tag,
|
required this.tag,
|
||||||
this.isSvg = false,
|
this.isSvg = false,
|
||||||
|
this.customHeaders,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -671,7 +691,8 @@ class FullScreenImageViewer extends ConsumerWidget {
|
|||||||
|
|
||||||
if (imageData.startsWith('http')) {
|
if (imageData.startsWith('http')) {
|
||||||
// Get authentication headers if available
|
// Get authentication headers if available
|
||||||
final headers = buildImageHeadersFromWidgetRef(ref);
|
final defaultHeaders = buildImageHeadersFromWidgetRef(ref);
|
||||||
|
final headers = _mergeHeaders(defaultHeaders, customHeaders);
|
||||||
|
|
||||||
if (isSvg || _isSvgUrl(imageData)) {
|
if (isSvg || _isSvgUrl(imageData)) {
|
||||||
imageWidget = SvgPicture.network(
|
imageWidget = SvgPicture.network(
|
||||||
@@ -818,13 +839,14 @@ class FullScreenImageViewer extends ConsumerWidget {
|
|||||||
if (api != null && api.serverConfig.customHeaders.isNotEmpty) {
|
if (api != null && api.serverConfig.customHeaders.isNotEmpty) {
|
||||||
headers.addAll(api.serverConfig.customHeaders);
|
headers.addAll(api.serverConfig.customHeaders);
|
||||||
}
|
}
|
||||||
|
final mergedHeaders = _mergeHeaders(headers, customHeaders);
|
||||||
|
|
||||||
final client = api?.dio ?? dio.Dio();
|
final client = api?.dio ?? dio.Dio();
|
||||||
final response = await client.get<List<int>>(
|
final response = await client.get<List<int>>(
|
||||||
imageData,
|
imageData,
|
||||||
options: dio.Options(
|
options: dio.Options(
|
||||||
responseType: dio.ResponseType.bytes,
|
responseType: dio.ResponseType.bytes,
|
||||||
headers: headers.isNotEmpty ? headers : null,
|
headers: mergedHeaders,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final data = response.data;
|
final data = response.data;
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
|||||||
maxHeight: 350,
|
maxHeight: 350,
|
||||||
),
|
),
|
||||||
disableAnimation: widget.isStreaming,
|
disableAnimation: widget.isStreaming,
|
||||||
|
httpHeaders: _headersForFile(imageFiles[0]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -191,6 +192,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
|||||||
maxHeight: 180,
|
maxHeight: 180,
|
||||||
),
|
),
|
||||||
disableAnimation: widget.isStreaming,
|
disableAnimation: widget.isStreaming,
|
||||||
|
httpHeaders: _headersForFile(entry.value),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -232,6 +234,7 @@ class _UserMessageBubbleState extends ConsumerState<UserMessageBubble> {
|
|||||||
maxHeight: imageCount == 3 ? 135 : 90,
|
maxHeight: imageCount == 3 ? 135 : 90,
|
||||||
),
|
),
|
||||||
disableAnimation: widget.isStreaming,
|
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.
|
// Assistant-only helpers removed; this widget renders only user bubbles.
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user