diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index ceb3614e..9dc4b238 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -100,6 +100,12 @@ export async function fetchModels(): Promise { return data.data || []; } +export async function fetchRecommendedModel(): Promise<{ model: string; reason: string }> { + const res = await fetch(`${getBase()}/v1/recommended-model`); + if (!res.ok) return { model: '', reason: 'Failed to fetch' }; + return res.json(); +} + export async function pullModel(modelName: string): Promise { // In Tauri, go through the Rust backend directly (avoids CORS / timeout // issues with long model downloads via fetch). diff --git a/frontend/src/pages/AgentsPage.tsx b/frontend/src/pages/AgentsPage.tsx index 71f37e3f..0f7288fa 100644 --- a/frontend/src/pages/AgentsPage.tsx +++ b/frontend/src/pages/AgentsPage.tsx @@ -22,6 +22,7 @@ import { saveToolCredentials, fetchModels, updateManagedAgent, + fetchRecommendedModel, } from '../lib/api'; import type { AgentTask, ChannelBinding, AgentTemplate, AgentMessage, ManagedAgent, LearningLogEntry, AgentTrace, ToolInfo } from '../lib/api'; import { @@ -205,8 +206,9 @@ function serializeInterval(hours: number, minutes: number, seconds: number): str } interface WizardState { - step: number; + step: 1 | 2; templateId: string; + templateData: AgentTemplate | null; name: string; instruction: string; model: string; @@ -219,8 +221,11 @@ interface WizardState { observationCompression: string; retrievalStrategy: string; taskDecomposition: string; + maxTurns: number; + temperature: number; } + function LaunchWizard({ templates, onClose, @@ -230,9 +235,19 @@ function LaunchWizard({ onClose: () => void; onLaunched: () => void; }) { + const UNIVERSAL_DEFAULTS = { + memoryExtraction: 'structured_json', + observationCompression: 'summarize', + retrievalStrategy: 'sqlite', + taskDecomposition: 'hierarchical', + maxTurns: 25, + temperature: 0.3, + }; + const [wizard, setWizard] = useState({ step: 1, templateId: '', + templateData: null, name: '', instruction: '', model: '', @@ -241,94 +256,60 @@ function LaunchWizard({ selectedTools: [], budget: '', routerPolicy: '', - memoryExtraction: 'causality_graph', - observationCompression: 'summarize', - retrievalStrategy: 'hybrid_with_self_eval', - taskDecomposition: 'phased', + ...UNIVERSAL_DEFAULTS, }); const [launching, setLaunching] = useState(false); + const [recommendedModel, setRecommendedModel] = useState(''); const models = useAppStore((s) => s.models); - const [availableTools, setAvailableTools] = useState([]); - const [expandedCategories, setExpandedCategories] = useState>(new Set()); - const [credentialInputs, setCredentialInputs] = useState>>({}); - const [savingCredentials, setSavingCredentials] = useState(null); useEffect(() => { - fetchAvailableTools().then(setAvailableTools).catch(() => {}); + fetchRecommendedModel().then((r) => { + setRecommendedModel(r.model); + if (!wizard.model) { + setWizard((w) => ({ ...w, model: r.model })); + } + }).catch(() => {}); }, []); - function getToolCategory(tool: ToolInfo): string { - if (tool.category && CATEGORY_MAP[tool.category]) return CATEGORY_MAP[tool.category]; - if (TOOL_NAME_FALLBACK[tool.name]) return TOOL_NAME_FALLBACK[tool.name]; - return 'Reasoning & AI'; - } - - function toggleCategory(cat: string) { - setExpandedCategories((prev) => { - const next = new Set(prev); - if (next.has(cat)) next.delete(cat); - else next.add(cat); - return next; - }); - } - - function handleToggleTool(name: string) { - if (name === 'browser') { - const has = BROWSER_SUB_TOOLS.every((t) => wizard.selectedTools.includes(t)); - if (has) { - update({ selectedTools: wizard.selectedTools.filter((t) => !BROWSER_SUB_TOOLS.includes(t)) }); - } else { - update({ selectedTools: [...new Set([...wizard.selectedTools, ...BROWSER_SUB_TOOLS])] }); - } + function selectTemplate(tpl: AgentTemplate | null) { + if (tpl) { + setWizard((w) => ({ + ...w, + step: 2, + templateId: tpl.id, + templateData: tpl, + name: '', + instruction: '', + model: recommendedModel || w.model, + scheduleType: (tpl as any).schedule_type || 'manual', + scheduleValue: (tpl as any).schedule_value || '', + selectedTools: (tpl as any).tools || [], + memoryExtraction: (tpl as any).memory_extraction || UNIVERSAL_DEFAULTS.memoryExtraction, + observationCompression: (tpl as any).observation_compression || UNIVERSAL_DEFAULTS.observationCompression, + retrievalStrategy: (tpl as any).retrieval_strategy || UNIVERSAL_DEFAULTS.retrievalStrategy, + taskDecomposition: (tpl as any).task_decomposition || UNIVERSAL_DEFAULTS.taskDecomposition, + maxTurns: (tpl as any).max_turns || UNIVERSAL_DEFAULTS.maxTurns, + temperature: (tpl as any).temperature ?? UNIVERSAL_DEFAULTS.temperature, + })); } else { - toggleTool(name); + setWizard((w) => ({ + ...w, + step: 2, + templateId: '', + templateData: null, + name: '', + instruction: '', + model: recommendedModel || w.model, + scheduleType: 'manual', + scheduleValue: '', + selectedTools: [], + ...UNIVERSAL_DEFAULTS, + })); } } - async function handleSaveCredentials(toolName: string) { - const inputs = credentialInputs[toolName]; - if (!inputs) return; - setSavingCredentials(toolName); - try { - await saveToolCredentials(toolName, inputs); - toast.success(`Credentials saved for ${toolName}`); - const updated = await fetchAvailableTools(); - setAvailableTools(updated); - } catch (err: any) { - toast.error(err.message || 'Failed to save credentials'); - } finally { - setSavingCredentials(null); - } - } - - function update(partial: Partial) { - setWizard((prev) => ({ ...prev, ...partial })); - } - - function toggleTool(id: string) { - const next = wizard.selectedTools.includes(id) - ? wizard.selectedTools.filter((t) => t !== id) - : [...wizard.selectedTools, id]; - update({ selectedTools: next }); - } - - function selectTemplate(id: string) { - const tpl = templates.find((t) => t.id === id); - update({ - templateId: id, - name: tpl?.name || wizard.name, - }); - } - async function handleLaunch() { - if ((wizard.scheduleType === 'cron' || wizard.scheduleType === 'interval') && !wizard.instruction.trim()) { - toast.error('Instruction is required for scheduled agents'); - return; - } - if (!wizard.name.trim()) { - toast.error('Agent name is required'); - return; - } + if (!wizard.name.trim()) { toast.error('Name is required'); return; } setLaunching(true); try { const config: Record = { @@ -336,633 +317,259 @@ function LaunchWizard({ schedule_value: wizard.scheduleValue || undefined, tools: wizard.selectedTools, learning_enabled: !!wizard.routerPolicy, + memory_extraction: wizard.memoryExtraction, + observation_compression: wizard.observationCompression, + retrieval_strategy: wizard.retrievalStrategy, + task_decomposition: wizard.taskDecomposition, + max_turns: wizard.maxTurns, + temperature: wizard.temperature, }; if (wizard.budget) config.budget = parseFloat(wizard.budget); if (wizard.instruction.trim()) config.instruction = wizard.instruction.trim(); if (wizard.model) config.model = wizard.model; if (wizard.routerPolicy) config.router_policy = wizard.routerPolicy; - config.memory_extraction = wizard.memoryExtraction; - config.observation_compression = wizard.observationCompression; - config.retrieval_strategy = wizard.retrievalStrategy; - config.task_decomposition = wizard.taskDecomposition; - const created = await createManagedAgent({ - name: wizard.name, + + await createManagedAgent({ + name: wizard.name.trim(), template_id: wizard.templateId || undefined, config, }); - toast.success(`Agent "${wizard.name}" launched`); - // Auto-run first tick for interval agents - if (wizard.scheduleType === 'interval' && created.id) { - runManagedAgent(created.id).catch(() => {}); - } + toast.success(`Agent "${wizard.name}" created`); onLaunched(); - } catch (err) { - toast.error('Could not create agent', { - description: 'Agent manager endpoint not available. Check that agent_manager.enabled = true in your config.', - }); + } catch (err: any) { + toast.error(err.message || 'Failed to create agent'); } finally { setLaunching(false); } } - return ( -
e.target === e.currentTarget && onClose()} - > -
- {/* Header */} -
-
- -

- Launch Agent -

+ const formatScheduleLabel = (type: string, value: string) => { + if (type === 'manual') return 'Manual (run on demand)'; + if (type === 'cron') return `Cron: ${value}`; + if (type === 'interval') { + const secs = parseInt(value, 10); + if (secs >= 3600) return `Every ${secs / 3600}h`; + if (secs >= 60) return `Every ${secs / 60}m`; + return `Every ${secs}s`; + } + return type; + }; + + // ── Step 1: Template Selection ── + if (wizard.step === 1) { + return ( +
+
+
+

New Agent — Choose Template

+
-
- {/* Step indicator */} -
- {([1, 2, 3] as const).map((s) => ( - - s ? 'var(--color-accent)' + '40' : 'var(--color-bg-secondary)', - color: wizard.step >= s ? (wizard.step === s ? '#fff' : 'var(--color-accent)') : 'var(--color-text-tertiary)', - }} - > - {s} - - {s < 3 && } - - ))} -
- + ))} +
+
+ ); + } - {/* Body */} -
- {/* Step 1: Template Picker */} - {wizard.step === 1 && ( + // ── Step 2: Configuration ── + return ( +
+
+
+
+ +

+ {wizard.templateData ? `New ${wizard.templateData.name}` : 'New Custom Agent'} +

+
+ +
+ +
+ {/* Name */} +
+ + setWizard((w) => ({ ...w, name: e.target.value }))} + placeholder="e.g. AI Research Tracker" + className="w-full px-3 py-2 rounded-lg text-sm bg-transparent" + style={{ border: '1px solid var(--color-border)', color: 'var(--color-text)' }} + /> +
+ + {/* Instruction */} +
+ +