import React, { useState, useEffect, useCallback } from 'react';
import { X, Settings, Bot, Zap, Shield, Wrench, Users, Server, ChevronDown, ChevronUp, Check, Loader2, FileText, Trash2, } from 'lucide-react';
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const PROFILE_OPTIONS = [
{ value: 'fast', label: 'Fast', hint: 'Low latency, fewer tool calls' },
{ value: 'balanced', label: 'Balanced', hint: 'Good mix of speed and depth' },
{ value: 'quality', label: 'Quality', hint: 'Thorough, multi-step reasoning' },
];
const BUILTIN_CAPABILITIES = [
{ id: 'generate_images', label: 'Generate images' },
{ id: 'generate_videos', label: 'Generate short videos' },
{ id: 'analyze_documents', label: 'Analyze documents' },
{ id: 'automate_external', label: 'Automate external services' },
];
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function SectionHeader({ icon: Icon, title, badge }) {
return (
{title}
{badge !== undefined && (
{badge}
)}
);
}
function Toggle({ checked, onChange, label }) {
return ( onChange(!checked)} className="flex items-center justify-between w-full group">
{label}
);
}
function StatusDot({ ok }) {
return ( );
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export function AgentSettingsPanel({ project, backendUrl, apiKey, onClose, onSaved }) {
const ag = project.agentic || {};
// --- Editable state ---
const [name, setName] = useState(project.name || '');
const [description, setDescription] = useState(project.description || '');
const [instructions, setInstructions] = useState(project.instructions || '');
const [goal, setGoal] = useState(ag.goal || '');
const [capabilities, setCapabilities] = useState(ag.capabilities || []);
const [profile, setProfile] = useState(ag.execution_profile || 'fast');
const [askFirst, setAskFirst] = useState(ag.ask_before_acting !== false);
const [toolIds, setToolIds] = useState(ag.tool_ids || []);
const [agentIds, setAgentIds] = useState(ag.a2a_agent_ids || []);
const [toolSource, setToolSource] = useState(ag.tool_source || 'all');
// --- Catalog data ---
const [catalogTools, setCatalogTools] = useState([]);
const [catalogAgents, setCatalogAgents] = useState([]);
const [catalogServers, setCatalogServers] = useState([]);
const [catalogLoading, setCatalogLoading] = useState(true);
// --- Documents ---
const [documents, setDocuments] = useState(project.files || []);
// --- UI state ---
const [saving, setSaving] = useState(false);
const [dirty, setDirty] = useState(false);
const [showTools, setShowTools] = useState(false);
const [showAgents, setShowAgents] = useState(false);
// Track dirtiness
useEffect(() => { setDirty(true); }, [name, description, instructions, goal, capabilities, profile, askFirst, toolIds, agentIds, toolSource]);
// Reset dirty on initial load
useEffect(() => { setDirty(false); }, []);
// --- Fetch catalog ---
useEffect(() => {
const headers = {};
if (apiKey)
headers['x-api-key'] = apiKey;
try {
if (typeof window !== 'undefined') {
const tok = window.localStorage.getItem('homepilot_auth_token') || '';
if (tok)
headers['authorization'] = `Bearer ${tok}`;
}
}
catch { /* ignore */ }
fetch(`${backendUrl}/v1/agentic/catalog`, { headers, credentials: 'include' })
.then((r) => r.ok ? r.json() : null)
.then((data) => {
if (data) {
setCatalogServers(Array.isArray(data.servers)
? data.servers.map((s) => ({
id: String(s.id || s.name),
name: String(s.name || s.id),
description: s.description,
enabled: s.enabled !== false,
tool_ids: Array.isArray(s.tool_ids)
? s.tool_ids
: (Array.isArray(s.associated_tools) ? s.associated_tools : []),
}))
: []);
setCatalogTools(Array.isArray(data.tools)
? data.tools.map((t) => ({ id: t.id || t.name, name: t.name, description: t.description, enabled: t.enabled !== false }))
: []);
setCatalogAgents(Array.isArray(data.a2a_agents)
? data.a2a_agents.map((a) => ({ id: a.id || a.name, name: a.name, description: a.description, enabled: a.enabled !== false }))
: []);
}
})
.catch(() => { })
.finally(() => setCatalogLoading(false));
}, [backendUrl, apiKey]);
// --- Derived: effective access policy (matches wizard + backend enforcement) ---
const enabledCatalogTools = catalogTools.filter((t) => t.enabled !== false);
const serverToolCount = (() => {
if (!toolSource.startsWith('server:'))
return 0;
const sid = toolSource.replace('server:', '');
const s = catalogServers.find((x) => x.id === sid);
return s?.tool_ids?.length || 0;
})();
const effectiveToolCount = (() => {
if (toolSource === 'none')
return 0;
if (toolSource === 'all')
return enabledCatalogTools.length;
if (toolSource.startsWith('server:'))
return serverToolCount;
return 0;
})();
const visibleTools = (() => {
if (toolSource === 'none')
return [];
if (toolSource === 'all')
return enabledCatalogTools;
if (toolSource.startsWith('server:')) {
const sid = toolSource.replace('server:', '');
const s = catalogServers.find((x) => x.id === sid);
if (!s?.tool_ids?.length)
return [];
const ids = new Set(s.tool_ids);
return enabledCatalogTools.filter((t) => ids.has(t.id));
}
return [];
})();
// --- Save ---
const handleSave = useCallback(async () => {
setSaving(true);
try {
const headers = { 'Content-Type': 'application/json' };
if (apiKey)
headers['x-api-key'] = apiKey;
// Build lookup from previously saved details so we can fall back if catalog is empty/stale
const prevToolDetails = {};
for (const d of (ag.tool_details || [])) {
if (d && typeof d === 'object' && d.id)
prevToolDetails[d.id] = d;
}
const prevAgentDetails = {};
for (const d of (ag.agent_details || [])) {
if (d && typeof d === 'object' && d.id)
prevAgentDetails[d.id] = d;
}
// Resolve human-readable names for tools & agents (catalog → previous save → fallback)
const toolDetails = toolIds.map((tid) => {
const t = catalogTools.find((x) => x.id === tid);
const prev = prevToolDetails[tid];
return {
id: tid,
name: t?.name || prev?.name || tid,
description: t?.description || prev?.description || '',
};
});
const agentDetailsList = agentIds.map((aid) => {
const a = catalogAgents.find((x) => x.id === aid);
const prev = prevAgentDetails[aid];
return {
id: aid,
name: a?.name || prev?.name || aid,
description: a?.description || prev?.description || '',
};
});
const body = {
name,
description,
instructions,
project_type: 'agent',
agentic: {
goal,
capabilities,
tool_ids: toolIds,
a2a_agent_ids: agentIds,
tool_details: toolDetails,
agent_details: agentDetailsList,
tool_source: toolSource,
ask_before_acting: askFirst,
execution_profile: profile,
},
};
const res = await fetch(`${backendUrl}/projects/${project.id}`, {
method: 'PUT',
headers,
body: JSON.stringify(body),
});
if (res.ok) {
const data = await res.json();
setDirty(false);
onSaved(data.project);
}
else {
alert('Failed to save project settings');
}
}
catch {
alert('Failed to save project settings');
}
finally {
setSaving(false);
}
}, [name, description, instructions, goal, capabilities, profile, askFirst, toolIds, agentIds, toolSource, backendUrl, apiKey, project.id, onSaved, catalogTools, catalogAgents, ag.tool_details, ag.agent_details]);
// --- Document delete ---
const handleDeleteDoc = async (docName) => {
if (!confirm(`Delete document "${docName}"?`))
return;
try {
const headers = {};
if (apiKey)
headers['x-api-key'] = apiKey;
const res = await fetch(`${backendUrl}/projects/${project.id}/documents/${encodeURIComponent(docName)}`, { method: 'DELETE', headers });
if (res.ok)
setDocuments((prev) => prev.filter((d) => d.name !== docName));
}
catch { /* silent */ }
};
const toggleCap = (id) => {
setCapabilities((prev) => prev.includes(id) ? prev.filter((c) => c !== id) : [...prev, id]);
};
const toggleTool = (id) => {
setToolIds((prev) => prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id]);
};
const toggleAgent = (id) => {
setAgentIds((prev) => prev.includes(id) ? prev.filter((a) => a !== id) : [...prev, id]);
};
return (
e.stopPropagation()}>
{/* ── Header ── */}
Agent Settings
Configure behavior, tools, and connections
{/* ── Content ── */}
{/* ─── Section: Identity ─── */}
{/* ─── Section: Behavior ─── */}
{/* Execution profile */}
Execution Profile
{PROFILE_OPTIONS.map((opt) => (
setProfile(opt.value)} className={[
'relative px-3 py-3 rounded-xl border text-left transition-all',
profile === opt.value
? 'bg-purple-500/15 border-purple-500/40 ring-1 ring-purple-500/20'
: 'bg-white/5 border-white/10 hover:bg-white/8 hover:border-white/15',
].join(' ')}>
{opt.label}
{opt.hint}
{profile === opt.value && (
)}
))}
{/* Toggle */}
When enabled, the agent will confirm before running tools or taking actions.
{/* ─── Section: Capabilities ─── */}
{BUILTIN_CAPABILITIES.map((cap) => {
const active = capabilities.includes(cap.id);
return (
toggleCap(cap.id)} className={[
'flex items-center gap-2.5 px-3 py-2.5 rounded-xl border text-left transition-all',
active
? 'bg-purple-500/15 border-purple-500/30'
: 'bg-white/5 border-white/10 hover:bg-white/8',
].join(' ')}>
{active && }
{cap.label}
);
})}
{/* ─── Section: Connected Tools ─── */}
{catalogLoading ? (
Loading catalog...
) : catalogTools.length === 0 ? (
No tools registered in Context Forge. Start the MCP servers and run the seed script.
) : (
setShowTools(!showTools)} className="flex items-center gap-2 text-xs text-white/50 hover:text-white/80 transition-colors mb-2">
{showTools ? : }
{showTools ? 'Collapse' : `Browse ${effectiveToolCount} in bundle (${toolIds.length} pinned)`}
{showTools && (
{visibleTools.map((tool) => {
const bound = toolIds.includes(tool.id);
return (
toggleTool(tool.id)} className={[
'w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all',
bound
? 'bg-purple-500/10 border border-purple-500/20'
: 'hover:bg-white/5 border border-transparent',
].join(' ')}>
{tool.name}
{tool.description && (
{tool.description}
)}
{bound && }
);
})}
)}
The tool bundle sets the runtime permission boundary. Checkmarks above are optional pinned tools
(UI hint) and do not expand access beyond the selected bundle.
Tool bundle:
setToolSource(e.target.value)} className="bg-white/5 border border-white/10 rounded-lg px-2 py-1 text-xs text-white focus:outline-none focus:border-purple-500/50">
All enabled tools
{catalogServers.map((s) => (
Virtual server: {s.name}
))}
No tools
)}
{/* ─── Section: Connected Agents ─── */}
{catalogLoading ? (
Loading...
) : catalogAgents.length === 0 ? (
No A2A agents registered.
) : (
setShowAgents(!showAgents)} className="flex items-center gap-2 text-xs text-white/50 hover:text-white/80 transition-colors mb-2">
{showAgents ? : }
{showAgents ? 'Collapse' : `Browse ${catalogAgents.length} available (${agentIds.length} connected)`}
{showAgents && (
{catalogAgents.map((agent) => {
const bound = agentIds.includes(agent.id);
return (
toggleAgent(agent.id)} className={[
'w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all',
bound
? 'bg-purple-500/10 border border-purple-500/20'
: 'hover:bg-white/5 border border-transparent',
].join(' ')}>
{agent.name}
{agent.description && (
{agent.description}
)}
{bound && }
);
})}
)}
)}
{/* ─── Section: Effective Access Summary ─── */}
Tool bundle
{toolSource === 'all'
? `All enabled tools (${effectiveToolCount})`
: toolSource === 'none'
? 'No tools'
: (() => {
const sid = toolSource.replace('server:', '');
const s = catalogServers.find((x) => x.id === sid);
return s ? `${s.name} (${effectiveToolCount})` : toolSource;
})()}
Connected agents
{agentIds.length === 0
? 'None'
: agentIds
.map((id) => catalogAgents.find((a) => a.id === id)?.name || id)
.join(', ')}
Pinned tools
{toolIds.length}
Execution
{profile} / {askFirst ? 'Ask first' : 'Auto-execute'}
{/* ─── Section: Instructions ─── */}
{/* ─── Section: Documents ─── */}
{documents.length === 0 ? (
No documents uploaded. Upload files when creating or using the project.
) : (
{documents.map((doc, i) => (
{doc.name}
{doc.size || ''}{doc.chunks ? ` \u00b7 ${doc.chunks} chunks` : ''}
handleDeleteDoc(doc.name)} className="p-1 text-white/30 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all rounded hover:bg-red-500/10">
))}
)}
{/* ── Footer ── */}
{dirty ? 'Unsaved changes' : 'All changes saved'}
Cancel
{saving ? (
Saving...
) : 'Save Changes'}
);
}