"use client"; import { useAtomValue } from "jotai"; import { ChevronRight, Loader2, Plug2, Search, } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { type FC, useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms"; import { documentTypeCountsAtom } from "@/atoms/documents/document-query.atoms"; import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { EnumConnectorName } from "@/contracts/enums/connector"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors"; import { authenticatedFetch } from "@/lib/auth-utils"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { cn } from "@/lib/utils"; // OAuth Connectors (Quick Connect) const OAUTH_CONNECTORS = [ { id: "google-gmail-connector", title: "Gmail", description: "Search through your emails", connectorType: EnumConnectorName.GOOGLE_GMAIL_CONNECTOR, authEndpoint: "/api/v1/auth/google/gmail/connector/add/", }, { id: "google-calendar-connector", title: "Google Calendar", description: "Search through your events", connectorType: EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR, authEndpoint: "/api/v1/auth/google/calendar/connector/add/", }, { id: "airtable-connector", title: "Airtable", description: "Search your Airtable bases", connectorType: EnumConnectorName.AIRTABLE_CONNECTOR, authEndpoint: "/api/v1/auth/airtable/connector/add/", }, ]; // Non-OAuth Connectors const OTHER_CONNECTORS = [ { id: "slack-connector", title: "Slack", description: "Search Slack messages", connectorType: EnumConnectorName.SLACK_CONNECTOR, }, { id: "discord-connector", title: "Discord", description: "Search Discord messages", connectorType: EnumConnectorName.DISCORD_CONNECTOR, }, { id: "notion-connector", title: "Notion", description: "Search Notion pages", connectorType: EnumConnectorName.NOTION_CONNECTOR, }, { id: "confluence-connector", title: "Confluence", description: "Search documentation", connectorType: EnumConnectorName.CONFLUENCE_CONNECTOR, }, { id: "bookstack-connector", title: "BookStack", description: "Search BookStack docs", connectorType: EnumConnectorName.BOOKSTACK_CONNECTOR, }, { id: "github-connector", title: "GitHub", description: "Search repositories", connectorType: EnumConnectorName.GITHUB_CONNECTOR, }, { id: "linear-connector", title: "Linear", description: "Search issues & projects", connectorType: EnumConnectorName.LINEAR_CONNECTOR, }, { id: "jira-connector", title: "Jira", description: "Search Jira issues", connectorType: EnumConnectorName.JIRA_CONNECTOR, }, { id: "clickup-connector", title: "ClickUp", description: "Search ClickUp tasks", connectorType: EnumConnectorName.CLICKUP_CONNECTOR, }, { id: "luma-connector", title: "Luma", description: "Search Luma events", connectorType: EnumConnectorName.LUMA_CONNECTOR, }, { id: "elasticsearch-connector", title: "Elasticsearch", description: "Search ES indexes", connectorType: EnumConnectorName.ELASTICSEARCH_CONNECTOR, }, { id: "webcrawler-connector", title: "Web Pages", description: "Crawl web content", connectorType: EnumConnectorName.WEBCRAWLER_CONNECTOR, }, { id: "tavily-api", title: "Tavily AI", description: "Search with Tavily", connectorType: EnumConnectorName.TAVILY_API, }, { id: "searxng", title: "SearxNG", description: "Search with SearxNG", connectorType: EnumConnectorName.SEARXNG_API, }, { id: "linkup-api", title: "Linkup API", description: "Search with Linkup", connectorType: EnumConnectorName.LINKUP_API, }, { id: "baidu-search-api", title: "Baidu Search", description: "Search with Baidu", connectorType: EnumConnectorName.BAIDU_SEARCH_API, }, ]; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@/components/ui/tabs"; export const ConnectorIndicator: FC = () => { const router = useRouter(); const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom); const { connectors, isLoading: connectorsLoading } = useSearchSourceConnectors( false, searchSpaceId ? Number(searchSpaceId) : undefined ); const { data: documentTypeCounts, isLoading: documentTypesLoading } = useAtomValue(documentTypeCountsAtom); const { data: allConnectors } = useAtomValue(connectorsAtom); const pathname = usePathname(); const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState("all"); const [connectingId, setConnectingId] = useState(null); const [isScrolled, setIsScrolled] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const isLoading = connectorsLoading || documentTypesLoading; // Synchronize state with URL path useEffect(() => { const pathParts = window.location.pathname.split("/"); const connectorsIdx = pathParts.indexOf("connectors"); if (connectorsIdx !== -1) { if (!isOpen) setIsOpen(true); // Detect tab from URL: .../connectors/active or .../connectors/all const tabFromUrl = pathParts[connectorsIdx + 1]; if (tabFromUrl === "active" || tabFromUrl === "all") { if (activeTab !== tabFromUrl) setActiveTab(tabFromUrl); } } else { if (isOpen) setIsOpen(false); } }, [pathname, isOpen, activeTab]); // 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; // Check which connectors are already connected const connectedTypes = new Set( (allConnectors || []).map((c: SearchSourceConnector) => c.connector_type) ); // Filter connectors based on search const filteredOAuth = OAUTH_CONNECTORS.filter(c => c.title.toLowerCase().includes(searchQuery.toLowerCase()) || c.description.toLowerCase().includes(searchQuery.toLowerCase()) ); const filteredOther = OTHER_CONNECTORS.filter(c => c.title.toLowerCase().includes(searchQuery.toLowerCase()) || c.description.toLowerCase().includes(searchQuery.toLowerCase()) ); // Handle OAuth connection const handleConnectOAuth = useCallback( async (connector: (typeof OAUTH_CONNECTORS)[0]) => { if (!searchSpaceId || !connector.authEndpoint) return; try { setConnectingId(connector.id); const response = await authenticatedFetch( `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}${connector.authEndpoint}?space_id=${searchSpaceId}`, { method: "GET" } ); if (!response.ok) { throw new Error(`Failed to initiate ${connector.title} OAuth`); } const data = await response.json(); window.location.href = data.auth_url; } catch (error) { console.error(`Error connecting to ${connector.title}:`, error); toast.error(`Failed to connect to ${connector.title}`); } finally { setConnectingId(null); } }, [searchSpaceId] ); const handleOpenChange = useCallback( (open: boolean) => { setIsOpen(open); const currentPath = window.location.pathname; const basePath = currentPath.split("/connectors")[0].replace(/\/$/, ""); if (open) { // Base state is /connectors/all const newUrl = `${basePath}/connectors/${activeTab}`; window.history.pushState({ modal: true }, "", newUrl); } else { // Return to base chat path window.history.pushState({ modal: false }, "", basePath || "/"); setIsScrolled(false); setSearchQuery(""); } }, [activeTab] ); const handleTabChange = useCallback( (value: string) => { setActiveTab(value); const currentPath = window.location.pathname; const basePath = currentPath.split("/connectors")[0].replace(/\/$/, ""); // Update URL to reflect the new tab state const newUrl = `${basePath}/connectors/${value}`; window.history.replaceState({ modal: true }, "", newUrl); }, [] ); const handleScroll = useCallback((e: React.UIEvent) => { setIsScrolled(e.currentTarget.scrollTop > 0); }, []); if (!searchSpaceId) return null; return ( handleOpenChange(true)} > {isLoading ? ( ) : ( <> {totalSourceCount > 0 && ( {totalSourceCount > 99 ? "99+" : totalSourceCount} )} )} {/* Header */}
Connectors Search across all your apps and data in one place.
All Connectors Active {totalSourceCount > 0 && ( {totalSourceCount} )}
setSearchQuery(e.target.value)} />
{/* Content */}
{/* Quick Connect */} {filteredOAuth.length > 0 && (

Quick Connect

{filteredOAuth.map((connector) => { const isConnected = connectedTypes.has(connector.connectorType); const isConnecting = connectingId === connector.id; return (
{getConnectorIcon(connector.connectorType, "size-6")}
{connector.title} {isConnected && ( )}

{isConnected ? "Connected" : connector.description}

); })}
)} {/* More Integrations */} {filteredOther.length > 0 && (

More Integrations

{filteredOther.map((connector) => { const isConnected = connectedTypes.has(connector.connectorType); return (
{getConnectorIcon(connector.connectorType, "size-6")}
{connector.title} {isConnected && ( )}

{connector.description}

); })}
)}
{hasSources ? (

Currently Active

{activeDocumentTypes.map(([docType, count]) => (
{getConnectorIcon(docType, "size-6")}

{getDocumentTypeLabel(docType)}

{count as number} documents indexed

))} {connectors.map((connector) => (
{getConnectorIcon(connector.connector_type, "size-6")}

{connector.name}

Status: Active

))}
) : (

No active sources

Connect your first service to start searching across all your data.

Browse available connectors
)}
); };