AgentExecutionsDebuggerTab.tsx12.8 KBView on GitHub
import { Check, Copy, ChevronRight, GitBranch, Search } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import React, { useState, useMemo } from 'react';
import { Link } from 'react-router';
import { cn } from '@/styles/stylingUtils';
import { JsonTreeView } from './JsonTreeView';

interface AgentExecutionsTabProps {
  executions: Record<string, any>;
  onCopy: (text: string, id: string) => void;
  copiedId: string | null;
}

export const AgentExecutionsDebuggerTab: React.FC<AgentExecutionsTabProps> = ({
  executions,
  onCopy,
  copiedId,
}) => {
  const [expandedExecutions, setExpandedExecutions] = useState<Set<string>>(new Set());
  const [searchQuery, setSearchQuery] = useState('');

  const executionEntries = useMemo(() => Object.entries(executions), [executions]);

  const filteredExecutions = useMemo(() => {
    if (!searchQuery) return executionEntries;

    const query = searchQuery.toLowerCase();
    return executionEntries.filter(([runId, execution]) => {
      if (runId.toLowerCase().includes(query)) return true;
      if (execution?.data?.conversationId?.toLowerCase().includes(query)) return true;
      if (execution?.data?.threadId?.toLowerCase().includes(query)) return true;
      if (execution?.data?.aopName?.toLowerCase().includes(query)) return true;
      if (execution?.data?.status?.toLowerCase().includes(query)) return true;
      try {
        const jsonString = JSON.stringify(execution).toLowerCase();
        if (jsonString.includes(query)) return true;
      } catch {}
      return false;
    });
  }, [executionEntries, searchQuery]);

  const toggleExecution = (runId: string) => {
    setExpandedExecutions((prev) => {
      const next = new Set(prev);
      if (next.has(runId)) {
        next.delete(runId);
      } else {
        next.add(runId);
      }
      return next;
    });
  };

  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 executions..."
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            className="w-full rounded border border-gray-200 py-1 pl-7 pr-2 text-xs focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800"
          />
        </div>
      </div>

      {/* Content Area */}
      <div className="flex-1 overflow-y-auto">
        {filteredExecutions.length === 0 ? (
          <div className="flex h-full items-center justify-center">
            <div className="text-center text-gray-500">
              <p className="text-xs">
                {searchQuery ? 'No matching executions found' : 'No agent executions loaded'}
              </p>
            </div>
          </div>
        ) : (
          <div className="space-y-1 p-2">
            {filteredExecutions.map(([runId, executionWithMeta]) => {
              const execution = executionWithMeta?.data;
              const isExpanded = expandedExecutions.has(runId);

              return (
                <div
                  key=[redacted]
                  className="overflow-hidden rounded border border-gray-200 dark:border-gray-700"
                >
                  {/* Header */}
                  <button
                    onClick={() => toggleExecution(runId)}
                    className="flex w-full items-center justify-between bg-gray-50 px-2 py-1 text-left transition-colors hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700"
                  >
                    <div className="flex items-center gap-2">
                      <motion.div
                        initial={false}
                        animate={{ rotate: isExpanded ? 90 : 0 }}
                        transition={{ duration: 0.2 }}
                      >
                        <ChevronRight className="h-3 w-3 text-gray-500" />
                      </motion.div>
                      <span className="font-mono text-xs text-gray-700 dark:text-gray-300">
                        {execution?.aopName || 'Unknown AOP'}
                      </span>
                      <span
                        className={cn(
                          'rounded px-1.5 py-0.5 text-xs font-medium',
                          execution?.status === 'completed' || execution?.status === 'final'
                            ? 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300'
                            : execution?.status === 'failed'
                              ? 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300'
                              : execution?.status === 'pending'
                                ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300'
                                : 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
                        )}
                      >
                        {execution?.status || 'unknown'}
                      </span>
                    </div>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        onCopy(runId, runId);
                      }}
                      className="rounded p-1 transition-colors hover:bg-gray-200 dark:hover:bg-gray-600"
                      title="Copy RunId"
                    >
                      {copiedId === runId ? (
                        <Check className="h-3 w-3 text-green-500" />
                      ) : (
                        <Copy className="h-3 w-3 text-gray-500" />
                      )}
                    </button>
                  </button>

                  {/* Expanded Content */}
                  <AnimatePresence>
                    {isExpanded && (
                      <motion.div
                        initial={{ height: 0, opacity: 0 }}
                        animate={{ height: 'auto', opacity: 1 }}
                        exit={{ height: 0, opacity: 0 }}
                        transition={{ duration: 0.2 }}
                      >
                        <div className="border-t border-gray-200 bg-white p-2 dark:border-gray-700 dark:bg-gray-900">
                          {/* Quick Info */}
                          <div className="mb-2 space-y-1 text-xs">
                            <div className="flex items-start gap-2">
                              <span className="font-medium text-gray-500 dark:text-gray-400">
                                Run ID:
                              </span>
                              <button
                                onClick={() => onCopy(runId, `${runId}-runid`)}
                                className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
                              >
                                <span className="font-mono text-gray-700 dark:text-gray-300">
                                  {runId}
                                </span>
                                {copiedId === `${runId}-runid` ? (
                                  <Check className="h-3 w-3 text-green-500" />
                                ) : (
                                  <Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
                                )}
                              </button>
                            </div>

                            <div className="flex items-start gap-2">
                              <span className="font-medium text-gray-500 dark:text-gray-400">
                                Trace:
                              </span>
                              <Link
                                to={`/settings/agentExecutions/${runId}`}
                                className="flex cursor-pointer items-center gap-1 rounded px-1 text-blue-600 transition-colors hover:bg-gray-100 hover:underline dark:text-blue-400 dark:hover:bg-gray-800"
                              >
                                <GitBranch className="h-3 w-3" />
                                Open waterfall
                              </Link>
                            </div>

                            {execution?.conversationId && (
                              <div className="flex items-start gap-2">
                                <span className="font-medium text-gray-500 dark:text-gray-400">
                                  Conversation:
                                </span>
                                <button
                                  onClick={() =>
                                    onCopy(execution.conversationId, `${runId}-convoid`)
                                  }
                                  className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
                                >
                                  <span className="font-mono text-purple-600 dark:text-purple-400">
                                    {execution.conversationId.slice(0, 20)}...
                                  </span>
                                  {copiedId === `${runId}-convoid` ? (
                                    <Check className="h-3 w-3 text-green-500" />
                                  ) : (
                                    <Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
                                  )}
                                </button>
                              </div>
                            )}

                            {execution?.threadId && (
                              <div className="flex items-start gap-2">
                                <span className="font-medium text-gray-500 dark:text-gray-400">
                                  Thread:
                                </span>
                                <button
                                  onClick={() => onCopy(execution.threadId, `${runId}-threadid`)}
                                  className="group flex items-center gap-1 rounded px-1 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
                                >
                                  <span className="font-mono text-blue-600 dark:text-blue-400">
                                    {execution.threadId.slice(0, 20)}...
                                  </span>
                                  {copiedId === `${runId}-threadid` ? (
                                    <Check className="h-3 w-3 text-green-500" />
                                  ) : (
                                    <Copy className="h-3 w-3 text-gray-400 opacity-0 group-hover:opacity-100" />
                                  )}
                                </button>
                              </div>
                            )}

                            {execution?.summary && (
                              <div className="flex flex-col gap-1">
                                <span className="font-medium text-gray-500 dark:text-gray-400">
                                  Summary:
                                </span>
                                <span className="text-gray-700 dark:text-gray-300">
                                  {execution.summary}
                                </span>
                              </div>
                            )}

                            <div className="flex items-start gap-2">
                              <span className="font-medium text-gray-500 dark:text-gray-400">
                                Created:
                              </span>
                              <span className="text-gray-700 dark:text-gray-300">
                                {execution?.createdAt
                                  ? new Date(execution.createdAt).toLocaleString()
                                  : 'N/A'}
                              </span>
                            </div>
                          </div>

                          {/* Full JSON */}
                          <details className="mt-2">
                            <summary className="cursor-pointer text-xs font-medium text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200">
                              View Full Data
                            </summary>
                            <div className="mt-1">
                              <JsonTreeView data={executionWithMeta} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-60" />
                            </div>
                          </details>
                        </div>
                      </motion.div>
                    )}
                  </AnimatePresence>
                </div>
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
};