Compare commits

..

4 Commits

Author SHA1 Message Date
hikari 0903033180 fix(ci): regenerate icon.ico with BMP-only entries
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m23s
CI / Lint & Check (pull_request) Successful in 13m5s
CI / Build Windows (pull_request) Successful in 28m40s
All six entries in the original icon.ico used PNG compression inside
the ICO container. Older versions of llvm-rc (used during Windows
cross-compilation) cannot parse PNG-compressed ICO entries, causing
the resource compilation step to fail since PR #1.

Regenerated with ImageMagick using -compress None to force BMP format
for all sizes including 256x256. Reverts the LLVM pinning attempts
as they are no longer needed.
2026-04-13 13:15:04 -07:00
hikari ef7f74a919 fix(ci): use LLVM 18 bin path instead of update-alternatives
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 2m6s
CI / Lint & Check (pull_request) Successful in 15m25s
CI / Build Windows (pull_request) Failing after 30m52s
clang-cl-18 does not have a versioned symlink in /usr/bin on this
runner, causing update-alternatives to fail and abort the step before
llvm-rc was ever configured. Prepending /usr/lib/llvm-18/bin to PATH
is more robust and ensures all LLVM 18 tools (clang-cl, lld-link,
llvm-rc) are resolved correctly without relying on symlink names.
2026-04-13 12:13:39 -07:00
hikari 42b4958c2a fix(ci): pin LLVM 18 for Windows cross-compilation
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m18s
CI / Lint & Check (pull_request) Successful in 14m56s
CI / Build Windows (pull_request) Failing after 2m31s
Older llvm-rc versions cannot handle PNG-compressed ICO entries,
causing the Windows build to fail. Pinning to LLVM 18 and setting
up alternatives ensures llvm-rc, clang-cl, and lld-link all resolve
to a version that handles the icon resource correctly.
2026-04-13 11:50:36 -07:00
hikari 662d6119fa feat: add image input to art/avatar modes and notes to replace mode (#16)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m8s
CI / Lint & Check (push) Successful in 12m40s
CI / Build Windows (push) Failing after 25m14s
## Summary

- Art and Avatar modes now support optional image uploads (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 and avatar modes

โœจ This PR was created with help from Hikari~ ๐ŸŒธ

Reviewed-on: #16
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-04-13 10:18:35 -07:00
5 changed files with 68 additions and 47 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.10.1",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-dialog": "2.3.0",
"@tauri-apps/plugin-fs": "2.4.0",
"react": "19.1.0",
+7 -2
View File
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@tauri-apps/api':
specifier: 2.10.1
version: 2.10.1
specifier: 2.5.0
version: 2.5.0
'@tauri-apps/plugin-dialog':
specifier: 2.3.0
version: 2.3.0
@@ -575,6 +575,9 @@ packages:
'@tauri-apps/api@2.10.1':
resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==}
'@tauri-apps/api@2.5.0':
resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==}
'@tauri-apps/cli-darwin-arm64@2.5.0':
resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==}
engines: {node: '>= 10'}
@@ -2844,6 +2847,8 @@ snapshots:
'@tauri-apps/api@2.10.1': {}
'@tauri-apps/api@2.5.0': {}
'@tauri-apps/cli-darwin-arm64@2.5.0':
optional: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 364 KiB

+14 -10
View File
@@ -83,22 +83,26 @@ fn build_user_gemini_parts(
user_image_base64: &Option<String>,
user_image_mime: &Option<String>,
) -> Vec<Value> {
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})]
}
+46 -34
View File
@@ -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 (
<div className="border-t border-purple-900/30 p-4 bg-[#0f0a1a]">
<div className="flex items-center gap-2 mb-2">
@@ -397,6 +401,15 @@ const inputArea = ({
type="file"
/>
<textarea
className={textareaClass}
disabled={isLoading}
onChange={handleTextChange}
placeholder="Add notes to include with the image (optional)..."
rows={2}
value={text}
/>
<button
className={replaceButtonClass}
disabled={isLoading || imageBase64 === undefined}
@@ -414,41 +427,40 @@ const inputArea = ({
</div>
: <div className="flex flex-col gap-3">
{mode === "replace"
? <div className="flex flex-col gap-2">
{imagePreview === undefined
? <button
className={pasteButtonClass}
onClick={handlePasteButtonClick}
<div className="flex flex-col gap-2">
{imagePreview === undefined
? <button
className={pasteButtonClass}
onClick={handlePasteButtonClick}
type="button"
>
{mode === "replace"
? "๐Ÿ“‹ Paste replacement image (optional)"
: "๐Ÿ“‹ Paste image (optional)"}
</button>
: <div className="relative inline-block">
<img
alt="Upload preview"
className="max-h-32 rounded-lg border border-purple-700/40"
src={imagePreview}
/>
<button
className={clearButtonClass}
onClick={clearImage}
type="button"
>
{"๐Ÿ“‹ Paste replacement image (optional)"}
{"ร—"}
</button>
: <div className="relative inline-block">
<img
alt="Upload preview"
className="max-h-32 rounded-lg border border-purple-700/40"
src={imagePreview}
/>
<button
className={clearButtonClass}
onClick={clearImage}
type="button"
>
{"ร—"}
</button>
</div>}
<input
accept="image/*"
className="hidden"
onChange={handleFileChange}
ref={fileInputReference}
type="file"
/>
</div>
: null}
</div>}
<input
accept="image/*"
className="hidden"
onChange={handleFileChange}
ref={fileInputReference}
type="file"
/>
</div>
<div className="flex gap-3 items-end">
<textarea
@@ -464,7 +476,7 @@ const inputArea = ({
/>
<button
className={sendButtonClass}
disabled={isLoading || text.trim().length === 0}
disabled={isSendDisabled}
onClick={handleSend}
type="button"
>