diff --git a/src/areas/generate/components/GenerationOptions.tsx b/src/areas/generate/components/GenerationOptions.tsx index 2a91d6d..7638f67 100644 --- a/src/areas/generate/components/GenerationOptions.tsx +++ b/src/areas/generate/components/GenerationOptions.tsx @@ -84,24 +84,42 @@ function FloatParam({ schema, value, onChange }: { schema: ParamSchema; value: a ) } +function IntInput({ value, onChange, placeholder, className }: { value: number; onChange: (v: number) => void; placeholder?: string; className: string }) { + const [text, setText] = useState(String(value)) + const prevValue = useRef(value) + if (prevValue.current !== value && parseInt(text, 10) !== value) { + prevValue.current = value + setText(String(value)) + } + return ( + { + const raw = e.target.value + if (raw !== '' && raw !== '-' && !/^-?\d+$/.test(raw)) return + setText(raw) + const n = parseInt(raw, 10) + if (!isNaN(n)) { prevValue.current = n; onChange(n) } + }} + className={className} + /> + ) +} + function IntParam({ schema, value, onChange }: { schema: ParamSchema; value: any; onChange: (v: any) => void }): JSX.Element { const isSeed = schema.id === 'seed' return (
- { - const v = parseInt(e.target.value) - onChange(isNaN(v) ? schema.default : v) - }} - className="w-full px-3 py-1.5 text-xs rounded-lg bg-zinc-900 border border-zinc-700/60 text-zinc-200 focus:outline-none focus:border-zinc-500 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + className="w-full px-3 py-1.5 text-xs rounded-lg bg-zinc-900 border border-zinc-700/60 text-zinc-200 focus:outline-none focus:border-zinc-500" /> {isSeed && (
) } - return ( - onChange(param.type === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10))} - className={inputCls} /> - ) + if (param.type === 'float') { + return onChange(v)} className={inputCls} /> + } + // int + return onChange(v)} className={inputCls} /> } // ─── Workflow dropdown ──────────────────────────────────────────────────────── @@ -362,8 +409,8 @@ function EmbeddedCanvas({ workflow, allExtensions }: { )) }, [setNodes]) - const { setCurrentJob, updateCurrentJob, selectedImagePath, selectedImageData } = useAppStore() - const { runState, run, cancel } = useWorkflowRunner(allExtensions) + const { setCurrentJob } = useAppStore() + const { runState, run, cancel } = useWorkflowRunStore() const isRunning = runState.status === 'running' // Update AddToScene node when run completes @@ -373,21 +420,6 @@ function EmbeddedCanvas({ workflow, allExtensions }: { if (out) updateNodeData(out.id, { params: { outputUrl: runState.outputUrl } }) }, [runState.status, runState.outputUrl]) - // Sync runState → currentJob - useEffect(() => { - if (runState.status === 'running') { - const total = runState.blockTotal - const overall = total > 0 - ? Math.round((runState.blockIndex / total) * 100 + runState.blockProgress / total) - : runState.blockProgress - updateCurrentJob({ status: 'generating', progress: overall, step: runState.blockStep }) - } else if (runState.status === 'done') { - updateCurrentJob({ status: 'done', progress: 100, outputUrl: runState.outputUrl }) - } else if (runState.status === 'error') { - updateCurrentJob({ status: 'error', error: runState.error }) - } - }, [runState]) - // Type mismatch detection const typeMismatch = useMemo(() => { const sorted = topoSortNodes(workflow.nodes, workflow.edges) @@ -418,28 +450,9 @@ function EmbeddedCanvas({ workflow, allExtensions }: { ) const handleGenerate = useCallback(() => { - const imageNode = nodes.find((n) => n.type === 'imageNode') - const imagePath = (imageNode?.data?.params?.filePath as string | undefined) ?? selectedImagePath ?? '' - const imageData = selectedImageData ?? undefined - - // Capture the current mesh URL *before* setCurrentJob overwrites it - const currentMeshUrl = useAppStore.getState().currentJob?.outputUrl - - setCurrentJob({ - id: crypto.randomUUID(), - imageFile: imagePath, - status: 'uploading', - progress: 0, - createdAt: Date.now(), - }) - - run( - { ...workflow, nodes: nodes as WFNode[], edges: edges as WFEdge[] }, - imagePath, - imageData, - currentMeshUrl, - ) - }, [nodes, edges, workflow, selectedImagePath, selectedImageData, allExtensions, setCurrentJob, run]) + const wf: Workflow = { ...workflow, nodes: nodes as WFNode[], edges: edges as WFEdge[] } + run(wf, allExtensions) + }, [nodes, edges, workflow, allExtensions, run]) return (
@@ -495,7 +508,7 @@ function EmbeddedCanvas({ workflow, allExtensions }: {
)} {isRunning ? ( - diff --git a/src/areas/workflows/nodes/ExtensionNode.tsx b/src/areas/workflows/nodes/ExtensionNode.tsx index 3e3bfa0..f386989 100644 --- a/src/areas/workflows/nodes/ExtensionNode.tsx +++ b/src/areas/workflows/nodes/ExtensionNode.tsx @@ -25,6 +25,55 @@ const TAG_CLS: Record = { const inputCls = 'w-full bg-zinc-800 border border-zinc-700 rounded-lg px-2 py-1 text-[11px] text-zinc-200 focus:outline-none focus:border-accent/60' +function IntInput({ value, onChange, className }: { value: number; onChange: (v: number) => void; className: string }) { + const [text, setText] = useState(String(value)) + const prevValue = useRef(value) + if (prevValue.current !== value && parseInt(text, 10) !== value) { + prevValue.current = value + setText(String(value)) + } + return ( + { + const raw = e.target.value + if (raw !== '' && raw !== '-' && !/^-?\d+$/.test(raw)) return + setText(raw) + const n = parseInt(raw, 10) + if (!isNaN(n)) { prevValue.current = n; onChange(n) } + }} + className={className} + /> + ) +} + +function FloatInput({ value, onChange, className }: { value: number; onChange: (v: number) => void; className: string }) { + const [text, setText] = useState(String(value)) + // Sync when external value changes (e.g. reset) + const prevValue = useRef(value) + if (prevValue.current !== value && parseFloat(text.replace(',', '.')) !== value) { + prevValue.current = value + setText(String(value)) + } + return ( + { + const raw = e.target.value.replace(',', '.') + if (raw !== '' && raw !== '-' && raw !== '.' && !/^-?\d*\.?\d*$/.test(raw)) return + setText(e.target.value) + const num = parseFloat(raw) + if (!isNaN(num)) { prevValue.current = num; onChange(num) } + }} + className={className} + /> + ) +} + function ParamControl({ param, value, onChange }: { param: ParamSchema value: number | string @@ -58,12 +107,11 @@ function ParamControl({ param, value, onChange }: {
) } - return ( - onChange(param.type === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10))} - className={inputCls} /> - ) + if (param.type === 'float') { + return onChange(v)} className={inputCls} /> + } + // int + return onChange(v)} className={inputCls} /> } // ─── ExtensionNode ──────────────────────────────────────────────────────────── diff --git a/src/areas/workflows/workflowRunStore.ts b/src/areas/workflows/workflowRunStore.ts index e9ff51c..1930907 100644 --- a/src/areas/workflows/workflowRunStore.ts +++ b/src/areas/workflows/workflowRunStore.ts @@ -76,6 +76,11 @@ export const useWorkflowRunStore = create((set) => ({ const ordered = topoSort(workflow.nodes, workflow.edges) const execNodes = ordered.filter((n) => n.type === 'extensionNode' && n.data.enabled) + // Capture before setCurrentJob overwrites currentJob + const selectedImagePath = appState.selectedImagePath ?? '' + const selectedImageData = appState.selectedImageData ?? undefined + const currentMeshUrl = appState.currentJob?.outputUrl + set({ activeWorkflowId: workflow.id, runState: { status: 'running', blockIndex: 0, blockTotal: execNodes.length, blockProgress: 0, blockStep: 'Starting…' }, @@ -83,7 +88,7 @@ export const useWorkflowRunStore = create((set) => ({ appState.setCurrentJob({ id: crypto.randomUUID(), - imageFile: '', + imageFile: selectedImagePath, status: 'generating', progress: 0, createdAt: Date.now(), @@ -94,10 +99,7 @@ export const useWorkflowRunStore = create((set) => ({ const settings = await window.electron.settings.get() const workspaceDir = settings.workspaceDir.replace(/\\/g, '/') - const nodeOutputs = new Map() - const selectedImagePath = useAppStore.getState().selectedImagePath ?? '' - const selectedImageData = useAppStore.getState().selectedImageData ?? undefined - const currentMeshUrl = useAppStore.getState().currentJob?.outputUrl + const nodeOutputs = new Map() // Pre-populate source nodes for (const node of ordered) {