From 10e013a983f72e7ce63749bec7e41a723b2fd4c7 Mon Sep 17 00:00:00 2001 From: Hikari Date: Mon, 13 Apr 2026 09:18:20 -0700 Subject: [PATCH] feat: add image input to art/avatar modes and notes to replace mode - Art and Avatar modes now support optional image uploads via paste or file picker, sent to Gemini alongside the text prompt - Initial Replace mode gains an optional textarea for notes to include with the uploaded image - Backend updated to send user images for all modes, appending replace instructions only when in replace mode - Send validation updated to allow image-only messages in art/avatar --- src-tauri/src/gemini.rs | 24 ++++++----- src/components/inputArea.tsx | 80 +++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src-tauri/src/gemini.rs b/src-tauri/src/gemini.rs index 994c3e2..f88e731 100644 --- a/src-tauri/src/gemini.rs +++ b/src-tauri/src/gemini.rs @@ -83,22 +83,26 @@ fn build_user_gemini_parts( user_image_base64: &Option, user_image_mime: &Option, ) -> Vec { - if mode == "replace" && user_image_base64.is_some() { + if let Some(image_data) = user_image_base64.as_deref() { let mime = user_image_mime.as_deref().unwrap_or("image/png"); - let data = user_image_base64.as_deref().unwrap_or(""); let base_text = user_text.as_deref().unwrap_or(""); - let final_text = if base_text.is_empty() { - REPLACE_MODE_APPEND.to_string() + let final_text = if mode == "replace" { + if base_text.is_empty() { + REPLACE_MODE_APPEND.to_string() + } else { + format!("{}\n{}", base_text, REPLACE_MODE_APPEND) + } } else { - format!("{}\n{}", base_text, REPLACE_MODE_APPEND) + base_text.to_string() }; - vec![ - json!({"inlineData": {"mimeType": mime, "data": data}}), - json!({"text": final_text}), - ] + let mut parts = vec![json!({"inlineData": {"mimeType": mime, "data": image_data}})]; + if !final_text.is_empty() { + parts.push(json!({"text": final_text})); + } + parts } else { - // Art/avatar mode, or replace mode follow-up correction (text only) + // No image: text-only message let text = user_text.as_deref().unwrap_or(""); vec![json!({"text": text})] } diff --git a/src/components/inputArea.tsx b/src/components/inputArea.tsx index 9b5f339..3f9faf5 100644 --- a/src/components/inputArea.tsx +++ b/src/components/inputArea.tsx @@ -256,7 +256,8 @@ const inputArea = ({ if (isInitialReplace && imageBase64 === undefined) { return; } - if (!isInitialReplace && text.trim().length === 0) { + const hasContent = text.trim().length > 0 || imageBase64 !== undefined; + if (!isInitialReplace && !hasContent) { return; } @@ -332,6 +333,9 @@ const inputArea = ({ : dropZoneInactiveClass, ].join(" "); + const hasNoContent = text.trim().length === 0 && imageBase64 === undefined; + const isSendDisabled = isLoading || hasNoContent; + return (
@@ -397,6 +401,15 @@ const inputArea = ({ type="file" /> +