import { useState, useEffect, useCallback } from "react"; import type { AgentCapability, ModelInfo, AgentGenerationSpec } from "../api"; import { createAgent, fetchModels } from "../api"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { ProviderIcon } from "./ProviderIcon"; import { AgentGenerationModal } from "./AgentGenerationModal"; export interface NewAgentDialogProps { isOpen: boolean; onClose: () => void; onCreated: () => void; projectId?: string; } const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [ { value: "triage", label: "Triage", icon: "πŸ”" }, { value: "executor", label: "Executor", icon: "⚑" }, { value: "reviewer", label: "Reviewer", icon: "πŸ‘" }, { value: "merger", label: "Merger", icon: "πŸ”€" }, { value: "scheduler", label: "Scheduler", icon: "⏰" }, { value: "engineer", label: "Engineer", icon: "πŸ› " }, { value: "custom", label: "Custom", icon: "πŸ”§" }, ]; type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high"; /** Set of valid AgentCapability values for mapping generated roles */ const VALID_CAPABILITIES = new Set(["triage", "executor", "reviewer", "merger", "scheduler", "engineer", "custom"]); interface RuntimeConfig { model: string; thinkingLevel: ThinkingLevel; maxTurns: number; } /** Preset agent template for one-click creation */ interface AgentPreset { /** Unique identifier for the preset */ id: string; /** Display name (e.g., "CEO", "CTO") */ name: string; /** Emoji icon */ icon: string; /** Professional title (e.g., "Chief Executive Officer") */ title: string; /** Agent capability role */ role: AgentCapability; /** Optional description of the agent's responsibilities */ description?: string; } const AGENT_PRESETS: AgentPreset[] = [ { id: "ceo", name: "CEO", icon: "πŸ‘”", title: "Chief Executive Officer", role: "custom", description: "Oversees project strategy, sets priorities, and coordinates between departments to ensure alignment with business goals." }, { id: "cto", name: "CTO", icon: "🧠", title: "Chief Technology Officer", role: "custom", description: "Defines technical architecture, evaluates technology choices, and guides engineering standards across the project." }, { id: "cmo", name: "CMO", icon: "πŸ“’", title: "Chief Marketing Officer", role: "custom", description: "Drives product positioning, audience engagement strategy, and content planning to grow user adoption." }, { id: "cfo", name: "CFO", icon: "πŸ’°", title: "Chief Financial Officer", role: "custom", description: "Manages budget allocation, cost optimization, and financial planning to maximize resource efficiency." }, { id: "engineer", name: "Engineer", icon: "πŸ‘¨β€πŸ’»", title: "Software Engineer", role: "engineer", description: "Implements features, fixes bugs, and writes well-tested code across the full application stack." }, { id: "backend-engineer", name: "Backend Engineer", icon: "βš™οΈ", title: "Backend Engineer", role: "engineer", description: "Builds and maintains server-side logic, APIs, database schemas, and background processing pipelines." }, { id: "frontend-engineer", name: "Frontend Engineer", icon: "🎨", title: "Frontend Engineer", role: "engineer", description: "Develops user interfaces, manages component libraries, and ensures responsive, accessible UI experiences." }, { id: "fullstack-engineer", name: "Fullstack Engineer", icon: "πŸš€", title: "Full Stack Engineer", role: "engineer", description: "Works across frontend and backend to deliver end-to-end features from database to user interface." }, { id: "qa-engineer", name: "QA Engineer", icon: "πŸ§ͺ", title: "Quality Assurance Engineer", role: "engineer", description: "Designs test plans, writes automated tests, and validates that features meet acceptance criteria before release." }, { id: "devops-engineer", name: "DevOps Engineer", icon: "πŸ”§", title: "DevOps Engineer", role: "engineer", description: "Manages infrastructure, deployment pipelines, and monitoring to ensure reliable and scalable service delivery." }, { id: "ci-engineer", name: "CI Engineer", icon: "⚑", title: "CI/CD Engineer", role: "engineer", description: "Builds and optimizes continuous integration and delivery pipelines for fast, reliable release cycles." }, { id: "security-engineer", name: "Security Engineer", icon: "πŸ›‘οΈ", title: "Security Engineer", role: "engineer", description: "Identifies vulnerabilities, enforces security best practices, and conducts audits to protect application integrity." }, { id: "data-engineer", name: "Data Engineer", icon: "πŸ“Š", title: "Data Engineer", role: "engineer", description: "Designs data pipelines, manages storage infrastructure, and ensures reliable data flow for analytics and features." }, { id: "ml-engineer", name: "ML Engineer", icon: "πŸ€–", title: "Machine Learning Engineer", role: "engineer", description: "Builds, trains, and deploys machine learning models, and integrates AI capabilities into the product." }, { id: "product-manager", name: "Product Manager", icon: "πŸ“‹", title: "Product Manager", role: "custom", description: "Defines product requirements, prioritizes the backlog, and coordinates cross-functional delivery from concept to launch." }, { id: "designer", name: "Designer", icon: "✏️", title: "Product Designer", role: "custom", description: "Creates wireframes, prototypes, and design systems that balance usability, aesthetics, and brand consistency." }, { id: "marketing-manager", name: "Marketing Manager", icon: "πŸ“£", title: "Marketing Manager", role: "custom", description: "Plans campaigns, manages content channels, and analyzes market data to drive brand awareness and growth." }, { id: "technical-writer", name: "Technical Writer", icon: "πŸ“", title: "Technical Writer", role: "custom", description: "Writes and maintains documentation, API references, and guides that help users and developers succeed." }, { id: "triage", name: "Triage Agent", icon: "πŸ”", title: "Task Triage Agent", role: "triage", description: "Analyzes incoming tasks, generates detailed specifications, and prepares PROMPT.md files for execution." }, { id: "reviewer", name: "Reviewer", icon: "πŸ‘οΈ", title: "Code Reviewer", role: "reviewer", description: "Reviews code changes for correctness, security, performance, and adherence to project coding standards." }, ]; export function NewAgentDialog({ isOpen, onClose, onCreated, projectId }: NewAgentDialogProps) { const [step, setStep] = useState(0); const [name, setName] = useState(""); const [title, setTitle] = useState(""); const [icon, setIcon] = useState(""); const [role, setRole] = useState("custom"); const [reportsTo, setReportsTo] = useState(""); const [instructionsPath, setInstructionsPath] = useState(""); const [instructionsText, setInstructionsText] = useState(""); const [runtimeConfig, setRuntimeConfig] = useState({ model: "", thinkingLevel: "off", maxTurns: 10, }); const [selectedPresetId, setSelectedPresetId] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const [isGenerationModalOpen, setIsGenerationModalOpen] = useState(false); // Model dropdown state const [availableModels, setAvailableModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [favoriteProviders, setFavoriteProviders] = useState([]); const [favoriteModels, setFavoriteModels] = useState([]); // Load models on mount (global data, not per-agent) useEffect(() => { setModelsLoading(true); fetchModels() .then((response) => { setAvailableModels(response.models); setFavoriteProviders(response.favoriteProviders); setFavoriteModels(response.favoriteModels); }) .catch(() => { // Gracefully handle β€” dropdown will show empty list }) .finally(() => setModelsLoading(false)); }, []); // Selected model in "provider/modelId" format, or "" for default const selectedModel = runtimeConfig.model.includes("/") ? runtimeConfig.model : ""; const handleGenerated = useCallback((spec: AgentGenerationSpec) => { // Map generated role to AgentCapability, default to "custom" if unrecognized const mappedRole = VALID_CAPABILITIES.has(spec.role) ? (spec.role as AgentCapability) : "custom"; setName(spec.title); setTitle(spec.description); setIcon(spec.icon); setRole(mappedRole); setRuntimeConfig(c => ({ ...c, thinkingLevel: spec.thinkingLevel, maxTurns: spec.maxTurns, })); setIsGenerationModalOpen(false); // Advance to Step 1 so user can review model selection setStep(1); }, []); const handleModelChange = useCallback((value: string) => { // value is "provider/modelId" or "" for default setRuntimeConfig(c => ({ ...c, model: value })); }, []); const handleToggleFavorite = useCallback(async (provider: string) => { const currentFavorites = favoriteProviders; const isFavorite = currentFavorites.includes(provider); const newFavorites = isFavorite ? currentFavorites.filter(p => p !== provider) : [provider, ...currentFavorites]; setFavoriteProviders(newFavorites); }, [favoriteProviders]); const handleToggleModelFavorite = useCallback(async (modelId: string) => { const currentFavorites = favoriteModels; const isFavorite = currentFavorites.includes(modelId); const newFavorites = isFavorite ? currentFavorites.filter(m => m !== modelId) : [modelId, ...currentFavorites]; setFavoriteModels(newFavorites); }, [favoriteModels]); const handlePresetSelect = useCallback((preset: AgentPreset) => { setSelectedPresetId(preset.id); setName(preset.name); setIcon(preset.icon); setTitle(preset.description ?? preset.title); setRole(preset.role); // Advance to Step 1 so user can review model selection setStep(1); }, []); if (!isOpen) return null; const handleClose = () => { setStep(0); setName(""); setTitle(""); setIcon(""); setRole("custom"); setReportsTo(""); setInstructionsPath(""); setInstructionsText(""); setRuntimeConfig({ model: "", thinkingLevel: "off", maxTurns: 10 }); setSelectedPresetId(null); setError(null); setIsGenerationModalOpen(false); onClose(); }; const handleCreate = async () => { if (!name.trim()) return; setIsSubmitting(true); setError(null); try { const runtimeCfg: Record = {}; if (runtimeConfig.model.trim()) runtimeCfg.model = runtimeConfig.model.trim(); if (runtimeConfig.thinkingLevel !== "off") runtimeCfg.thinkingLevel = runtimeConfig.thinkingLevel; if (runtimeConfig.maxTurns !== 10) runtimeCfg.maxTurns = runtimeConfig.maxTurns; await createAgent({ name: name.trim(), role, ...(title.trim() ? { title: title.trim() } : {}), ...(icon.trim() ? { icon: icon.trim() } : {}), ...(reportsTo.trim() ? { reportsTo: reportsTo.trim() } : {}), ...(instructionsPath.trim() ? { instructionsPath: instructionsPath.trim() } : {}), ...(instructionsText.trim() ? { instructionsText: instructionsText.trim() } : {}), ...(Object.keys(runtimeCfg).length > 0 ? { runtimeConfig: runtimeCfg } : {}), }, projectId); handleClose(); onCreated(); } catch (err: unknown) { setError(err instanceof Error ? err.message : "Failed to create agent"); } finally { setIsSubmitting(false); } }; const selectedRole = AGENT_ROLES.find(r => r.value === role); return (
{ if (e.target === e.currentTarget) handleClose(); }}>
{/* Header */}
New Agent
{/* Step indicator */}
{[0, 1, 2].map(i => (
))}
{/* Body */}
{step === 0 && (
{/* Quick Start Presets */}
Choose a preset or fill in details manually
{AGENT_PRESETS.map(preset => ( ))}
setName(e.target.value)} autoFocus />
setTitle(e.target.value)} />
{AGENT_ROLES.map(r => ( ))}
setReportsTo(e.target.value)} />
setInstructionsPath(e.target.value)} />