"use client"; import { useState, useEffect, useRef, useCallback, useId } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { api } from "@/lib/api"; interface WorkspaceOption { id: string; name: string; tier: number; } interface HermesProvider { id: string; label: string; envVar: string; } // All providers supported by Hermes runtime via providers.resolve_provider() export const HERMES_PROVIDERS: HermesProvider[] = [ { id: "anthropic", label: "Anthropic (Claude)", envVar: "ANTHROPIC_API_KEY" }, { id: "openai", label: "OpenAI", envVar: "OPENAI_API_KEY" }, { id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" }, { id: "xai", label: "xAI (Grok)", envVar: "XAI_API_KEY" }, { id: "gemini", label: "Google Gemini", envVar: "GEMINI_API_KEY" }, { id: "qwen", label: "Qwen (Alibaba)", envVar: "QWEN_API_KEY" }, { id: "glm", label: "GLM (Zhipu AI)", envVar: "GLM_API_KEY" }, { id: "kimi", label: "Kimi (Moonshot)", envVar: "KIMI_API_KEY" }, { id: "minimax", label: "MiniMax", envVar: "MINIMAX_API_KEY" }, { id: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY" }, { id: "groq", label: "Groq", envVar: "GROQ_API_KEY" }, { id: "mistral", label: "Mistral", envVar: "MISTRAL_API_KEY" }, { id: "together", label: "Together AI", envVar: "TOGETHER_API_KEY" }, { id: "fireworks", label: "Fireworks AI", envVar: "FIREWORKS_API_KEY" }, { id: "hermes", label: "Hermes / Nous (legacy)", envVar: "HERMES_API_KEY" }, ]; export function CreateWorkspaceButton() { const [open, setOpen] = useState(false); const [name, setName] = useState(""); const [role, setRole] = useState(""); const [tier, setTier] = useState(1); const [template, setTemplate] = useState(""); const [parentId, setParentId] = useState(""); const [budgetLimit, setBudgetLimit] = useState(""); const [creating, setCreating] = useState(false); const [error, setError] = useState(null); const [workspaces, setWorkspaces] = useState([]); // Hermes-specific state const [hermesProvider, setHermesProvider] = useState("anthropic"); const [hermesApiKey, setHermesApiKey] = useState(""); // Refs for roving tabIndex on the tier radio group (WCAG 2.1 arrow-key nav) const radioRefs = useRef>([]); const TIERS = [ { value: 1, label: "T1", desc: "Sandboxed" }, { value: 2, label: "T2", desc: "Standard" }, { value: 3, label: "T3", desc: "Full Access" }, ]; const handleRadioKeyDown = useCallback( (e: React.KeyboardEvent, currentIndex: number) => { if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); const next = (currentIndex + 1) % TIERS.length; setTier(TIERS[next].value); radioRefs.current[next]?.focus(); } else if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); const prev = (currentIndex - 1 + TIERS.length) % TIERS.length; setTier(TIERS[prev].value); radioRefs.current[prev]?.focus(); } }, // TIERS is stable (module-level constant pattern), setTier is stable from useState // eslint-disable-next-line react-hooks/exhaustive-deps [] ); const isHermes = template.trim().toLowerCase() === "hermes"; // Reset form and load workspaces whenever dialog opens useEffect(() => { if (!open) return; setName(""); setRole(""); setTier(1); setTemplate(""); setParentId(""); setBudgetLimit(""); setError(null); setHermesProvider("anthropic"); setHermesApiKey(""); api .get("/workspaces") .then((ws) => setWorkspaces(ws)) .catch(() => {}); }, [open]); const handleCreate = async () => { if (!name.trim()) { setError("Name is required"); return; } if (isHermes && !hermesApiKey.trim()) { setError("API key is required for Hermes workspaces"); return; } setCreating(true); setError(null); const provider = isHermes ? HERMES_PROVIDERS.find((p) => p.id === hermesProvider) : undefined; try { const parsedBudget = budgetLimit.trim() ? parseFloat(budgetLimit) : null; await api.post("/workspaces", { name: name.trim(), role: role.trim() || undefined, template: template.trim() || undefined, tier, parent_id: parentId || undefined, budget_limit: parsedBudget, canvas: { x: Math.random() * 400 + 100, y: Math.random() * 300 + 100 }, ...(isHermes && provider ? { secrets: { [provider.envVar]: hermesApiKey.trim() } } : {}), }); setOpen(false); } catch (e) { setError(e instanceof Error ? e.message : "Failed to create workspace"); } finally { setCreating(false); } }; return ( Create Workspace

Add a new workspace node to the canvas

Tier
{TIERS.map((t, idx) => ( ))}
{/* Hermes provider configuration — shown only when template === "hermes" */} {isHermes && (

Hermes Provider

Choose the AI provider and paste your API key. The key is stored as an encrypted workspace secret.

setHermesApiKey(e.target.value)} placeholder="sk-…" aria-label="Hermes API key" autoComplete="off" className="w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/20 transition-colors font-mono" />
)} {error && (
{error}
)}
); } function InputField({ label, value, onChange, placeholder, required, mono, type = "text", helper, }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string; required?: boolean; mono?: boolean; type?: string; helper?: string; }) { // useId() generates a stable, unique ID for the label↔input association, // satisfying WCAG 2.1 SC 1.3.1 (Info and Relationships, Level A). const inputId = useId(); return (
onChange(e.target.value)} placeholder={placeholder} min={type === "number" ? "0" : undefined} step={type === "number" ? "0.01" : undefined} className={`w-full bg-zinc-800/60 border border-zinc-700/50 rounded-lg px-3 py-2 text-sm text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-blue-500/60 focus:ring-1 focus:ring-blue-500/20 transition-colors ${mono ? "font-mono text-xs" : ""}`} /> {helper && (

{helper}

)}
); }