"use client"; import { useAtomValue, useSetAtom } from "jotai"; import { AlertTriangle, Cable, Settings } from "lucide-react"; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom"; import { globalNewLLMConfigsAtom, llmPreferencesAtom, } from "@/atoms/new-llm-config/new-llm-config-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms"; import { currentUserAtom } from "@/atoms/user/user-query.atoms"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { useConnectorsElectric } from "@/hooks/use-connectors-electric"; import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker"; import { cn } from "@/lib/utils"; import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header"; import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view"; import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view"; import { IndexingConfigurationView } from "./connector-popup/connector-configs/views/indexing-configuration-view"; import { COMPOSIO_CONNECTORS, OAUTH_CONNECTORS, } from "./connector-popup/constants/connector-constants"; import { useConnectorDialog } from "./connector-popup/hooks/use-connector-dialog"; import { useIndexingConnectors } from "./connector-popup/hooks/use-indexing-connectors"; import { ActiveConnectorsTab } from "./connector-popup/tabs/active-connectors-tab"; import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab"; import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view"; import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view"; export interface ConnectorIndicatorHandle { open: () => void; } interface ConnectorIndicatorProps { showTrigger?: boolean; } export const ConnectorIndicator = forwardRef( ({ showTrigger = true }, ref) => { const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom); useAtomValue(currentUserAtom); const { data: preferences = {}, isFetching: preferencesLoading } = useAtomValue(llmPreferencesAtom); const { data: globalConfigs = [], isFetching: globalConfigsLoading } = useAtomValue(globalNewLLMConfigsAtom); // Check if document summary LLM is properly configured // - If ID is 0 (Auto mode), we need global configs to be available // - If ID is positive (user config) or negative (specific global config), it's configured // - If ID is null/undefined, it's not configured const docSummaryLlmId = preferences.document_summary_llm_id; const isAutoMode = docSummaryLlmId === 0; const hasGlobalConfigs = globalConfigs.length > 0; const hasDocumentSummaryLLM = docSummaryLlmId !== null && docSummaryLlmId !== undefined && // If it's Auto mode, we need global configs to actually be available (!isAutoMode || hasGlobalConfigs); const llmConfigLoading = preferencesLoading || globalConfigsLoading; // Fetch document type counts via the lightweight /type-counts endpoint (cached 10 min) const { data: documentTypeCounts, isFetching: documentTypesLoading } = useAtomValue(documentTypeCountsAtom); // Read status inbox items from shared atom (populated by LayoutDataProvider) // instead of creating a duplicate useInbox("status") hook. const statusInboxItems = useAtomValue(statusInboxItemsAtom); const inboxItems = useMemo( () => statusInboxItems.filter((item) => item.type === "connector_indexing"), [statusInboxItems] ); // Use the custom hook for dialog state management const { isOpen, activeTab, connectingId, isScrolled, searchQuery, indexingConfig, indexingConnector, indexingConnectorConfig, editingConnector, connectingConnectorType, isCreatingConnector, startDate, endDate, isStartingIndexing, isSaving, isDisconnecting, periodicEnabled, frequencyMinutes, enableSummary, allConnectors, viewingAccountsType, viewingMCPList, isYouTubeView, isFromOAuth, setSearchQuery, setStartDate, setEndDate, setPeriodicEnabled, setFrequencyMinutes, setEnableSummary, handleOpenChange, handleTabChange, handleScroll, handleConnectOAuth, handleConnectNonOAuth, handleCreateWebcrawler, handleCreateYouTubeCrawler, handleSubmitConnectForm, handleStartIndexing, handleSkipIndexing, handleStartEdit, handleSaveConnector, handleDisconnectConnector, handleBackFromEdit, handleBackFromConnect, handleBackFromYouTube, handleViewAccountsList, handleBackFromAccountsList, handleBackFromMCPList, handleAddNewMCPFromList, handleQuickIndexConnector, connectorConfig, setConnectorConfig, setIndexingConnectorConfig, setConnectorName, } = useConnectorDialog(); const [pickerOpen, setPickerOpen] = useState(false); useEffect(() => { const onOpen = () => setPickerOpen(true); const onClose = () => setPickerOpen(false); window.addEventListener(PICKER_OPEN_EVENT, onOpen); window.addEventListener(PICKER_CLOSE_EVENT, onClose); return () => { window.removeEventListener(PICKER_OPEN_EVENT, onOpen); window.removeEventListener(PICKER_CLOSE_EVENT, onClose); }; }, []); // Fetch connectors using Electric SQL + PGlite for real-time updates // This provides instant updates when connectors change, without polling const { connectors: connectorsFromElectric = [], loading: connectorsLoading, error: connectorsError, refreshConnectors: refreshConnectorsElectric, } = useConnectorsElectric(searchSpaceId); // Fallback to API if Electric is not available or fails // Use Electric data if: 1) we have data, or 2) still loading without error // Use API data if: Electric failed (has error) or finished loading with no data const useElectricData = connectorsFromElectric.length > 0 || (connectorsLoading && !connectorsError); const connectors = useElectricData ? connectorsFromElectric : allConnectors || []; // Manual refresh function that works with both Electric and API const refreshConnectors = async () => { if (useElectricData) { await refreshConnectorsElectric(); } else { // Fallback: use allConnectors from useConnectorDialog (which uses connectorsAtom) // The connectorsAtom will handle refetching if needed } }; // Track indexing state locally - clears automatically when Electric SQL detects last_indexed_at changed // Also clears when failed notifications are detected const { indexingConnectorIds, startIndexing, stopIndexing } = useIndexingConnectors( connectors as SearchSourceConnector[], inboxItems ); const isLoading = connectorsLoading || documentTypesLoading; // Get document types that have documents in the search space const activeDocumentTypes = documentTypeCounts ? Object.entries(documentTypeCounts).filter(([, count]) => count > 0) : []; const hasConnectors = connectors.length > 0; const hasSources = hasConnectors || activeDocumentTypes.length > 0; const totalSourceCount = connectors.length + activeDocumentTypes.length; const activeConnectorsCount = connectors.length; // Check which connectors are already connected // Using Electric SQL + PGlite for real-time connector updates const connectedTypes = new Set( (connectors || []).map((c: SearchSourceConnector) => c.connector_type) ); useImperativeHandle(ref, () => ({ open: () => handleOpenChange(true), })); if (!searchSpaceId) return null; return ( {showTrigger && ( handleOpenChange(true)} > {isLoading ? ( ) : ( <> {activeConnectorsCount > 0 && ( {activeConnectorsCount > 99 ? "99+" : activeConnectorsCount} )} )} )} {isOpen && createPortal( ); } ); ConnectorIndicator.displayName = "ConnectorIndicator";