AOPDebuggerTab.tsx9.4 KBView on GitHub import { Check, ChevronDown, ChevronRight, Copy, Loader2, Search } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { useAOPs } from '@/modules/aop/hooks/use-aops';
import React, { useState, useMemo } from 'react';
import { cn } from '@/styles/stylingUtils';
import { JsonTreeView } from './JsonTreeView';
interface AOPTabProps {
onCopy: (text: string, id: string) => void;
copiedId: string | null;
}
export const AOPDebuggerTab: React.FC<AOPTabProps> = ({ onCopy, copiedId }) => {
const { data, isLoading, error } = useAOPs();
const [expandedAops, setExpandedAops] = useState<Set<string>>(new Set());
const [searchQuery, setSearchQuery] = useState('');
const aops = data?.aops ?? [];
const filteredAops = useMemo(() => {
if (!searchQuery) return aops;
const query = searchQuery.toLowerCase();
return aops.filter((aop) => {
if (aop.name?.toLowerCase().includes(query)) return true;
if (aop.id?.toLowerCase().includes(query)) return true;
if (aop.selectionProcedure?.toLowerCase().includes(query)) return true;
return false;
});
}, [aops, searchQuery]);
const toggleAop = (aopId: string) => {
setExpandedAops((prev) => {
const next = new Set(prev);
if (next.has(aopId)) {
next.delete(aopId);
} else {
next.add(aopId);
}
return next;
});
};
const safeStringify = (obj: unknown, indent = 2): string => {
try {
return JSON.stringify(obj, null, indent);
} catch (error) {
return `[Error serializing: ${error instanceof Error ? error.message : 'Unknown'}]`;
}
};
if (isLoading) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-gray-400" />
<span className="ml-2 text-xs text-gray-500">Loading AOPs...</span>
</div>
);
}
if (error) {
return (
<div className="flex h-full items-center justify-center p-4">
<div className="rounded-md bg-red-50 p-3 text-xs text-red-700 dark:bg-red-900/20 dark:text-red-400">
Failed to load AOPs: {error.message}
</div>
</div>
);
}
return (
<div className="flex h-full flex-col">
{/* Search Bar */}
<div className="border-b border-gray-200 p-2 dark:border-gray-700">
<div className="relative">
<Search className="absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="Search AOPs by name, ID, or procedure..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full rounded border border-gray-300 bg-white py-1 pl-7 pr-2 text-xs placeholder:text-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
{/* Content */}
<div className="flex-1 space-y-2 overflow-y-auto p-2">
{filteredAops.length === 0 ? (
<div className="py-4 text-center text-xs text-gray-500">
{searchQuery ? 'No AOPs match your search' : 'No AOPs found'}
</div>
) : (
filteredAops.map((aop) => {
const isExpanded = expandedAops.has(aop.id);
return (
<div
key=[redacted]
className="rounded-lg border border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-950"
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-2 hover:bg-gray-100 dark:hover:bg-gray-900/80',
isExpanded ? 'rounded-t-lg' : 'rounded-lg',
)}
onClick={() => toggleAop(aop.id)}
>
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="h-3 w-3 shrink-0" />
) : (
<ChevronRight className="h-3 w-3 shrink-0" />
)}
<span className="text-xs font-medium">{aop.name}</span>
{aop.color && (
<div
className="h-3 w-3 shrink-0 rounded-full"
style={{ backgroundColor: aop.color }}
/>
)}
</div>
<span className="ml-5 font-mono text-[10px] text-gray-500 dark:text-gray-400">
{aop.id}
</span>
</div>
<div className="flex items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(aop), aop.id);
}}
className="rounded p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700"
>
{copiedId === aop.id ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
className="overflow-hidden"
>
<div
className="space-y-2 border-t border-gray-200 p-2 dark:border-gray-700"
onClick={(e) => e.stopPropagation()}
>
{/* Selection Procedure */}
<div>
<div className="mb-1 text-[10px] font-semibold text-gray-600 dark:text-gray-400">
Selection Procedure
</div>
<pre className="max-h-32 cursor-text select-text overflow-auto rounded bg-white p-1.5 text-[10px] dark:bg-gray-800">
{aop.selectionProcedure}
</pre>
</div>
{/* Agent Operating Procedure */}
<div>
<div className="mb-1 text-[10px] font-semibold text-gray-600 dark:text-gray-400">
Agent Operating Procedure
</div>
<pre className="max-h-48 cursor-text select-text overflow-auto rounded bg-white p-1.5 text-[10px] whitespace-pre-wrap dark:bg-gray-800">
Content now stored in system skill documents
</pre>
</div>
{/* Custom Field Definitions */}
{aop.customFieldDefinitions && (
<div>
<div className="mb-1 text-[10px] font-semibold text-gray-600 dark:text-gray-400">
Custom Field Definitions
</div>
<JsonTreeView data={aop.customFieldDefinitions} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-32" />
</div>
)}
{/* Conversation Field Definitions */}
{aop.conversationFieldDefinitions && (
<div>
<div className="mb-1 text-[10px] font-semibold text-gray-600 dark:text-gray-400">
Conversation Field Definitions
</div>
<JsonTreeView data={aop.conversationFieldDefinitions} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-32" />
</div>
)}
{/* Full Raw JSON */}
<div>
<div className="mb-1 text-[10px] font-semibold text-gray-600 dark:text-gray-400">
Full JSON
</div>
<JsonTreeView data={aop} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={1} className="max-h-64" />
</div>
{/* Timestamps */}
<div className="flex gap-4 text-[10px] text-gray-500 dark:text-gray-400">
<span>Created: {new Date(aop.createdAt).toLocaleString()}</span>
<span>Updated: {new Date(aop.updatedAt).toLocaleString()}</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})
)}
</div>
{/* Footer */}
<div className="border-t border-gray-200 p-2 text-center text-xs text-gray-500 dark:border-gray-700 dark:text-gray-400">
{searchQuery
? `${filteredAops.length} of ${aops.length} AOPs`
: `${aops.length} AOP${aops.length !== 1 ? 's' : ''}`}
</div>
</div>
);
};