rdesign/frontend/node_modules/@copilotkitnext/react/dist/hooks/use-interrupt.mjs

170 lines
5.1 KiB
JavaScript

import { useCopilotKit } from "../providers/CopilotKitProvider.mjs";
import { useAgent } from "./use-agent.mjs";
import { useCallback, useEffect, useMemo, useState } from "react";
//#region src/hooks/use-interrupt.tsx
const INTERRUPT_EVENT_NAME = "on_interrupt";
function isPromiseLike(value) {
return (typeof value === "object" || typeof value === "function") && value !== null && typeof Reflect.get(value, "then") === "function";
}
/**
* Handles agent interrupts (`on_interrupt`) with optional filtering, preprocessing, and resume behavior.
*
* The hook listens to custom events on the active agent, stores interrupt payloads per run,
* and surfaces a render callback once the run finalizes. Call `resolve` from your UI to resume
* execution with user-provided data.
*
* - `renderInChat: true` (default): the element is published into `<CopilotChat>` and this hook returns `void`.
* - `renderInChat: false`: the hook returns the interrupt element so you can place it anywhere in your component tree.
*
* `event.value` is typed as `any` since the interrupt payload shape depends on your agent.
* Type-narrow it in your callbacks (e.g. `handler`, `enabled`, `render`) as needed.
*
* @typeParam TResult - Inferred from `handler` return type. Exposed as `result` in `render`.
* @param config - Interrupt configuration (renderer, optional handler/filter, and render mode).
* @returns When `renderInChat` is `false`, returns the interrupt element (or `null` when idle).
* Otherwise returns `void` and publishes the element into chat. In `render`, `result` is always
* either the handler's resolved return value or `null` (including when no handler is provided,
* when filtering skips the interrupt, or when handler execution fails).
*
* @example
* ```tsx
* import { useInterrupt } from "@copilotkitnext/react";
*
* function InterruptUI() {
* useInterrupt({
* render: ({ event, resolve }) => (
* <div>
* <p>{event.value.question}</p>
* <button onClick={() => resolve({ approved: true })}>Approve</button>
* <button onClick={() => resolve({ approved: false })}>Reject</button>
* </div>
* ),
* });
*
* return null;
* }
* ```
*
* @example
* ```tsx
* import { useInterrupt } from "@copilotkitnext/react";
*
* function CustomPanel() {
* const interruptElement = useInterrupt({
* renderInChat: false,
* enabled: (event) => event.value.startsWith("approval:"),
* handler: async ({ event }) => ({ label: event.value.toUpperCase() }),
* render: ({ event, result, resolve }) => (
* <aside>
* <strong>{result?.label ?? ""}</strong>
* <button onClick={() => resolve({ value: event.value })}>Continue</button>
* </aside>
* ),
* });
*
* return <>{interruptElement}</>;
* }
* ```
*/
function useInterrupt(config) {
const { copilotkit } = useCopilotKit();
const { agent } = useAgent({ agentId: config.agentId });
const [pendingEvent, setPendingEvent] = useState(null);
const [handlerResult, setHandlerResult] = useState(null);
useEffect(() => {
let localInterrupt = null;
const subscription = agent.subscribe({
onCustomEvent: ({ event }) => {
if (event.name === INTERRUPT_EVENT_NAME) localInterrupt = {
name: event.name,
value: event.value
};
},
onRunStartedEvent: () => {
localInterrupt = null;
setPendingEvent(null);
},
onRunFinalized: () => {
if (localInterrupt) {
setPendingEvent(localInterrupt);
localInterrupt = null;
}
},
onRunFailed: () => {
localInterrupt = null;
}
});
return () => subscription.unsubscribe();
}, [agent]);
const resolve = useCallback((response) => {
setPendingEvent(null);
copilotkit.runAgent({
agent,
forwardedProps: { command: { resume: response } }
});
}, [agent, copilotkit]);
useEffect(() => {
if (!pendingEvent) {
setHandlerResult(null);
return;
}
if (config.enabled && !config.enabled(pendingEvent)) {
setHandlerResult(null);
return;
}
const handler = config.handler;
if (!handler) {
setHandlerResult(null);
return;
}
let cancelled = false;
const maybePromise = handler({
event: pendingEvent,
resolve
});
if (isPromiseLike(maybePromise)) Promise.resolve(maybePromise).then((resolved) => {
if (!cancelled) setHandlerResult(resolved);
}).catch(() => {
if (!cancelled) setHandlerResult(null);
});
else setHandlerResult(maybePromise);
return () => {
cancelled = true;
};
}, [
pendingEvent,
config.enabled,
config.handler,
resolve
]);
const element = useMemo(() => {
if (!pendingEvent) return null;
if (config.enabled && !config.enabled(pendingEvent)) return null;
return config.render({
event: pendingEvent,
result: handlerResult,
resolve
});
}, [
pendingEvent,
handlerResult,
config.enabled,
config.render,
resolve
]);
useEffect(() => {
if (config.renderInChat === false) return;
copilotkit.setInterruptElement(element);
return () => copilotkit.setInterruptElement(null);
}, [
element,
config.renderInChat,
copilotkit
]);
if (config.renderInChat === false) return element;
}
//#endregion
export { useInterrupt };
//# sourceMappingURL=use-interrupt.mjs.map