refactor: more flows

This commit is contained in:
cogwheel0
2025-09-02 13:20:02 +05:30
parent dc231cfa52
commit 77e6a15215
5 changed files with 182 additions and 101 deletions

View File

@@ -9,6 +9,8 @@ import '../../features/auth/providers/unified_auth_providers.dart';
import '../../features/chat/providers/chat_providers.dart';
import '../../features/chat/services/file_attachment_service.dart';
import '../../core/providers/app_providers.dart';
import '../../shared/services/tasks/task_queue.dart';
import 'package:path/path.dart' as path;
import 'navigation_service.dart';
// Server chat creation/title generation occur on first send via chat providers
@@ -152,13 +154,17 @@ Future<void> _processPayload(Ref ref, SharedPayload payload) async {
if (files.isNotEmpty) {
ref.read(attachedFilesProvider.notifier).addFiles(files);
// Enqueue uploads via task queue to unify progress + retry
final activeConv = ref.read(activeConversationProvider);
for (final file in files) {
final uploadStream = svc.uploadFile(file);
uploadStream.listen((state) {
ref
.read(attachedFilesProvider.notifier)
.updateFileState(file.path, state);
}, onError: (_) {});
try {
await ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
fileSize: await file.length(),
);
} catch (_) {}
}
}
}

View File

@@ -2026,51 +2026,13 @@ Future<void> _triggerTitleGeneration(
List<Map<String, dynamic>> messages,
String model,
) async {
// Enqueue background title generation task
try {
final api = ref.read(apiServiceProvider);
if (api == null) return;
// Call the title generation endpoint
final generatedTitle = await api.generateTitle(
conversationId: conversationId,
messages: messages,
model: model,
);
if (generatedTitle != null &&
generatedTitle.isNotEmpty &&
generatedTitle != 'New Chat') {
// Update the active conversation with the new title
final activeConversation = ref.read(activeConversationProvider);
if (activeConversation?.id == conversationId) {
final updated = activeConversation!.copyWith(
title: generatedTitle,
updatedAt: DateTime.now(),
);
ref.read(activeConversationProvider.notifier).state = updated;
// Save the updated title to the server
try {
final currentMessages = ref.read(chatMessagesProvider);
await api.updateConversationWithMessages(
conversationId,
currentMessages,
title: generatedTitle,
model: model,
);
} catch (e) {
// Handle title save errors silently
}
// Refresh the conversations list
ref.invalidate(conversationsProvider);
}
} else {
// Fall back to background checking
_checkForTitleInBackground(ref, conversationId);
}
} catch (e) {
// Fall back to background checking
await ref
.read(taskQueueProvider.notifier)
.enqueueGenerateTitle(conversationId: conversationId);
} catch (_) {
// Best effort background check remains
_checkForTitleInBackground(ref, conversationId);
}
}
@@ -2124,59 +2086,13 @@ Future<void> _checkForTitleInBackground(
// Save current conversation to OpenWebUI server
Future<void> _saveConversationToServer(dynamic ref) async {
// Enqueue save task; local fallback remains if queue fails
try {
final api = ref.read(apiServiceProvider);
final messages = ref.read(chatMessagesProvider);
final activeConversation = ref.read(activeConversationProvider);
final selectedModel = ref.read(selectedModelProvider);
if (api == null || messages.isEmpty || activeConversation == null) {
return;
}
// Check if the last assistant message is truly empty (no text and no files)
final lastMessage = messages.last;
if (lastMessage.role == 'assistant' &&
lastMessage.content.trim().isEmpty &&
(lastMessage.files == null || lastMessage.files!.isEmpty) &&
(lastMessage.attachmentIds == null ||
lastMessage.attachmentIds!.isEmpty)) {
return;
}
// Update the existing conversation with all messages (including assistant response)
try {
await api.updateConversationWithMessages(
activeConversation.id,
messages,
model: selectedModel?.id,
);
// Update local state
final updatedConversation = activeConversation.copyWith(
messages: messages,
updatedAt: DateTime.now(),
);
ref.read(activeConversationProvider.notifier).state = updatedConversation;
} catch (e) {
// Fallback to local storage if server update fails
await _saveConversationLocally(ref);
return;
}
// Refresh conversations list to show the updated conversation
// Adding a small delay to prevent rapid invalidations that could cause duplicates
Future.delayed(const Duration(milliseconds: 100), () {
try {
if (ref.mounted == true) {
ref.invalidate(conversationsProvider);
}
} catch (_) {}
});
} catch (e) {
// Fallback to local storage
await ref
.read(taskQueueProvider.notifier)
.enqueueSaveConversation(conversationId: activeConversation?.id);
} catch (_) {
await _saveConversationLocally(ref);
}
}

View File

@@ -74,6 +74,30 @@ abstract class OutboundTask with _$OutboundTask {
String? error,
}) = GenerateImageTask;
const factory OutboundTask.saveConversation({
required String id,
String? conversationId,
@Default(TaskStatus.queued) TaskStatus status,
@Default(0) int attempt,
String? idempotencyKey,
DateTime? enqueuedAt,
DateTime? startedAt,
DateTime? completedAt,
String? error,
}) = SaveConversationTask;
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;
factory OutboundTask.fromJson(Map<String, dynamic> json) =>
_$OutboundTaskFromJson(json);

View File

@@ -226,4 +226,63 @@ class TaskQueueNotifier extends StateNotifier<List<OutboundTask>> {
await _save();
}
}
Future<String> enqueueUploadMedia({
required String? conversationId,
required String filePath,
required String fileName,
int? fileSize,
String? mimeType,
String? checksum,
}) async {
final id = _uuid.v4();
final task = OutboundTask.uploadMedia(
id: id,
conversationId: conversationId,
filePath: filePath,
fileName: fileName,
fileSize: fileSize,
mimeType: mimeType,
checksum: checksum,
enqueuedAt: DateTime.now(),
);
state = [...state, task];
await _save();
_process();
return id;
}
Future<String> enqueueSaveConversation({
required String? conversationId,
String? idempotencyKey,
}) async {
final id = _uuid.v4();
final task = OutboundTask.saveConversation(
id: id,
conversationId: conversationId,
idempotencyKey: idempotencyKey,
enqueuedAt: DateTime.now(),
);
state = [...state, task];
await _save();
_process();
return id;
}
Future<String> 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;
}
}

View File

@@ -20,6 +20,8 @@ class TaskWorker {
uploadMedia: _performUploadMedia,
executeToolCall: _performExecuteToolCall,
generateImage: _performGenerateImage,
saveConversation: _performSaveConversation,
generateTitle: _performGenerateTitle,
);
}
@@ -278,4 +280,78 @@ class TaskWorker {
_ref.read(chat.chatMessagesProvider.notifier).finishStreaming();
}
}
Future<void> _performSaveConversation(SaveConversationTask task) async {
final api = _ref.read(apiServiceProvider);
final messages = _ref.read(chat.chatMessagesProvider);
final activeConv = _ref.read(activeConversationProvider);
final selectedModel = _ref.read(selectedModelProvider);
if (api == null || messages.isEmpty || activeConv == null) return;
// Skip if last assistant is empty placeholder
final last = messages.last;
if (last.role == 'assistant' &&
last.content.trim().isEmpty &&
(last.files?.isEmpty ?? true) &&
(last.attachmentIds?.isEmpty ?? true)) {
return;
}
try {
await api.updateConversationWithMessages(
activeConv.id,
messages,
model: selectedModel?.id,
);
final updated = activeConv.copyWith(
messages: messages,
updatedAt: DateTime.now(),
);
_ref.read(activeConversationProvider.notifier).state = updated;
_ref.invalidate(conversationsProvider);
} catch (_) {}
}
Future<void> _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 = <Map<String, dynamic>>[];
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).state = updated;
try {
final cur = _ref.read(chat.chatMessagesProvider);
await api.updateConversationWithMessages(
updated.id,
cur,
title: updated.title,
model: selectedModel.id,
);
} catch (_) {}
_ref.invalidate(conversationsProvider);
}
}
} catch (_) {}
}
}