feat: add user-selectable aspect ratio and resolution per thread
CI / Lint & Check (pull_request) Failing after 10m50s
CI / Build Windows (pull_request) Has been skipped
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m50s

Adds a two-step new thread modal: step one picks mode, step two
configures aspect ratio (Art mode only, six options) and resolution
(all modes: 1K/2K/4K). Settings are stored on the thread and forwarded
to the Gemini API on every send, retry, and edit. Also regenerates
icon.ico with Python to produce a clean all-BMP ICO compatible with
both Tauri's proc macro and llvm-rc cross-compilation.
This commit is contained in:
2026-04-13 16:29:09 -07:00
parent 5bfd25e60d
commit f3c2e8fa40
9 changed files with 405 additions and 43 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 298 KiB

+25 -7
View File
@@ -46,12 +46,19 @@ fn build_safety_settings() -> Value {
])
}
fn build_generation_config(mode: &str) -> Value {
fn build_generation_config(
mode: &str,
aspect_ratio: Option<&str>,
image_size: &str,
) -> Value {
let image_config = match mode {
"avatar" => json!({ "aspectRatio": "1:1", "imageSize": "4K" }),
"art" => json!({ "aspectRatio": "16:9", "imageSize": "4K" }),
"avatar" => json!({ "aspectRatio": "1:1", "imageSize": image_size }),
"art" => {
let ratio = aspect_ratio.unwrap_or("16:9");
json!({ "aspectRatio": ratio, "imageSize": image_size })
}
// replace mode: omit aspectRatio so the model infers it from the source image
_ => json!({ "imageSize": "4K" }),
_ => json!({ "imageSize": image_size }),
};
json!({
"imageConfig": image_config,
@@ -111,6 +118,8 @@ fn build_user_gemini_parts(
pub async fn call_gemini(
api_key: String,
mode: String,
aspect_ratio: Option<String>,
image_size: String,
history: Vec<ThreadMessage>,
user_text: Option<String>,
user_image_base64: Option<String>,
@@ -160,7 +169,11 @@ pub async fn call_gemini(
contents.push(json!({"role": "user", "parts": user_parts}));
let generation_config = build_generation_config(mode.as_str());
let generation_config = build_generation_config(
mode.as_str(),
aspect_ratio.as_deref(),
image_size.as_str(),
);
let safety_settings = build_safety_settings();
let request_body = json!({
@@ -252,8 +265,13 @@ pub async fn call_gemini(
let candidates_tokens = usage["candidatesTokenCount"].as_u64().unwrap_or(0);
let image_part_count = result_parts.iter().filter(|p| p.part_type == "image").count() as u64;
// Image output tokens (4K = 2000 tokens each) billed at $120/1M tokens
let image_output_tokens = image_part_count * 2_000_u64;
// Image output tokens per image vary by resolution, billed at $120/1M tokens
let tokens_per_image: u64 = match image_size.as_str() {
"1K" => 500,
"2K" => 1_000,
_ => 2_000, // 4K default
};
let image_output_tokens = image_part_count * tokens_per_image;
// Remaining candidates tokens are text/thinking, billed at $12/1M tokens
let text_output_tokens = candidates_tokens.saturating_sub(image_output_tokens);
+13 -2
View File
@@ -49,13 +49,24 @@ async fn save_config(config: Config) -> Result<(), String> {
async fn send_message(
api_key: String,
mode: String,
aspect_ratio: Option<String>,
image_size: String,
history: Vec<ThreadMessage>,
user_text: Option<String>,
user_image_base64: Option<String>,
user_image_mime: Option<String>,
) -> Result<SendMessageResult, String> {
let (parts, cost_usd) =
call_gemini(api_key, mode, history, user_text, user_image_base64, user_image_mime).await?;
let (parts, cost_usd) = call_gemini(
api_key,
mode,
aspect_ratio,
image_size,
history,
user_text,
user_image_base64,
user_image_mime,
)
.await?;
Ok(SendMessageResult { parts, cost_usd })
}
+8
View File
@@ -25,11 +25,19 @@ pub struct ThreadMessage {
pub cost_usd: Option<f64>,
}
fn default_image_size() -> String {
"4K".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thread {
pub id: String,
pub name: String,
pub mode: String,
#[serde(rename = "aspectRatio", skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
#[serde(rename = "imageSize", default = "default_image_size")]
pub image_size: String,
pub messages: Vec<ThreadMessage>,
#[serde(rename = "createdAt")]
pub created_at: i64,