refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client

This commit is contained in:
Sepehr Ramezani
2026-04-19 19:21:27 +02:00
parent 5296c4da2c
commit 25529a24b8
2476 changed files with 127934 additions and 101962 deletions

View File

@@ -0,0 +1,499 @@
'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-slate-300 hover:text-slate-500 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
} | 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'],
}
// --- 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 [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))
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/30 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
{/* Header — editable agent name */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-100">
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="text-lg font-semibold text-slate-800 bg-transparent border-none outline-none focus:ring-0 p-0 flex-1 placeholder:text-slate-300"
placeholder={t('agents.form.namePlaceholder')}
required
/>
<button onClick={onCancel} className="p-1 rounded-md hover:bg-slate-100 ml-3">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-5">
{/* Agent Type */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">{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'
: 'border-slate-200 text-slate-600 hover:border-slate-300'}
`}
>
<div className="font-medium">{t(at.labelKey)}</div>
<div className="text-xs text-slate-400 mt-0.5">{t(at.descKey)}</div>
</button>
))}
</div>
</div>
{/* Research Topic (researcher only) — replaces Description for this type */}
{type === 'researcher' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('agents.form.researchTopic')}<FieldHelp tooltip={t('agents.help.tooltips.researchTopic')} /></label>
<input
type="text"
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder={t('agents.form.researchTopicPlaceholder')}
/>
</div>
)}
{/* Description (for non-researcher types) */}
{type !== 'researcher' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('agents.form.description')}<FieldHelp tooltip={t('agents.help.tooltips.description')} /></label>
<input
type="text"
value={description}
onChange={e => setDescription(e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
placeholder={t('agents.form.descriptionPlaceholder')}
/>
</div>
)}
{/* URLs (scraper and custom only — researcher uses search, not URLs) */}
{(type === 'scraper' || type === 'custom') && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">
{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="flex-1 px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
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 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="block text-sm font-medium text-slate-700 mb-1.5">{t('agents.form.sourceNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.sourceNotebook')} /></label>
<select
value={sourceNotebookId}
onChange={e => setSourceNotebookId(e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white"
>
<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="block text-sm font-medium text-slate-700 mb-1.5">{t('agents.form.targetNotebook')}<FieldHelp tooltip={t('agents.help.tooltips.targetNotebook')} /></label>
<select
value={targetNotebookId}
onChange={e => setTargetNotebookId(e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white"
>
<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="block text-sm font-medium text-slate-700 mb-1.5">{t('agents.form.frequency')}<FieldHelp tooltip={t('agents.help.tooltips.frequency')} /></label>
<select
value={frequency}
onChange={e => setFrequency(e.target.value)}
className="w-full px-3 py-2 text-sm border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary bg-white"
>
<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>
{/* 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'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<Mail className={`w-4 h-4 flex-shrink-0 ${notifyEmail ? 'text-primary' : 'text-slate-400'}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-700">{t('agents.form.notifyEmail')}</div>
<div className="text-xs text-slate-400">{t('agents.form.notifyEmailHint')}</div>
</div>
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${notifyEmail ? 'bg-primary' : 'bg-slate-200'}`}>
<div className={`w-4 h-4 bg-white 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'
: 'border-slate-200 hover:border-slate-300'
}`}
>
<ImageIcon className={`w-4 h-4 flex-shrink-0 ${includeImages ? 'text-primary' : 'text-slate-400'}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-slate-700">{t('agents.form.includeImages')}</div>
<div className="text-xs text-slate-400">{t('agents.form.includeImagesHint')}</div>
</div>
<div className={`w-9 h-5 rounded-full transition-colors flex-shrink-0 ${includeImages ? 'bg-primary' : 'bg-slate-200'}`}>
<div className={`w-4 h-4 bg-white 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-slate-500 hover:text-slate-700 font-medium w-full pt-2 border-t border-slate-100"
>
{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-slate-700 mb-1">
{t('agents.form.instructions')}
<FieldHelp tooltip={t('agents.help.tooltips.instructions')} />
<span className="text-xs text-slate-400 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-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary resize-y min-h-[80px]"
placeholder={t('agents.form.instructionsPlaceholder')}
/>
</div>
{/* Advanced: Tools */}
<div>
<label className="block text-sm font-medium text-slate-700 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'
: 'border-slate-200 text-slate-600 hover:border-slate-300'}
`}
>
<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-500 bg-amber-50 px-1.5 py-0.5 rounded-full">{t('agents.tools.configNeeded')}</span>
)}
</button>
)
})}
</div>
{selectedTools.length > 0 && (
<p className="text-xs text-slate-400 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-slate-700 mb-1.5">
{t('agents.tools.maxSteps')}<FieldHelp tooltip={t('agents.help.tooltips.maxSteps')} />
<span className="text-slate-400 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-slate-400">
<span>3</span>
<span>25</span>
</div>
</div>
)}
</>
)}
{/* Actions */}
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-slate-600 bg-slate-100 rounded-lg hover:bg-slate-200 transition-colors"
>
{t('agents.form.cancel')}
</button>
<button
type="submit"
disabled={isSaving}
className="px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50"
>
{isSaving ? t('agents.form.saving') : agent ? t('agents.form.save') : t('agents.form.create')}
</button>
</div>
</form>
</div>
</div>
)
}

View File

@@ -0,0 +1,96 @@
'use client'
/**
* Agent Help Modal
* Rich contextual help guide for the Agents page.
* Collapsible sections with Markdown content inside each.
*/
import { X, LifeBuoy } from 'lucide-react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { useLanguage } from '@/lib/i18n'
interface AgentHelpProps {
onClose: () => void
}
const SECTIONS = [
{ key: 'whatIsAgent', defaultOpen: true },
{ key: 'howToUse', defaultOpen: false },
{ key: 'types', defaultOpen: false },
{ key: 'advanced', defaultOpen: false },
{ key: 'tools', defaultOpen: false },
{ key: 'frequency', defaultOpen: false },
{ key: 'targetNotebook', defaultOpen: false },
{ key: 'templates', defaultOpen: false },
{ key: 'tips', defaultOpen: false },
] as const
export function AgentHelp({ onClose }: AgentHelpProps) {
const { t } = useLanguage()
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl w-full max-w-3xl max-h-[85vh] flex flex-col mx-4">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 dark:border-slate-700 shrink-0">
<div className="flex items-center gap-2.5">
<LifeBuoy className="w-5 h-5 text-primary" />
<h2 className="text-lg font-semibold">{t('agents.help.title')}</h2>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content — collapsible sections */}
<div className="flex-1 overflow-y-auto px-6 py-2">
{SECTIONS.map(section => (
<details
key={section.key}
open={section.defaultOpen}
className="group border-b border-slate-100 dark:border-slate-800 last:border-b-0"
>
<summary className="flex items-center gap-2 cursor-pointer py-3 font-medium text-slate-800 dark:text-slate-200 select-none hover:text-primary transition-colors text-sm">
<span className="text-primary text-xs transition-transform group-open:rotate-90">&#9656;</span>
{t(`agents.help.${section.key}`)}
</summary>
<div className="pb-4 pl-5 prose prose-slate dark:prose-invert prose-sm max-w-none
prose-headings:font-semibold prose-headings:text-slate-800 dark:prose-headings:text-slate-200
prose-h3:text-sm prose-h3:mt-3 prose-h3:mb-1
prose-p:leading-relaxed prose-p:text-slate-600 dark:prose-p:text-slate-400 prose-p:my-1.5
prose-li:text-slate-600 dark:prose-li:text-slate-400 prose-li:my-0.5
prose-strong:text-slate-700 dark:prose-strong:text-slate-300
prose-code:text-primary prose-code:bg-primary/5 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-xs prose-code:before:content-none prose-code:after:content-none
prose-ul:my-2 prose-ol:my-2
prose-hr:border-slate-200 dark:prose-hr:border-slate-700
prose-table:text-xs
prose-th:text-left prose-th:font-medium prose-th:text-slate-700 dark:prose-th:text-slate-300 prose-th:py-1 prose-th:pr-3
prose-td:text-slate-600 dark:prose-td:text-slate-400 prose-td:py-1 prose-td:pr-3
prose-blockquote:border-primary/30 prose-blockquote:text-slate-500 dark:prose-blockquote:text-slate-400
">
<Markdown remarkPlugins={[remarkGfm]}>
{t(`agents.help.${section.key}Content`)}
</Markdown>
</div>
</details>
))}
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-200 dark:border-slate-700 shrink-0">
<button
onClick={onClose}
className="w-full px-4 py-2.5 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
>
{t('agents.help.close')}
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,170 @@
'use client'
/**
* Agent Run Log
* Shows execution history for an agent.
*/
import { useState, useEffect } from 'react'
import { X, CheckCircle2, XCircle, Loader2, Clock, ChevronDown, Wrench } from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import { fr } from 'date-fns/locale/fr'
import { enUS } from 'date-fns/locale/en-US'
import { useLanguage } from '@/lib/i18n'
interface AgentRunLogProps {
agentId: string
agentName: string
onClose: () => void
}
interface Action {
id: string
status: string
result?: string | null
log?: string | null
input?: string | null
toolLog?: string | null
tokensUsed?: number | null
createdAt: string | Date
}
interface ToolLogStep {
step: number
text?: string
toolCalls?: Array<{ toolName: string; args: any }>
toolResults?: Array<{ toolName: string; preview?: string }>
}
const statusKeys: Record<string, string> = {
success: 'agents.status.success',
failure: 'agents.status.failure',
running: 'agents.status.running',
pending: 'agents.status.pending',
}
export function AgentRunLog({ agentId, agentName, onClose }: AgentRunLogProps) {
const { t, language } = useLanguage()
const [actions, setActions] = useState<Action[]>([])
const [loading, setLoading] = useState(true)
const dateLocale = language === 'fr' ? fr : enUS
useEffect(() => {
async function load() {
try {
const { getAgentActions } = await import('@/app/actions/agent-actions')
const data = await getAgentActions(agentId)
setActions(data)
} catch {
// Silent fail
} finally {
setLoading(false)
}
}
load()
}, [agentId])
return (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-xl max-w-md w-full max-h-[70vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
<div>
<h3 className="font-semibold text-slate-800">{t('agents.runLog.title')}</h3>
<p className="text-xs text-slate-400">{agentName}</p>
</div>
<button onClick={onClose} className="p-1 rounded-md hover:bg-slate-100">
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
{/* List */}
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{loading && (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-5 h-5 animate-spin text-slate-400" />
</div>
)}
{!loading && actions.length === 0 && (
<p className="text-center text-sm text-slate-400 py-8">
{t('agents.runLog.noHistory')}
</p>
)}
{actions.map(action => {
let toolSteps: ToolLogStep[] = []
try {
toolSteps = action.toolLog ? JSON.parse(action.toolLog) : []
} catch {}
return (
<div
key={action.id}
className={`
p-3 rounded-lg border
${action.status === 'success' ? 'bg-green-50/50 border-green-100' : ''}
${action.status === 'failure' ? 'bg-red-50/50 border-red-100' : ''}
${action.status === 'running' ? 'bg-blue-50/50 border-blue-100' : ''}
${action.status === 'pending' ? 'bg-slate-50 border-slate-100' : ''}
`}
>
<div className="flex items-start gap-3">
<div className="mt-0.5">
{action.status === 'success' && <CheckCircle2 className="w-4 h-4 text-green-500" />}
{action.status === 'failure' && <XCircle className="w-4 h-4 text-red-500" />}
{action.status === 'running' && <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />}
{action.status === 'pending' && <Clock className="w-4 h-4 text-slate-400" />}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">
{t(statusKeys[action.status] || action.status)}
</span>
<span className="text-xs text-slate-400">
{formatDistanceToNow(new Date(action.createdAt), { addSuffix: true, locale: dateLocale })}
</span>
</div>
{action.log && (
<p className="text-xs text-slate-500 mt-1 line-clamp-2">{action.log}</p>
)}
</div>
</div>
{/* Tool trace */}
{toolSteps.length > 0 && (
<details className="mt-2">
<summary className="flex items-center gap-1.5 text-xs text-primary cursor-pointer hover:text-primary/80 font-medium">
<Wrench className="w-3 h-3" />
{t('agents.runLog.toolTrace', { count: toolSteps.length })}
<ChevronDown className="w-3 h-3" />
</summary>
<div className="mt-2 space-y-2 pl-2">
{toolSteps.map((step, i) => (
<div key={i} className="text-xs border-l-2 border-primary/30 pl-2 py-1">
<span className="font-medium text-slate-600">{t('agents.runLog.step', { num: step.step })}</span>
{step.toolCalls && step.toolCalls.length > 0 && (
<div className="mt-1 space-y-1">
{step.toolCalls.map((tc, j) => (
<div key={j} className="bg-slate-100 rounded px-2 py-1">
<span className="font-mono text-primary">{tc.toolName}</span>
<span className="text-slate-400 ml-1">
{JSON.stringify(tc.args).substring(0, 80)}
</span>
</div>
))}
</div>
)}
</div>
))}
</div>
</details>
)}
</div>
)
})}
</div>
</div>
</div>
)
}