diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index 7d9119c..a72dca8 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -1819,107 +1819,6 @@ class ApiService { return null; } - // Generate title for conversation using dedicated endpoint - Future generateTitle({ - required String conversationId, - required List> messages, - required String model, - }) async { - try { - debugPrint('DEBUG: Generating title for conversation: $conversationId'); - - final response = await _dio.post( - '/api/v1/tasks/title/completions', - data: {'chat_id': conversationId, 'messages': messages, 'model': model}, - ); - - if (response.statusCode == 200 && response.data != null) { - DebugLogger.log('Raw title response received successfully'); - - // Parse the complex response structure - String? extractedTitle; - - try { - final responseData = response.data as Map; - - // Check if there's a direct title field - if (responseData.containsKey('title')) { - extractedTitle = responseData['title']?.toString(); - } - // Check if it's in choices format (OpenAI-style response) - else if (responseData.containsKey('choices') && - responseData['choices'] is List) { - final choices = responseData['choices'] as List; - if (choices.isNotEmpty) { - final firstChoice = choices[0] as Map; - if (firstChoice.containsKey('message')) { - final message = firstChoice['message'] as Map; - final content = message['content']?.toString() ?? ''; - - // Extract title from JSON-formatted content - if (content.contains('```json') && content.contains('```')) { - // Extract JSON from markdown code block - final jsonStart = content.indexOf('```json') + 7; - final jsonEnd = content.lastIndexOf('```'); - if (jsonEnd > jsonStart) { - final jsonString = content - .substring(jsonStart, jsonEnd) - .trim(); - try { - final jsonData = - jsonDecode(jsonString) as Map; - extractedTitle = jsonData['title']?.toString(); - } catch (e) { - debugPrint( - 'DEBUG: Failed to parse JSON from title response: $e', - ); - } - } - } else { - // Try to parse the content directly as JSON - try { - final jsonData = - jsonDecode(content) as Map; - extractedTitle = jsonData['title']?.toString(); - } catch (e) { - // If not JSON, use content as-is - extractedTitle = content; - } - } - } - } - } - - // Clean up the extracted title - if (extractedTitle != null && extractedTitle.isNotEmpty) { - // Remove any remaining markdown formatting - extractedTitle = extractedTitle - .replaceAll(RegExp(r'```.*?```', dotAll: true), '') - .trim(); - extractedTitle = extractedTitle - .replaceAll(RegExp(r'^[{"]|["}]$'), '') - .trim(); - - // Ensure it's not just "New Chat" or empty - if (extractedTitle.isNotEmpty && extractedTitle != 'New Chat') { - debugPrint( - 'DEBUG: Successfully extracted title: $extractedTitle', - ); - return extractedTitle; - } - } - } catch (e) { - debugPrint('DEBUG: Error parsing title response: $e'); - } - - debugPrint('DEBUG: Could not extract valid title from response'); - } - } catch (e) { - debugPrint('DEBUG: Failed to generate title: $e'); - } - return null; - } - // Send chat completed notification Future sendChatCompleted({ required String chatId, diff --git a/lib/core/services/streaming_helper.dart b/lib/core/services/streaming_helper.dart index 0bee080..2e2db98 100644 --- a/lib/core/services/streaming_helper.dart +++ b/lib/core/services/streaming_helper.dart @@ -219,6 +219,57 @@ StreamSubscription attachUnifiedChunkedStreaming({ } catch (_) {} } + bool refreshingSnapshot = false; + Future refreshConversationSnapshot() async { + if (refreshingSnapshot) return; + final chatId = activeConversationId; + if (chatId == null || chatId.isEmpty) { + return; + } + if (api == null) return; + + refreshingSnapshot = true; + try { + final conversation = await api.getConversation(chatId); + + if (conversation.title.isNotEmpty && conversation.title != 'New Chat') { + onChatTitleUpdated?.call(conversation.title); + } + + if (conversation.messages.isEmpty) { + return; + } + + ChatMessage? foundAssistant; + for (final message in conversation.messages.reversed) { + if (message.role == 'assistant') { + foundAssistant = message; + break; + } + } + + final assistant = foundAssistant; + if (assistant == null) { + return; + } + + setFollowUps(assistant.id, assistant.followUps); + updateMessageById(assistant.id, (current) { + return current.copyWith( + followUps: List.from(assistant.followUps), + statusHistory: assistant.statusHistory, + sources: assistant.sources, + metadata: {...?current.metadata, ...?assistant.metadata}, + usage: assistant.usage, + ); + }); + } catch (_) { + // Best-effort refresh; ignore failures. + } finally { + refreshingSnapshot = false; + } + } + void channelLineHandlerFactory(String channel) { void handler(dynamic line) { try { @@ -446,6 +497,8 @@ StreamSubscription attachUnifiedChunkedStreaming({ ); } catch (_) {} + Future.microtask(refreshConversationSnapshot); + final msgs = getMessages(); if (msgs.isNotEmpty && msgs.last.role == 'assistant') { final lastContent = msgs.last.content.trim(); @@ -897,6 +950,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ // If SSE-driven (no dynamic channel/background flow), finish now if (!usingDynamicChannel && !isBackgroundFlow) { finishStreaming(); + Future.microtask(refreshConversationSnapshot); } socketWatchdog?.stop(); }, @@ -906,6 +960,7 @@ StreamSubscription attachUnifiedChunkedStreaming({ } catch (_) {} suppressSocketContent = false; finishStreaming(); + Future.microtask(refreshConversationSnapshot); socketWatchdog?.stop(); }, ); diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index 5618255..b1ea78b 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -1463,34 +1463,6 @@ Future _sendMessageInternal( // We'll add the assistant message placeholder after we get the message ID from the API (or immediately in reviewer mode) - // Immediately trigger title generation after user message is sent (first turn only) - try { - final currentConversation = ref.read(activeConversationProvider); - if (currentConversation != null && - currentConversation.title == 'New Chat') { - final currentMessages = ref.read(chatMessagesProvider); - if (currentMessages.length == 1 && currentMessages.first.role == 'user') { - final List> formatted = [ - { - 'id': currentMessages.first.id, - 'role': currentMessages.first.role, - 'content': currentMessages.first.content, - 'timestamp': - currentMessages.first.timestamp.millisecondsSinceEpoch ~/ 1000, - }, - ]; - _triggerTitleGeneration( - ref, - currentConversation.id, - formatted, - selectedModel.id, - ); - } - } - } catch (e) { - // Silent fail for early title generation - } - // Reviewer mode: simulate a response locally and return if (reviewerMode) { // Add assistant message placeholder @@ -1936,71 +1908,6 @@ Please try sending the message again, or try without attachments.''', } } -// Trigger title generation using the dedicated endpoint -Future _triggerTitleGeneration( - dynamic ref, - String conversationId, - List> messages, - String model, -) async { - // Enqueue background title generation task - try { - await ref - .read(taskQueueProvider.notifier) - .enqueueGenerateTitle(conversationId: conversationId); - } catch (_) { - // Best effort background check remains - _checkForTitleInBackground(ref, conversationId); - } -} - -// Background function to check for title updates without blocking UI -Future _checkForTitleInBackground( - dynamic ref, - String conversationId, -) async { - try { - final api = ref.read(apiServiceProvider); - if (api == null) return; - - // Wait a bit before first check to give server time to generate - await Future.delayed(const Duration(seconds: 3)); - - // Try a few times with increasing delays - for (int i = 0; i < 3; i++) { - try { - final updatedConv = await api.getConversation(conversationId); - - if (updatedConv.title != 'New Chat' && updatedConv.title.isNotEmpty) { - // Update the active conversation with the new title - final activeConversation = ref.read(activeConversationProvider); - if (activeConversation?.id == conversationId) { - final updated = activeConversation!.copyWith( - title: updatedConv.title, - updatedAt: DateTime.now(), - ); - ref.read(activeConversationProvider.notifier).set(updated); - - // Refresh the conversations list - ref.invalidate(conversationsProvider); - } - - return; // Title found, stop checking - } - - // Wait before next check (3s, 5s, 7s) - if (i < 2) { - await Future.delayed(Duration(seconds: 2 + (i * 2))); - } - } catch (e) { - break; // Stop on error - } - } - } catch (e) { - // Handle background title check errors silently - } -} - // Save current conversation to OpenWebUI server // Removed server persistence; only local caching is used in mobile app. diff --git a/lib/shared/services/tasks/outbound_task.dart b/lib/shared/services/tasks/outbound_task.dart index fa2dc0a..1a26b54 100644 --- a/lib/shared/services/tasks/outbound_task.dart +++ b/lib/shared/services/tasks/outbound_task.dart @@ -68,18 +68,6 @@ abstract class OutboundTask with _$OutboundTask { String? error, }) = GenerateImageTask; - const factory OutboundTask.generateTitle({ - required String id, - required String conversationId, - @Default(TaskStatus.queued) TaskStatus status, - @Default(0) int attempt, - String? idempotencyKey, - DateTime? enqueuedAt, - DateTime? startedAt, - DateTime? completedAt, - String? error, - }) = GenerateTitleTask; - const factory OutboundTask.imageToDataUrl({ required String id, String? conversationId, @@ -103,7 +91,6 @@ abstract class OutboundTask with _$OutboundTask { uploadMedia: (t) => t.conversationId, executeToolCall: (t) => t.conversationId, generateImage: (t) => t.conversationId, - generateTitle: (t) => t.conversationId, imageToDataUrl: (t) => t.conversationId, ); diff --git a/lib/shared/services/tasks/task_queue.dart b/lib/shared/services/tasks/task_queue.dart index 4706539..6661bb9 100644 --- a/lib/shared/services/tasks/task_queue.dart +++ b/lib/shared/services/tasks/task_queue.dart @@ -301,23 +301,6 @@ class TaskQueueNotifier extends Notifier> { // Removed: enqueueSaveConversation — mobile app no longer persists chats to server. - Future enqueueGenerateTitle({ - required String conversationId, - String? idempotencyKey, - }) async { - final id = _uuid.v4(); - final task = OutboundTask.generateTitle( - id: id, - conversationId: conversationId, - idempotencyKey: idempotencyKey, - enqueuedAt: DateTime.now(), - ); - state = [...state, task]; - await _save(); - _process(); - return id; - } - Future enqueueImageToDataUrl({ required String? conversationId, required String filePath, diff --git a/lib/shared/services/tasks/task_worker.dart b/lib/shared/services/tasks/task_worker.dart index b27d4c2..cf9e7fe 100644 --- a/lib/shared/services/tasks/task_worker.dart +++ b/lib/shared/services/tasks/task_worker.dart @@ -21,8 +21,6 @@ class TaskWorker { executeToolCall: _performExecuteToolCall, generateImage: _performGenerateImage, imageToDataUrl: _performImageToDataUrl, - // saveConversation removed — we no longer push chat state to server - generateTitle: _performGenerateTitle, ); } @@ -338,42 +336,4 @@ class TaskWorker { }, ); } - - // _performSaveConversation removed - - Future _performGenerateTitle(GenerateTitleTask task) async { - final api = _ref.read(apiServiceProvider); - final activeConv = _ref.read(activeConversationProvider); - final selectedModel = _ref.read(selectedModelProvider); - if (api == null || selectedModel == null) return; - try { - final messages = _ref.read(chat.chatMessagesProvider); - final formatted = >[]; - for (final msg in messages) { - formatted.add({ - 'id': msg.id, - 'role': msg.role, - 'content': msg.content, - 'timestamp': msg.timestamp.millisecondsSinceEpoch ~/ 1000, - }); - } - final title = await api.generateTitle( - conversationId: task.conversationId, - messages: formatted, - model: selectedModel.id, - ); - if (title != null && title.isNotEmpty && title != 'New Chat') { - if (activeConv != null && activeConv.id == task.conversationId) { - final updated = activeConv.copyWith( - title: title.length > 100 ? '${title.substring(0, 100)}...' : title, - updatedAt: DateTime.now(), - ); - _ref.read(activeConversationProvider.notifier).set(updated); - // Do not push full messages to server; skip remote update. - // Optionally refresh list to reflect server-side title when it’s generated there. - _ref.invalidate(conversationsProvider); - } - } - } catch (_) {} - } }