Files
tatsumi/src/components/newThreadModal.tsx
T
hikari f2c4fb34b7
CI / Lint & Check (push) Failing after 12s
CI / Build Windows (push) Has been skipped
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m5s
feat: initial Tatsumi release
Tatsumi is a Tauri desktop app for generating AI character art of Naomi
using Google Gemini's image model. Features three generation modes
(avatar, art, replace), persistent conversation threads, message
editing and deletion, retry support, cost tracking, and an about modal
with lore-accurate self-introduction from Emi Carrigan.
2026-04-09 20:16:54 -07:00

184 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Modal for selecting a new thread generation mode.
* @copyright Naomi Carrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable max-lines-per-function -- Component JSX inherently requires many lines */
import { useCallback, type JSX, type MouseEvent } from "react";
import type { Mode } from "../types/index.ts";
interface ModeOption {
colour: string;
description: string;
icon: string;
label: string;
mode: Mode;
}
const avatarDescription = [
"Generate a square 1:1 portrait.",
"Perfect for profile pictures and avatars.",
].join(" ");
const artDescription = [
"Generate wide 16:9 landscape artwork.",
"Great for wallpapers and banners.",
].join(" ");
const replaceDescription = [
"Upload an image and have Naomi redrawn in",
"anime style with the same pose and outfit.",
].join(" ");
const modeOptionList: Array<ModeOption> = [
{
colour: "purple",
description: avatarDescription,
icon: "👤",
label: "Avatar Mode",
mode: "avatar",
},
{
colour: "pink",
description: artDescription,
icon: "🎨",
label: "Art Mode",
mode: "art",
},
{
colour: "blue",
description: replaceDescription,
icon: "🔄",
label: "Replace Mode",
mode: "replace",
},
];
const colourMap: Record<
string,
{ badge: string; button: string; hover: string }
> = {
blue: {
badge: "bg-blue-900/40 text-blue-300",
button: "bg-blue-600 hover:bg-blue-500",
hover: "hover:border-blue-400 hover:bg-blue-900/20",
},
pink: {
badge: "bg-pink-900/40 text-pink-300",
button: "bg-pink-600 hover:bg-pink-500",
hover: "hover:border-pink-400 hover:bg-pink-900/20",
},
purple: {
badge: "bg-purple-900/40 text-purple-300",
button: "bg-purple-600 hover:bg-purple-500",
hover: "hover:border-purple-400 hover:bg-purple-900/20",
},
};
const validModesSet = new Set<string>([ "avatar", "art", "replace" ]);
const isMode = (value: string): value is Mode => {
return validModesSet.has(value);
};
const overlayClass = [
"fixed inset-0 bg-black/70 backdrop-blur-sm",
"flex items-center justify-center z-50",
].join(" ");
const panelClass = [
"bg-[#1a1028] border border-purple-900/40",
"rounded-2xl p-8 max-w-2xl w-full mx-4",
"shadow-2xl shadow-purple-900/30",
].join(" ");
interface NewThreadModalProperties {
readonly onClose: ()=> void;
readonly onSelect: (mode: Mode)=> void;
}
/**
* Renders the new thread modal for selecting a generation mode.
* @param props - The component props.
* @param props.onClose - Callback to close the modal.
* @param props.onSelect - Callback when a mode is selected.
* @returns The JSX element.
*/
const threadModal = ({
onClose,
onSelect,
}: NewThreadModalProperties): JSX.Element => {
const handleModeSelect = useCallback(
(event: MouseEvent<HTMLButtonElement>): void => {
const rawMode = event.currentTarget.dataset.mode;
if (rawMode !== undefined && isMode(rawMode)) {
onSelect(rawMode);
}
},
[ onSelect ],
);
return (
<div className={overlayClass}>
<div className={panelClass}>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">
{"New Thread"}
</h2>
<button
className="text-gray-400 hover:text-white transition-colors"
onClick={onClose}
type="button"
>
{"×"}
</button>
</div>
<p className="text-gray-400 mb-6">
{"Choose a generation mode to begin:"}
</p>
<div className="flex flex-col gap-4">
{modeOptionList.map((option) => {
const colours = colourMap[option.colour];
const buttonClass = [
"flex items-center gap-4 p-5 rounded-xl",
"border border-purple-900/30 bg-[#241836]",
"text-left transition-all duration-200",
colours?.hover ?? "",
].join(" ");
return (
<button
className={buttonClass}
data-mode={option.mode}
key={option.mode}
onClick={handleModeSelect}
type="button"
>
<span className="text-4xl">{option.icon}</span>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="text-white font-semibold text-lg">
{option.label}
</span>
<span
className={`text-xs px-2 py-0.5 rounded-full ${colours?.badge ?? ""}`}
>
{option.mode}
</span>
</div>
<p className="text-gray-400 text-sm">{option.description}</p>
</div>
<span className="text-gray-500 text-xl">{"→"}</span>
</button>
);
})}
</div>
</div>
</div>
);
};
export { threadModal as NewThreadModal };