216 lines
7.0 KiB
TypeScript
216 lines
7.0 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import { deleteWorkflowAction } from "@/app/actions/workspaces";
|
|
import { t } from "@/lib/i18n";
|
|
import type { Workflow, WorkflowStep } from "@/lib/types/http";
|
|
import { useToast } from "@/components/toast-provider";
|
|
import { SettingsSaveCancelledError, useSettingsSaveContributor } from "./settings-save-provider";
|
|
import {
|
|
areStepDraftsEqual,
|
|
createWorkflowDraftSaveProgress,
|
|
persistWorkflowDraft,
|
|
remapWorkflowDraftSteps,
|
|
} from "./workflow-card-actions";
|
|
import type { useWorkflowMutationGuard } from "./workflow-mutation-guard";
|
|
import { hasInvalidWorkflowSessionTargets } from "./workflow-session-target-validation";
|
|
|
|
const TEMP_WORKFLOW_PREFIX = "temp-workflow-";
|
|
|
|
function workflowSaveInvalidReason(
|
|
workflowName: string,
|
|
modelConfigResolutionPending: boolean,
|
|
invalidSessionTargets: boolean,
|
|
translate: (key: string) => string,
|
|
): string | undefined {
|
|
if (!workflowName.trim()) return translate("workflows:workflowNameIsRequired");
|
|
if (modelConfigResolutionPending) return translate("agents:resolvingModelOptions");
|
|
if (invalidSessionTargets) return translate("workflows:workflowSessionTargetMustBeRepaired");
|
|
return undefined;
|
|
}
|
|
|
|
type WorkflowSavedParams = {
|
|
clientWorkflow: Workflow;
|
|
submittedWorkflow: Workflow;
|
|
savedWorkflow: Workflow;
|
|
currentSteps: WorkflowStep[];
|
|
savedSteps: WorkflowStep[];
|
|
finalizeIdentity: boolean;
|
|
};
|
|
|
|
type WorkflowDraftContributorArgs = {
|
|
workflow: Workflow;
|
|
isWorkflowDirty: boolean;
|
|
workflowSteps: WorkflowStep[];
|
|
savedWorkflowSteps: WorkflowStep[];
|
|
setWorkflowSteps: Dispatch<SetStateAction<WorkflowStep[]>>;
|
|
setSavedWorkflowSteps: Dispatch<SetStateAction<WorkflowStep[]>>;
|
|
mutationGuard: ReturnType<typeof useWorkflowMutationGuard>;
|
|
toast: ReturnType<typeof useToast>["toast"];
|
|
onWorkflowSaved: (params: WorkflowSavedParams) => void;
|
|
onDiscardWorkflow: () => void;
|
|
onDeleteWorkflow: () => Promise<unknown>;
|
|
isSessionConfigResolutionPending?: boolean;
|
|
};
|
|
|
|
function useWorkflowDraftPersistence(args: WorkflowDraftContributorArgs) {
|
|
const { workflow, workflowSteps, savedWorkflowSteps } = args;
|
|
const latestStepsRef = useRef(workflowSteps);
|
|
latestStepsRef.current = workflowSteps;
|
|
const saveProgressRef = useRef(createWorkflowDraftSaveProgress());
|
|
const saveFailedRef = useRef(false);
|
|
const revision = JSON.stringify({
|
|
workflow: [
|
|
workflow.name,
|
|
workflow.description ?? "",
|
|
workflow.prompt ?? "",
|
|
workflow.agent_profile_id ?? "",
|
|
],
|
|
steps: workflowSteps,
|
|
});
|
|
const latestRevisionRef = useRef(revision);
|
|
latestRevisionRef.current = revision;
|
|
|
|
const persistSubmittedDraft = async (submittedRevision: string | number) => {
|
|
let result: Awaited<ReturnType<typeof persistWorkflowDraft>>;
|
|
try {
|
|
result = await persistWorkflowDraft({
|
|
workflow,
|
|
draftSteps: workflowSteps,
|
|
savedSteps: savedWorkflowSteps,
|
|
progress: saveProgressRef.current,
|
|
});
|
|
saveFailedRef.current = false;
|
|
} catch (error) {
|
|
saveFailedRef.current = true;
|
|
throw error;
|
|
}
|
|
const unchanged = submittedRevision === latestRevisionRef.current;
|
|
const currentSteps = unchanged
|
|
? result.steps
|
|
: remapWorkflowDraftSteps(
|
|
latestStepsRef.current,
|
|
result.workflow.id,
|
|
saveProgressRef.current.stepIds,
|
|
);
|
|
args.setSavedWorkflowSteps(result.steps);
|
|
args.setWorkflowSteps(currentSteps);
|
|
args.onWorkflowSaved({
|
|
clientWorkflow: workflow,
|
|
submittedWorkflow: workflow,
|
|
savedWorkflow: result.workflow,
|
|
currentSteps,
|
|
savedSteps: result.steps,
|
|
finalizeIdentity: unchanged,
|
|
});
|
|
};
|
|
|
|
return {
|
|
revision,
|
|
latestRevisionRef,
|
|
persistSubmittedDraft,
|
|
saveProgressRef,
|
|
saveFailedRef,
|
|
};
|
|
}
|
|
|
|
export function useWorkflowDraftContributor(args: WorkflowDraftContributorArgs) {
|
|
const {
|
|
workflow,
|
|
workflowSteps,
|
|
savedWorkflowSteps,
|
|
mutationGuard,
|
|
toast,
|
|
isSessionConfigResolutionPending = false,
|
|
} = args;
|
|
const persistence = useWorkflowDraftPersistence(args);
|
|
const removingDraftRef = useRef(false);
|
|
const [isRemovingDraft, setIsRemovingDraft] = useState(false);
|
|
const stepsDirty = !areStepDraftsEqual(workflowSteps, savedWorkflowSteps);
|
|
const invalidSessionTargets = hasInvalidWorkflowSessionTargets(workflowSteps);
|
|
|
|
useSettingsSaveContributor({
|
|
id: `workflow:${workflow.id}`,
|
|
order: 100,
|
|
revision: persistence.revision,
|
|
isDirty: args.isWorkflowDirty || stepsDirty,
|
|
canSave:
|
|
workflow.name.trim().length > 0 &&
|
|
!isSessionConfigResolutionPending &&
|
|
!invalidSessionTargets,
|
|
invalidReason: workflowSaveInvalidReason(
|
|
workflow.name,
|
|
isSessionConfigResolutionPending,
|
|
invalidSessionTargets,
|
|
t,
|
|
),
|
|
save: async (submittedRevision) => {
|
|
if (!workflow.id.startsWith(TEMP_WORKFLOW_PREFIX)) {
|
|
await persistence.persistSubmittedDraft(submittedRevision);
|
|
return;
|
|
}
|
|
let guardReturned = false;
|
|
let operationStarted = false;
|
|
await mutationGuard.guardMutation({
|
|
baselineSteps: [],
|
|
proposedSteps: workflowSteps,
|
|
intent: "create",
|
|
operation: async () => {
|
|
operationStarted = true;
|
|
try {
|
|
await persistence.persistSubmittedDraft(submittedRevision);
|
|
} catch (error) {
|
|
if (!guardReturned) throw error;
|
|
toast({
|
|
title: t("workflows:failedToSaveWorkflowChanges"),
|
|
description: error instanceof Error ? error.message : t("common:requestFailed"),
|
|
variant: "error",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
guardReturned = true;
|
|
if (!operationStarted) throw new SettingsSaveCancelledError();
|
|
},
|
|
discard: async (submittedRevision) => {
|
|
const persistedDraft = persistence.saveProgressRef.current.workflow;
|
|
if (workflow.id.startsWith(TEMP_WORKFLOW_PREFIX) && persistedDraft) {
|
|
await deleteWorkflowAction(persistedDraft.id);
|
|
} else if (persistence.saveFailedRef.current) {
|
|
throw new Error(t("workflows:retryPartialSaveBeforeLeaving"));
|
|
}
|
|
if (
|
|
submittedRevision !== undefined &&
|
|
!Object.is(submittedRevision, persistence.latestRevisionRef.current)
|
|
) {
|
|
return;
|
|
}
|
|
args.setWorkflowSteps(savedWorkflowSteps);
|
|
args.onDiscardWorkflow();
|
|
},
|
|
});
|
|
|
|
const removeDraftWorkflow = async () => {
|
|
if (removingDraftRef.current) return;
|
|
removingDraftRef.current = true;
|
|
setIsRemovingDraft(true);
|
|
try {
|
|
const persistedDraft = persistence.saveProgressRef.current.workflow;
|
|
if (workflow.id.startsWith(TEMP_WORKFLOW_PREFIX) && persistedDraft) {
|
|
await deleteWorkflowAction(persistedDraft.id);
|
|
}
|
|
await args.onDeleteWorkflow();
|
|
} finally {
|
|
removingDraftRef.current = false;
|
|
setIsRemovingDraft(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
hasUnsavedChanges: args.isWorkflowDirty || stepsDirty,
|
|
removeDraftWorkflow,
|
|
isRemovingDraft,
|
|
};
|
|
}
|