12 changed files with 325 additions and 109 deletions
@ -0,0 +1,120 @@ |
|||
"use client"; |
|||
|
|||
import { useEffect, useRef } from "react"; |
|||
import { App } from "antd"; |
|||
import { useShallow } from "zustand/shallow"; |
|||
import { useI18n } from "@/i18n"; |
|||
import { updateCanvasWorkflow } from "@/lib/canvasWorkflowApi"; |
|||
import { useCanvasWorkflowStore } from "@/store/canvasWorkflowStore"; |
|||
import { useWorkflowStore } from "@/store/workflowStore"; |
|||
import { buildSerializedWorkflow } from "@/store/utils/workflowSerialization"; |
|||
import { pickCanvasWorkflowCoverImage } from "@/utils/canvasWorkflowCoverImage"; |
|||
|
|||
const CANVAS_WORKFLOW_AUTO_UPDATE_DELAY_MS = 5_000; |
|||
|
|||
export function useCanvasWorkflowAutoUpdate() { |
|||
const { t } = useI18n(); |
|||
const { message } = App.useApp(); |
|||
const saveVersionRef = useRef(0); |
|||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
|||
|
|||
const { |
|||
currentCanvasWorkflowId, |
|||
currentCanvasWorkflowTitle, |
|||
currentCanvasWorkflowDescription, |
|||
isLoadingCanvasWorkflow, |
|||
} = useCanvasWorkflowStore(useShallow((state) => ({ |
|||
currentCanvasWorkflowId: state.currentCanvasWorkflowId, |
|||
currentCanvasWorkflowTitle: state.currentCanvasWorkflowTitle, |
|||
currentCanvasWorkflowDescription: state.currentCanvasWorkflowDescription, |
|||
isLoadingCanvasWorkflow: state.isLoadingCanvasWorkflow, |
|||
}))); |
|||
|
|||
const { |
|||
workflowId, |
|||
workflowName, |
|||
nodes, |
|||
edges, |
|||
edgeStyle, |
|||
groups, |
|||
hasUnsavedChanges, |
|||
markAsSaved, |
|||
} = useWorkflowStore(useShallow((state) => ({ |
|||
workflowId: state.workflowId, |
|||
workflowName: state.workflowName, |
|||
nodes: state.nodes, |
|||
edges: state.edges, |
|||
edgeStyle: state.edgeStyle, |
|||
groups: state.groups, |
|||
hasUnsavedChanges: state.hasUnsavedChanges, |
|||
markAsSaved: state.markAsSaved, |
|||
}))); |
|||
|
|||
useEffect(() => { |
|||
if (timerRef.current) { |
|||
clearTimeout(timerRef.current); |
|||
timerRef.current = null; |
|||
} |
|||
|
|||
if (!currentCanvasWorkflowId || !hasUnsavedChanges || isLoadingCanvasWorkflow) { |
|||
return; |
|||
} |
|||
|
|||
saveVersionRef.current += 1; |
|||
const scheduledVersion = saveVersionRef.current; |
|||
|
|||
timerRef.current = setTimeout(() => { |
|||
const workflow = buildSerializedWorkflow({ |
|||
workflowId, |
|||
workflowName, |
|||
nameOverride: workflowName || currentCanvasWorkflowTitle || undefined, |
|||
nodes, |
|||
edges, |
|||
edgeStyle, |
|||
groups, |
|||
}); |
|||
const title = workflowName?.trim() || currentCanvasWorkflowTitle || workflow.name; |
|||
const description = currentCanvasWorkflowDescription.trim(); |
|||
const coverUrl = pickCanvasWorkflowCoverImage(nodes); |
|||
|
|||
void updateCanvasWorkflow({ |
|||
id: currentCanvasWorkflowId, |
|||
title, |
|||
...(description ? { desp: description } : {}), |
|||
properties: JSON.stringify(workflow), |
|||
...(coverUrl ? { coverUrl } : {}), |
|||
}) |
|||
.then(() => { |
|||
if (saveVersionRef.current === scheduledVersion) { |
|||
markAsSaved(); |
|||
} |
|||
}) |
|||
.catch((error) => { |
|||
const errorMessage = error instanceof Error ? error.message : t("workflowSave.failed"); |
|||
void message.error(errorMessage); |
|||
}); |
|||
}, CANVAS_WORKFLOW_AUTO_UPDATE_DELAY_MS); |
|||
|
|||
return () => { |
|||
if (timerRef.current) { |
|||
clearTimeout(timerRef.current); |
|||
timerRef.current = null; |
|||
} |
|||
}; |
|||
}, [ |
|||
currentCanvasWorkflowId, |
|||
currentCanvasWorkflowTitle, |
|||
currentCanvasWorkflowDescription, |
|||
isLoadingCanvasWorkflow, |
|||
workflowId, |
|||
workflowName, |
|||
nodes, |
|||
edges, |
|||
edgeStyle, |
|||
groups, |
|||
hasUnsavedChanges, |
|||
markAsSaved, |
|||
message, |
|||
t, |
|||
]); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import type { WorkflowNode } from "@/types"; |
|||
|
|||
const COVER_IMAGE_KEYS = [ |
|||
"image", |
|||
"outputImage", |
|||
"previewImg", |
|||
"sourceImage", |
|||
"capturedImage", |
|||
"imageA", |
|||
"imageB", |
|||
] as const; |
|||
|
|||
function isUsableImageValue(value: unknown): value is string { |
|||
return typeof value === "string" && ( |
|||
value.startsWith("data:image/") || |
|||
value.startsWith("http://") || |
|||
value.startsWith("https://") || |
|||
value.startsWith("/api/images/") |
|||
); |
|||
} |
|||
|
|||
export function pickCanvasWorkflowCoverImage(nodes: WorkflowNode[]): string | undefined { |
|||
for (const node of nodes) { |
|||
const data = node.data as Record<string, unknown>; |
|||
for (const key of COVER_IMAGE_KEYS) { |
|||
const value = data[key]; |
|||
if (isUsableImageValue(value)) { |
|||
return value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return undefined; |
|||
} |
|||
Loading…
Reference in new issue