feat: generated images parsed

This commit is contained in:
cogwheel0
2025-08-20 23:42:31 +05:30
parent 6cea654b88
commit bc2f60e685
4 changed files with 156 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ sealed class ChatMessage with _$ChatMessage {
String? model, String? model,
@Default(false) bool isStreaming, @Default(false) bool isStreaming,
List<String>? attachmentIds, List<String>? attachmentIds,
List<Map<String, dynamic>>? files, // For generated images
Map<String, dynamic>? metadata, Map<String, dynamic>? metadata,
List<Map<String, dynamic>>? sources, List<Map<String, dynamic>>? sources,
Map<String, dynamic>? usage, Map<String, dynamic>? usage,

View File

@@ -27,6 +27,9 @@ class ApiService {
// Public getter for dio instance // Public getter for dio instance
Dio get dio => _dio; Dio get dio => _dio;
// Public getter for base URL
String get baseUrl => serverConfig.url;
// Callback to notify when auth token becomes invalid // Callback to notify when auth token becomes invalid
void Function()? onAuthTokenInvalid; void Function()? onAuthTokenInvalid;
@@ -718,18 +721,34 @@ class ApiService {
role = 'user'; role = 'user';
} }
// Parse attachments from 'files' field // Parse attachments and generated images from 'files' field
List<String>? attachmentIds; List<String>? attachmentIds;
List<Map<String, dynamic>>? files;
if (msgData['files'] != null) { if (msgData['files'] != null) {
final filesList = msgData['files'] as List; final filesList = msgData['files'] as List;
attachmentIds = filesList
.where((file) => file is Map && file['file_id'] != null) // Separate user uploads (with file_id) from generated images (with type and url)
.map((file) => file['file_id'] as String) final userAttachments = <String>[];
.toList(); final generatedFiles = <Map<String, dynamic>>[];
if (attachmentIds.isEmpty) { for (final file in filesList) {
attachmentIds = null; 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( return ChatMessage(
@@ -739,6 +758,7 @@ class ApiService {
timestamp: _parseTimestamp(msgData['timestamp']), timestamp: _parseTimestamp(msgData['timestamp']),
model: msgData['model'] as String?, model: msgData['model'] as String?,
attachmentIds: attachmentIds, attachmentIds: attachmentIds,
files: files,
); );
} }

View File

@@ -287,6 +287,13 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
const SizedBox(height: Spacing.md), 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 && if (widget.isStreaming &&
(widget.message.content.trim().isEmpty || (widget.message.content.trim().isEmpty ||
widget.message.content == '[TYPING_INDICATOR]')) widget.message.content == '[TYPING_INDICATOR]'))
@@ -400,6 +407,59 @@ class _AssistantMessageWidgetState extends ConsumerState<AssistantMessageWidget>
} }
} }
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<Widget>((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() { Widget _buildTypingIndicator() {
return Consumer( return Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import '../../../shared/theme/theme_extensions.dart'; import '../../../shared/theme/theme_extensions.dart';
import '../../../core/providers/app_providers.dart'; import '../../../core/providers/app_providers.dart';
import '../../auth/providers/unified_auth_providers.dart';
// Global cache for image data to prevent reloading // Global cache for image data to prevent reloading
final _globalImageCache = <String, String>{}; final _globalImageCache = <String, String>{};
@@ -69,6 +70,32 @@ class _EnhancedImageAttachmentState
} }
return; 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); final api = ref.read(apiServiceProvider);
if (api == null) { if (api == null) {
@@ -231,9 +258,28 @@ class _EnhancedImageAttachmentState
} }
Widget _buildNetworkImage() { Widget _buildNetworkImage() {
// Get authentication headers if available
final api = ref.read(apiServiceProvider);
final authToken = ref.read(authTokenProvider3);
final headers = <String, String>{};
// 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( final imageWidget = CachedNetworkImage(
imageUrl: _cachedImageData!, imageUrl: _cachedImageData!,
fit: BoxFit.cover, fit: BoxFit.cover,
httpHeaders: headers.isNotEmpty ? headers : null,
placeholder: (context, url) => _buildLoadingState(), placeholder: (context, url) => _buildLoadingState(),
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
_errorMessage = error.toString(); _errorMessage = error.toString();
@@ -312,7 +358,7 @@ class _EnhancedImageAttachmentState
} }
} }
class FullScreenImageViewer extends StatelessWidget { class FullScreenImageViewer extends ConsumerWidget {
final String imageData; final String imageData;
final String tag; final String tag;
@@ -323,13 +369,32 @@ class FullScreenImageViewer extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
Widget imageWidget; Widget imageWidget;
if (imageData.startsWith('http')) { if (imageData.startsWith('http')) {
// Get authentication headers if available
final api = ref.read(apiServiceProvider);
final authToken = ref.read(authTokenProvider3);
final headers = <String, String>{};
// 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( imageWidget = CachedNetworkImage(
imageUrl: imageData, imageUrl: imageData,
fit: BoxFit.contain, fit: BoxFit.contain,
httpHeaders: headers.isNotEmpty ? headers : null,
placeholder: (context, url) => Center( placeholder: (context, url) => Center(
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: context.conduitTheme.buttonPrimary, color: context.conduitTheme.buttonPrimary,