import React, { useState, useEffect, useRef, useMemo } from 'react'; import { resolveFileUrl } from './resolveFileUrl'; import { FolderKanban, Plus, BookOpen, Briefcase, StickyNote, Apple, X, UploadCloud, FileText, Sparkles, ChevronRight, Settings, Image as ImageIcon, Search, ArrowRight, MessageSquare, Film, Trash2, Edit, Bot, User, Download, } from 'lucide-react'; // Phase 7: enriched catalog hook + connections panel (additive imports) import { useAgenticCatalog } from '../agentic/useAgenticCatalog'; import { ConnectionsPanel } from '../agentic/ConnectionsPanel'; import { AgentSettingsPanel } from './components/AgentSettingsPanel'; import { PersonaSettingsPanel } from './components/PersonaSettingsPanel'; import { PersonaWizard } from './PersonaWizard'; import { PersonaImportModal, PersonaExportButton } from './PersonaImportExport'; import { CommunityGallery } from './CommunityGallery'; import { ToolsTab } from './tools'; import { McpServersTab } from './mcp'; // --- Components --- const ProjectCard = ({ icon: Icon, iconColor, title, type, description, onClick, onDelete, onEdit, onExport, isExample, avatarUrl, }: { icon: React.ElementType iconColor: string title: string type: string description: string onClick: () => void onDelete?: () => void onEdit?: () => void onExport?: React.ReactNode isExample?: boolean avatarUrl?: string | null }) => (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick() } }} tabIndex={0} role="button" className="flex flex-col gap-3 p-5 rounded-2xl bg-white/5 hover:bg-white/10 transition-all duration-200 cursor-pointer border border-white/10 hover:border-white/20 h-full focus:outline-none focus:ring-2 focus:ring-[var(--hp-focus-ring)] focus:ring-offset-2 focus:ring-offset-black" > {/* Header */}
{avatarUrl ? (
{ (e.target as HTMLImageElement).style.display = 'none' }} />
) : (
)}

{title}

{type ? ( {type} ) : null}
{/* Description */}

{description}

{/* Actions row (below content, no overlap with badge) */} {!isExample && (onDelete || onEdit || onExport) ? (
{onExport} {onEdit ? ( ) : null} {onDelete ? ( ) : null}
) : null}
); const TabButton = ({ active, label, onClick }: { active: boolean label: string onClick: () => void }) => ( ); const FileUploadItem = ({ name, size, onRemove }: { name: string size: string onRemove?: () => void }) => (
{name} {size}
{onRemove && ( )}
); type AgentCapability = { id: string; label: string; description?: string } const ProjectWizard = ({ onClose, onSave, backendUrl, apiKey, onOpenPersonaWizard, }: { onClose: () => void onSave: (data: any) => void backendUrl: string apiKey?: string onOpenPersonaWizard?: () => void }) => { const [step, setStep] = useState(1) const [files, setFiles] = useState>([]) const [projectName, setProjectName] = useState('') const [description, setDescription] = useState('') const [instructions, setInstructions] = useState('') const [projectType, setProjectType] = useState<'chat' | 'image' | 'video' | 'agent'>('chat') const fileInputRef = useRef(null) // --- Agent project settings (additive only) --- const [agentGoal, setAgentGoal] = useState('') const [agentCapabilities, setAgentCapabilities] = useState([]) const [agentAskBeforeActing, setAgentAskBeforeActing] = useState(true) const [agentExecutionProfile, setAgentExecutionProfile] = useState<'fast' | 'balanced' | 'quality'>('fast') // Dynamic capabilities from backend const [availableCapabilities, setAvailableCapabilities] = useState([]) const [capabilitiesLoaded, setCapabilitiesLoaded] = useState(false) // Tool/agent selection state const [selectedToolIds, setSelectedToolIds] = useState([]) const [selectedA2AAgentIds, setSelectedA2AAgentIds] = useState([]) // Phase 7: enriched catalog with virtual servers + tool bundle selection const [toolSource, setToolSource] = useState('all') const enrichedCatalog = useAgenticCatalog({ backendUrl, apiKey, enabled: projectType === 'agent', }) // Derive catalog tools/agents/gateways from the enriched catalog (single source of truth) const catalogTools = useMemo((): Array<{ id: string; name: string; description?: string; enabled?: boolean }> => { const tools = enrichedCatalog.catalog?.tools if (!tools) return [] return tools .filter((t) => t.id && t.name) .map((t) => ({ id: t.id, name: t.name, description: t.description || undefined, enabled: t.enabled ?? undefined })) }, [enrichedCatalog.catalog?.tools]) const catalogAgents = useMemo((): Array<{ id: string; name: string; description?: string; enabled?: boolean }> => { const agents = enrichedCatalog.catalog?.a2a_agents if (!agents) return [] return agents .filter((a) => a.id && a.name) .map((a) => ({ id: a.id, name: a.name, description: a.description || undefined, enabled: a.enabled ?? undefined })) }, [enrichedCatalog.catalog?.a2a_agents]) const catalogGateways = useMemo(() => { const gateways = enrichedCatalog.catalog?.gateways if (!gateways) return [] return gateways.filter((g) => g.id && g.name) }, [enrichedCatalog.catalog?.gateways]) // Additive: inline registration forms (collapsed by default) const [showRegisterTool, setShowRegisterTool] = useState(false) const [showRegisterAgent, setShowRegisterAgent] = useState(false) const [showRegisterGateway, setShowRegisterGateway] = useState(false) const [registerBusy, setRegisterBusy] = useState(false) const [registerMsg, setRegisterMsg] = useState('') const [syncBusy, setSyncBusy] = useState(false) // Registration form fields const [regToolName, setRegToolName] = useState('') const [regToolDesc, setRegToolDesc] = useState('') const [regToolUrl, setRegToolUrl] = useState('') const [regAgentName, setRegAgentName] = useState('') const [regAgentDesc, setRegAgentDesc] = useState('') const [regAgentUrl, setRegAgentUrl] = useState('') const [regGatewayName, setRegGatewayName] = useState('') const [regGatewayUrl, setRegGatewayUrl] = useState('') const [regGatewayTransport, setRegGatewayTransport] = useState('SSE') const toggleSelectedTool = (id: string) => { setSelectedToolIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])) } const toggleSelectedAgent = (id: string) => { setSelectedA2AAgentIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])) } // Sync tool checkboxes when the bundle dropdown changes // "All enabled tools" → check all enabled, "server:X" → check that server's tools, "none" → uncheck all const catalogServers = useMemo(() => enrichedCatalog.catalog?.servers || [], [enrichedCatalog.catalog?.servers]) useEffect(() => { if (projectType !== 'agent') return if (!enrichedCatalog.catalog) return // catalog not loaded yet let nextIds: string[] = [] if (toolSource === 'all') { nextIds = catalogTools.filter((t) => t.enabled !== false).map((t) => t.id) } else if (toolSource.startsWith('server:')) { const sid = toolSource.replace('server:', '') const server = catalogServers.find((s) => s.id === sid) if (server) { const serverSet = new Set(server.tool_ids || []) nextIds = catalogTools.filter((t) => serverSet.has(t.id)).map((t) => t.id) } } // For 'none' → nextIds stays empty setSelectedToolIds(nextIds) }, [toolSource, catalogTools, catalogServers, projectType, enrichedCatalog.catalog]) // Refresh catalog — delegates to the enriched catalog hook (single source of truth) const refreshCatalog = async () => { await enrichedCatalog.refresh() } // Registration handlers const handleRegisterTool = async () => { if (!regToolName.trim()) return setRegisterBusy(true) setRegisterMsg('') try { const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['x-api-key'] = apiKey const res = await fetch(`${backendUrl}/v1/agentic/register/tool`, { method: 'POST', headers, body: JSON.stringify({ name: regToolName.trim(), description: regToolDesc.trim(), url: regToolUrl.trim() || undefined, input_schema: { type: 'object', properties: { text: { type: 'string' } } }, }), }) const data = await res.json() if (data?.ok) { setRegisterMsg(`Tool "${regToolName}" registered`) setRegToolName(''); setRegToolDesc(''); setRegToolUrl('') setShowRegisterTool(false) await refreshCatalog() if (data.id) setSelectedToolIds((prev) => [...prev, data.id]) } else { setRegisterMsg(`Failed: ${data?.detail || 'unknown error'}`) } } catch (e: any) { setRegisterMsg(`Error: ${e?.message || e}`) } finally { setRegisterBusy(false) } } const handleRegisterAgent = async () => { if (!regAgentName.trim() || !regAgentUrl.trim()) return setRegisterBusy(true) setRegisterMsg('') try { const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['x-api-key'] = apiKey const res = await fetch(`${backendUrl}/v1/agentic/register/agent`, { method: 'POST', headers, body: JSON.stringify({ name: regAgentName.trim(), description: regAgentDesc.trim(), endpoint_url: regAgentUrl.trim(), }), }) const data = await res.json() if (data?.ok) { setRegisterMsg(`Agent "${regAgentName}" registered`) setRegAgentName(''); setRegAgentDesc(''); setRegAgentUrl('') setShowRegisterAgent(false) await refreshCatalog() if (data.id) setSelectedA2AAgentIds((prev) => [...prev, data.id]) } else { setRegisterMsg(`Failed: ${data?.detail || 'unknown error'}`) } } catch (e: any) { setRegisterMsg(`Error: ${e?.message || e}`) } finally { setRegisterBusy(false) } } const handleRegisterGateway = async () => { if (!regGatewayName.trim() || !regGatewayUrl.trim()) return setRegisterBusy(true) setRegisterMsg('') try { const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['x-api-key'] = apiKey const res = await fetch(`${backendUrl}/v1/agentic/register/gateway`, { method: 'POST', headers, body: JSON.stringify({ name: regGatewayName.trim(), url: regGatewayUrl.trim(), transport: regGatewayTransport, auto_refresh: true, }), }) const data = await res.json() if (data?.ok) { setRegisterMsg(`Gateway "${regGatewayName}" registered — ${data?.detail || ''}`) setRegGatewayName(''); setRegGatewayUrl('') setShowRegisterGateway(false) await refreshCatalog() } else { setRegisterMsg(`Failed: ${data?.detail || 'unknown error'}`) } } catch (e: any) { setRegisterMsg(`Error: ${e?.message || e}`) } finally { setRegisterBusy(false) } } const handleSyncHomePilot = async () => { setSyncBusy(true) setRegisterMsg('') try { const headers: Record = { 'Content-Type': 'application/json' } if (apiKey) headers['x-api-key'] = apiKey const res = await fetch(`${backendUrl}/v1/agentic/sync`, { method: 'POST', headers, }) const data = await res.json() const sync = data?.sync if (sync) { const parts = [] if (sync.tools_registered > 0) parts.push(`${sync.tools_registered} tools registered`) if (sync.tools_updated > 0) parts.push(`${sync.tools_updated} tools updated`) if (sync.tools_skipped > 0) parts.push(`${sync.tools_skipped} tools already existed`) if (sync.agents_registered > 0) parts.push(`${sync.agents_registered} agents registered`) if (sync.virtual_servers_created > 0) parts.push(`${sync.virtual_servers_created} virtual servers created`) if (sync.mcp_servers_reachable < sync.mcp_servers_total) parts.push(`${sync.mcp_servers_total - sync.mcp_servers_reachable} MCP servers unreachable`) setRegisterMsg(parts.length > 0 ? `Sync complete: ${parts.join(', ')}` : 'Sync complete (everything up to date)') } else { setRegisterMsg('Sync completed') } await enrichedCatalog.refresh() } catch (e: any) { setRegisterMsg(`Sync error: ${e?.message || e}`) } finally { setSyncBusy(false) } } const totalSteps = projectType === 'agent' ? 4 : 2 // Catalog: human labels only — no MCP/tool/agent IDs shown to user const capabilityCatalog: Array<{ id: string label: string desc: string requiresHint: string }> = [ { id: 'generate_images', label: 'Generate images', desc: 'Create images from text prompts.', requiresHint: 'Requires image generation tools to be installed.', }, { id: 'generate_videos', label: 'Generate short videos', desc: 'Create short videos from prompts.', requiresHint: 'Requires video generation tools to be installed.', }, { id: 'analyze_documents', label: 'Analyze documents', desc: 'Answer questions using uploaded files.', requiresHint: 'Requires document analysis tools to be installed.', }, { id: 'automate_external', label: 'Automate external services', desc: 'Run actions across connected apps.', requiresHint: 'Requires automation tools to be installed.', }, ] const availableSet = new Set(availableCapabilities.map((c) => c.id)) const toggleCapability = (id: string) => { setAgentCapabilities((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id])) } // Fetch dynamic capabilities (best-effort, graceful fallback) useEffect(() => { if (projectType !== 'agent') return if (capabilitiesLoaded) return const run = async () => { try { const headers: Record = {} if (apiKey) headers['x-api-key'] = apiKey const res = await fetch(`${backendUrl}/v1/agentic/capabilities`, { headers }) if (!res.ok) throw new Error(`HTTP ${res.status}`) const data = await res.json() const caps: AgentCapability[] = Array.isArray(data?.capabilities) ? data.capabilities .map((c: any) => ({ id: String(c?.id || ''), label: String(c?.label || c?.id || ''), description: typeof c?.description === 'string' ? c.description : undefined, })) .filter((c: any) => c.id && c.label) : [] setAvailableCapabilities(caps) } catch { // Degrade gracefully: show everything disabled setAvailableCapabilities([]) } finally { setCapabilitiesLoaded(true) } } void run() }, [apiKey, backendUrl, capabilitiesLoaded, projectType]) // Smart defaults: preselect capabilities that match the user's goal text useEffect(() => { if (projectType !== 'agent') return if (!capabilitiesLoaded) return if (agentCapabilities.length > 0) return const goalText = `${agentGoal} ${description} ${instructions}`.toLowerCase() const availIds = new Set(availableCapabilities.map((c) => c.id)) const next: string[] = [] if ( (goalText.includes('image') || goalText.includes('logo') || goalText.includes('design') || goalText.includes('picture')) && availIds.has('generate_images') ) next.push('generate_images') if ( (goalText.includes('video') || goalText.includes('animation') || goalText.includes('clip')) && availIds.has('generate_videos') ) next.push('generate_videos') setAgentCapabilities(next) }, [agentCapabilities.length, agentGoal, availableCapabilities, capabilitiesLoaded, description, instructions, projectType]) // --- File handlers (unchanged) --- const handleDrop = (e: React.DragEvent) => { e.preventDefault() const droppedFiles = Array.from(e.dataTransfer.files) const newFiles = droppedFiles.map((f) => ({ name: f.name, size: `${(f.size / 1024 / 1024).toFixed(2)} MB`, file: f, })) setFiles([...files, ...newFiles]) } const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files) const newFiles = selectedFiles.map((f) => ({ name: f.name, size: `${(f.size / 1024 / 1024).toFixed(2)} MB`, file: f, })) setFiles([...files, ...newFiles]) } } const handleCreate = () => { const projectData: any = { name: projectName || 'Untitled Project', description, instructions, files: files, project_type: projectType, } // Agent metadata: capabilities + optional real Forge bindings if (projectType === 'agent') { // Resolve human-readable names for tools & agents so the system prompt can reference them const toolDetails = selectedToolIds.map((tid) => { const t = catalogTools.find((x) => x.id === tid) return { id: tid, name: t?.name || tid, description: t?.description || '' } }) const agentDetails = selectedA2AAgentIds.map((aid) => { const a = catalogAgents.find((x) => x.id === aid) return { id: aid, name: a?.name || aid, description: a?.description || '' } }) projectData.agentic = { goal: agentGoal, capabilities: agentCapabilities, // Additive: store selected Forge bindings (optional, for real tool/agent wiring) tool_ids: selectedToolIds, a2a_agent_ids: selectedA2AAgentIds, // Resolved details for system prompt tool_details: toolDetails, agent_details: agentDetails, // Phase 7: virtual-server-first tool scope tool_source: toolSource, ask_before_acting: agentAskBeforeActing, execution_profile: agentExecutionProfile, } } onSave(projectData) } return (
e.stopPropagation()} > {/* Header */}

Create new project

Step {step} of {totalSteps}

{/* Content */}
{/* STEP 1: Details */} {step === 1 && (
{/* Project Type Selection */}
{[ { id: 'chat', icon: MessageSquare, label: 'Chat / LLM', desc: 'Custom AI assistant', color: 'blue' }, { id: 'image', icon: ImageIcon, label: 'Image', desc: 'Image generation', color: 'purple' }, { id: 'video', icon: Film, label: 'Video', desc: 'Video generation', color: 'green' }, { id: 'agent', icon: Bot, label: 'Agent', desc: 'Advanced help with tools', color: 'amber' }, { id: 'persona', icon: User, label: 'Persona', desc: 'Custom personality + avatar', color: 'pink' }, ].map((type) => ( ))}
{/* Project Name & Description */}
setProjectName(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white placeholder-white/40 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-all" /> setDescription(e.target.value)} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-white/40 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-all" />
{/* Agent Goal (only for Agent type) */} {projectType === 'agent' && (
What is this agent for?