import { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { AlertCircle, ChevronDown, CircleHelp, LoaderCircle, Play, RotateCcw, } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { eventGroupLabel, eventPatternDescription, eventPatternLabel, groupEventPatterns, } from '@/app/home/components/event-patterns/event-pattern-groups'; import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent'; interface AgentDebugPanelProps { agentId: string; availableEventTypes: string[]; supportedEventPatterns?: string[]; beforeRun?: () => Promise; hasUnsavedChanges?: boolean; onOpenRunnerConfig?: () => void; } interface DebugEntry { id: string; direction: 'input' | 'output' | 'error'; eventType: string; text: string; errorCode?: string; detail?: string; } const EVENT_PRESET_DATA: Record< string, { text: string; data: Record } > = { 'message.received': { text: '', data: {}, }, 'group.member_joined': { text: 'A new member joined the group.', data: { group_id: 'debug-group', member_id: 'debug-user', member_name: 'Debug User', }, }, 'group.member_left': { text: 'A member left the group.', data: { group_id: 'debug-group', member_id: 'debug-user', member_name: 'Debug User', }, }, 'friend.request_received': { text: 'A user sent a friend request.', data: { requester_id: 'debug-user', requester_name: 'Debug User', message: 'Hello', }, }, 'feedback.received': { text: 'The user submitted feedback.', data: { rating: 5, content: 'Debug feedback', }, }, }; function createDebugSessionId(agentId: string) { const nonce = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); return `webui:${agentId}:${nonce}`; } function matchesEventPattern(pattern: string, eventType: string) { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); return new RegExp(`^${escaped.replaceAll('*', '.*')}$`).test(eventType); } export default function AgentDebugPanel({ agentId, availableEventTypes, supportedEventPatterns = ['*'], beforeRun, hasUnsavedChanges = false, onOpenRunnerConfig, }: AgentDebugPanelProps) { const { t } = useTranslation(); const [preset, setPreset] = useState('message.received'); const [customEventType, setCustomEventType] = useState('custom.event'); const [inputText, setInputText] = useState(''); const [eventDataText, setEventDataText] = useState('{}'); const [running, setRunning] = useState(false); const [entries, setEntries] = useState([]); const sessionIdRef = useRef(createDebugSessionId(agentId)); const eventType = preset === 'custom' ? customEventType.trim() : preset; const isMessageEvent = eventType.startsWith('message.'); const supportedLabel = useMemo( () => supportedEventPatterns.join(', '), [supportedEventPatterns], ); const availableEvents = useMemo(() => { const concretePatterns = supportedEventPatterns.filter( (pattern) => pattern !== '*' && !pattern.endsWith('.*'), ); return Array.from(new Set([...availableEventTypes, ...concretePatterns])) .filter((candidate) => supportedEventPatterns.some((pattern) => matchesEventPattern(pattern, candidate), ), ) .sort(); }, [availableEventTypes, supportedEventPatterns]); const eventGroups = useMemo( () => groupEventPatterns(availableEvents), [availableEvents], ); const supportsCustomEvent = supportedEventPatterns.some( (pattern) => pattern === '*' || pattern.endsWith('.*'), ); useEffect(() => { if ( availableEvents.includes(preset) || (preset === 'custom' && supportsCustomEvent) ) { return; } selectPreset(availableEvents[0] ?? 'custom'); }, [availableEvents, preset, supportsCustomEvent]); function selectPreset(value: string) { setPreset(value); const nextPreset = EVENT_PRESET_DATA[value] ?? { text: '', data: {} }; setInputText(nextPreset.text); setEventDataText(JSON.stringify(nextPreset.data, null, 2)); } function resetSession() { sessionIdRef.current = createDebugSessionId(agentId); setEntries([]); } async function runDebugEvent() { if (!eventType) { toast.error(t('agents.debugEventTypeRequired')); return; } if (isMessageEvent && !inputText.trim()) { toast.error(t('agents.debugInputRequired')); return; } if ( !supportedEventPatterns.some((pattern) => matchesEventPattern(pattern, eventType), ) ) { toast.error(t('agents.debugUnsupportedEvent')); return; } let eventData: Record; try { const parsed = JSON.parse(eventDataText || '{}'); if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { throw new Error('payload must be an object'); } eventData = parsed as Record; } catch { toast.error(t('agents.debugInvalidPayload')); return; } setRunning(true); if (hasUnsavedChanges && beforeRun && !(await beforeRun())) { setRunning(false); return; } const requestId = globalThis.crypto?.randomUUID?.() ?? String(Date.now()); setEntries((current) => [ ...current, { id: `input:${requestId}`, direction: 'input', eventType, text: inputText.trim() || JSON.stringify(eventData, null, 2), }, ]); try { const result = await httpClient.debugAgent(agentId, { event_type: eventType, text: inputText.trim(), data: eventData, conversation_id: sessionIdRef.current, }); setEntries((current) => [ ...current, { id: `output:${result.event_id}`, direction: 'output', eventType, text: result.final_text || t('agents.debugNoTextOutput'), }, ]); if (isMessageEvent) setInputText(''); } catch (error) { const errorCode = typeof error === 'object' && error && 'code' in error ? String((error as { code?: string }).code || '') : ''; const message = typeof error === 'object' && error && 'msg' in error ? String((error as { msg?: string }).msg || '') : t('agents.debugRunFailed'); const isConfigError = errorCode.endsWith('.config_invalid'); const isExecutionError = errorCode === 'runner_execution_failed'; const isTimeout = errorCode === 'runner.timeout'; const friendlyMessage = isConfigError ? t('agents.debugRunnerConfigInvalidDescription', { message: message === 'api-key is required' ? t('agents.debugApiKeyRequired') : message, }) : isExecutionError ? t('agents.debugRunnerExecutionFailedDescription') : isTimeout ? t('agents.debugRunnerTimeoutDescription') : message || t('agents.debugRunFailed'); setEntries((current) => [ ...current, { id: `error:${requestId}`, direction: 'error', eventType, text: friendlyMessage, errorCode, detail: isExecutionError || isTimeout ? message || t('agents.debugRunFailed') : undefined, }, ]); } finally { setRunning(false); } } return (

{t('agents.debugTranscript')}

{t('agents.debugTranscriptDescription')}

{entries.length === 0 ? ( {t('agents.debugEmptyTitle')} {t('agents.debugEmptyTranscript')} ) : (
{entries.map((entry) => ( {entry.direction === 'error' && }
{entry.eventType} {entry.direction === 'output' ? t('agents.debugAgentOutput') : entry.direction === 'error' ? t('common.error') : t('agents.debugTestInput')}
                  {entry.text}
                
{entry.detail && (
                        {entry.detail}
                      
)} {(entry.errorCode?.endsWith('.config_invalid') || entry.errorCode === 'runner_execution_failed' || entry.errorCode === 'runner.timeout') && onOpenRunnerConfig && ( )}
))}
)}
{preset === 'custom' && (
setCustomEventType(event.target.value)} placeholder="custom.event" />
)}