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