diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 9e62349..c69cd3d 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -1047,24 +1047,87 @@ Future _sendMessageInternal( final msgs = ref.read(chatMessagesProvider); if (msgs.isEmpty || msgs.last.role != 'assistant') return; final content = msgs.last.content; - if (content.isEmpty || !content.contains('>[]; - for (final entry in parsed.toolCalls) { - if (entry.files != null && entry.files!.isNotEmpty) { - collected.addAll(_extractFilesFromResult(entry.files)); - } - if (entry.result != null) { - collected.addAll(_extractFilesFromResult(entry.result)); + + // First, try the complete parsing approach + if (content.contains('"]+\.(jpg|jpeg|png|gif|webp)', caseSensitive: false); + final urlMatches = urlPattern.allMatches(content); + for (final match in urlMatches) { + final url = match.group(0); + if (url != null && url.isNotEmpty) { + collected.add({'type': 'image', 'url': url}); + } + } + + // Look for JSON-like structures in streaming content + final jsonPattern = RegExp(r'\{[^}]*"url"[^}]*:[^}]*"(data:image/[^"]+|https?://[^"]+\.(jpg|jpeg|png|gif|webp))"[^}]*\}', caseSensitive: false); + final jsonMatches = jsonPattern.allMatches(content); + for (final match in jsonMatches) { + final url = RegExp(r'"url"[^:]*:[^"]*"([^"]+)"').firstMatch(match.group(0) ?? '')?.group(1); + if (url != null && url.isNotEmpty) { + collected.add({'type': 'image', 'url': url}); + } + } + + // Look for image generation results in partial results/files attributes + final partialResultsPattern = RegExp(r'(result|files)="([^"]*(?:data:image/[^"]*|https?://[^"]*\.(jpg|jpeg|png|gif|webp))[^"]*)"', caseSensitive: false); + final partialMatches = partialResultsPattern.allMatches(content); + for (final match in partialMatches) { + final attrValue = match.group(2); + if (attrValue != null) { + // Try to parse as JSON array or single value + try { + final decoded = json.decode(attrValue); + collected.addAll(_extractFilesFromResult(decoded)); + } catch (_) { + // If not JSON, check if it's a direct URL + if (attrValue.startsWith('data:image/') || + RegExp(r'https?://[^\s]+\.(jpg|jpeg|png|gif|webp)$', caseSensitive: false).hasMatch(attrValue)) { + collected.add({'type': 'image', 'url': attrValue}); + } + } + } + } + } + if (collected.isEmpty) return; + final existing = msgs.last.files ?? >[]; final seen = { for (final f in existing) if (f['url'] is String) (f['url'] as String) else '', }..removeWhere((e) => e.isEmpty); + final merged = >[...existing]; for (final f in collected) { final url = f['url'] as String?; @@ -1073,6 +1136,7 @@ Future _sendMessageInternal( seen.add(url); } } + if (merged.length != existing.length) { ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction( (m) => m.copyWith(files: merged), @@ -1537,6 +1601,40 @@ Future _sendMessageInternal( } } catch (_) {} } catch (_) {} + } else if (type == 'files' && payload != null) { + // Handle files event from socket (image generation results) + try { + DebugLogger.stream('Socket files event received: ${payload.toString()}'); + final files = _extractFilesFromResult(payload); + if (files.isNotEmpty) { + final msgs = ref.read(chatMessagesProvider); + if (msgs.isNotEmpty && msgs.last.role == 'assistant') { + final existing = msgs.last.files ?? >[]; + final seen = { + for (final f in existing) + if (f['url'] is String) (f['url'] as String) else '', + }..removeWhere((e) => e.isEmpty); + final merged = >[...existing]; + for (final f in files) { + final url = f['url'] as String?; + if (url != null && url.isNotEmpty && !seen.contains(url)) { + merged.add({'type': 'image', 'url': url}); + seen.add(url); + } + } + if (merged.length != existing.length) { + DebugLogger.stream('Socket files: Adding ${merged.length - existing.length} new images'); + final updatedMessage = ref.read(chatMessagesProvider).last.copyWith(files: merged); + DebugLogger.stream('Socket files: Updated message files count: ${updatedMessage.files?.length}'); + ref.read(chatMessagesProvider.notifier).updateLastMessageWithFunction( + (ChatMessage m) => m.copyWith(files: merged), + ); + } + } + } + } catch (e) { + DebugLogger.stream('Socket files event error: $e'); + } } } catch (_) {} } diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index a6344bf..3dd2cc0 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -570,7 +570,7 @@ class _AssistantMessageWidgetState extends ConsumerState const SizedBox(height: Spacing.md), ], - // Display generated images from files property + // Display generated images from files property - OUTSIDE AnimatedSwitcher to prevent fade issues if (widget.message.files != null && widget.message.files!.isNotEmpty) ...[ _buildGeneratedImages(), @@ -785,8 +785,7 @@ class _AssistantMessageWidgetState extends ConsumerState maxWidth: 500, maxHeight: 400, ), - disableAnimation: widget - .isStreaming, // Disable animation during streaming + disableAnimation: false, // Keep animations enabled to prevent black display ); }, ), @@ -809,8 +808,7 @@ class _AssistantMessageWidgetState extends ConsumerState maxWidth: imageCount == 2 ? 245 : 160, maxHeight: imageCount == 2 ? 245 : 160, ), - disableAnimation: - widget.isStreaming, // Disable animation during streaming + disableAnimation: false, // Keep animations enabled to prevent black display ); }).toList(), ), diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index e06294a..f7a7a96 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -247,22 +247,8 @@ class _EnhancedImageAttachmentState Widget build(BuildContext context) { super.build(context); // Required for AutomaticKeepAliveClientMixin - // Use a single container with AnimatedSwitcher for smooth transitions - return AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - layoutBuilder: (currentChild, previousChildren) { - return Stack( - alignment: Alignment.center, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], - ); - }, - child: _buildContent(), - ); + // Directly return content without AnimatedSwitcher to prevent black flash during streaming + return _buildContent(); } Widget _buildContent() { @@ -286,11 +272,8 @@ class _EnhancedImageAttachmentState imageWidget = _buildBase64Image(); } - // Apply fade animation only when first showing content - if (!widget.disableAnimation && _hasShownContent) { - return FadeTransition(opacity: _fadeAnimation, child: imageWidget); - } - + // Always show the image without fade transitions during streaming to prevent black display + // The AutomaticKeepAliveClientMixin and global caching should preserve the image state return imageWidget; }