From 78f101ac853aeea5d17067bec4215e070fc71fed Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Mon, 23 Mar 2026 12:01:07 +0100
Subject: [PATCH] feat: attach button with camera/gallery/files, atomic bundle send
---
lib/screens/chat_screen.dart | 83 +++++++++++++++++++----------------------
1 files changed, 38 insertions(+), 45 deletions(-)
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index cec56f8..90fabf2 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -671,51 +671,45 @@
);
if (result == null || result.files.isEmpty) return;
+ // Build attachments list
+ final attachments = <Map<String, dynamic>>[];
for (final file in result.files) {
if (file.path == null) continue;
final bytes = await File(file.path!).readAsBytes();
final b64 = base64Encode(bytes);
final mimeType = _guessMimeType(file.name);
- final isImage = mimeType.startsWith('image/');
- if (isImage) {
- final message = Message.image(
+ attachments.add({
+ 'data': b64,
+ 'mimeType': mimeType,
+ 'fileName': file.name,
+ });
+
+ // Show in chat
+ if (mimeType.startsWith('image/')) {
+ ref.read(messagesProvider.notifier).addMessage(Message.image(
role: MessageRole.user,
imageBase64: b64,
content: file.name,
status: MessageStatus.sent,
- );
- ref.read(messagesProvider.notifier).addMessage(message);
- _ws?.send({
- 'type': 'image',
- 'imageBase64': b64,
- 'mimeType': mimeType,
- 'caption': file.name,
- 'sessionId': targetSessionId,
- });
+ ));
} else {
- // Non-image file: send as text with file info, save file to temp for session
- final tmpDir = await getTemporaryDirectory();
- final tmpPath = '${tmpDir.path}/${file.name}';
- await File(tmpPath).writeAsBytes(bytes);
-
- final message = Message.text(
+ ref.read(messagesProvider.notifier).addMessage(Message.text(
role: MessageRole.user,
content: '📎 ${file.name} (${_formatSize(bytes.length)})',
status: MessageStatus.sent,
- );
- ref.read(messagesProvider.notifier).addMessage(message);
- // Send file as base64 with metadata
- _ws?.send({
- 'type': 'file',
- 'fileBase64': b64,
- 'fileName': file.name,
- 'mimeType': mimeType,
- 'fileSize': bytes.length,
- 'sessionId': targetSessionId,
- });
+ ));
}
}
+
+ // Send all files as one atomic bundle
+ _ws?.send({
+ 'type': 'bundle',
+ 'caption': '',
+ 'attachments': attachments,
+ 'sessionId': targetSessionId,
+ });
+
_scrollToBottom();
}
@@ -839,8 +833,21 @@
textCaption = '';
}
- // Send voice FIRST so Whisper transcribes it and the [PAILot:voice] prefix
- // sets the reply channel. Images follow — Claude sees transcript + images together.
+ // Send everything as a single atomic bundle — the server will compose
+ // all attachments + caption into one terminal input (like dropping files + text)
+ final attachments = encodedImages.map((b64) =>
+ <String, dynamic>{'data': b64, 'mimeType': 'image/jpeg'}
+ ).toList();
+
+ _ws?.send({
+ 'type': 'bundle',
+ 'caption': textCaption,
+ if (voiceB64 != null) 'audioBase64': voiceB64,
+ 'attachments': attachments,
+ 'sessionId': targetSessionId,
+ });
+
+ // If voice caption, also send separately for Whisper transcription
if (voiceB64 != null) {
final voiceMsg = Message.voice(
role: MessageRole.user,
@@ -853,20 +860,6 @@
'audioBase64': voiceB64,
'content': '',
'messageId': voiceMsg.id,
- 'sessionId': targetSessionId,
- });
- }
-
- // Send images — first with text caption (if any), rest without
- for (var i = 0; i < encodedImages.length; i++) {
- final isFirst = i == 0;
- final msgCaption = isFirst ? textCaption : '';
-
- _ws?.send({
- 'type': 'image',
- 'imageBase64': encodedImages[i],
- 'mimeType': 'image/jpeg',
- 'caption': msgCaption,
'sessionId': targetSessionId,
});
}
--
Gitblit v1.3.1