crm-table.tsx30.7 KBView on GitHub
/**
 * CRM Table Component
 * Main table component for displaying and managing CRM contacts
 */

import type {
  CompanyCombined,
  NextStep,
  ScheduledAction,
  UserTask,
  WorkingMemoryField,
} from '../store/crmSlice';
import { Columns, LayoutGrid, Loader2, Plus, RefreshCw, Table as TableIcon, X } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { useOptimisticConversationActions } from '../hooks/use-optimistic-conversation-actions';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useUpsertWorkingMemory } from '../hooks/use-working-memory';
import { useIsFetching, useIsMutating } from '@tanstack/react-query';
import { useConversationNavigation } from '@/modules/conversations';
import { useSession } from '@/modules/auth/utils/auth-client';
import { useVirtualizer } from '@tanstack/react-virtual';
import type { CRMColumn } from '../store/crmSlice';
import type { ConversationEvent } from '../types';
import { useSearchParams } from 'react-router';
import { DEFAULT_STATUS_ENUM } from '../types';
import { format } from 'date-fns';

import { ComposeDisplay } from '@/modules/drafting/components/compose-display';
import { ThreadDisplay } from '@/modules/threads/thread/components/thread-display';
import { ConversationListHotkeys } from './conversation-list-hotkeys';
import { useCRMConversations } from '../hooks/use-crm-conversations';
import { useCRMConfiguration } from '../hooks/use-crm-configuration';
import { useBulkSelection } from '../hooks/use-bulk-selection';
import { VirtualizedTableRow } from './VirtualizedTableRow';
import { useCreateConversation } from '../hooks/use-crm';
import { useAOPs } from '@/modules/aop/hooks/use-aops';
import { HotkeyBar } from '@/components/ui/hotkey-bar';
import { ConversationView } from './ConversationView';
import { CRMSearchInput } from './crm-search-input';
import { CRMTableHeader } from './CRMTableHeader';
import { Button } from '@/components/ui/button';
import { useCedarStore } from '@/modules/store';
import { CRMPresetBar } from './CRMPresetBar';
import { cn } from '@/lib/utils';

export function CRMTable() {
  // Get view mode from URL params (for toggle button state)
  const [searchParams, setSearchParams] = useSearchParams();
  const viewMode = searchParams.get('view') || 'table';

  // Fetch conversations with infinite scroll
  const [conversationsQuery, hasNextPage, loadMore] = useCRMConversations();

  // Get session for working memory updates
  const { data: session } = useSession();
  const { mutateAsync: upsertWorkingMemory } = useUpsertWorkingMemory();

  // Use the unified CRM configuration hook
  // This handles: AOP selection, column generation, enum options, validation
  // Returns only visible columns by default, allColumns includes hidden ones
  const { columns: visibleColumns, allColumns } = useCRMConfiguration();

  // On mount, clear activePresetId and show all columns.
  // HomeView sets activePresetId for its tab presets; the full CRM table shows everything.
  const setActivePresetId = useCedarStore((state) => state.setActivePresetId);
  const setColumnVisibilities = useCedarStore((state) => state.setColumnVisibilities);
  const columns = useCedarStore((state) => state.columns);

  // Run once when columns first become available (replaces removed columnsInitialized flag)
  const columnsInitAppliedRef = useRef(false);
  useEffect(() => {
    if (columnsInitAppliedRef.current || Object.keys(columns).length === 0) return;
    columnsInitAppliedRef.current = true;

    // Clear preset so late-arriving columns aren't hidden
    setActivePresetId(null);

    // Make all columns visible (full CRM table view)
    const visibilities: Record<string, boolean> = {};
    Object.keys(columns).forEach((columnId) => {
      visibilities[columnId] = true;
    });
    setColumnVisibilities(visibilities);
  }, [columns, setActivePresetId, setColumnVisibilities]);

  const { mutateAsync: createConversation } = useCreateConversation();
  const { optimisticUpdateConversation } = useOptimisticConversationActions();

  // Get list ordering from conversationsSlice (for keyboard navigation and rendering)
  const conversationList = useCedarStore((state) => state.getCurrentConversationList());

  // Get selection from crmSlice (bulk operations)
  const selectedIds = useCedarStore((state) => state.conversationSelection);
  const clearConversationSelection = useCedarStore((state) => state.clearConversationSelection);
  const toggleConversationSelection = useCedarStore((state) => state.toggleConversationSelection);
  const setConversationSelection = useCedarStore((state) => state.setConversationSelection);
  const selectionAnchorId = useCedarStore((state) => state.selectionAnchorId);
  const setSelectionAnchorId = useCedarStore((state) => state.setSelectionAnchorId);

  // Bulk selection hook
  const { getSelectMode, handleClickWithSelection } = useBulkSelection({
    items: conversationList,
    selectedIds,
    setSelection: setConversationSelection,
    toggleSelection: toggleConversationSelection,
    clearSelection: clearConversationSelection,
    anchorId: selectionAnchorId,
    setAnchorId: setSelectionAnchorId,
  });

  // Get filters for clear all button (computed from columns)
  const getFilters = useCedarStore((state) => state.getFilters);
  const filters = getFilters();

  // Get selectedAopIds and derive names for display (from AOPs data)
  const selectedAopIds = useCedarStore((state) => state.selectedAopIds);
  const { data: aopsData } = useAOPs();
  const selectedAopNames = useMemo(() => {
    if (!selectedAopIds || selectedAopIds.length === 0 || !aopsData?.aops) return [];
    return selectedAopIds
      .map((id) => aopsData.aops.find((a) => a.id === id)?.name)
      .filter((name): name is string => !!name);
  }, [selectedAopIds, aopsData?.aops]);

  // Get column actions from store
  const toggleColumnVisibility = useCedarStore((state) => state.toggleColumnVisibility);
  const reorderColumns = useCedarStore((state) => state.reorderColumns);
  const setColumnWidth = useCedarStore((state) => state.setColumnWidth);
  const resetColumnConfig = useCedarStore((state) => state.resetColumnConfig);

  // Calculate hidden columns for "Show All" button
  const hiddenColumns = useMemo(() => {
    return allColumns.filter((col) => !col.visible);
  }, [allColumns]);
  const hiddenColumnCount = hiddenColumns.length;

  // Helper to check if column should have textarea width (needed for resetColumnConfig)
  const isTextareaColumn = useCallback((id: string) => {
    return (
      id === 'primaryCompany' ||
      id === 'status' ||
      id === 'scheduledAction' ||
      id === 'nextSteps' ||
      id === 'history'
    );
  }, []);

  // Reset all columns to visible
  const handleResetColumns = useCallback(() => {
    resetColumnConfig(isTextareaColumn);
  }, [resetColumnConfig, isTextareaColumn]);

  // Column state for width/resize tracking (temporary state during drag)
  const [columnSizing, setColumnSizing] = useState<Record<string, number>>({});

  // Count active filters (excluding search and type since type is always set)
  const activeFilterCount = Object.entries(filters).filter(([key, value]) => {
    if (key === 'search' || key === 'type') return false;
    if (Array.isArray(value)) return value.length > 0;
    // crmSynced is a boolean - only count if it's explicitly set (not undefined)
    if (key === 'crmSynced') return value !== undefined;
    return value !== undefined && value !== '' && value !== null;
  }).length;

  const renderCountRef = useRef(0);
  renderCountRef.current += 1;

  useEffect(() => {
    if (process.env.NODE_ENV !== 'development') return;
    console.debug(
      `[CRMTable] Main table render count: ${renderCountRef.current} | Conversations: ${conversationList.length}`,
    );
  });

  // Ref for the table container (for virtualization and keyboard navigation)
  const tableContainerRef = useRef<HTMLDivElement>(null);

  // Enable keyboard navigation (j/k, arrow keys, Enter, Escape, s to select hovered)
  const { handleMouseEnter } = useConversationNavigation({
    items: conversationList,
    containerRef: tableContainerRef,
  });

  // Check if conversation is open
  const isConversationOpen = useCedarStore((state) => state.isConversationOpen);
  const setIsConversationOpen = useCedarStore((state) => state.setIsConversationOpen);
  const threadId = useCedarStore((state) => state.selectedThreadId);
  const isThreadOpen = useCedarStore((state) => state.isThreadOpen);
  const newEmail = useCedarStore((state) => state.newEmail);
  const isViewOpen = isConversationOpen || isThreadOpen || newEmail;

  // Calculate column widths from visible columns
  const columnWidths = useMemo(() => {
    const widths: Record<string, number> = {};
    visibleColumns.forEach((col) => {
      // Use stored width from column config, or fall back to columnSizing, or defaults
      // Column config already has type-aware default widths from initializeColumns
      widths[col.id] = columnSizing[col.id] || col.width || 200;
    });
    return widths;
  }, [visibleColumns, columnSizing]);

  // Calculate total table width
  const totalSize = useMemo(() => {
    return Object.values(columnWidths).reduce((sum, width) => sum + width, 0);
  }, [columnWidths]);

  // Handle column resize (disabled when conversation or thread is open)
  const handleResizeStart = useCallback(
    (columnId: string, startX: number, startWidth: number) => {
      if (isViewOpen) return; // Disable resizing when conversation or thread is open

      let finalWidth = startWidth;

      const handleMouseMove = (moveEvent: MouseEvent | TouchEvent) => {
        const currentX = 'touches' in moveEvent ? moveEvent.touches[0].clientX : moveEvent.clientX;
        const diff = currentX - startX;
        const newWidth = Math.max(80, startWidth + diff);
        finalWidth = newWidth;
        setColumnSizing((prev) => ({ ...prev, [columnId]: newWidth }));
      };

      const handleMouseUp = () => {
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
        document.removeEventListener('touchmove', handleMouseMove);
        document.removeEventListener('touchend', handleMouseUp);

        // Persist the final width to Zustand store
        setColumnWidth(columnId, finalWidth);

        // Clear temporary sizing state
        setColumnSizing((prev) => {
          const next = { ...prev };
          delete next[columnId];
          return next;
        });
      };

      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', handleMouseUp);
      document.addEventListener('touchmove', handleMouseMove);
      document.addEventListener('touchend', handleMouseUp);
    },
    [isViewOpen, setColumnWidth],
  );

  // Save cell value to database (called on blur) - using optimistic updates
  const handleCellBlur = useCallback(
    async (
      rowId: string,
      columnId: string,
      value:
        | string
        | boolean
        | number
        | ConversationEvent[]
        | NextStep
        | CompanyCombined
        | ScheduledAction
        | UserTask[]
        | WorkingMemoryField[]
        | null
        | undefined,
    ) => {
      // rowId is the conversationId, so we can use it directly
      if (!rowId) return;

      // Handle custom fields (columns starting with wm_)
      if (columnId.startsWith('wm_') && columnId !== 'wm_custom') {
        const fieldName = columnId.substring(3); // Remove 'wm_' prefix
        if (!session?.user?.id) return;

        await upsertWorkingMemory({
          conversationId: rowId,
          name: fieldName,
          value: typeof value === 'string' ? value : String(value || ''),
        });
        return;
      }

      // Skip custom memory column (read-only for now)
      if (columnId === 'wm_custom') {
        return;
      }

      // Skip user tasks column (read-only)
      if (columnId === 'currentTasks') {
        return;
      }

      // Map UI columns to database fields
      const updates: Record<string, string | number | Date | null> = {};

      if (columnId === 'nextSteps') {
        updates.nextSteps = (value as string) || null;
      } else if (columnId === 'nextStepDate') {
        const dateStr = value as string;
        if (dateStr && dateStr.trim()) {
          const date = new Date(dateStr);
          updates.nextStepDate = isNaN(date.getTime()) ? null : date;
        } else {
          updates.nextStepDate = null;
        }
      } else if (columnId === 'status') {
        updates.status = (value as string) || null;
      } else if (columnId === 'statusOverview') {
        updates.statusOverview = (value as string) || null;
      } else if (columnId === 'primaryCompany') {
        // Handle combined company column - only update name (company is read-only)
        const companyCombined = value as CompanyCombined;
        updates.name = companyCombined.name || null;
      } else if (columnId === 'type') {
        // Type is now derived from AOP, skip updating (handled by aop-type cell)
        return;
      } else if (columnId === 'priority') {
        updates.priority = value as string;
      } else if (columnId === 'dealValue') {
        const numValue = Number(value);
        updates.dealValue = isNaN(numValue) ? null : numValue;
      }

      // Use optimistic update - will immediately update Zustand and call server
      await optimisticUpdateConversation(rowId, updates);
    },
    [optimisticUpdateConversation, upsertWorkingMemory, session?.user?.id],
  );

  // Add new row
  const addRow = async () => {
    await createConversation({
      name: `New Conversation ${Date.now()}`,
      status: DEFAULT_STATUS_ENUM.PROSPECTING,
    });
  };

  const handleRowClick = useCallback(
    (rowId: string) => {
      handleClickWithSelection(rowId, () => {
        // Simple selection - just highlight the row
        useCedarStore.getState().setActiveConversationId(rowId);
      });
    },
    [handleClickWithSelection],
  );

  // Handle mouse enter for keyboard navigation
  const handleRowMouseEnter = useCallback(
    (rowId: string) => {
      handleMouseEnter(rowId);
    },
    [handleMouseEnter],
  );

  const isFetching = useIsFetching();
  const isMutating = useIsMutating();
  const isLoading = isFetching > 0 || isMutating > 0;

  const handleCloseConversation = useCallback(() => {
    // Only close the conversation view, keep the conversation selected
    setIsConversationOpen(false);
  }, [setIsConversationOpen]);

  // Handle column reordering
  const handleReorderColumns = useCallback(
    (newOrder: CRMColumn[]) => {
      const columnIds = newOrder.map((col) => col.id);
      reorderColumns(columnIds);
    },
    [reorderColumns],
  );

  // Delete column (hide instead of delete)
  const deleteColumn = useCallback(
    (columnId: string) => {
      toggleColumnVisibility(columnId);
    },
    [toggleColumnVisibility],
  );

  // Update column (placeholder for compatibility)
  const updateColumn = useCallback(() => {
    console.warn('updateColumn called - column updates should use specific Zustand actions');
  }, []);

  // Determine if we should measure elements (not Firefox)
  const shouldMeasure =
    typeof window !== 'undefined' &&
    typeof navigator !== 'undefined' &&
    navigator.userAgent.indexOf('Firefox') === -1;

  // Virtualize rows with TanStack Virtual
  const rowVirtualizer = useVirtualizer({
    count: conversationList.length,
    getScrollElement: () => tableContainerRef.current,
    estimateSize: () => 60,
    measureElement: shouldMeasure
      ? (element) => element?.getBoundingClientRect().height
      : undefined,
    overscan: 5,
  });

  const virtualItems = rowVirtualizer.getVirtualItems();

  // Scroll detection for infinite scroll - load more when near bottom
  useEffect(() => {
    const container = tableContainerRef.current;
    if (!container) return;

    const handleScroll = () => {
      const { scrollTop, scrollHeight, clientHeight } = container;
      const distanceFromBottom = scrollHeight - scrollTop - clientHeight;

      // Trigger load more when within 500px of bottom
      if (
        distanceFromBottom < 500 &&
        hasNextPage &&
        !conversationsQuery.isFetching &&
        !conversationsQuery.isFetchingNextPage
      ) {
        void loadMore();
      }
    };

    container.addEventListener('scroll', handleScroll);
    return () => container.removeEventListener('scroll', handleScroll);
  }, [hasNextPage, conversationsQuery.isFetching, conversationsQuery.isFetchingNextPage, loadMore]);

  return (
    <TooltipProvider delayDuration={300}>
      {/* URL⇄store sync is now a single root-level LayoutUrlSync (url-driven-layout Phase 3). */}
      <ConversationListHotkeys />

      <div className="flex h-full flex-col py-2">
        {/* Header - Hidden when conversation is open */}
        {!isViewOpen && (
          <div className="shrink-0 space-y-2 pb-2">
            <div className="flex items-center">
              {/* Left side: Search Input and Action Buttons */}
              <div className="flex items-center gap-2 flex-1">
                <CRMSearchInput />
                <Tooltip>
                  <TooltipTrigger asChild>
                    <Button
                      onClick={() => conversationsQuery.refetch()}
                      size="sm"
                      variant="ghost"
                      className="h-8 w-8 p-0"
                      disabled={isLoading}
                    >
                      <RefreshCw className={cn('h-4 w-4', isLoading && 'animate-spin')} />
                    </Button>
                  </TooltipTrigger>
                  <TooltipContent>
                    <p>Refresh all conversations</p>
                  </TooltipContent>
                </Tooltip>
                {activeFilterCount > 0 && (
                  <Tooltip>
                    <TooltipTrigger asChild>
                      <Button
                        onClick={() => {
                          // Clear all column filters and search query
                          useCedarStore.getState().clearAllColumnFilters();
                          useCedarStore.getState().setSearchQuery(undefined);
                        }}
                        size="sm"
                        variant="outline"
                        className="gap-2"
                      >
                        <X className="h-4 w-4" />
                        Clear all filters ({activeFilterCount})
                      </Button>
                    </TooltipTrigger>
                    <TooltipContent className="max-w-xs">
                      <div className="space-y-1.5">
                        <p className="font-semibold">Active Filters:</p>
                        {/* Type is always shown (AOP selection), shown as context not filter */}
                        {selectedAopNames.length > 0 && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Types (AOPs): </span>
                            <span>{selectedAopNames.join(', ')}</span>
                          </div>
                        )}
                        {filters.status && filters.status.length > 0 && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Status: </span>
                            <span>{filters.status.join(', ')}</span>
                          </div>
                        )}
                        {filters.excludedStatus && filters.excludedStatus.length > 0 && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Excluded Status: </span>
                            <span>{filters.excludedStatus.join(', ')}</span>
                          </div>
                        )}
                        {filters.priority && filters.priority.length > 0 && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Priority: </span>
                            <span>{filters.priority.join(', ')}</span>
                          </div>
                        )}
                        {filters.excludedPriority && filters.excludedPriority.length > 0 && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Excluded Priority: </span>
                            <span>{filters.excludedPriority.join(', ')}</span>
                          </div>
                        )}
                        {filters.lastContact && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">
                              Last Contact{' '}
                              {filters.lastContact.operator === 'empty'
                                ? 'Not Set'
                                : filters.lastContact.operator === 'before'
                                  ? 'Before'
                                  : filters.lastContact.operator === 'after'
                                    ? 'After'
                                    : 'On'}
                              {filters.lastContact.operator !== 'empty' && ': '}
                            </span>
                            {filters.lastContact.operator !== 'empty' && (
                              <span>
                                {format(new Date(filters.lastContact.date), 'MMM d, yyyy')}
                              </span>
                            )}
                          </div>
                        )}
                        {filters.dealValue && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">
                              Deal Size{' '}
                              {filters.dealValue.operator === 'empty'
                                ? 'Not Set'
                                : filters.dealValue.operator === 'gt'
                                  ? '>'
                                  : filters.dealValue.operator === 'lt'
                                    ? '<'
                                    : '='}
                              {filters.dealValue.operator !== 'empty' && ' '}
                            </span>
                            {filters.dealValue.operator !== 'empty' && (
                              <span>${Number(filters.dealValue.value).toLocaleString()}</span>
                            )}
                          </div>
                        )}
                        {(filters.dealValueMin || filters.dealValueMax) && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">Deal Value: </span>
                            <span>
                              {filters.dealValueMin && filters.dealValueMax
                                ? `$${filters.dealValueMin} - $${filters.dealValueMax}`
                                : filters.dealValueMin
                                  ? `≥ $${filters.dealValueMin}`
                                  : `≤ $${filters.dealValueMax}`}
                            </span>
                          </div>
                        )}
                        {filters.crmSynced !== undefined && (
                          <div className="text-xs">
                            <span className="text-muted-foreground">CRM Synced: </span>
                            <span>{filters.crmSynced ? 'Synced' : 'Not Synced'}</span>
                          </div>
                        )}
                      </div>
                    </TooltipContent>
                  </Tooltip>
                )}
                {/* Show Hidden Columns Button */}
                {hiddenColumnCount > 0 && (
                  <Tooltip>
                    <TooltipTrigger asChild>
                      <Button
                        onClick={handleResetColumns}
                        size="sm"
                        variant="outline"
                        className="gap-2"
                      >
                        <Columns className="h-4 w-4" />
                        Show hidden columns ({hiddenColumnCount})
                      </Button>
                    </TooltipTrigger>
                    <TooltipContent className="max-w-xs">
                      <div className="space-y-1.5">
                        <p className="font-semibold">Hidden Columns:</p>
                        {hiddenColumns.map((col) => (
                          <div key=[redacted] className="text-xs">
                            <span>{col.name || col.id}</span>
                          </div>
                        ))}
                      </div>
                    </TooltipContent>
                  </Tooltip>
                )}
                {/* Right side: Action Buttons */}
                <div className="flex shrink-0 gap-2">
                  {/* View Toggle Button */}
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={() => {
                      const newView = viewMode === 'table' ? 'kanban' : 'table';
                      setSearchParams({ view: newView });
                    }}
                    className="gap-2"
                  >
                    {viewMode === 'table' ? (
                      <>
                        <LayoutGrid className="h-4 w-4" />
                        Kanban View
                      </>
                    ) : (
                      <>
                        <TableIcon className="h-4 w-4" />
                        Table View
                      </>
                    )}
                  </Button>
                  {selectedIds.length > 0 && (
                    <Button variant="ghost" size="sm" onClick={clearConversationSelection}>
                      Clear Selection ({selectedIds.length})
                    </Button>
                  )}
                  {/* COMMENTED OUT: Add Conversation button */}
                  {/* <Button onClick={addRow} size="sm">
                <Plus className="mr-2 h-4 w-4" />
                Add Conversation
              </Button> */}
                  {/* COMMENTED OUT: Add Column Popover */}
                  {/* <AddColumnPopover
                isOpen={isAddColumnOpen}
                onOpenChange={setIsAddColumnOpen}
                newColumn={newColumn}
                onColumnChange={setNewColumn}
                onAdd={addColumn}
              /> */}
                </div>
              </div>
            </div>

            {/* Preset Bar */}
            <CRMPresetBar />
          </div>
        )}

        {/* Table Container with Virtualization - Grid System */}
        <div className="flex min-h-0 flex-1 gap-0 overflow-hidden mb-2">
          {/* Conversation/Thread View - Full Width when open */}
          {isViewOpen && (
            <div className="min-h-0 flex-1 overflow-hidden">
              {newEmail ? (
                <div className="flex h-full w-full max-w-full flex-col">
                  <ComposeDisplay />
                </div>
              ) : isThreadOpen && threadId ? (
                <ThreadDisplay />
              ) : (
                <ConversationView onClose={handleCloseConversation} />
              )}
            </div>
          )}

          {/* Table Section - Hidden when conversation or thread is open */}
          {!isViewOpen && (
            <div className="overflow-hidden flex-1">
              <div
                ref={tableContainerRef}
                className={cn(
                  'h-full scrollbar-hide overflow-auto',
                  getSelectMode() === 'range' && 'select-none',
                )}
                style={{ position: 'relative' }}
              >
                <table style={{ display: 'grid' }} className="w-full">
                  {/* Fixed Header with CSS Grid */}
                  <CRMTableHeader
                    columnsConfig={visibleColumns}
                    columnWidths={columnWidths}
                    totalSize={totalSize}
                    onReorder={handleReorderColumns}
                    onUpdateColumn={updateColumn}
                    onDeleteColumn={deleteColumn}
                    onResizeStart={handleResizeStart}
                  />

                  {/* Virtualized Table Body */}
                  {conversationList.length > 0 && (
                    <tbody
                      style={{
                        display: 'grid',
                        height: `${rowVirtualizer.getTotalSize()}px`,
                        width: totalSize,
                        position: 'relative',
                      }}
                    >
                      {virtualItems.map((virtualRow) => {
                        const conversation = conversationList[virtualRow.index];
                        if (!conversation) return null;

                        return (
                          <VirtualizedTableRow
                            key=[redacted]
                            conversationId={conversation.id}
                            columns={visibleColumns}
                            columnWidths={columnWidths}
                            virtualRow={virtualRow}
                            rowVirtualizer={rowVirtualizer}
                            onCellBlur={handleCellBlur}
                            onClick={handleRowClick}
                            onMouseEnter={handleRowMouseEnter}
                          />
                        );
                      })}
                    </tbody>
                  )}
                </table>

                {/* Empty state */}
                {conversationList.length === 0 && (
                  <div className="flex flex-col items-center justify-center h-64">
                    <p className="text-muted-foreground mb-4">No conversations found</p>
                    <Button onClick={addRow} variant="outline" size="sm">
                      <Plus className="mr-2 h-4 w-4" />
                      Add your first conversation
                    </Button>
                  </div>
                )}

                {/* Loading indicator at bottom */}
                {conversationsQuery.isFetchingNextPage && (
                  <div className="flex justify-center py-6">
                    <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
                  </div>
                )}

                {/* No more results indicator */}
                {!hasNextPage && conversationList.length > 0 && (
                  <div className="text-muted-foreground py-6 text-center text-sm">
                    No more conversations
                  </div>
                )}
              </div>
            </div>
          )}
        </div>

        {/* Hotkey bar - fixed at bottom */}
        <div className="w-full shrink-0">
          <HotkeyBar />
        </div>
      </div>
    </TooltipProvider>
  );
}