CurrentThreadDebuggerTab.tsx14.5 KBView on GitHub import type {
ConversationSummary,
ConversationWithMetadata,
} from '@/modules/conversations/slice/conversationsSlice';
import type { ParsedMessage, ThreadData } from '@/modules/threads/threadList/store/threadSlice';
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 CurrentThreadTabProps {
currentThreadId: string | null;
currentConversationId: string | null;
threadData: ThreadData | undefined;
conversationData: ConversationWithMetadata | undefined;
currentConversationList: ConversationSummary[];
onCopy: (text: string, id: string) => void;
copiedId: string | null;
}
export const CurrentThreadDebuggerTab: React.FC<CurrentThreadTabProps> = ({
currentThreadId,
currentConversationId,
threadData,
conversationData,
currentConversationList,
onCopy,
copiedId,
}) => {
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
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 ID row component - always visible, label + value on same row
const renderCompactId = (
id: string,
label: string,
value: string | null | undefined,
color: string = 'blue',
) => {
const hasValue = !!value;
const colorClasses = {
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',
};
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 as keyof typeof colorClasses] || 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 = {
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',
};
return (
<div
className={cn(
'rounded-lg border transition-all',
colorClasses[color as keyof typeof colorClasses] || 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>
);
};
const renderMessageItem = (message: ParsedMessage, index: number) => {
const messageId = `message-${index}`;
const isExpanded = expandedSections.has(messageId);
const draftSessionId = message.draftSessionId ?? null;
const localDraft = message.userEdited === true;
return (
<div
key=[redacted]
className="rounded-lg border border-blue-200 bg-blue-50 transition-all dark:border-blue-800 dark:bg-blue-950"
>
<div
className={cn(
'flex cursor-pointer items-center justify-between p-2 transition-colors hover:bg-blue-100 dark:hover:bg-blue-900/80',
isExpanded ? 'rounded-t-lg' : 'rounded-lg',
)}
onClick={() => toggleSection(messageId)}
>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-xs font-medium">Message {index + 1}</span>
{message.isDraft && (
<span className="rounded bg-amber-200 px-1.5 py-0.5 text-xs font-medium text-amber-900 dark:bg-amber-900 dark:text-amber-200">
DRAFT
</span>
)}
</div>
{!isExpanded && (
<div className="space-y-0.5 text-xs">
<div className="flex items-start gap-2">
<span className="shrink-0 font-medium text-gray-600 dark:text-gray-400">
Snippet:
</span>
<span className="min-w-0 truncate text-gray-900 dark:text-gray-100">
{message.snippet || 'N/A'}
</span>
</div>
{message.isDraft !== undefined && (
<div className="flex items-center gap-2">
<span className="shrink-0 font-medium text-gray-600 dark:text-gray-400">
Is Draft:
</span>
<span className="text-gray-900 dark:text-gray-100">
{String(message.isDraft)}
</span>
</div>
)}
{message.draftId && (
<div className="flex items-center gap-2">
<span className="shrink-0 font-medium text-gray-600 dark:text-gray-400">
Draft ID:
</span>
<span className="min-w-0 truncate font-mono text-gray-900 dark:text-gray-100">
{message.draftId}
</span>
</div>
)}
{message.isDraft && (
<div className="flex items-center gap-2">
<span className="shrink-0 font-medium text-gray-600 dark:text-gray-400">
Local Draft:
</span>
<span className="text-gray-900 dark:text-gray-100">{String(localDraft)}</span>
</div>
)}
{message.isDraft && (
<div className="flex items-center gap-2">
<span className="shrink-0 font-medium text-gray-600 dark:text-gray-400">
Draft Session ID:
</span>
<span className="min-w-0 truncate font-mono text-gray-900 dark:text-gray-100">
{draftSessionId ?? 'N/A'}
</span>
</div>
)}
</div>
)}
</div>
<div className="flex shrink-0 items-center gap-1">
<button
onClick={(e) => {
e.stopPropagation();
onCopy(safeStringify(message), messageId);
}}
className="rounded p-0.5 transition-colors hover:bg-white/50 dark:hover:bg-black/20"
>
{copiedId === messageId ? (
<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 && (
<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">
<JsonTreeView
data={message}
onCopy={onCopy}
copiedId={copiedId}
defaultExpandDepth={2}
className="max-h-[50vh]"
/>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
return (
<div className="h-full space-y-2 overflow-y-auto p-2">
{!currentThreadId && !currentConversationId ? (
<div className="flex h-full items-center justify-center py-8 text-center text-xs text-gray-500 dark:text-gray-400">
<div className="space-y-2">
<p className="">No thread selected</p>
<p className="text-xs text-gray-400">Open an email thread to see debug data</p>
{/* CRM Conversation List */}
{renderSection(
'currentConversationList',
`Current Conversation List (${currentConversationList.length})`,
currentConversationList.length > 0
? safeStringify(currentConversationList)
: undefined,
'gray',
currentConversationList.length > 0 ? currentConversationList : undefined,
)}
</div>
</div>
) : (
<>
{/* Compact ID rows - always visible, non-expandable */}
{currentThreadId && renderCompactId('threadId', 'Thread ID', currentThreadId, 'blue')}
{currentConversationId &&
renderCompactId('conversationId', 'Conversation ID', currentConversationId, 'purple')}
{/* Expandable sections for full data */}
{/* Thread Data Section */}
{currentThreadId &&
renderSection(
'threadData',
'Full Thread Data',
threadData ? safeStringify(threadData) : undefined,
'green',
threadData,
)}
{/* Conversation Data Section */}
{currentConversationId &&
renderSection(
'conversationData',
'Full Conversation Data',
conversationData ? safeStringify(conversationData) : undefined,
'purple',
conversationData,
)}
{/* Additional Thread Info */}
{threadData && (
<>
{/* Individual Messages */}
{threadData.messages && threadData.messages.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-medium text-gray-700 dark:text-gray-300">
Thread Messages ({threadData.messages.length})
</div>
{threadData.messages.map((message, index) => renderMessageItem(message, index))}
</div>
)}
{renderSection(
'threadLatest',
'Latest Message',
threadData.latest ? safeStringify(threadData.latest) : undefined,
'blue',
threadData.latest ?? undefined,
)}
</>
)}
{/* Conversation Additional Info */}
{conversationData && (
<>
{renderSection(
'conversationTimestamp',
'Last Loaded At',
conversationData.lastLoadedAt
? new Date(conversationData.lastLoadedAt).toLocaleString()
: undefined,
'gray',
)}
</>
)}
</>
)}
</div>
);
};