diff --git a/api/routers/generation.py b/api/routers/generation.py index ad59899..d64843d 100644 --- a/api/routers/generation.py +++ b/api/routers/generation.py @@ -127,8 +127,7 @@ async def _run_generation(job_id: str, image_bytes: bytes, params: dict, collect job.status = "running" def progress_cb(pct: int, step: str = "") -> None: - if pct > job.progress: - job.progress = pct + job.progress = pct if step: job.step = step diff --git a/api/services/extension_process.py b/api/services/extension_process.py index 60ce678..df0e8f7 100644 --- a/api/services/extension_process.py +++ b/api/services/extension_process.py @@ -190,7 +190,7 @@ def _read_loop(self, proc: subprocess.Popen, msg_queue: queue.Queue) -> None: try: msg_queue.put(json.loads(line)) except json.JSONDecodeError: - print(f"[{self.MODEL_ID}] {line}", file=sys.stderr) + print(f"[{self.MODEL_ID}] bad JSON: {line}", file=sys.stderr) finally: msg_queue.put(None) # sentinel: process is done diff --git a/src/areas/generate/components/WorkflowPanel.tsx b/src/areas/generate/components/WorkflowPanel.tsx index 5d6c14f..ceabe76 100644 --- a/src/areas/generate/components/WorkflowPanel.tsx +++ b/src/areas/generate/components/WorkflowPanel.tsx @@ -450,6 +450,7 @@ function EmbeddedCanvas({ workflow, allExtensions }: { const [edges, setEdges, onEdgesChange] = useEdgesState(workflow.edges as FlowEdge[]) const { updateNodeData } = useReactFlow() const { navigate } = useNavStore() + const saveTimer = useRef | null>(null) // Direct patch into controlled nodes state — no React Flow store dependency const patchNode = useCallback((nodeId, patch) => { @@ -458,6 +459,36 @@ function EmbeddedCanvas({ workflow, allExtensions }: { )) }, [setNodes]) + // ─── Tab sync ────────────────────────────────────────────────────────────── + const lastSyncedAtRef = useRef(workflow.updatedAt) + const didMountRef = useRef(false) + + // Sync local state when Workflows tab saves to the store (Workflows→Generate) + useEffect(() => { + if (workflow.updatedAt === lastSyncedAtRef.current) return + setNodes(workflow.nodes as FlowNode[]) + setEdges(workflow.edges as FlowEdge[]) + lastSyncedAtRef.current = workflow.updatedAt + }, [workflow.updatedAt]) + + // Debounced save to the store when local state changes (Generate→Workflows) + // No cleanup return — lets the timer fire even if user navigates away + useEffect(() => { + if (!didMountRef.current) { didMountRef.current = true; return } + if (saveTimer.current) clearTimeout(saveTimer.current) + saveTimer.current = setTimeout(() => { + const now = new Date().toISOString() + const updated: Workflow = { + ...workflow, + nodes: nodes as WFNode[], + edges: edges as WFEdge[], + updatedAt: now, + } + lastSyncedAtRef.current = now + useWorkflowsStore.getState().save(updated) + }, 500) + }, [nodes, edges]) + const currentMeshUrl = useAppStore((s) => s.currentJob?.outputUrl) const showToast = useAppStore((s) => s.showToast) const { runState, run, cancel } = useWorkflowRunStore() @@ -659,7 +690,6 @@ export default function WorkflowPanel() { {workflow ? ( diff --git a/src/areas/workflows/WorkflowsPage.tsx b/src/areas/workflows/WorkflowsPage.tsx index 2f5c849..31b0a61 100644 --- a/src/areas/workflows/WorkflowsPage.tsx +++ b/src/areas/workflows/WorkflowsPage.tsx @@ -807,7 +807,8 @@ function WorkflowCanvasInner({ const historyRef = useRef([{ nodes: workflow.nodes as Node[], edges: workflow.edges as Edge[], name: workflow.name }]) const histIdxRef = useRef(0) const [histIdx, setHistIdx] = useState(0) - const skipPushRef = useRef(true) // skip the initial autosave-triggered push + const skipPushRef = useRef(true) // skip the initial autosave-triggered push + const lastSavedAtRef = useRef(workflow.updatedAt) // Re-sync when workflow switches useEffect(() => { @@ -818,19 +819,34 @@ function WorkflowCanvasInner({ histIdxRef.current = 0 setHistIdx(0) skipPushRef.current = true + lastSavedAtRef.current = workflow.updatedAt }, [workflow.id]) - // Auto-save + history push debounced + // Re-sync when Generate tab (or another external source) saves param changes + useEffect(() => { + if (workflow.updatedAt === lastSavedAtRef.current) return + setNodes(workflow.nodes as Node[]) + setEdges(workflow.edges as Edge[]) + setName(workflow.name) + skipPushRef.current = true + lastSavedAtRef.current = workflow.updatedAt + }, [workflow.updatedAt]) + + // Auto-save + history push debounced. + // No cleanup return — lets the timer fire even if the user navigates away + // before the debounce expires, keeping both tabs in sync. useEffect(() => { if (saveTimer.current) clearTimeout(saveTimer.current) saveTimer.current = setTimeout(() => { + const now = new Date().toISOString() const updated: Workflow = { ...workflow, name, nodes: nodes as WFNode[], edges: edges as WFEdge[], - updatedAt: new Date().toISOString(), + updatedAt: now, } + lastSavedAtRef.current = now onSave(updated) if (!skipPushRef.current) { @@ -844,7 +860,6 @@ function WorkflowCanvasInner({ } skipPushRef.current = false }, 500) - return () => { if (saveTimer.current) clearTimeout(saveTimer.current) } }, [nodes, edges, name]) const preflightIssues = useMemo(() => {