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