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 type { GenerateMode, GenerateModel, GenerateSettings } from "@editor/tools"; import { BottomControlSelectMenu, type BottomControlSelectOption } from "./SelectMenu"; import { BottomControlSlider } from "./Slider"; 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"]; }; export function GenerateControls({ settings, dispatch }: GenerateControlsProps) { const [models, setModels] = useState[]>([{ value: "auto", label: "Auto" }]); const [samplers, setSamplers] = useState[]>([{ value: settings.sampler, label: settings.sampler }]); const [schedulers, setSchedulers] = useState[]>([{ value: settings.scheduler, label: settings.scheduler }]); 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(); 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: { models?: string[]; samplers?: string[]; schedulers?: string[] }) => { if (cancelled) return; setModels([{ value: "auto", label: "Auto" }, ...(body.models ?? []).map((model) => ({ value: model, label: model }))]); if (body.samplers?.length) setSamplers(body.samplers.map((sampler) => ({ value: sampler, label: sampler }))); if (body.schedulers?.length) setSchedulers(body.schedulers.map((scheduler) => ({ value: scheduler, label: scheduler }))); }) .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}