* feat(#195): add chat attachment support for files and images - Add Attachment data model with computed properties (isImage, formattedSize, fileExtension) - Extend ChatMessage with optional attachments list - Add Coil 2.7.0 for async image loading in Compose - Extend ChatViewModel with pending attachment state, addAttachment/removeAttachment/clearAttachments - Build attachment-prefixed message text (data URIs for images, file refs for others) - Add paperclip button and file picker launcher (ActivityResultContracts.GetContent) to ChatScreen - Add animated attachment preview chips with thumbnails before sending - Render attachments inline in UserBubble (AsyncImage for images, file cards for others) - Add string resources for attachment UI features * fix(#195): rewrite attachment upload protocol to match Hermes backend RPC methods BREAKS: removes old buildAttachmentText() that embedded base64 data URIs in prompt text. Changes: - Replace inline data-URI encoding with proper backend RPC flow: - image.attach_bytes for images (fire-and-forget, auto-queues to session) - file.attach for non-image files (awaited, returns @file: ref text) - Add CompletableDeferred to PendingRequest so sendMessage can await RPC responses - Add readContentUriBytes() to read file content from content:// URIs - Add sendRpcAndAwait() suspend function for sequential RPC calls - Add RpcCallException for awaited RPC error handling - Suppress error UI for awaited RPCs (caller handles them directly) - Keep ChatScreen/ChatBubble UI layer unchanged (file picker, previews, inline images) * feat(#195): add camera capture support via FileProvider + TakePicture contract - Creates temporary photo files in cacheDir via FileProvider - Launches system camera with ActivityResultContracts.TakePicture - Adds camera icon button (📷) next to the paperclip in ChatInputBar - Captured photos added as image/jpeg attachments via same addAttachment flow - FileProvider configured with cache-root path in res/xml/file_paths.xml - Error handling for launch failures and capture failures with snackbar * refactor(#195): group camera/image/file into single attach dropdown - Replaces separate 📷 and 📎 buttons with a single 📎 button - DropdownMenu opens with three options: - 📷 Camera → TakePicture via FileProvider temp URI - 🖼️ Image → GetContent("image/*") - 📄 File → GetContent("*/*") - Removes onAttachClick, adds onImageTap/onFileTap callbacks - Input bar stays clean with one attachment affordance * fix(#195): add missing imports for CI compilation ChatScreen.kt: - android.util.Log - androidx.compose.animation.core.* (infiniteRepeatable, tween, LinearEasing, RepeatMode, rememberInfiniteTransition) - androidx.compose.foundation.layout.widthIn - androidx.compose.material.icons.filled.AttachFile - androidx.compose.material.icons.filled.InsertDriveFile - androidx.compose.ui.layout.ContentScale - coil.compose.AsyncImage ChatBubble.kt: - androidx.compose.material.icons.filled.InsertDriveFile * fix(#195): resolve android-lint LocalContextGetResourceValueCall error Replace context.getString(R.string.chat_camera_error) with a composable-level cameraErrorMsg captured via stringResource() to avoid querying resources via LocalContext.current in a callback. * fix(#195): migrate InsertDriveFile to AutoMirrored, fix chip double-remove - Replace deprecated Icons.Filled.InsertDriveFile with Icons.AutoMirrored.Filled.InsertDriveFile in ChatBubble.kt and ChatScreen.kt - Remove onClick from AttachmentChip Surface so only the dedicated close button triggers removal, not accidental taps on the chip body --------- Co-authored-by: Hermes Agent <hermes@agent.local>
This commit is contained in:
1 parent
c64b025a54
commit
4849f4c1c6
12 files changed
+971
-30
No files matched your search
@@ -0,0 +1,31 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
/**
|
||||
* Represents a file attached to a chat message.
|
||||
*
|
||||
* @param uri Content URI or file path to the attachment
|
||||
* @param name Display name of the file
|
||||
* @param mimeType MIME type (e.g., "image/jpeg", "application/pdf")
|
||||
* @param size File size in bytes
|
||||
* @param localPath Optional local file system path for direct access
|
||||
*/
|
||||
data class Attachment(
|
||||
val uri: String,
|
||||
val name: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
) {
|
||||
val isImage: Boolean
|
||||
get() = mimeType.startsWith("image/")
|
||||
|
||||
val formattedSize: String
|
||||
get() =
|
||||
when {
|
||||
size < 1024 -> "$size B"
|
||||
size < 1024 * 1024 -> "${size / 1024} KB"
|
||||
else -> "%.1f MB".format(size.toDouble() / (1024 * 1024))
|
||||
}
|
||||
|
||||
val fileExtension: String
|
||||
get() = name.substringAfterLast('.', "").lowercase()
|
||||
}
|
||||
@@ -22,4 +22,12 @@ object WsMethods {
|
||||
const val COMMANDS_CATALOG = "commands.catalog"
|
||||
const val COMMAND_DISPATCH = "command.dispatch"
|
||||
const val COMMAND_RESOLVE = "command.resolve"
|
||||
|
||||
// ── Attachments ───────────────────────────────────────────────────────
|
||||
|
||||
/** Upload image bytes (base64) from a remote client. */
|
||||
const val IMAGE_ATTACH_BYTES = "image.attach_bytes"
|
||||
|
||||
/** Stage a non-image file (data URL) for agent access. */
|
||||
const val FILE_ATTACH = "file.attach"
|
||||
}
|
||||
Reference in new issue
Block a user