feat: add user-selectable aspect ratio and resolution per thread (#18)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m14s
CI / Lint & Check (push) Successful in 12m50s
CI / Build Windows (push) Successful in 28m36s

## Summary

- Adds a two-step thread creation modal — step 1 picks the mode, step 2 configures generation options
- Art mode now supports user-selectable aspect ratio (1:1, 4:3, 3:4, 16:9, 9:16, 21:9)
- All three modes (Art, Avatar, Replace) now support user-selectable resolution (1K, 2K, 4K)
- Mode label in the input area reflects the chosen settings (e.g. `🩷 Art Mode (16:9) · 4K`)
- Backend cost calculation now scales with resolution
- Regenerated `icon.ico` with clean BMP-only entries to fix Windows local builds

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #18
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #18.
This commit is contained in:
2026-04-13 18:24:46 -07:00
committed by Naomi Carrigan
parent 5bfd25e60d
commit 9f45ee329d
9 changed files with 426 additions and 48 deletions
+34 -9
View File
@@ -68,19 +68,19 @@ const modeLabel = (mode: Mode): string => {
interface BuildUserPartsOptions {
readonly imageBase64?: string;
readonly imageMime?: string;
readonly mode: Mode;
readonly text: string;
}
const buildUserParts = ({
imageBase64,
imageMime,
mode,
text,
}: BuildUserPartsOptions): Array<MessagePart> => {
const userParts: Array<MessagePart> = [];
if (mode === "replace" && imageBase64 !== undefined) {
if (imageBase64 === undefined) {
userParts.push({ text: text, type: "text" });
} else {
userParts.push({
imageData: imageBase64,
mimeType: imageMime ?? "image/png",
@@ -89,8 +89,6 @@ const buildUserParts = ({
if (text.length > 0) {
userParts.push({ text: text, type: "text" });
}
} else {
userParts.push({ text: text, type: "text" });
}
return userParts;
@@ -174,13 +172,18 @@ const threadView = ({
onLoadingChange(true);
onErrorChange(undefined);
const { messages, mode } = thread;
const {
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const userParts = buildUserParts({
/* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */
...(imageBase64 !== undefined && { imageBase64 }),
/* eslint-disable-next-line stylistic/no-extra-parens -- Required for conditional spread with exactOptionalPropertyTypes */
...(imageMime !== undefined && { imageMime }),
mode,
text,
});
@@ -217,7 +220,9 @@ const threadView = ({
"send_message",
{
apiKey,
aspectRatio,
history,
imageSize,
mode,
userImageBase64,
userImageMime,
@@ -253,7 +258,13 @@ const threadView = ({
const handleRetry = useCallback(
(modelMessageIndex: number): void => {
const { messages, mode } = thread;
const {
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const userMessageIndex = modelMessageIndex - 1;
const userMessage = messages[userMessageIndex];
@@ -300,7 +311,9 @@ const threadView = ({
}
const response = await invoke<SendResult>("send_message", {
apiKey,
aspectRatio,
history,
imageSize,
mode,
userImageBase64,
userImageMime,
@@ -353,7 +366,13 @@ const threadView = ({
const handleEditCommit = useCallback(
(messageIndex: number, editedText: string): void => {
const { messages, mode } = thread;
const {
aspectRatio,
imageSize: rawImageSize,
messages,
mode,
} = thread;
const imageSize = rawImageSize ?? "4K";
const originalMessage = messages[messageIndex];
if (originalMessage === undefined) {
return;
@@ -404,7 +423,9 @@ const threadView = ({
}
const response = await invoke<SendResult>("send_message", {
apiKey,
aspectRatio,
history,
imageSize,
mode,
userImageBase64,
userImageMime,
@@ -536,7 +557,11 @@ const threadView = ({
{...(pendingInput?.text !== undefined && {
initialText: pendingInput.text,
})}
{...(thread.aspectRatio !== undefined && {
aspectRatio: thread.aspectRatio,
})}
hasMessages={thread.messages.length > 0}
imageSize={thread.imageSize ?? "4K"}
isLoading={isLoading}
mode={thread.mode}
onInputChange={onPendingInputChange}