import { useSendToAgentChat } from "@agent-native/core/client/agent-chat"; import { ExtensionSlot } from "@agent-native/core/client/extensions"; import { useT } from "@agent-native/core/client/i18n"; import { IconPlus, IconCheck, IconSettings, IconChevronLeft, IconArrowUp, } from "@tabler/icons-react"; import { useState, useEffect, useRef } from "react"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import { Spinner } from "@/components/ui/spinner"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useApolloPerson } from "@/hooks/use-apollo"; import { useIntegration, useAllIntegrations, useHubSpotContact, useGongCalls, usePylonContact, isAuthError, } from "@/hooks/use-integrations"; import type { MailIntegrationStatuses } from "@/lib/integration-status"; import { isMcpEmbedSurface } from "@/lib/mcp-embed"; import { cn } from "@/lib/utils"; function safeExternalHref(value?: string | null): string | null { if (!value) return null; try { const url = new URL(value.trim()); return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null; } catch { return null; } } // ─── Integration definitions ──────────────────────────────────────────────── type ProviderId = "apollo" | "hubspot" | "gong" | "pylon"; interface IntegrationDef { id: ProviderId; name: string; descriptionKey: string; keyPlaceholder: string; helpUrl: string; helpSteps: string[]; logo: React.ReactNode; } const INTEGRATIONS: IntegrationDef[] = [ { id: "apollo", name: "Apollo", descriptionKey: "mail.integrations.apolloDescription", keyPlaceholder: "Apollo API key...", helpUrl: "https://app.apollo.io/#/settings/integrations/api", helpSteps: [ "Log in to Apollo.io", "Go to Settings > Integrations > API", 'Click "Connect" to generate a key', ], logo: ( ), }, { id: "hubspot", name: "HubSpot", descriptionKey: "mail.integrations.hubspotDescription", keyPlaceholder: "HubSpot private app token...", helpUrl: "https://developers.hubspot.com/docs/api/private-apps", helpSteps: [ "Go to HubSpot > Settings > Integrations > Private Apps", "Create a private app with CRM scopes", "Copy the access token", ], logo: ( ), }, { id: "gong", name: "Gong", descriptionKey: "mail.integrations.gongDescription", keyPlaceholder: "Gong API key or access_key:secret...", helpUrl: "https://app.gong.io/company/api", helpSteps: [ "Go to Gong > Company Settings > API", "Generate API credentials", "Copy the access key (or key:secret)", ], logo: ( ), }, { id: "pylon", name: "Pylon", descriptionKey: "mail.integrations.pylonDescription", keyPlaceholder: "Pylon API token...", helpUrl: "https://docs.usepylon.com/pylon-docs/developer/api", helpSteps: [ "Go to Pylon > Settings > API", "Create an API token (requires Admin)", "Copy the bearer token", ], logo: ( ), }, ]; // ─── Main Sidebar Component ───────────────────────────────────────────────── export function IntegrationsSidebar({ email, displayName, recentEmails, threadId, focusedEmailId, }: { email: string; displayName: string; recentEmails: { id: string; subject: string }[]; threadId?: string; focusedEmailId?: string; }) { const statuses = useAllIntegrations(); const anyConnected = statuses.apollo || statuses.hubspot || statuses.gong || statuses.pylon; return (
{/* Integration data sections */} {statuses.apollo && } {statuses.hubspot && } {statuses.gong && } {statuses.pylon && } {/* Generic profile if nothing connected */} {!anyConnected && (

{displayName}

{displayName !== email && (

{email}

)}

{email.split("@")[1]}

)} {/* Recent emails */} {recentEmails.length > 0 && ( <>

Recent

{recentEmails.map((e) => (

{e.subject.length > 40 ? e.subject.slice(0, 40) + "..." : e.subject}

))}
)} {/* Tool extension-point slot — user-installed widgets render here */} {/* Integration setup */}
); } // ─── Integration Setup ────────────────────────────────────────────────────── function IntegrationSetup({ statuses }: { statuses: MailIntegrationStatuses }) { const [expanded, setExpanded] = useState(false); const [configuring, setConfiguring] = useState(null); if (configuring) { const def = INTEGRATIONS.find((i) => i.id === configuring)!; return ( setConfiguring(null)} /> ); } if (!expanded) { const connectedCount = [ statuses.apollo, statuses.hubspot, statuses.gong, statuses.pylon, ].filter(Boolean).length; return (
); } return (

Integrations

{INTEGRATIONS.map((def) => { const connected = statuses[def.id]; return ( setConfiguring(def.id)} /> ); })}
); } function AddIntegrationButton() { const t = useT(); const [open, setOpen] = useState(false); const [value, setValue] = useState(""); const textareaRef = useRef(null); const { send, codeRequiredDialog } = useSendToAgentChat(); useEffect(() => { if (open) { setTimeout(() => textareaRef.current?.focus(), 50); } else { setValue(""); } }, [open]); const handleSubmit = () => { const prompt = value.trim(); if (!prompt) return; send({ message: `Add a new integration to the sidebar: ${prompt}`, context: `The user wants to add a new integration to the contact sidebar. Their request: "${prompt}". Look at the existing integrations in client/components/email/IntegrationsSidebar.tsx and the pattern in server/routes/ and client/hooks/use-integrations.ts. Add the new provider following the same pattern: server route, hook, sidebar section, and logo. Use the real brand logo SVG if possible.`, submit: true, requiresCode: true, }); setValue(""); setOpen(false); }; return ( <> {codeRequiredDialog}
{t("mail.integrations.newIntegration")}