Files
zk-data-agent/frontend/app/src/components/thread/agent-inbox/hooks/use-interrupted-actions.tsx
T
2026-05-06 12:10:22 +08:00

241 lines
6.4 KiB
TypeScript

import { useStreamContext } from "@/providers/Stream";
import { END } from "@langchain/langgraph/web";
import { Interrupt } from "@langchain/langgraph-sdk";
import { toast } from "sonner";
import {
Dispatch,
KeyboardEvent,
MutableRefObject,
SetStateAction,
useEffect,
useRef,
useState,
} from "react";
import { Decision, DecisionWithEdits, HITLRequest, SubmitType } from "../types";
import { buildDecisionFromState, createDefaultHumanResponse } from "../utils";
interface UseInterruptedActionsInput {
interrupt: Interrupt<HITLRequest>;
}
interface UseInterruptedActionsValue {
handleSubmit: (
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | KeyboardEvent,
) => Promise<void>;
handleResolve: (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => Promise<void>;
streaming: boolean;
streamFinished: boolean;
loading: boolean;
supportsMultipleMethods: boolean;
hasEdited: boolean;
hasAddedResponse: boolean;
approveAllowed: boolean;
humanResponse: DecisionWithEdits[];
selectedSubmitType: SubmitType | undefined;
setSelectedSubmitType: Dispatch<SetStateAction<SubmitType | undefined>>;
setHumanResponse: Dispatch<SetStateAction<DecisionWithEdits[]>>;
setHasAddedResponse: Dispatch<SetStateAction<boolean>>;
setHasEdited: Dispatch<SetStateAction<boolean>>;
initialHumanInterruptEditValue: MutableRefObject<Record<string, string>>;
}
export default function useInterruptedActions({
interrupt,
}: UseInterruptedActionsInput): UseInterruptedActionsValue {
const thread = useStreamContext();
const [humanResponse, setHumanResponse] = useState<DecisionWithEdits[]>([]);
const [loading, setLoading] = useState(false);
const [streaming, setStreaming] = useState(false);
const [streamFinished, setStreamFinished] = useState(false);
const [selectedSubmitType, setSelectedSubmitType] = useState<SubmitType>();
const [hasEdited, setHasEdited] = useState(false);
const [hasAddedResponse, setHasAddedResponse] = useState(false);
const [approveAllowed, setApproveAllowed] = useState(false);
const initialHumanInterruptEditValue = useRef<Record<string, string>>({});
useEffect(() => {
const hitlValue = interrupt.value as HITLRequest | undefined;
initialHumanInterruptEditValue.current = {};
if (!hitlValue) {
setHumanResponse([]);
setSelectedSubmitType(undefined);
setApproveAllowed(false);
setHasEdited(false);
setHasAddedResponse(false);
return;
}
try {
const { responses, defaultSubmitType, hasApprove } =
createDefaultHumanResponse(hitlValue, initialHumanInterruptEditValue);
setHumanResponse(responses);
setSelectedSubmitType(defaultSubmitType);
setApproveAllowed(hasApprove);
setHasEdited(false);
setHasAddedResponse(false);
} catch (error) {
console.error("Error formatting and setting human response state", error);
setHumanResponse([]);
setSelectedSubmitType(undefined);
setApproveAllowed(false);
}
}, [interrupt]);
const resumeRun = (decisions: Decision[]): boolean => {
try {
thread.submit(
{},
{
command: {
resume: {
decisions,
},
},
},
);
return true;
} catch (error) {
console.error("Error sending human response", error);
return false;
}
};
const handleSubmit = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent> | KeyboardEvent,
) => {
e.preventDefault();
const { decision, error } = buildDecisionFromState(
humanResponse,
selectedSubmitType,
);
if (!decision) {
toast.error("Error", {
description: error ?? "Unsupported response type.",
duration: 5000,
richColors: true,
closeButton: true,
});
return;
}
if (error) {
toast.error("Error", {
description: error,
duration: 5000,
richColors: true,
closeButton: true,
});
return;
}
let errorOccurred = false;
initialHumanInterruptEditValue.current = {};
try {
setLoading(true);
setStreaming(true);
const resumedSuccessfully = resumeRun([decision]);
if (!resumedSuccessfully) {
errorOccurred = true;
return;
}
toast("Success", {
description: "Response submitted successfully.",
duration: 5000,
});
setStreamFinished(true);
} catch (error: any) {
console.error("Error sending human response", error);
errorOccurred = true;
if ("message" in error && error.message.includes("Invalid assistant")) {
toast("Error: Invalid assistant ID", {
description:
"The provided assistant ID was not found in this graph. Please update the assistant ID in the settings and try again.",
richColors: true,
closeButton: true,
duration: 5000,
});
} else {
toast.error("Error", {
description: "Failed to submit response.",
richColors: true,
closeButton: true,
duration: 5000,
});
}
} finally {
setStreaming(false);
setLoading(false);
if (errorOccurred) {
setStreamFinished(false);
}
}
};
const handleResolve = async (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => {
e.preventDefault();
setLoading(true);
initialHumanInterruptEditValue.current = {};
try {
thread.submit(
{},
{
command: {
goto: END,
},
},
);
toast("Success", {
description: "Marked thread as resolved.",
duration: 3000,
});
} catch (error) {
console.error("Error marking thread as resolved", error);
toast.error("Error", {
description: "Failed to mark thread as resolved.",
richColors: true,
closeButton: true,
duration: 3000,
});
} finally {
setLoading(false);
}
};
const supportsMultipleMethods =
humanResponse.filter((response) =>
["edit", "approve", "reject"].includes(response.type),
).length > 1;
return {
handleSubmit,
handleResolve,
humanResponse,
selectedSubmitType,
streaming,
streamFinished,
loading,
supportsMultipleMethods,
hasEdited,
hasAddedResponse,
approveAllowed,
setSelectedSubmitType,
setHumanResponse,
setHasAddedResponse,
setHasEdited,
initialHumanInterruptEditValue,
};
}