import { useEffect, useRef, useState, type RefObject } from "react"; import { CaretDown, CaretUp } from "@phosphor-icons/react"; import { commandIds } from "@commands/ids"; import type { AppStore } from "@editor/store"; import { generateArchitectureDefaults } from "@editor/tools"; import type { GenerateArchitecture, GenerateMode, GenerateModel, GenerateSettings } from "@editor/tools"; import { BottomControlSelectMenu, type BottomControlSelectOption } from "./SelectMenu"; import { BottomControlSlider } from "./Slider"; const architectures = [ { value: "sdxl", label: "SDXL" }, { value: "z-image", label: "Z-Image" }, { value: "z-image-turbo", label: "Z-Image Turbo" }, { value: "anima", label: "Anima" }, ] satisfies readonly BottomControlSelectOption[]; const modes = [ { value: "text-to-image", label: "Text → image" }, { value: "image-to-image", label: "Image → image" }, { value: "inpaint", label: "Inpaint" }, { value: "outpaint", label: "Outpaint" }, ] satisfies readonly BottomControlSelectOption[]; const sizePresets = [ { label: "1:1", w: 1024, h: 1024 }, { label: "4:3", w: 1152, h: 896 }, { label: "3:4", w: 896, h: 1152 }, { label: "16:9", w: 1344, h: 768 }, { label: "9:16", w: 768, h: 1344 }, ] as const; const inpaintPolarityOptions = [ { value: "hidden", label: "Hidden / erased" }, { value: "revealed", label: "Revealed / painted" }, ] satisfies readonly BottomControlSelectOption[]; const inpaintMaskedContentOptions = [ { value: "neutral", label: "Neutral fill" }, { value: "original", label: "Original gray" }, { value: "originalColor", label: "Original color" }, { value: "edges", label: "Edge map" }, ] satisfies readonly BottomControlSelectOption[]; export type GenerateControlsProps = { settings: GenerateSettings; dispatch: AppStore["dispatch"]; }; type ComfyArchitectureOption = { value: GenerateArchitecture; label: string; defaultModel: string; models: string[]; supportedModes: GenerateMode[]; }; type ComfyOptionsResponse = { architectures?: ComfyArchitectureOption[]; models?: string[]; textEncoders?: string[]; vaes?: string[]; samplers?: string[]; schedulers?: string[]; }; export function GenerateControls({ settings, dispatch }: GenerateControlsProps) { const [comfyOptions, setComfyOptions] = useState(); const [advancedOpen, setAdvancedOpen] = useState(false); const [outpaintOpen, setOutpaintOpen] = useState(false); const [inpaintOpen, setInpaintOpen] = useState(false); const [sizeOpen, setSizeOpen] = useState(false); const sizeRef = useRef(null); const [error, setError] = useState(); const modelOptions = resolveModelOptions(settings, comfyOptions); const supportOptions = resolveSupportOptions(settings, comfyOptions); const samplerOptions = resolveStringOptions(comfyOptions?.samplers, settings.sampler); const schedulerOptions = resolveStringOptions(comfyOptions?.schedulers, settings.scheduler); const modeOptions = resolveModeOptions(settings, comfyOptions); useEffect(() => { let cancelled = false; void fetch("/api/comfy/models") .then((response) => response.ok ? response.json() : Promise.reject(new Error("Unable to load ComfyUI models"))) .then((body: ComfyOptionsResponse) => { if (cancelled) return; setComfyOptions(body); }) .catch((reason: unknown) => { if (!cancelled) setError(reason instanceof Error ? reason.message : "Unable to load ComfyUI models"); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (!sizeOpen) return; const close = (event: PointerEvent) => { if (!sizeRef.current?.contains(event.target as Node)) setSizeOpen(false); }; window.addEventListener("pointerdown", close); return () => window.removeEventListener("pointerdown", close); }, [sizeOpen]); return (
{error ?

{error}

: null}