UXDebuggerTab.tsx8.6 KBView on GitHub
import type { CommandItem, NavigationState, SidePanelMode, ViewType } from '@/modules/ux/uxSlice';
import { Check, Copy, ChevronDown, ChevronRight } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
import { cn } from '@/styles/stylingUtils';
import React, { useState } from 'react';
import { JsonTreeView } from './JsonTreeView';

interface UXTabProps {
  sidePanelMode: SidePanelMode;
  navigation: NavigationState;
  chatThreadId: string;
  activeCanvasId: string | null;
  isGlobalCanvasOpen: boolean;
  activeView: ViewType | null;
  commandStack: CommandItem[];
  commandModalItemId: string | null;
  onCopy: (text: string, id: string) => void;
  copiedId: string | null;
}

export const UXDebuggerTab: React.FC<UXTabProps> = ({
  sidePanelMode,
  navigation,
  chatThreadId,
  activeCanvasId,
  isGlobalCanvasOpen,
  activeView,
  commandStack,
  commandModalItemId,
  onCopy,
  copiedId,
}) => {
  const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['overview']));

  const toggleSection = (sectionId: string) => {
    setExpandedSections((prev) => {
      const next = new Set(prev);
      if (next.has(sectionId)) {
        next.delete(sectionId);
      } else {
        next.add(sectionId);
      }
      return next;
    });
  };

  const safeStringify = (obj: unknown, indent = 2): string => {
    try {
      return JSON.stringify(obj, null, indent);
    } catch (error) {
      return `[Error serializing object: ${error instanceof Error ? error.message : 'Unknown error'}]`;
    }
  };

  // Compact row component for simple values
  const renderCompactRow = (
    id: string,
    label: string,
    value: string | null | undefined,
    color: string = 'blue',
  ) => {
    const hasValue = !!value;

    const colorClasses: Record<string, string> = {
      blue: 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800',
      purple: 'bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800',
      green: 'bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800',
      amber: 'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800',
      cyan: 'bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800',
    };

    return (
      <div
        onClick={() => hasValue && onCopy(value || '', id)}
        className={cn(
          'flex cursor-pointer items-center justify-between gap-2 rounded-lg border p-2 transition-colors',
          hasValue && 'hover:bg-white/50 dark:hover:bg-black/20',
          colorClasses[color] || colorClasses.blue,
        )}
      >
        <div className="flex min-w-0 flex-1 items-center gap-2">
          <span className="shrink-0 text-xs font-medium text-gray-700 dark:text-gray-300">
            {label}:
          </span>
          {hasValue ? (
            <span className="min-w-0 truncate font-mono text-xs text-gray-900 dark:text-gray-100">
              {value}
            </span>
          ) : (
            <span className="text-xs text-gray-500 dark:text-gray-400">No data</span>
          )}
        </div>
        {hasValue && (
          <div className="shrink-0 rounded p-0.5">
            {copiedId === id ? (
              <Check className="h-3 w-3 text-green-600" />
            ) : (
              <Copy className="h-3 w-3" />
            )}
          </div>
        )}
      </div>
    );
  };

  const renderSection = (
    id: string,
    title: string,
    content: string | null | undefined,
    color: string = 'blue',
    parsedData?: unknown,
  ) => {
    const isExpanded = expandedSections.has(id);
    const hasContent = !!content;

    const colorClasses: Record<string, string> = {
      blue: 'bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/80',
      purple:
        'bg-purple-50 dark:bg-purple-950 border-purple-200 dark:border-purple-800 hover:bg-purple-100 dark:hover:bg-purple-900/80',
      green:
        'bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/80',
      gray: 'bg-gray-50 dark:bg-gray-950 border-gray-200 dark:border-gray-800 hover:bg-gray-100 dark:hover:bg-gray-900/80',
      amber:
        'bg-amber-50 dark:bg-amber-950 border-amber-200 dark:border-amber-800 hover:bg-amber-100 dark:hover:bg-amber-900/80',
      cyan: 'bg-cyan-50 dark:bg-cyan-950 border-cyan-200 dark:border-cyan-800 hover:bg-cyan-100 dark:hover:bg-cyan-900/80',
    };

    return (
      <div
        className={cn('rounded-lg border transition-all', colorClasses[color] || colorClasses.blue)}
      >
        <div
          className={cn(
            'flex cursor-pointer items-center justify-between p-2 transition-colors',
            isExpanded ? 'rounded-t-lg' : 'rounded-lg',
          )}
          onClick={() => toggleSection(id)}
        >
          <div className="flex flex-1 items-center gap-1.5">
            <span className="text-xs font-medium">{title}</span>
            {!hasContent && (
              <span className="rounded bg-gray-200 px-1.5 py-0.5 text-xs dark:bg-gray-700">
                No Data
              </span>
            )}
          </div>
          <div className="flex items-center gap-1">
            {hasContent && (
              <button
                onClick={(e) => {
                  e.stopPropagation();
                  onCopy(content || '', id);
                }}
                className="rounded p-0.5 transition-colors hover:bg-white/50 dark:hover:bg-black/20"
              >
                {copiedId === id ? (
                  <Check className="h-3 w-3 text-green-600" />
                ) : (
                  <Copy className="h-3 w-3" />
                )}
              </button>
            )}
            {isExpanded ? (
              <ChevronDown className="h-3 w-3" />
            ) : (
              <ChevronRight className="h-3 w-3" />
            )}
          </div>
        </div>

        <AnimatePresence>
          {isExpanded && hasContent && (
            <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="rounded-b-lg p-2 pt-0">
                {parsedData !== undefined ? (
                  <JsonTreeView data={parsedData} onCopy={onCopy} copiedId={copiedId} defaultExpandDepth={2} className="max-h-[66vh]" />
                ) : (
                  <pre className="max-h-[66vh] overflow-x-auto overflow-y-auto rounded bg-white/50 p-2 text-xs dark:bg-black/20">
                    {content}
                  </pre>
                )}
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    );
  };

  // Full UX state object for export
  const fullUXState = {
    sidePanelMode,
    navigation,
    chatThreadId,
    activeCanvasId,
    isGlobalCanvasOpen,
    activeView,
    commandStack,
    commandModalItemId,
  };

  return (
    <div className="h-full space-y-2 overflow-y-auto p-2">
      {/* Header */}
      <div className="text-xs font-medium text-gray-700 dark:text-gray-300">UX State</div>

      {/* Compact rows for quick view */}
      {renderCompactRow('sidePanelMode', 'Side Panel Mode', sidePanelMode, 'purple')}
      {renderCompactRow('navigationPage', 'Current Page', navigation.page || 'null', 'green')}
      {renderCompactRow('chatThreadId', 'Chat Thread ID', chatThreadId, 'cyan')}
      {renderCompactRow('activeCanvasId', 'Canvas ID', activeCanvasId || 'null', 'amber')}
      {renderCompactRow('isGlobalCanvasOpen', 'Canvas Open', String(isGlobalCanvasOpen), 'amber')}
      {renderCompactRow('activeView', 'Active View', activeView ?? 'none', 'blue')}

      {/* Command Bar */}
      <div className="text-xs font-medium text-gray-700 dark:text-gray-300">Command Bar</div>
      {renderCompactRow(
        'commandStack',
        'Command Stack',
        commandStack.length ? `[${commandStack.map((c) => c.id).join(', ')}]` : '[]',
        'purple',
      )}
      {renderCompactRow('commandModalItemId', 'Modal Item ID', commandModalItemId || 'null', 'purple')}
      {renderSection(
        'commandStackDetail',
        'Command Stack Detail',
        commandStack.length ? safeStringify(commandStack) : null,
        'purple',
        commandStack.length ? commandStack : undefined,
      )}

      {/* Full state sections */}
      {renderSection(
        'navigationState',
        'Full Navigation State',
        safeStringify(navigation),
        'green',
        navigation,
      )}
      {renderSection('fullUXState', 'Full UX State', safeStringify(fullUXState), 'amber', fullUXState)}
    </div>
  );
};