Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion app/src/main/kotlin/com/google/ai/sample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
}
27 changes: 22 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Security Vulnerability: Using innerHTML with unsanitized content creates XSS vulnerability. The 'text' variable from API responses should be sanitized before rendering.1

Suggested change
.bubble-thumb{display:block;max-width:160px;max-height:160px;border-radius:8px;margin-bottom:8px;cursor:pointer;object-fit:cover}
messageElement.textContent = text;

Footnotes

  1. CWE-79: Cross-site Scripting (XSS) - https://cwe.mitre.org/data/definitions/79.html

.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}
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -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 = `<div class="bubble user">${escHtml(text)}</div>
const thumbHtml = imageDataUri
? `<img class="bubble-thumb" src="${escAttr(imageDataUri)}" alt="Screenshot" onclick="_showThumbLightbox('${escAttr(imageDataUri)}')" />`
: '';
wrap.innerHTML = `<div class="bubble user">${thumbHtml}${escHtml(text)}</div>
<button class="btn-icon" onclick="undoLast('${escAttr(text)}')" title="Undo">&#10227;</button>`;
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 = `<img src="${escAttr(src)}" alt="Screenshot" />`;
document.body.appendChild(overlay);
}
function addModelBubble(text, isPending) {
const wrap = document.createElement('div');
wrap.className = 'bubble-wrap';
Expand Down Expand Up @@ -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 = '';
Expand Down
Loading