refactor: debug logs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -33,9 +33,7 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
debugPrint(
|
||||
'DEBUG: Active conversation changed - Previous: ${previous?.id}, Next: ${next?.id}',
|
||||
);
|
||||
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}');
|
||||
|
||||
// Only react when the conversation actually changes
|
||||
if (previous?.id == next?.id) {
|
||||
@@ -50,15 +48,11 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
_cancelMessageStream();
|
||||
|
||||
if (next != null) {
|
||||
debugPrint(
|
||||
'DEBUG: Loading ${next.messages.length} messages for conversation ${next.id}',
|
||||
);
|
||||
state = next.messages;
|
||||
|
||||
// Update selected model if conversation has a different model
|
||||
_updateModelForConversation(next);
|
||||
} else {
|
||||
debugPrint('DEBUG: Clearing messages - no active conversation');
|
||||
state = [];
|
||||
}
|
||||
});
|
||||
@@ -76,35 +70,20 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
}
|
||||
|
||||
Future<void> _updateModelForConversation(Conversation conversation) async {
|
||||
debugPrint(
|
||||
'DEBUG: _updateModelForConversation called for conversation ${conversation.id}',
|
||||
);
|
||||
|
||||
// Check if conversation has a model specified
|
||||
if (conversation.model == null || conversation.model!.isEmpty) {
|
||||
debugPrint('DEBUG: Conversation has no model specified');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: Conversation model: ${conversation.model}');
|
||||
|
||||
final currentSelectedModel = _ref.read(selectedModelProvider);
|
||||
|
||||
// If the conversation's model is different from the currently selected one
|
||||
if (currentSelectedModel?.id != conversation.model) {
|
||||
debugPrint(
|
||||
'DEBUG: Conversation model (${conversation.model}) differs from selected model (${currentSelectedModel?.id})',
|
||||
);
|
||||
|
||||
// Get available models to find the matching one
|
||||
try {
|
||||
final models = await _ref.read(modelsProvider.future);
|
||||
debugPrint('DEBUG: Available models count: ${models.length}');
|
||||
|
||||
if (models.isEmpty) {
|
||||
debugPrint(
|
||||
'DEBUG: No models available, cannot update selected model',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,16 +95,11 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
if (conversationModel != null) {
|
||||
// Update the selected model
|
||||
_ref.read(selectedModelProvider.notifier).state = conversationModel;
|
||||
debugPrint(
|
||||
'DEBUG: Updated selected model to ${conversationModel.name} (${conversationModel.id}) for conversation ${conversation.id}',
|
||||
);
|
||||
} else {
|
||||
debugPrint(
|
||||
'DEBUG: Conversation model ${conversation.model} not found in available models: ${models.map((m) => m.id).join(', ')}',
|
||||
);
|
||||
// Model not found in available models - silently continue
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to update model for conversation: $e');
|
||||
// Model update failed - silently continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,25 +154,15 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
}
|
||||
|
||||
void appendToLastMessage(String content) {
|
||||
debugPrint('DEBUG: appendToLastMessage called with: "$content"');
|
||||
|
||||
if (state.isEmpty) {
|
||||
debugPrint('DEBUG: No messages to append to');
|
||||
return;
|
||||
}
|
||||
|
||||
final lastMessage = state.last;
|
||||
if (lastMessage.role != 'assistant') {
|
||||
debugPrint(
|
||||
'DEBUG: Last message is not assistant, role: ${lastMessage.role}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Appending to message ${lastMessage.id}, current length: ${lastMessage.content.length}',
|
||||
);
|
||||
|
||||
// If the current content is just the typing indicator, replace it instead of appending
|
||||
final newContent = lastMessage.content == '[TYPING_INDICATOR]'
|
||||
? content
|
||||
@@ -208,30 +172,22 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
...state.sublist(0, state.length - 1),
|
||||
lastMessage.copyWith(content: newContent),
|
||||
];
|
||||
debugPrint('DEBUG: New content length: ${state.last.content.length}');
|
||||
}
|
||||
|
||||
void replaceLastMessageContent(String content) {
|
||||
debugPrint('DEBUG: replaceLastMessageContent called with: "$content"');
|
||||
if (state.isEmpty) {
|
||||
debugPrint('DEBUG: No messages to replace content for');
|
||||
return;
|
||||
}
|
||||
|
||||
final lastMessage = state.last;
|
||||
if (lastMessage.role != 'assistant') {
|
||||
debugPrint(
|
||||
'DEBUG: Last message is not assistant, role: ${lastMessage.role}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: Replacing content for message ${lastMessage.id}');
|
||||
state = [
|
||||
...state.sublist(0, state.length - 1),
|
||||
lastMessage.copyWith(content: content),
|
||||
];
|
||||
debugPrint('DEBUG: Replaced content length: ${state.last.content.length}');
|
||||
}
|
||||
|
||||
void finishStreaming() {
|
||||
@@ -249,7 +205,7 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint(
|
||||
'DEBUG: ChatMessagesNotifier disposing - cancelling ${_subscriptions.length} subscriptions',
|
||||
'ChatMessagesNotifier disposing - ${_subscriptions.length} subscriptions',
|
||||
);
|
||||
|
||||
// Cancel all tracked subscriptions
|
||||
@@ -265,22 +221,17 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
_conversationListener?.close();
|
||||
_conversationListener = null;
|
||||
|
||||
debugPrint('DEBUG: ChatMessagesNotifier disposed successfully');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Start a new chat (unified function for both "New Chat" button and home screen)
|
||||
void startNewChat(dynamic ref) {
|
||||
debugPrint('DEBUG: Starting new chat - clearing all state');
|
||||
|
||||
// Clear active conversation
|
||||
ref.read(activeConversationProvider.notifier).state = null;
|
||||
|
||||
// Clear messages
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
|
||||
debugPrint('DEBUG: New chat state cleared');
|
||||
}
|
||||
|
||||
// Available tools provider
|
||||
@@ -332,19 +283,14 @@ bool validateFileCount(int currentCount, int newFilesCount, int? maxCount) {
|
||||
|
||||
// Helper function to get file content as base64
|
||||
Future<String?> _getFileAsBase64(dynamic api, String fileId) async {
|
||||
debugPrint('DEBUG: _getFileAsBase64 called for fileId: $fileId');
|
||||
|
||||
// Check if this is already a data URL (for images)
|
||||
if (fileId.startsWith('data:')) {
|
||||
debugPrint('DEBUG: FileId is already a data URL, returning as-is');
|
||||
return fileId;
|
||||
}
|
||||
|
||||
try {
|
||||
// First, get file info to determine if it's an image
|
||||
debugPrint('DEBUG: Getting file info for $fileId');
|
||||
final fileInfo = await api.getFileInfo(fileId);
|
||||
debugPrint('DEBUG: File info received: $fileInfo');
|
||||
|
||||
// Try different fields for filename - check all possible field names
|
||||
final fileName =
|
||||
@@ -356,29 +302,19 @@ Future<String?> _getFileAsBase64(dynamic api, String fileId) async {
|
||||
fileInfo['original_filename'] ??
|
||||
'';
|
||||
|
||||
debugPrint('DEBUG: Processing file: $fileName (fileId: $fileId)');
|
||||
|
||||
final ext = fileName.toLowerCase().split('.').last;
|
||||
debugPrint('DEBUG: File extension: $ext');
|
||||
|
||||
// Only process image files
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp'].contains(ext)) {
|
||||
debugPrint('DEBUG: Skipping non-image file: $fileName (extension: $ext)');
|
||||
return null;
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: Getting base64 content for image: $fileName');
|
||||
|
||||
// Get file content as base64 string
|
||||
final fileContent = await api.getFileContent(fileId);
|
||||
debugPrint(
|
||||
'DEBUG: Got file content for $fileName, type: ${fileContent.runtimeType}, length: ${fileContent.length}',
|
||||
);
|
||||
|
||||
// The API service returns base64 string directly
|
||||
return fileContent;
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Error getting file content for $fileId: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -389,22 +325,16 @@ Future<void> regenerateMessage(
|
||||
String userMessageContent,
|
||||
List<String>? attachments,
|
||||
) async {
|
||||
debugPrint(
|
||||
'DEBUG: regenerateMessage called with content: $userMessageContent',
|
||||
);
|
||||
|
||||
final reviewerMode = ref.read(reviewerModeProvider);
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final selectedModel = ref.read(selectedModelProvider);
|
||||
|
||||
if ((!reviewerMode && api == null) || selectedModel == null) {
|
||||
debugPrint('DEBUG: Missing API service or model for regeneration');
|
||||
throw Exception('No API service or model selected');
|
||||
}
|
||||
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
if (activeConversation == null) {
|
||||
debugPrint('DEBUG: No active conversation for regeneration');
|
||||
throw Exception('No active conversation');
|
||||
}
|
||||
|
||||
@@ -503,7 +433,6 @@ Future<void> regenerateMessage(
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
await _saveConversationLocally(ref);
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Error during message regeneration: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -515,9 +444,6 @@ Future<void> sendMessage(
|
||||
List<String>? attachments, [
|
||||
List<String>? toolIds,
|
||||
]) async {
|
||||
debugPrint(
|
||||
'DEBUG: sendMessage called with message: $message, attachments: $attachments, tools: $toolIds',
|
||||
);
|
||||
await _sendMessageInternal(ref, message, attachments, toolIds);
|
||||
}
|
||||
|
||||
@@ -528,33 +454,19 @@ Future<void> _sendMessageInternal(
|
||||
List<String>? attachments, [
|
||||
List<String>? toolIds,
|
||||
]) async {
|
||||
debugPrint('DEBUG: _sendMessageInternal called');
|
||||
debugPrint('DEBUG: Message: $message');
|
||||
debugPrint('DEBUG: Attachments: $attachments');
|
||||
|
||||
final reviewerMode = ref.read(reviewerModeProvider);
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final selectedModel = ref.read(selectedModelProvider);
|
||||
|
||||
debugPrint('DEBUG: API service: ${api != null ? 'available' : 'null'}');
|
||||
debugPrint('DEBUG: Selected model: ${selectedModel?.name ?? 'null'}');
|
||||
|
||||
if ((!reviewerMode && api == null) || selectedModel == null) {
|
||||
debugPrint('DEBUG: Missing API service or model');
|
||||
throw Exception('No API service or model selected');
|
||||
}
|
||||
|
||||
// Check if we need to create a new conversation first
|
||||
var activeConversation = ref.read(activeConversationProvider);
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Active conversation before send: ${activeConversation?.id}',
|
||||
);
|
||||
|
||||
// Create user message first
|
||||
debugPrint(
|
||||
'DEBUG: Creating user message with attachments: $attachments, tools: $toolIds',
|
||||
);
|
||||
|
||||
final userMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'user',
|
||||
@@ -565,9 +477,6 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
if (activeConversation == null) {
|
||||
// Create new conversation with the first message included
|
||||
debugPrint('DEBUG: Creating new conversation with first message');
|
||||
|
||||
// Create local conversation first
|
||||
final localConversation = Conversation(
|
||||
id: const Uuid().v4(),
|
||||
title: 'New Chat',
|
||||
@@ -602,22 +511,12 @@ Future<void> _sendMessageInternal(
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(userMessage);
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Created conversation ${serverConversation.id} on server with first message',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Server conversation ID: ${serverConversation.id}, Title: ${serverConversation.title}',
|
||||
);
|
||||
|
||||
// Invalidate conversations provider to refresh the list
|
||||
// Adding a small delay to prevent rapid invalidations that could cause duplicates
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
ref.invalidate(conversationsProvider);
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'DEBUG: Failed to create conversation on server, using local: $e',
|
||||
);
|
||||
// Still add the message locally
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(userMessage);
|
||||
}
|
||||
@@ -628,7 +527,6 @@ Future<void> _sendMessageInternal(
|
||||
} else {
|
||||
// Add user message to existing conversation
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(userMessage);
|
||||
debugPrint('DEBUG: User message added with ID: ${userMessage.id}');
|
||||
}
|
||||
|
||||
// We'll add the assistant message placeholder after we get the message ID from the API (or immediately in reviewer mode)
|
||||
@@ -687,27 +585,9 @@ Future<void> _sendMessageInternal(
|
||||
// Skip only empty assistant message placeholders that are currently streaming
|
||||
// Include completed messages (both user and assistant) for conversation history
|
||||
if (msg.role.isNotEmpty && msg.content.isNotEmpty && !msg.isStreaming) {
|
||||
debugPrint(
|
||||
'DEBUG: Processing message: role=${msg.role}, content=${msg.content.substring(0, msg.content.length > 50 ? 50 : msg.content.length)}..., attachments=${msg.attachmentIds}',
|
||||
);
|
||||
|
||||
// Check if message has attachments (images and non-images)
|
||||
if (msg.attachmentIds != null && msg.attachmentIds!.isNotEmpty) {
|
||||
debugPrint(
|
||||
'DEBUG: Message has ${msg.attachmentIds!.length} attachments',
|
||||
);
|
||||
|
||||
// Check if this is a Gemini model that requires special handling
|
||||
final isGeminiModel = selectedModel.id.toLowerCase().contains('gemini');
|
||||
debugPrint('DEBUG: Is Gemini model: $isGeminiModel');
|
||||
debugPrint('DEBUG: Model ID: ${selectedModel.id}');
|
||||
debugPrint('DEBUG: Model name: ${selectedModel.name}');
|
||||
debugPrint(
|
||||
'DEBUG: Model ID lowercase: ${selectedModel.id.toLowerCase()}',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Contains gemini: ${selectedModel.id.toLowerCase().contains('gemini')}',
|
||||
);
|
||||
// All models use the same content array format (OpenWebUI standard)
|
||||
|
||||
// Use the same content array format for all models (OpenWebUI standard)
|
||||
final List<Map<String, dynamic>> contentArray = [];
|
||||
@@ -717,26 +597,19 @@ Future<void> _sendMessageInternal(
|
||||
// Add text content first
|
||||
if (msg.content.isNotEmpty) {
|
||||
contentArray.add({'type': 'text', 'text': msg.content});
|
||||
debugPrint('DEBUG: Added text content to array');
|
||||
}
|
||||
|
||||
// Add image attachments with proper MIME type handling; collect non-image attachments
|
||||
for (final attachmentId in msg.attachmentIds!) {
|
||||
debugPrint('DEBUG: Processing attachment: $attachmentId');
|
||||
try {
|
||||
final base64Data = await _getFileAsBase64(api, attachmentId);
|
||||
if (base64Data != null) {
|
||||
debugPrint(
|
||||
'DEBUG: Got base64 data for attachment $attachmentId, length: ${base64Data.length}',
|
||||
);
|
||||
|
||||
// Check if this is already a data URL
|
||||
if (base64Data.startsWith('data:')) {
|
||||
contentArray.add({
|
||||
'type': 'image_url',
|
||||
'image_url': {'url': base64Data},
|
||||
});
|
||||
debugPrint('DEBUG: Added image with data URL');
|
||||
} else {
|
||||
// For server files, determine MIME type from file extension
|
||||
// Only call getFileInfo if attachmentId is not a data URL
|
||||
@@ -754,46 +627,31 @@ Future<void> _sendMessageInternal(
|
||||
mimeType = 'image/webp';
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Using MIME type: $mimeType for file: $fileName',
|
||||
);
|
||||
|
||||
contentArray.add({
|
||||
'type': 'image_url',
|
||||
'image_url': {'url': 'data:$mimeType;base64,$base64Data'},
|
||||
});
|
||||
debugPrint('DEBUG: Added image with MIME type: $mimeType');
|
||||
} else {
|
||||
debugPrint('DEBUG: Skipping getFileInfo for data URL');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'DEBUG: No base64 data returned for attachment $attachmentId',
|
||||
);
|
||||
// Treat as non-image file; include minimal descriptor so server can resolve by id
|
||||
nonImageFiles.add({'id': attachmentId, 'type': 'file'});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to load attachment $attachmentId: $e');
|
||||
// Handle attachment processing errors silently
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('DEBUG: Final content array length: ${contentArray.length}');
|
||||
final messageMap = <String, dynamic>{
|
||||
'role': msg.role,
|
||||
'content': contentArray,
|
||||
};
|
||||
if (nonImageFiles.isNotEmpty) {
|
||||
debugPrint(
|
||||
'DEBUG: Adding ${nonImageFiles.length} non-image file(s) to message map',
|
||||
);
|
||||
messageMap['files'] = nonImageFiles;
|
||||
}
|
||||
conversationMessages.add(messageMap);
|
||||
} else {
|
||||
// Regular text-only message
|
||||
debugPrint('DEBUG: Regular text-only message');
|
||||
conversationMessages.add({'role': msg.role, 'content': msg.content});
|
||||
}
|
||||
}
|
||||
@@ -803,17 +661,10 @@ Future<void> _sendMessageInternal(
|
||||
final webSearchEnabled = ref.read(webSearchEnabledProvider);
|
||||
final imageGenerationEnabled = ref.read(imageGenerationEnabledProvider);
|
||||
|
||||
// Debug log to track feature toggle states
|
||||
debugPrint('DEBUG: Web search toggle state: $webSearchEnabled');
|
||||
debugPrint('DEBUG: Image generation toggle state: $imageGenerationEnabled');
|
||||
|
||||
// Prepare tools list - pass tool IDs directly
|
||||
final List<String>? toolIdsForApi = (toolIds != null && toolIds.isNotEmpty)
|
||||
? toolIds
|
||||
: null;
|
||||
if (toolIdsForApi != null) {
|
||||
debugPrint('DEBUG: Including tool IDs: $toolIdsForApi');
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the model's actual supported parameters if available
|
||||
@@ -827,12 +678,6 @@ Future<void> _sendMessageInternal(
|
||||
'structured_outputs',
|
||||
];
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Model ${selectedModel.name} supported parameters: ${selectedModel.supportedParameters}',
|
||||
);
|
||||
debugPrint('DEBUG: Model ID: ${selectedModel.id}');
|
||||
debugPrint('DEBUG: Is multimodal: ${selectedModel.isMultimodal}');
|
||||
|
||||
// Create comprehensive model item matching OpenWebUI format exactly
|
||||
final modelItem = {
|
||||
'id': selectedModel.id,
|
||||
@@ -917,29 +762,6 @@ Future<void> _sendMessageInternal(
|
||||
'tags': <dynamic>[],
|
||||
};
|
||||
|
||||
debugPrint('DEBUG: Using basic model item for ${selectedModel.name}');
|
||||
|
||||
debugPrint('DEBUG: Final conversationMessages being sent to API:');
|
||||
debugPrint('DEBUG: Messages count: ${conversationMessages.length}');
|
||||
for (int i = 0; i < conversationMessages.length; i++) {
|
||||
final msg = conversationMessages[i];
|
||||
debugPrint(
|
||||
'DEBUG: Message $i: role=${msg['role']}, content type=${msg['content'].runtimeType}',
|
||||
);
|
||||
if (msg['content'] is List) {
|
||||
final contentArray = msg['content'] as List;
|
||||
debugPrint(
|
||||
'DEBUG: Message $i content array length: ${contentArray.length}',
|
||||
);
|
||||
for (int j = 0; j < contentArray.length; j++) {
|
||||
final item = contentArray[j];
|
||||
debugPrint(
|
||||
'DEBUG: Content item $j: type=${item['type']}, has_image_url=${item.containsKey('image_url')}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If image generation is enabled and we want image-only, skip assistant SSE
|
||||
if (imageGenerationEnabled) {
|
||||
// Create assistant placeholder
|
||||
@@ -955,7 +777,6 @@ Future<void> _sendMessageInternal(
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(imageOnlyAssistant);
|
||||
|
||||
try {
|
||||
debugPrint('DEBUG: Image-only mode - triggering image generation');
|
||||
final imageResponse = await api.generateImage(prompt: message);
|
||||
|
||||
// Extract image URLs or base64 data URIs from response
|
||||
@@ -1075,7 +896,6 @@ Future<void> _sendMessageInternal(
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Image-only mode generation failed: $e');
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
}
|
||||
|
||||
@@ -1100,10 +920,6 @@ Future<void> _sendMessageInternal(
|
||||
final assistantMessageId = response.messageId;
|
||||
final sessionId = response.sessionId;
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Response IDs - Message: $assistantMessageId, Session: $sessionId',
|
||||
);
|
||||
|
||||
// Add assistant message placeholder with the generated ID and immediate typing indicator
|
||||
final assistantMessage = ChatMessage(
|
||||
id: assistantMessageId,
|
||||
@@ -1305,8 +1121,6 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
final streamSubscription = persistentController.stream.listen(
|
||||
(chunk) {
|
||||
debugPrint('DEBUG: Received stream chunk: "$chunk"');
|
||||
|
||||
// Check for web search indicators in the stream
|
||||
if (webSearchEnabled && !isSearching) {
|
||||
// Check if this is the start of web search
|
||||
@@ -1359,7 +1173,6 @@ Future<void> _sendMessageInternal(
|
||||
onDone: () async {
|
||||
// Unregister from persistent service
|
||||
persistentService.unregisterStream(streamId);
|
||||
debugPrint('DEBUG: Stream completed in chat provider');
|
||||
// Mark streaming as complete immediately for better UX
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
|
||||
@@ -1402,15 +1215,6 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
// Send chat completed notification to OpenWebUI first
|
||||
try {
|
||||
debugPrint(
|
||||
'DEBUG: Sending chat completed notification to OpenWebUI',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Active conversation ID: ${activeConversation.id}',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Chat ID: ${activeConversation.id}, Message ID: $assistantMessageId, Messages: ${formattedMessages.length}',
|
||||
);
|
||||
await api.sendChatCompleted(
|
||||
chatId: activeConversation.id,
|
||||
messageId: assistantMessageId, // Use message ID from response
|
||||
@@ -1419,25 +1223,17 @@ Future<void> _sendMessageInternal(
|
||||
modelItem: modelItem, // Include model metadata
|
||||
sessionId: sessionId, // Include session ID
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Chat completed notification sent successfully for chat ID: ${activeConversation.id}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Chat completed notification failed: $e');
|
||||
debugPrint('DEBUG: Error details: $e');
|
||||
// Continue even if this fails - it's non-critical
|
||||
}
|
||||
|
||||
// Fetch the latest conversation state without waiting for title generation
|
||||
debugPrint('DEBUG: Fetching latest conversation state...');
|
||||
debugPrint('DEBUG: Current message count: ${messages.length}');
|
||||
|
||||
try {
|
||||
// Quick fetch to get the current state - no waiting for title generation
|
||||
final updatedConv = await api.getConversation(
|
||||
activeConversation.id,
|
||||
);
|
||||
debugPrint('DEBUG: Current title: ${updatedConv.title}');
|
||||
|
||||
// Check if we should update the title (only on first response and if server has one)
|
||||
final shouldUpdateTitle =
|
||||
@@ -1476,20 +1272,8 @@ Future<void> _sendMessageInternal(
|
||||
if (serverMsg != null && serverMsg.content.isNotEmpty) {
|
||||
// Use server content if available and non-empty
|
||||
// This replaces any temporary progress indicators with real content
|
||||
debugPrint(
|
||||
'DEBUG: Replacing local content with server content for message ${localMsg.id}',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Local content: "${localMsg.content.substring(0, math.min(100, localMsg.content.length))}..."',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Server content: "${serverMsg.content.substring(0, math.min(100, serverMsg.content.length))}..."',
|
||||
);
|
||||
|
||||
// Stream the server content through StreamChunker for word-by-word effect
|
||||
debugPrint(
|
||||
'DEBUG: Streaming server content through chunker for word-by-word display',
|
||||
);
|
||||
|
||||
// Clear only the last message content in-place to avoid list reset flicker
|
||||
final currentList = [...currentMessages];
|
||||
@@ -1521,22 +1305,17 @@ Future<void> _sendMessageInternal(
|
||||
// Process chunks
|
||||
chunkedStream.listen(
|
||||
(chunk) {
|
||||
debugPrint('DEBUG: Server content chunk: "$chunk"');
|
||||
ref
|
||||
.read(chatMessagesProvider.notifier)
|
||||
.appendToLastMessage(chunk);
|
||||
},
|
||||
onDone: () {
|
||||
debugPrint('DEBUG: Server content streaming completed');
|
||||
// Mark streaming as complete
|
||||
ref
|
||||
.read(chatMessagesProvider.notifier)
|
||||
.finishStreaming();
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint(
|
||||
'DEBUG: Server content streaming error: $error',
|
||||
);
|
||||
// Fall back to direct replacement
|
||||
final currentMessages = ref.read(chatMessagesProvider);
|
||||
if (currentMessages.isNotEmpty) {
|
||||
@@ -1559,9 +1338,6 @@ Future<void> _sendMessageInternal(
|
||||
} else {
|
||||
// Handle case where streaming failed and we still have typing indicator
|
||||
if (localMsg.content == '[TYPING_INDICATOR]') {
|
||||
debugPrint(
|
||||
'DEBUG: Found orphaned typing indicator for message ${localMsg.id} - replacing with empty content',
|
||||
);
|
||||
// Replace typing indicator with empty content so UI can show loading state
|
||||
updatedMessages.add(
|
||||
localMsg.copyWith(content: '', isStreaming: false),
|
||||
@@ -1574,10 +1350,6 @@ Future<void> _sendMessageInternal(
|
||||
}
|
||||
|
||||
if (shouldUpdateTitle) {
|
||||
debugPrint(
|
||||
'DEBUG: Server generated title: ${updatedConv.title}',
|
||||
);
|
||||
|
||||
// Ensure the title is reasonable (not too long)
|
||||
final cleanTitle = updatedConv.title.length > 100
|
||||
? '${updatedConv.title.substring(0, 100)}...'
|
||||
@@ -1592,8 +1364,6 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
updatedConversation;
|
||||
|
||||
debugPrint('DEBUG: Conversation title updated successfully');
|
||||
} else {
|
||||
// Update just the messages without changing title
|
||||
final updatedConversation = activeConversation.copyWith(
|
||||
@@ -1603,26 +1373,18 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
updatedConversation;
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Conversation content updated with server response',
|
||||
);
|
||||
}
|
||||
|
||||
// Streaming already marked as complete when stream ended
|
||||
debugPrint('DEBUG: Server content replacement completed');
|
||||
|
||||
// Start background title check for first message exchanges
|
||||
if (messages.length <= 2 && updatedConv.title == 'New Chat') {
|
||||
debugPrint('DEBUG: Starting background title check...');
|
||||
_checkForTitleInBackground(ref, activeConversation.id);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to fetch server content: $e');
|
||||
// Streaming already marked as complete when stream ended
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Chat completed error: $e');
|
||||
// Continue without failing the entire process
|
||||
// Note: Conversation still syncs via _saveConversationToServer
|
||||
// Streaming already marked as complete when stream ended
|
||||
@@ -1631,16 +1393,13 @@ Future<void> _sendMessageInternal(
|
||||
}
|
||||
|
||||
// Save conversation to OpenWebUI server only after streaming is complete
|
||||
debugPrint('DEBUG: About to save conversation to server...');
|
||||
// Add a small delay to ensure the last message content is fully updated
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await _saveConversationToServer(ref);
|
||||
debugPrint('DEBUG: Conversation save completed');
|
||||
|
||||
// If image generation is enabled, trigger image generation with the user's prompt
|
||||
if (imageGenerationEnabled) {
|
||||
try {
|
||||
debugPrint('DEBUG: Image generation enabled - triggering request');
|
||||
final imageResponse = await api.generateImage(prompt: message);
|
||||
|
||||
// Extract image URLs or base64 data URIs from response
|
||||
@@ -1734,10 +1493,6 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
final generatedFiles = extractGeneratedFiles(imageResponse);
|
||||
if (generatedFiles.isNotEmpty) {
|
||||
debugPrint(
|
||||
'DEBUG: Image generation returned ${generatedFiles.length} file(s)',
|
||||
);
|
||||
|
||||
// Attach images to the last assistant message
|
||||
ref
|
||||
.read(chatMessagesProvider.notifier)
|
||||
@@ -1750,16 +1505,13 @@ Future<void> _sendMessageInternal(
|
||||
|
||||
// Save updated conversation with images
|
||||
await _saveConversationToServer(ref);
|
||||
} else {
|
||||
debugPrint('DEBUG: No images found in generation response');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Image generation failed: $e');
|
||||
// Handle image generation errors silently
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
debugPrint('DEBUG: Stream error in chat provider: $error');
|
||||
// Mark streaming as complete on error
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
|
||||
@@ -1768,12 +1520,6 @@ Future<void> _sendMessageInternal(
|
||||
if (error.toString().contains(
|
||||
'Socket.IO streaming not fully implemented',
|
||||
)) {
|
||||
debugPrint(
|
||||
'DEBUG: Socket.IO streaming failed, but server may have generated response',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Keeping assistant message for server content replacement',
|
||||
);
|
||||
// Don't remove the message - let the server content replacement handle it
|
||||
// The onDone callback will fetch the actual response from the server
|
||||
return; // Exit early to avoid removing the message
|
||||
@@ -1785,9 +1531,6 @@ Future<void> _sendMessageInternal(
|
||||
// Handle different types of errors
|
||||
if (error.toString().contains('400')) {
|
||||
// Bad request errors - likely malformed request format
|
||||
debugPrint(
|
||||
'DEBUG: Bad request error (400) - malformed request format',
|
||||
);
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1814,7 +1557,6 @@ This might be because:
|
||||
ref.invalidate(authStateManagerProvider);
|
||||
} else if (error.toString().contains('500')) {
|
||||
// Server errors - add user-friendly error message
|
||||
debugPrint('DEBUG: Server error (500) - OpenWebUI server issue');
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1838,7 +1580,6 @@ This usually means:
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||
} else if (error.toString().contains('timeout')) {
|
||||
// Timeout errors
|
||||
debugPrint('DEBUG: Request timeout error');
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1864,7 +1605,6 @@ This might be because:
|
||||
|
||||
// Don't throw the error to prevent unhandled exceptions
|
||||
// The error message has been added to the chat
|
||||
debugPrint('DEBUG: Chat error handled gracefully: ${error.toString()}');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1878,7 +1618,6 @@ This might be because:
|
||||
|
||||
// Add user-friendly error message instead of rethrowing
|
||||
if (e.toString().contains('400')) {
|
||||
debugPrint('DEBUG: Bad request error (400) during initial request setup');
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1895,7 +1634,6 @@ Please try sending the message again, or try without attachments.''',
|
||||
);
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||
} else if (e.toString().contains('500')) {
|
||||
debugPrint('DEBUG: Server error (500) during initial request setup');
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1920,7 +1658,6 @@ Please try sending the message again, or try without attachments.''',
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(errorMessage);
|
||||
} else {
|
||||
// For other errors, provide a generic message and rethrow
|
||||
debugPrint('DEBUG: Unexpected error during chat request: $e');
|
||||
final errorMessage = ChatMessage(
|
||||
id: const Uuid().v4(),
|
||||
role: 'assistant',
|
||||
@@ -1946,10 +1683,6 @@ Future<void> _triggerTitleGeneration(
|
||||
final api = ref.read(apiServiceProvider);
|
||||
if (api == null) return;
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Requesting title generation for conversation $conversationId',
|
||||
);
|
||||
|
||||
// Call the title generation endpoint
|
||||
final generatedTitle = await api.generateTitle(
|
||||
conversationId: conversationId,
|
||||
@@ -1960,8 +1693,6 @@ Future<void> _triggerTitleGeneration(
|
||||
if (generatedTitle != null &&
|
||||
generatedTitle.isNotEmpty &&
|
||||
generatedTitle != 'New Chat') {
|
||||
debugPrint('DEBUG: Title generated successfully: $generatedTitle');
|
||||
|
||||
// Update the active conversation with the new title
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
if (activeConversation?.id == conversationId) {
|
||||
@@ -1973,9 +1704,6 @@ Future<void> _triggerTitleGeneration(
|
||||
|
||||
// Save the updated title to the server
|
||||
try {
|
||||
debugPrint(
|
||||
'DEBUG: Saving generated title to server: $generatedTitle',
|
||||
);
|
||||
final currentMessages = ref.read(chatMessagesProvider);
|
||||
await api.updateConversationWithMessages(
|
||||
conversationId,
|
||||
@@ -1983,21 +1711,18 @@ Future<void> _triggerTitleGeneration(
|
||||
title: generatedTitle,
|
||||
model: model,
|
||||
);
|
||||
debugPrint('DEBUG: Title saved to server successfully');
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to save title to server: $e');
|
||||
// Handle title save errors silently
|
||||
}
|
||||
|
||||
// Refresh the conversations list
|
||||
ref.invalidate(conversationsProvider);
|
||||
}
|
||||
} else {
|
||||
debugPrint('DEBUG: Title generation did not return a valid title');
|
||||
// Fall back to background checking
|
||||
_checkForTitleInBackground(ref, conversationId);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Title generation failed: $e');
|
||||
// Fall back to background checking
|
||||
_checkForTitleInBackground(ref, conversationId);
|
||||
}
|
||||
@@ -2021,10 +1746,6 @@ Future<void> _checkForTitleInBackground(
|
||||
final updatedConv = await api.getConversation(conversationId);
|
||||
|
||||
if (updatedConv.title != 'New Chat' && updatedConv.title.isNotEmpty) {
|
||||
debugPrint(
|
||||
'DEBUG: Background title update found: ${updatedConv.title}',
|
||||
);
|
||||
|
||||
// Update the active conversation with the new title
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
if (activeConversation?.id == conversationId) {
|
||||
@@ -2046,34 +1767,23 @@ Future<void> _checkForTitleInBackground(
|
||||
await Future.delayed(Duration(seconds: 2 + (i * 2)));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Background title check error: $e');
|
||||
break; // Stop on error
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Background title check completed without finding generated title',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Background title check failed: $e');
|
||||
// Handle background title check errors silently
|
||||
}
|
||||
}
|
||||
|
||||
// Save current conversation to OpenWebUI server
|
||||
Future<void> _saveConversationToServer(dynamic ref) async {
|
||||
try {
|
||||
debugPrint('DEBUG: _saveConversationToServer started');
|
||||
final api = ref.read(apiServiceProvider);
|
||||
final messages = ref.read(chatMessagesProvider);
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
final selectedModel = ref.read(selectedModelProvider);
|
||||
|
||||
debugPrint(
|
||||
'DEBUG: Conversation save state - API: ${api != null}, Messages: ${messages.length}, Active: ${activeConversation?.id}, Model: ${selectedModel?.id}',
|
||||
);
|
||||
|
||||
if (api == null || messages.isEmpty || activeConversation == null) {
|
||||
debugPrint('DEBUG: Skipping conversation save - missing required data');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2084,20 +1794,10 @@ Future<void> _saveConversationToServer(dynamic ref) async {
|
||||
(lastMessage.files == null || lastMessage.files!.isEmpty) &&
|
||||
(lastMessage.attachmentIds == null ||
|
||||
lastMessage.attachmentIds!.isEmpty)) {
|
||||
debugPrint(
|
||||
'DEBUG: Skipping conversation save - assistant message has no content or files',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the existing conversation with all messages (including assistant response)
|
||||
debugPrint(
|
||||
'DEBUG: Updating conversation ${activeConversation.id} with complete message history',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Conversation ID being updated: ${activeConversation.id}',
|
||||
);
|
||||
debugPrint('DEBUG: Number of messages to save: ${messages.length}');
|
||||
|
||||
try {
|
||||
await api.updateConversationWithMessages(
|
||||
@@ -2113,31 +1813,18 @@ Future<void> _saveConversationToServer(dynamic ref) async {
|
||||
);
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state = updatedConversation;
|
||||
debugPrint(
|
||||
'DEBUG: Successfully updated conversation on server: ${activeConversation.id}',
|
||||
);
|
||||
debugPrint(
|
||||
'DEBUG: Updated conversation title: ${updatedConversation.title}',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Failed to update conversation on server: $e');
|
||||
debugPrint('DEBUG: Error details: $e');
|
||||
// Fallback to local storage if server update fails
|
||||
await _saveConversationLocally(ref);
|
||||
return;
|
||||
}
|
||||
|
||||
// Refresh conversations list to show the updated conversation
|
||||
debugPrint(
|
||||
'DEBUG: Invalidating conversations provider after successful save',
|
||||
);
|
||||
// Adding a small delay to prevent rapid invalidations that could cause duplicates
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
ref.invalidate(conversationsProvider);
|
||||
debugPrint('DEBUG: Conversations provider invalidated');
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Error saving conversation to server: $e');
|
||||
// Fallback to local storage
|
||||
await _saveConversationLocally(ref);
|
||||
}
|
||||
@@ -2186,7 +1873,7 @@ Future<void> _saveConversationLocally(dynamic ref) async {
|
||||
ref.read(activeConversationProvider.notifier).state = updatedConversation;
|
||||
ref.invalidate(conversationsProvider);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving conversation locally: $e');
|
||||
// Handle local storage errors silently
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2358,9 +2045,6 @@ final regenerateLastMessageProvider = Provider<Future<void> Function()>((ref) {
|
||||
ref.read(chatMessagesProvider.notifier).addMessage(placeholder);
|
||||
|
||||
try {
|
||||
debugPrint(
|
||||
'DEBUG: Regenerate image-only - triggering image generation',
|
||||
);
|
||||
final imageResponse = await api.generateImage(
|
||||
prompt: lastUserMessage.content,
|
||||
);
|
||||
@@ -2473,7 +2157,6 @@ final regenerateLastMessageProvider = Provider<Future<void> Function()>((ref) {
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Regenerate image-only failed: $e');
|
||||
ref.read(chatMessagesProvider.notifier).finishStreaming();
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -195,7 +195,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
);
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state = welcomeConv;
|
||||
debugPrint('Auto-loaded demo conversation: ${welcomeConv.title}');
|
||||
debugPrint('Auto-loaded demo conversation');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,35 +240,20 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
}
|
||||
|
||||
void _handleMessageSend(String text, dynamic selectedModel) async {
|
||||
debugPrint('DEBUG: Starting message send process');
|
||||
debugPrint('DEBUG: Message text: $text');
|
||||
debugPrint('DEBUG: Selected model: ${selectedModel?.name ?? 'null'}');
|
||||
|
||||
if (selectedModel == null) {
|
||||
debugPrint('DEBUG: No model selected');
|
||||
return;
|
||||
}
|
||||
|
||||
final isOnline = ref.read(isOnlineProvider);
|
||||
final isReviewerMode = ref.read(reviewerModeProvider);
|
||||
debugPrint(
|
||||
'DEBUG: Online status: $isOnline, Reviewer mode: $isReviewerMode',
|
||||
);
|
||||
|
||||
if (!isOnline && !isReviewerMode) {
|
||||
debugPrint('DEBUG: Offline - cannot send message');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get attached files and use uploadedFileIds when sendMessage is updated to accept file IDs
|
||||
final attachedFiles = ref.read(attachedFilesProvider);
|
||||
debugPrint('DEBUG: Attached files count: ${attachedFiles.length}');
|
||||
|
||||
for (final file in attachedFiles) {
|
||||
debugPrint(
|
||||
'DEBUG: File - Name: ${file.fileName}, Status: ${file.status}, FileId: ${file.fileId}',
|
||||
);
|
||||
}
|
||||
|
||||
final uploadedFileIds = attachedFiles
|
||||
.where(
|
||||
@@ -279,11 +264,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
.map((file) => file.fileId!)
|
||||
.toList();
|
||||
|
||||
debugPrint('DEBUG: Uploaded file IDs: $uploadedFileIds');
|
||||
|
||||
// Get selected tools
|
||||
final toolIds = ref.read(selectedToolIdsProvider);
|
||||
debugPrint('DEBUG: Selected tool IDs: $toolIds');
|
||||
|
||||
// Send message with file attachments and tools using existing provider logic
|
||||
await sendMessage(
|
||||
@@ -293,11 +275,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
toolIds.isNotEmpty ? toolIds : null,
|
||||
);
|
||||
|
||||
debugPrint('DEBUG: Message sent successfully');
|
||||
|
||||
// Clear attachments after successful send
|
||||
ref.read(attachedFilesProvider.notifier).clearAll();
|
||||
debugPrint('DEBUG: Attachments cleared');
|
||||
|
||||
// Scroll to bottom after sending message (only if user was near bottom)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -311,7 +290,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('DEBUG: Message send error: $e');
|
||||
// Message send failed - error already handled by sendMessage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user