refactor: image uploads

This commit is contained in:
cogwheel0
2025-09-02 19:08:23 +05:30
parent 77e6a15215
commit 4491fa5861
5 changed files with 199 additions and 37 deletions

View File

@@ -158,12 +158,22 @@ Future<void> _processPayload(Ref ref, SharedPayload payload) async {
final activeConv = ref.read(activeConversationProvider);
for (final file in files) {
try {
await ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
fileSize: await file.length(),
);
final ext = path.extension(file.path).toLowerCase();
final isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext);
if (isImage) {
await ref.read(taskQueueProvider.notifier).enqueueImageToDataUrl(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
);
} else {
await ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
fileSize: await file.length(),
);
}
} catch (_) {}
}
}

View File

@@ -20,6 +20,8 @@ import '../widgets/file_attachment_widget.dart';
// import '../widgets/voice_input_sheet.dart'; // deprecated: replaced by inline voice input
import '../services/voice_input_service.dart';
import '../services/file_attachment_service.dart';
import 'package:path/path.dart' as path;
import '../../../shared/services/tasks/task_queue.dart';
import '../../tools/providers/tools_providers.dart';
import '../../navigation/widgets/chats_drawer.dart';
import '../../../shared/widgets/offline_indicator.dart';
@@ -33,7 +35,6 @@ import '../../onboarding/views/onboarding_sheet.dart';
import '../../../shared/widgets/sheet_handle.dart';
import '../../../shared/widgets/conduit_components.dart';
import '../../../core/services/settings_service.dart';
import '../../../shared/services/tasks/task_queue.dart';
// Removed unused PlatformUtils import
import '../../../core/services/platform_service.dart' as ps;
import 'package:flutter/gestures.dart' show DragStartBehavior;
@@ -358,20 +359,30 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Add files to the attachment list
ref.read(attachedFilesProvider.notifier).addFiles(files);
// Start uploading files
// Enqueue uploads via task queue for unified retry/progress
final activeConv = ref.read(activeConversationProvider);
for (final file in files) {
final uploadStream = fileService.uploadFile(file);
uploadStream.listen(
(state) {
ref
.read(attachedFilesProvider.notifier)
.updateFileState(file.path, state);
},
onError: (error) {
if (!mounted) return;
debugPrint('Upload failed: $error');
},
);
try {
final ext = path.extension(file.path).toLowerCase();
final isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].contains(ext);
if (isImage) {
await ref.read(taskQueueProvider.notifier).enqueueImageToDataUrl(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
);
} else {
await ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: file.path,
fileName: path.basename(file.path),
fileSize: await file.length(),
);
}
} catch (e) {
if (!mounted) return;
debugPrint('Enqueue upload failed: $e');
}
}
} catch (e) {
if (!mounted) return;
@@ -428,23 +439,18 @@ class _ChatPageState extends ConsumerState<ChatPage> {
ref.read(attachedFilesProvider.notifier).addFiles([image]);
debugPrint('DEBUG: Image added to attachment list');
// Start uploading image
debugPrint('DEBUG: Starting image upload...');
final uploadStream = fileService.uploadFile(image);
uploadStream.listen(
(state) {
debugPrint(
'DEBUG: Upload state update - Status: ${state.status}, Progress: ${state.progress}, FileId: ${state.fileId}',
);
ref
.read(attachedFilesProvider.notifier)
.updateFileState(image.path, state);
},
onError: (error) {
debugPrint('DEBUG: Image upload error: $error');
if (!mounted) return;
},
);
// Enqueue upload via task queue for unified retry/progress
debugPrint('DEBUG: Enqueueing image upload...');
final activeConv = ref.read(activeConversationProvider);
try {
await ref.read(taskQueueProvider.notifier).enqueueImageToDataUrl(
conversationId: activeConv?.id,
filePath: image.path,
fileName: path.basename(image.path),
);
} catch (e) {
debugPrint('DEBUG: Enqueue image upload failed: $e');
}
} catch (e) {
debugPrint('DEBUG: Image attachment error: $e');
if (!mounted) return;

View File

@@ -98,6 +98,20 @@ abstract class OutboundTask with _$OutboundTask {
String? error,
}) = GenerateTitleTask;
const factory OutboundTask.imageToDataUrl({
required String id,
String? conversationId,
required String filePath,
required String fileName,
@Default(TaskStatus.queued) TaskStatus status,
@Default(0) int attempt,
String? idempotencyKey,
DateTime? enqueuedAt,
DateTime? startedAt,
DateTime? completedAt,
String? error,
}) = ImageToDataUrlTask;
factory OutboundTask.fromJson(Map<String, dynamic> json) =>
_$OutboundTaskFromJson(json);

View File

@@ -285,4 +285,25 @@ class TaskQueueNotifier extends StateNotifier<List<OutboundTask>> {
_process();
return id;
}
Future<String> enqueueImageToDataUrl({
required String? conversationId,
required String filePath,
required String fileName,
String? idempotencyKey,
}) async {
final id = _uuid.v4();
final task = OutboundTask.imageToDataUrl(
id: id,
conversationId: conversationId,
filePath: filePath,
fileName: fileName,
idempotencyKey: idempotencyKey,
enqueuedAt: DateTime.now(),
);
state = [...state, task];
await _save();
_process();
return id;
}
}

View File

@@ -1,5 +1,7 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/providers/app_providers.dart';
@@ -7,6 +9,8 @@ import '../../../core/services/attachment_upload_queue.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/services/file_attachment_service.dart';
import 'package:path/path.dart' as path;
import 'package:uuid/uuid.dart';
import 'outbound_task.dart';
@@ -20,6 +24,7 @@ class TaskWorker {
uploadMedia: _performUploadMedia,
executeToolCall: _performExecuteToolCall,
generateImage: _performGenerateImage,
imageToDataUrl: _performImageToDataUrl,
saveConversation: _performSaveConversation,
generateTitle: _performGenerateTitle,
);
@@ -96,6 +101,34 @@ class TaskWorker {
entry = null;
}
if (entry == null) return;
// Reflect progress into UI attachment state if that file is present
try {
final current = _ref.read(attachedFilesProvider);
final idx = current.indexWhere((f) => f.file.path == task.filePath);
if (idx != -1) {
final existing = current[idx];
final status = switch (entry.status) {
QueuedAttachmentStatus.pending => FileUploadStatus.uploading,
QueuedAttachmentStatus.uploading => FileUploadStatus.uploading,
QueuedAttachmentStatus.completed => FileUploadStatus.completed,
QueuedAttachmentStatus.failed => FileUploadStatus.failed,
QueuedAttachmentStatus.cancelled => FileUploadStatus.failed,
};
final newState = FileUploadState(
file: File(task.filePath),
fileName: task.fileName,
fileSize: task.fileSize ?? existing.fileSize,
progress: status == FileUploadStatus.completed ? 1.0 : existing.progress,
status: status,
fileId: entry.fileId ?? existing.fileId,
error: entry.lastError,
);
_ref
.read(attachedFilesProvider.notifier)
.updateFileState(task.filePath, newState);
}
} catch (_) {}
switch (entry.status) {
case QueuedAttachmentStatus.completed:
case QueuedAttachmentStatus.failed:
@@ -281,6 +314,84 @@ class TaskWorker {
}
}
Future<void> _performImageToDataUrl(ImageToDataUrlTask task) async {
try {
// Update UI to uploading state first
try {
final current = _ref.read(attachedFilesProvider);
final idx = current.indexWhere((f) => f.file.path == task.filePath);
if (idx != -1) {
final existing = current[idx];
final uploading = FileUploadState(
file: existing.file,
fileName: task.fileName,
fileSize: existing.fileSize,
progress: 0.5,
status: FileUploadStatus.uploading,
fileId: existing.fileId,
);
_ref.read(attachedFilesProvider.notifier).updateFileState(
task.filePath,
uploading,
);
}
} catch (_) {}
// Read file and convert to data URL
final file = File(task.filePath);
final bytes = await file.readAsBytes();
final b64 = base64Encode(bytes);
final ext = path.extension(task.fileName).toLowerCase();
String mime = 'image/png';
if (ext == '.jpg' || ext == '.jpeg') mime = 'image/jpeg';
else if (ext == '.gif') mime = 'image/gif';
else if (ext == '.webp') mime = 'image/webp';
final dataUrl = 'data:$mime;base64,$b64';
// Mark as completed with data URL as fileId
try {
final current = _ref.read(attachedFilesProvider);
final idx = current.indexWhere((f) => f.file.path == task.filePath);
if (idx != -1) {
final existing = current[idx];
final done = FileUploadState(
file: existing.file,
fileName: task.fileName,
fileSize: existing.fileSize,
progress: 1.0,
status: FileUploadStatus.completed,
fileId: dataUrl,
);
_ref.read(attachedFilesProvider.notifier).updateFileState(
task.filePath,
done,
);
}
} catch (_) {}
} catch (e) {
try {
final current = _ref.read(attachedFilesProvider);
final idx = current.indexWhere((f) => f.file.path == task.filePath);
if (idx != -1) {
final existing = current[idx];
final failed = FileUploadState(
file: existing.file,
fileName: task.fileName,
fileSize: existing.fileSize,
progress: 0.0,
status: FileUploadStatus.failed,
fileId: existing.fileId,
error: e.toString(),
);
_ref.read(attachedFilesProvider.notifier).updateFileState(
task.filePath,
failed,
);
}
} catch (_) {}
}
}
Future<void> _performSaveConversation(SaveConversationTask task) async {
final api = _ref.read(apiServiceProvider);
final messages = _ref.read(chat.chatMessagesProvider);