refactor: migrate image gen to task based
This commit is contained in:
@@ -2513,12 +2513,13 @@ final regenerateLastMessageProvider = Provider<Future<void> Function()>((ref) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resend the message via normal flow
|
// Resend the message via task queue (unified flow)
|
||||||
await _sendMessageInternal(
|
final activeConv = ref.read(activeConversationProvider);
|
||||||
ref,
|
await ref.read(taskQueueProvider.notifier).enqueueSendText(
|
||||||
lastUserMessage.content,
|
conversationId: activeConv?.id,
|
||||||
lastUserMessage.attachmentIds,
|
text: lastUserMessage.content,
|
||||||
);
|
attachments: lastUserMessage.attachmentIds,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,19 @@ abstract class OutboundTask with _$OutboundTask {
|
|||||||
String? error,
|
String? error,
|
||||||
}) = ExecuteToolCallTask;
|
}) = ExecuteToolCallTask;
|
||||||
|
|
||||||
|
const factory OutboundTask.generateImage({
|
||||||
|
required String id,
|
||||||
|
String? conversationId,
|
||||||
|
required String prompt,
|
||||||
|
@Default(TaskStatus.queued) TaskStatus status,
|
||||||
|
@Default(0) int attempt,
|
||||||
|
String? idempotencyKey,
|
||||||
|
DateTime? enqueuedAt,
|
||||||
|
DateTime? startedAt,
|
||||||
|
DateTime? completedAt,
|
||||||
|
String? error,
|
||||||
|
}) = GenerateImageTask;
|
||||||
|
|
||||||
factory OutboundTask.fromJson(Map<String, dynamic> json) =>
|
factory OutboundTask.fromJson(Map<String, dynamic> json) =>
|
||||||
_$OutboundTaskFromJson(json);
|
_$OutboundTaskFromJson(json);
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,25 @@ class TaskQueueNotifier extends StateNotifier<List<OutboundTask>> {
|
|||||||
_process();
|
_process();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> enqueueGenerateImage({
|
||||||
|
required String? conversationId,
|
||||||
|
required String prompt,
|
||||||
|
String? idempotencyKey,
|
||||||
|
}) async {
|
||||||
|
final id = _uuid.v4();
|
||||||
|
final task = OutboundTask.generateImage(
|
||||||
|
id: id,
|
||||||
|
conversationId: conversationId,
|
||||||
|
prompt: prompt,
|
||||||
|
idempotencyKey: idempotencyKey,
|
||||||
|
enqueuedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
state = [...state, task];
|
||||||
|
await _save();
|
||||||
|
_process();
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _process() async {
|
Future<void> _process() async {
|
||||||
if (_processing) return;
|
if (_processing) return;
|
||||||
_processing = true;
|
_processing = true;
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import '../../../core/providers/app_providers.dart';
|
import '../../../core/providers/app_providers.dart';
|
||||||
import '../../../core/services/attachment_upload_queue.dart';
|
import '../../../core/services/attachment_upload_queue.dart';
|
||||||
import '../../../core/utils/debug_logger.dart';
|
import '../../../core/utils/debug_logger.dart';
|
||||||
|
import '../../../core/models/chat_message.dart';
|
||||||
import '../../../features/chat/providers/chat_providers.dart' as chat;
|
import '../../../features/chat/providers/chat_providers.dart' as chat;
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
import 'outbound_task.dart';
|
import 'outbound_task.dart';
|
||||||
|
|
||||||
class TaskWorker {
|
class TaskWorker {
|
||||||
@@ -17,6 +19,7 @@ class TaskWorker {
|
|||||||
sendTextMessage: _performSendText,
|
sendTextMessage: _performSendText,
|
||||||
uploadMedia: _performUploadMedia,
|
uploadMedia: _performUploadMedia,
|
||||||
executeToolCall: _performExecuteToolCall,
|
executeToolCall: _performExecuteToolCall,
|
||||||
|
generateImage: _performGenerateImage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,4 +120,162 @@ class TaskWorker {
|
|||||||
// We keep this task type for future local tools or MCP bridges.
|
// We keep this task type for future local tools or MCP bridges.
|
||||||
debugPrint('ExecuteToolCallTask stub: ${task.toolName}');
|
debugPrint('ExecuteToolCallTask stub: ${task.toolName}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _performGenerateImage(GenerateImageTask task) async {
|
||||||
|
final api = _ref.read(apiServiceProvider);
|
||||||
|
final selectedModel = _ref.read(selectedModelProvider);
|
||||||
|
if (api == null) {
|
||||||
|
throw Exception('API not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assistant placeholder to show progress
|
||||||
|
try {
|
||||||
|
final placeholder = ChatMessage(
|
||||||
|
id: const Uuid().v4(),
|
||||||
|
role: 'assistant',
|
||||||
|
content: '',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
model: selectedModel?.id,
|
||||||
|
isStreaming: true,
|
||||||
|
);
|
||||||
|
_ref.read(chat.chatMessagesProvider.notifier).addMessage(placeholder);
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Generate images
|
||||||
|
List<Map<String, dynamic>> _extractGeneratedFiles(dynamic resp) {
|
||||||
|
final results = <Map<String, dynamic>>[];
|
||||||
|
if (resp is List) {
|
||||||
|
for (final item in resp) {
|
||||||
|
if (item is String && item.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': item});
|
||||||
|
} else if (item is Map) {
|
||||||
|
final url = item['url'];
|
||||||
|
final b64 = item['b64_json'] ?? item['b64'];
|
||||||
|
if (url is String && url.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': url});
|
||||||
|
} else if (b64 is String && b64.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
if (resp is! Map) return results;
|
||||||
|
final data = resp['data'];
|
||||||
|
if (data is List) {
|
||||||
|
for (final item in data) {
|
||||||
|
if (item is Map) {
|
||||||
|
final url = item['url'];
|
||||||
|
final b64 = item['b64_json'] ?? item['b64'];
|
||||||
|
if (url is String && url.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': url});
|
||||||
|
} else if (b64 is String && b64.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
|
||||||
|
}
|
||||||
|
} else if (item is String && item.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': item});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final images = resp['images'];
|
||||||
|
if (images is List) {
|
||||||
|
for (final item in images) {
|
||||||
|
if (item is String && item.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': item});
|
||||||
|
} else if (item is Map) {
|
||||||
|
final url = item['url'];
|
||||||
|
final b64 = item['b64_json'] ?? item['b64'];
|
||||||
|
if (url is String && url.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': url});
|
||||||
|
} else if (b64 is String && b64.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': 'data:image/png;base64,$b64'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final singleUrl = resp['url'];
|
||||||
|
if (singleUrl is String && singleUrl.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': singleUrl});
|
||||||
|
}
|
||||||
|
final singleB64 = resp['b64_json'] ?? resp['b64'];
|
||||||
|
if (singleB64 is String && singleB64.isNotEmpty) {
|
||||||
|
results.add({'type': 'image', 'url': 'data:image/png;base64,$singleB64'});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final imageResponse = await api.generateImage(prompt: task.prompt);
|
||||||
|
final generatedFiles = _extractGeneratedFiles(imageResponse);
|
||||||
|
if (generatedFiles.isNotEmpty) {
|
||||||
|
_ref.read(chat.chatMessagesProvider.notifier).updateLastMessageWithFunction(
|
||||||
|
(m) => m.copyWith(files: generatedFiles, isStreaming: false),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync conversation to server
|
||||||
|
try {
|
||||||
|
final messages = _ref.read(chat.chatMessagesProvider);
|
||||||
|
final activeConv = _ref.read(activeConversationProvider);
|
||||||
|
if (activeConv != null && messages.isNotEmpty) {
|
||||||
|
await api.updateConversationWithMessages(
|
||||||
|
activeConv.id,
|
||||||
|
messages,
|
||||||
|
model: selectedModel?.id,
|
||||||
|
);
|
||||||
|
// Update local active conversation messages
|
||||||
|
final updated = activeConv.copyWith(
|
||||||
|
messages: messages,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
_ref.read(activeConversationProvider.notifier).state = updated;
|
||||||
|
_ref.invalidate(conversationsProvider);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// Trigger title generation (best-effort)
|
||||||
|
try {
|
||||||
|
final activeConv = _ref.read(activeConversationProvider);
|
||||||
|
final messages = _ref.read(chat.chatMessagesProvider);
|
||||||
|
final modelId = selectedModel?.id;
|
||||||
|
if (activeConv != null && modelId != null) {
|
||||||
|
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: activeConv.id,
|
||||||
|
messages: formatted,
|
||||||
|
model: modelId,
|
||||||
|
);
|
||||||
|
if (title != null && title.isNotEmpty && title != 'New Chat') {
|
||||||
|
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: modelId,
|
||||||
|
);
|
||||||
|
} catch (_) {}
|
||||||
|
_ref.invalidate(conversationsProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
} else {
|
||||||
|
_ref.read(chat.chatMessagesProvider.notifier).finishStreaming();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_ref.read(chat.chatMessagesProvider.notifier).finishStreaming();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user