Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s
Replaced ~100+ hardcoded French and English text strings across 30+ components with proper i18n t() calls. Added 57 new translation keys to all 15 locale files (ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh). Key changes: - contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders) - ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history) - note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist) - lab-skeleton.tsx: French loading text → t() - admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed - New AI chat component, agent cards, sidebar, settings panel i18n cleanup Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
577 lines
25 KiB
TypeScript
577 lines
25 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Agent Form Component
|
|
* Simplified form for creating and editing agents.
|
|
* Novice-friendly: hides system prompt and tools behind "Advanced mode".
|
|
*/
|
|
|
|
import { useState, useMemo, useRef } from 'react'
|
|
import { X, Plus, Trash2, Globe, FileSearch, FilePlus, FileText, ExternalLink, Brain, ChevronDown, ChevronUp, HelpCircle, Mail, ImageIcon } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { useLanguage } from '@/lib/i18n'
|
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
|
|
// --- Types ---
|
|
|
|
type AgentType = 'scraper' | 'researcher' | 'monitor' | 'custom'
|
|
|
|
/** Small "?" tooltip shown next to form labels */
|
|
function FieldHelp({ tooltip }: { tooltip: string }) {
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button type="button" className="inline-flex items-center ml-1 text-muted-foreground/40 hover:text-muted-foreground transition-colors">
|
|
<HelpCircle className="w-3.5 h-3.5" />
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className="max-w-xs text-balance">
|
|
{tooltip}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
interface AgentFormProps {
|
|
agent?: {
|
|
id: string
|
|
name: string
|
|
description?: string | null
|
|
type?: string | null
|
|
role: string
|
|
sourceUrls?: string | null
|
|
sourceNotebookId?: string | null
|
|
targetNotebookId?: string | null
|
|
frequency: string
|
|
tools?: string | null
|
|
maxSteps?: number
|
|
notifyEmail?: boolean
|
|
includeImages?: boolean
|
|
scheduledTime?: string | null
|
|
scheduledDay?: number | null
|
|
timezone?: string | null
|
|
} | null
|
|
notebooks: { id: string; name: string; icon?: string | null }[]
|
|
onSave: (data: FormData) => Promise<void>
|
|
onCancel: () => void
|
|
}
|
|
|
|
// --- Tool presets per type ---
|
|
const TOOL_PRESETS: Record<string, string[]> = {
|
|
scraper: ['web_scrape', 'note_create', 'memory_search'],
|
|
researcher: ['web_search', 'web_scrape', 'note_search', 'note_create', 'memory_search'],
|
|
monitor: ['note_search', 'note_read', 'note_create', 'memory_search'],
|
|
custom: ['memory_search'],
|
|
}
|
|
|
|
// --- Shared class strings ---
|
|
const labelCls = 'block text-sm font-medium text-foreground mb-1.5'
|
|
const labelCls2 = 'block text-sm font-medium text-foreground mb-2'
|
|
const inputCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground'
|
|
const selectCls = 'w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-card text-foreground'
|
|
const toggleOffBorder = 'border-border hover:border-primary/30'
|
|
const toggleOffIcon = 'text-muted-foreground'
|
|
const toggleOffLabel = 'text-sm font-medium text-foreground'
|
|
const toggleOffHint = 'text-xs text-muted-foreground'
|
|
|
|
// --- Component ---
|
|
|
|
export function AgentForm({ agent, notebooks, onSave, onCancel }: AgentFormProps) {
|
|
const { t } = useLanguage()
|
|
const [name, setName] = useState(agent?.name || '')
|
|
const [description, setDescription] = useState(agent?.description || '')
|
|
const [type, setType] = useState<AgentType>((agent?.type as AgentType) || 'scraper')
|
|
const [role, setRole] = useState(agent?.role || '')
|
|
const [urls, setUrls] = useState<string[]>(() => {
|
|
if (agent?.sourceUrls) {
|
|
try { return JSON.parse(agent.sourceUrls) } catch { return [''] }
|
|
}
|
|
return ['']
|
|
})
|
|
const [sourceNotebookId, setSourceNotebookId] = useState(agent?.sourceNotebookId || '')
|
|
const [targetNotebookId, setTargetNotebookId] = useState(agent?.targetNotebookId || '')
|
|
const [frequency, setFrequency] = useState(agent?.frequency || 'manual')
|
|
const [scheduledTime, setScheduledTime] = useState(agent?.scheduledTime || '08:00')
|
|
const [scheduledDay, setScheduledDay] = useState<number>(agent?.scheduledDay ?? 1)
|
|
const [timezone] = useState(() => {
|
|
try { return Intl.DateTimeFormat().resolvedOptions().timeZone } catch { return 'UTC' }
|
|
})
|
|
const [selectedTools, setSelectedTools] = useState<string[]>(() => {
|
|
if (agent?.tools) {
|
|
try {
|
|
const parsed = JSON.parse(agent.tools)
|
|
if (parsed.length > 0) return parsed
|
|
} catch { /* fall through to presets */ }
|
|
}
|
|
// New agent or old agent with empty tools: use preset defaults
|
|
const defaultType = (agent?.type as AgentType) || 'scraper'
|
|
return TOOL_PRESETS[defaultType] || []
|
|
})
|
|
const [maxSteps, setMaxSteps] = useState(agent?.maxSteps || 10)
|
|
const [notifyEmail, setNotifyEmail] = useState(agent?.notifyEmail || false)
|
|
const [includeImages, setIncludeImages] = useState(agent?.includeImages || false)
|
|
const [isSaving, setIsSaving] = useState(false)
|
|
const [showAdvanced, setShowAdvanced] = useState(() => {
|
|
// Auto-open advanced if editing an agent with custom tools or custom prompt
|
|
if (agent?.tools) {
|
|
try {
|
|
const tools = JSON.parse(agent.tools)
|
|
if (tools.length > 0) return true
|
|
} catch { /* ignore */ }
|
|
}
|
|
// Also open if agent has a custom role (instructions)
|
|
if (agent?.role && agent.role.trim().length > 0) return true
|
|
return false
|
|
})
|
|
|
|
// Tool definitions
|
|
const availableTools = useMemo(() => [
|
|
{ id: 'web_search', icon: Globe, labelKey: 'agents.tools.webSearch', external: true },
|
|
{ id: 'web_scrape', icon: ExternalLink, labelKey: 'agents.tools.webScrape', external: true },
|
|
{ id: 'note_search', icon: FileSearch, labelKey: 'agents.tools.noteSearch', external: false },
|
|
{ id: 'note_read', icon: FileText, labelKey: 'agents.tools.noteRead', external: false },
|
|
{ id: 'note_create', icon: FilePlus, labelKey: 'agents.tools.noteCreate', external: false },
|
|
{ id: 'url_fetch', icon: ExternalLink, labelKey: 'agents.tools.urlFetch', external: false },
|
|
{ id: 'memory_search', icon: Brain, labelKey: 'agents.tools.memorySearch', external: false },
|
|
], [])
|
|
|
|
// Track previous type to detect user-initiated type changes
|
|
const prevTypeRef = useRef(type)
|
|
|
|
// When user explicitly changes type (not on mount), reset tools to presets
|
|
if (prevTypeRef.current !== type) {
|
|
prevTypeRef.current = type
|
|
// This is a user-initiated type change, not a mount
|
|
// We queue the state update to happen after render
|
|
setSelectedTools(TOOL_PRESETS[type] || [])
|
|
setRole('')
|
|
}
|
|
|
|
const addUrl = () => setUrls([...urls, ''])
|
|
const removeUrl = (index: number) => setUrls(urls.filter((_, i) => i !== index))
|
|
const updateUrl = (index: number, value: string) => {
|
|
const newUrls = [...urls]
|
|
newUrls[index] = value
|
|
setUrls(newUrls)
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!name.trim()) {
|
|
toast.error(t('agents.form.nameRequired'))
|
|
return
|
|
}
|
|
|
|
setIsSaving(true)
|
|
try {
|
|
const formData = new FormData()
|
|
formData.set('name', name.trim())
|
|
formData.set('description', description.trim())
|
|
formData.set('type', type)
|
|
formData.set('role', role || t(`agents.defaultRoles.${type}`))
|
|
formData.set('frequency', frequency)
|
|
formData.set('targetNotebookId', targetNotebookId)
|
|
|
|
if (type === 'monitor') {
|
|
formData.set('sourceNotebookId', sourceNotebookId)
|
|
}
|
|
|
|
const validUrls = urls.filter(u => u.trim())
|
|
if (validUrls.length > 0) {
|
|
formData.set('sourceUrls', JSON.stringify(validUrls))
|
|
}
|
|
|
|
formData.set('tools', JSON.stringify(selectedTools))
|
|
formData.set('maxSteps', String(maxSteps))
|
|
formData.set('notifyEmail', String(notifyEmail))
|
|
formData.set('includeImages', String(includeImages))
|
|
formData.set('scheduledTime', scheduledTime)
|
|
formData.set('scheduledDay', String(scheduledDay))
|
|
formData.set('timezone', timezone)
|
|
|
|
await onSave(formData)
|
|
} catch {
|
|
toast.error(t('agents.toasts.saveError'))
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
const showSourceNotebook = type === 'monitor'
|
|
|
|
const agentTypes: { value: AgentType; labelKey: string; descKey: string }[] = [
|
|
{ value: 'researcher', labelKey: 'agents.types.researcher', descKey: 'agents.typeDescriptions.researcher' },
|
|
{ value: 'scraper', labelKey: 'agents.types.scraper', descKey: 'agents.typeDescriptions.scraper' },
|
|
{ value: 'monitor', labelKey: 'agents.types.monitor', descKey: 'agents.typeDescriptions.monitor' },
|
|
{ value: 'custom', labelKey: 'agents.types.custom', descKey: 'agents.typeDescriptions.custom' },
|
|
]
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/20 flex justify-end z-50" onClick={onCancel}>
|
|
<div
|
|
className="bg-card shadow-2xl w-full max-w-md h-full overflow-y-auto animate-in slide-in-from-right duration-300 flex flex-col border-l border-border/40"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
{/* Header — editable agent name */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={e => setName(e.target.value)}
|
|
className="text-lg font-semibold text-card-foreground bg-transparent border-none outline-none focus:ring-0 p-0 flex-1 placeholder:text-muted-foreground/40"
|
|
placeholder={t('agents.form.namePlaceholder')}
|
|
required
|
|
/>
|
|
<button onClick={onCancel} className="p-1 rounded-md hover:bg-accent ml-3">
|
|
<X className="w-5 h-5 text-muted-foreground" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-6 space-y-5">
|
|
{/* Agent Type */}
|
|
<div>
|
|
<label className={labelCls2}>{t('agents.form.agentType')}<FieldHelp tooltip={t('agents.help.tooltips.agentType')} /></label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{agentTypes.map(at => (
|
|
<button
|
|
key={at.value}
|
|
type="button"
|
|
onClick={() => setType(at.value)}
|
|
className={`
|
|
text-left px-3 py-2.5 rounded-lg border-2 transition-all text-sm
|
|
${type === at.value
|
|
? 'border-primary bg-primary/5 text-primary font-medium'
|
|
: `${toggleOffBorder} text-muted-foreground`}
|
|
`}
|
|
>
|
|
<div className="font-medium">{t(at.labelKey)}</div>
|
|
<div className="text-xs text-muted-foreground mt-0.5">{t(at.descKey)}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Research Topic (researcher only) — replaces Description for this type */}
|
|
{type === 'researcher' && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.researchTopic')}<FieldHelp tooltip={t('agents.help.tooltips.researchTopic')} /></label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
className={inputCls}
|
|
placeholder={t('agents.form.researchTopicPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description (for non-researcher types) */}
|
|
{type !== 'researcher' && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.description')}<FieldHelp tooltip={t('agents.help.tooltips.description')} /></label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
className={inputCls}
|
|
placeholder={t('agents.form.descriptionPlaceholder')}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* URLs (scraper and custom only — researcher uses search, not URLs) */}
|
|
{(type === 'scraper' || type === 'custom') && (
|
|
<div>
|
|
<label className={labelCls}>
|
|
{t('agents.form.urlsLabel')}<FieldHelp tooltip={t('agents.help.tooltips.urls')} />
|
|
</label>
|
|
<div className="space-y-2">
|
|
{urls.map((url, i) => (
|
|
<div key={i} className="flex gap-2">
|
|
<input
|
|
type="url"
|
|
value={url}
|
|
onChange={e => updateUrl(i, e.target.value)}
|
|
className={inputCls}
|
|
placeholder="https://example.com"
|
|
/>
|
|
{urls.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => removeUrl(i)}
|
|
className="p-2 text-red-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-950 rounded-lg transition-colors"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
<button
|
|
type="button"
|
|
onClick={addUrl}
|
|
className="flex items-center gap-1.5 text-xs text-primary hover:text-primary/80 font-medium"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
{t('agents.form.addUrl')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Source Notebook (monitor only) */}
|
|
{showSourceNotebook && (
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.sourceNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.sourceNotebook')} /></label>
|
|
<select
|
|
value={sourceNotebookId}
|
|
onChange={e => setSourceNotebookId(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="">{t('agents.form.selectNotebook')}</option>
|
|
{notebooks.map(nb => (
|
|
<option key={nb.id} value={nb.id}>
|
|
{nb.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
|
|
{/* Target Notebook */}
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.targetNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.targetNotebook')} /></label>
|
|
<select
|
|
value={targetNotebookId}
|
|
onChange={e => setTargetNotebookId(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="">{t('agents.form.inbox')}</option>
|
|
{notebooks.map(nb => (
|
|
<option key={nb.id} value={nb.id}>
|
|
{nb.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Frequency */}
|
|
<div>
|
|
<label className={labelCls}>{t('agents.form.frequency')}<FieldHelp tooltip={t('agents.help.tooltips.frequency')} /></label>
|
|
<select
|
|
value={frequency}
|
|
onChange={e => setFrequency(e.target.value)}
|
|
className={selectCls}
|
|
>
|
|
<option value="manual">{t('agents.frequencies.manual')}</option>
|
|
<option value="hourly">{t('agents.frequencies.hourly')}</option>
|
|
<option value="daily">{t('agents.frequencies.daily')}</option>
|
|
<option value="weekly">{t('agents.frequencies.weekly')}</option>
|
|
<option value="monthly">{t('agents.frequencies.monthly')}</option>
|
|
</select>
|
|
</div>
|
|
|
|
{/* Schedule config: time + day pickers (hidden for manual/hourly) */}
|
|
{frequency !== 'manual' && frequency !== 'hourly' && (
|
|
<div className="flex gap-3">
|
|
{/* Day selector (weekly/monthly only) */}
|
|
{frequency === 'weekly' && (
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.dayOfWeek')}</label>
|
|
<select
|
|
value={scheduledDay}
|
|
onChange={e => setScheduledDay(Number(e.target.value))}
|
|
className={selectCls}
|
|
>
|
|
{[
|
|
{ value: 0, label: t('agents.schedule.days.mon') },
|
|
{ value: 1, label: t('agents.schedule.days.tue') },
|
|
{ value: 2, label: t('agents.schedule.days.wed') },
|
|
{ value: 3, label: t('agents.schedule.days.thu') },
|
|
{ value: 4, label: t('agents.schedule.days.fri') },
|
|
{ value: 5, label: t('agents.schedule.days.sat') },
|
|
{ value: 6, label: t('agents.schedule.days.sun') },
|
|
].map(d => (
|
|
<option key={d.value} value={d.value}>{d.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
{frequency === 'monthly' && (
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.dayOfMonth')}</label>
|
|
<select
|
|
value={scheduledDay}
|
|
onChange={e => setScheduledDay(Number(e.target.value))}
|
|
className={selectCls}
|
|
>
|
|
{Array.from({ length: 31 }, (_, i) => i + 1).map(d => (
|
|
<option key={d} value={d}>{d}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
{/* Time picker */}
|
|
<div className="flex-1">
|
|
<label className={labelCls}>{t('agents.schedule.time')}</label>
|
|
<input
|
|
type="time"
|
|
value={scheduledTime}
|
|
onChange={e => setScheduledTime(e.target.value)}
|
|
className={inputCls}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Email Notification */}
|
|
<div
|
|
onClick={() => setNotifyEmail(!notifyEmail)}
|
|
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
|
notifyEmail
|
|
? 'border-primary bg-primary/5'
|
|
: toggleOffBorder
|
|
}`}
|
|
>
|
|
<Mail className={`w-4 h-4 flex-shrink-0 ${notifyEmail ? 'text-primary' : toggleOffIcon}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={toggleOffLabel}>{t('agents.form.notifyEmail')}</div>
|
|
<div className={toggleOffHint}>{t('agents.form.notifyEmailHint')}</div>
|
|
</div>
|
|
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${notifyEmail ? 'bg-primary' : 'bg-muted'}`}>
|
|
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${notifyEmail ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Include Images */}
|
|
<div
|
|
onClick={() => setIncludeImages(!includeImages)}
|
|
className={`flex items-center gap-3 p-3 rounded-lg border-2 cursor-pointer transition-all ${
|
|
includeImages
|
|
? 'border-primary bg-primary/5'
|
|
: toggleOffBorder
|
|
}`}
|
|
>
|
|
<ImageIcon className={`w-4 h-4 flex-shrink-0 ${includeImages ? 'text-primary' : toggleOffIcon}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className={toggleOffLabel}>{t('agents.form.includeImages')}</div>
|
|
<div className={toggleOffHint}>{t('agents.form.includeImagesHint')}</div>
|
|
</div>
|
|
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${includeImages ? 'bg-primary' : 'bg-muted'}`}>
|
|
<div className={`w-4 h-4 bg-card rounded-full shadow-sm transition-transform mt-0.5 ${includeImages ? 'translate-x-4.5 ml-0.5' : 'ml-0.5'}`} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Advanced mode toggle */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
|
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground font-medium w-full pt-2 border-t border-border"
|
|
>
|
|
{showAdvanced ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
|
{t('agents.form.advancedMode')}
|
|
</button>
|
|
|
|
{/* Advanced: System Prompt */}
|
|
{showAdvanced && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1">
|
|
{t('agents.form.instructions')}
|
|
<FieldHelp tooltip={t('agents.help.tooltips.instructions')} />
|
|
<span className="text-xs text-muted-foreground font-normal ml-1">({t('agents.form.instructionsHint')})</span>
|
|
</label>
|
|
<textarea
|
|
value={role}
|
|
onChange={e => setRole(e.target.value)}
|
|
rows={3}
|
|
className="w-full px-3 py-2 text-sm border border-input rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary resize-y min-h-[80px] bg-card text-foreground"
|
|
placeholder={t('agents.form.instructionsPlaceholder')}
|
|
/>
|
|
</div>
|
|
|
|
{/* Advanced: Tools */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-2">{t('agents.tools.title')}<FieldHelp tooltip={t('agents.help.tooltips.tools')} /></label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{availableTools.map(at => {
|
|
const Icon = at.icon
|
|
const isSelected = selectedTools.includes(at.id)
|
|
return (
|
|
<button
|
|
key={at.id}
|
|
type="button"
|
|
onClick={() => {
|
|
setSelectedTools(prev =>
|
|
isSelected ? prev.filter(t => t !== at.id) : [...prev, at.id]
|
|
)
|
|
}}
|
|
className={`
|
|
flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition-all text-left
|
|
${isSelected
|
|
? 'border-primary bg-primary/5 text-primary font-medium'
|
|
: `${toggleOffBorder} text-muted-foreground`}
|
|
`}
|
|
>
|
|
<Icon className="w-4 h-4 flex-shrink-0" />
|
|
<span>{t(at.labelKey)}</span>
|
|
{at.external && !isSelected && (
|
|
<span className="ml-auto text-[10px] text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950 px-1.5 py-0.5 rounded-full">{t('agents.tools.configNeeded')}</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
{selectedTools.length > 0 && (
|
|
<p className="text-xs text-muted-foreground mt-1.5">
|
|
{t('agents.tools.selected', { count: selectedTools.length })}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Advanced: Max Steps */}
|
|
{selectedTools.length > 0 && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-foreground mb-1.5">
|
|
{t('agents.tools.maxSteps')}<FieldHelp tooltip={t('agents.help.tooltips.maxSteps')} />
|
|
<span className="text-muted-foreground font-normal ml-1">({maxSteps})</span>
|
|
</label>
|
|
<input
|
|
type="range"
|
|
min={3}
|
|
max={25}
|
|
value={maxSteps}
|
|
onChange={e => setMaxSteps(Number(e.target.value))}
|
|
className="w-full accent-primary"
|
|
/>
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>3</span>
|
|
<span>25</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Actions */}
|
|
<div className="flex items-center justify-end gap-3 pt-6 pb-4 mt-auto border-t border-border/40 bg-card sticky bottom-0">
|
|
<button
|
|
type="button"
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-sm font-medium text-muted-foreground bg-muted rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
{t('agents.form.cancel')}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSaving}
|
|
className="px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 shadow-sm"
|
|
>
|
|
{isSaving ? t('agents.form.saving') : agent ? t('agents.form.save') : t('agents.form.create')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|