TestDebuggerTab.tsx32.5 KBView on GitHub
/**
 * Test Tab Component
 * Provides UI for testing sync functionality with controlled parameters
 */


import { TestTube, Play, Loader2, Bell } from 'lucide-react';
import { useTRPC, useTRPCClient } from '@/providers/query-provider';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useState, useEffect, useMemo } from 'react';
import { useNotify } from '@/app/hooks/useNotify';
import { isElectron } from '@/lib/is-electron';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';

export function TestDebuggerTab() {
  const trpc = useTRPC();
  const notify = useNotify();
  const trpcClient = useTRPCClient();
  const [maxThreads, setMaxThreads] = useState(200);
  const [workflowId, setWorkflowId] = useState<string | null>(null);
  const [isSyncing, setIsSyncing] = useState(false);
  const [threadId, setThreadId] = useState('');
  const [isSyncingThread, setIsSyncingThread] = useState(false);
  const [threadSyncResult, setThreadSyncResult] = useState<{
    success: boolean;
    message?: string;
    error?: string;
  } | null>(null);
  const [isSyncingLoadingThread, setIsSyncingLoadingThread] = useState(false);
  const [loadingThreadSyncResult, setLoadingThreadSyncResult] = useState<{
    success: boolean;
    message?: string;
    error?: string;
    threadId?: string;
  } | null>(null);
  const [selectedLabels, setSelectedLabels] = useState<Set<string>>(new Set());
  const [labelSyncWorkflows, setLabelSyncWorkflows] = useState<Map<string, string>>(new Map());
  const [isSyncingLabels, setIsSyncingLabels] = useState(false);

  // Search-based sync state
  const [searchQuery, setSearchQuery] = useState('');
  const [isSearching, setIsSearching] = useState(false);
  const [isExecutingSearchResults, setIsExecutingSearchResults] = useState(false);
  const [searchResults, setSearchResults] = useState<Array<{ id: string; subject?: string }>>([]);
  const [executionProgress, setExecutionProgress] = useState<{
    current: number;
    total: number;
    completed: number;
    failed: number;
  } | null>(null);
  const [executionResults, setExecutionResults] = useState<
    Array<{
      threadId: string;
      success: boolean;
      error?: string;
    }>
  >([]);



  // Fetch user labels
  const labelsQuery = useQuery(trpc.labels.list.queryOptions(void 0));

  // Poll for sync progress
  const { data: progressData } = useQuery({
    queryKey: ['mail', 'getSyncProgress', { workflowId }],
    queryFn: () => trpcClient.mail.getSyncProgress.query({ workflowId: workflowId! }),
    enabled: !!workflowId,
    refetchInterval: 2000,
  });

  const syncMutation = useMutation({
    mutationFn: (input: {
      maxThreads: number;
      folder?: string;
      bypassAgentExecutionEnabledCheck?: boolean;
    }) => trpcClient.mail.triggerSyncThreads.mutate(input),
    onSuccess: (data) => {
      // The route returns workflowId: null when the sync workflow binding is unavailable.
      setWorkflowId(data.workflowId);
      setIsSyncing(!!data.workflowId);
    },
    onError: (_error: Error) => {
      console.error('Sync failed:', _error);
      setIsSyncing(false);
    },
  });

  const syncLabelsMutation = useMutation({
    mutationFn: (input: {
      maxThreads: number;
      folder: string;
      bypassAgentExecutionEnabledCheck?: boolean;
    }) => trpcClient.mail.triggerSyncThreads.mutate(input),
    onSuccess: (data, variables) => {
      if (!data.workflowId) return;
      setLabelSyncWorkflows((prev) => {
        const newMap = new Map(prev);
        newMap.set(variables.folder, data.workflowId);
        return newMap;
      });
    },
    onError: (error: Error, variables) => {
      console.error(`Sync failed for label ${variables.folder}:`, error);
    },
  });

  const syncThreadMutation = useMutation({
    mutationFn: (input: {
      threadId: string;
      bypassAgentExecutionEnabledCheck?: boolean;
      reexecute?: boolean;
    }) => trpcClient.mail.syncThread.mutate(input),
    onSuccess: (data) => {
      setThreadSyncResult(data);
      setIsSyncingThread(false);
    },
    onError: (error: Error) => {
      console.error('Thread sync failed:', error);
      setThreadSyncResult({
        success: false,
        error: error.message || 'Unknown error occurred',
      });
      setIsSyncingThread(false);
    },
  });


  const handleSyncSentFolder = async () => {
    setIsSyncing(true);
    try {
      const result = await syncMutation.mutateAsync({
        maxThreads,
        folder: 'SENT',
        bypassAgentExecutionEnabledCheck: true, // Always bypass enabled check when called from debugger
      });
      setWorkflowId(result.workflowId);
    } catch (_error) {
      console.error('Failed to start sync:', _error);
      setIsSyncing(false);
    }
  };

  const handleSyncSelectedLabels = async () => {
    if (selectedLabels.size === 0) {
      return;
    }

    setIsSyncingLabels(true);
    const workflows = new Map<string, string>();

    // Sync each selected label
    const syncPromises = Array.from(selectedLabels).map(async (labelId) => {
      try {
        const result = await syncLabelsMutation.mutateAsync({
          maxThreads,
          folder: labelId,
          bypassAgentExecutionEnabledCheck: true,
        });
        if (result.workflowId) workflows.set(labelId, result.workflowId);
      } catch (error) {
        console.error(`Failed to sync label ${labelId}:`, error);
      }
    });

    await Promise.allSettled(syncPromises);
    setLabelSyncWorkflows((prev) => {
      const newMap = new Map(prev);
      workflows.forEach((workflowId, labelId) => {
        newMap.set(labelId, workflowId);
      });
      return newMap;
    });
    setIsSyncingLabels(false);
  };

  const handleLabelToggle = (labelId: string, checked: boolean) => {
    setSelectedLabels((prev) => {
      const newSet = new Set(prev);
      if (checked) {
        newSet.add(labelId);
      } else {
        newSet.delete(labelId);
      }
      return newSet;
    });
  };

  // Filter out system labels and only show user labels
  const userLabels = useMemo(() => {
    if (!labelsQuery.data) return [];
    return labelsQuery.data.filter((label) => label.type === 'user');
  }, [labelsQuery.data]);

  const handleSyncThread = async () => {
    if (!threadId.trim()) {
      setThreadSyncResult({
        success: false,
        error: 'Please enter a thread ID',
      });
      return;
    }

    setIsSyncingThread(true);
    setThreadSyncResult(null);
    try {
      await syncThreadMutation.mutateAsync({
        threadId: threadId.trim(),
        bypassAgentExecutionEnabledCheck: true, // Always bypass enabled check when called from debugger
      });
    } catch (_error) {
      console.error('Failed to sync thread:', _error);
      setIsSyncingThread(false);
    }
  };

  const handleSyncLoadingThread = async () => {
    setIsSyncingLoadingThread(true);
    setLoadingThreadSyncResult(null);

    try {
      // Query threads from SENT folder (same as the sync sent section)
      const result = await trpcClient.mail.listThreads.query({
        q: 'label:SENT',
        maxResults: 100,
        cursor: undefined,
      });

      const threads = result.threads || [];

      // Find first thread with loadingConversation or failedCreatingConversation
      const loadingThread = threads.find((thread) => {
        const conversationId = (thread.$raw as { conversationId?: string | null } | undefined)
          ?.conversationId;
        return (
          conversationId === 'loadingConversation' ||
          conversationId === 'failedCreatingConversation'
        );
      });

      if (!loadingThread) {
        setLoadingThreadSyncResult({
          success: false,
          error: 'No threads found with loadingConversation or failedCreatingConversation',
        });
        setIsSyncingLoadingThread(false);
        return;
      }

      // Sync the found thread with reexecute flag to bypass new message check
      const syncResult = await syncThreadMutation.mutateAsync({
        threadId: loadingThread.id,
        bypassAgentExecutionEnabledCheck: true,
        reexecute: true, // Force re-execution even if message already has execution
      });

      setLoadingThreadSyncResult({
        success: syncResult.success,
        message: syncResult.message || `Successfully synced thread ${loadingThread.id}`,
        error: syncResult.error,
        threadId: loadingThread.id,
      });
    } catch (error) {
      console.error('Failed to sync loading thread:', error);
      setLoadingThreadSyncResult({
        success: false,
        error: error instanceof Error ? error.message : 'Unknown error occurred',
      });
    } finally {
      setIsSyncingLoadingThread(false);
    }
  };

  const handleSearchThreads = async () => {
    if (!searchQuery.trim()) {
      return;
    }

    setIsSearching(true);
    setSearchResults([]);
    setExecutionResults([]);
    setExecutionProgress(null);

    try {
      // Call listThreads with search query
      const result = await trpcClient.mail.listThreads.query({
        q: searchQuery.trim(),
        maxResults: 100, // Limit to 100 threads max
        cursor: undefined,
      });

      const threads = result.threads || [];
      setSearchResults(
        threads.map((t) => ({
          id: t.id,
          subject: (t.$raw as { latestSubject?: string } | undefined)?.latestSubject || undefined,
        })),
      );
    } catch (error) {
      console.error('Search failed:', error);
    } finally {
      setIsSearching(false);
    }
  };

  const handleExecuteSearchResults = async () => {
    if (searchResults.length === 0) {
      return;
    }

    setIsExecutingSearchResults(true);
    setExecutionProgress({
      current: 0,
      total: searchResults.length,
      completed: 0,
      failed: 0,
    });
    setExecutionResults([]);

    // Execute threads sequentially
    for (let i = 0; i < searchResults.length; i++) {
      const thread = searchResults[i];
      setExecutionProgress({
        current: i + 1,
        total: searchResults.length,
        completed: executionResults.filter((r) => r.success).length,
        failed: executionResults.filter((r) => !r.success).length,
      });

      try {
        const result = await syncThreadMutation.mutateAsync({
          threadId: thread.id,
          bypassAgentExecutionEnabledCheck: true,
        });

        setExecutionResults((prev) => [
          ...prev,
          {
            threadId: thread.id,
            success: result.success,
            error: result.error,
          },
        ]);

        setExecutionProgress((prev) =>
          prev
            ? {
                ...prev,
                current: i + 1,
                completed: prev.completed + (result.success ? 1 : 0),
                failed: prev.failed + (result.success ? 0 : 1),
              }
            : null,
        );

        // Small delay between executions to avoid overwhelming the system
        if (i < searchResults.length - 1) {
          await new Promise((resolve) => setTimeout(resolve, 500));
        }
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        setExecutionResults((prev) => [
          ...prev,
          {
            threadId: thread.id,
            success: false,
            error: errorMessage,
          },
        ]);

        setExecutionProgress((prev) =>
          prev
            ? {
                ...prev,
                current: i + 1,
                failed: prev.failed + 1,
              }
            : null,
        );
      }
    }

    setIsExecutingSearchResults(false);
    setExecutionProgress((prev) =>
      prev
        ? {
            ...prev,
            current: prev.total,
          }
        : null,
    );
  };


  // Check if sync is complete
  useEffect(() => {
    if (progressData?.status === 'completed' || progressData?.status === 'failed') {
      setIsSyncing(false);
    }
  }, [progressData?.status]);

  const output = progressData?.output as
    | {
        totalSynced?: number;
        totalPagesProcessed?: number;
        totalThreads?: number;
        totalSuccessfulSyncs?: number;
        totalFailedSyncs?: number;
        pageWorkflowResults?: Array<{
          pageNumber: number;
          workflowId: string;
          status: string;
          synced: number;
        }>;
      }
    | null
    | undefined;

  return (
    <div className="flex h-full flex-col gap-4 overflow-y-auto p-4">
      <div className="flex items-center gap-2">
        <TestTube className="h-4 w-4" />
        <h3 className="text-sm font-semibold">Sync Testing</h3>
      </div>

      {isElectron() && (
        <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
          <div className="flex items-center gap-2">
            <Bell className="h-4 w-4" />
            <Label className="text-xs font-medium">Electron Notifications</Label>
          </div>
          <Button
            size="sm"
            variant="outline"
            className="w-full text-xs"
            onClick={() => notify('Cedar Mail', 'Draft ready — Test notification from debugger')}
          >
            Test notification
          </Button>
        </div>
      )}


      <div className="space-y-4">
        {/* Search-based Sync Section */}
        <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
          <div className="flex items-center gap-2">
            <TestTube className="h-4 w-4" />
            <Label className="text-xs font-medium">Search & Sync Threads</Label>
          </div>
          <div className="space-y-2">
            <div className="flex gap-2">
              <Input
                type="text"
                placeholder="Enter search query (e.g., 'from:<email>' or 'subject:meeting')"
                value={searchQuery}
                onChange={(e) => setSearchQuery(e.target.value)}
                disabled={isSearching || isExecutingSearchResults}
                className="h-8 flex-1 text-xs"
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && !isSearching && searchQuery.trim()) {
                    handleSearchThreads();
                  }
                }}
              />
              <Button
                onClick={handleSearchThreads}
                disabled={isSearching || isExecutingSearchResults || !searchQuery.trim()}
                size="sm"
                variant="outline"
              >
                {isSearching ? (
                  <>
                    <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                    Searching...
                  </>
                ) : (
                  <>
                    <Play className="mr-2 h-3 w-3" />
                    Search
                  </>
                )}
              </Button>
            </div>

            {searchResults.length > 0 && (
              <div className="space-y-2">
                <div className="flex items-center justify-between text-xs">
                  <span className="text-gray-600 dark:text-gray-400">
                    Found {searchResults.length} thread{searchResults.length !== 1 ? 's' : ''}
                  </span>
                  <Button
                    onClick={handleExecuteSearchResults}
                    disabled={isExecutingSearchResults}
                    size="sm"
                    variant="default"
                    className="h-7"
                  >
                    {isExecutingSearchResults ? (
                      <>
                        <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                        Executing...
                      </>
                    ) : (
                      <>
                        <Play className="mr-2 h-3 w-3" />
                        Execute All Sequentially
                      </>
                    )}
                  </Button>
                </div>

                {executionProgress && (
                  <div className="space-y-1 rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
                    <div className="flex justify-between">
                      <span className="text-gray-600 dark:text-gray-400">Progress:</span>
                      <span className="font-medium">
                        {executionProgress.current} / {executionProgress.total}
                      </span>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-green-600 dark:text-green-400">Completed:</span>
                      <span className="font-medium">{executionProgress.completed}</span>
                    </div>
                    <div className="flex justify-between">
                      <span className="text-red-600 dark:text-red-400">Failed:</span>
                      <span className="font-medium">{executionProgress.failed}</span>
                    </div>
                    <div className="mt-1 h-2 w-full overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
                      <div
                        className="h-full bg-blue-500 transition-all duration-300"
                        style={{
                          width: `${(executionProgress.current / executionProgress.total) * 100}%`,
                        }}
                      />
                    </div>
                  </div>
                )}

                {executionResults.length > 0 && (
                  <div className="max-h-48 space-y-1 overflow-y-auto rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
                    <div className="font-medium">Execution Results:</div>
                    {executionResults.map((result, idx) => {
                      const thread = searchResults.find((t) => t.id === result.threadId);
                      return (
                        <div
                          key=[redacted]
                          className={`flex items-start justify-between rounded px-2 py-1 ${
                            result.success
                              ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
                              : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
                          }`}
                        >
                          <div className="flex-1 truncate">
                            <div className="font-medium">
                              {idx + 1}. {thread?.subject || result.threadId}
                            </div>
                            {result.error && (
                              <div className="mt-0.5 text-xs opacity-75">{result.error}</div>
                            )}
                          </div>
                          <div className="ml-2 flex-shrink-0">{result.success ? '✓' : '✗'}</div>
                        </div>
                      );
                    })}
                  </div>
                )}

                {!isExecutingSearchResults && executionResults.length === 0 && (
                  <div className="max-h-32 space-y-1 overflow-y-auto rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
                    {searchResults.slice(0, 10).map((thread, idx) => (
                      <div key=[redacted] className="truncate">
                        {idx + 1}. {thread.subject || thread.id}
                      </div>
                    ))}
                    {searchResults.length > 10 && (
                      <div className="text-gray-500">... and {searchResults.length - 10} more</div>
                    )}
                  </div>
                )}
              </div>
            )}
          </div>
        </div>

        {/* Sync Loading Thread Section */}
        <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
          <Label className="text-xs font-medium">
            Sync Thread with Loading/Failed Conversation
          </Label>
          <div className="space-y-2">
            <Button
              onClick={handleSyncLoadingThread}
              disabled={isSyncingLoadingThread}
              size="sm"
              variant="outline"
              className="w-full"
            >
              {isSyncingLoadingThread ? (
                <>
                  <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                  Finding and syncing...
                </>
              ) : (
                <>
                  <Play className="mr-2 h-3 w-3" />
                  Sync First Loading/Failed Thread
                </>
              )}
            </Button>
            {loadingThreadSyncResult && (
              <div
                className={`mt-2 rounded-md p-2 text-xs ${
                  loadingThreadSyncResult.success
                    ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
                    : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
                }`}
              >
                {loadingThreadSyncResult.success ? (
                  <div>
                    <div className="font-medium">{loadingThreadSyncResult.message}</div>
                    {loadingThreadSyncResult.threadId && (
                      <div className="mt-1 text-xs opacity-75">
                        Thread ID: {loadingThreadSyncResult.threadId}
                      </div>
                    )}
                  </div>
                ) : (
                  <div>
                    <div className="font-medium">Error:</div>
                    <div>{loadingThreadSyncResult.error}</div>
                  </div>
                )}
              </div>
            )}
          </div>
        </div>

        {/* Sync Single Thread Section */}
        <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
          <Label htmlFor="threadId" className="text-xs font-medium">
            Sync Specific Thread
          </Label>
          <div className="flex gap-2">
            <Input
              id="threadId"
              type="text"
              placeholder="Paste thread ID here"
              value={threadId}
              onChange={(e) => setThreadId(e.target.value)}
              disabled={isSyncingThread}
              className="h-8 flex-1 text-xs"
            />
            <Button
              onClick={handleSyncThread}
              disabled={isSyncingThread || !threadId.trim()}
              size="sm"
              variant="outline"
            >
              {isSyncingThread ? (
                <>
                  <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                  Syncing...
                </>
              ) : (
                <>
                  <Play className="mr-2 h-3 w-3" />
                  Sync Thread
                </>
              )}
            </Button>
          </div>
          {threadSyncResult && (
            <div
              className={`mt-2 rounded-md p-2 text-xs ${
                threadSyncResult.success
                  ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
                  : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'
              }`}
            >
              {threadSyncResult.success ? (
                <div className="font-medium">{threadSyncResult.message}</div>
              ) : (
                <div>
                  <div className="font-medium">Error:</div>
                  <div>{threadSyncResult.error}</div>
                </div>
              )}
            </div>
          )}
        </div>

        {/* Sync Labels Section */}
        <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
          <Label className="text-xs font-medium">Sync Labels</Label>
          {labelsQuery.isLoading ? (
            <div className="flex items-center gap-2 text-xs text-gray-500">
              <Loader2 className="h-3 w-3 animate-spin" />
              Loading labels...
            </div>
          ) : labelsQuery.error ? (
            <div className="text-xs text-red-600 dark:text-red-400">
              Failed to load labels: {labelsQuery.error.message}
            </div>
          ) : userLabels.length === 0 ? (
            <div className="text-xs text-gray-500">No user labels available</div>
          ) : (
            <>
              <div className="max-h-48 space-y-2 overflow-y-auto">
                {userLabels.map((label) => (
                  <div
                    key=[redacted]
                    className="flex items-center gap-2 rounded-md p-2 hover:bg-gray-50 dark:hover:bg-gray-800"
                  >
                    <Checkbox
                      id={`label-${label.id}`}
                      checked={selectedLabels.has(label.id)}
                      onCheckedChange={(checked) => handleLabelToggle(label.id, checked === true)}
                      disabled={isSyncingLabels}
                    />
                    <Label
                      htmlFor={`label-${label.id}`}
                      className="flex-1 cursor-pointer text-xs font-normal"
                    >
                      {label.name}
                    </Label>
                    {label.color && (
                      <div
                        className="h-4 w-4 rounded"
                        style={{ backgroundColor: label.color.backgroundColor }}
                      />
                    )}
                  </div>
                ))}
              </div>
              <div className="flex items-center justify-between gap-2 pt-2">
                <div className="text-xs text-gray-500">
                  {selectedLabels.size} label{selectedLabels.size !== 1 ? 's' : ''} selected
                </div>
                <Button
                  onClick={handleSyncSelectedLabels}
                  disabled={isSyncingLabels || selectedLabels.size === 0}
                  size="sm"
                  variant="outline"
                  className="h-8"
                >
                  {isSyncingLabels ? (
                    <>
                      <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                      Syncing...
                    </>
                  ) : (
                    <>
                      <Play className="mr-2 h-3 w-3" />
                      Sync Selected
                    </>
                  )}
                </Button>
              </div>
              {labelSyncWorkflows.size > 0 && (
                <div className="mt-2 space-y-1 rounded-md bg-gray-50 p-2 text-xs dark:bg-gray-800">
                  <div className="font-medium">Active Label Syncs:</div>
                  {Array.from(labelSyncWorkflows.entries()).map(([labelId, workflowId]) => {
                    const label = userLabels.find((l) => l.id === labelId);
                    return (
                      <div key=[redacted] className="flex justify-between">
                        <span className="text-gray-600 dark:text-gray-400">
                          {label?.name || labelId}:
                        </span>
                        <span className="break-all font-mono text-xs">{workflowId}</span>
                      </div>
                    );
                  })}
                </div>
              )}
            </>
          )}
        </div>

        <div className="border-t border-gray-200 pt-4 dark:border-gray-700">
          <div className="mb-4 space-y-2">
            <Label htmlFor="maxThreads" className="text-xs">
              Max Threads (1-1000)
            </Label>
            <Input
              id="maxThreads"
              type="number"
              min={1}
              max={1000}
              value={maxThreads}
              onChange={(e) => setMaxThreads(parseInt(e.target.value) || 200)}
              disabled={isSyncing}
              className="h-8 text-xs"
            />
          </div>

          <Button onClick={handleSyncSentFolder} disabled={isSyncing} size="sm" className="w-full">
            {isSyncing ? (
              <>
                <Loader2 className="mr-2 h-3 w-3 animate-spin" />
                Syncing...
              </>
            ) : (
              <>
                <Play className="mr-2 h-3 w-3" />
                Start Sync (SENT folder)
              </>
            )}
          </Button>
        </div>

        {workflowId && (
          <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
            <div className="text-xs font-medium">Workflow ID:</div>
            <div className="break-all text-xs text-gray-600 dark:text-gray-400">{workflowId}</div>
          </div>
        )}

        {progressData && (
          <div className="space-y-2 rounded-md border border-gray-200 p-3 dark:border-gray-700">
            <div className="flex items-center justify-between">
              <span className="text-xs font-medium">Status:</span>
              <span
                className={`text-xs font-semibold ${
                  progressData.status === 'completed'
                    ? 'text-green-600 dark:text-green-400'
                    : progressData.status === 'failed'
                      ? 'text-red-600 dark:text-red-400'
                      : 'text-blue-600 dark:text-blue-400'
                }`}
              >
                {progressData.status.toUpperCase()}
              </span>
            </div>

            {output && (
              <div className="space-y-1 text-xs">
                <div className="flex justify-between">
                  <span className="text-gray-600 dark:text-gray-400">Total Synced:</span>
                  <span className="font-medium">{output.totalSynced ?? 0}</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-600 dark:text-gray-400">Pages Processed:</span>
                  <span className="font-medium">{output.totalPagesProcessed ?? 0}</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-600 dark:text-gray-400">Total Threads:</span>
                  <span className="font-medium">{output.totalThreads ?? 0}</span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-600 dark:text-gray-400">Successful:</span>
                  <span className="font-medium text-green-600 dark:text-green-400">
                    {output.totalSuccessfulSyncs ?? 0}
                  </span>
                </div>
                <div className="flex justify-between">
                  <span className="text-gray-600 dark:text-gray-400">Failed:</span>
                  <span className="font-medium text-red-600 dark:text-red-400">
                    {output.totalFailedSyncs ?? 0}
                  </span>
                </div>

                {output.pageWorkflowResults && output.pageWorkflowResults.length > 0 && (
                  <div className="mt-2 space-y-1">
                    <div className="text-xs font-medium">Page Results:</div>
                    <div className="max-h-32 space-y-1 overflow-y-auto">
                      {output.pageWorkflowResults.map((page) => (
                        <div
                          key=[redacted]
                          className="flex justify-between rounded bg-gray-50 px-2 py-1 text-xs dark:bg-gray-800"
                        >
                          <span>Page {page.pageNumber}</span>
                          <span className="font-medium">{page.synced} synced</span>
                        </div>
                      ))}
                    </div>
                  </div>
                )}
              </div>
            )}

            {progressData.error && (
              <div className="mt-2 rounded bg-red-50 p-2 text-xs text-red-600 dark:bg-red-900/20 dark:text-red-400">
                Error: {progressData.error}
              </div>
            )}
          </div>
        )}

        <div className="mt-4 space-y-2 rounded-md bg-blue-50 p-3 text-xs dark:bg-blue-900/20">
          <div className="font-semibold text-blue-900 dark:text-blue-300">Testing Guide:</div>
          <ul className="list-inside list-disc space-y-1 text-blue-800 dark:text-blue-200">
            <li>Paste a thread ID and click &quot;Sync Thread&quot; to sync a specific thread</li>
            <li>
              Select one or more labels and click &quot;Sync Selected&quot; to sync threads with
              those labels
            </li>
            <li>Set max threads to limit how many threads to sync in bulk</li>
            <li>Click &quot;Start Sync&quot; to begin syncing the SENT folder</li>
            <li>Progress updates automatically every 2 seconds for bulk syncs</li>
            <li>Monitor the status and results in real-time</li>
          </ul>
        </div>
      </div>
    </div>
  );
}