refactor: debug logs

This commit is contained in:
cogwheel0
2025-08-21 19:11:17 +05:30
parent 69e8243f31
commit b7ba048047
4 changed files with 28 additions and 421 deletions

View File

@@ -91,38 +91,6 @@ class ApiService {
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (options.path == '/api/chat/completions') {
debugPrint('===== SSE REQUEST DEBUG =====');
debugPrint('Path: ${options.path}');
debugPrint('Method: ${options.method}');
debugPrint('Headers: ${options.headers}');
debugPrint('Content-Type: ${options.contentType}');
// Log the raw data being sent
if (options.data != null) {
if (options.data is Map) {
final dataMap = options.data as Map<String, dynamic>;
DebugLogger.log('Data type: Map');
DebugLogger.log('Data keys: ${dataMap.keys.toList()}');
DebugLogger.log(
'Has background_tasks: ${dataMap.containsKey('background_tasks')}',
);
DebugLogger.log(
'Has session_id: ${dataMap.containsKey('session_id')}',
);
DebugLogger.log('Has id: ${dataMap.containsKey('id')}');
DebugLogger.log(
'Data structure logged (full data suppressed)',
);
} else {
DebugLogger.log('Data type: ${options.data.runtimeType}');
DebugLogger.log(
'Data structure logged (full data suppressed)',
);
}
}
debugPrint('===== END SSE REQUEST DEBUG =====');
}
handler.next(options);
},
),
@@ -134,9 +102,7 @@ class ApiService {
// Initialize validation interceptor asynchronously
validationInterceptor.initialize().catchError((error) {
debugPrint(
'ApiService: Failed to initialize validation interceptor: $error',
);
// Handle validation initialization errors silently
});
}
@@ -193,13 +159,11 @@ class ApiService {
result['modelsAvailable'] = models != null && models.isNotEmpty;
result['modelCount'] = models?.length ?? 0;
} catch (e) {
debugPrint('DEBUG: Error checking models: $e');
result['modelsAvailable'] = false;
}
}
} catch (e) {
result['error'] = e.toString();
debugPrint('DEBUG: Server status check failed: $e');
}
return result;
@@ -208,27 +172,18 @@ class ApiService {
// Authentication
Future<Map<String, dynamic>> login(String username, String password) async {
try {
debugPrint(
'DEBUG: Attempting login to ${serverConfig.url}/api/v1/auths/signin',
);
final response = await _dio.post(
'/api/v1/auths/signin',
data: {'email': username, 'password': password},
);
debugPrint('DEBUG: Login successful, status: ${response.statusCode}');
return response.data;
} catch (e) {
if (e is DioException) {
debugPrint('DEBUG: Login DioException: ${e.type}');
debugPrint('DEBUG: Response status: ${e.response?.statusCode}');
debugPrint('DEBUG: Response headers: ${e.response?.headers}');
debugPrint('DEBUG: Request URL: ${e.requestOptions.uri}');
// Handle specific redirect cases
if (e.response?.statusCode == 307 || e.response?.statusCode == 308) {
final location = e.response?.headers.value('location');
if (location != null) {
debugPrint('DEBUG: Server redirecting to: $location');
throw Exception(
'Server redirect detected. Please check your server URL configuration. Redirect to: $location',
);
@@ -274,7 +229,6 @@ class ApiService {
// Get default model configuration from OpenWebUI user settings
Future<String?> getDefaultModel() async {
try {
debugPrint('DEBUG: Fetching default model from user settings');
final response = await _dio.get('/api/v1/users/user/settings');
DebugLogger.log('User settings retrieved successfully');
@@ -324,19 +278,14 @@ class ApiService {
// Conversations - Updated to use correct OpenWebUI API
Future<List<Conversation>> getConversations({int? limit, int? skip}) async {
debugPrint('DEBUG: Fetching conversations from OpenWebUI API');
debugPrint('DEBUG: Making request to ${serverConfig.url}/api/v1/chats/');
debugPrint('DEBUG: Auth token present: ${authToken != null}');
List<dynamic> allRegularChats = [];
if (limit == null) {
// Fetch all conversations using pagination
debugPrint('DEBUG: Fetching ALL conversations using pagination');
int currentPage = 0;
while (true) {
debugPrint('DEBUG: Fetching page $currentPage');
final response = await _dio.get(
'/api/v1/chats/',
queryParameters: {'page': currentPage},
@@ -349,12 +298,8 @@ class ApiService {
}
final pageChats = response.data as List;
debugPrint(
'DEBUG: Page $currentPage returned ${pageChats.length} conversations',
);
if (pageChats.isEmpty) {
debugPrint('DEBUG: No more conversations, stopping pagination');
break;
}

View File

@@ -7,7 +7,7 @@ class DebugLogger {
/// Log debug information
static void log(String message) {
if (_enabled) {
debugPrint('🔍 $message');
debugPrint('DEBUG: $message');
}
}
@@ -15,9 +15,9 @@ class DebugLogger {
static void error(String message, [Object? error]) {
if (_enabled) {
if (error != null) {
debugPrint(' $message: $error');
debugPrint('ERROR: $message: $error');
} else {
debugPrint(' $message');
debugPrint('ERROR: $message');
}
}
}
@@ -25,49 +25,49 @@ class DebugLogger {
/// Log warnings
static void warning(String message) {
if (_enabled) {
debugPrint('⚠️ $message');
debugPrint('WARN: $message');
}
}
/// Log success/info messages
static void info(String message) {
if (_enabled) {
debugPrint(' $message');
debugPrint('INFO: $message');
}
}
/// Log navigation events
static void navigation(String message) {
if (_enabled) {
debugPrint('🧭 $message');
debugPrint('NAV: $message');
}
}
/// Log authentication events
static void auth(String message) {
if (_enabled) {
debugPrint('🔐 $message');
debugPrint('AUTH: $message');
}
}
/// Log streaming events
static void stream(String message) {
if (_enabled) {
debugPrint('📡 $message');
debugPrint('STREAM: $message');
}
}
/// Log validation events
static void validation(String message) {
if (_enabled) {
debugPrint(' $message');
debugPrint('VALIDATION: $message');
}
}
/// Log storage events
static void storage(String message) {
if (_enabled) {
debugPrint('💾 $message');
debugPrint('STORAGE: $message');
}
}
}

View File

@@ -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;

View File

@@ -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
}
}