From b26768cec5a8c3b680150123431f0811dccb2d65 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Wed, 31 Dec 2025 15:42:50 +0530 Subject: [PATCH] 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. --- .../baidu-search-api-connect-form.tsx | 158 ++++++++ .../components/linkup-api-connect-form.tsx | 158 ++++++++ .../components/searxng-connect-form.tsx | 343 ++++++++++++++++++ .../connect-forms/connector-benefits.ts | 15 + .../connector-popup/connect-forms/index.tsx | 9 + .../components/baidu-search-api-config.tsx | 92 +++++ .../components/linkup-api-config.tsx | 92 +++++ .../components/searxng-config.tsx | 322 ++++++++++++++++ .../connector-configs/index.tsx | 9 + .../views/connector-connect-view.tsx | 3 + .../hooks/use-connector-dialog.ts | 9 +- .../tabs/all-connectors-tab.tsx | 5 +- 12 files changed, 1211 insertions(+), 4 deletions(-) create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/baidu-search-api-config.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linkup-api-config.tsx create mode 100644 surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/searxng-config.tsx diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx new file mode 100644 index 000000000..d84bd7797 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/baidu-search-api-connect-form.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info } 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 baiduSearchApiFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), +}); + +type BaiduSearchApiFormValues = z.infer; + +export const BaiduSearchApiConnectForm: FC = ({ + onSubmit, + isSubmitting, +}) => { + const isSubmittingRef = useRef(false); + const form = useForm({ + resolver: zodResolver(baiduSearchApiFormSchema), + defaultValues: { + name: "Baidu Search Connector", + api_key: "", + }, + }); + + const handleSubmit = async (values: BaiduSearchApiFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.BAIDU_SEARCH_API, + config: { + BAIDU_API_KEY: values.api_key, + }, + is_indexable: false, + last_indexed_at: null, + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + next_scheduled_at: null, + }); + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ API Key Required + + You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing up at{" "} + + qianfan.cloud.baidu.com + + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + Baidu AppBuilder API Key + + + + + Your API key will be encrypted and stored securely. + + + + )} + /> + + +
+ + {/* What you get section */} + {getConnectorBenefits(EnumConnectorName.BAIDU_SEARCH_API) && ( +
+

What you get with Baidu Search:

+
    + {getConnectorBenefits(EnumConnectorName.BAIDU_SEARCH_API)?.map((benefit) => ( +
  • {benefit}
  • + ))} +
+
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx new file mode 100644 index 000000000..dedb8c72e --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/linkup-api-connect-form.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info } 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 linkupApiFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + api_key: z.string().min(10, { + message: "API key is required and must be valid.", + }), +}); + +type LinkupApiFormValues = z.infer; + +export const LinkupApiConnectForm: FC = ({ + onSubmit, + isSubmitting, +}) => { + const isSubmittingRef = useRef(false); + const form = useForm({ + resolver: zodResolver(linkupApiFormSchema), + defaultValues: { + name: "Linkup API Connector", + api_key: "", + }, + }); + + const handleSubmit = async (values: LinkupApiFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.LINKUP_API, + config: { + LINKUP_API_KEY: values.api_key, + }, + is_indexable: false, + last_indexed_at: null, + periodic_indexing_enabled: false, + indexing_frequency_minutes: null, + next_scheduled_at: null, + }); + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ API Key Required + + You'll need a Linkup API key to use this connector. You can get one by signing up at{" "} + + linkup.ai + + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + Linkup API Key + + + + + Your API key will be encrypted and stored securely. + + + + )} + /> + + +
+ + {/* What you get section */} + {getConnectorBenefits(EnumConnectorName.LINKUP_API) && ( +
+

What you get with Linkup API:

+
    + {getConnectorBenefits(EnumConnectorName.LINKUP_API)?.map((benefit) => ( +
  • {benefit}
  • + ))} +
+
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx new file mode 100644 index 000000000..b1c19a612 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/searxng-connect-form.tsx @@ -0,0 +1,343 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info } 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 { Switch } from "@/components/ui/switch"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import type { ConnectFormProps } from "../index"; +import { getConnectorBenefits } from "../connector-benefits"; + +const searxngFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + host: z + .string({ required_error: "Host is required." }) + .url({ message: "Enter a valid SearxNG host URL (e.g. https://searxng.example.org)." }), + api_key: z.string().optional(), + engines: z.string().optional(), + categories: z.string().optional(), + language: z.string().optional(), + safesearch: z + .string() + .regex(/^[0-2]?$/, { message: "SafeSearch must be 0, 1, or 2." }) + .optional(), + verify_ssl: z.boolean().default(true), +}); + +type SearxngFormValues = z.infer; + +const parseCommaSeparated = (value?: string | null) => { + 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 SearxngConnectForm: FC = ({ + onSubmit, + isSubmitting, +}) => { + const isSubmittingRef = useRef(false); + const form = useForm({ + resolver: zodResolver(searxngFormSchema), + defaultValues: { + name: "SearxNG Connector", + host: "", + api_key: "", + engines: "", + categories: "", + language: "", + safesearch: "", + verify_ssl: true, + }, + }); + + const handleSubmit = async (values: SearxngFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + const config: Record = { + SEARXNG_HOST: values.host.trim(), + }; + + const apiKey = values.api_key?.trim(); + if (apiKey) config.SEARXNG_API_KEY = apiKey; + + const engines = parseCommaSeparated(values.engines); + if (engines) config.SEARXNG_ENGINES = engines; + + const categories = parseCommaSeparated(values.categories); + if (categories) config.SEARXNG_CATEGORIES = categories; + + const language = values.language?.trim(); + if (language) config.SEARXNG_LANGUAGE = language; + + const safesearch = values.safesearch?.trim(); + if (safesearch) { + const parsed = Number(safesearch); + if (!Number.isNaN(parsed)) { + config.SEARXNG_SAFESEARCH = parsed; + } + } + + // Include verify flag only when disabled to keep config minimal + if (values.verify_ssl === false) { + config.SEARXNG_VERIFY_SSL = false; + } + + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.SEARXNG_API, + 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 ( +
+ + +
+ SearxNG Instance Required + + You need access to a running SearxNG instance. Refer to the{" "} + + SearxNG installation guide + {" "} + for setup instructions. If your instance requires an API key, include it below. + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + ( + + SearxNG Host + + + + + Provide the full base URL to your SearxNG instance. Include the protocol (http/https). + + + + )} + /> + + ( + + API Key (optional) + + + + + Leave empty if your SearxNG instance does not enforce API keys. + + + + )} + /> + +
+ ( + + Engines (optional) + + + + + Comma-separated list to target specific engines. + + + + )} + /> + + ( + + Categories (optional) + + + + + Comma-separated list of SearxNG categories. + + + + )} + /> +
+ +
+ ( + + Preferred Language (optional) + + + + + IETF language tag (e.g. en, en-US). Leave blank to inherit defaults. + + + + )} + /> + + ( + + SafeSearch Level (optional) + + + + + Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance default. + + + + )} + /> +
+ + ( + +
+ Verify SSL Certificates + + Disable only when connecting to instances with self-signed certificates. + +
+ + + +
+ )} + /> + + +
+ + {/* What you get section */} + {getConnectorBenefits(EnumConnectorName.SEARXNG_API) && ( +
+

What you get with SearxNG:

+
    + {getConnectorBenefits(EnumConnectorName.SEARXNG_API)?.map((benefit) => ( +
  • {benefit}
  • + ))} +
+
+ )} +
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts index 7dd6dd5cf..362a8ffa3 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts @@ -23,6 +23,21 @@ export function getConnectorBenefits(connectorType: string): string[] | null { "Real-time information from the web", "Enhanced search capabilities for your projects", ], + SEARXNG_API: [ + "Privacy-focused meta-search across multiple engines", + "Self-hosted search instance for full control", + "Real-time web search results from multiple sources", + ], + LINKUP_API: [ + "AI-powered search results tailored to your queries", + "Real-time information from the web", + "Enhanced search capabilities for your projects", + ], + BAIDU_SEARCH_API: [ + "Intelligent search tailored for Chinese web content", + "Real-time information from Baidu's search index", + "AI-powered summarization with source references", + ], // Add other connectors as needed // GITHUB_CONNECTOR: [...], }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index 2c0ead6ec..2a1170303 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -1,6 +1,9 @@ import type { FC } from "react"; +import { BaiduSearchApiConnectForm } from "./components/baidu-search-api-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { LinearConnectForm } from "./components/linear-connect-form"; +import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; +import { SearxngConnectForm } from "./components/searxng-connect-form"; import { TavilyApiConnectForm } from "./components/tavily-api-connect-form"; export interface ConnectFormProps { @@ -34,6 +37,12 @@ export function getConnectFormComponent( switch (connectorType) { case "TAVILY_API": return TavilyApiConnectForm; + case "SEARXNG_API": + return SearxngConnectForm; + case "LINKUP_API": + return LinkupApiConnectForm; + case "BAIDU_SEARCH_API": + return BaiduSearchApiConnectForm; case "LINEAR_CONNECTOR": return LinearConnectForm; case "ELASTICSEARCH_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/baidu-search-api-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/baidu-search-api-config.tsx new file mode 100644 index 000000000..7089878c9 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/baidu-search-api-config.tsx @@ -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 = ({ + connector, + onConfigChange, + onNameChange, +}) => { + const [apiKey, setApiKey] = useState( + (connector.config?.BAIDU_API_KEY as string) || "" + ); + const [name, setName] = useState(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 ( +
+ {/* Connector Name */} +
+
+ + handleNameChange(e.target.value)} + placeholder="My Baidu Search Connector" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ A friendly name to identify this connector. +

+
+
+ + {/* Configuration */} +
+
+

Configuration

+
+ +
+ + handleApiKeyChange(e.target.value)} + placeholder="Enter your Baidu API key" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Update the Baidu API Key if needed. +

+
+
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linkup-api-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linkup-api-config.tsx new file mode 100644 index 000000000..95bc6e823 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/linkup-api-config.tsx @@ -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 = ({ + connector, + onConfigChange, + onNameChange, +}) => { + const [apiKey, setApiKey] = useState( + (connector.config?.LINKUP_API_KEY as string) || "" + ); + const [name, setName] = useState(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 ( +
+ {/* Connector Name */} +
+
+ + handleNameChange(e.target.value)} + placeholder="My Linkup API Connector" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ A friendly name to identify this connector. +

+
+
+ + {/* Configuration */} +
+
+

Configuration

+
+ +
+ + handleApiKeyChange(e.target.value)} + placeholder="Enter your Linkup API key" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Update the Linkup API Key if needed. +

+
+
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/searxng-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/searxng-config.tsx new file mode 100644 index 000000000..3b2b37f84 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/searxng-config.tsx @@ -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 = ({ + connector, + onConfigChange, + onNameChange, +}) => { + const [host, setHost] = useState( + (connector.config?.SEARXNG_HOST as string) || "" + ); + const [apiKey, setApiKey] = useState( + (connector.config?.SEARXNG_API_KEY as string) || "" + ); + const [engines, setEngines] = useState( + arrayToString(connector.config?.SEARXNG_ENGINES) + ); + const [categories, setCategories] = useState( + arrayToString(connector.config?.SEARXNG_CATEGORIES) + ); + const [language, setLanguage] = useState( + (connector.config?.SEARXNG_LANGUAGE as string) || "" + ); + const [safesearch, setSafesearch] = useState( + connector.config?.SEARXNG_SAFESEARCH !== undefined + ? String(connector.config.SEARXNG_SAFESEARCH) + : "" + ); + const [verifySsl, setVerifySsl] = useState( + connector.config?.SEARXNG_VERIFY_SSL !== undefined + ? (connector.config.SEARXNG_VERIFY_SSL as boolean) + : true + ); + const [name, setName] = useState(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) => { + 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 ( +
+ {/* Connector Name */} +
+
+ + handleNameChange(e.target.value)} + placeholder="My SearxNG Connector" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ A friendly name to identify this connector. +

+
+
+ + {/* Configuration */} +
+
+

Configuration

+
+ +
+
+ + handleHostChange(e.target.value)} + placeholder="https://searxng.example.org" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Update the SearxNG Host if needed. +

+
+ +
+ + handleApiKeyChange(e.target.value)} + placeholder="Enter API key if your instance requires one" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Leave empty if your SearxNG instance does not enforce API keys. +

+
+ +
+
+ + handleEnginesChange(e.target.value)} + placeholder="google,bing,duckduckgo" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Comma-separated list to target specific engines. +

+
+ +
+ + handleCategoriesChange(e.target.value)} + placeholder="general,it,science" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Comma-separated list of SearxNG categories. +

+
+
+ +
+
+ + handleLanguageChange(e.target.value)} + placeholder="en-US" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ IETF language tag (e.g. en, en-US). Leave blank to inherit defaults. +

+
+ +
+ + handleSafesearchChange(e.target.value)} + placeholder="0 (off), 1 (moderate), 2 (strict)" + className="border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance default. +

+
+
+ +
+
+ +

+ Disable only when connecting to instances with self-signed certificates. +

+
+ +
+
+
+
+ ); +}; + diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index b03ed574d..5674a4a3e 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -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": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index eb0102008..a7c95fea8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -44,6 +44,9 @@ export const ConnectorConnectView: FC = ({ // Map connector types to their form IDs const formIdMap: Record = { 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", }; diff --git a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts index b3114b631..7843561e8 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/hooks/use-connector-dialog.ts @@ -657,14 +657,17 @@ export const useConnectorDialog = () => { (oauthConnector) => oauthConnector.connectorType === connector.connector_type ); - // Check if this is webcrawler, Tavily API, Linear, or Elasticsearch (can be managed in popup) + // Check if this is webcrawler, Tavily API, SearxNG, Linkup, Baidu, Linear, or Elasticsearch (can be managed in popup) const isWebcrawler = connector.connector_type === EnumConnectorName.WEBCRAWLER_CONNECTOR; const isTavilyApi = connector.connector_type === EnumConnectorName.TAVILY_API; + const isSearxng = connector.connector_type === EnumConnectorName.SEARXNG_API; + const isLinkup = connector.connector_type === EnumConnectorName.LINKUP_API; + const isBaidu = connector.connector_type === EnumConnectorName.BAIDU_SEARCH_API; const isLinear = connector.connector_type === EnumConnectorName.LINEAR_CONNECTOR; const isElasticsearch = connector.connector_type === EnumConnectorName.ELASTICSEARCH_CONNECTOR; - // If not OAuth, not webcrawler, not Tavily API, not Linear, and not Elasticsearch, redirect to old connector edit page - if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isLinear && !isElasticsearch) { + // If not OAuth, not webcrawler, not Tavily API, not SearxNG, not Linkup, not Baidu, not Linear, and not Elasticsearch, redirect to old connector edit page + if (!isOAuthConnector && !isWebcrawler && !isTavilyApi && !isSearxng && !isLinkup && !isBaidu && !isLinear && !isElasticsearch) { router.push(`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`); return; } diff --git a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx index 887961bed..0d5d0ef1c 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/tabs/all-connectors-tab.tsx @@ -94,6 +94,9 @@ export const AllConnectorsTab: FC = ({ // 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"; @@ -107,7 +110,7 @@ export const AllConnectorsTab: FC = ({ const handleConnect = isWebcrawler && onCreateWebcrawler ? onCreateWebcrawler - : (isTavily || isLinear || isElasticsearch) && onConnectNonOAuth + : (isTavily || isSearxng || isLinkup || isBaidu || isLinear || isElasticsearch) && onConnectNonOAuth ? () => onConnectNonOAuth(connector.connectorType) : () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${connector.id}`);