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>
  );
};