diff --git a/client/src/components/AgentCreateModal.tsx b/client/src/components/AgentCreateModal.tsx index 8a3bf9e..637ed83 100644 --- a/client/src/components/AgentCreateModal.tsx +++ b/client/src/components/AgentCreateModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -13,7 +13,7 @@ import { } from "@/components/ui/select"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; -import { Loader2, Plus, RefreshCw } from "lucide-react"; +import { Loader2, Plus, Wand2, Sparkles } from "lucide-react"; interface AgentCreateModalProps { open: boolean; @@ -22,27 +22,40 @@ interface AgentCreateModalProps { } const AGENT_ROLES = [ - { value: "developer", label: "Developer - Code generation & testing" }, - { value: "researcher", label: "Researcher - Data analysis & research" }, - { value: "executor", label: "Executor - Task automation" }, - { value: "monitor", label: "Monitor - System monitoring" }, + { value: "developer", label: "Developer — Code generation & testing" }, + { value: "researcher", label: "Researcher — Data analysis & research" }, + { value: "executor", label: "Executor — Task automation" }, + { value: "monitor", label: "Monitor — System monitoring" }, + { value: "analyst", label: "Analyst — Data processing & reports" }, + { value: "writer", label: "Writer — Content generation" }, + { value: "coordinator", label: "Coordinator — Multi-agent orchestration" }, ]; -// Providers are loaded dynamically from server config — no hardcoded list +const DEFAULT_FORM = { + name: "", + description: "", + role: "developer", + provider: "", + model: "", + temperature: 0.7, + maxTokens: 2048, + systemPrompt: "", +}; + +function toNum(v: string | number | null | undefined, fallback = 0): number { + if (v === null || v === undefined) return fallback; + const n = typeof v === "string" ? parseFloat(v) : v; + return isNaN(n) ? fallback : n; +} export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateModalProps) { - const [formData, setFormData] = useState({ - name: "", - description: "", - role: "developer", - provider: "Ollama", - model: "deepseek-v3.2", - temperature: 0.7, - maxTokens: 2048, - systemPrompt: "", - }); - + const [formData, setFormData] = useState({ ...DEFAULT_FORM }); const [isLoading, setIsLoading] = useState(false); + const [isAutoFilling, setIsAutoFilling] = useState(false); + const [maxTokensHint, setMaxTokensHint] = useState(null); + const [modelInfoLoading, setModelInfoLoading] = useState(false); + + // ─── Remote data ─────────────────────────────────────────────────────────── const { data: modelsData, isLoading: modelsLoading } = trpc.ollama.models.useQuery(undefined, { staleTime: 60_000, @@ -50,22 +63,46 @@ export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateM const { data: configData } = trpc.config.providers.useQuery(undefined, { staleTime: 300_000, }); - // Only providers configured on server const connectedProviders = configData?.providers ?? []; + // ─── Model info (context_length) ─────────────────────────────────────────── + const { data: modelInfoData, isFetching: modelInfoFetching } = trpc.ollama.modelInfo.useQuery( + { modelId: formData.model }, + { + enabled: !!formData.model, + staleTime: 300_000, + } + ); + + // When model info arrives, update maxTokens with context_length + useEffect(() => { + if (modelInfoData?.contextLength && modelInfoData.contextLength > 0) { + setMaxTokensHint(modelInfoData.contextLength); + setFormData((prev) => ({ ...prev, maxTokens: modelInfoData.contextLength })); + } + }, [modelInfoData]); + + // Auto-select first provider when providers load + useEffect(() => { + if (connectedProviders.length > 0 && !formData.provider) { + setFormData((prev) => ({ ...prev, provider: connectedProviders[0].name })); + } + }, [connectedProviders]); + + // Auto-select first model when models load + useEffect(() => { + const availableModels: string[] = modelsData?.models?.map((m: any) => m.id || m) ?? []; + if (availableModels.length > 0 && !formData.model) { + setFormData((prev) => ({ ...prev, model: availableModels[0] })); + } + }, [modelsData]); + + // ─── Mutations ───────────────────────────────────────────────────────────── + const createMutation = trpc.agents.create.useMutation({ onSuccess: () => { toast.success("Agent created successfully"); - setFormData({ - name: "", - description: "", - role: "developer", - provider: "Ollama", - model: "deepseek-v3.2", - temperature: 0.7, - maxTokens: 2048, - systemPrompt: "", - }); + setFormData({ ...DEFAULT_FORM }); onOpenChange(false); onSuccess?.(); }, @@ -74,19 +111,46 @@ export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateM }, }); + const compileMutation = trpc.agentCompiler.compile.useMutation({ + onSuccess: (result) => { + if (!result.success || !result.config) { + toast.error("AI auto-fill failed — fill fields manually"); + setIsAutoFilling(false); + return; + } + const cfg = result.config; + setFormData((prev) => ({ + ...prev, + role: cfg.role || prev.role, + provider: cfg.provider || prev.provider, + model: cfg.model || prev.model, + temperature: toNum(cfg.temperature, 0.7), + maxTokens: cfg.maxTokens || prev.maxTokens, + systemPrompt: cfg.systemPrompt || prev.systemPrompt, + })); + toast.success("AI filled the fields — review and save"); + setIsAutoFilling(false); + }, + onError: (err) => { + toast.error(`Auto-fill error: ${err.message}`); + setIsAutoFilling(false); + }, + }); + + // ─── Handlers ────────────────────────────────────────────────────────────── + const handleCreate = async () => { if (!formData.name.trim()) { toast.error("Agent name is required"); return; } - setIsLoading(true); try { await createMutation.mutateAsync({ name: formData.name, description: formData.description, role: formData.role, - provider: formData.provider, + provider: formData.provider || "Ollama", model: formData.model, temperature: formData.temperature, maxTokens: formData.maxTokens, @@ -98,167 +162,247 @@ export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateM } }; - // All models from API — no provider filtering (API returns only what's connected) + const handleAutoFill = async () => { + if (!formData.name.trim()) { + toast.error("Enter Agent Name first — the AI needs it to suggest parameters"); + return; + } + setIsAutoFilling(true); + const spec = `Agent name: ${formData.name}\n${formData.description ? `Description: ${formData.description}` : ""}`.trim(); + await compileMutation.mutateAsync({ + specification: spec, + name: formData.name, + preferredProvider: formData.provider || undefined, + preferredModel: formData.model || undefined, + }); + }; + + const handleModelChange = (model: string) => { + setFormData((prev) => ({ ...prev, model })); + setMaxTokensHint(null); + }; + + // ─── Derived ─────────────────────────────────────────────────────────────── const availableModels: string[] = modelsData?.models?.map((m: any) => m.id || m) ?? []; + const canAutoFill = formData.name.trim().length > 0; return ( - + - Deploy New Agent + + + Deploy New Agent +
- {/* Basic Info */} -
- - setFormData({ ...formData, name: e.target.value })} - className="font-mono" - /> + {/* ── Name + Description ─────────────────────────────────────────── */} +
+
+ + setFormData({ ...formData, name: e.target.value })} + className="font-mono" + /> +
+ {/* Magic wand — auto-fill remaining fields via AI */} +
-
- + {canAutoFill && !isAutoFilling && ( +

+ + Click Auto-fill to let AI suggest temperature, system prompt and other params +

+ )} + +
+