feat: generated images parsed
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user