import { useState, useCallback, useEffect, useRef } from "react"; import type { AgentGenerationSpec } from "../api"; import { startAgentGeneration, generateAgentSpec, cancelAgentGeneration, } from "../api"; interface AgentGenerationModalProps { isOpen: boolean; onClose: () => void; onGenerated: (spec: AgentGenerationSpec) => void; projectId?: string; } type ViewState = | { type: "input" } | { type: "loading" } | { type: "preview"; spec: AgentGenerationSpec; sessionId: string }; const MIN_ROLE_LENGTH = 3; const MAX_ROLE_LENGTH = 1000; /** * Modal for AI-assisted agent creation. * * The user enters a role description and the system generates a complete * agent specification including title, icon, system prompt, and suggested * runtime configuration. * * Follows the same general modal pattern as PlanningModeModal but simplified * (no multi-step Q&A — single input → single generation result). */ export function AgentGenerationModal({ isOpen, onClose, onGenerated, projectId, }: AgentGenerationModalProps) { const [roleDescription, setRoleDescription] = useState(""); const [view, setView] = useState({ type: "input" }); const [error, setError] = useState(null); const [systemPromptExpanded, setSystemPromptExpanded] = useState(false); const sessionIdRef = useRef(null); const textareaRef = useRef(null); // Focus textarea on open useEffect(() => { if (isOpen && view.type === "input") { textareaRef.current?.focus(); } }, [isOpen, view.type]); // Cleanup session on unmount or modal close useEffect(() => { if (!isOpen && sessionIdRef.current) { const sid = sessionIdRef.current; sessionIdRef.current = null; cancelAgentGeneration(sid, projectId).catch(() => { /* ignore cleanup errors */ }); } }, [isOpen, projectId]); // Handle escape key useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { handleCancel(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); const handleCancel = useCallback(() => { // Cleanup session server-side if (sessionIdRef.current) { const sid = sessionIdRef.current; sessionIdRef.current = null; cancelAgentGeneration(sid, projectId).catch(() => { /* ignore cleanup errors */ }); } setRoleDescription(""); setView({ type: "input" }); setError(null); setSystemPromptExpanded(false); onClose(); }, [onClose, projectId]); const handleGenerate = useCallback(async () => { if (!roleDescription.trim() || roleDescription.trim().length < MIN_ROLE_LENGTH) return; setError(null); setView({ type: "loading" }); try { // Phase 1: Start session const { sessionId } = await startAgentGeneration(roleDescription.trim(), projectId); sessionIdRef.current = sessionId; // Phase 2: Generate spec (single combined loading state) const { spec } = await generateAgentSpec(sessionId, projectId); setView({ type: "preview", spec, sessionId }); } catch (err: unknown) { const message = err instanceof Error ? err.message : "Failed to generate agent specification"; // Handle rate limit errors with user-friendly message if (message.includes("429") || message.toLowerCase().includes("rate limit")) { setError("Too many requests. Please wait a moment and try again."); } else { setError(message); } setView({ type: "input" }); sessionIdRef.current = null; } }, [roleDescription, projectId]); const handleRegenerate = useCallback(async () => { // Cancel existing session and create a new one if (sessionIdRef.current) { const oldSid = sessionIdRef.current; sessionIdRef.current = null; try { await cancelAgentGeneration(oldSid, projectId); } catch { /* ignore */ } } // Re-run generation with same role description await handleGenerate(); }, [handleGenerate, projectId]); const handleUseSpec = useCallback(() => { if (view.type !== "preview") return; // Clear session ref so we don't cancel on close (we're using the spec) sessionIdRef.current = null; onGenerated(view.spec); // Reset and close setRoleDescription(""); setView({ type: "input" }); setError(null); setSystemPromptExpanded(false); onClose(); }, [view, onGenerated, onClose]); if (!isOpen) return null; const canGenerate = roleDescription.trim().length >= MIN_ROLE_LENGTH && roleDescription.trim().length <= MAX_ROLE_LENGTH; return (
{ if (e.target === e.currentTarget) handleCancel(); }} >
{/* Header */}
Generate Agent
{/* Body */}
{error && (
{error}
)} {view.type === "input" && (

Describe your agent's role and the AI will generate a complete specification including system prompt, suggested configuration, and more.