feat: add ComfyUI integration for image generation and management

- Implemented ComfyGenerateRequest type and associated functions for generating images using various architectures and modes.
- Added functions for listing generation options and handling image uploads.
- Created workflows for different generation modes including SDXL, Z-Image, Z-Image Turbo, and Anima.
- Introduced GenerationJobStatus component to display the status of ongoing generation jobs.
- Developed MaskControls for managing mask operations and displaying mask analysis.
- Created palette items for tool selection, layer management, and generation settings.
This commit is contained in:
syntaxbullet
2026-07-10 23:15:02 +02:00
parent 41bbd1e16b
commit 5e4b548ad4
80 changed files with 2114 additions and 1954 deletions

View File

@@ -1,23 +1,24 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect } from "react";
import { DownloadSimple, FolderOpen, Sparkle, Stack } from "@phosphor-icons/react";
import type { ImageStudioApp } from "@app/app";
import { commandIds } from "@commands/ids";
import { BottomControlsIsland } from "./BottomControlsIsland";
import { brushUnavailableHint } from "./canvas/brush";
import { brushUnavailableHint } from "@operations/paint/brush";
import { CanvasViewport } from "./CanvasViewport";
import { CommandPalette } from "./CommandPalette";
import { GenerateSheet } from "./GenerateSheet";
import { GenerationJobStatus } from "./GenerationJobStatus";
import { LayersSheet } from "./LayersSheet";
import { ShortcutsDisplay } from "./ShortcutsDisplay";
import { ToolOverlay } from "./ToolOverlay";
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
import type { AppState } from "@editor/state";
import type { ToolId } from "@editor/tools";
import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index";
import { shallowEqual, useAppState } from "./useAppState";
import { downloadArtboardPng } from "./exportArtboardPng";
import { downloadArtboardPng } from "@operations/export/downloadArtboard";
import { useImageImport } from "./useImageImport";
import { useViewportActivityIsland } from "./useViewportActivityIsland";
import { loadGenerationResources } from "@operations/generation/loadResources";
import "./index.css";
export type AppProps = {
@@ -26,40 +27,25 @@ export type AppProps = {
export function App({ app }: AppProps) {
const shellState = useAppState(app.store, selectAppShellState, shallowEqual);
const { document, selection, viewport, tools, generation, commandPalette, transformSession, maskEdit } = shellState;
const { document, selection, viewport, tools, generation, commandPalette, transformSession, maskEdit, workspace } = shellState;
const viewportActivityIsland = useViewportActivityIsland(viewport);
const imageImport = useImageImport(app.store);
const [layersOpen, setLayersOpen] = useState(false);
const previousGenerateTool = useRef<ToolId>("select");
const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection);
const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0];
const generateOpen = tools.activeTool === "generate";
const generateOpen = workspace.panel === "generate";
const layersOpen = workspace.panel === "layers";
useEffect(() => {
if (tools.activeTool !== "generate") {
previousGenerateTool.current = tools.activeTool;
return;
}
setLayersOpen(false);
}, [tools.activeTool]);
const getGenerateReturnTool = useCallback((): ToolId => {
return previousGenerateTool.current === "generate" ? "select" : previousGenerateTool.current;
}, []);
if (generateOpen) void loadGenerationResources(app.store);
}, [app.store, generateOpen]);
const openGenerate = useCallback(() => {
const activeTool = app.store.getState().editor.tools.activeTool;
if (activeTool !== "generate") previousGenerateTool.current = activeTool;
app.store.dispatch(commandIds.toolSetActive, { tool: "generate" });
setLayersOpen(false);
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "generate" });
}, [app.store]);
const closeGenerate = useCallback(() => {
if (app.store.getState().editor.tools.activeTool === "generate") {
app.store.dispatch(commandIds.toolSetActive, { tool: getGenerateReturnTool() });
}
}, [app.store, getGenerateReturnTool]);
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" });
}, [app.store]);
const toggleGenerate = useCallback(() => {
if (generateOpen) closeGenerate();
@@ -67,19 +53,16 @@ export function App({ app }: AppProps) {
}, [closeGenerate, generateOpen, openGenerate]);
const openLayers = useCallback(() => {
closeGenerate();
setLayersOpen(true);
}, [closeGenerate]);
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "layers" });
}, [app.store]);
const closeLayers = useCallback(() => {
setLayersOpen(false);
}, []);
app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" });
}, [app.store]);
const toggleLayers = useCallback(() => {
const nextOpen = !layersOpen;
if (nextOpen) closeGenerate();
setLayersOpen(nextOpen);
}, [closeGenerate, layersOpen]);
app.store.dispatch(commandIds.workspaceSetPanel, { panel: layersOpen ? "none" : "layers" });
}, [app.store, layersOpen]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@@ -167,6 +150,9 @@ export function App({ app }: AppProps) {
/>
<header className="pointer-events-none absolute inset-x-3 top-3 z-10 flex h-20 items-center justify-end gap-4 rounded-full px-4 text-white backdrop-blur-xl">
<div className="pointer-events-auto flex items-center gap-2">
<button type="button" className="border-0 bg-transparent p-0" onClick={openGenerate} title="Open generation activity">
<GenerationJobStatus generation={generation} compact />
</button>
<button type="button" className={topBarButtonClass()} onClick={imageImport.openFilePicker}>
<FolderOpen size={24} />
</button>
@@ -190,6 +176,7 @@ export function App({ app }: AppProps) {
</div>
<GenerateSheet
settings={tools.generate}
resources={generation.resources}
open={generateOpen}
dispatch={app.store.dispatch}
/>
@@ -243,6 +230,7 @@ type AppShellState = {
commandPalette: AppState["editor"]["commandPalette"];
transformSession: AppState["editor"]["transformSession"];
maskEdit: AppState["editor"]["maskEdit"];
workspace: AppState["editor"]["workspace"];
};
function selectAppShellState(state: AppState): AppShellState {
@@ -255,6 +243,7 @@ function selectAppShellState(state: AppState): AppShellState {
commandPalette: state.editor.commandPalette,
transformSession: state.editor.transformSession,
maskEdit: state.editor.maskEdit,
workspace: state.editor.workspace,
};
}