diff --git a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt index e9528bb6..b380325e 100644 --- a/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt +++ b/app/src/main/kotlin/com/google/ai/sample/MainActivity.kt @@ -1594,8 +1594,31 @@ class MainActivity : ComponentActivity() { if (lastUser != null && lastUser.id != lastObservedUserMessageId) { lastObservedUserMessageId = lastUser.id val escaped = escapeForJs(lastUser.text) + // Load the first attached image (e.g. the screenshot just sent to the AI), if + // any, as a base64 data URI so the WebView can render a small thumbnail + // directly inside the user bubble instead of relying on a separate native + // Toast ("Screenshot added, sending to AI..."). + val imageDataUri = lastUser.imageUris.firstOrNull()?.let { uriString -> + try { + val uri = Uri.parse(uriString) + contentResolver.openInputStream(uri)?.use { input -> + val bytes = input.readBytes() + val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + bitmap?.let { com.google.ai.sample.network.PuterApiClient.bitmapToBase64DataUri(it) } + } + } catch (e: Exception) { + Log.w(TAG, "Failed to load image for chat thumbnail: ${e.message}") + null + } + } + val escapedImage = imageDataUri?.let { escapeForJs(it) } wv.post { - wv.evaluateJavascript("window.onUserMessage && window.onUserMessage('$escaped')", null) + val js = if (escapedImage != null) { + "window.onUserMessage && window.onUserMessage('$escaped', '$escapedImage')" + } else { + "window.onUserMessage && window.onUserMessage('$escaped')" + } + wv.evaluateJavascript(js, null) } } diff --git a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreenshotUiNotifier.kt b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreenshotUiNotifier.kt index 8b7d7c60..e3e46636 100644 --- a/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreenshotUiNotifier.kt +++ b/app/src/main/kotlin/com/google/ai/sample/feature/multimodal/PhotoReasoningScreenshotUiNotifier.kt @@ -10,11 +10,13 @@ internal object PhotoReasoningScreenshotUiNotifier { } fun showSendingToAi(context: Context, onStatus: (String) -> Unit) { - onStatus("Screenshot added, sending to AI...") - Toast.makeText(context, com.google.ai.sample.util.UiStringsConfig.get("toast_screenshot_sending", "Screenshot added, sending to AI..."), Toast.LENGTH_SHORT).show() + // No-op Toast: the screenshot now appears as a thumbnail directly inside the user + // bubble in the WebView (see index.html addUserBubble), so this separate native + // notification is no longer needed. onStatus() is kept for any other status-area + // consumers, but intentionally left blank here. } fun showAddedToConversation(context: Context) { - Toast.makeText(context, com.google.ai.sample.util.UiStringsConfig.get("toast_screenshot_added", "Screenshot added to conversation"), Toast.LENGTH_SHORT).show() + // No-op: superseded by the in-bubble screenshot thumbnail in the WebView. } } diff --git a/index.html b/index.html index c1df46f3..ffc6f0e6 100644 --- a/index.html +++ b/index.html @@ -221,6 +221,9 @@ .bubble-wrap.user-wrap{flex-direction:row-reverse} .bubble{border-radius:12px;padding:16px;font-size:17px;line-height:1.55;max-width:82%;word-break:break-word} .bubble.user{background:var(--bubble-user-bg);color:var(--bubble-user-text);white-space:pre-wrap} +.bubble-thumb{display:block;max-width:160px;max-height:160px;border-radius:8px;margin-bottom:8px;cursor:pointer;object-fit:cover} +.thumb-lightbox{position:fixed;inset:0;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;z-index:9999;padding:24px;box-sizing:border-box} +.thumb-lightbox img{max-width:100%;max-height:100%;border-radius:8px} .bubble.model{background:var(--bubble-model-bg);color:var(--bubble-model-text);display:flex;align-items:flex-start;gap:8px} .model-icon{flex-shrink:0;width:24px;height:24px;border-radius:50%;background:#fff;display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;margin-top:1px} .model-text{flex:1;white-space:pre-wrap} @@ -1212,10 +1215,12 @@ // Called by Android after assembling the full user message (typed text + screen elements + // Termux output). Appends a new user bubble so every message appears in order at the bottom. -window.onUserMessage = function(text) { +// imageDataUri (optional) is a base64 data: URI of the screenshot just sent to the AI; when +// present it's rendered as a small thumbnail inside the bubble instead of a separate toast. +window.onUserMessage = function(text, imageDataUri) { if (text) { text = _applyPendingTruncationWarning(text); - addUserBubble(text); + addUserBubble(text, imageDataUri); } }; @@ -4285,14 +4290,25 @@ /* ════════════════════════════════════════════════════════ CHAT ════════════════════════════════════════════════════════ */ -function addUserBubble(text) { +function addUserBubble(text, imageDataUri) { const wrap = document.createElement('div'); wrap.className = 'bubble-wrap user-wrap'; - wrap.innerHTML = `
${escHtml(text)}
+ const thumbHtml = imageDataUri + ? `Screenshot` + : ''; + wrap.innerHTML = `
${thumbHtml}${escHtml(text)}
`; document.getElementById('chat-messages').appendChild(wrap); scrollToBottom(); } +// Tapping a screenshot thumbnail opens it full-size in a simple lightbox overlay. +function _showThumbLightbox(src) { + const overlay = document.createElement('div'); + overlay.className = 'thumb-lightbox'; + overlay.onclick = () => overlay.remove(); + overlay.innerHTML = `Screenshot`; + document.body.appendChild(overlay); +} function addModelBubble(text, isPending) { const wrap = document.createElement('div'); wrap.className = 'bubble-wrap'; @@ -4394,7 +4410,8 @@ // Adding it here too would cause the bubble to appear twice. // In demo/browser mode there is no Android callback, so we add it directly. if (!getInAndroid()) { - addUserBubble(text || "(attached images)"); + const firstThumb = selectedImages.find(img => !img.isVideo && img.objectUrl); + addUserBubble(text || "(attached images)", firstThumb ? firstThumb.objectUrl : null); } input.value = ''; input.style.height = '';