Files
aiturk-mobil/sketches/input-bar-chat-screen/index.html
T
M57andHermes Agent 4849f4c1c6 feat(#195): chat attachment support for files and images (#418)
* 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>
2026-06-29 16:33:12 +03:00

401 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChatInputBar — Layout (v2)</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #111;
color: #eee;
padding: 24px;
display: flex;
flex-direction: column;
gap: 36px;
}
.label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #888;
margin-bottom: 6px;
}
.bar {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
max-width: 500px;
background: rgba(30,30,35,0.92);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 24px;
padding: 4px 8px;
backdrop-filter: blur(8px);
position: relative;
}
.icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
background: transparent;
cursor: pointer;
font-size: 18px;
flex-shrink: 0;
color: #999;
transition: background 0.15s;
}
.icon-btn:hover { background: rgba(255,255,255,0.08); }
.icon-btn:disabled { opacity: 0.25; cursor: default; }
.send-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
cursor: pointer;
transition: all 0.15s;
background: var(--btn-bg, rgba(255,255,255,0.08));
color: var(--btn-color, #999);
}
.send-btn.mic { --btn-bg: rgba(255,255,255,0.08); --btn-color: #ccc; }
.send-btn.send { --btn-bg: #4f8cff; --btn-color: #fff; }
.send-btn.stop { --btn-bg: #e53935; --btn-color: #fff; }
.input-field {
flex: 1;
border: 1px solid rgba(255,255,255,0.12);
border-radius: 20px;
background: transparent;
padding: 8px 14px;
font-size: 14px;
color: #eee;
outline: none;
min-height: 36px;
max-height: 120px;
transition: border-color 0.15s;
font-family: inherit;
line-height: 1.5;
}
.input-field:focus { border-color: #4f8cff; }
.input-field::placeholder { color: #666; }
.input-field:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.chips-row {
display: flex;
gap: 8px;
padding: 4px 12px;
max-width: 500px;
overflow-x: auto;
}
.chip {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.10);
border-radius: 16px;
padding: 4px 6px 4px 10px;
cursor: pointer;
transition: background 0.12s;
}
.chip:hover { background: rgba(255,255,255,0.10); }
.chip .thumb {
width: 20px; height: 20px;
border-radius: 4px;
background: #333;
overflow: hidden;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #888;
}
.chip .name {
font-size: 12px;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #ccc;
}
.chip .close {
width: 18px; height: 18px;
border-radius: 50%;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: #888;
flex-shrink: 0;
}
.chip .close:hover { background: rgba(255,255,255,0.12); }
/* Dropdown menu */
.menu-wrapper {
position: relative;
}
.menu {
position: absolute;
bottom: 36px;
left: 0;
min-width: 180px;
background: rgba(42,42,50,0.98);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 4px 0;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 10;
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
cursor: pointer;
border: none;
background: transparent;
color: #ddd;
font-size: 14px;
width: 100%;
text-align: left;
transition: background 0.1s;
font-family: inherit;
}
.menu-item:hover { background: rgba(255,255,255,0.06); }
.menu-item .icon { font-size: 18px; width: 24px; text-align: center; flex-shrink: 0; }
.variant { margin-bottom: 8px; }
.note {
font-size: 12px;
color: #777;
margin-top: 4px;
line-height: 1.5;
}
.states {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Simple arrow for "tap here" */
.tap-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(79,140,255,0.12);
color: #7aadff;
padding: 2px 10px;
border-radius: 8px;
font-size: 11px;
margin-left: 4px;
}
</style>
</head>
<body>
<h2 style="color:#ddd; font-weight:500; font-size:18px; margin-bottom:4px;">📱 ChatInputBar (v2)</h2>
<p style="color:#777; font-size:13px; margin-bottom:20px;">
Single 📎 attach button → dropdown menu with Camera / Image / File
</p>
<div class="states">
<!-- 1. Empty + menu open -->
<div class="variant">
<div class="label">1. Empty — mic visible, dropdown open</div>
<div class="bar">
<div class="menu-wrapper">
<button class="icon-btn" title="Attach" style="background:rgba(255,255,255,0.08);">📎</button>
<div class="menu">
<button class="menu-item"><span class="icon">📷</span> Camera</button>
<button class="menu-item"><span class="icon">🖼️</span> Image</button>
<button class="menu-item"><span class="icon">📄</span> File</button>
</div>
</div>
<input class="input-field" placeholder="Type a message…" disabled />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn mic" title="Voice input">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
</div>
<div class="note">📎 button opens a Material 3 DropdownMenu with three options. Selecting one fires the launcher and closes the menu.</div>
</div>
<!-- 2. Empty, menu closed -->
<div class="variant">
<div class="label">2. Empty — normal state</div>
<div class="bar">
<button class="icon-btn" title="Attach">📎</button>
<input class="input-field" placeholder="Type a message…" disabled />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn mic" title="Voice input">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
</div>
<div class="note">Default look — single 📎, no clutter. Mic visible when input is blank &amp; no attachments.</div>
</div>
<!-- 3. Typing -->
<div class="variant">
<div class="label">3. Typing — send shown</div>
<div class="bar">
<button class="icon-btn" title="Attach">📎</button>
<input class="input-field" placeholder="Type a message…" value="Deploy the new config, but keep the old one as backup" style="color:#eee;" />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn send" title="Send">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
<div class="note">Mic → Send (blue) when text or attachments present.</div>
</div>
<!-- 4. Attachments -->
<div class="variant">
<div class="label">4. With attachments — preview chips</div>
<div class="chips-row">
<div class="chip">
<span class="thumb">📷</span>
<span class="name">photo_20260629.jpg</span>
<button class="close">✕</button>
</div>
<div class="chip">
<span class="thumb">📄</span>
<span class="name">config.yaml</span>
<button class="close">✕</button>
</div>
<div class="chip">
<span class="thumb">📷</span>
<span class="name">screenshot.png</span>
<button class="close">✕</button>
</div>
</div>
<div class="bar" style="margin-top:0;">
<button class="icon-btn" title="Attach">📎</button>
<input class="input-field" placeholder="Type a message…" value="Here's what I'm looking at" style="color:#eee;" />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn send" title="Send">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
<div class="note">Image chips → AsyncImage thumbnail. File chips → file icon. Close ✕ removes.</div>
</div>
<!-- 5. Listening -->
<div class="variant">
<div class="label">5. Listening — stop shown</div>
<div class="bar">
<button class="icon-btn" title="Attach" disabled>📎</button>
<input class="input-field" placeholder="Type a message…" disabled />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn stop" title="Stop listening">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
</div>
<div class="note">While listening, input + attach disabled. Red Stop replaces Mic.</div>
</div>
<!-- 6. Disconnected -->
<div class="variant">
<div class="label">6. Disconnected — everything disabled</div>
<div class="bar" style="opacity:0.5;">
<button class="icon-btn" disabled title="Attach">📎</button>
<input class="input-field" placeholder="Not connected" disabled />
<div style="width:4px;flex-shrink:0;"></div>
<button class="send-btn mic" disabled title="Voice input">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>
</button>
</div>
<div class="note">Everything dimmed, placeholder "Not connected".</div>
</div>
</div>
<!-- Full structural diagram -->
<hr style="border-color:rgba(255,255,255,0.06); margin: 28px 0 16px;" />
<h3 style="color:#ddd; font-weight:500; font-size:14px; margin-bottom:12px;">📐 Structural hierarchy</h3>
<pre style="font-size:12px; color:#aaa; line-height:1.6; background:rgba(255,255,255,0.03); padding:16px; border-radius:12px; max-width:600px;">
Card (roundedCornerShape(24dp), surface alpha 0.85)
└ Column
├ [conditional] Slash command suggestions
│ └ LazyColumn (maxHeight 200dp)
├ [conditional] Attachment preview chips
│ └ Row
│ └ AttachmentChip × N
│ └ Surface (roundedCorner 16dp)
│ ├ [if image] AsyncImage(20dp)
│ ├ [else] Icon(InsertDriveFile, 16dp)
│ └ name + Close IconButton(18dp)
└ Row (main input area)
├ Box (positioned for dropdown)
│ ├ IconButton 📎 AttachFile (36dp)
│ └ DropdownMenu (expanded = showAttachmentMenu)
│ ├ DropdownMenuItem: 📷 Camera → onCameraTap()
│ ├ DropdownMenuItem: 🖼️ Image → onImageTap() → launch("image/*")
│ └ DropdownMenuItem: 📄 File → onFileTap() → launch("*/*")
├ OutlinedTextField (weight=1f, maxLines=4, rounded 20dp)
├ Spacer(4dp)
└ AnimatedContent (mic ↔ send ↔ listening)
├ mic: FilledTonalButton(Mic icon, 40dp, circle)
├ send: FilledTonalButton(Send icon, 40dp, circle, blue)
└ listening: FilledTonalButton(Stop icon, 40dp, circle, red)
</pre>
<hr style="border-color:rgba(255,255,255,0.06); margin: 28px 0 16px;" />
<h3 style="color:#ddd; font-weight:500; font-size:14px; margin-bottom:12px;">🎯 What each dropdown option does</h3>
<table style="font-size:13px; color:#ccc; border-collapse: collapse; max-width:600px;">
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);">
<th style="padding:8px 12px; text-align:left; color:#888;">Option</th>
<th style="padding:8px 12px; text-align:left; color:#888;">Launcher</th>
<th style="padding:8px 12px; text-align:left; color:#888;">MIME</th>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);">
<td style="padding:8px 12px;">📷 Camera</td>
<td style="padding:8px 12px;"><code>TakePicture</code> → temp FileProvider URI</td>
<td style="padding:8px 12px;">image/jpeg</td>
</tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);">
<td style="padding:8px 12px;">🖼️ Image</td>
<td style="padding:8px 12px;"><code>GetContent("image/*")</code></td>
<td style="padding:8px 12px;">image/* (gallery picker)</td>
</tr>
<tr>
<td style="padding:8px 12px;">📄 File</td>
<td style="padding:8px 12px;"><code>GetContent("*/*")</code></td>
<td style="padding:8px 12px;">*/* (system file picker)</td>
</tr>
</table>
</body>
</html>