feat: Add Circleback connector integration

- Implement CirclebackConnectForm for user input and configuration
- Update connector benefits to include Circleback features
- Add CirclebackConfig component for webhook configuration
- Integrate Circleback into connector selection and management UI
- Update connector constants and document mappings for Circleback
This commit is contained in:
Anish Sarkar 2026-01-01 02:59:49 +05:30
parent 2373421212
commit e2f4f6c8e0
23 changed files with 370 additions and 296 deletions

View file

@ -0,0 +1,121 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info, Webhook } from "lucide-react";
import type { FC } from "react";
import { useRef } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { ConnectFormProps } from "../index";
import { getConnectorBenefits } from "../connector-benefits";
const circlebackFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
});
type CirclebackFormValues = z.infer<typeof circlebackFormSchema>;
export const CirclebackConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false);
const form = useForm<CirclebackFormValues>({
resolver: zodResolver(circlebackFormSchema),
defaultValues: {
name: "Circleback Connector",
},
});
const handleSubmit = async (values: CirclebackFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.CIRCLEBACK_CONNECTOR,
config: {},
is_indexable: false,
last_indexed_at: null,
periodic_indexing_enabled: false,
indexing_frequency_minutes: null,
next_scheduled_at: null,
});
} finally {
isSubmittingRef.current = false;
}
};
return (
<div className="space-y-6 pb-6">
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
<Webhook className="h-3 w-3 sm:h-4 sm:w-4 shrink-0 ml-1" />
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Webhook-Based Integration</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
Circleback uses webhooks to automatically send meeting data. After connecting, you'll receive a webhook URL to configure in your Circleback settings.
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form id="circleback-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Circleback Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.CIRCLEBACK_CONNECTOR) && (
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
<h4 className="text-xs sm:text-sm font-medium">What you get with Circleback:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.CIRCLEBACK_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>
)}
</div>
);
};

View file

@ -101,6 +101,13 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
"Keep your search results up-to-date with latest Luma content",
"Index your Luma events for enhanced search capabilities",
],
CIRCLEBACK_CONNECTOR: [
"Automatically receive meeting notes, transcripts, and action items",
"Access meeting details, attendees, and insights",
"Search through all your Circleback meeting records",
"Real-time updates via webhook integration",
"No manual indexing required - meetings are added automatically",
],
};
return benefits[connectorType] || null;

View file

@ -1,6 +1,7 @@
import type { FC } from "react";
import { BaiduSearchApiConnectForm } from "./components/baidu-search-api-connect-form";
import { BookStackConnectForm } from "./components/bookstack-connect-form";
import { CirclebackConnectForm } from "./components/circleback-connect-form";
import { ClickUpConnectForm } from "./components/clickup-connect-form";
import { ConfluenceConnectForm } from "./components/confluence-connect-form";
import { DiscordConnectForm } from "./components/discord-connect-form";
@ -74,6 +75,8 @@ export function getConnectFormComponent(
return ClickUpConnectForm;
case "LUMA_CONNECTOR":
return LumaConnectForm;
case "CIRCLEBACK_CONNECTOR":
return CirclebackConnectForm;
// Add other connector types here as needed
default:
return null;

View file

@ -0,0 +1,156 @@
"use client";
import { Copy, Webhook, Check } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import type { ConnectorConfigProps } from "../index";
import { authenticatedFetch } from "@/lib/auth-utils";
export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const CirclebackConfig: FC<CirclebackConfigProps> = ({
connector,
onNameChange,
}) => {
const [name, setName] = useState<string>(connector.name || "");
const [webhookUrl, setWebhookUrl] = useState<string>("");
const [webhookInfo, setWebhookInfo] = useState<{ webhook_url: string; search_space_id: number; method: string; content_type: string; description: string; note: string } | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [copied, setCopied] = useState(false);
// Update name when connector changes
useEffect(() => {
setName(connector.name || "");
}, [connector.name]);
// Fetch webhook info
useEffect(() => {
const fetchWebhookInfo = async () => {
if (!connector.search_space_id) return;
setIsLoading(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/webhooks/circleback/${connector.search_space_id}/info`
);
if (response.ok) {
const data = await response.json();
setWebhookInfo(data);
setWebhookUrl(data.webhook_url || "");
}
} catch (error) {
console.error("Failed to fetch webhook info:", error);
} finally {
setIsLoading(false);
}
};
fetchWebhookInfo();
}, [connector.search_space_id]);
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
const handleCopyWebhookUrl = async () => {
if (webhookUrl) {
await navigator.clipboard.writeText(webhookUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Circleback Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
</div>
</div>
{/* Webhook Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base flex items-center gap-2">
<Webhook className="h-4 w-4" />
Webhook Configuration
</h3>
</div>
{isLoading ? (
<p className="text-[10px] sm:text-xs text-muted-foreground">
Loading webhook information...
</p>
) : webhookUrl ? (
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Webhook URL</Label>
<div className="flex gap-2">
<Input
value={webhookUrl}
readOnly
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono text-xs"
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCopyWebhookUrl}
className="shrink-0"
>
{copied ? (
<>
<Check className="h-4 w-4 mr-2" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4 mr-2" />
Copy
</>
)}
</Button>
</div>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Use this URL in your Circleback automation settings to send meeting data to SurfSense.
</p>
</div>
) : (
<p className="text-[10px] sm:text-xs text-muted-foreground">
Unable to load webhook URL. Please try refreshing the page.
</p>
)}
{webhookInfo && (
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Webhook className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-xs sm:text-sm">Configuration Instructions</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0 mt-1">
Configure this URL in Circleback Settings Automations Create automation Send webhook request.
The webhook will automatically send meeting notes, transcripts, and action items to this search space.
</AlertDescription>
</Alert>
)}
</div>
</div>
);
};

View file

@ -4,6 +4,7 @@ import type { FC } from "react";
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 { ClickUpConfig } from "./components/clickup-config";
import { ConfluenceConfig } from "./components/confluence-config";
import { DiscordConfig } from "./components/discord-config";
@ -69,6 +70,8 @@ export function getConnectorConfigComponent(
return ClickUpConfig;
case "LUMA_CONNECTOR":
return LumaConfig;
case "CIRCLEBACK_CONNECTOR":
return CirclebackConfig;
// OAuth connectors (Gmail, Calendar, Airtable) and others don't need special config UI
default:
return null;

View file

@ -62,6 +62,7 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
JIRA_CONNECTOR: "jira-connect-form",
CLICKUP_CONNECTOR: "clickup-connect-form",
LUMA_CONNECTOR: "luma-connect-form",
CIRCLEBACK_CONNECTOR: "circleback-connect-form",
};
const formId = formIdMap[connectorType];
if (formId) {

View file

@ -38,7 +38,7 @@ export const CRAWLERS = [
id: "youtube-crawler",
title: "YouTube",
description: "Crawl YouTube channels and playlists",
connectorType: null, // Not a connector, handled separately
connectorType: EnumConnectorName.YOUTUBE_CONNECTOR,
},
{
id: "webcrawler-connector",
@ -140,6 +140,12 @@ export const OTHER_CONNECTORS = [
description: "Search with Baidu",
connectorType: EnumConnectorName.BAIDU_SEARCH_API,
},
{
id: "circleback-connector",
title: "Circleback",
description: "Receive meeting notes via webhook",
connectorType: EnumConnectorName.CIRCLEBACK_CONNECTOR,
},
] as const;
// Re-export IndexingConfigState from schemas for backward compatibility

View file

@ -183,23 +183,24 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredOther.map((connector) => {
// Special handling for connectors that can be created in popup
const isWebcrawler = connector.id === "webcrawler-connector";
const isTavily = connector.id === "tavily-api";
const isSearxng = connector.id === "searxng";
const isLinkup = connector.id === "linkup-api";
const isBaidu = connector.id === "baidu-search-api";
const isLinear = connector.id === "linear-connector";
const isElasticsearch = connector.id === "elasticsearch-connector";
const isSlack = connector.id === "slack-connector";
const isDiscord = connector.id === "discord-connector";
const isNotion = connector.id === "notion-connector";
const isConfluence = connector.id === "confluence-connector";
const isBookStack = connector.id === "bookstack-connector";
const isGithub = connector.id === "github-connector";
const isJira = connector.id === "jira-connector";
const isClickUp = connector.id === "clickup-connector";
const isLuma = connector.id === "luma-connector";
// Special handling for connectors that can be created in popup
const isWebcrawler = connector.id === "webcrawler-connector";
const isTavily = connector.id === "tavily-api";
const isSearxng = connector.id === "searxng";
const isLinkup = connector.id === "linkup-api";
const isBaidu = connector.id === "baidu-search-api";
const isLinear = connector.id === "linear-connector";
const isElasticsearch = connector.id === "elasticsearch-connector";
const isSlack = connector.id === "slack-connector";
const isDiscord = connector.id === "discord-connector";
const isNotion = connector.id === "notion-connector";
const isConfluence = connector.id === "confluence-connector";
const isBookStack = connector.id === "bookstack-connector";
const isGithub = connector.id === "github-connector";
const isJira = connector.id === "jira-connector";
const isClickUp = connector.id === "clickup-connector";
const isLuma = connector.id === "luma-connector";
const isCircleback = connector.id === "circleback-connector";
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
@ -215,7 +216,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma) && onConnectNonOAuth
: (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch || isSlack || isDiscord || isNotion || isConfluence || isBookStack || isGithub || isJira || isClickUp || isLuma || isCircleback) && onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);

View file

@ -24,6 +24,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
LUMA_CONNECTOR: "LUMA_CONNECTOR",
ELASTICSEARCH_CONNECTOR: "ELASTICSEARCH_CONNECTOR",
BOOKSTACK_CONNECTOR: "BOOKSTACK_CONNECTOR",
CIRCLEBACK_CONNECTOR: "CIRCLEBACK",
// Special mappings (connector type differs from document type)
GOOGLE_DRIVE_CONNECTOR: "GOOGLE_DRIVE_FILE",

View file

@ -1,6 +1,5 @@
"use client";
import { IconBrandYoutube } from "@tabler/icons-react";
import { ArrowLeft } from "lucide-react";
import { TagInput, type Tag as TagType } from "emblor";
import { useAtom } from "jotai";
@ -12,6 +11,8 @@ import { toast } from "sonner";
import { createDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
const youtubeRegex =
/^(https:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})$/;
@ -129,7 +130,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
<div className="flex items-center gap-4 mb-6">
<div className="flex h-14 w-14 items-center justify-center rounded-xl border border-slate-400/30">
<IconBrandYoutube className="h-7 w-7" />
{getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")}
</div>
<div>
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">
@ -236,10 +237,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
{t("processing")}
</>
) : (
<>
<IconBrandYoutube className="mr-2 h-4 w-4" />
{t("submit")}
</>
t("submit")
)}
</Button>
</div>