feat: Add support for SearxNG, Linkup, and Baidu Search connectors, including configuration forms and benefits display, enhance connector dialog for new connectors, and improve overall connector management functionality.

This commit is contained in:
Anish Sarkar 2025-12-31 15:42:50 +05:30
parent 36d25e9505
commit b26768cec5
12 changed files with 1211 additions and 4 deletions

View file

@ -0,0 +1,92 @@
"use client";
import { KeyRound } 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 type { ConnectorConfigProps } from "../index";
export interface BaiduSearchApiConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const BaiduSearchApiConfig: FC<BaiduSearchApiConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.BAIDU_API_KEY as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
useEffect(() => {
const key = (connector.config?.BAIDU_API_KEY as string) || "";
setApiKey(key);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleApiKeyChange = (value: string) => {
setApiKey(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
BAIDU_API_KEY: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
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 Baidu Search 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>
{/* 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">Configuration</h3>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
Baidu AppBuilder API Key
</Label>
<Input
type="password"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder="Enter your Baidu API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update the Baidu API Key if needed.
</p>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,92 @@
"use client";
import { KeyRound } 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 type { ConnectorConfigProps } from "../index";
export interface LinkupApiConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const LinkupApiConfig: FC<LinkupApiConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.LINKUP_API_KEY as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
useEffect(() => {
const key = (connector.config?.LINKUP_API_KEY as string) || "";
setApiKey(key);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleApiKeyChange = (value: string) => {
setApiKey(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
LINKUP_API_KEY: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
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 Linkup API 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>
{/* 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">Configuration</h3>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
Linkup API Key
</Label>
<Input
type="password"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder="Enter your Linkup API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update the Linkup API Key if needed.
</p>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,322 @@
"use client";
import { KeyRound, Globe } 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 { Switch } from "@/components/ui/switch";
import type { ConnectorConfigProps } from "../index";
export interface SearxngConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
const arrayToString = (arr: unknown): string => {
if (!arr) return "";
if (Array.isArray(arr)) {
return arr.join(", ");
}
return String(arr);
};
const stringToArray = (value: string): string[] | undefined => {
if (!value) return undefined;
const items = value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
return items.length > 0 ? items : undefined;
};
export const SearxngConfig: FC<SearxngConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [host, setHost] = useState<string>(
(connector.config?.SEARXNG_HOST as string) || ""
);
const [apiKey, setApiKey] = useState<string>(
(connector.config?.SEARXNG_API_KEY as string) || ""
);
const [engines, setEngines] = useState<string>(
arrayToString(connector.config?.SEARXNG_ENGINES)
);
const [categories, setCategories] = useState<string>(
arrayToString(connector.config?.SEARXNG_CATEGORIES)
);
const [language, setLanguage] = useState<string>(
(connector.config?.SEARXNG_LANGUAGE as string) || ""
);
const [safesearch, setSafesearch] = useState<string>(
connector.config?.SEARXNG_SAFESEARCH !== undefined
? String(connector.config.SEARXNG_SAFESEARCH)
: ""
);
const [verifySsl, setVerifySsl] = useState<boolean>(
connector.config?.SEARXNG_VERIFY_SSL !== undefined
? (connector.config.SEARXNG_VERIFY_SSL as boolean)
: true
);
const [name, setName] = useState<string>(connector.name || "");
// Update all fields when connector changes
useEffect(() => {
const hostValue = (connector.config?.SEARXNG_HOST as string) || "";
const apiKeyValue = (connector.config?.SEARXNG_API_KEY as string) || "";
const enginesValue = arrayToString(connector.config?.SEARXNG_ENGINES);
const categoriesValue = arrayToString(connector.config?.SEARXNG_CATEGORIES);
const languageValue = (connector.config?.SEARXNG_LANGUAGE as string) || "";
const safesearchValue =
connector.config?.SEARXNG_SAFESEARCH !== undefined
? String(connector.config.SEARXNG_SAFESEARCH)
: "";
const verifySslValue =
connector.config?.SEARXNG_VERIFY_SSL !== undefined
? (connector.config.SEARXNG_VERIFY_SSL as boolean)
: true;
setHost(hostValue);
setApiKey(apiKeyValue);
setEngines(enginesValue);
setCategories(categoriesValue);
setLanguage(languageValue);
setSafesearch(safesearchValue);
setVerifySsl(verifySslValue);
setName(connector.name || "");
}, [connector.config, connector.name]);
const updateConfig = (updates: Record<string, unknown>) => {
if (onConfigChange) {
onConfigChange({
...connector.config,
...updates,
});
}
};
const handleHostChange = (value: string) => {
setHost(value);
updateConfig({ SEARXNG_HOST: value });
};
const handleApiKeyChange = (value: string) => {
setApiKey(value);
if (value) {
updateConfig({ SEARXNG_API_KEY: value });
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_API_KEY;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleEnginesChange = (value: string) => {
setEngines(value);
const enginesArray = stringToArray(value);
if (enginesArray) {
updateConfig({ SEARXNG_ENGINES: enginesArray });
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_ENGINES;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleCategoriesChange = (value: string) => {
setCategories(value);
const categoriesArray = stringToArray(value);
if (categoriesArray) {
updateConfig({ SEARXNG_CATEGORIES: categoriesArray });
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_CATEGORIES;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleLanguageChange = (value: string) => {
setLanguage(value);
if (value) {
updateConfig({ SEARXNG_LANGUAGE: value });
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_LANGUAGE;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleSafesearchChange = (value: string) => {
setSafesearch(value);
if (value) {
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
updateConfig({ SEARXNG_SAFESEARCH: parsed });
}
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_SAFESEARCH;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleVerifySslChange = (value: boolean) => {
setVerifySsl(value);
if (value === false) {
updateConfig({ SEARXNG_VERIFY_SSL: false });
} else {
const newConfig = { ...connector.config };
delete newConfig.SEARXNG_VERIFY_SSL;
if (onConfigChange) {
onConfigChange(newConfig);
}
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
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 SearxNG 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>
{/* 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">Configuration</h3>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<Globe className="h-4 w-4" />
SearxNG Host
</Label>
<Input
value={host}
onChange={(e) => handleHostChange(e.target.value)}
placeholder="https://searxng.example.org"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update the SearxNG Host if needed.
</p>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
API Key (optional)
</Label>
<Input
type="password"
value={apiKey}
onChange={(e) => handleApiKeyChange(e.target.value)}
placeholder="Enter API key if your instance requires one"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Leave empty if your SearxNG instance does not enforce API keys.
</p>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Engines (optional)</Label>
<Input
value={engines}
onChange={(e) => handleEnginesChange(e.target.value)}
placeholder="google,bing,duckduckgo"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Comma-separated list to target specific engines.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Categories (optional)</Label>
<Input
value={categories}
onChange={(e) => handleCategoriesChange(e.target.value)}
placeholder="general,it,science"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Comma-separated list of SearxNG categories.
</p>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Preferred Language (optional)</Label>
<Input
value={language}
onChange={(e) => handleLanguageChange(e.target.value)}
placeholder="en-US"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
IETF language tag (e.g. en, en-US). Leave blank to inherit defaults.
</p>
</div>
<div className="space-y-2">
<Label className="text-xs sm:text-sm">SafeSearch Level (optional)</Label>
<Input
value={safesearch}
onChange={(e) => handleSafesearchChange(e.target.value)}
placeholder="0 (off), 1 (moderate), 2 (strict)"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance default.
</p>
</div>
</div>
<div className="flex items-center justify-between rounded-lg border border-slate-400/20 p-3 sm:p-4">
<div>
<Label className="text-xs sm:text-sm">Verify SSL Certificates</Label>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Disable only when connecting to instances with self-signed certificates.
</p>
</div>
<Switch checked={verifySsl} onCheckedChange={handleVerifySslChange} />
</div>
</div>
</div>
</div>
);
};

View file

@ -2,9 +2,12 @@
import type { FC } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { BaiduSearchApiConfig } from "./components/baidu-search-api-config";
import { ElasticsearchConfig } from "./components/elasticsearch-config";
import { GoogleDriveConfig } from "./components/google-drive-config";
import { LinearConfig } from "./components/linear-config";
import { LinkupApiConfig } from "./components/linkup-api-config";
import { SearxngConfig } from "./components/searxng-config";
import { TavilyApiConfig } from "./components/tavily-api-config";
import { WebcrawlerConfig } from "./components/webcrawler-config";
@ -27,6 +30,12 @@ export function getConnectorConfigComponent(
return GoogleDriveConfig;
case "TAVILY_API":
return TavilyApiConfig;
case "SEARXNG_API":
return SearxngConfig;
case "LINKUP_API":
return LinkupApiConfig;
case "BAIDU_SEARCH_API":
return BaiduSearchApiConfig;
case "LINEAR_CONNECTOR":
return LinearConfig;
case "WEBCRAWLER_CONNECTOR":

View file

@ -44,6 +44,9 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
// Map connector types to their form IDs
const formIdMap: Record<string, string> = {
TAVILY_API: "tavily-connect-form",
SEARXNG_API: "searxng-connect-form",
LINKUP_API: "linkup-api-connect-form",
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
LINEAR_CONNECTOR: "linear-connect-form",
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
};