feat: add cancel functionality for prompt execution with request ID tracking
This commit is contained in:
parent
43abf370c7
commit
e15111a8fe
@ -48,6 +48,7 @@ const PromptExecute = (): ReactElement => {
|
||||
// Execution result
|
||||
const [result, setResult] = useState<AICompletion | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeRequestId, setActiveRequestId] = useState<string | null>(null);
|
||||
|
||||
// State & Refs for smooth typing effect
|
||||
const [displayedContent, setDisplayedContent] = useState("");
|
||||
@ -56,6 +57,7 @@ const PromptExecute = (): ReactElement => {
|
||||
const displayedContentRef = useRef("");
|
||||
const finalResultRef = useRef<AICompletion | null>(null);
|
||||
const isStreamDoneRef = useRef(false);
|
||||
const isCancelledRef = useRef(false);
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const loadData = async (): Promise<void> => {
|
||||
@ -173,6 +175,63 @@ const PromptExecute = (): ReactElement => {
|
||||
typingTimerRef.current = setInterval(tick, 15);
|
||||
};
|
||||
|
||||
const handleCancel = async (): Promise<void> => {
|
||||
if (!id || !activeRequestId) {
|
||||
showToast.error("No active request found to cancel.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If the stream is already finished and we are just catching up on typing,
|
||||
// don't hit the backend cancel endpoint since it will return NOT_FOUND.
|
||||
if (isStreamDoneRef.current) {
|
||||
showToast.info("Execution has already completed.");
|
||||
setIsExecuting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isCancelledRef.current = true;
|
||||
await aiService.cancelPromptExecution(id, activeRequestId);
|
||||
showToast.info("Cancellation requested successfully.");
|
||||
|
||||
if (typingTimerRef.current) {
|
||||
clearInterval(typingTimerRef.current);
|
||||
typingTimerRef.current = null;
|
||||
}
|
||||
setIsExecuting(false);
|
||||
const abortedText = displayedContentRef.current + "\n\n[Execution Cancelled by User]";
|
||||
setDisplayedContent(abortedText);
|
||||
displayedContentRef.current = abortedText;
|
||||
|
||||
setResult((prev) => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
id: "cancelled",
|
||||
content: abortedText,
|
||||
};
|
||||
});
|
||||
} catch (err: any) {
|
||||
// If the error indicates not found, it means the request already finished or was cancelled.
|
||||
const errStr = err.message || String(err);
|
||||
if (
|
||||
errStr.includes("No active execution") ||
|
||||
err.code === "NOT_FOUND" ||
|
||||
err.response?.status === 404 ||
|
||||
errStr.includes("404")
|
||||
) {
|
||||
showToast.info("Execution has already completed.");
|
||||
if (typingTimerRef.current) {
|
||||
clearInterval(typingTimerRef.current);
|
||||
typingTimerRef.current = null;
|
||||
}
|
||||
setIsExecuting(false);
|
||||
} else {
|
||||
showToast.error("Failed to cancel execution: " + errStr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecute = async (): Promise<void> => {
|
||||
if (!prompt || !id) return;
|
||||
|
||||
@ -196,6 +255,8 @@ const PromptExecute = (): ReactElement => {
|
||||
targetContentRef.current = "";
|
||||
finalResultRef.current = null;
|
||||
isStreamDoneRef.current = false;
|
||||
isCancelledRef.current = false;
|
||||
setActiveRequestId(null);
|
||||
|
||||
setIsExecuting(true);
|
||||
setResult({
|
||||
@ -233,10 +294,17 @@ const PromptExecute = (): ReactElement => {
|
||||
clearInterval(typingTimerRef.current);
|
||||
typingTimerRef.current = null;
|
||||
}
|
||||
// If the user cancelled the execution, we ignore the subsequent network abort/error.
|
||||
if (isCancelledRef.current) {
|
||||
return;
|
||||
}
|
||||
setError(errMessage);
|
||||
setResult(null);
|
||||
setIsExecuting(false);
|
||||
showToast.error(errMessage);
|
||||
},
|
||||
(requestId) => {
|
||||
setActiveRequestId(requestId);
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -280,12 +348,18 @@ const PromptExecute = (): ReactElement => {
|
||||
description: "Compile template variables and execute the prompt.",
|
||||
action: (
|
||||
<div className="flex gap-2">
|
||||
<SecondaryButton onClick={() => navigate("/tenant/ai/prompts")}>
|
||||
<SecondaryButton onClick={() => navigate("/tenant/ai/prompts")} disabled={isExecuting}>
|
||||
Back to Prompts
|
||||
</SecondaryButton>
|
||||
<PrimaryButton onClick={handleExecute} disabled={isExecuting}>
|
||||
{isExecuting ? "Executing..." : "Execute Prompt"}
|
||||
</PrimaryButton>
|
||||
{isExecuting ? (
|
||||
<PrimaryButton onClick={handleCancel} className="bg-red-600 hover:bg-red-700 border-red-600 text-white">
|
||||
Cancel
|
||||
</PrimaryButton>
|
||||
) : (
|
||||
<PrimaryButton onClick={handleExecute}>
|
||||
Execute Prompt
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
@ -352,10 +426,19 @@ const PromptExecute = (): ReactElement => {
|
||||
{result.provider} • {result.model}
|
||||
</StatusBadge>
|
||||
{result.id === "streaming" ? (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-[#E6FFFB] text-[#08979C] border border-[#B5F5EC] animate-pulse">
|
||||
<span className="w-2 h-2 rounded-full bg-[#13C2C2] mr-1.5 animate-ping inline-block" />
|
||||
Streaming response...
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-[#E6FFFB] text-[#08979C] border border-[#B5F5EC] animate-pulse">
|
||||
<span className="w-2 h-2 rounded-full bg-[#13C2C2] mr-1.5 animate-ping inline-block" />
|
||||
Streaming response...
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-red-50 text-red-600 border border-red-200 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<StatusBadge variant="success">
|
||||
@ -370,6 +453,12 @@ const PromptExecute = (): ReactElement => {
|
||||
<Activity className="w-3.5 h-3.5 mr-1.5 inline" />
|
||||
{result.usage?.total_tokens ?? 0} tokens
|
||||
</StatusBadge>
|
||||
{(result.requestId || result.request_id) && (
|
||||
<StatusBadge variant="process">
|
||||
<Tag className="w-3.5 h-3.5 mr-1.5 inline" />
|
||||
ID: {result.requestId || result.request_id}
|
||||
</StatusBadge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -253,10 +253,11 @@ class AIService {
|
||||
|
||||
async executePromptStream(
|
||||
id: string,
|
||||
payload: { variables?: Record<string, unknown>; provider?: string; model?: string; temperature?: number; max_tokens?: number },
|
||||
payload: { variables?: Record<string, unknown>; provider?: string; model?: string; temperature?: number; max_tokens?: number; clientRequestId?: string },
|
||||
onChunk: (chunk: string) => void,
|
||||
onDone: (result: AICompletion) => void,
|
||||
onError: (error: string) => void
|
||||
onError: (error: string) => void,
|
||||
onRequestId?: (requestId: string) => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
const store = (window as any).__REDUX_STORE__;
|
||||
@ -307,6 +308,10 @@ class AIService {
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (data.clientRequestId && onRequestId) {
|
||||
console.log("[aiService] Parsed clientRequestId from stream:", data.clientRequestId);
|
||||
onRequestId(data.clientRequestId);
|
||||
}
|
||||
if (data.content) {
|
||||
onChunk(data.content);
|
||||
}
|
||||
@ -324,6 +329,12 @@ class AIService {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelPromptExecution(id: string, requestId: string): Promise<{ success: boolean; message?: string }> {
|
||||
console.log("[aiService] cancelPromptExecution called for request ID:", requestId);
|
||||
const response = await apiClient.post(`/ai/prompts/${id}/cancel`, { request_id: requestId });
|
||||
return unwrap(response);
|
||||
}
|
||||
|
||||
async testPrompt(
|
||||
id: string,
|
||||
payload: { variables?: Record<string, unknown>; provider?: string; model?: string; temperature?: number; max_tokens?: number },
|
||||
|
||||
@ -63,6 +63,8 @@ export interface AICompletion {
|
||||
completed_at?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
requestId?: string;
|
||||
request_id?: string;
|
||||
}
|
||||
|
||||
export interface AICompletionListResponse {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user