mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
Merge pull request #392 from Aki-07/feature/add-searxng-connector
Add Searxng connector
This commit is contained in:
commit
08661e686c
15 changed files with 838 additions and 7 deletions
|
|
@ -62,6 +62,7 @@ Open source and easy to deploy locally.
|
||||||
|
|
||||||
### ℹ️ **External Sources**
|
### ℹ️ **External Sources**
|
||||||
- Search Engines (Tavily, LinkUp)
|
- Search Engines (Tavily, LinkUp)
|
||||||
|
- SearxNG (self-hosted instances)
|
||||||
- Slack
|
- Slack
|
||||||
- Linear
|
- Linear
|
||||||
- Jira
|
- Jira
|
||||||
|
|
@ -303,4 +304,3 @@ For detailed contribution guidelines, please see our [CONTRIBUTING.md](CONTRIBUT
|
||||||
|
|
||||||
---
|
---
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
"""Add SearxNG connector enum value
|
||||||
|
|
||||||
|
Revision ID: 26
|
||||||
|
Revises: 25
|
||||||
|
Create Date: 2025-01-18 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "26"
|
||||||
|
down_revision: str | None = "25"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Safely add SEARXNG_API to searchsourceconnectortype enum."""
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_type t
|
||||||
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
|
WHERE t.typname = 'searchsourceconnectortype' AND e.enumlabel = 'SEARXNG_API'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE searchsourceconnectortype ADD VALUE 'SEARXNG_API';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade not supported for enum edits."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
@ -1033,6 +1033,30 @@ async def fetch_relevant_documents(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif connector == "SEARXNG_API":
|
||||||
|
(
|
||||||
|
source_object,
|
||||||
|
searx_chunks,
|
||||||
|
) = await connector_service.search_searxng(
|
||||||
|
user_query=reformulated_query,
|
||||||
|
user_id=user_id,
|
||||||
|
search_space_id=search_space_id,
|
||||||
|
top_k=top_k,
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_object:
|
||||||
|
all_sources.append(source_object)
|
||||||
|
all_raw_documents.extend(searx_chunks)
|
||||||
|
|
||||||
|
if streaming_service and writer:
|
||||||
|
writer(
|
||||||
|
{
|
||||||
|
"yield_value": streaming_service.format_terminal_info_delta(
|
||||||
|
f"🌐 Found {len(searx_chunks)} SearxNG results related to your query"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
elif connector == "LINKUP_API":
|
elif connector == "LINKUP_API":
|
||||||
linkup_mode = "standard"
|
linkup_mode = "standard"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ class DocumentType(str, Enum):
|
||||||
class SearchSourceConnectorType(str, Enum):
|
class SearchSourceConnectorType(str, Enum):
|
||||||
SERPER_API = "SERPER_API" # NOT IMPLEMENTED YET : DON'T REMEMBER WHY : MOST PROBABLY BECAUSE WE NEED TO CRAWL THE RESULTS RETURNED BY IT
|
SERPER_API = "SERPER_API" # NOT IMPLEMENTED YET : DON'T REMEMBER WHY : MOST PROBABLY BECAUSE WE NEED TO CRAWL THE RESULTS RETURNED BY IT
|
||||||
TAVILY_API = "TAVILY_API"
|
TAVILY_API = "TAVILY_API"
|
||||||
|
SEARXNG_API = "SEARXNG_API"
|
||||||
LINKUP_API = "LINKUP_API"
|
LINKUP_API = "LINKUP_API"
|
||||||
SLACK_CONNECTOR = "SLACK_CONNECTOR"
|
SLACK_CONNECTOR = "SLACK_CONNECTOR"
|
||||||
NOTION_CONNECTOR = "NOTION_CONNECTOR"
|
NOTION_CONNECTOR = "NOTION_CONNECTOR"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
import httpx
|
||||||
from linkup import LinkupClient
|
from linkup import LinkupClient
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
@ -372,6 +374,192 @@ class ConnectorService:
|
||||||
"sources": [],
|
"sources": [],
|
||||||
}, []
|
}, []
|
||||||
|
|
||||||
|
async def search_searxng(
|
||||||
|
self,
|
||||||
|
user_query: str,
|
||||||
|
user_id: str,
|
||||||
|
search_space_id: int,
|
||||||
|
top_k: int = 20,
|
||||||
|
) -> tuple:
|
||||||
|
"""
|
||||||
|
Search using a configured SearxNG instance and return both sources and documents.
|
||||||
|
"""
|
||||||
|
searx_connector = await self.get_connector_by_type(
|
||||||
|
user_id, SearchSourceConnectorType.SEARXNG_API, search_space_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not searx_connector:
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
config = searx_connector.config or {}
|
||||||
|
host = config.get("SEARXNG_HOST")
|
||||||
|
|
||||||
|
if not host:
|
||||||
|
print("SearxNG connector is missing SEARXNG_HOST configuration")
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
api_key = config.get("SEARXNG_API_KEY")
|
||||||
|
engines = config.get("SEARXNG_ENGINES")
|
||||||
|
categories = config.get("SEARXNG_CATEGORIES")
|
||||||
|
language = config.get("SEARXNG_LANGUAGE")
|
||||||
|
safesearch = config.get("SEARXNG_SAFESEARCH")
|
||||||
|
|
||||||
|
def _parse_bool(value: Any, default: bool = True) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
lowered = value.strip().lower()
|
||||||
|
if lowered in {"true", "1", "yes", "on"}:
|
||||||
|
return True
|
||||||
|
if lowered in {"false", "0", "no", "off"}:
|
||||||
|
return False
|
||||||
|
return default
|
||||||
|
|
||||||
|
verify_ssl = _parse_bool(config.get("SEARXNG_VERIFY_SSL", True))
|
||||||
|
|
||||||
|
safesearch_value: int | None = None
|
||||||
|
if isinstance(safesearch, str):
|
||||||
|
safesearch_clean = safesearch.strip()
|
||||||
|
if safesearch_clean.isdigit():
|
||||||
|
safesearch_value = int(safesearch_clean)
|
||||||
|
elif isinstance(safesearch, (int, float)):
|
||||||
|
safesearch_value = int(safesearch)
|
||||||
|
|
||||||
|
if safesearch_value is not None and not (0 <= safesearch_value <= 2):
|
||||||
|
safesearch_value = None
|
||||||
|
|
||||||
|
def _format_list(value: Any) -> str | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.strip()
|
||||||
|
return value or None
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
cleaned = [str(item).strip() for item in value if str(item).strip()]
|
||||||
|
return ",".join(cleaned) if cleaned else None
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
"q": user_query,
|
||||||
|
"format": "json",
|
||||||
|
"language": language or "",
|
||||||
|
"limit": max(1, min(top_k, 50)),
|
||||||
|
}
|
||||||
|
|
||||||
|
engines_param = _format_list(engines)
|
||||||
|
if engines_param:
|
||||||
|
params["engines"] = engines_param
|
||||||
|
|
||||||
|
categories_param = _format_list(categories)
|
||||||
|
if categories_param:
|
||||||
|
params["categories"] = categories_param
|
||||||
|
|
||||||
|
if safesearch_value is not None:
|
||||||
|
params["safesearch"] = safesearch_value
|
||||||
|
|
||||||
|
if not params.get("language"):
|
||||||
|
params.pop("language")
|
||||||
|
|
||||||
|
headers = {"Accept": "application/json"}
|
||||||
|
if api_key:
|
||||||
|
headers["X-API-KEY"] = api_key
|
||||||
|
|
||||||
|
searx_endpoint = urljoin(host if host.endswith("/") else f"{host}/", "search")
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=20.0, verify=verify_ssl) as client:
|
||||||
|
response = await client.get(
|
||||||
|
searx_endpoint,
|
||||||
|
params=params,
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
print(f"Error searching with SearxNG: {exc!s}")
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = response.json()
|
||||||
|
except ValueError:
|
||||||
|
print("Failed to decode JSON response from SearxNG")
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
searx_results = data.get("results", [])
|
||||||
|
if not searx_results:
|
||||||
|
return {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": [],
|
||||||
|
}, []
|
||||||
|
|
||||||
|
sources_list: list[dict[str, Any]] = []
|
||||||
|
documents: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
async with self.counter_lock:
|
||||||
|
for result in searx_results:
|
||||||
|
description = result.get("content") or result.get("snippet") or ""
|
||||||
|
if len(description) > 160:
|
||||||
|
description = f"{description[:157]}..."
|
||||||
|
|
||||||
|
source = {
|
||||||
|
"id": self.source_id_counter,
|
||||||
|
"title": result.get("title", "SearxNG Result"),
|
||||||
|
"description": description,
|
||||||
|
"url": result.get("url", ""),
|
||||||
|
}
|
||||||
|
sources_list.append(source)
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"url": result.get("url", ""),
|
||||||
|
"engines": result.get("engines", []),
|
||||||
|
"category": result.get("category"),
|
||||||
|
"source": "SEARXNG_API",
|
||||||
|
}
|
||||||
|
|
||||||
|
document = {
|
||||||
|
"chunk_id": self.source_id_counter,
|
||||||
|
"content": description or result.get("content", ""),
|
||||||
|
"score": result.get("score", 0.0),
|
||||||
|
"document": {
|
||||||
|
"id": self.source_id_counter,
|
||||||
|
"title": result.get("title", "SearxNG Result"),
|
||||||
|
"document_type": "SEARXNG_API",
|
||||||
|
"metadata": metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
documents.append(document)
|
||||||
|
self.source_id_counter += 1
|
||||||
|
|
||||||
|
result_object = {
|
||||||
|
"id": 11,
|
||||||
|
"name": "SearxNG Search",
|
||||||
|
"type": "SEARXNG_API",
|
||||||
|
"sources": sources_list,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result_object, documents
|
||||||
|
|
||||||
async def search_slack(
|
async def search_slack(
|
||||||
self,
|
self,
|
||||||
user_query: str,
|
user_query: str,
|
||||||
|
|
|
||||||
|
|
@ -424,6 +424,22 @@ def validate_connector_config(
|
||||||
connector_rules = {
|
connector_rules = {
|
||||||
"SERPER_API": {"required": ["SERPER_API_KEY"], "validators": {}},
|
"SERPER_API": {"required": ["SERPER_API_KEY"], "validators": {}},
|
||||||
"TAVILY_API": {"required": ["TAVILY_API_KEY"], "validators": {}},
|
"TAVILY_API": {"required": ["TAVILY_API_KEY"], "validators": {}},
|
||||||
|
"SEARXNG_API": {
|
||||||
|
"required": ["SEARXNG_HOST"],
|
||||||
|
"optional": [
|
||||||
|
"SEARXNG_API_KEY",
|
||||||
|
"SEARXNG_ENGINES",
|
||||||
|
"SEARXNG_CATEGORIES",
|
||||||
|
"SEARXNG_LANGUAGE",
|
||||||
|
"SEARXNG_SAFESEARCH",
|
||||||
|
"SEARXNG_VERIFY_SSL",
|
||||||
|
],
|
||||||
|
"validators": {
|
||||||
|
"SEARXNG_HOST": lambda: validate_url_field(
|
||||||
|
"SEARXNG_HOST", "SearxNG"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
"LINKUP_API": {"required": ["LINKUP_API_KEY"], "validators": {}},
|
"LINKUP_API": {"required": ["LINKUP_API_KEY"], "validators": {}},
|
||||||
"SLACK_CONNECTOR": {"required": ["SLACK_BOT_TOKEN"], "validators": {}},
|
"SLACK_CONNECTOR": {"required": ["SLACK_BOT_TOKEN"], "validators": {}},
|
||||||
"NOTION_CONNECTOR": {
|
"NOTION_CONNECTOR": {
|
||||||
|
|
@ -484,10 +500,21 @@ def validate_connector_config(
|
||||||
if not rules:
|
if not rules:
|
||||||
return config # Unknown connector type, pass through
|
return config # Unknown connector type, pass through
|
||||||
|
|
||||||
# Validate required keys match exactly
|
required_keys = set(rules["required"])
|
||||||
if set(config.keys()) != set(rules["required"]):
|
optional_keys = set(rules.get("optional", []))
|
||||||
|
config_keys = set(config.keys())
|
||||||
|
|
||||||
|
# Validate that no unexpected keys are present
|
||||||
|
if not config_keys.issubset(required_keys | optional_keys):
|
||||||
|
allowed_keys = list(required_keys | optional_keys)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"For {connector_type_str} connector type, config must only contain these keys: {rules['required']}"
|
f"For {connector_type_str} connector type, config may only contain these keys: {allowed_keys}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate that all required keys are present
|
||||||
|
if not required_keys.issubset(config_keys):
|
||||||
|
raise ValueError(
|
||||||
|
f"For {connector_type_str} connector type, config must include these keys: {sorted(required_keys)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply custom validators first (these check format before emptiness)
|
# Apply custom validators first (these check format before emptiness)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,13 @@ const connectorCategories: ConnectorCategory[] = [
|
||||||
icon: getConnectorIcon(EnumConnectorName.TAVILY_API, "h-6 w-6"),
|
icon: getConnectorIcon(EnumConnectorName.TAVILY_API, "h-6 w-6"),
|
||||||
status: "available",
|
status: "available",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "searxng",
|
||||||
|
title: "SearxNG",
|
||||||
|
description: "Use your own SearxNG meta-search instance for web results.",
|
||||||
|
icon: getConnectorIcon(EnumConnectorName.SEARXNG_API, "h-6 w-6"),
|
||||||
|
status: "available",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "linkup-api",
|
id: "linkup-api",
|
||||||
title: "Linkup API",
|
title: "Linkup API",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ArrowLeft, Check, Info, Loader2 } from "lucide-react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
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 { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||||
|
import { useSearchSourceConnectors } from "@/hooks/use-search-source-connectors";
|
||||||
|
|
||||||
|
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<typeof searxngFormSchema>;
|
||||||
|
|
||||||
|
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 default function SearxngConnectorPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const searchSpaceId = params.search_space_id as string;
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const { createConnector } = useSearchSourceConnectors();
|
||||||
|
|
||||||
|
const form = useForm<SearxngFormValues>({
|
||||||
|
resolver: zodResolver(searxngFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "SearxNG Connector",
|
||||||
|
host: "",
|
||||||
|
api_key: "",
|
||||||
|
engines: "",
|
||||||
|
categories: "",
|
||||||
|
language: "",
|
||||||
|
safesearch: "",
|
||||||
|
verify_ssl: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: SearxngFormValues) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
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 createConnector(
|
||||||
|
{
|
||||||
|
name: values.name,
|
||||||
|
connector_type: EnumConnectorName.SEARXNG_API,
|
||||||
|
config,
|
||||||
|
is_indexable: false,
|
||||||
|
last_indexed_at: null,
|
||||||
|
},
|
||||||
|
parseInt(searchSpaceId)
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.success("SearxNG connector created successfully!");
|
||||||
|
router.push(`/dashboard/${searchSpaceId}/connectors`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating SearxNG connector:", error);
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to create connector");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto py-8 max-w-3xl">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="mb-6"
|
||||||
|
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
Back to Connectors
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
||||||
|
{getConnectorIcon(EnumConnectorName.SEARXNG_API, "h-6 w-6")}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Connect SearxNG</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Bring your self-hosted SearxNG meta-search engine into SurfSense.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<Card className="border-2 border-border">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">Connect SearxNG</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Integrate SurfSense with any SearxNG instance to broaden your search coverage while
|
||||||
|
preserving privacy and control.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Alert className="mb-6 bg-muted">
|
||||||
|
<Info className="h-4 w-4" />
|
||||||
|
<AlertTitle>SearxNG Instance Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
You need access to a running SearxNG instance. Refer to the{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.searxng.org/admin/installation-docker.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
SearxNG installation guide
|
||||||
|
</a>{" "}
|
||||||
|
for setup instructions. If your instance requires an API key, include it below.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Connector Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My SearxNG Connector" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>A friendly name to identify this connector.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>SearxNG Host</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://searxng.example.org" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Provide the full base URL to your SearxNG instance. Include the protocol
|
||||||
|
(http/https).
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="api_key"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Key (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter API key if your instance requires one"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Leave empty if your SearxNG instance does not enforce API keys.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="engines"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Engines (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="google,bing,duckduckgo" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Comma-separated list to target specific engines.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="categories"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Categories (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="general,it,science" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>Comma-separated list of SearxNG categories.</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="language"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Preferred Language (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="en-US" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
IETF language tag (e.g. en, en-US). Leave blank to inherit defaults.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="safesearch"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>SafeSearch Level (optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="0 (off), 1 (moderate), 2 (strict)" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance
|
||||||
|
default.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="verify_ssl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-4">
|
||||||
|
<div>
|
||||||
|
<FormLabel>Verify SSL Certificates</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Disable only when connecting to instances with self-signed certificates.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CardFooter className="flex justify-end px-0">
|
||||||
|
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Connecting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
Connect SearxNG
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,8 @@ export function SourceDetailSheet({
|
||||||
const [summaryOpen, setSummaryOpen] = useState(false);
|
const [summaryOpen, setSummaryOpen] = useState(false);
|
||||||
|
|
||||||
// Check if this is a source type that should render directly from node
|
// Check if this is a source type that should render directly from node
|
||||||
const isDirectRenderSource = sourceType === "TAVILY_API" || sourceType === "LINKUP_API";
|
const isDirectRenderSource =
|
||||||
|
sourceType === "TAVILY_API" || sourceType === "LINKUP_API" || sourceType === "SEARXNG_API";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && chunkId && !isDirectRenderSource) {
|
if (open && chunkId && !isDirectRenderSource) {
|
||||||
|
|
@ -108,7 +109,7 @@ export function SourceDetailSheet({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Direct render for TAVILY_API and LINKUP_API */}
|
{/* Direct render for web search providers */}
|
||||||
{isDirectRenderSource && (
|
{isDirectRenderSource && (
|
||||||
<ScrollArea className="h-[calc(100vh-10rem)]">
|
<ScrollArea className="h-[calc(100vh-10rem)]">
|
||||||
<div className="px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,13 @@ export const editConnectorSchema = z.object({
|
||||||
NOTION_INTEGRATION_TOKEN: z.string().optional(),
|
NOTION_INTEGRATION_TOKEN: z.string().optional(),
|
||||||
SERPER_API_KEY: z.string().optional(),
|
SERPER_API_KEY: z.string().optional(),
|
||||||
TAVILY_API_KEY: z.string().optional(),
|
TAVILY_API_KEY: z.string().optional(),
|
||||||
|
SEARXNG_HOST: z.string().optional(),
|
||||||
|
SEARXNG_API_KEY: z.string().optional(),
|
||||||
|
SEARXNG_ENGINES: z.string().optional(),
|
||||||
|
SEARXNG_CATEGORIES: z.string().optional(),
|
||||||
|
SEARXNG_LANGUAGE: z.string().optional(),
|
||||||
|
SEARXNG_SAFESEARCH: z.string().optional(),
|
||||||
|
SEARXNG_VERIFY_SSL: z.string().optional(),
|
||||||
LINEAR_API_KEY: z.string().optional(),
|
LINEAR_API_KEY: z.string().optional(),
|
||||||
LINKUP_API_KEY: z.string().optional(),
|
LINKUP_API_KEY: z.string().optional(),
|
||||||
DISCORD_BOT_TOKEN: z.string().optional(),
|
DISCORD_BOT_TOKEN: z.string().optional(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
export enum EnumConnectorName {
|
export enum EnumConnectorName {
|
||||||
SERPER_API = "SERPER_API",
|
SERPER_API = "SERPER_API",
|
||||||
TAVILY_API = "TAVILY_API",
|
TAVILY_API = "TAVILY_API",
|
||||||
|
SEARXNG_API = "SEARXNG_API",
|
||||||
LINKUP_API = "LINKUP_API",
|
LINKUP_API = "LINKUP_API",
|
||||||
SLACK_CONNECTOR = "SLACK_CONNECTOR",
|
SLACK_CONNECTOR = "SLACK_CONNECTOR",
|
||||||
NOTION_CONNECTOR = "NOTION_CONNECTOR",
|
NOTION_CONNECTOR = "NOTION_CONNECTOR",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
|
||||||
return <Link {...iconProps} />;
|
return <Link {...iconProps} />;
|
||||||
case EnumConnectorName.TAVILY_API:
|
case EnumConnectorName.TAVILY_API:
|
||||||
return <IconWorldWww {...iconProps} />;
|
return <IconWorldWww {...iconProps} />;
|
||||||
|
case EnumConnectorName.SEARXNG_API:
|
||||||
|
return <Globe {...iconProps} />;
|
||||||
case EnumConnectorName.SLACK_CONNECTOR:
|
case EnumConnectorName.SLACK_CONNECTOR:
|
||||||
return <IconBrandSlack {...iconProps} />;
|
return <IconBrandSlack {...iconProps} />;
|
||||||
case EnumConnectorName.NOTION_CONNECTOR:
|
case EnumConnectorName.NOTION_CONNECTOR:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,38 @@ import {
|
||||||
useSearchSourceConnectors,
|
useSearchSourceConnectors,
|
||||||
} from "@/hooks/use-search-source-connectors";
|
} from "@/hooks/use-search-source-connectors";
|
||||||
|
|
||||||
|
const normalizeListInput = (value: unknown): string[] => {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item).trim()).filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item.length > 0);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const arraysEqual = (a: string[], b: string[]): boolean => {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((value, index) => value === b[index]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeBoolean = (value: unknown): boolean | null => {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const lowered = value.trim().toLowerCase();
|
||||||
|
if (["true", "1", "yes", "on"].includes(lowered)) return true;
|
||||||
|
if (["false", "0", "no", "off"].includes(lowered)) return false;
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
if (value === 1) return true;
|
||||||
|
if (value === 0) return false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export function useConnectorEditPage(connectorId: number, searchSpaceId: string) {
|
export function useConnectorEditPage(connectorId: number, searchSpaceId: string) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
|
|
@ -48,6 +80,13 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
NOTION_INTEGRATION_TOKEN: "",
|
NOTION_INTEGRATION_TOKEN: "",
|
||||||
SERPER_API_KEY: "",
|
SERPER_API_KEY: "",
|
||||||
TAVILY_API_KEY: "",
|
TAVILY_API_KEY: "",
|
||||||
|
SEARXNG_HOST: "",
|
||||||
|
SEARXNG_API_KEY: "",
|
||||||
|
SEARXNG_ENGINES: "",
|
||||||
|
SEARXNG_CATEGORIES: "",
|
||||||
|
SEARXNG_LANGUAGE: "",
|
||||||
|
SEARXNG_SAFESEARCH: "",
|
||||||
|
SEARXNG_VERIFY_SSL: "",
|
||||||
LINEAR_API_KEY: "",
|
LINEAR_API_KEY: "",
|
||||||
DISCORD_BOT_TOKEN: "",
|
DISCORD_BOT_TOKEN: "",
|
||||||
CONFLUENCE_BASE_URL: "",
|
CONFLUENCE_BASE_URL: "",
|
||||||
|
|
@ -74,6 +113,23 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "",
|
NOTION_INTEGRATION_TOKEN: config.NOTION_INTEGRATION_TOKEN || "",
|
||||||
SERPER_API_KEY: config.SERPER_API_KEY || "",
|
SERPER_API_KEY: config.SERPER_API_KEY || "",
|
||||||
TAVILY_API_KEY: config.TAVILY_API_KEY || "",
|
TAVILY_API_KEY: config.TAVILY_API_KEY || "",
|
||||||
|
SEARXNG_HOST: config.SEARXNG_HOST || "",
|
||||||
|
SEARXNG_API_KEY: config.SEARXNG_API_KEY || "",
|
||||||
|
SEARXNG_ENGINES: Array.isArray(config.SEARXNG_ENGINES)
|
||||||
|
? config.SEARXNG_ENGINES.join(", ")
|
||||||
|
: config.SEARXNG_ENGINES || "",
|
||||||
|
SEARXNG_CATEGORIES: Array.isArray(config.SEARXNG_CATEGORIES)
|
||||||
|
? config.SEARXNG_CATEGORIES.join(", ")
|
||||||
|
: config.SEARXNG_CATEGORIES || "",
|
||||||
|
SEARXNG_LANGUAGE: config.SEARXNG_LANGUAGE || "",
|
||||||
|
SEARXNG_SAFESEARCH:
|
||||||
|
config.SEARXNG_SAFESEARCH !== undefined && config.SEARXNG_SAFESEARCH !== null
|
||||||
|
? String(config.SEARXNG_SAFESEARCH)
|
||||||
|
: "",
|
||||||
|
SEARXNG_VERIFY_SSL:
|
||||||
|
config.SEARXNG_VERIFY_SSL !== undefined && config.SEARXNG_VERIFY_SSL !== null
|
||||||
|
? String(config.SEARXNG_VERIFY_SSL)
|
||||||
|
: "",
|
||||||
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
|
LINEAR_API_KEY: config.LINEAR_API_KEY || "",
|
||||||
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
|
LINKUP_API_KEY: config.LINKUP_API_KEY || "",
|
||||||
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
|
DISCORD_BOT_TOKEN: config.DISCORD_BOT_TOKEN || "",
|
||||||
|
|
@ -238,6 +294,93 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY };
|
newConfig = { TAVILY_API_KEY: formData.TAVILY_API_KEY };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "SEARXNG_API": {
|
||||||
|
const host = (formData.SEARXNG_HOST || "").trim();
|
||||||
|
if (!host) {
|
||||||
|
toast.error("SearxNG host is required.");
|
||||||
|
setIsSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidateConfig: Record<string, any> = { SEARXNG_HOST: host };
|
||||||
|
let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim();
|
||||||
|
|
||||||
|
const apiKey = (formData.SEARXNG_API_KEY || "").trim();
|
||||||
|
const originalApiKey = (originalConfig.SEARXNG_API_KEY || "").trim();
|
||||||
|
if (apiKey !== originalApiKey) {
|
||||||
|
candidateConfig.SEARXNG_API_KEY = apiKey || null;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEngines = normalizeListInput(formData.SEARXNG_ENGINES || "");
|
||||||
|
const originalEngines = normalizeListInput(originalConfig.SEARXNG_ENGINES);
|
||||||
|
if (!arraysEqual(newEngines, originalEngines)) {
|
||||||
|
candidateConfig.SEARXNG_ENGINES = newEngines;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCategories = normalizeListInput(formData.SEARXNG_CATEGORIES || "");
|
||||||
|
const originalCategories = normalizeListInput(originalConfig.SEARXNG_CATEGORIES);
|
||||||
|
if (!arraysEqual(newCategories, originalCategories)) {
|
||||||
|
candidateConfig.SEARXNG_CATEGORIES = newCategories;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = (formData.SEARXNG_LANGUAGE || "").trim();
|
||||||
|
const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim();
|
||||||
|
if (language !== originalLanguage) {
|
||||||
|
candidateConfig.SEARXNG_LANGUAGE = language || null;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const safesearchRaw = (formData.SEARXNG_SAFESEARCH || "").trim();
|
||||||
|
const originalSafesearch = originalConfig.SEARXNG_SAFESEARCH;
|
||||||
|
if (safesearchRaw) {
|
||||||
|
const parsed = Number(safesearchRaw);
|
||||||
|
if (
|
||||||
|
Number.isNaN(parsed) ||
|
||||||
|
!Number.isInteger(parsed) ||
|
||||||
|
parsed < 0 ||
|
||||||
|
parsed > 2
|
||||||
|
) {
|
||||||
|
toast.error("SearxNG SafeSearch must be 0, 1, or 2.");
|
||||||
|
setIsSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsed !== Number(originalSafesearch)) {
|
||||||
|
candidateConfig.SEARXNG_SAFESEARCH = parsed;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
} else if (originalSafesearch !== undefined && originalSafesearch !== null) {
|
||||||
|
candidateConfig.SEARXNG_SAFESEARCH = null;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyRaw = (formData.SEARXNG_VERIFY_SSL || "").trim().toLowerCase();
|
||||||
|
const originalVerifyBool = normalizeBoolean(originalConfig.SEARXNG_VERIFY_SSL);
|
||||||
|
if (verifyRaw) {
|
||||||
|
let parsedBool: boolean | null = null;
|
||||||
|
if (["true", "1", "yes", "on"].includes(verifyRaw)) parsedBool = true;
|
||||||
|
else if (["false", "0", "no", "off"].includes(verifyRaw)) parsedBool = false;
|
||||||
|
if (parsedBool === null) {
|
||||||
|
toast.error("SearxNG SSL verification must be true or false.");
|
||||||
|
setIsSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (parsedBool !== originalVerifyBool) {
|
||||||
|
candidateConfig.SEARXNG_VERIFY_SSL = parsedBool;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
} else if (originalVerifyBool !== null) {
|
||||||
|
candidateConfig.SEARXNG_VERIFY_SSL = null;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
newConfig = candidateConfig;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "LINEAR_CONNECTOR":
|
case "LINEAR_CONNECTOR":
|
||||||
if (formData.LINEAR_API_KEY !== originalConfig.LINEAR_API_KEY) {
|
if (formData.LINEAR_API_KEY !== originalConfig.LINEAR_API_KEY) {
|
||||||
|
|
@ -367,6 +510,33 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
|
||||||
editForm.setValue("SERPER_API_KEY", newlySavedConfig.SERPER_API_KEY || "");
|
editForm.setValue("SERPER_API_KEY", newlySavedConfig.SERPER_API_KEY || "");
|
||||||
} else if (connector.connector_type === "TAVILY_API") {
|
} else if (connector.connector_type === "TAVILY_API") {
|
||||||
editForm.setValue("TAVILY_API_KEY", newlySavedConfig.TAVILY_API_KEY || "");
|
editForm.setValue("TAVILY_API_KEY", newlySavedConfig.TAVILY_API_KEY || "");
|
||||||
|
} else if (connector.connector_type === "SEARXNG_API") {
|
||||||
|
editForm.setValue("SEARXNG_HOST", newlySavedConfig.SEARXNG_HOST || "");
|
||||||
|
editForm.setValue("SEARXNG_API_KEY", newlySavedConfig.SEARXNG_API_KEY || "");
|
||||||
|
editForm.setValue(
|
||||||
|
"SEARXNG_ENGINES",
|
||||||
|
normalizeListInput(newlySavedConfig.SEARXNG_ENGINES).join(", ")
|
||||||
|
);
|
||||||
|
editForm.setValue(
|
||||||
|
"SEARXNG_CATEGORIES",
|
||||||
|
normalizeListInput(newlySavedConfig.SEARXNG_CATEGORIES).join(", ")
|
||||||
|
);
|
||||||
|
editForm.setValue(
|
||||||
|
"SEARXNG_LANGUAGE",
|
||||||
|
newlySavedConfig.SEARXNG_LANGUAGE || ""
|
||||||
|
);
|
||||||
|
editForm.setValue(
|
||||||
|
"SEARXNG_SAFESEARCH",
|
||||||
|
newlySavedConfig.SEARXNG_SAFESEARCH === null ||
|
||||||
|
newlySavedConfig.SEARXNG_SAFESEARCH === undefined
|
||||||
|
? ""
|
||||||
|
: String(newlySavedConfig.SEARXNG_SAFESEARCH)
|
||||||
|
);
|
||||||
|
const verifyValue = normalizeBoolean(newlySavedConfig.SEARXNG_VERIFY_SSL);
|
||||||
|
editForm.setValue(
|
||||||
|
"SEARXNG_VERIFY_SSL",
|
||||||
|
verifyValue === null ? "" : String(verifyValue)
|
||||||
|
);
|
||||||
} else if (connector.connector_type === "LINEAR_CONNECTOR") {
|
} else if (connector.connector_type === "LINEAR_CONNECTOR") {
|
||||||
editForm.setValue("LINEAR_API_KEY", newlySavedConfig.LINEAR_API_KEY || "");
|
editForm.setValue("LINEAR_API_KEY", newlySavedConfig.LINEAR_API_KEY || "");
|
||||||
} else if (connector.connector_type === "LINKUP_API") {
|
} else if (connector.connector_type === "LINKUP_API") {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
SERPER_API: "Serper API",
|
SERPER_API: "Serper API",
|
||||||
TAVILY_API: "Tavily API",
|
TAVILY_API: "Tavily API",
|
||||||
// Add other connector types here as needed
|
SEARXNG_API: "SearxNG",
|
||||||
};
|
};
|
||||||
return typeMap[type] || type;
|
return typeMap[type] || type;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export const getConnectorTypeDisplay = (type: string): string => {
|
||||||
const typeMap: Record<string, string> = {
|
const typeMap: Record<string, string> = {
|
||||||
SERPER_API: "Serper API",
|
SERPER_API: "Serper API",
|
||||||
TAVILY_API: "Tavily API",
|
TAVILY_API: "Tavily API",
|
||||||
|
SEARXNG_API: "SearxNG",
|
||||||
SLACK_CONNECTOR: "Slack",
|
SLACK_CONNECTOR: "Slack",
|
||||||
NOTION_CONNECTOR: "Notion",
|
NOTION_CONNECTOR: "Notion",
|
||||||
GITHUB_CONNECTOR: "GitHub",
|
GITHUB_CONNECTOR: "GitHub",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue