feat: composio connector

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-01-21 22:57:58 -08:00
parent 6e331c3b85
commit 8c625d4237
28 changed files with 6177 additions and 3021 deletions

View file

@ -21,6 +21,7 @@ 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 { ComposioToolkitView } from "./connector-popup/views/composio-toolkit-view";
import { ConnectorAccountsListView } from "./connector-popup/views/connector-accounts-list-view";
import { YouTubeCrawlerView } from "./connector-popup/views/youtube-crawler-view";
@ -87,6 +88,12 @@ export const ConnectorIndicator: FC = () => {
setConnectorConfig,
setIndexingConnectorConfig,
setConnectorName,
// Composio
viewingComposio,
connectingComposioToolkit,
handleOpenComposio,
handleBackFromComposio,
handleConnectComposioToolkit,
} = useConnectorDialog();
// Fetch connectors using Electric SQL + PGlite for real-time updates
@ -176,6 +183,20 @@ export const ConnectorIndicator: FC = () => {
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : viewingComposio && searchSpaceId ? (
<ComposioToolkitView
searchSpaceId={searchSpaceId}
connectedToolkits={
(connectors || [])
.filter((c: SearchSourceConnector) => c.connector_type === "COMPOSIO_CONNECTOR")
.map((c: SearchSourceConnector) => c.config?.toolkit_id as string)
.filter(Boolean)
}
onBack={handleBackFromComposio}
onConnectToolkit={handleConnectComposioToolkit}
isConnecting={connectingComposioToolkit !== null}
connectingToolkitId={connectingComposioToolkit}
/>
) : viewingMCPList ? (
<ConnectorAccountsListView
connectorType="MCP_CONNECTOR"
@ -312,6 +333,7 @@ export const ConnectorIndicator: FC = () => {
onCreateYouTubeCrawler={handleCreateYouTubeCrawler}
onManage={handleStartEdit}
onViewAccountsList={handleViewAccountsList}
onOpenComposio={handleOpenComposio}
/>
</TabsContent>

View file

@ -0,0 +1,78 @@
"use client";
import { Zap } from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ComposioConnectorCardProps {
id: string;
title: string;
description: string;
connectorCount?: number;
onConnect: () => void;
}
export const ComposioConnectorCard: FC<ComposioConnectorCardProps> = ({
id,
title,
description,
connectorCount = 0,
onConnect,
}) => {
const hasConnections = connectorCount > 0;
return (
<div
className={cn(
"group relative flex items-center gap-4 p-4 rounded-xl text-left transition-all duration-200 w-full border",
"border-violet-500/20 bg-gradient-to-br from-violet-500/5 to-purple-500/5",
"hover:border-violet-500/40 hover:from-violet-500/10 hover:to-purple-500/10"
)}
>
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg transition-colors shrink-0 border",
"bg-gradient-to-br from-violet-500/10 to-purple-500/10 border-violet-500/20"
)}
>
<Image
src="/connectors/composio.svg"
alt="Composio"
width={24}
height={24}
className="size-6"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5">
<span className="text-[14px] font-semibold leading-tight truncate">{title}</span>
<Zap className="size-3.5 text-violet-500" />
</div>
{hasConnections ? (
<p className="text-[10px] text-muted-foreground mt-1 flex items-center gap-1.5">
<span>
{connectorCount} {connectorCount === 1 ? "connection" : "connections"}
</span>
</p>
) : (
<p className="text-[10px] text-muted-foreground mt-1">{description}</p>
)}
</div>
<Button
size="sm"
variant={hasConnections ? "secondary" : "default"}
className={cn(
"h-8 text-[11px] px-3 rounded-lg shrink-0 font-medium shadow-xs",
!hasConnections && "bg-violet-600 hover:bg-violet-700 text-white",
hasConnections &&
"bg-white text-slate-700 hover:bg-slate-50 border-0 dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80"
)}
onClick={onConnect}
>
{hasConnections ? "Manage" : "Browse"}
</Button>
</div>
);
};

View file

@ -0,0 +1,160 @@
"use client";
import { ExternalLink, Info, Zap } from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { Badge } from "@/components/ui/badge";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { cn } from "@/lib/utils";
interface ComposioConfigProps {
connector: SearchSourceConnector;
onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void;
}
// Get toolkit display info
const getToolkitInfo = (toolkitId: string): { name: string; icon: string; description: string } => {
switch (toolkitId) {
case "googledrive":
return {
name: "Google Drive",
icon: "/connectors/google-drive.svg",
description: "Files and documents from Google Drive",
};
case "gmail":
return {
name: "Gmail",
icon: "/connectors/google-gmail.svg",
description: "Emails from Gmail",
};
case "googlecalendar":
return {
name: "Google Calendar",
icon: "/connectors/google-calendar.svg",
description: "Events from Google Calendar",
};
case "slack":
return {
name: "Slack",
icon: "/connectors/slack.svg",
description: "Messages from Slack",
};
case "notion":
return {
name: "Notion",
icon: "/connectors/notion.svg",
description: "Pages from Notion",
};
case "github":
return {
name: "GitHub",
icon: "/connectors/github.svg",
description: "Repositories from GitHub",
};
default:
return {
name: toolkitId,
icon: "/connectors/composio.svg",
description: "Connected via Composio",
};
}
};
export const ComposioConfig: FC<ComposioConfigProps> = ({ connector }) => {
const toolkitId = connector.config?.toolkit_id as string;
const toolkitName = connector.config?.toolkit_name as string;
const isIndexable = connector.config?.is_indexable as boolean;
const composioAccountId = connector.config?.composio_connected_account_id as string;
const toolkitInfo = getToolkitInfo(toolkitId);
return (
<div className="space-y-6">
{/* Toolkit Info Card */}
<div className="rounded-xl border border-violet-500/20 bg-gradient-to-br from-violet-500/5 to-purple-500/5 p-4">
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-gradient-to-br from-violet-500/10 to-purple-500/10 border border-violet-500/20 shrink-0">
<Image
src={toolkitInfo.icon}
alt={toolkitInfo.name}
width={24}
height={24}
className="size-6"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h3 className="text-sm font-semibold">{toolkitName || toolkitInfo.name}</h3>
<Badge
variant="secondary"
className="text-[10px] px-1.5 py-0 h-5 bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20"
>
<Zap className="size-3 mr-0.5" />
Composio
</Badge>
</div>
<p className="text-xs text-muted-foreground">{toolkitInfo.description}</p>
</div>
</div>
</div>
{/* Connection Details */}
<div className="space-y-3">
<h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Connection Details
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between py-2 px-3 rounded-lg bg-muted/50">
<span className="text-xs text-muted-foreground">Toolkit</span>
<span className="text-xs font-medium">{toolkitId}</span>
</div>
<div className="flex items-center justify-between py-2 px-3 rounded-lg bg-muted/50">
<span className="text-xs text-muted-foreground">Indexing Supported</span>
<Badge
variant={isIndexable ? "default" : "secondary"}
className={cn(
"text-[10px] px-1.5 py-0 h-5",
isIndexable
? "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
: "bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20"
)}
>
{isIndexable ? "Yes" : "Coming Soon"}
</Badge>
</div>
{composioAccountId && (
<div className="flex items-center justify-between py-2 px-3 rounded-lg bg-muted/50">
<span className="text-xs text-muted-foreground">Account ID</span>
<span className="text-xs font-mono text-muted-foreground truncate max-w-[150px]">
{composioAccountId}
</span>
</div>
)}
</div>
</div>
{/* Info Banner */}
<div className="rounded-lg border border-border/50 bg-muted/30 p-3">
<div className="flex items-start gap-2.5">
<Info className="size-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="space-y-1">
<p className="text-xs text-muted-foreground leading-relaxed">
This connection uses Composio&apos;s managed OAuth, which means you don&apos;t need to
wait for app verification. Your data is securely accessed through Composio.
</p>
<a
href="https://composio.dev"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-violet-600 dark:text-violet-400 hover:underline"
>
Learn more about Composio
<ExternalLink className="size-3" />
</a>
</div>
</div>
</div>
</div>
);
};

View file

@ -5,6 +5,7 @@ import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { BaiduSearchApiConfig } from "./components/baidu-search-api-config";
import { BookStackConfig } from "./components/bookstack-config";
import { CirclebackConfig } from "./components/circleback-config";
import { ComposioConfig } from "./components/composio-config";
import { ClickUpConfig } from "./components/clickup-config";
import { ConfluenceConfig } from "./components/confluence-config";
import { DiscordConfig } from "./components/discord-config";
@ -73,6 +74,8 @@ export function getConnectorConfigComponent(
return CirclebackConfig;
case "MCP_CONNECTOR":
return MCPConfig;
case "COMPOSIO_CONNECTOR":
return ComposioConfig;
// OAuth connectors (Gmail, Calendar, Airtable, Notion) and others don't need special config UI
default:
return null;

View file

@ -168,5 +168,56 @@ export const OTHER_CONNECTORS = [
},
] as const;
// Composio Connector (Single entry that opens toolkit selector)
export const COMPOSIO_CONNECTORS = [
{
id: "composio-connector",
title: "Composio",
description: "Connect 100+ apps via Composio (Google, Slack, Notion, etc.)",
connectorType: EnumConnectorName.COMPOSIO_CONNECTOR,
// No authEndpoint - handled via toolkit selector view
},
] as const;
// Composio Toolkits (available integrations via Composio)
export const COMPOSIO_TOOLKITS = [
{
id: "googledrive",
name: "Google Drive",
description: "Search your Drive files",
isIndexable: true,
},
{
id: "gmail",
name: "Gmail",
description: "Search through your emails",
isIndexable: true,
},
{
id: "googlecalendar",
name: "Google Calendar",
description: "Search through your events",
isIndexable: true,
},
{
id: "slack",
name: "Slack",
description: "Search Slack messages",
isIndexable: false,
},
{
id: "notion",
name: "Notion",
description: "Search Notion pages",
isIndexable: false,
},
{
id: "github",
name: "GitHub",
description: "Search repositories",
isIndexable: false,
},
] as const;
// Re-export IndexingConfigState from schemas for backward compatibility
export type { IndexingConfigState } from "./connector-popup.schemas";

View file

@ -83,6 +83,10 @@ export const useConnectorDialog = () => {
// MCP list view state (for managing multiple MCP connectors)
const [viewingMCPList, setViewingMCPList] = useState(false);
// Composio toolkit view state
const [viewingComposio, setViewingComposio] = useState(false);
const [connectingComposioToolkit, setConnectingComposioToolkit] = useState<string | null>(null);
// Track if we came from accounts list when entering edit mode
const [cameFromAccountsList, setCameFromAccountsList] = useState<{
connectorType: string;
@ -155,6 +159,17 @@ export const useConnectorDialog = () => {
setViewingMCPList(true);
}
// Clear Composio view if view is not "composio" anymore
if (params.view !== "composio" && viewingComposio) {
setViewingComposio(false);
setConnectingComposioToolkit(null);
}
// Handle Composio view
if (params.view === "composio" && !viewingComposio) {
setViewingComposio(true);
}
// Handle connect view
if (params.view === "connect" && params.connectorType && !connectingConnectorType) {
setConnectingConnectorType(params.connectorType);
@ -846,6 +861,63 @@ export const useConnectorDialog = () => {
router.replace(url.pathname + url.search, { scroll: false });
}, [router]);
// Handle opening Composio toolkit view
const handleOpenComposio = useCallback(() => {
if (!searchSpaceId) return;
setViewingComposio(true);
// Update URL to show Composio view
const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors");
url.searchParams.set("view", "composio");
window.history.pushState({ modal: true }, "", url.toString());
}, [searchSpaceId]);
// Handle going back from Composio view
const handleBackFromComposio = useCallback(() => {
setViewingComposio(false);
setConnectingComposioToolkit(null);
const url = new URL(window.location.href);
url.searchParams.set("modal", "connectors");
url.searchParams.delete("view");
router.replace(url.pathname + url.search, { scroll: false });
}, [router]);
// Handle connecting a Composio toolkit
const handleConnectComposioToolkit = useCallback(
async (toolkitId: string) => {
if (!searchSpaceId) return;
setConnectingComposioToolkit(toolkitId);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/auth/composio/connector/add?space_id=${searchSpaceId}&toolkit_id=${toolkitId}`,
{ method: "GET" }
);
if (!response.ok) {
throw new Error(`Failed to initiate Composio OAuth for ${toolkitId}`);
}
const data = await response.json();
if (data.auth_url) {
// Redirect to Composio OAuth
window.location.href = data.auth_url;
} else {
throw new Error("No authorization URL received from Composio");
}
} catch (error) {
console.error("Error connecting Composio toolkit:", error);
toast.error(`Failed to connect ${toolkitId}. Please try again.`);
setConnectingComposioToolkit(null);
}
},
[searchSpaceId]
);
// Handle starting indexing
const handleStartIndexing = useCallback(
async (refreshConnectors: () => void) => {
@ -1506,6 +1578,7 @@ export const useConnectorDialog = () => {
allConnectors,
viewingAccountsType,
viewingMCPList,
viewingComposio,
// Setters
setSearchQuery,
@ -1541,5 +1614,12 @@ export const useConnectorDialog = () => {
connectorConfig,
setConnectorConfig,
setIndexingConnectorConfig,
// Composio
viewingComposio,
connectingComposioToolkit,
handleOpenComposio,
handleBackFromComposio,
handleConnectComposioToolkit,
};
};

View file

@ -4,7 +4,8 @@ import type { FC } from "react";
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { ConnectorCard } from "../components/connector-card";
import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants";
import { ComposioConnectorCard } from "../components/composio-connector-card";
import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS, COMPOSIO_CONNECTORS } from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
/**
@ -34,6 +35,7 @@ interface AllConnectorsTabProps {
onCreateYouTubeCrawler?: () => void;
onManage?: (connector: SearchSourceConnector) => void;
onViewAccountsList?: (connectorType: string, connectorTitle: string) => void;
onOpenComposio?: () => void;
}
export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
@ -49,6 +51,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
onCreateYouTubeCrawler,
onManage,
onViewAccountsList,
onOpenComposio,
}) => {
// Filter connectors based on search
const filteredOAuth = OAUTH_CONNECTORS.filter(
@ -69,6 +72,20 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
c.description.toLowerCase().includes(searchQuery.toLowerCase())
);
// Filter Composio connectors
const filteredComposio = COMPOSIO_CONNECTORS.filter(
(c) =>
c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.description.toLowerCase().includes(searchQuery.toLowerCase())
);
// Count Composio connectors
const composioConnectorCount = allConnectors
? allConnectors.filter(
(c: SearchSourceConnector) => c.connector_type === EnumConnectorName.COMPOSIO_CONNECTOR
).length
: 0;
return (
<div className="space-y-8">
{/* Quick Connect */}
@ -137,6 +154,30 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
</section>
)}
{/* Composio Integrations */}
{filteredComposio.length > 0 && onOpenComposio && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">Managed OAuth</h3>
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20 font-medium">
No verification needed
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredComposio.map((connector) => (
<ComposioConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorCount={composioConnectorCount}
onConnect={onOpenComposio}
/>
))}
</div>
</section>
)}
{/* More Integrations */}
{filteredOther.length > 0 && (
<section>

View file

@ -30,6 +30,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
// Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",
WEBCRAWLER_CONNECTOR: "CRAWLED_URL",
COMPOSIO_CONNECTOR: "COMPOSIO_CONNECTOR",
};
/**

View file

@ -0,0 +1,301 @@
"use client";
import {
ArrowLeft,
Calendar,
Check,
ExternalLink,
Github,
Loader2,
Mail,
HardDrive,
MessageSquare,
FileText,
Zap,
} from "lucide-react";
import Image from "next/image";
import type { FC } from "react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ComposioToolkit {
id: string;
name: string;
description: string;
isIndexable: boolean;
}
interface ComposioToolkitViewProps {
searchSpaceId: string;
connectedToolkits: string[];
onBack: () => void;
onConnectToolkit: (toolkitId: string) => void;
isConnecting: boolean;
connectingToolkitId: string | null;
}
// Available Composio toolkits
const COMPOSIO_TOOLKITS: ComposioToolkit[] = [
{
id: "googledrive",
name: "Google Drive",
description: "Search your Drive files and documents",
isIndexable: true,
},
{
id: "gmail",
name: "Gmail",
description: "Search through your emails",
isIndexable: true,
},
{
id: "googlecalendar",
name: "Google Calendar",
description: "Search through your events",
isIndexable: true,
},
{
id: "slack",
name: "Slack",
description: "Search Slack messages",
isIndexable: false,
},
{
id: "notion",
name: "Notion",
description: "Search Notion pages",
isIndexable: false,
},
{
id: "github",
name: "GitHub",
description: "Search repositories and code",
isIndexable: false,
},
];
// Get icon for toolkit
const getToolkitIcon = (toolkitId: string, className?: string) => {
const iconClass = className || "size-5";
switch (toolkitId) {
case "googledrive":
return <Image src="/connectors/google-drive.svg" alt="Google Drive" width={20} height={20} className={iconClass} />;
case "gmail":
return <Image src="/connectors/google-gmail.svg" alt="Gmail" width={20} height={20} className={iconClass} />;
case "googlecalendar":
return <Image src="/connectors/google-calendar.svg" alt="Google Calendar" width={20} height={20} className={iconClass} />;
case "slack":
return <Image src="/connectors/slack.svg" alt="Slack" width={20} height={20} className={iconClass} />;
case "notion":
return <Image src="/connectors/notion.svg" alt="Notion" width={20} height={20} className={iconClass} />;
case "github":
return <Image src="/connectors/github.svg" alt="GitHub" width={20} height={20} className={iconClass} />;
default:
return <Zap className={iconClass} />;
}
};
export const ComposioToolkitView: FC<ComposioToolkitViewProps> = ({
searchSpaceId,
connectedToolkits,
onBack,
onConnectToolkit,
isConnecting,
connectingToolkitId,
}) => {
const [hoveredToolkit, setHoveredToolkit] = useState<string | null>(null);
// Separate indexable and non-indexable toolkits
const indexableToolkits = COMPOSIO_TOOLKITS.filter((t) => t.isIndexable);
const nonIndexableToolkits = COMPOSIO_TOOLKITS.filter((t) => !t.isIndexable);
return (
<div className="flex flex-col h-full">
{/* Header */}
<div className="px-6 sm:px-12 pt-8 sm:pt-10 pb-4 sm:pb-6 border-b border-border/50 bg-muted">
{/* Back button */}
<button
type="button"
onClick={onBack}
className="flex items-center gap-2 text-xs sm:text-sm text-muted-foreground hover:text-foreground mb-6 w-fit transition-colors"
>
<ArrowLeft className="size-4" />
Back to connectors
</button>
{/* Header content */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="flex gap-4 flex-1 w-full sm:w-auto">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-violet-500/20 to-purple-500/20 border border-violet-500/30 shrink-0">
<Image
src="/connectors/composio.svg"
alt="Composio"
width={28}
height={28}
className="size-7"
/>
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">
Composio
</h2>
<p className="text-xs sm:text-sm text-muted-foreground mt-1">
Connect 100+ apps with managed OAuth - no verification needed
</p>
</div>
</div>
<a
href="https://composio.dev"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
<span>Powered by Composio</span>
<ExternalLink className="size-3" />
</a>
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 sm:px-12 py-6 sm:py-8">
{/* Indexable Toolkits (Google Services) */}
<section className="mb-8">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-foreground">Google Services</h3>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20">
Indexable
</Badge>
</div>
<p className="text-xs text-muted-foreground mb-4">
Connect Google services via Composio&apos;s verified OAuth app. Your data will be indexed and searchable.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{indexableToolkits.map((toolkit) => {
const isConnected = connectedToolkits.includes(toolkit.id);
const isThisConnecting = connectingToolkitId === toolkit.id;
return (
<div
key={toolkit.id}
onMouseEnter={() => setHoveredToolkit(toolkit.id)}
onMouseLeave={() => setHoveredToolkit(null)}
className={cn(
"group relative flex flex-col p-4 rounded-xl border transition-all duration-200",
isConnected
? "border-emerald-500/30 bg-emerald-500/5"
: "border-border bg-card hover:border-violet-500/30 hover:bg-violet-500/5"
)}
>
<div className="flex items-start justify-between mb-3">
<div
className={cn(
"flex h-10 w-10 items-center justify-center rounded-lg border transition-colors",
isConnected
? "bg-emerald-500/10 border-emerald-500/20"
: "bg-muted border-border group-hover:border-violet-500/20 group-hover:bg-violet-500/10"
)}
>
{getToolkitIcon(toolkit.id, "size-5")}
</div>
{isConnected && (
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20">
<Check className="size-3 mr-0.5" />
Connected
</Badge>
)}
</div>
<h4 className="text-sm font-medium mb-1">{toolkit.name}</h4>
<p className="text-xs text-muted-foreground mb-4 flex-1">
{toolkit.description}
</p>
<Button
size="sm"
variant={isConnected ? "secondary" : "default"}
className={cn(
"w-full h-8 text-xs font-medium",
!isConnected && "bg-violet-600 hover:bg-violet-700 text-white"
)}
onClick={() => onConnectToolkit(toolkit.id)}
disabled={isConnecting || isConnected}
>
{isThisConnecting ? (
<>
<Loader2 className="size-3 mr-1.5 animate-spin" />
Connecting...
</>
) : isConnected ? (
"Connected"
) : (
"Connect"
)}
</Button>
</div>
);
})}
</div>
</section>
{/* Non-Indexable Toolkits (Coming Soon) */}
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-foreground">More Integrations</h3>
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 h-5 bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20">
Coming Soon
</Badge>
</div>
<p className="text-xs text-muted-foreground mb-4">
Connect these services for future indexing support. Currently available for connection only.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 opacity-60">
{nonIndexableToolkits.map((toolkit) => (
<div
key={toolkit.id}
className="group relative flex flex-col p-4 rounded-xl border border-border bg-card/50"
>
<div className="flex items-start justify-between mb-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg border bg-muted border-border">
{getToolkitIcon(toolkit.id, "size-5")}
</div>
<Badge variant="outline" className="text-[10px] px-1.5 py-0 h-5">
Soon
</Badge>
</div>
<h4 className="text-sm font-medium mb-1">{toolkit.name}</h4>
<p className="text-xs text-muted-foreground mb-4 flex-1">
{toolkit.description}
</p>
<Button
size="sm"
variant="outline"
className="w-full h-8 text-xs font-medium"
disabled
>
Coming Soon
</Button>
</div>
))}
</div>
</section>
{/* Info footer */}
<div className="mt-8 p-4 rounded-xl bg-muted/50 border border-border/50">
<div className="flex items-start gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-500/10 border border-violet-500/20 shrink-0">
<Zap className="size-4 text-violet-500" />
</div>
<div>
<h4 className="text-sm font-medium mb-1">Why use Composio?</h4>
<p className="text-xs text-muted-foreground leading-relaxed">
Composio provides pre-verified OAuth apps, so you don&apos;t need to wait for Google app verification.
Your data is securely processed through Composio&apos;s managed authentication.
</p>
</div>
</div>
</div>
</div>
</div>
);
};