import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { ClipboardIcon } from "@heroicons/react/24/outline"; import { AnimatePresence, motion } from "framer-motion"; import { Suspense, useCallback, useEffect, useRef, useState } from "react"; import { SparkleListIcon } from "~/assets/icons/SparkleListIcon"; import { Button } from "~/components/primitives/Buttons"; import { StreamdownRenderer } from "~/components/code/StreamdownRenderer"; import { Header3 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Spinner } from "~/components/primitives/Spinner"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { cn } from "~/utils/cn"; type StreamEventType = | { type: "thinking"; content: string } | { type: "result"; success: true; payload: string } | { type: "result"; success: false; error: string }; export function AIPayloadTabContent({ onPayloadGenerated, payloadSchema, taskIdentifier, getCurrentPayload, generateButtonLabel = "Generate payload", placeholder, examplePromptsOverride, isAgent = false, showExamplePromptsHeader = true, }: { onPayloadGenerated: (payload: string) => void; payloadSchema?: unknown; taskIdentifier: string; getCurrentPayload?: () => string; generateButtonLabel?: string; placeholder?: string; examplePromptsOverride?: string[]; isAgent?: boolean; showExamplePromptsHeader?: boolean; }) { const [prompt, setPrompt] = useState(""); const [isLoading, setIsLoading] = useState(false); const isLoadingRef = useRef(false); const [thinking, setThinking] = useState(""); const [error, setError] = useState(null); const [showThinking, setShowThinking] = useState(false); const [lastResult, setLastResult] = useState<"success" | "error" | null>(null); const [lastPayload, setLastPayload] = useState(null); const textareaRef = useRef(null); const abortControllerRef = useRef(null); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const resourcePath = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/test/ai-generate-payload`; const submitGeneration = useCallback( async (queryPrompt: string) => { if (!queryPrompt.trim() || isLoadingRef.current) return; isLoadingRef.current = true; setIsLoading(true); setThinking(""); setError(null); setShowThinking(true); setLastResult(null); setLastPayload(null); if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); try { const formData = new FormData(); formData.append("prompt", queryPrompt); formData.append("taskIdentifier", taskIdentifier); formData.append("isAgent", isAgent ? "true" : "false"); if (payloadSchema) { formData.append("payloadSchema", JSON.stringify(payloadSchema)); } const currentPayload = getCurrentPayload?.(); if (currentPayload) { formData.append("currentPayload", currentPayload); } const response = await fetch(resourcePath, { method: "POST", body: formData, signal: abortControllerRef.current.signal, }); if (!response.ok) { const errorData = (await response.json()) as { error?: string }; setError(errorData.error || "Failed to generate payload"); setIsLoading(false); setLastResult("error"); return; } const reader = response.body?.getReader(); if (!reader) { setError("No response stream"); setIsLoading(false); setLastResult("error"); return; } const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n\n"); buffer = lines.pop() || ""; for (const line of lines) { if (line.startsWith("data: ")) { try { const event = JSON.parse(line.slice(6)) as StreamEventType; processStreamEvent(event); } catch { // Ignore parse errors } } } } if (buffer.startsWith("data: ")) { try { const event = JSON.parse(buffer.slice(6)) as StreamEventType; processStreamEvent(event); } catch { // Ignore parse errors } } } catch (err) { if (err instanceof Error && err.name === "AbortError") return; setError(err instanceof Error ? err.message : "An error occurred"); setLastResult("error"); } finally { isLoadingRef.current = false; setIsLoading(false); } }, [resourcePath, taskIdentifier, payloadSchema, getCurrentPayload, isAgent] ); const processStreamEvent = useCallback( (event: StreamEventType) => { switch (event.type) { case "thinking": setThinking((prev) => prev + event.content); break; case "result": if (event.success) { onPayloadGenerated(event.payload); setLastPayload(event.payload); setPrompt(""); setLastResult("success"); } else { setError(event.error); setLastResult("error"); } break; } }, [onPayloadGenerated] ); const handleSubmit = useCallback( (e?: React.FormEvent) => { e?.preventDefault(); submitGeneration(prompt); }, [prompt, submitGeneration] ); useEffect(() => { return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, []); useEffect(() => { if (error) { const timer = setTimeout(() => setError(null), 15000); return () => clearTimeout(timer); } }, [error]); const examplePrompts = examplePromptsOverride ?? (payloadSchema ? [ "Generate a valid payload", "Generate a payload with edge cases", "Generate a minimal payload with only required fields", ] : [ "Generate a simple JSON payload", "Generate a payload with nested objects", "Generate a payload with an array of items", ]); return (