/** * FormEditorTabs - Clean tabbed form editor for agent configuration * * Design follows session/server panel patterns: * - Minimal borders, spacing-based hierarchy * - Section headers as uppercase labels * - shadcn Select components */ import React, { useState, useMemo } from 'react'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs'; import { Input } from '../ui/input'; import { Textarea } from '../ui/textarea'; import { Button } from '../ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { Settings, Brain, Wrench, Eye, EyeOff, Plus, Trash2, Info, Loader2, ChevronRight, ChevronDown, Server, } from 'lucide-react'; import type { AgentConfig, ContributorConfig } from '@dexto/core'; import { LLM_PROVIDERS, MCP_SERVER_TYPES } from '@dexto/core'; import { cn } from '@/lib/utils'; import { useDiscovery } from '../hooks/useDiscovery'; import { useLLMCatalog } from '../hooks/useLLM'; // Providers that support custom baseURL const BASE_URL_PROVIDERS = ['openai-compatible', 'litellm']; interface FormEditorTabsProps { config: AgentConfig; onChange: (config: AgentConfig) => void; errors?: Record; } type TabValue = 'model' | 'behavior' | 'tools'; export default function FormEditorTabs({ config, onChange, errors = {} }: FormEditorTabsProps) { const [activeTab, setActiveTab] = useState('model'); // Count errors per tab const modelErrors = Object.keys(errors).filter( (k) => k.startsWith('llm.') || k === 'greeting' ).length; const behaviorErrors = Object.keys(errors).filter((k) => k.startsWith('systemPrompt')).length; const toolsErrors = Object.keys(errors).filter( (k) => k.startsWith('mcpServers') || k.startsWith('internalTools') || k.startsWith('customTools') ).length; return ( setActiveTab(v as TabValue)} className="flex flex-col h-full" > } badge={modelErrors > 0 ? : undefined} > Model } badge={behaviorErrors > 0 ? : undefined} > Behavior } badge={toolsErrors > 0 ? : undefined} > Tools ); } function ErrorBadge({ count }: { count: number }) { return ( {count} ); } // ============================================================================ // MODEL TAB - LLM Configuration // ============================================================================ interface TabProps { config: AgentConfig; onChange: (config: AgentConfig) => void; errors: Record; } function ModelTab({ config, onChange, errors }: TabProps) { const [showApiKey, setShowApiKey] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const { data: catalogData, isLoading: catalogLoading } = useLLMCatalog({ mode: 'grouped' }); const currentProvider = config.llm?.provider || ''; const supportsBaseURL = BASE_URL_PROVIDERS.includes(currentProvider); const providerModels = useMemo(() => { if (!catalogData || !('providers' in catalogData) || !currentProvider) return []; const providerData = catalogData.providers[currentProvider as keyof typeof catalogData.providers]; if (!providerData?.models) return []; return providerData.models.map((m) => ({ id: m.name, displayName: m.displayName || m.name, })); }, [catalogData, currentProvider]); const updateLLM = (updates: Partial>) => { onChange({ ...config, llm: { ...config.llm, ...updates } as AgentConfig['llm'], }); }; return (
{/* Language Model Section */}
{catalogLoading ? (
Loading...
) : providerModels.length > 0 ? ( ) : ( updateLLM({ model: e.target.value })} placeholder={ currentProvider ? 'Enter model name' : 'Select provider first' } aria-invalid={!!errors['llm.model']} /> )}
updateLLM({ apiKey: e.target.value })} placeholder="$ANTHROPIC_API_KEY" className="pr-10" />
{/* Base URL - Only for OpenAI-compatible providers */} {supportsBaseURL && ( updateLLM({ baseURL: e.target.value || undefined }) } placeholder="https://api.example.com/v1" /> )} {/* Advanced Settings */} {showAdvanced && (
updateLLM({ maxOutputTokens: e.target.value ? parseInt(e.target.value, 10) : undefined, }) } placeholder="4096" min="1" />
)}
{/* Greeting Section */}
onChange({ ...config, greeting: e.target.value })} placeholder="Hello! How can I help you today?" />
); } // ============================================================================ // BEHAVIOR TAB - System Prompt // ============================================================================ function BehaviorTab({ config, onChange, errors }: TabProps) { const getPromptContent = (): string => { if (!config.systemPrompt) return ''; if (typeof config.systemPrompt === 'string') return config.systemPrompt; const primary = config.systemPrompt.contributors?.find((c) => c.type === 'static'); return primary && 'content' in primary ? primary.content : ''; }; const updatePromptContent = (content: string) => { if (!config.systemPrompt || typeof config.systemPrompt === 'string') { onChange({ ...config, systemPrompt: { contributors: [ { id: 'primary', type: 'static', priority: 0, enabled: true, content }, ], }, }); } else { const contributors = [...(config.systemPrompt.contributors || [])]; const primaryIdx = contributors.findIndex((c) => c.id === 'primary'); if (primaryIdx >= 0) { contributors[primaryIdx] = { ...contributors[primaryIdx], content, } as ContributorConfig; } else { contributors.unshift({ id: 'primary', type: 'static', priority: 0, enabled: true, content, }); } onChange({ ...config, systemPrompt: { contributors } }); } }; const hasMultipleContributors = config.systemPrompt && typeof config.systemPrompt === 'object' && config.systemPrompt.contributors && config.systemPrompt.contributors.length > 1; return (