Merge pull request #655 from AnishSarkar22/fix/connector

fix: Circleback & Google Drive connectors and UI improvements
This commit is contained in:
Rohan Verma 2026-01-01 22:25:21 -08:00 committed by GitHub
commit 8e9a84c194
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
92 changed files with 3087 additions and 3199 deletions

View file

@ -20,6 +20,7 @@ import { AppSidebarProvider } from "@/components/sidebar/AppSidebarProvider";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { DocumentUploadDialogProvider } from "@/components/assistant-ui/document-upload-popup";
export function DashboardClientLayout({
children,
@ -240,32 +241,34 @@ export function DashboardClientLayout({
}
return (
<SidebarProvider className="h-full overflow-hidden" open={open} onOpenChange={setOpen}>
{/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
<AppSidebarProvider
searchSpaceId={searchSpaceId}
navSecondary={translatedNavSecondary}
navMain={translatedNavMain}
/>
<SidebarInset className="h-full ">
<main className="flex flex-col h-full">
<header className="sticky top-0 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<div className="hidden md:flex items-center gap-2">
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
<DocumentUploadDialogProvider>
<SidebarProvider className="h-full overflow-hidden" open={open} onOpenChange={setOpen}>
{/* Use AppSidebarProvider which fetches user, search space, and recent chats */}
<AppSidebarProvider
searchSpaceId={searchSpaceId}
navSecondary={translatedNavSecondary}
navMain={translatedNavMain}
/>
<SidebarInset className="h-full ">
<main className="flex flex-col h-full">
<header className="sticky top-0 flex h-16 shrink-0 items-center gap-2 bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 border-b">
<div className="flex items-center justify-between w-full gap-2 px-4">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<div className="hidden md:flex items-center gap-2">
<Separator orientation="vertical" className="h-6" />
<DashboardBreadcrumb />
</div>
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />
</div>
</div>
<div className="flex items-center gap-2">
<LanguageSwitcher />
</div>
</div>
</header>
<div className="flex-1 overflow-hidden">{children}</div>
</main>
</SidebarInset>
</SidebarProvider>
</header>
<div className="flex-1 overflow-hidden">{children}</div>
</main>
</SidebarInset>
</SidebarProvider>
</DocumentUploadDialogProvider>
);
}

View file

@ -1,857 +0,0 @@
"use client";
import { format } from "date-fns";
import { useAtomValue } from "jotai";
import {
Calendar as CalendarIcon,
Clock,
Edit,
Folder,
HardDrive,
Info,
Loader2,
Plus,
RefreshCw,
Trash2,
} from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import {
deleteConnectorMutationAtom,
indexConnectorMutationAtom,
updateConnectorMutationAtom,
} from "@/atoms/connectors/connector-mutation.atoms";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import { cn } from "@/lib/utils";
import { GoogleDriveFolderTree } from "@/components/connectors/google-drive-folder-tree";
export default function ConnectorsPage() {
const t = useTranslations("connectors");
const tCommon = useTranslations("common");
// Helper function to format date with time
const formatDateTime = (dateString: string | null): string => {
if (!dateString) return t("never");
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
};
const router = useRouter();
const params = useParams();
const searchSpaceId = params.search_space_id as string;
const today = new Date();
const { data: connectors = [], isLoading, error } = useAtomValue(connectorsAtom);
const { mutateAsync: deleteConnector } = useAtomValue(deleteConnectorMutationAtom);
const { mutateAsync: indexConnector } = useAtomValue(indexConnectorMutationAtom);
const { mutateAsync: updateConnector } = useAtomValue(updateConnectorMutationAtom);
const [connectorToDelete, setConnectorToDelete] = useState<number | null>(null);
const [indexingConnectorId, setIndexingConnectorId] = useState<number | null>(null);
const [datePickerOpen, setDatePickerOpen] = useState(false);
const [selectedConnectorForIndexing, setSelectedConnectorForIndexing] = useState<number | null>(
null
);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
// Periodic indexing state
const [periodicDialogOpen, setPeriodicDialogOpen] = useState(false);
const [selectedConnectorForPeriodic, setSelectedConnectorForPeriodic] = useState<number | null>(
null
);
const [periodicEnabled, setPeriodicEnabled] = useState(false);
const [frequencyMinutes, setFrequencyMinutes] = useState<string>("1440");
const [customFrequency, setCustomFrequency] = useState<string>("");
const [isSavingPeriodic, setIsSavingPeriodic] = useState(false);
// Google Drive folder and file selection state
const [driveFolderDialogOpen, setDriveFolderDialogOpen] = useState(false);
const [selectedFolders, setSelectedFolders] = useState<Array<{ id: string; name: string }>>([]);
const [selectedFiles, setSelectedFiles] = useState<Array<{ id: string; name: string }>>([]);
useEffect(() => {
if (error) {
toast.error(t("failed_load"));
console.error("Error fetching connectors:", error);
}
}, [error, t]);
// Handle connector deletion
const handleDeleteConnector = async () => {
if (connectorToDelete === null) return;
try {
await deleteConnector({ id: connectorToDelete });
} catch (error) {
console.error("Error deleting connector:", error);
} finally {
setConnectorToDelete(null);
}
};
// Handle opening date picker for indexing
const handleOpenDatePicker = (connectorId: number) => {
// Check if this is a Google Drive connector
const connector = connectors.find((c) => c.id === connectorId);
if (connector?.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR) {
// Open folder selection dialog for Google Drive
handleOpenDriveFolderDialog(connectorId);
} else {
// Open date picker for other connectors
setSelectedConnectorForIndexing(connectorId);
setDatePickerOpen(true);
}
};
const handleOpenDriveFolderDialog = (connectorId: number) => {
setSelectedConnectorForIndexing(connectorId);
setDriveFolderDialogOpen(true);
};
// Handle Google Drive folder and file indexing
const handleIndexGoogleDrive = async () => {
if (selectedConnectorForIndexing === null || (selectedFolders.length === 0 && selectedFiles.length === 0)) {
toast.error("Please select at least one folder or file");
return;
}
setDriveFolderDialogOpen(false);
try {
setIndexingConnectorId(selectedConnectorForIndexing);
await indexConnector({
connector_id: selectedConnectorForIndexing,
body: {
folders: selectedFolders,
files: selectedFiles,
},
queryParams: {
search_space_id: searchSpaceId,
},
});
toast.success(t("indexing_started"));
} catch (error) {
console.error("Error indexing connector content:", error);
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
} finally {
setIndexingConnectorId(null);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setSelectedFiles([]);
}
};
// Handle connector indexing with dates
const handleIndexConnector = async () => {
if (selectedConnectorForIndexing === null) return;
setDatePickerOpen(false);
try {
setIndexingConnectorId(selectedConnectorForIndexing);
const startDateStr = startDate ? format(startDate, "yyyy-MM-dd") : undefined;
const endDateStr = endDate ? format(endDate, "yyyy-MM-dd") : undefined;
await indexConnector({
connector_id: selectedConnectorForIndexing,
queryParams: {
search_space_id: searchSpaceId,
start_date: startDateStr,
end_date: endDateStr,
},
});
toast.success(t("indexing_started"));
} catch (error) {
console.error("Error indexing connector content:", error);
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
} finally {
setIndexingConnectorId(null);
setSelectedConnectorForIndexing(null);
setStartDate(undefined);
setEndDate(undefined);
}
};
// Handle indexing without date picker (for quick indexing)
const handleQuickIndexConnector = async (connectorId: number) => {
setIndexingConnectorId(connectorId);
try {
await indexConnector({
connector_id: connectorId,
queryParams: {
search_space_id: searchSpaceId,
},
});
toast.success(t("indexing_started"));
} catch (error) {
console.error("Error indexing connector content:", error);
toast.error(error instanceof Error ? error.message : t("indexing_failed"));
} finally {
setIndexingConnectorId(null);
}
};
// Handle opening periodic indexing dialog
const handleOpenPeriodicDialog = (connectorId: number) => {
const connector = connectors.find((c) => c.id === connectorId);
if (!connector) return;
setSelectedConnectorForPeriodic(connectorId);
setPeriodicEnabled(connector.periodic_indexing_enabled);
if (connector.indexing_frequency_minutes) {
// Check if it's a preset value
const presetValues = ["15", "60", "360", "720", "1440", "10080"];
if (presetValues.includes(connector.indexing_frequency_minutes.toString())) {
setFrequencyMinutes(connector.indexing_frequency_minutes.toString());
setCustomFrequency("");
} else {
setFrequencyMinutes("custom");
setCustomFrequency(connector.indexing_frequency_minutes.toString());
}
} else {
setFrequencyMinutes("1440");
setCustomFrequency("");
}
setPeriodicDialogOpen(true);
};
// Handle saving periodic indexing configuration
const handleSavePeriodicIndexing = async () => {
if (selectedConnectorForPeriodic === null) return;
const connector = connectors.find((c) => c.id === selectedConnectorForPeriodic);
if (!connector) return;
setIsSavingPeriodic(true);
try {
// Determine the frequency value
let frequency: number | null = null;
if (periodicEnabled) {
if (frequencyMinutes === "custom") {
frequency = parseInt(customFrequency, 10);
if (isNaN(frequency) || frequency <= 0) {
toast.error("Please enter a valid frequency in minutes");
setIsSavingPeriodic(false);
return;
}
} else {
frequency = parseInt(frequencyMinutes, 10);
}
}
await updateConnector({
id: selectedConnectorForPeriodic,
data: {
periodic_indexing_enabled: periodicEnabled,
indexing_frequency_minutes: frequency,
},
});
toast.success(
periodicEnabled
? "Periodic indexing enabled successfully"
: "Periodic indexing disabled successfully"
);
setPeriodicDialogOpen(false);
} catch (error) {
console.error("Error updating periodic indexing:", error);
toast.error(error instanceof Error ? error.message : "Failed to update periodic indexing");
} finally {
setIsSavingPeriodic(false);
setSelectedConnectorForPeriodic(null);
}
};
// Format frequency for display
const formatFrequency = (minutes: number): string => {
if (minutes < 60) return `${minutes}m`;
if (minutes < 1440) return `${Math.floor(minutes / 60)}h`;
if (minutes < 10080) return `${Math.floor(minutes / 1440)}d`;
return `${Math.floor(minutes / 10080)}w`;
};
return (
<div className="container mx-auto py-8 px-4 max-w-6xl min-h-[calc(100vh-64px)]">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-8 flex items-center justify-between gap-2"
>
<div>
<h1 className="text-xl md:text-3xl font-bold tracking-tight">{t("title")}</h1>
<p className="text-xs md:text-base text-muted-foreground mt-2">{t("subtitle")}</p>
</div>
<Button
className="h-8 text-xs px-3 md:h-10 md:text-sm md:px-4"
onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}
>
<Plus className="mr-2 h-3 w-3 md:h-4 md:w-4" />
{t("add_connector")}
</Button>
</motion.div>
{isLoading ? (
<div className="flex justify-center py-8">
<div className="animate-pulse text-center">
<div className="h-6 w-32 bg-muted rounded mx-auto mb-2"></div>
<div className="h-4 w-48 bg-muted rounded mx-auto"></div>
</div>
</div>
) : connectors.length === 0 ? (
<div className="text-center py-12">
<h3 className="text-lg font-medium mb-2">{t("no_connectors")}</h3>
<p className="text-muted-foreground mb-6">{t("no_connectors_desc")}</p>
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
<Plus className="mr-2 h-4 w-4" />
{t("add_first")}
</Button>
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("name")}</TableHead>
<TableHead>{t("type")}</TableHead>
<TableHead>{t("last_indexed")}</TableHead>
<TableHead>{t("periodic")}</TableHead>
<TableHead className="text-right">{t("actions")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{connectors.map((connector) => (
<TableRow key={connector.id}>
<TableCell className="font-medium">{connector.name}</TableCell>
<TableCell>{getConnectorIcon(connector.connector_type)}</TableCell>
<TableCell>
{connector.is_indexable
? formatDateTime(connector.last_indexed_at)
: t("not_indexable")}
</TableCell>
<TableCell>
{connector.is_indexable ? (
connector.periodic_indexing_enabled ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
<Clock className="h-4 w-4" />
<span className="text-sm font-medium">
{connector.indexing_frequency_minutes
? formatFrequency(connector.indexing_frequency_minutes)
: "Enabled"}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>
Runs every {connector.indexing_frequency_minutes} minutes
{connector.next_scheduled_at && (
<>
<br />
Next: {formatDateTime(connector.next_scheduled_at)}
</>
)}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<span className="text-sm text-muted-foreground">Disabled</span>
)
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{connector.is_indexable && (
<div className="flex gap-1">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => handleOpenDatePicker(connector.id)}
disabled={indexingConnectorId === connector.id}
>
{indexingConnectorId === connector.id ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR ? (
<Folder className="h-4 w-4" />
) : (
<CalendarIcon className="h-4 w-4" />
)}
<span className="sr-only">
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
? "Select folder to index"
: t("index_date_range")}
</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
? "Select folder to index"
: t("index_date_range")}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* Hide quick index button for Google Drive (requires folder selection) */}
{connector.connector_type !== EnumConnectorName.GOOGLE_DRIVE_CONNECTOR && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => handleQuickIndexConnector(connector.id)}
disabled={indexingConnectorId === connector.id}
>
{indexingConnectorId === connector.id ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="sr-only">{t("quick_index")}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("quick_index_auto")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
{connector.is_indexable && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => handleOpenPeriodicDialog(connector.id)}
>
<Clock className="h-4 w-4" />
<span className="sr-only">Configure Periodic Indexing</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Configure Periodic Indexing</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Button
variant="outline"
size="sm"
onClick={() =>
router.push(
`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`
)
}
>
<Edit className="h-4 w-4" />
<span className="sr-only">{tCommon("edit")}</span>
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="text-destructive-foreground hover:bg-destructive/10"
onClick={() => setConnectorToDelete(connector.id)}
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">{tCommon("delete")}</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("delete_connector")}</AlertDialogTitle>
<AlertDialogDescription>
{t("delete_confirm")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setConnectorToDelete(null)}>
{tCommon("cancel")}
</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={handleDeleteConnector}
>
{tCommon("delete")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{/* Date Picker Dialog */}
<Dialog open={datePickerOpen} onOpenChange={setDatePickerOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{t("select_date_range")}</DialogTitle>
<DialogDescription>{t("select_date_range_desc")}</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="start-date">{t("start_date")}</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id="start-date"
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!startDate && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{startDate ? format(startDate, "PPP") : t("pick_date")}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={startDate}
onSelect={setStartDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="end-date">{t("end_date")}</Label>
<Popover>
<PopoverTrigger asChild>
<Button
id="end-date"
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!endDate && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{endDate ? format(endDate, "PPP") : t("pick_date")}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={endDate} onSelect={setEndDate} initialFocus />
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setStartDate(undefined);
setEndDate(undefined);
}}
>
{t("clear_dates")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const thirtyDaysAgo = new Date(today);
thirtyDaysAgo.setDate(today.getDate() - 30);
setStartDate(thirtyDaysAgo);
setEndDate(today);
}}
>
{t("last_30_days")}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const yearAgo = new Date(today);
yearAgo.setFullYear(today.getFullYear() - 1);
setStartDate(yearAgo);
setEndDate(today);
}}
>
{t("last_year")}
</Button>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setDatePickerOpen(false);
setSelectedConnectorForIndexing(null);
setStartDate(undefined);
setEndDate(undefined);
}}
>
{tCommon("cancel")}
</Button>
<Button onClick={handleIndexConnector}>{t("start_indexing")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Google Drive Folder Selection Dialog */}
<Dialog open={driveFolderDialogOpen} onOpenChange={setDriveFolderDialogOpen}>
<DialogContent className="w-auto max-w-full">
<DialogHeader>
<DialogTitle>Select Google Drive Folders & Files</DialogTitle>
<DialogDescription className="flex items-start gap-2 text-sm p-2 border mt-1 rounded ">
<Info className="h-4 w-4 shrink-0 text-blue-500" />
<span>
Select folders and/or individual files to index. For folders, only files <strong>directly in each folder</strong> will be
processedsubfolders must be selected separately.
</span>
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 overflow-hidden w-full">
<div className="space-y-3 w-full overflow-hidden">
<Label>Browse Folders</Label>
{selectedConnectorForIndexing && (
<GoogleDriveFolderTree
connectorId={selectedConnectorForIndexing}
selectedFolders={selectedFolders}
onSelectFolders={(folders) => {
setSelectedFolders(folders);
}}
selectedFiles={selectedFiles}
onSelectFiles={(files) => {
setSelectedFiles(files);
}}
/>
)}
</div>
{(selectedFolders.length > 0 || selectedFiles.length > 0) && (
<div className="p-3 bg-muted rounded-lg text-sm space-y-2">
{selectedFolders.length > 0 && (
<div>
<p className="font-medium mb-1">
Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}:
</p>
<div className="max-h-24 overflow-y-auto">
{selectedFolders.map((folder) => (
<p key={folder.id} className="text-sm text-muted-foreground truncate" title={folder.name}>
📁 {folder.name}
</p>
))}
</div>
</div>
)}
{selectedFiles.length > 0 && (
<div>
<p className="font-medium mb-1">
Selected {selectedFiles.length} file{selectedFiles.length > 1 ? "s" : ""}:
</p>
<div className="max-h-24 overflow-y-auto">
{selectedFiles.map((file) => (
<p key={file.id} className="text-sm text-muted-foreground truncate" title={file.name}>
📄 {file.name}
</p>
))}
</div>
</div>
)}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setDriveFolderDialogOpen(false);
setSelectedConnectorForIndexing(null);
setSelectedFolders([]);
setSelectedFiles([]);
}}
>
{tCommon("cancel")}
</Button>
<Button onClick={handleIndexGoogleDrive} disabled={selectedFolders.length === 0 && selectedFiles.length === 0}>
{t("start_indexing")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Periodic Indexing Configuration Dialog */}
<Dialog open={periodicDialogOpen} onOpenChange={setPeriodicDialogOpen}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Configure Periodic Indexing</DialogTitle>
<DialogDescription>
Set up automatic indexing at regular intervals for this connector.
</DialogDescription>
</DialogHeader>
<div className="grid gap-6 py-4">
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<Label htmlFor="periodic-enabled" className="text-base">
Enable Periodic Indexing
</Label>
<p className="text-sm text-muted-foreground">
Automatically index this connector at regular intervals
</p>
</div>
<Switch
id="periodic-enabled"
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
/>
</div>
{periodicEnabled && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="frequency">Indexing Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes}>
<SelectTrigger id="frequency">
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent>
<SelectItem value="15">Every 15 minutes</SelectItem>
<SelectItem value="60">Every hour</SelectItem>
<SelectItem value="360">Every 6 hours</SelectItem>
<SelectItem value="720">Every 12 hours</SelectItem>
<SelectItem value="1440">Daily (24 hours)</SelectItem>
<SelectItem value="10080">Weekly (7 days)</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
</SelectContent>
</Select>
</div>
{frequencyMinutes === "custom" && (
<div className="space-y-2">
<Label htmlFor="custom-frequency">Custom Frequency (minutes)</Label>
<Input
id="custom-frequency"
type="number"
min="1"
placeholder="Enter minutes"
value={customFrequency}
onChange={(e) => setCustomFrequency(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Enter the number of minutes between each indexing run
</p>
</div>
)}
<div className="rounded-lg bg-muted p-3 text-sm">
<p className="font-medium mb-1">Preview:</p>
<p className="text-muted-foreground">
{frequencyMinutes === "custom" && customFrequency
? `Will run every ${customFrequency} minutes`
: frequencyMinutes === "15"
? "Will run every 15 minutes"
: frequencyMinutes === "60"
? "Will run every hour"
: frequencyMinutes === "360"
? "Will run every 6 hours"
: frequencyMinutes === "720"
? "Will run every 12 hours"
: frequencyMinutes === "1440"
? "Will run daily (every 24 hours)"
: frequencyMinutes === "10080"
? "Will run weekly (every 7 days)"
: "Select a frequency above"}
</p>
</div>
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setPeriodicDialogOpen(false);
setSelectedConnectorForPeriodic(null);
}}
>
Cancel
</Button>
<Button onClick={handleSavePeriodicIndexing} disabled={isSavingPeriodic}>
{isSavingPeriodic && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Save Configuration
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View file

@ -3,8 +3,6 @@
import type React from "react";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
type IconComponent = React.ComponentType<{ size?: number; className?: string }>;
export function getDocumentTypeIcon(type: string): React.ReactNode {
return getConnectorIcon(type);
}

View file

@ -2,9 +2,10 @@
import { ChevronDown, ChevronUp, FileX, Plus } from "lucide-react";
import { motion } from "motion/react";
import { useParams, useRouter } from "next/navigation";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import React from "react";
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { DocumentViewer } from "@/components/document-viewer";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@ -69,9 +70,9 @@ export function DocumentsTableShell({
onSortChange: (key: SortKey) => void;
}) {
const t = useTranslations("documents");
const router = useRouter();
const params = useParams();
const searchSpaceId = params.search_space_id;
const { openDialog } = useDocumentUploadDialog();
const sorted = React.useMemo(
() => sortDocuments(documents, sortKey, sortDesc),
@ -137,19 +138,16 @@ export function DocumentsTableShell({
<div className="rounded-full bg-muted p-4">
<FileX className="h-8 w-8 text-muted-foreground" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button
onClick={() => router.push(`/dashboard/${searchSpaceId}/documents/upload`)}
className="mt-2"
>
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
<div className="space-y-2">
<h3 className="text-lg font-semibold">{t("no_documents")}</h3>
<p className="text-sm text-muted-foreground">
Get started by uploading your first document.
</p>
</div>
<Button onClick={openDialog} className="mt-2">
<Plus className="mr-2 h-4 w-4" />
Upload Documents
</Button>
</motion.div>
</div>
) : (

View file

@ -1,36 +0,0 @@
"use client";
import { Upload } from "lucide-react";
import { motion } from "motion/react";
import { useParams } from "next/navigation";
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
export default function UploadDocumentsPage() {
const params = useParams();
const search_space_id = params.search_space_id as string;
return (
<div className="container mx-auto py-8 px-4 min-h-[calc(100vh-64px)]">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="space-y-6"
>
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-2xl sm:text-4xl font-bold tracking-tight flex items-center justify-center gap-3">
<Upload className="h-6 w-6 sm:h-8 sm:w-8" />
Upload Documents
</h1>
<p className="text-muted-foreground text-sm sm:text-lg">
Upload documents to your search space for AI-powered search and chat
</p>
</div>
{/* Document Upload */}
<DocumentUploadTab searchSpaceId={search_space_id} />
</motion.div>
</div>
);
}

View file

@ -517,4 +517,4 @@ export default function EditorPage() {
</AlertDialog>
</motion.div>
);
}
}

View file

@ -30,28 +30,19 @@ export default function DashboardLayout({
{
title: "Chat",
url: `/dashboard/${search_space_id}/new-chat`,
icon: "SquareTerminal",
icon: "MessageCircle",
items: [],
},
{
title: "Sources",
url: "#",
icon: "Database",
items: [
{
title: "Manage Documents",
url: `/dashboard/${search_space_id}/documents`,
},
{
title: "Manage Connectors",
url: `/dashboard/${search_space_id}/connectors`,
},
],
title: "Documents",
url: `/dashboard/${search_space_id}/documents`,
icon: "SquareLibrary",
items: [],
},
{
title: "Logs",
url: `/dashboard/${search_space_id}/logs`,
icon: "FileText",
icon: "Logs",
items: [],
},
];

View file

@ -9,7 +9,10 @@ import { CheckIcon, CopyIcon, DownloadIcon, RefreshCwIcon } from "lucide-react";
import type { FC } from "react";
import { useContext } from "react";
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
import { ThinkingStepsContext, ThinkingStepsDisplay } from "@/components/assistant-ui/thinking-steps";
import {
ThinkingStepsContext,
ThinkingStepsDisplay,
} from "@/components/assistant-ui/thinking-steps";
import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { BranchPicker } from "@/components/assistant-ui/branch-picker";
@ -115,4 +118,3 @@ const AssistantActionBar: FC = () => {
</ActionBarPrimitive.Root>
);
};

View file

@ -9,8 +9,8 @@ import {
} from "@assistant-ui/react";
import { FileText, Loader2, Paperclip, PlusIcon, Upload, XIcon } from "lucide-react";
import Image from "next/image";
import { useParams, useRouter } from "next/navigation";
import { type FC, type PropsWithChildren, useRef, useEffect, useState } from "react";
import { useDocumentUploadDialog } from "./document-upload-popup";
import { useShallow } from "zustand/shallow";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@ -319,30 +319,33 @@ export const ComposerAttachments: FC = () => {
};
export const ComposerAddAttachment: FC = () => {
const router = useRouter();
const params = useParams();
const searchSpaceId = params.search_space_id as string;
const chatAttachmentInputRef = useRef<HTMLInputElement>(null);
const { openDialog } = useDocumentUploadDialog();
const handleFileUpload = () => {
router.push(`/dashboard/${searchSpaceId}/documents/upload`);
openDialog();
};
const handleChatAttachment = () => {
chatAttachmentInputRef.current?.click();
};
// Prevent event bubbling when file input is clicked
const handleFileInputClick = (e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
};
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TooltipIconButton
tooltip="Upload documents or add attachment"
tooltip="Upload"
side="bottom"
variant="ghost"
size="icon"
className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
aria-label="Upload documents or add attachment"
aria-label="Upload"
>
<PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
</TooltipIconButton>
@ -350,11 +353,11 @@ export const ComposerAddAttachment: FC = () => {
<DropdownMenuContent align="start" className="w-48 bg-background border-border">
<DropdownMenuItem onSelect={handleChatAttachment} className="cursor-pointer">
<Paperclip className="size-4" />
<span>Add attachment(s)</span>
<span>Add attachment</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={handleFileUpload} className="cursor-pointer">
<Upload className="size-4" />
<span>File upload</span>
<span>Upload Files</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -365,6 +368,7 @@ export const ComposerAddAttachment: FC = () => {
multiple
className="hidden"
accept="image/*,application/pdf,.doc,.docx,.txt"
onClick={handleFileInputClick}
/>
</ComposerPrimitive.AddAttachment>
</>

View file

@ -30,4 +30,3 @@ export const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({ className,
</BranchPickerPrimitive.Root>
);
};

View file

@ -1,7 +1,6 @@
import { AssistantIf, ComposerPrimitive, useAssistantState } from "@assistant-ui/react";
import { useAtomValue } from "jotai";
import { AlertCircle, ArrowUpIcon, Loader2, Plus, Plug2, SquareIcon } from "lucide-react";
import Link from "next/link";
import type { FC } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
@ -38,11 +37,10 @@ const ConnectorIndicator: FC = () => {
? Object.entries(documentTypeCounts).filter(([_, count]) => count > 0)
: [];
const nonIndexableConnectors = connectors.filter((connector) => !connector.is_indexable);
const hasConnectors = nonIndexableConnectors.length > 0;
// Count only active connectors (matching what's shown in the Active tab)
const activeConnectorsCount = connectors.length;
const hasConnectors = activeConnectorsCount > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = nonIndexableConnectors.length + activeDocumentTypes.length;
const handleMouseEnter = useCallback(() => {
// Clear any pending close timeout
@ -76,7 +74,9 @@ const ConnectorIndicator: FC = () => {
"text-muted-foreground"
)}
aria-label={
hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector"
hasConnectors
? `View ${activeConnectorsCount} active connectors`
: "Add your first connector"
}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
@ -86,9 +86,9 @@ const ConnectorIndicator: FC = () => {
) : (
<>
<Plug2 className="size-4" />
{totalSourceCount > 0 && (
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
{totalSourceCount > 99 ? "99+" : totalSourceCount}
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
@ -104,44 +104,64 @@ const ConnectorIndicator: FC = () => {
>
{hasSources ? (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Connected Sources</p>
<span className="text-xs font-medium bg-muted px-1.5 py-0.5 rounded">
{totalSourceCount}
</span>
</div>
<div className="flex flex-wrap gap-2">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(docType, "size-3.5")}
<span className="truncate max-w-[100px]">{getDocumentTypeLabel(docType)}</span>
<span className="flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-medium rounded-full bg-primary/10 text-primary">
{count > 999 ? "999+" : count}
</span>
{activeConnectorsCount > 0 && (
<div className="flex items-center justify-between">
<p className="text-xs font-medium text-muted-foreground">Active Connectors</p>
<span className="text-xs font-medium bg-muted px-1.5 py-0.5 rounded">
{activeConnectorsCount}
</span>
</div>
)}
{activeConnectorsCount > 0 && (
<div className="flex flex-wrap gap-2">
{connectors.map((connector) => (
<div
key={`connector-${connector.id}`}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(connector.connector_type, "size-3.5")}
<span className="truncate max-w-[100px]">{connector.name}</span>
</div>
))}
</div>
)}
{activeDocumentTypes.length > 0 && (
<>
{activeConnectorsCount > 0 && (
<div className="pt-2 border-t border-border/50">
<p className="text-xs font-medium text-muted-foreground mb-2">Documents</p>
</div>
)}
<div className="flex flex-wrap gap-2">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(docType, "size-3.5")}
<span className="truncate max-w-[100px]">
{getDocumentTypeLabel(docType)}
</span>
<span className="flex items-center justify-center min-w-[18px] h-[18px] px-1 text-[10px] font-medium rounded-full bg-primary/10 text-primary">
{count > 999 ? "999+" : count}
</span>
</div>
))}
</div>
))}
{nonIndexableConnectors.map((connector) => (
<div
key={`connector-${connector.id}`}
className="flex items-center gap-1.5 rounded-md bg-muted/80 px-2.5 py-1.5 text-xs border border-border/50"
>
{getConnectorIcon(connector.connector_type, "size-3.5")}
<span className="truncate max-w-[100px]">{connector.name}</span>
</div>
))}
</div>
</>
)}
<div className="pt-1 border-t border-border/50">
<Link
href={`/dashboard/${searchSpaceId}/connectors/add`}
<button
type="button"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
onClick={() => {
/* Connector popup should be opened via the connector indicator button */
}}
>
<Plus className="size-3" />
Add more sources
<ChevronRightIcon className="size-3" />
</Link>
</button>
</div>
</div>
) : (
@ -150,13 +170,16 @@ const ConnectorIndicator: FC = () => {
<p className="text-xs text-muted-foreground">
Add documents or connect data sources to enhance search results.
</p>
<Link
href={`/dashboard/${searchSpaceId}/connectors/add`}
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors mt-1"
onClick={() => {
/* Connector popup should be opened via the connector indicator button */
}}
>
<Plus className="size-3" />
Add Connector
</Link>
</button>
</div>
)}
</PopoverContent>
@ -268,4 +291,3 @@ export const ComposerAction: FC = () => {
</div>
);
};

View file

@ -8,10 +8,7 @@ import {
mentionedDocumentIdsAtom,
mentionedDocumentsAtom,
} from "@/atoms/chat/mentioned-documents.atom";
import {
ComposerAddAttachment,
ComposerAttachments,
} from "@/components/assistant-ui/attachment";
import { ComposerAddAttachment, ComposerAttachments } from "@/components/assistant-ui/attachment";
import { ComposerAction } from "@/components/assistant-ui/composer-action";
import {
InlineMentionEditor,
@ -237,4 +234,3 @@ export const Composer: FC = () => {
</ComposerPrimitive.Root>
);
};

View file

@ -10,14 +10,8 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { cacheKeys } from "@/lib/query-client/cache-keys";
import { connectorsApiService } from "@/lib/apis/connectors-api.service";
import {
Dialog,
DialogContent,
} from "@/components/ui/dialog";
import {
Tabs,
TabsContent,
} from "@/components/ui/tabs";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { cn } from "@/lib/utils";
import { AllConnectorsTab } from "./connector-popup/tabs/all-connectors-tab";
@ -35,19 +29,15 @@ export const ConnectorIndicator: FC = () => {
const searchParams = useSearchParams();
const { data: documentTypeCounts, isLoading: documentTypesLoading } =
useAtomValue(documentTypeCountsAtom);
// Check if YouTube view is active
const isYouTubeView = searchParams.get("view") === "youtube";
// Track active indexing tasks
const { summary: logsSummary } = useLogsSummary(
searchSpaceId ? Number(searchSpaceId) : 0,
24,
{
enablePolling: true,
refetchInterval: 5000,
}
);
const { summary: logsSummary } = useLogsSummary(searchSpaceId ? Number(searchSpaceId) : 0, 24, {
enablePolling: true,
refetchInterval: 5000,
});
// Use the custom hook for dialog state management
const {
@ -91,6 +81,7 @@ export const ConnectorIndicator: FC = () => {
handleBackFromEdit,
handleBackFromConnect,
handleBackFromYouTube,
handleQuickIndexConnector,
connectorConfig,
setConnectorConfig,
setIndexingConnectorConfig,
@ -159,6 +150,7 @@ export const ConnectorIndicator: FC = () => {
const hasConnectors = connectors.length > 0;
const hasSources = hasConnectors || activeDocumentTypes.length > 0;
const totalSourceCount = connectors.length + activeDocumentTypes.length;
const activeConnectorsCount = connectors.length; // Only actual connectors, not document types
// Check which connectors are already connected
const connectedTypes = new Set(
@ -170,7 +162,7 @@ export const ConnectorIndicator: FC = () => {
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<TooltipIconButton
tooltip={hasSources ? `Manage ${totalSourceCount} sources` : "Connect your data"}
tooltip={hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"}
side="bottom"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
@ -179,7 +171,7 @@ export const ConnectorIndicator: FC = () => {
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
)}
aria-label={
hasSources ? `View ${totalSourceCount} connected sources` : "Add your first connector"
hasConnectors ? `View ${activeConnectorsCount} connectors` : "Add your first connector"
}
onClick={() => handleOpenChange(true)}
>
@ -188,9 +180,9 @@ export const ConnectorIndicator: FC = () => {
) : (
<>
<Cable className="size-4 stroke-[1.5px]" />
{totalSourceCount > 0 && (
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm">
{totalSourceCount > 99 ? "99+" : totalSourceCount}
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
@ -200,10 +192,7 @@ export const ConnectorIndicator: FC = () => {
<DialogContent className="max-w-3xl w-[95vw] sm:w-full h-[90vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-6 sm:[&>button]:right-12 [&>button]:top-8 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button_svg]:size-5">
{/* YouTube Crawler View - shown when adding YouTube videos */}
{isYouTubeView && searchSpaceId ? (
<YouTubeCrawlerView
searchSpaceId={searchSpaceId}
onBack={handleBackFromYouTube}
/>
<YouTubeCrawlerView searchSpaceId={searchSpaceId} onBack={handleBackFromYouTube} />
) : connectingConnectorType ? (
<ConnectorConnectView
connectorType={connectingConnectorType}
@ -224,6 +213,7 @@ export const ConnectorIndicator: FC = () => {
frequencyMinutes={frequencyMinutes}
isSaving={isSaving}
isDisconnecting={isDisconnecting}
isIndexing={indexingConnectorIds.has(editingConnector.id)}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
onPeriodicEnabledChange={setPeriodicEnabled}
@ -231,16 +221,25 @@ export const ConnectorIndicator: FC = () => {
onSave={() => handleSaveConnector(() => refreshConnectors())}
onDisconnect={() => handleDisconnectConnector(() => refreshConnectors())}
onBack={handleBackFromEdit}
onQuickIndex={
editingConnector.connector_type !== "GOOGLE_DRIVE_CONNECTOR"
? () => handleQuickIndexConnector(editingConnector.id)
: undefined
}
onConfigChange={setConnectorConfig}
onNameChange={setConnectorName}
/>
) : indexingConfig ? (
<IndexingConfigurationView
config={indexingConfig}
connector={indexingConnector ? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
} : undefined}
connector={
indexingConnector
? {
...indexingConnector,
config: indexingConnectorConfig || indexingConnector.config,
}
: undefined
}
startDate={startDate}
endDate={endDate}
periodicEnabled={periodicEnabled}
@ -255,11 +254,15 @@ export const ConnectorIndicator: FC = () => {
onSkip={handleSkipIndexing}
/>
) : (
<Tabs value={activeTab} onValueChange={handleTabChange} className="flex-1 flex flex-col min-h-0">
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex-1 flex flex-col min-h-0"
>
{/* Header */}
<ConnectorDialogHeader
activeTab={activeTab}
totalSourceCount={totalSourceCount}
totalSourceCount={activeConnectorsCount}
searchQuery={searchQuery}
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
@ -270,25 +273,26 @@ export const ConnectorIndicator: FC = () => {
<div className="flex-1 min-h-0 relative overflow-hidden">
<div className="h-full overflow-y-auto" onScroll={handleScroll}>
<div className="px-6 sm:px-12 py-6 sm:py-8 pb-16 sm:pb-16">
<TabsContent value="all" className="m-0">
<AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={allConnectors}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds}
logsSummary={logsSummary}
onConnectOAuth={handleConnectOAuth}
onConnectNonOAuth={handleConnectNonOAuth}
onCreateWebcrawler={handleCreateWebcrawler}
onCreateYouTubeCrawler={handleCreateYouTubeCrawler}
onManage={handleStartEdit}
/>
</TabsContent>
<TabsContent value="all" className="m-0">
<AllConnectorsTab
searchQuery={searchQuery}
searchSpaceId={searchSpaceId}
connectedTypes={connectedTypes}
connectingId={connectingId}
allConnectors={allConnectors}
documentTypeCounts={documentTypeCounts}
indexingConnectorIds={indexingConnectorIds}
logsSummary={logsSummary}
onConnectOAuth={handleConnectOAuth}
onConnectNonOAuth={handleConnectNonOAuth}
onCreateWebcrawler={handleCreateWebcrawler}
onCreateYouTubeCrawler={handleCreateYouTubeCrawler}
onManage={handleStartEdit}
/>
</TabsContent>
<ActiveConnectorsTab
searchQuery={searchQuery}
hasSources={hasSources}
totalSourceCount={totalSourceCount}
activeDocumentTypes={activeDocumentTypes}

View file

@ -3,6 +3,7 @@
import { IconBrandYoutube } from "@tabler/icons-react";
import { FileText, Loader2 } from "lucide-react";
import { type FC } from "react";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { LogActiveTask } from "@/contracts/types/log.types";
@ -16,6 +17,7 @@ interface ConnectorCardProps {
isConnected?: boolean;
isConnecting?: boolean;
documentCount?: number;
lastIndexedAt?: string | null;
isIndexing?: boolean;
activeTask?: LogActiveTask;
onConnect?: () => void;
@ -33,6 +35,20 @@ function extractIndexedCount(message: string | undefined): number | null {
return match ? parseInt(match[1], 10) : null;
}
/**
* Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs")
*/
function formatDocumentCount(count: number | undefined): string {
if (count === undefined || count === 0) return "0 docs";
if (count < 1000) return `${count} docs`;
if (count < 1000000) {
const k = (count / 1000).toFixed(1);
return `${k.replace(/\.0$/, "")}k docs`;
}
const m = (count / 1000000).toFixed(1);
return `${m.replace(/\.0$/, "")}M docs`;
}
export const ConnectorCard: FC<ConnectorCardProps> = ({
id,
title,
@ -41,6 +57,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
isConnected = false,
isConnecting = false,
documentCount,
lastIndexedAt,
isIndexing = false,
activeTask,
onConnect,
@ -55,11 +72,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
return (
<div className="flex items-center gap-2 w-full max-w-[200px]">
<span className="text-[11px] text-primary font-medium whitespace-nowrap">
{indexingCount !== null ? (
<>{indexingCount.toLocaleString()} indexed</>
) : (
"Syncing..."
)}
{indexingCount !== null ? <>{indexingCount.toLocaleString()} indexed</> : "Syncing..."}
</span>
{/* Indeterminate progress bar with animation */}
<div className="relative flex-1 h-1 overflow-hidden rounded-full bg-primary/20">
@ -70,18 +83,16 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
}
if (isConnected) {
if (documentCount !== undefined && documentCount > 0) {
// Show last indexed date for connected connectors
if (lastIndexedAt) {
return (
<span className="inline-flex items-center gap-1.5">
<FileText className="size-3 flex-shrink-0" />
<span className="whitespace-nowrap">
{documentCount.toLocaleString()} document{documentCount !== 1 ? "s" : ""}
</span>
<span className="whitespace-nowrap">
Last indexed: {format(new Date(lastIndexedAt), "MMM d, yyyy")}
</span>
);
}
// Fallback for connected but no documents yet
return <span className="whitespace-nowrap">No documents indexed</span>;
// Fallback for connected but never indexed
return <span className="whitespace-nowrap">Never indexed</span>;
}
return description;
@ -102,16 +113,20 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
<div className="flex items-center gap-2">
<span className="text-[14px] font-semibold leading-tight">{title}</span>
</div>
<div className="text-[11px] text-muted-foreground mt-1">
{getStatusContent()}
</div>
<div className="text-[11px] text-muted-foreground mt-1">{getStatusContent()}</div>
{isConnected && documentCount !== undefined && (
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
)}
</div>
<Button
size="sm"
variant={isConnected ? "secondary" : "default"}
className={cn(
"h-8 text-[11px] px-3 rounded-lg flex-shrink-0 font-medium",
isConnected && "bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80",
isConnected &&
"bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80",
!isConnected && "shadow-xs"
)}
onClick={isConnected ? onManage : onConnect}
@ -123,6 +138,8 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
"Syncing..."
) : isConnected ? (
"Manage"
) : id === "youtube-crawler" ? (
"Add"
) : connectorType ? (
"Connect"
) : (
@ -132,4 +149,3 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
</div>
);
};

View file

@ -1,16 +1,9 @@
"use client";
import { Search } from "lucide-react";
import { Search, X } from "lucide-react";
import type { FC } from "react";
import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";
interface ConnectorDialogHeaderProps {
@ -74,14 +67,26 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
<input
type="text"
placeholder="Search"
className="w-full bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 focus:bg-slate-400/10 dark:focus:bg-white/10 border border-border rounded-xl pl-9 pr-4 py-2 text-sm transition-all outline-none placeholder:text-muted-foreground/50"
className={cn(
"w-full bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 focus:bg-slate-400/10 dark:focus:bg-white/10 border border-border rounded-xl pl-9 py-2 text-sm transition-all outline-none placeholder:text-muted-foreground/50",
searchQuery ? "pr-9" : "pr-4"
)}
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
/>
{searchQuery && (
<button
type="button"
onClick={() => onSearchChange("")}
className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
aria-label="Clear search"
>
<X className="size-4" />
</button>
)}
</div>
</div>
</div>
</div>
);
};

View file

@ -43,13 +43,16 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
<h3 className="font-medium text-sm sm:text-base mb-4">Select Date Range</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-6">
Choose how far back you want to sync your data. You can always re-index later with different dates.
Choose how far back you want to sync your data. You can always re-index later with different
dates.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Start Date */}
<div className="space-y-2">
<Label htmlFor="start-date" className="text-xs sm:text-sm">Start Date</Label>
<Label htmlFor="start-date" className="text-xs sm:text-sm">
Start Date
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
@ -77,7 +80,9 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
{/* End Date */}
<div className="space-y-2">
<Label htmlFor="end-date" className="text-xs sm:text-sm">End Date</Label>
<Label htmlFor="end-date" className="text-xs sm:text-sm">
End Date
</Label>
<Popover>
<PopoverTrigger asChild>
<Button
@ -137,4 +142,3 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
</div>
);
};

View file

@ -37,9 +37,11 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
</div>
{enabled && (
<div className="mt-4 pt-4 border-t border-border/100 space-y-3">
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select value={frequencyMinutes} onValueChange={onFrequencyChange}>
<SelectTrigger
id="frequency"
@ -48,12 +50,24 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -62,4 +76,3 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
</div>
);
};

View file

@ -32,10 +32,7 @@ const baiduSearchApiFormSchema = z.object({
type BaiduSearchApiFormValues = z.infer<typeof baiduSearchApiFormSchema>;
export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const form = useForm<BaiduSearchApiFormValues>({
resolver: zodResolver(baiduSearchApiFormSchema),
@ -77,7 +74,8 @@ export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">API Key Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing up at{" "}
You'll need a Baidu AppBuilder API key to use this connector. You can get one by signing
up at{" "}
<a
href="https://qianfan.cloud.baidu.com/"
target="_blank"
@ -92,7 +90,11 @@ export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
<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="baidu-search-api-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="baidu-search-api-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -100,11 +102,11 @@ export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Baidu Search Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Baidu Search Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -122,12 +124,12 @@ export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Baidu AppBuilder API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Baidu API key"
<Input
type="password"
placeholder="Enter your Baidu API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -155,4 +157,3 @@ export const BaiduSearchApiConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -53,10 +53,7 @@ const bookstackConnectorFormSchema = z.object({
type BookStackConnectorFormValues = z.infer<typeof bookstackConnectorFormSchema>;
export const BookStackConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const BookStackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -110,14 +107,19 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">API Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a BookStack API Token to use this connector. You can create one from your BookStack instance settings.
You'll need a BookStack API Token to use this connector. You can create one from your
BookStack instance 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="bookstack-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="bookstack-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -125,11 +127,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My BookStack Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My BookStack Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -147,16 +149,17 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">BookStack Base URL</FormLabel>
<FormControl>
<Input
<Input
type="url"
placeholder="https://your-bookstack-instance.com"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
placeholder="https://your-bookstack-instance.com"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
The base URL of your BookStack instance (e.g., https://your-bookstack-instance.com).
The base URL of your BookStack instance (e.g.,
https://your-bookstack-instance.com).
</FormDescription>
<FormMessage />
</FormItem>
@ -170,11 +173,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Token ID</FormLabel>
<FormControl>
<Input
placeholder="Your Token ID"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="Your Token ID"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -192,12 +195,12 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Token Secret</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Token Secret"
<Input
type="password"
placeholder="Your Token Secret"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -211,7 +214,7 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -229,14 +232,24 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -244,12 +257,24 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -264,7 +289,9 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_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 BookStack integration:</h4>
<h4 className="text-xs sm:text-sm font-medium">
What you get with BookStack integration:
</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
@ -274,7 +301,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -283,14 +314,17 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The BookStack connector uses the BookStack REST API to fetch all pages from your BookStack instance that your account has access to.
The BookStack connector uses the BookStack REST API to fetch all pages from your
BookStack instance that your account has access to.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves pages that have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves pages that have been updated
since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
@ -302,13 +336,16 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create an API token from your BookStack instance. The token requires "Access System API" permission.
You need to create an API token from your BookStack instance. The token requires
"Access System API" permission.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create an API Token
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your BookStack instance</li>
<li>Click on your profile icon Edit Profile</li>
@ -320,15 +357,19 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Your user account must have "Access System API" permission. The connector will only index content your account can view.
Your user account must have "Access System API" permission. The connector will
only index content your account can view.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Rate Limiting</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
BookStack API has a rate limit of 180 requests per minute. The connector automatically handles rate limiting to ensure reliable indexing.
BookStack API has a rate limit of 180 requests per minute. The connector
automatically handles rate limiting to ensure reliable indexing.
</AlertDescription>
</Alert>
</div>
@ -341,13 +382,16 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>BookStack</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>BookStack</strong>{" "}
Connector.
</li>
<li>
Enter your <strong>BookStack Instance URL</strong> (e.g., https://docs.example.com)
Enter your <strong>BookStack Instance URL</strong> (e.g.,
https://docs.example.com)
</li>
<li>
Enter your <strong>Token ID</strong> and <strong>Token Secret</strong> from your BookStack API token.
Enter your <strong>Token ID</strong> and <strong>Token Secret</strong> from your
BookStack API token.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
@ -376,4 +420,3 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -29,10 +29,7 @@ const circlebackFormSchema = z.object({
type CirclebackFormValues = z.infer<typeof circlebackFormSchema>;
export const CirclebackConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const CirclebackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const form = useForm<CirclebackFormValues>({
resolver: zodResolver(circlebackFormSchema),
@ -71,14 +68,19 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
<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.
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">
<form
id="circleback-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -86,11 +88,11 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
<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"
<Input
placeholder="My Circleback Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -118,4 +120,3 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -49,10 +49,7 @@ const clickupConnectorFormSchema = z.object({
type ClickUpConnectorFormValues = z.infer<typeof clickupConnectorFormSchema>;
export const ClickUpConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const ClickUpConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -117,7 +114,11 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<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="clickup-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="clickup-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -125,11 +126,11 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My ClickUp Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My ClickUp Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -147,12 +148,12 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">ClickUp API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="pk_..."
<Input
type="password"
placeholder="pk_..."
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -166,7 +167,7 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -184,14 +185,24 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -199,12 +210,24 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -229,7 +252,11 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -238,14 +265,17 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The ClickUp connector uses the ClickUp API to fetch all tasks and projects that your API token has access to within your workspace.
The ClickUp connector uses the ClickUp API to fetch all tasks and projects that your
API token has access to within your workspace.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves tasks that have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves tasks that have been updated
since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
@ -257,19 +287,23 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need a ClickUp personal API token to use this connector. The token will be used to read your ClickUp data.
You need a ClickUp personal API token to use this connector. The token will be
used to read your ClickUp data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Get Your API Token</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Get Your API Token
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your ClickUp account</li>
<li>Click your avatar in the upper-right corner and select "Settings"</li>
<li>In the sidebar, click "Apps"</li>
<li>
Under "API Token", click <strong>Generate</strong> or <strong>Regenerate</strong>
Under "API Token", click <strong>Generate</strong> or{" "}
<strong>Regenerate</strong>
</li>
<li>Copy the generated token (it typically starts with "pk_")</li>
<li>
@ -288,15 +322,20 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all tasks and projects that your user account can see. Make sure your account has appropriate permissions for the workspaces you want to index.
The API Token will have access to all tasks and projects that your user
account can see. Make sure your account has appropriate permissions for the
workspaces you want to index.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only tasks, comments, and basic metadata will be indexed. ClickUp attachments and linked files are not indexed by this connector.
Only tasks, comments, and basic metadata will be indexed. ClickUp
attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
@ -309,7 +348,8 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>ClickUp</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>ClickUp</strong>{" "}
Connector.
</li>
<li>
Place your <strong>API Token</strong> in the form field.
@ -341,4 +381,3 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -51,10 +51,7 @@ const confluenceConnectorFormSchema = z.object({
type ConfluenceConnectorFormValues = z.infer<typeof confluenceConnectorFormSchema>;
export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const ConfluenceConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -123,7 +120,11 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<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="confluence-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="confluence-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -131,11 +132,11 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Confluence Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Confluence Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -153,16 +154,17 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Confluence Base URL</FormLabel>
<FormControl>
<Input
<Input
type="url"
placeholder="https://your-domain.atlassian.net"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
placeholder="https://your-domain.atlassian.net"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
The base URL of your Confluence instance (e.g., https://your-domain.atlassian.net).
The base URL of your Confluence instance (e.g.,
https://your-domain.atlassian.net).
</FormDescription>
<FormMessage />
</FormItem>
@ -176,13 +178,13 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl>
<Input
<Input
type="email"
placeholder="your-email@example.com"
placeholder="your-email@example.com"
autoComplete="email"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -200,12 +202,12 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Token"
<Input
type="password"
placeholder="Your API Token"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -219,7 +221,7 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -237,14 +239,24 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -252,12 +264,24 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -272,7 +296,9 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_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 Confluence integration:</h4>
<h4 className="text-xs sm:text-sm font-medium">
What you get with Confluence integration:
</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
@ -282,7 +308,11 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -291,14 +321,17 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Confluence connector uses the Confluence REST API to fetch all pages and comments that your account has access to within your Confluence instance.
The Confluence connector uses the Confluence REST API to fetch all pages and
comments that your account has access to within your Confluence instance.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves pages and comments that have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves pages and comments that have
been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
@ -308,15 +341,20 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Read-Only Access is Sufficient</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Read-Only Access is Sufficient
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You only need read access for this connector to work. The API Token will only be used to read your Confluence data.
You only need read access for this connector to work. The API Token will only be
used to read your Confluence data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create an API Token
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Atlassian account</li>
<li>
@ -343,15 +381,20 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all spaces and pages that your user account can see. Make sure your account has appropriate permissions for the spaces you want to index.
The API Token will have access to all spaces and pages that your user account
can see. Make sure your account has appropriate permissions for the spaces you
want to index.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only pages, comments, and basic metadata will be indexed. Confluence attachments and linked files are not indexed by this connector.
Only pages, comments, and basic metadata will be indexed. Confluence
attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
@ -364,10 +407,12 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Confluence</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>Confluence</strong>{" "}
Connector.
</li>
<li>
Enter your <strong>Confluence Instance URL</strong> (e.g., https://yourcompany.atlassian.net)
Enter your <strong>Confluence Instance URL</strong> (e.g.,
https://yourcompany.atlassian.net)
</li>
<li>
Enter your <strong>Email Address</strong> associated with your Atlassian account
@ -402,4 +447,3 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -42,19 +42,14 @@ const discordConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
bot_token: z
.string()
.min(10, {
message: "Discord Bot Token is required and must be valid.",
}),
bot_token: z.string().min(10, {
message: "Discord Bot Token is required and must be valid.",
}),
});
type DiscordConnectorFormValues = z.infer<typeof discordConnectorFormSchema>;
export const DiscordConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const DiscordConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -119,7 +114,11 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<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="discord-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="discord-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -127,11 +126,11 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Discord Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Discord Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -149,12 +148,12 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Discord Bot Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Bot Token"
<Input
type="password"
placeholder="Your Bot Token"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -168,7 +167,7 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -186,14 +185,24 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -201,12 +210,24 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -231,7 +252,11 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -240,13 +265,13 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Discord connector uses the Discord API to fetch messages from all accessible channels
that the bot token has access to within a server.
The Discord connector uses the Discord API to fetch messages from all accessible
channels that the bot token has access to within a server.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves messages that
have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves messages that have been
updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your
@ -262,16 +287,19 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Bot Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create a Discord application and bot to get a bot token.
The bot needs read access to channels and messages.
You need to create a Discord application and bot to get a bot token. The bot
needs read access to channels and messages.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create a Discord Application</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create a Discord Application
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Go to{" "}
<li>
Go to{" "}
<a
href="https://discord.com/developers/applications"
target="_blank"
@ -281,30 +309,56 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
https://discord.com/developers/applications
</a>
</li>
<li>Click <strong>New Application</strong></li>
<li>Enter an application name and click <strong>Create</strong></li>
<li>
Click <strong>New Application</strong>
</li>
<li>
Enter an application name and click <strong>Create</strong>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Create a Bot</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Create a Bot
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Navigate to <strong>Bot</strong> in the sidebar</li>
<li>Click <strong>Add Bot</strong> and confirm</li>
<li>Under <strong>Privileged Gateway Intents</strong>, enable:
<li>
Navigate to <strong>Bot</strong> in the sidebar
</li>
<li>
Click <strong>Add Bot</strong> and confirm
</li>
<li>
Under <strong>Privileged Gateway Intents</strong>, enable:
<ul className="list-disc pl-5 mt-1 space-y-1">
<li><code className="bg-muted px-1 py-0.5 rounded">MESSAGE CONTENT INTENT</code> - Required to read message content</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">
MESSAGE CONTENT INTENT
</code>{" "}
- Required to read message content
</li>
</ul>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 3: Get Bot Token and Invite Bot</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 3: Get Bot Token and Invite Bot
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Under <strong>Token</strong>, click <strong>Reset Token</strong> and copy the token</li>
<li>Navigate to <strong>OAuth2 URL Generator</strong></li>
<li>Select <strong>bot</strong> scope and <strong>Read Messages</strong> permission</li>
<li>
Under <strong>Token</strong>, click <strong>Reset Token</strong> and copy
the token
</li>
<li>
Navigate to <strong>OAuth2 URL Generator</strong>
</li>
<li>
Select <strong>bot</strong> scope and <strong>Read Messages</strong>{" "}
permission
</li>
<li>Copy the generated URL and open it in your browser</li>
<li>Select your server and authorize the bot</li>
</ol>
@ -351,4 +405,3 @@ export const DiscordConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -73,10 +73,7 @@ const elasticsearchConnectorFormSchema = z
type ElasticsearchConnectorFormValues = z.infer<typeof elasticsearchConnectorFormSchema>;
export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const authBasicId = useId();
const authApiKeyId = useId();
@ -187,7 +184,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<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="elasticsearch-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="elasticsearch-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -195,11 +196,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Elasticsearch Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Elasticsearch Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -231,7 +232,8 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Enter the complete Elasticsearch endpoint URL. We'll automatically extract the hostname, port, and SSL settings.
Enter the complete Elasticsearch endpoint URL. We'll automatically extract the
hostname, port, and SSL settings.
</FormDescription>
<FormMessage />
</FormItem>
@ -241,7 +243,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Show parsed URL details */}
{form.watch("endpoint_url") && (
<div className="rounded-lg border border-border bg-muted/50 p-3">
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Parsed Connection Details:</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Parsed Connection Details:
</h4>
<div className="text-[10px] sm:text-xs text-muted-foreground space-y-1">
{(() => {
try {
@ -305,7 +309,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator>
</RadioGroup.Item>
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">API Key</Label>
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">
API Key
</Label>
</div>
<div className="flex items-center space-x-2">
@ -318,7 +324,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator>
</RadioGroup.Item>
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">Username & Password</Label>
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">
Username & Password
</Label>
</div>
</RadioGroup.Root>
</FormControl>
@ -337,12 +345,12 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Username</FormLabel>
<FormControl>
<Input
placeholder="elastic"
autoComplete="username"
<Input
placeholder="elastic"
autoComplete="username"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormMessage />
@ -392,7 +400,8 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Enter your Elasticsearch API key (base64 encoded). This will be stored securely.
Enter your Elasticsearch API key (base64 encoded). This will be stored
securely.
</FormDescription>
<FormMessage />
</FormItem>
@ -409,11 +418,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Index Selection</FormLabel>
<FormControl>
<Input
placeholder="logs-*, documents-*, app-logs"
<Input
placeholder="logs-*, documents-*, app-logs"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -454,7 +463,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Advanced Configuration */}
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="advanced">
<AccordionTrigger className="text-xs sm:text-sm">Advanced Configuration</AccordionTrigger>
<AccordionTrigger className="text-xs sm:text-sm">
Advanced Configuration
</AccordionTrigger>
<AccordionContent className="space-y-4">
{/* Default Search Query */}
<FormField
@ -467,15 +478,16 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<span className="text-muted-foreground">(Optional)</span>
</FormLabel>
<FormControl>
<Input
placeholder="*"
<Input
placeholder="*"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Default Elasticsearch query to use for searches. Use "*" to match all documents.
Default Elasticsearch query to use for searches. Use "*" to match all
documents.
</FormDescription>
<FormMessage />
</FormItem>
@ -489,19 +501,19 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">
Search Fields{" "}
<span className="text-muted-foreground">(Optional)</span>
Search Fields <span className="text-muted-foreground">(Optional)</span>
</FormLabel>
<FormControl>
<Input
placeholder="title, content, description"
<Input
placeholder="title, content, description"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Comma-separated list of specific fields to search in (e.g., "title, content, description"). Leave empty to search all fields.
Comma-separated list of specific fields to search in (e.g., "title,
content, description"). Leave empty to search all fields.
</FormDescription>
<FormMessage />
</FormItem>
@ -542,15 +554,14 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{...field}
onChange={(e) =>
field.onChange(
e.target.value === ""
? undefined
: parseInt(e.target.value, 10)
e.target.value === "" ? undefined : parseInt(e.target.value, 10)
)
}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Maximum number of documents to retrieve per search (1-10,000). Leave empty to use Elasticsearch's default limit.
Maximum number of documents to retrieve per search (1-10,000). Leave empty
to use Elasticsearch's default limit.
</FormDescription>
<FormMessage />
</FormItem>
@ -563,7 +574,7 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -581,14 +592,24 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -596,12 +617,24 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -616,7 +649,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_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 Elasticsearch integration:</h4>
<h4 className="text-xs sm:text-sm font-medium">
What you get with Elasticsearch integration:
</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
@ -626,7 +661,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -635,7 +674,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Elasticsearch connector allows you to search and retrieve documents from your Elasticsearch cluster. Configure connection details, select specific indices, and set search parameters to make your existing data searchable within SurfSense.
The Elasticsearch connector allows you to search and retrieve documents from your
Elasticsearch cluster. Configure connection details, select specific indices, and
set search parameters to make your existing data searchable within SurfSense.
</p>
</div>
@ -644,43 +685,73 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Connection Setup</h3>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Get your Elasticsearch endpoint</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Get your Elasticsearch endpoint
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
You'll need the endpoint URL for your Elasticsearch cluster. This typically looks like:
You'll need the endpoint URL for your Elasticsearch cluster. This typically
looks like:
</p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>Cloud: <code className="bg-muted px-1 py-0.5 rounded">https://your-cluster.es.region.aws.com:443</code></li>
<li>Self-hosted: <code className="bg-muted px-1 py-0.5 rounded">https://elasticsearch.example.com:9200</code></li>
<li>
Cloud:{" "}
<code className="bg-muted px-1 py-0.5 rounded">
https://your-cluster.es.region.aws.com:443
</code>
</li>
<li>
Self-hosted:{" "}
<code className="bg-muted px-1 py-0.5 rounded">
https://elasticsearch.example.com:9200
</code>
</li>
</ul>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Configure authentication</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Configure authentication
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Elasticsearch requires authentication. You can use either:
</p>
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
<strong>API Key:</strong> A base64-encoded API key. You can create one in Elasticsearch by running:
<strong>API Key:</strong> A base64-encoded API key. You can create one in
Elasticsearch by running:
<pre className="bg-muted p-2 rounded mt-1 text-[9px] overflow-x-auto">
<code>POST /_security/api_key</code>
</pre>
</li>
<li>
<strong>Username & Password:</strong> Basic authentication using your Elasticsearch username and password.
<strong>Username & Password:</strong> Basic authentication using your
Elasticsearch username and password.
</li>
</ul>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 3: Select indices</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 3: Select indices
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Specify which indices to search. You can:
</p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground">
<li>Use wildcards: <code className="bg-muted px-1 py-0.5 rounded">logs-*</code> to match multiple indices</li>
<li>List specific indices: <code className="bg-muted px-1 py-0.5 rounded">logs-2024, documents-2024</code></li>
<li>Leave empty to search all accessible indices (not recommended for performance)</li>
<li>
Use wildcards: <code className="bg-muted px-1 py-0.5 rounded">logs-*</code>{" "}
to match multiple indices
</li>
<li>
List specific indices:{" "}
<code className="bg-muted px-1 py-0.5 rounded">
logs-2024, documents-2024
</code>
</li>
<li>
Leave empty to search all accessible indices (not recommended for
performance)
</li>
</ul>
</div>
</div>
@ -694,19 +765,30 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Query</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-2">
The default query used for searches. Use <code className="bg-muted px-1 py-0.5 rounded">*</code> to match all documents, or specify a more complex Elasticsearch query.
The default query used for searches. Use{" "}
<code className="bg-muted px-1 py-0.5 rounded">*</code> to match all
documents, or specify a more complex Elasticsearch query.
</p>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Fields</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-2">
Limit searches to specific fields for better performance. Common fields include:
Limit searches to specific fields for better performance. Common fields
include:
</p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground">
<li><code className="bg-muted px-1 py-0.5 rounded">title</code> - Document titles</li>
<li><code className="bg-muted px-1 py-0.5 rounded">content</code> - Main content</li>
<li><code className="bg-muted px-1 py-0.5 rounded">description</code> - Descriptions</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">title</code> - Document
titles
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">content</code> - Main content
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">description</code> -
Descriptions
</li>
</ul>
<p className="text-[10px] sm:text-xs text-muted-foreground mt-2">
Leave empty to search all fields in your documents.
@ -716,7 +798,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Maximum Documents</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Set a limit on the number of documents retrieved per search (1-10,000). This helps control response times and resource usage. Leave empty to use Elasticsearch's default limit.
Set a limit on the number of documents retrieved per search (1-10,000). This
helps control response times and resource usage. Leave empty to use
Elasticsearch's default limit.
</p>
</div>
</div>
@ -731,28 +815,38 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Connection Issues</h4>
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
<strong>Invalid URL:</strong> Ensure your endpoint URL includes the protocol (https://) and port number if required.
<strong>Invalid URL:</strong> Ensure your endpoint URL includes the protocol
(https://) and port number if required.
</li>
<li>
<strong>SSL/TLS Errors:</strong> Verify that your cluster uses HTTPS and the certificate is valid. Self-signed certificates may require additional configuration.
<strong>SSL/TLS Errors:</strong> Verify that your cluster uses HTTPS and the
certificate is valid. Self-signed certificates may require additional
configuration.
</li>
<li>
<strong>Connection Timeout:</strong> Check your network connectivity and firewall settings. Ensure the Elasticsearch cluster is accessible from SurfSense servers.
<strong>Connection Timeout:</strong> Check your network connectivity and
firewall settings. Ensure the Elasticsearch cluster is accessible from
SurfSense servers.
</li>
</ul>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Authentication Issues</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Authentication Issues
</h4>
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
<strong>Invalid Credentials:</strong> Double-check your username/password or API key. API keys must be base64-encoded.
<strong>Invalid Credentials:</strong> Double-check your username/password or
API key. API keys must be base64-encoded.
</li>
<li>
<strong>Permission Denied:</strong> Ensure your API key or user account has read permissions for the indices you want to search.
<strong>Permission Denied:</strong> Ensure your API key or user account has
read permissions for the indices you want to search.
</li>
<li>
<strong>API Key Format:</strong> Elasticsearch API keys are typically base64-encoded strings. Make sure you're using the full key value.
<strong>API Key Format:</strong> Elasticsearch API keys are typically
base64-encoded strings. Make sure you're using the full key value.
</li>
</ul>
</div>
@ -761,13 +855,16 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Issues</h4>
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
<strong>No Results:</strong> Verify that your index selection matches existing indices. Use wildcards carefully.
<strong>No Results:</strong> Verify that your index selection matches
existing indices. Use wildcards carefully.
</li>
<li>
<strong>Slow Searches:</strong> Limit the number of indices or use specific index names instead of wildcards. Reduce the maximum documents limit.
<strong>Slow Searches:</strong> Limit the number of indices or use specific
index names instead of wildcards. Reduce the maximum documents limit.
</li>
<li>
<strong>Field Not Found:</strong> Ensure the search fields you specify actually exist in your Elasticsearch documents.
<strong>Field Not Found:</strong> Ensure the search fields you specify
actually exist in your Elasticsearch documents.
</li>
</ul>
</div>
@ -776,7 +873,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Need More Help?</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
If you continue to experience issues, check your Elasticsearch cluster logs and ensure your cluster version is compatible. For Elasticsearch Cloud deployments, verify your access policies and IP allowlists.
If you continue to experience issues, check your Elasticsearch cluster logs
and ensure your cluster version is compatible. For Elasticsearch Cloud
deployments, verify your access policies and IP allowlists.
</AlertDescription>
</Alert>
</div>
@ -788,4 +887,3 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -57,10 +57,7 @@ const githubConnectorFormSchema = z.object({
type GithubConnectorFormValues = z.infer<typeof githubConnectorFormSchema>;
export const GithubConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const GithubConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -92,7 +89,7 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
isSubmittingRef.current = true;
try {
const repoList = stringToArray(values.repo_full_names);
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.GITHUB_CONNECTOR,
@ -122,7 +119,8 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Personal Access Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a GitHub Personal Access Token to use this connector. You can create one from{" "}
You'll need a GitHub Personal Access Token to use this connector. You can create one
from{" "}
<a
href="https://github.com/settings/tokens"
target="_blank"
@ -137,7 +135,11 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<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="github-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="github-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -145,11 +147,11 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My GitHub Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My GitHub Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -167,16 +169,17 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">GitHub Personal Access Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ghp_..."
<Input
type="password"
placeholder="ghp_..."
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your GitHub PAT will be encrypted and stored securely. It typically starts with "ghp_" or "github_pat_".
Your GitHub PAT will be encrypted and stored securely. It typically starts with
"ghp_" or "github_pat_".
</FormDescription>
<FormMessage />
</FormItem>
@ -190,15 +193,16 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Repository Names</FormLabel>
<FormControl>
<Input
placeholder="owner/repo1, owner/repo2"
<Input
placeholder="owner/repo1, owner/repo2"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Comma-separated list of repository full names (e.g., "owner/repo1, owner/repo2").
Comma-separated list of repository full names (e.g., "owner/repo1,
owner/repo2").
</FormDescription>
<FormMessage />
</FormItem>
@ -222,7 +226,7 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -240,14 +244,24 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -255,12 +269,24 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -285,7 +311,11 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -294,7 +324,10 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The GitHub connector uses a Personal Access Token (PAT) to authenticate with the GitHub API. You provide a comma-separated list of repository full names (e.g., "owner/repo1, owner/repo2") that you want to index. The connector indexes relevant files (code, markdown, text) from the selected repositories.
The GitHub connector uses a Personal Access Token (PAT) to authenticate with the
GitHub API. You provide a comma-separated list of repository full names (e.g.,
"owner/repo1, owner/repo2") that you want to index. The connector indexes relevant
files (code, markdown, text) from the selected repositories.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
@ -303,7 +336,8 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<li>Large files (over 1MB) are skipped during indexing.</li>
<li>Only specified repositories are indexed.</li>
<li>
Indexing runs periodically (check connector settings for frequency) to keep content up-to-date.
Indexing runs periodically (check connector settings for frequency) to keep
content up-to-date.
</li>
</ul>
</div>
@ -313,15 +347,20 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Personal Access Token Required</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Personal Access Token Required
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch repositories. The PAT will be stored securely to enable indexing.
You'll need a GitHub PAT with the appropriate scopes (e.g., 'repo') to fetch
repositories. The PAT will be stored securely to enable indexing.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Generate GitHub PAT</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Generate GitHub PAT
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Go to your GitHub{" "}
@ -336,39 +375,46 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
</li>
<li>
Click on <strong>Personal access tokens</strong>, then choose{" "}
<strong>Tokens (classic)</strong> or <strong>Fine-grained tokens</strong> (recommended if available).
<strong>Tokens (classic)</strong> or <strong>Fine-grained tokens</strong>{" "}
(recommended if available).
</li>
<li>
Click <strong>Generate new token</strong> (and choose the appropriate type).
</li>
<li>Give your token a descriptive name (e.g., "SurfSense Connector").</li>
<li>Set an expiration date for the token (recommended for security).</li>
<li>
Give your token a descriptive name (e.g., "SurfSense Connector").
</li>
<li>
Set an expiration date for the token (recommended for security).
</li>
<li>
Under <strong>Select scopes</strong> (for classic tokens) or <strong>Repository access</strong> (for fine-grained), grant the necessary permissions. At minimum, the <strong>`repo`</strong> scope (or equivalent read access to repositories for fine-grained tokens) is required to read repository content.
Under <strong>Select scopes</strong> (for classic tokens) or{" "}
<strong>Repository access</strong> (for fine-grained), grant the necessary
permissions. At minimum, the <strong>`repo`</strong> scope (or equivalent
read access to repositories for fine-grained tokens) is required to read
repository content.
</li>
<li>
Click <strong>Generate token</strong>.
</li>
<li>
<strong>Important:</strong> Copy your new PAT immediately. You won't be able to see it again after leaving the page.
<strong>Important:</strong> Copy your new PAT immediately. You won't be able
to see it again after leaving the page.
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Specify repositories</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Specify repositories
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Enter a comma-separated list of repository full names in the format "owner/repo1, owner/repo2". The connector will index files from only the specified repositories.
Enter a comma-separated list of repository full names in the format
"owner/repo1, owner/repo2". The connector will index files from only the
specified repositories.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Repository Access</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Make sure your PAT has access to all repositories you want to index. Private repositories require appropriate permissions.
Make sure your PAT has access to all repositories you want to index. Private
repositories require appropriate permissions.
</AlertDescription>
</Alert>
</div>
@ -381,13 +427,15 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>GitHub</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>GitHub</strong>{" "}
Connector.
</li>
<li>
Enter your <strong>GitHub Personal Access Token</strong> in the form field.
</li>
<li>
Enter a comma-separated list of <strong>Repository Names</strong> (e.g., "owner/repo1, owner/repo2").
Enter a comma-separated list of <strong>Repository Names</strong> (e.g.,
"owner/repo1, owner/repo2").
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
@ -416,4 +464,3 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -51,10 +51,7 @@ const jiraConnectorFormSchema = z.object({
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
export const JiraConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const JiraConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -123,7 +120,11 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<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="jira-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="jira-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -131,11 +132,11 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Jira Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Jira Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -153,12 +154,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Jira Base URL</FormLabel>
<FormControl>
<Input
<Input
type="url"
placeholder="https://your-domain.atlassian.net"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
placeholder="https://your-domain.atlassian.net"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -176,13 +177,13 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl>
<Input
<Input
type="email"
placeholder="your-email@example.com"
placeholder="your-email@example.com"
autoComplete="email"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -200,12 +201,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Token"
<Input
type="password"
placeholder="Your API Token"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -219,7 +220,7 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -237,14 +238,24 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -252,12 +263,24 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -282,7 +305,11 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -291,14 +318,17 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Jira connector uses the Jira REST API with Basic Authentication to fetch all issues and comments that your account has access to within your Jira instance.
The Jira connector uses the Jira REST API with Basic Authentication to fetch all
issues and comments that your account has access to within your Jira instance.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves issues and comments that have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves issues and comments that have
been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
@ -308,15 +338,20 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Read-Only Access is Sufficient</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Read-Only Access is Sufficient
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You only need read access for this connector to work. The API Token will only be used to read your Jira data.
You only need read access for this connector to work. The API Token will only be
used to read your Jira data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API Token</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create an API Token
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Atlassian account</li>
<li>
@ -343,15 +378,20 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API Token will have access to all projects and issues that your user account can see. Make sure your account has appropriate permissions for the projects you want to index.
The API Token will have access to all projects and issues that your user
account can see. Make sure your account has appropriate permissions for the
projects you want to index.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only issues, comments, and basic metadata will be indexed. Jira attachments and linked files are not indexed by this connector.
Only issues, comments, and basic metadata will be indexed. Jira attachments
and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
@ -364,10 +404,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Jira</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>Jira</strong>{" "}
Connector.
</li>
<li>
Enter your <strong>Jira Instance URL</strong> (e.g., https://yourcompany.atlassian.net)
Enter your <strong>Jira Instance URL</strong> (e.g.,
https://yourcompany.atlassian.net)
</li>
<li>
Enter your <strong>Email Address</strong> associated with your Atlassian account
@ -404,4 +446,3 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -54,10 +54,7 @@ const linearConnectorFormSchema = z.object({
type LinearConnectorFormValues = z.infer<typeof linearConnectorFormSchema>;
export const LinearConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const LinearConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -122,7 +119,11 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<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="linear-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="linear-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -130,11 +131,11 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Linear Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Linear Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -152,16 +153,17 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Linear API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="lin_api_..."
<Input
type="password"
placeholder="lin_api_..."
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your Linear API Key will be encrypted and stored securely. It typically starts with "lin_api_".
Your Linear API Key will be encrypted and stored securely. It typically starts
with "lin_api_".
</FormDescription>
<FormMessage />
</FormItem>
@ -171,7 +173,7 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -189,14 +191,24 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -204,12 +216,24 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -234,7 +258,11 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -243,13 +271,13 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Linear connector uses the Linear GraphQL API to fetch all issues and
comments that the API key has access to within a workspace.
The Linear connector uses the Linear GraphQL API to fetch all issues and comments
that the API key has access to within a workspace.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves issues and comments that
have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves issues and comments that have
been updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your
@ -263,16 +291,20 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Read-Only Access is Sufficient</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Read-Only Access is Sufficient
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You only need a read-only API key for this connector to work. This limits
the permissions to just reading your Linear data.
You only need a read-only API key for this connector to work. This limits the
permissions to just reading your Linear data.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create an API key</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create an API key
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log in to your Linear account</li>
<li>
@ -297,25 +329,27 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
Click <strong>Create</strong> to generate the API key.
</li>
<li>
Copy the generated API key that starts with 'lin_api_' as it will only
be shown once.
Copy the generated API key that starts with 'lin_api_' as it will only be
shown once.
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API key will have access to all issues and comments that your user
account can see. If you're creating the key as an admin, it will have
access to all issues in the workspace.
The API key will have access to all issues and comments that your user account
can see. If you're creating the key as an admin, it will have access to all
issues in the workspace.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only issues and comments will be indexed. Linear attachments and
linked files are not indexed by this connector.
Only issues and comments will be indexed. Linear attachments and linked
files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
@ -361,4 +395,3 @@ export const LinearConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -32,10 +32,7 @@ const linkupApiFormSchema = z.object({
type LinkupApiFormValues = z.infer<typeof linkupApiFormSchema>;
export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const LinkupApiConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const form = useForm<LinkupApiFormValues>({
resolver: zodResolver(linkupApiFormSchema),
@ -79,12 +76,12 @@ export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Linkup API key to use this connector. You can get one by signing up at{" "}
<a
href="https://linkup.ai"
href="https://linkup.so"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
linkup.ai
linkup.so
</a>
</AlertDescription>
</div>
@ -92,7 +89,11 @@ export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
<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="linkup-api-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="linkup-api-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -100,11 +101,11 @@ export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Linkup API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Linkup API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -122,12 +123,12 @@ export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Linkup API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Linkup API key"
<Input
type="password"
placeholder="Enter your Linkup API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -155,4 +156,3 @@ export const LinkupApiConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -49,10 +49,7 @@ const lumaConnectorFormSchema = z.object({
type LumaConnectorFormValues = z.infer<typeof lumaConnectorFormSchema>;
export const LumaConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const LumaConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -117,7 +114,11 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<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="luma-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="luma-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -125,11 +126,11 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Luma Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Luma Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -147,12 +148,12 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Luma API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your API Key"
<Input
type="password"
placeholder="Your API Key"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -166,7 +167,7 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -184,14 +185,24 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -199,12 +210,24 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -229,7 +252,11 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -238,14 +265,17 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Luma connector uses the Luma API to fetch all events that your API key has access to.
The Luma connector uses the Luma API to fetch all events that your API key has
access to.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves events that have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves events that have been updated
since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your search results within minutes.
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
@ -257,13 +287,16 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">API Key Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need a Luma API key to use this connector. The key will be used to read your Luma events with read-only permissions.
You need a Luma API key to use this connector. The key will be used to read your
Luma events with read-only permissions.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Get Your API Key</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Get Your API Key
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Log into your Luma account</li>
<li>Navigate to your account settings</li>
@ -286,15 +319,20 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Grant necessary access</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Grant necessary access
</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
The API key will have access to all events that your user account can see. Make sure your account has appropriate permissions for the events you want to index.
The API key will have access to all events that your user account can see.
Make sure your account has appropriate permissions for the events you want to
index.
</p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
Only event details, descriptions, and attendee information will be indexed. Event attachments and linked files are not indexed by this connector.
Only event details, descriptions, and attendee information will be indexed.
Event attachments and linked files are not indexed by this connector.
</AlertDescription>
</Alert>
</div>
@ -307,7 +345,8 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Luma</strong> Connector.
Navigate to the Connector Dashboard and select the <strong>Luma</strong>{" "}
Connector.
</li>
<li>
Place your <strong>API Key</strong> in the form field.
@ -339,4 +378,3 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -42,19 +42,14 @@ const notionConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
integration_token: z
.string()
.min(10, {
message: "Notion Integration Token is required and must be valid.",
}),
integration_token: z.string().min(10, {
message: "Notion Integration Token is required and must be valid.",
}),
});
type NotionConnectorFormValues = z.infer<typeof notionConnectorFormSchema>;
export const NotionConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const NotionConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -119,7 +114,11 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<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="notion-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="notion-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -127,11 +126,11 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Notion Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Notion Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -149,16 +148,17 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Notion Integration Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="ntn_..."
<Input
type="password"
placeholder="ntn_..."
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your Notion Integration Token will be encrypted and stored securely. It typically starts with "ntn_".
Your Notion Integration Token will be encrypted and stored securely. It
typically starts with "ntn_".
</FormDescription>
<FormMessage />
</FormItem>
@ -168,7 +168,7 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -186,14 +186,24 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -201,12 +211,24 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -231,7 +253,11 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -240,13 +266,13 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Notion connector uses the Notion API to fetch pages from all accessible workspaces
that the integration token has access to.
The Notion connector uses the Notion API to fetch pages from all accessible
workspaces that the integration token has access to.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves pages that
have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves pages that have been updated
since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your
@ -260,7 +286,9 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Integration Token Required</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Integration Token Required
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create a Notion integration and share pages with it to get access.
The integration needs read access to pages.
@ -269,9 +297,12 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create a Notion Integration</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create a Notion Integration
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Go to{" "}
<li>
Go to{" "}
<a
href="https://www.notion.so/my-integrations"
target="_blank"
@ -281,21 +312,35 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
https://www.notion.so/my-integrations
</a>
</li>
<li>Click <strong>+ New integration</strong></li>
<li>
Click <strong>+ New integration</strong>
</li>
<li>Enter a name for your integration (e.g., "Search Connector")</li>
<li>Select your workspace</li>
<li>Under <strong>Capabilities</strong>, enable <strong>Read content</strong></li>
<li>Click <strong>Submit</strong> to create the integration</li>
<li>Copy the <strong>Internal Integration Token</strong> (starts with "ntn_")</li>
<li>
Under <strong>Capabilities</strong>, enable <strong>Read content</strong>
</li>
<li>
Click <strong>Submit</strong> to create the integration
</li>
<li>
Copy the <strong>Internal Integration Token</strong> (starts with "ntn_")
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Share Pages with Integration</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Share Pages with Integration
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Open the Notion pages or databases you want to index</li>
<li>Click the <strong></strong> (three dots) menu in the top right</li>
<li>Select <strong>Add connections</strong> or <strong>Connections</strong></li>
<li>
Click the <strong></strong> (three dots) menu in the top right
</li>
<li>
Select <strong>Add connections</strong> or <strong>Connections</strong>
</li>
<li>Search for and select your integration</li>
<li>Repeat for all pages you want to index</li>
</ol>
@ -303,8 +348,8 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Important</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
The integration can only access pages that have been explicitly shared with it.
Make sure to share all pages you want to index.
The integration can only access pages that have been explicitly shared with
it. Make sure to share all pages you want to index.
</AlertDescription>
</Alert>
</div>
@ -350,4 +395,3 @@ export const NotionConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -52,10 +52,7 @@ const parseCommaSeparated = (value?: string | null) => {
return items.length > 0 ? items : undefined;
};
export const SearxngConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const SearxngConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const form = useForm<SearxngFormValues>({
resolver: zodResolver(searxngFormSchema),
@ -146,7 +143,11 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
<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="searxng-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="searxng-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -154,11 +155,11 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My SearxNG Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My SearxNG Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -176,15 +177,16 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">SearxNG Host</FormLabel>
<FormControl>
<Input
placeholder="https://searxng.example.org"
<Input
placeholder="https://searxng.example.org"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Provide the full base URL to your SearxNG instance. Include the protocol (http/https).
Provide the full base URL to your SearxNG instance. Include the protocol
(http/https).
</FormDescription>
<FormMessage />
</FormItem>
@ -222,11 +224,11 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Engines (optional)</FormLabel>
<FormControl>
<Input
placeholder="google,bing,duckduckgo"
<Input
placeholder="google,bing,duckduckgo"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -244,11 +246,11 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Categories (optional)</FormLabel>
<FormControl>
<Input
placeholder="general,it,science"
<Input
placeholder="general,it,science"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -266,13 +268,15 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
name="language"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Preferred Language (optional)</FormLabel>
<FormLabel className="text-xs sm:text-sm">
Preferred Language (optional)
</FormLabel>
<FormControl>
<Input
placeholder="en-US"
<Input
placeholder="en-US"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -288,17 +292,20 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
name="safesearch"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">SafeSearch Level (optional)</FormLabel>
<FormLabel className="text-xs sm:text-sm">
SafeSearch Level (optional)
</FormLabel>
<FormControl>
<Input
placeholder="0 (off), 1 (moderate), 2 (strict)"
<Input
placeholder="0 (off), 1 (moderate), 2 (strict)"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance default.
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance
default.
</FormDescription>
<FormMessage />
</FormItem>
@ -318,7 +325,11 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} disabled={isSubmitting} />
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={isSubmitting}
/>
</FormControl>
</FormItem>
)}
@ -341,4 +352,3 @@ export const SearxngConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -42,19 +42,14 @@ const slackConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
bot_token: z
.string()
.min(10, {
message: "Slack Bot Token is required and must be valid.",
}),
bot_token: z.string().min(10, {
message: "Slack Bot Token is required and must be valid.",
}),
});
type SlackConnectorFormValues = z.infer<typeof slackConnectorFormSchema>;
export const SlackConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const SlackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -104,7 +99,8 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Bot User OAuth Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Slack Bot User OAuth Token to use this connector. You can create a Slack app and get the token from{" "}
You'll need a Slack Bot User OAuth Token to use this connector. You can create a Slack
app and get the token from{" "}
<a
href="https://api.slack.com/apps"
target="_blank"
@ -119,7 +115,11 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<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="slack-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="slack-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -127,11 +127,11 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Slack Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Slack Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -149,16 +149,17 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Slack Bot User OAuth Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="xoxb-..."
<Input
type="password"
placeholder="xoxb-..."
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your Bot User OAuth Token will be encrypted and stored securely. It typically starts with "xoxb-".
Your Bot User OAuth Token will be encrypted and stored securely. It typically
starts with "xoxb-".
</FormDescription>
<FormMessage />
</FormItem>
@ -168,7 +169,7 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
@ -186,14 +187,24 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals
</p>
</div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} />
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">Sync Frequency</Label>
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}>
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
@ -201,12 +212,24 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
@ -231,7 +254,11 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
)}
{/* Documentation Section */}
<Accordion type="single" collapsible className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5">
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
@ -240,13 +267,13 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Slack connector uses the Slack Web API to fetch messages from all accessible channels
that the bot token has access to within a workspace.
The Slack connector uses the Slack Web API to fetch messages from all accessible
channels that the bot token has access to within a workspace.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves messages that
have been updated since the last indexing attempt.
For follow up indexing runs, the connector retrieves messages that have been
updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your
@ -260,18 +287,23 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Bot User OAuth Token Required</AlertTitle>
<AlertTitle className="text-[10px] sm:text-xs">
Bot User OAuth Token Required
</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create a Slack app and install it to your workspace to get a Bot User OAuth Token.
The bot needs read access to channels and messages.
You need to create a Slack app and install it to your workspace to get a Bot
User OAuth Token. The bot needs read access to channels and messages.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 1: Create a Slack App</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create a Slack App
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Go to{" "}
<li>
Go to{" "}
<a
href="https://api.slack.com/apps"
target="_blank"
@ -281,36 +313,74 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
https://api.slack.com/apps
</a>
</li>
<li>Click <strong>Create New App</strong> and choose "From scratch"</li>
<li>
Click <strong>Create New App</strong> and choose "From scratch"
</li>
<li>Enter an app name and select your workspace</li>
<li>Click <strong>Create App</strong></li>
<li>
Click <strong>Create App</strong>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 2: Configure Bot Scopes</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Configure Bot Scopes
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Navigate to <strong>OAuth & Permissions</strong> in the sidebar</li>
<li>Under <strong>Bot Token Scopes</strong>, add the following scopes:
<li>
Navigate to <strong>OAuth & Permissions</strong> in the sidebar
</li>
<li>
Under <strong>Bot Token Scopes</strong>, add the following scopes:
<ul className="list-disc pl-5 mt-1 space-y-1">
<li><code className="bg-muted px-1 py-0.5 rounded">channels:read</code> - View basic information about public channels</li>
<li><code className="bg-muted px-1 py-0.5 rounded">channels:history</code> - View messages in public channels</li>
<li><code className="bg-muted px-1 py-0.5 rounded">groups:read</code> - View basic information about private channels</li>
<li><code className="bg-muted px-1 py-0.5 rounded">groups:history</code> - View messages in private channels</li>
<li><code className="bg-muted px-1 py-0.5 rounded">im:read</code> - View basic information about direct messages</li>
<li><code className="bg-muted px-1 py-0.5 rounded">im:history</code> - View messages in direct messages</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">channels:read</code> -
View basic information about public channels
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">channels:history</code> -
View messages in public channels
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">groups:read</code> - View
basic information about private channels
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">groups:history</code> -
View messages in private channels
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">im:read</code> - View
basic information about direct messages
</li>
<li>
<code className="bg-muted px-1 py-0.5 rounded">im:history</code> - View
messages in direct messages
</li>
</ul>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Step 3: Install App to Workspace</h4>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 3: Install App to Workspace
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>Go to <strong>Install App</strong> in the sidebar</li>
<li>Click <strong>Install to Workspace</strong></li>
<li>Review the permissions and click <strong>Allow</strong></li>
<li>Copy the <strong>Bot User OAuth Token</strong> from the "OAuth & Permissions" page (starts with "xoxb-")</li>
<li>
Go to <strong>Install App</strong> in the sidebar
</li>
<li>
Click <strong>Install to Workspace</strong>
</li>
<li>
Review the permissions and click <strong>Allow</strong>
</li>
<li>
Copy the <strong>Bot User OAuth Token</strong> from the "OAuth &
Permissions" page (starts with "xoxb-")
</li>
</ol>
</div>
</div>
@ -355,4 +425,3 @@ export const SlackConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -32,10 +32,7 @@ const tavilyApiFormSchema = z.object({
type TavilyApiFormValues = z.infer<typeof tavilyApiFormSchema>;
export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
onSubmit,
isSubmitting,
}) => {
export const TavilyApiConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const form = useForm<TavilyApiFormValues>({
resolver: zodResolver(tavilyApiFormSchema),
@ -92,7 +89,11 @@ export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
<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="tavily-connect-form" onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 sm:space-y-6">
<form
id="tavily-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
@ -100,11 +101,11 @@ export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Tavily API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
<Input
placeholder="My Tavily API Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -122,12 +123,12 @@ export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
<FormItem>
<FormLabel className="text-xs sm:text-sm">Tavily API Key</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter your Tavily API key"
<Input
type="password"
placeholder="Enter your Tavily API key"
className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
@ -155,4 +156,3 @@ export const TavilyApiConnectForm: FC<ConnectFormProps> = ({
</div>
);
};

View file

@ -112,4 +112,3 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
return benefits[connectorType] || null;
}

View file

@ -41,9 +41,7 @@ export type ConnectFormComponent = FC<ConnectFormProps>;
/**
* Factory function to get the appropriate connect form component for a connector type
*/
export function getConnectFormComponent(
connectorType: string
): ConnectFormComponent | null {
export function getConnectFormComponent(connectorType: string): ConnectFormComponent | null {
switch (connectorType) {
case "TAVILY_API":
return TavilyApiConnectForm;
@ -82,4 +80,3 @@ export function getConnectFormComponent(
return null;
}
}

View file

@ -16,9 +16,7 @@ export const BaiduSearchApiConfig: FC<BaiduSearchApiConfigProps> = ({
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.BAIDU_API_KEY as string) || ""
);
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
@ -89,4 +87,3 @@ export const BaiduSearchApiConfig: FC<BaiduSearchApiConfigProps> = ({
</div>
);
};

View file

@ -148,4 +148,3 @@ export const BookStackConfig: FC<BookStackConfigProps> = ({
</div>
);
};

View file

@ -1,8 +1,9 @@
"use client";
import { Copy, Webhook, Check } from "lucide-react";
import { Copy, Webhook, Check, Info } from "lucide-react";
import { useState, useEffect } from "react";
import type { FC } from "react";
import { z } from "zod";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
@ -14,13 +15,22 @@ export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const CirclebackConfig: FC<CirclebackConfigProps> = ({
connector,
onNameChange,
}) => {
// Type-safe schema for webhook info response
const circlebackWebhookInfoSchema = z.object({
webhook_url: z.string(),
search_space_id: z.number(),
method: z.string(),
content_type: z.string(),
description: z.string(),
note: z.string(),
});
export type CirclebackWebhookInfo = z.infer<typeof circlebackWebhookInfoSchema>;
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 [webhookInfo, setWebhookInfo] = useState<CirclebackWebhookInfo | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [copied, setCopied] = useState(false);
@ -33,19 +43,31 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
useEffect(() => {
const fetchWebhookInfo = async () => {
if (!connector.search_space_id) return;
const baseUrl = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL;
if (!baseUrl) {
console.error("NEXT_PUBLIC_FASTAPI_BACKEND_URL is not configured");
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const response = await authenticatedFetch(
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/webhooks/circleback/${connector.search_space_id}/info`
`${baseUrl}/api/v1/webhooks/circleback/${connector.search_space_id}/info`
);
if (response.ok) {
const data = await response.json();
setWebhookInfo(data);
setWebhookUrl(data.webhook_url || "");
const data: unknown = await response.json();
// Runtime validation with zod schema
const validatedData = circlebackWebhookInfoSchema.parse(data);
setWebhookInfo(validatedData);
setWebhookUrl(validatedData.webhook_url);
}
} catch (error) {
console.error("Failed to fetch webhook info:", error);
// Reset state on error
setWebhookInfo(null);
setWebhookUrl("");
} finally {
setIsLoading(false);
}
@ -107,11 +129,12 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
<Input
value={webhookUrl}
readOnly
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono text-xs"
disabled
className="border-slate-400/20 focus-visible:border-slate-400/40 font-mono text-xs bg-muted/50 cursor-default select-all"
/>
<Button
type="button"
variant="outline"
variant="secondary"
size="sm"
onClick={handleCopyWebhookUrl}
className="shrink-0"
@ -141,11 +164,12 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
{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" />
<Info 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.
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>
)}
@ -153,4 +177,3 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
</div>
);
};

View file

@ -89,4 +89,3 @@ export const ClickUpConfig: FC<ClickUpConfigProps> = ({
</div>
);
};

View file

@ -19,9 +19,7 @@ export const ConfluenceConfig: FC<ConfluenceConfigProps> = ({
const [baseUrl, setBaseUrl] = useState<string>(
(connector.config?.CONFLUENCE_BASE_URL as string) || ""
);
const [email, setEmail] = useState<string>(
(connector.config?.CONFLUENCE_EMAIL as string) || ""
);
const [email, setEmail] = useState<string>((connector.config?.CONFLUENCE_EMAIL as string) || "");
const [apiToken, setApiToken] = useState<string>(
(connector.config?.CONFLUENCE_API_TOKEN as string) || ""
);
@ -149,4 +147,3 @@ export const ConfluenceConfig: FC<ConfluenceConfigProps> = ({
</div>
);
};

View file

@ -89,4 +89,3 @@ export const DiscordConfig: FC<DiscordConfigProps> = ({
</div>
);
};

View file

@ -271,7 +271,9 @@ export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator>
</RadioGroup.Item>
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">API Key</Label>
<Label htmlFor={authApiKeyId} className="text-xs sm:text-sm">
API Key
</Label>
</div>
<div className="flex items-center space-x-2">
@ -284,7 +286,9 @@ export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator>
</RadioGroup.Item>
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">Username & Password</Label>
<Label htmlFor={authBasicId} className="text-xs sm:text-sm">
Username & Password
</Label>
</div>
</RadioGroup.Root>
@ -435,4 +439,3 @@ export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
</div>
);
};

View file

@ -20,7 +20,10 @@ export const GithubConfig: FC<GithubConfigProps> = ({
const stringToArray = (arr: string[] | string | undefined): string[] => {
if (Array.isArray(arr)) return arr;
if (typeof arr === "string") {
return arr.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
return arr
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
}
return [];
};
@ -147,4 +150,3 @@ export const GithubConfig: FC<GithubConfigProps> = ({
</div>
);
};

View file

@ -13,19 +13,21 @@ interface SelectedFolder {
name: string;
}
export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
connector,
onConfigChange,
}) => {
// Initialize with existing selected folders from connector config
const existingFolders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
// Initialize with existing selected folders and files from connector config
const existingFolders =
(connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
const [showFolderSelector, setShowFolderSelector] = useState(false);
// Update selected folders when connector config changes
// Update selected folders and files when connector config changes
useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
setSelectedFolders(folders);
setSelectedFiles(files);
}, [connector.config]);
const handleSelectFolders = (folders: SelectedFolder[]) => {
@ -35,28 +37,61 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
onConfigChange({
...connector.config,
selected_folders: folders,
selected_files: selectedFiles, // Preserve existing files
});
}
};
const handleSelectFiles = (files: SelectedFolder[]) => {
setSelectedFiles(files);
if (onConfigChange) {
// Store file IDs and names in config for indexing
onConfigChange({
...connector.config,
selected_folders: selectedFolders, // Preserve existing folders
selected_files: files,
});
}
};
const totalSelected = selectedFolders.length + selectedFiles.length;
return (
<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">Folder Selection</h3>
<h3 className="font-medium text-sm sm:text-base">Folder & File Selection</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Select specific folders to index. Only files directly in each folder will be processedsubfolders must be selected separately.
Select specific folders and/or individual files to index. Only files directly in each
folder will be processedsubfolders must be selected separately.
</p>
</div>
{selectedFolders.length > 0 && (
{totalSelected > 0 && (
<div className="p-2 sm:p-3 bg-muted rounded-lg text-xs sm:text-sm space-y-1 sm:space-y-2">
<p className="font-medium">
Selected {selectedFolders.length} folder{selectedFolders.length > 1 ? "s" : ""}:
Selected {totalSelected} item{totalSelected > 1 ? "s" : ""}:
{selectedFolders.length > 0 &&
` ${selectedFolders.length} folder${selectedFolders.length > 1 ? "s" : ""}`}
{selectedFiles.length > 0 &&
` ${selectedFiles.length} file${selectedFiles.length > 1 ? "s" : ""}`}
</p>
<div className="max-h-20 sm:max-h-24 overflow-y-auto">
<div className="max-h-20 sm:max-h-24 overflow-y-auto space-y-1">
{selectedFolders.map((folder) => (
<p key={folder.id} className="text-xs sm:text-sm text-muted-foreground truncate" title={folder.name}>
{folder.name}
<p
key={folder.id}
className="text-xs sm:text-sm text-muted-foreground truncate"
title={folder.name}
>
📁 {folder.name}
</p>
))}
{selectedFiles.map((file) => (
<p
key={file.id}
className="text-xs sm:text-sm text-muted-foreground truncate"
title={file.name}
>
📄 {file.name}
</p>
))}
</div>
@ -69,6 +104,8 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
connectorId={connector.id}
selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
/>
<Button
type="button"
@ -87,17 +124,17 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
onClick={() => setShowFolderSelector(true)}
className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 hover:bg-slate-400/10 dark:hover:bg-white/10 text-xs sm:text-sm h-8 sm:h-9"
>
{selectedFolders.length > 0 ? "Change Folder Selection" : "Select Folders"}
{totalSelected > 0 ? "Change Selection" : "Select Folders & Files"}
</Button>
)}
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center gap-2 [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0" />
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
Folder selection is used when indexing. You can change this selection when you start indexing.
Folder and file selection is used when indexing. You can change this selection when you
start indexing.
</AlertDescription>
</Alert>
</div>
);
};

View file

@ -11,17 +11,9 @@ export interface JiraConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const JiraConfig: FC<JiraConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [baseUrl, setBaseUrl] = useState<string>(
(connector.config?.JIRA_BASE_URL as string) || ""
);
const [email, setEmail] = useState<string>(
(connector.config?.JIRA_EMAIL as string) || ""
);
export const JiraConfig: FC<JiraConfigProps> = ({ connector, onConfigChange, onNameChange }) => {
const [baseUrl, setBaseUrl] = useState<string>((connector.config?.JIRA_BASE_URL as string) || "");
const [email, setEmail] = useState<string>((connector.config?.JIRA_EMAIL as string) || "");
const [apiToken, setApiToken] = useState<string>(
(connector.config?.JIRA_API_TOKEN as string) || ""
);
@ -149,4 +141,3 @@ export const JiraConfig: FC<JiraConfigProps> = ({
</div>
);
};

View file

@ -16,9 +16,7 @@ export const LinearConfig: FC<LinearConfigProps> = ({
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.LINEAR_API_KEY as string) || ""
);
const [apiKey, setApiKey] = useState<string>((connector.config?.LINEAR_API_KEY as string) || "");
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
@ -89,4 +87,3 @@ export const LinearConfig: FC<LinearConfigProps> = ({
</div>
);
};

View file

@ -16,9 +16,7 @@ export const LinkupApiConfig: FC<LinkupApiConfigProps> = ({
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.LINKUP_API_KEY as string) || ""
);
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
@ -89,4 +87,3 @@ export const LinkupApiConfig: FC<LinkupApiConfigProps> = ({
</div>
);
};

View file

@ -11,14 +11,8 @@ export interface LumaConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const LumaConfig: FC<LumaConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.LUMA_API_KEY as string) || ""
);
export const LumaConfig: FC<LumaConfigProps> = ({ connector, onConfigChange, onNameChange }) => {
const [apiKey, setApiKey] = useState<string>((connector.config?.LUMA_API_KEY as string) || "");
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
@ -89,4 +83,3 @@ export const LumaConfig: FC<LumaConfigProps> = ({
</div>
);
};

View file

@ -89,4 +89,3 @@ export const NotionConfig: FC<NotionConfigProps> = ({
</div>
);
};

View file

@ -34,15 +34,9 @@ export const SearxngConfig: FC<SearxngConfigProps> = ({
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 [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)
);
@ -300,7 +294,8 @@ export const SearxngConfig: FC<SearxngConfigProps> = ({
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.
Set 0, 1, or 2 to adjust SafeSearch filtering. Leave blank to use the instance
default.
</p>
</div>
</div>
@ -319,4 +314,3 @@ export const SearxngConfig: FC<SearxngConfigProps> = ({
</div>
);
};

View file

@ -11,11 +11,7 @@ export interface SlackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const SlackConfig: FC<SlackConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
export const SlackConfig: FC<SlackConfigProps> = ({ connector, onConfigChange, onNameChange }) => {
const [botToken, setBotToken] = useState<string>(
(connector.config?.SLACK_BOT_TOKEN as string) || ""
);
@ -89,4 +85,3 @@ export const SlackConfig: FC<SlackConfigProps> = ({
</div>
);
};

View file

@ -16,9 +16,7 @@ export const TavilyApiConfig: FC<TavilyApiConfigProps> = ({
onConfigChange,
onNameChange,
}) => {
const [apiKey, setApiKey] = useState<string>(
(connector.config?.TAVILY_API_KEY as string) || ""
);
const [apiKey, setApiKey] = useState<string>((connector.config?.TAVILY_API_KEY as string) || "");
const [name, setName] = useState<string>(connector.name || "");
// Update API key and name when connector changes
@ -89,4 +87,3 @@ export const TavilyApiConfig: FC<TavilyApiConfigProps> = ({
</div>
);
};

View file

@ -10,14 +10,11 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { ConnectorConfigProps } from "../index";
export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
connector,
onConfigChange,
}) => {
export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
// Initialize with existing config values
const existingApiKey = (connector.config?.FIRECRAWL_API_KEY as string | undefined) || "";
const existingUrls = (connector.config?.INITIAL_URLS as string | undefined) || "";
const [apiKey, setApiKey] = useState(existingApiKey);
const [initialUrls, setInitialUrls] = useState(existingUrls);
const [showApiKey, setShowApiKey] = useState(false);
@ -43,9 +40,11 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
const handleUrlsChange = (value: string) => {
setInitialUrls(value);
if (onConfigChange) {
// Preserve newlines for multi-line URL input
// Backend will handle trimming individual URLs when splitting by newline
onConfigChange({
...connector.config,
INITIAL_URLS: value.trim() || undefined,
INITIAL_URLS: value || undefined,
});
}
};
@ -55,7 +54,8 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Web Crawler Configuration</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Configure your web crawler settings. You can add a Firecrawl API key for enhanced crawling or use the free fallback option.
Configure your web crawler settings. You can add a Firecrawl API key for enhanced crawling
or use the free fallback option.
</p>
</div>
@ -118,10 +118,10 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center gap-2 [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0" />
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
Configuration is saved when you start indexing. You can update these settings anytime from the connector management page.
Configuration is saved when you start indexing. You can update these settings anytime from
the connector management page.
</AlertDescription>
</Alert>
</div>
);
};

View file

@ -77,4 +77,3 @@ export function getConnectorConfigComponent(
return null;
}
}

View file

@ -126,17 +126,17 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
{/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
<Button
variant="ghost"
onClick={onBack}
disabled={isSubmitting}
<Button
variant="ghost"
onClick={onBack}
disabled={isSubmitting}
className="text-xs sm:text-sm"
>
Cancel
</Button>
<Button
onClick={handleFormSubmit}
disabled={isSubmitting}
<Button
onClick={handleFormSubmit}
disabled={isSubmitting}
className="text-xs sm:text-sm min-w-[140px] disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none"
>
{isSubmitting ? (
@ -145,13 +145,10 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
Connecting...
</>
) : (
<>
Connect {getConnectorTypeDisplay(connectorType)}
</>
<>Connect {getConnectorTypeDisplay(connectorType)}</>
)}
</Button>
</div>
</div>
);
};

View file

@ -1,6 +1,6 @@
"use client";
import { ArrowLeft, Info, Loader2, Trash2 } from "lucide-react";
import { ArrowLeft, Info, Loader2, RefreshCw, Trash2 } from "lucide-react";
import { type FC, useState, useCallback, useRef, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
@ -18,6 +18,7 @@ interface ConnectorEditViewProps {
frequencyMinutes: string;
isSaving: boolean;
isDisconnecting: boolean;
isIndexing?: boolean;
onStartDateChange: (date: Date | undefined) => void;
onEndDateChange: (date: Date | undefined) => void;
onPeriodicEnabledChange: (enabled: boolean) => void;
@ -25,7 +26,8 @@ interface ConnectorEditViewProps {
onSave: () => void;
onDisconnect: () => void;
onBack: () => void;
onConfigChange?: (config: Record<string, any>) => void;
onQuickIndex?: () => void;
onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void;
}
@ -37,6 +39,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
frequencyMinutes,
isSaving,
isDisconnecting,
isIndexing = false,
onStartDateChange,
onEndDateChange,
onPeriodicEnabledChange,
@ -44,6 +47,7 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
onSave,
onDisconnect,
onBack,
onQuickIndex,
onConfigChange,
onNameChange,
}) => {
@ -59,12 +63,13 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
const checkScrollState = useCallback(() => {
if (!scrollContainerRef.current) return;
const target = scrollContainerRef.current;
const scrolled = target.scrollTop > 0;
const hasMore = target.scrollHeight > target.clientHeight &&
const hasMore =
target.scrollHeight > target.clientHeight &&
target.scrollTop + target.clientHeight < target.scrollHeight - 10;
setIsScrolled(scrolled);
setHasMoreContent(hasMore);
}, []);
@ -79,11 +84,11 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
const resizeObserver = new ResizeObserver(() => {
checkScrollState();
});
if (scrollContainerRef.current) {
resizeObserver.observe(scrollContainerRef.current);
}
return () => {
resizeObserver.disconnect();
};
@ -105,10 +110,12 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
return (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Fixed Header */}
<div className={cn(
"flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10 transition-shadow duration-200 relative z-10",
isScrolled && "shadow-sm"
)}>
<div
className={cn(
"flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10 transition-shadow duration-200 relative z-10",
isScrolled && "shadow-sm"
)}
>
{/* Back button */}
<button
type="button"
@ -120,26 +127,50 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</button>
{/* Connector header */}
<div className="flex items-center gap-4 mb-6">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 border border-primary/20">
{getConnectorIcon(connector.connector_type, "size-7")}
</div>
<div className="flex-1">
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">
{connector.name}
</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
Manage your connector settings and sync configuration
</p>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">
<div className="flex items-center gap-4 flex-1 w-full sm:w-auto">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 border border-primary/20 flex-shrink-0">
{getConnectorIcon(connector.connector_type, "size-7")}
</div>
<div className="flex-1 min-w-0">
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">{connector.name}</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
Manage your connector settings and sync configuration
</p>
</div>
</div>
{/* Quick Index Button - only show for indexable connectors, but not for Google Drive (requires folder selection) */}
{connector.is_indexable &&
onQuickIndex &&
connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && (
<Button
variant="secondary"
size="sm"
onClick={onQuickIndex}
disabled={isIndexing || isSaving || isDisconnecting}
className="text-xs sm:text-sm bg-slate-400/10 dark:bg-white/10 hover:bg-slate-400/20 dark:hover:bg-white/20 border-slate-400/20 dark:border-white/20 w-full sm:w-auto"
>
{isIndexing ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Indexing...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Quick Index
</>
)}
</Button>
)}
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 min-h-0 relative overflow-hidden">
<div
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto px-6 sm:px-12"
className="h-full overflow-y-auto px-6 sm:px-12"
onScroll={handleScroll}
>
<div className="space-y-6 pb-6 pt-2">
@ -156,21 +187,25 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
{connector.is_indexable && (
<>
{/* Date range selector - not shown for Google Drive (uses folder selection) or Webcrawler (uses config) */}
{connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && connector.connector_type !== "WEBCRAWLER_CONNECTOR" && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
{connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" &&
connector.connector_type !== "WEBCRAWLER_CONNECTOR" && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
/>
)}
{/* Periodic sync - not shown for Google Drive */}
{connector.connector_type !== "GOOGLE_DRIVE_CONNECTOR" && (
<PeriodicSyncConfig
enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange}
/>
)}
<PeriodicSyncConfig
enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange}
/>
</>
)}
@ -181,9 +216,12 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
<Info className="size-4" />
</div>
<div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Re-indexing runs in the background</p>
<p className="font-medium text-xs sm:text-sm">
Re-indexing runs in the background
</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
You can continue using SurfSense while we sync your data. Check the Active tab to see progress.
You can continue using SurfSense while we sync your data. Check the Active tab
to see progress.
</p>
</div>
</div>
@ -201,49 +239,56 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</div>
{/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted border-t border-border">
<div className="flex-shrink-0 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3 sm:gap-0 px-6 sm:px-12 py-6 sm:py-6 bg-muted border-t border-border">
{showDisconnectConfirm ? (
<div className="flex items-center gap-3">
<span className="text-xs sm:text-sm text-muted-foreground">Are you sure?</span>
<Button
variant="destructive"
size="sm"
onClick={handleDisconnectConfirm}
disabled={isDisconnecting}
className="text-xs sm:text-sm"
>
{isDisconnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Disconnecting...
</>
) : (
"Confirm Disconnect"
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDisconnectCancel}
disabled={isDisconnecting}
className="text-xs sm:text-sm"
>
Cancel
</Button>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 flex-1 sm:flex-initial">
<span className="text-xs sm:text-sm text-muted-foreground sm:whitespace-nowrap">
Are you sure?
</span>
<div className="flex items-center gap-2 sm:gap-3">
<Button
variant="destructive"
size="sm"
onClick={handleDisconnectConfirm}
disabled={isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2"
>
{isDisconnecting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Disconnecting...
</>
) : (
"Confirm Disconnect"
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleDisconnectCancel}
disabled={isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-10 sm:h-auto py-2 sm:py-2"
>
Cancel
</Button>
</div>
</div>
) : (
<Button
variant="destructive"
size="sm"
onClick={handleDisconnectClick}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm"
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
>
<Trash2 className="mr-2 h-4 w-4" />
Disconnect
</Button>
)}
<Button onClick={onSave} disabled={isSaving || isDisconnecting} className="text-xs sm:text-sm">
<Button
onClick={onSave}
disabled={isSaving || isDisconnecting}
className="text-xs sm:text-sm flex-1 sm:flex-initial h-12 sm:h-auto py-3 sm:py-2"
>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -257,4 +302,3 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
</div>
);
};

View file

@ -45,7 +45,7 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
}) => {
// Get connector-specific config component
const ConnectorConfigComponent = useMemo(
() => connector ? getConnectorConfigComponent(connector.connector_type) : null,
() => (connector ? getConnectorConfigComponent(connector.connector_type) : null),
[connector]
);
const [isScrolled, setIsScrolled] = useState(false);
@ -54,12 +54,13 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
const checkScrollState = useCallback(() => {
if (!scrollContainerRef.current) return;
const target = scrollContainerRef.current;
const scrolled = target.scrollTop > 0;
const hasMore = target.scrollHeight > target.clientHeight &&
const hasMore =
target.scrollHeight > target.clientHeight &&
target.scrollTop + target.clientHeight < target.scrollHeight - 10;
setIsScrolled(scrolled);
setHasMoreContent(hasMore);
}, []);
@ -74,11 +75,11 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
const resizeObserver = new ResizeObserver(() => {
checkScrollState();
});
if (scrollContainerRef.current) {
resizeObserver.observe(scrollContainerRef.current);
}
return () => {
resizeObserver.disconnect();
};
@ -87,10 +88,12 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
return (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
{/* Fixed Header */}
<div className={cn(
"flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10 transition-shadow duration-200 relative z-10",
isScrolled && "shadow-sm"
)}>
<div
className={cn(
"flex-shrink-0 px-6 sm:px-12 pt-8 sm:pt-10 transition-shadow duration-200 relative z-10",
isScrolled && "shadow-sm"
)}
>
{/* Back button */}
<button
type="button"
@ -119,39 +122,40 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
{/* Scrollable Content */}
<div className="flex-1 min-h-0 relative overflow-hidden">
<div
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto px-6 sm:px-12"
className="h-full overflow-y-auto px-6 sm:px-12"
onScroll={handleScroll}
>
<div className="space-y-6 pb-6 pt-2">
{/* Connector-specific configuration */}
{ConnectorConfigComponent && connector && (
<ConnectorConfigComponent
connector={connector}
onConfigChange={onConfigChange}
/>
<ConnectorConfigComponent connector={connector} onConfigChange={onConfigChange} />
)}
{/* Date range selector and periodic sync - only shown for indexable connectors */}
{connector?.is_indexable && (
<>
{/* Date range selector - not shown for Google Drive (uses folder selection) or Webcrawler (uses config) */}
{config.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && config.connectorType !== "WEBCRAWLER_CONNECTOR" && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
{config.connectorType !== "GOOGLE_DRIVE_CONNECTOR" &&
config.connectorType !== "WEBCRAWLER_CONNECTOR" && (
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={onStartDateChange}
onEndDateChange={onEndDateChange}
/>
)}
{/* Periodic sync - not shown for Google Drive */}
{config.connectorType !== "GOOGLE_DRIVE_CONNECTOR" && (
<PeriodicSyncConfig
enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange}
/>
)}
<PeriodicSyncConfig
enabled={periodicEnabled}
frequencyMinutes={frequencyMinutes}
onEnabledChange={onPeriodicEnabledChange}
onFrequencyChange={onFrequencyChange}
/>
</>
)}
@ -164,7 +168,8 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
<div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Indexing runs in the background</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
You can continue using SurfSense while we sync your data. Check the Active tab to see progress.
You can continue using SurfSense while we sync your data. Check the Active tab
to see progress.
</p>
</div>
</div>
@ -183,10 +188,19 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
{/* Fixed Footer - Action buttons */}
<div className="flex-shrink-0 flex items-center justify-between px-6 sm:px-12 py-6 bg-muted">
<Button variant="ghost" onClick={onSkip} disabled={isStartingIndexing} className="text-xs sm:text-sm">
<Button
variant="ghost"
onClick={onSkip}
disabled={isStartingIndexing}
className="text-xs sm:text-sm"
>
Skip for now
</Button>
<Button onClick={onStartIndexing} disabled={isStartingIndexing} className="text-xs sm:text-sm">
<Button
onClick={onStartIndexing}
disabled={isStartingIndexing}
className="text-xs sm:text-sm"
>
{isStartingIndexing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
@ -200,4 +214,3 @@ export const IndexingConfigurationView: FC<IndexingConfigurationViewProps> = ({
</div>
);
};

View file

@ -150,4 +150,3 @@ export const OTHER_CONNECTORS = [
// Re-export IndexingConfigState from schemas for backward compatibility
export type { IndexingConfigState } from "./connector-popup.schemas";

View file

@ -80,7 +80,7 @@ export function parseConnectorPopupQueryParams(
params: URLSearchParams | Record<string, string | null>
): ConnectorPopupQueryParams {
const obj: Record<string, string | undefined> = {};
if (params instanceof URLSearchParams) {
params.forEach((value, key) => {
obj[key] = value || undefined;
@ -90,7 +90,7 @@ export function parseConnectorPopupQueryParams(
obj[key] = value || undefined;
});
}
return connectorPopupQueryParamsSchema.parse(obj);
}
@ -107,4 +107,3 @@ export function parseOAuthAuthResponse(data: unknown): OAuthAuthResponse {
export function validateIndexingConfigState(data: unknown): IndexingConfigState {
return indexingConfigStateSchema.parse(data);
}

View file

@ -35,4 +35,3 @@ export type {
// Hooks
export { useConnectorDialog } from "./hooks/use-connector-dialog";

View file

@ -1,19 +1,20 @@
"use client";
import { format } from "date-fns";
import { Cable, FileText, Loader2 } from "lucide-react";
import { ArrowRight, Cable, Loader2 } from "lucide-react";
import type { FC } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types";
import { cn } from "@/lib/utils";
import {
TabsContent,
} from "@/components/ui/tabs";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import { TabsContent } from "@/components/ui/tabs";
interface ActiveConnectorsTabProps {
searchQuery: string;
hasSources: boolean;
totalSourceCount: number;
activeDocumentTypes: Array<[string, number]>;
@ -26,107 +27,186 @@ interface ActiveConnectorsTabProps {
}
export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
searchQuery,
hasSources,
activeDocumentTypes,
connectors,
indexingConnectorIds,
logsSummary,
searchSpaceId,
onTabChange,
onManage,
}) => {
const router = useRouter();
const handleViewAllDocuments = () => {
router.push(`/dashboard/${searchSpaceId}/documents`);
};
// Convert activeDocumentTypes array to Record for utility function
const documentTypeCounts = activeDocumentTypes.reduce(
(acc, [docType, count]) => {
acc[docType] = count;
return acc;
},
{} as Record<string, number>
);
// Format document count (e.g., "1.2k docs", "500 docs", "1.5M docs")
const formatDocumentCount = (count: number | undefined): string => {
if (count === undefined || count === 0) return "0 docs";
if (count < 1000) return `${count} docs`;
if (count < 1000000) {
const k = (count / 1000).toFixed(1);
return `${k.replace(/\.0$/, "")}k docs`;
}
const m = (count / 1000000).toFixed(1);
return `${m.replace(/\.0$/, "")}M docs`;
};
// Document types that should be shown as cards (not from connectors)
// These are: EXTENSION (browser extension), FILE (uploaded files), NOTE (editor notes),
// YOUTUBE_VIDEO (YouTube videos), and CRAWLED_URL (web pages - shown separately even though it can come from WEBCRAWLER_CONNECTOR)
const standaloneDocumentTypes = ["EXTENSION", "FILE", "NOTE", "YOUTUBE_VIDEO", "CRAWLED_URL"];
// Filter to only show standalone document types that have documents (count > 0)
const standaloneDocuments = activeDocumentTypes
.filter(([docType, count]) => standaloneDocumentTypes.includes(docType) && count > 0)
.map(([docType, count]) => ({
type: docType,
count,
label: getDocumentTypeLabel(docType),
}))
.filter((doc) => {
if (!searchQuery) return true;
return doc.label.toLowerCase().includes(searchQuery.toLowerCase());
});
// Filter connectors based on search query
const filteredConnectors = connectors.filter((connector) => {
if (!searchQuery) return true;
const searchLower = searchQuery.toLowerCase();
return (
connector.name.toLowerCase().includes(searchLower) ||
connector.connector_type.toLowerCase().includes(searchLower)
);
});
return (
<TabsContent value="active" className="m-0">
{hasSources ? (
<div className="space-y-6">
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">
Currently Active
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{activeDocumentTypes.map(([docType, count]) => (
<div
key={docType}
className="flex items-center gap-4 p-4 rounded-xl bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 border border-border transition-all"
>
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-slate-400/5 dark:bg-white/5 border border-slate-400/5 dark:border-white/5">
{getConnectorIcon(docType, "size-6")}
</div>
<div>
<p className="text-[14px] font-semibold leading-tight">
{getDocumentTypeLabel(docType)}
</p>
<p className="text-[11px] text-muted-foreground mt-1 inline-flex items-center gap-1.5">
<FileText className="size-3 flex-shrink-0" />
<span className="whitespace-nowrap">
{(count as number).toLocaleString()} document{count !== 1 ? "s" : ""}
</span>
</p>
</div>
{/* Active Connectors Section */}
{filteredConnectors.length > 0 && (
<div className="space-y-4">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-muted-foreground">Active Connectors</h3>
</div>
))}
{connectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find(
(task: LogActiveTask) => task.connector_id === connector.id
);
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find(
(task: LogActiveTask) => task.connector_id === connector.id
);
const documentCount = getDocumentCountForConnector(
connector.connector_type,
documentTypeCounts
);
return (
<div
key={`connector-${connector.id}`}
className={cn(
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
isIndexing
? "bg-primary/5 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
)}
>
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg border",
isIndexing
? "bg-primary/10 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
)}
>
{getConnectorIcon(connector.connector_type, "size-6")}
</div>
<div className="flex-1 min-w-0">
<p className="text-[14px] font-semibold leading-tight truncate">
{connector.name}
</p>
{isIndexing ? (
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
<Loader2 className="size-3 animate-spin" />
Indexing...
{activeTask?.message && (
<span className="text-muted-foreground truncate max-w-[150px]">
{activeTask.message}
</span>
return (
<div
key={`connector-${connector.id}`}
className={cn(
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
isIndexing
? "bg-primary/5 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
)}
>
<div
className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg border",
isIndexing
? "bg-primary/10 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5"
)}
</p>
) : (
<p className="text-[11px] text-muted-foreground mt-1">
{connector.last_indexed_at
? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}`
: "Never indexed"}
</p>
)}
</div>
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80"
onClick={onManage ? () => onManage(connector) : undefined}
disabled={isIndexing}
>
{getConnectorIcon(connector.connector_type, "size-6")}
</div>
<div className="flex-1 min-w-0">
<p className="text-[14px] font-semibold leading-tight truncate">
{connector.name}
</p>
{isIndexing ? (
<p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
<Loader2 className="size-3 animate-spin" />
Indexing...
{activeTask?.message && (
<span className="text-muted-foreground truncate max-w-[150px]">
{activeTask.message}
</span>
)}
</p>
) : (
<p className="text-[11px] text-muted-foreground mt-1">
{connector.last_indexed_at
? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}`
: "Never indexed"}
</p>
)}
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
</div>
<Button
variant="secondary"
size="sm"
className="h-8 text-[11px] px-3 rounded-lg font-medium bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80"
onClick={onManage ? () => onManage(connector) : undefined}
disabled={isIndexing}
>
{isIndexing ? "Syncing..." : "Manage"}
</Button>
</div>
);
})}
</div>
</div>
)}
{/* Standalone Documents Section */}
{standaloneDocuments.length > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-muted-foreground">Documents</h3>
<Button
variant="ghost"
size="sm"
onClick={handleViewAllDocuments}
className="h-7 text-xs text-muted-foreground hover:text-foreground gap-1.5"
>
View all documents
<ArrowRight className="size-3" />
</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
{standaloneDocuments.map((doc) => (
<div
key={doc.type}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full border border-border bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10 transition-all"
>
{isIndexing ? "Syncing..." : "Manage"}
</Button>
</div>
);
})}
</div>
<div className="flex items-center justify-center">
{getConnectorIcon(doc.type, "size-3.5")}
</div>
<span className="text-[12px] font-medium">{doc.label}</span>
<span className="text-[11px] text-muted-foreground">
{formatDocumentCount(doc.count)}
</span>
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="flex flex-col items-center justify-center py-20 text-center">
@ -149,4 +229,3 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</TabsContent>
);
};

View file

@ -1,11 +1,10 @@
"use client";
import { useRouter } from "next/navigation";
import { type FC } from "react";
import type { FC } from "react";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogActiveTask, LogSummary } from "@/contracts/types/log.types";
import { OAUTH_CONNECTORS, CRAWLERS, OTHER_CONNECTORS } from "../constants/connector-constants";
import { ConnectorCard } from "../components/connector-card";
import { CRAWLERS, OAUTH_CONNECTORS, OTHER_CONNECTORS } from "../constants/connector-constants";
import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
interface AllConnectorsTabProps {
@ -39,8 +38,6 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
onCreateYouTubeCrawler,
onManage,
}) => {
const router = useRouter();
// Helper to find active task for a connector
const getActiveTaskForConnector = (connectorId: number): LogActiveTask | undefined => {
if (!logsSummary?.active_tasks) return undefined;
@ -74,98 +71,46 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
{filteredOAuth.length > 0 && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">
Quick Connect
</h3>
<h3 className="text-sm font-semibold text-muted-foreground">Quick Connect</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredOAuth.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={() => onConnectOAuth(connector)}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
</div>
</section>
)}
{/* Content Sources */}
{filteredCrawlers.length > 0 && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">
Content Sources
</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredCrawlers.map((crawler) => {
const isYouTube = crawler.id === "youtube-crawler";
const isWebcrawler = crawler.id === "webcrawler-connector";
// For crawlers that are actual connectors, check connection status
const isConnected = crawler.connectorType
? connectedTypes.has(crawler.connectorType)
: false;
const isConnecting = connectingId === crawler.id;
{filteredOAuth.map((connector) => {
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && crawler.connectorType && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === crawler.connectorType)
: undefined;
const actualConnector =
isConnected && allConnectors
? allConnectors.find(
(c: SearchSourceConnector) => c.connector_type === connector.connectorType
)
: undefined;
const documentCount = crawler.connectorType
? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts)
: undefined;
const documentCount = getDocumentCountForConnector(
connector.connectorType,
documentTypeCounts
);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
const handleConnect = isYouTube && onCreateYouTubeCrawler
? onCreateYouTubeCrawler
: isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: crawler.connectorType && onConnectNonOAuth
? () => onConnectNonOAuth(crawler.connectorType!)
: crawler.connectorType
? () => router.push(`/dashboard/${searchSpaceId}/connectors/add/${crawler.id}`)
: () => {}; // Fallback for non-connector crawlers
const activeTask = actualConnector
? getActiveTaskForConnector(actualConnector.id)
: undefined;
return (
<ConnectorCard
key={crawler.id}
id={crawler.id}
title={crawler.title}
description={crawler.description}
connectorType={crawler.connectorType || undefined}
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
onConnect={() => onConnectOAuth(connector)}
onManage={
actualConnector && onManage ? () => onManage(actualConnector) : undefined
}
/>
);
})}
@ -177,70 +122,127 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
{filteredOther.length > 0 && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">
More Integrations
</h3>
<h3 className="text-sm font-semibold text-muted-foreground">More Integrations</h3>
</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";
const isCircleback = connector.id === "circleback-connector";
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
// Find the actual connector object if connected
const actualConnector = isConnected && allConnectors
? allConnectors.find((c: SearchSourceConnector) => c.connector_type === connector.connectorType)
: undefined;
const isConnected = connectedTypes.has(connector.connectorType);
const isConnecting = connectingId === connector.id;
const documentCount = getDocumentCountForConnector(connector.connectorType, documentTypeCounts);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector ? getActiveTaskForConnector(actualConnector.id) : undefined;
// Find the actual connector object if connected
const actualConnector =
isConnected && allConnectors
? allConnectors.find(
(c: SearchSourceConnector) => c.connector_type === connector.connectorType
)
: undefined;
const handleConnect = isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: (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}`);
const documentCount = getDocumentCountForConnector(
connector.connectorType,
documentTypeCounts
);
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector
? getActiveTaskForConnector(actualConnector.id)
: undefined;
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
/>
);
})}
const handleConnect = onConnectNonOAuth
? () => onConnectNonOAuth(connector.connectorType)
: () => {}; // Fallback - connector popup should handle all connector types
return (
<ConnectorCard
key={connector.id}
id={connector.id}
title={connector.title}
description={connector.description}
connectorType={connector.connectorType}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
onManage={
actualConnector && onManage ? () => onManage(actualConnector) : undefined
}
/>
);
})}
</div>
</section>
)}
{/* Content Sources */}
{filteredCrawlers.length > 0 && (
<section>
<div className="flex items-center gap-2 mb-4">
<h3 className="text-sm font-semibold text-muted-foreground">Content Sources</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{filteredCrawlers.map((crawler) => {
const isYouTube = crawler.id === "youtube-crawler";
const isWebcrawler = crawler.id === "webcrawler-connector";
// For crawlers that are actual connectors, check connection status
const isConnected = crawler.connectorType
? connectedTypes.has(crawler.connectorType)
: false;
const isConnecting = connectingId === crawler.id;
// Find the actual connector object if connected
const actualConnector =
isConnected && crawler.connectorType && allConnectors
? allConnectors.find(
(c: SearchSourceConnector) => c.connector_type === crawler.connectorType
)
: undefined;
const documentCount = crawler.connectorType
? getDocumentCountForConnector(crawler.connectorType, documentTypeCounts)
: undefined;
const isIndexing = actualConnector && indexingConnectorIds?.has(actualConnector.id);
const activeTask = actualConnector
? getActiveTaskForConnector(actualConnector.id)
: undefined;
const handleConnect =
isYouTube && onCreateYouTubeCrawler
? onCreateYouTubeCrawler
: isWebcrawler && onCreateWebcrawler
? onCreateWebcrawler
: crawler.connectorType && onConnectNonOAuth
? () => {
if (crawler.connectorType) {
onConnectNonOAuth(crawler.connectorType);
}
}
: () => {}; // Fallback for non-connector crawlers
return (
<ConnectorCard
key={crawler.id}
id={crawler.id}
title={crawler.title}
description={crawler.description}
connectorType={crawler.connectorType || undefined}
isConnected={isConnected}
isConnecting={isConnecting}
documentCount={documentCount}
lastIndexedAt={actualConnector?.last_indexed_at}
isIndexing={isIndexing}
activeTask={activeTask}
onConnect={handleConnect}
onManage={
actualConnector && onManage ? () => onManage(actualConnector) : undefined
}
/>
);
})}
</div>
</section>
)}
</div>
);
};

View file

@ -2,7 +2,7 @@
/**
* Maps SearchSourceConnectorType to DocumentType for fetching document counts
*
*
* Note: Some connectors don't have a direct 1:1 mapping to document types:
* - Search API connectors (TAVILY_API, SEARXNG_API, etc.) don't index documents
* - WEBCRAWLER_CONNECTOR maps to CRAWLED_URL document type
@ -35,9 +35,7 @@ export const CONNECTOR_TO_DOCUMENT_TYPE: Record<string, string> = {
* Get the document type for a given connector type
* Returns undefined if the connector doesn't index documents (e.g., search APIs)
*/
export function getDocumentTypeForConnector(
connectorType: string
): string | undefined {
export function getDocumentTypeForConnector(connectorType: string): string | undefined {
return CONNECTOR_TO_DOCUMENT_TYPE[connectorType];
}
@ -62,4 +60,3 @@ export function getDocumentCountForConnector(
export function isIndexableConnectorType(connectorType: string): boolean {
return connectorType in CONNECTOR_TO_DOCUMENT_TYPE;
}

View file

@ -22,10 +22,7 @@ interface YouTubeCrawlerViewProps {
onBack: () => void;
}
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
searchSpaceId,
onBack,
}) => {
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId, onBack }) => {
const t = useTranslations("add_youtube");
const router = useRouter();
const [videoTags, setVideoTags] = useState<TagType[]>([]);
@ -133,12 +130,8 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
{getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")}
</div>
<div>
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">
{t("title")}
</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
{t("subtitle")}
</p>
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight">{t("title")}</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">{t("subtitle")}</p>
</div>
</div>
</div>
@ -159,7 +152,8 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
styleClasses={{
inlineTagsContainer:
"border border-slate-400/20 rounded-lg bg-muted/50 shadow-sm shadow-black/5 transition-shadow focus-within:border-slate-400/40 focus-within:outline-none focus-within:ring-[3px] focus-within:ring-ring/20 p-1 gap-1",
input: "w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent",
input:
"w-full min-w-[80px] focus-visible:outline-none shadow-none px-2 h-7 text-foreground/90 placeholder:text-muted-foreground bg-transparent",
tag: {
body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex",
closeButton:
@ -172,11 +166,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
<p className="text-xs text-muted-foreground mt-1">{t("hint")}</p>
</div>
{error && (
<div className="text-sm text-red-500 mt-2">
{error}
</div>
)}
{error && <div className="text-sm text-red-500 mt-2">{error}</div>}
<div className="bg-muted/50 rounded-lg p-4 text-sm">
<h4 className="font-medium mb-2">{t("tips_title")}</h4>
@ -244,4 +234,3 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
</div>
);
};

View file

@ -0,0 +1,111 @@
"use client";
import { useAtomValue } from "jotai";
import {
type FC,
createContext,
useContext,
useState,
useCallback,
useRef,
type ReactNode,
} from "react";
import { useRouter } from "next/navigation";
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
// Context for opening the dialog from anywhere
interface DocumentUploadDialogContextType {
openDialog: () => void;
closeDialog: () => void;
}
const DocumentUploadDialogContext = createContext<DocumentUploadDialogContextType | null>(null);
export const useDocumentUploadDialog = () => {
const context = useContext(DocumentUploadDialogContext);
if (!context) {
throw new Error("useDocumentUploadDialog must be used within DocumentUploadDialogProvider");
}
return context;
};
// Provider component
export const DocumentUploadDialogProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
const isClosingRef = useRef(false);
const openDialog = useCallback(() => {
// Prevent opening if we just closed (debounce)
if (isClosingRef.current) {
return;
}
setIsOpen(true);
}, []);
const closeDialog = useCallback(() => {
isClosingRef.current = true;
setIsOpen(false);
// Reset the flag after a short delay to allow for file picker to close
setTimeout(() => {
isClosingRef.current = false;
}, 300);
}, []);
const handleOpenChange = useCallback(
(open: boolean) => {
if (!open) {
// Only close if not already in closing state
if (!isClosingRef.current) {
closeDialog();
}
} else {
// Only open if not in the middle of closing
if (!isClosingRef.current) {
setIsOpen(true);
}
}
},
[closeDialog]
);
return (
<DocumentUploadDialogContext.Provider value={{ openDialog, closeDialog }}>
{children}
<DocumentUploadPopupContent isOpen={isOpen} onOpenChange={handleOpenChange} />
</DocumentUploadDialogContext.Provider>
);
};
// Internal component that renders the dialog
const DocumentUploadPopupContent: FC<{
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ isOpen, onOpenChange }) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const router = useRouter();
if (!searchSpaceId) return null;
const handleSuccess = () => {
onOpenChange(false);
router.push(`/dashboard/${searchSpaceId}/documents`);
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl w-[95vw] sm:w-full h-[calc(100vh-2rem)] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden border border-border bg-muted text-foreground [&>button]:right-3 sm:[&>button]:right-12 [&>button]:top-4 sm:[&>button]:top-10 [&>button]:opacity-80 hover:[&>button]:opacity-100 [&>button]:z-[100] [&>button_svg]:size-4 sm:[&>button_svg]:size-5">
<div className="flex-1 min-h-0 relative overflow-hidden">
<div className="h-full overflow-y-auto">
<div className="px-3 sm:px-12 pt-12 sm:pt-24 pb-6 sm:pb-16">
<DocumentUploadTab searchSpaceId={searchSpaceId} onSuccess={handleSuccess} />
</div>
</div>
{/* Bottom fade shadow */}
<div className="absolute bottom-0 left-0 right-0 h-2 sm:h-7 bg-gradient-to-t from-muted via-muted/80 to-transparent pointer-events-none z-10" />
</div>
</DialogContent>
</Dialog>
);
};

View file

@ -24,4 +24,3 @@ export const EditComposer: FC = () => {
</MessagePrimitive.Root>
);
};

View file

@ -204,4 +204,3 @@ export const ThinkingStepsScrollHandler: FC = () => {
return null; // This component doesn't render anything
};

View file

@ -16,4 +16,3 @@ export const ThreadScrollToBottom: FC = () => {
</ThreadPrimitive.ScrollToBottom>
);
};

View file

@ -69,4 +69,3 @@ export const ThreadWelcome: FC = () => {
</div>
);
};

View file

@ -26,15 +26,7 @@ import {
SquareIcon,
} from "lucide-react";
import { useParams } from "next/navigation";
import {
type FC,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { type FC, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import {
mentionedDocumentIdsAtom,

View file

@ -70,4 +70,3 @@ const UserActionBar: FC = () => {
</ActionBarPrimitive.Root>
);
};

View file

@ -223,9 +223,17 @@ export function GoogleDriveFolderTree({
const childFiles = children?.filter((c) => !c.isFolder) || [];
const indentSize = 0.75; // Smaller indent for mobile
return (
<div key={item.id} className="w-full sm:ml-[calc(var(--level)*1.25rem)]" style={{ marginLeft: `${level * indentSize}rem`, '--level': level } as React.CSSProperties & { '--level'?: number }}>
<div
key={item.id}
className="w-full sm:ml-[calc(var(--level)*1.25rem)]"
style={
{ marginLeft: `${level * indentSize}rem`, "--level": level } as React.CSSProperties & {
"--level"?: number;
}
}
>
<div
className={cn(
"flex items-center group gap-1 sm:gap-2 h-auto py-1 sm:py-2 px-1 sm:px-2 rounded-md",
@ -265,17 +273,9 @@ export function GoogleDriveFolderTree({
toggleFileSelection(item.id, item.name);
}
}}
className="shrink-0 z-20 group-hover:border-white group-hover:border"
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20"
onClick={(e) => e.stopPropagation()}
/>
{isFolder && (
<Checkbox
checked={isSelected}
onCheckedChange={() => toggleFolderSelection(item.id, item.name)}
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4"
onClick={(e) => e.stopPropagation()}
/>
)}
<div className="shrink-0">
{isFolder ? (
@ -310,7 +310,9 @@ export function GoogleDriveFolderTree({
{childFiles.map((child) => renderItem(child, level + 1))}
{children.length === 0 && (
<div className="text-[10px] sm:text-xs text-muted-foreground py-1 sm:py-2 pl-1 sm:pl-2">Empty folder</div>
<div className="text-[10px] sm:text-xs text-muted-foreground py-1 sm:py-2 pl-1 sm:pl-2">
Empty folder
</div>
)}
</div>
)}
@ -319,15 +321,15 @@ export function GoogleDriveFolderTree({
};
return (
<div className="border rounded-md w-full overflow-hidden">
<div className="border border-slate-400/20 dark:border-white/20 rounded-md w-full overflow-hidden">
<ScrollArea className="h-[300px] sm:h-[450px] w-full">
<div className="p-1 sm:p-2 pr-2 sm:pr-4 w-full overflow-x-hidden">
<div className="mb-1 sm:mb-2 pb-1 sm:pb-2 border-b">
<div className="mb-1 sm:mb-2 pb-1 sm:pb-2 border-b border-slate-400/20 dark:border-white/20">
<div className="flex items-center gap-1 sm:gap-2 h-auto py-1 sm:py-2 px-1 sm:px-2 rounded-md hover:bg-accent cursor-pointer">
<Checkbox
checked={isFolderSelected("root")}
onCheckedChange={() => toggleFolderSelection("root", "My Drive")}
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4"
className="shrink-0 h-3.5 w-3.5 sm:h-4 sm:w-4 border-slate-400/20 dark:border-white/20"
/>
<HardDrive className="h-3 w-3 sm:h-4 sm:w-4 text-primary shrink-0" />
<button

View file

@ -97,7 +97,6 @@ export function DashboardBreadcrumb() {
const sectionLabels: Record<string, string> = {
"new-chat": t("chat") || "Chat",
documents: t("documents"),
connectors: t("connectors"),
logs: t("logs"),
settings: t("settings"),
editor: t("editor"),
@ -156,62 +155,12 @@ export function DashboardBreadcrumb() {
return breadcrumbs;
}
// Handle connector sub-sections
if (section === "connectors") {
// Handle specific connector types
if (subSection === "add" && segments[4]) {
const connectorType = segments[4];
const connectorLabels: Record<string, string> = {
"github-connector": "GitHub",
"jira-connector": "Jira",
"confluence-connector": "Confluence",
"bookstack-connector": "BookStack",
"discord-connector": "Discord",
"linear-connector": "Linear",
"clickup-connector": "ClickUp",
"slack-connector": "Slack",
"notion-connector": "Notion",
"tavily-api": "Tavily API",
"linkup-api": "LinkUp API",
"luma-connector": "Luma",
"elasticsearch-connector": "Elasticsearch",
};
const connectorLabel = connectorLabels[connectorType] || connectorType;
breadcrumbs.push({
label: "Connectors",
href: `/dashboard/${segments[1]}/connectors`,
});
breadcrumbs.push({
label: "Add Connector",
href: `/dashboard/${segments[1]}/connectors/add`,
});
breadcrumbs.push({ label: connectorLabel });
return breadcrumbs;
}
const connectorLabels: Record<string, string> = {
add: t("add_connector"),
manage: t("manage_connectors"),
};
const connectorLabel = connectorLabels[subSection] || subSection;
breadcrumbs.push({
label: t("connectors"),
href: `/dashboard/${segments[1]}/connectors`,
});
breadcrumbs.push({ label: connectorLabel });
return breadcrumbs;
}
// Handle other sub-sections
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
const subSectionLabels: Record<string, string> = {
upload: t("upload_documents"),
youtube: t("add_youtube"),
webpage: t("add_webpages"),
add: t("add_connector"),
edit: t("edit_connector"),
manage: t("manage"),
};

View file

@ -3,6 +3,7 @@
import { useAtomValue } from "jotai";
import {
AlertCircle,
ArrowLeftRight,
BookOpen,
Cable,
ChevronsUpDown,
@ -12,7 +13,9 @@ import {
FileText,
Info,
LogOut,
Logs,
type LucideIcon,
MessageCircle,
MessageCircleMore,
MoonIcon,
Podcast,
@ -148,6 +151,8 @@ export const iconMap: Record<string, LucideIcon> = {
Podcast,
Users,
RefreshCw,
MessageCircle,
Logs,
};
const defaultData = {
@ -291,7 +296,6 @@ export const AppSidebar = memo(function AppSidebar({
const { theme, setTheme } = useTheme();
const { data: user, isPending: isLoadingUser } = useAtomValue(currentUserAtom);
const [isClient, setIsClient] = useState(false);
const [isSourcesExpanded, setIsSourcesExpanded] = useState(false);
useEffect(() => {
setIsClient(true);
@ -414,7 +418,7 @@ export const AppSidebar = memo(function AppSidebar({
</>
)}
<DropdownMenuItem onClick={() => router.push("/dashboard")}>
<SquareLibrary className="mr-2 h-4 w-4" />
<ArrowLeftRight className="mr-2 h-4 w-4" />
Switch workspace
</DropdownMenuItem>
</DropdownMenuGroup>
@ -443,19 +447,14 @@ export const AppSidebar = memo(function AppSidebar({
</SidebarHeader>
<SidebarContent className="gap-1">
<NavMain items={processedNavMain} onSourcesExpandedChange={setIsSourcesExpanded} />
<NavMain items={processedNavMain} />
<NavChats
chats={processedRecentChats}
searchSpaceId={searchSpaceId}
isSourcesExpanded={isSourcesExpanded}
/>
<NavChats chats={processedRecentChats} searchSpaceId={searchSpaceId} />
<NavNotes
notes={processedRecentNotes}
onAddNote={onAddNote}
searchSpaceId={searchSpaceId}
isSourcesExpanded={isSourcesExpanded}
/>
</SidebarContent>
<SidebarFooter>

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
@ -30,7 +30,6 @@ import {
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { AllChatsSidebar } from "./all-chats-sidebar";
@ -53,7 +52,6 @@ interface NavChatsProps {
chats: ChatItem[];
defaultOpen?: boolean;
searchSpaceId?: string;
isSourcesExpanded?: boolean;
}
// Map of icon names to their components
@ -64,28 +62,15 @@ const actionIconMap: Record<string, LucideIcon> = {
RefreshCw,
};
export function NavChats({
chats,
defaultOpen = true,
searchSpaceId,
isSourcesExpanded = false,
}: NavChatsProps) {
export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const pathname = usePathname();
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false);
// Auto-collapse on smaller screens when Sources is expanded
useEffect(() => {
if (isSourcesExpanded && isMobile) {
setIsOpen(false);
}
}, [isSourcesExpanded, isMobile]);
// Handle chat deletion with loading state
const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => {
setIsDeleting(chatId);

View file

@ -31,10 +31,9 @@ interface NavItem {
interface NavMainProps {
items: NavItem[];
onSourcesExpandedChange?: (expanded: boolean) => void;
}
export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) {
export function NavMain({ items }: NavMainProps) {
const t = useTranslations("nav_menu");
const pathname = usePathname();
@ -100,16 +99,9 @@ export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) {
});
// Handle collapsible state change
const handleOpenChange = useCallback(
(title: string, isOpen: boolean) => {
setExpandedItems((prev) => ({ ...prev, [title]: isOpen }));
// Notify parent when Sources is expanded/collapsed
if (title === "Sources" && onSourcesExpandedChange) {
onSourcesExpandedChange(isOpen);
}
},
[onSourcesExpandedChange]
);
const handleOpenChange = useCallback((title: string, isOpen: boolean) => {
setExpandedItems((prev) => ({ ...prev, [title]: isOpen }));
}, []);
return (
<SidebarGroup>

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
@ -31,7 +31,6 @@ import {
useSidebar,
} from "@/components/ui/sidebar";
import { useLogsSummary } from "@/hooks/use-logs";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils";
import { AllNotesSidebar } from "./all-notes-sidebar";
@ -55,7 +54,6 @@ interface NavNotesProps {
onAddNote?: () => void;
defaultOpen?: boolean;
searchSpaceId?: string;
isSourcesExpanded?: boolean;
}
// Map of icon names to their components
@ -65,17 +63,10 @@ const actionIconMap: Record<string, LucideIcon> = {
MoreHorizontal,
};
export function NavNotes({
notes,
onAddNote,
defaultOpen = true,
searchSpaceId,
isSourcesExpanded = false,
}: NavNotesProps) {
export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const pathname = usePathname();
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar();
const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen);
@ -98,13 +89,6 @@ export function NavNotes({
);
}, [summary?.active_tasks]);
// Auto-collapse on smaller screens when Sources is expanded
useEffect(() => {
if (isSourcesExpanded && isMobile) {
setIsOpen(false);
}
}, [isSourcesExpanded, isMobile]);
// Handle note deletion with loading state
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
setIsDeleting(noteId);

View file

@ -5,10 +5,16 @@ import { CheckCircle2, FileType, Info, Loader2, Tag, Upload, X } from "lucide-re
import { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState, useRef } from "react";
import { useDropzone } from "react-dropzone";
import { toast } from "sonner";
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@ -24,115 +30,106 @@ import { GridPattern } from "./GridPattern";
interface DocumentUploadTabProps {
searchSpaceId: string;
onSuccess?: () => void;
}
export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
const audioFileTypes = {
"audio/mpeg": [".mp3", ".mpeg", ".mpga"],
"audio/mp4": [".mp4", ".m4a"],
"audio/wav": [".wav"],
"audio/webm": [".webm"],
"text/markdown": [".md", ".markdown"],
"text/plain": [".txt"],
};
const commonTypes = {
"application/pdf": [".pdf"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
"text/html": [".html", ".htm"],
"text/csv": [".csv"],
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
"image/bmp": [".bmp"],
"image/webp": [".webp"],
"image/tiff": [".tiff"],
};
const FILE_TYPE_CONFIG: Record<string, Record<string, string[]>> = {
LLAMACLOUD: {
...commonTypes,
"application/msword": [".doc"],
"application/vnd.ms-word.document.macroEnabled.12": [".docm"],
"application/msword-template": [".dot"],
"application/vnd.ms-word.template.macroEnabled.12": [".dotm"],
"application/vnd.ms-powerpoint": [".ppt"],
"application/vnd.ms-powerpoint.template.macroEnabled.12": [".pptm"],
"application/vnd.ms-powerpoint.template": [".pot"],
"application/vnd.openxmlformats-officedocument.presentationml.template": [".potx"],
"application/vnd.ms-excel": [".xls"],
"application/vnd.ms-excel.sheet.macroEnabled.12": [".xlsm"],
"application/vnd.ms-excel.sheet.binary.macroEnabled.12": [".xlsb"],
"application/vnd.ms-excel.workspace": [".xlw"],
"application/rtf": [".rtf"],
"application/xml": [".xml"],
"application/epub+zip": [".epub"],
"text/tab-separated-values": [".tsv"],
"text/html": [".html", ".htm", ".web"],
"image/gif": [".gif"],
"image/svg+xml": [".svg"],
...audioFileTypes,
},
DOCLING: {
...commonTypes,
"text/asciidoc": [".adoc", ".asciidoc"],
"text/html": [".html", ".htm", ".xhtml"],
"image/tiff": [".tiff", ".tif"],
...audioFileTypes,
},
default: {
...commonTypes,
"application/msword": [".doc"],
"message/rfc822": [".eml"],
"application/epub+zip": [".epub"],
"image/heic": [".heic"],
"application/vnd.ms-outlook": [".msg"],
"application/vnd.oasis.opendocument.text": [".odt"],
"text/x-org": [".org"],
"application/pkcs7-signature": [".p7s"],
"application/vnd.ms-powerpoint": [".ppt"],
"text/x-rst": [".rst"],
"application/rtf": [".rtf"],
"text/tab-separated-values": [".tsv"],
"application/vnd.ms-excel": [".xls"],
"application/xml": [".xml"],
...audioFileTypes,
},
};
const cardClass = "border border-border bg-slate-400/5 dark:bg-white/5";
export function DocumentUploadTab({ searchSpaceId, onSuccess }: DocumentUploadTabProps) {
const t = useTranslations("upload_documents");
const router = useRouter();
const [files, setFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState(0);
// Use the uploadDocumentMutationAtom
const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom);
const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation;
const fileInputRef = useRef<HTMLInputElement>(null);
const audioFileTypes = {
"audio/mpeg": [".mp3", ".mpeg", ".mpga"],
"audio/mp4": [".mp4", ".m4a"],
"audio/wav": [".wav"],
"audio/webm": [".webm"],
"text/markdown": [".md", ".markdown"],
"text/plain": [".txt"],
};
const getAcceptedFileTypes = () => {
const acceptedFileTypes = useMemo(() => {
const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE;
return FILE_TYPE_CONFIG[etlService || "default"] || FILE_TYPE_CONFIG.default;
}, []);
if (etlService === "LLAMACLOUD") {
return {
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"application/vnd.ms-word.document.macroEnabled.12": [".docm"],
"application/msword-template": [".dot"],
"application/vnd.ms-word.template.macroEnabled.12": [".dotm"],
"application/vnd.ms-powerpoint": [".ppt"],
"application/vnd.ms-powerpoint.template.macroEnabled.12": [".pptm"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
"application/vnd.ms-powerpoint.template": [".pot"],
"application/vnd.openxmlformats-officedocument.presentationml.template": [".potx"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"application/vnd.ms-excel": [".xls"],
"application/vnd.ms-excel.sheet.macroEnabled.12": [".xlsm"],
"application/vnd.ms-excel.sheet.binary.macroEnabled.12": [".xlsb"],
"application/vnd.ms-excel.workspace": [".xlw"],
"application/rtf": [".rtf"],
"application/xml": [".xml"],
"application/epub+zip": [".epub"],
"text/csv": [".csv"],
"text/tab-separated-values": [".tsv"],
"text/html": [".html", ".htm", ".web"],
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
"image/gif": [".gif"],
"image/bmp": [".bmp"],
"image/svg+xml": [".svg"],
"image/tiff": [".tiff"],
"image/webp": [".webp"],
...audioFileTypes,
};
} else if (etlService === "DOCLING") {
return {
"application/pdf": [".pdf"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
"text/asciidoc": [".adoc", ".asciidoc"],
"text/html": [".html", ".htm", ".xhtml"],
"text/csv": [".csv"],
"image/png": [".png"],
"image/jpeg": [".jpg", ".jpeg"],
"image/tiff": [".tiff", ".tif"],
"image/bmp": [".bmp"],
"image/webp": [".webp"],
...audioFileTypes,
};
} else {
return {
"image/bmp": [".bmp"],
"text/csv": [".csv"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [".docx"],
"message/rfc822": [".eml"],
"application/epub+zip": [".epub"],
"image/heic": [".heic"],
"text/html": [".html"],
"image/jpeg": [".jpeg", ".jpg"],
"image/png": [".png"],
"application/vnd.ms-outlook": [".msg"],
"application/vnd.oasis.opendocument.text": [".odt"],
"text/x-org": [".org"],
"application/pkcs7-signature": [".p7s"],
"application/pdf": [".pdf"],
"application/vnd.ms-powerpoint": [".ppt"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation": [".pptx"],
"text/x-rst": [".rst"],
"application/rtf": [".rtf"],
"image/tiff": [".tiff"],
"text/tab-separated-values": [".tsv"],
"application/vnd.ms-excel": [".xls"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"],
"application/xml": [".xml"],
...audioFileTypes,
};
}
};
const acceptedFileTypes = getAcceptedFileTypes();
const supportedExtensions = Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort();
const supportedExtensions = useMemo(
() => Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort(),
[acceptedFileTypes]
);
const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles((prevFiles) => [...prevFiles, ...acceptedFiles]);
setFiles((prev) => [...prev, ...acceptedFiles]);
}, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({
@ -140,12 +137,12 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
accept: acceptedFileTypes,
maxSize: 50 * 1024 * 1024,
noClick: false,
noKeyboard: false,
});
const removeFile = (index: number) => {
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index));
};
// Handle file input click to prevent event bubbling that might reopen dialog
const handleFileInputClick = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}, []);
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
@ -155,114 +152,93 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
};
const totalFileSize = files.reduce((total, file) => total + file.size, 0);
const handleUpload = async () => {
setUploadProgress(0);
trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalFileSize);
// Track upload started
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
trackDocumentUploadStarted(Number(searchSpaceId), files.length, totalSize);
// Create a progress interval to simulate progress
const progressInterval = setInterval(() => {
setUploadProgress((prev) => {
if (prev >= 90) return prev;
return prev + Math.random() * 10;
});
setUploadProgress((prev) => (prev >= 90 ? prev : prev + Math.random() * 10));
}, 200);
// Use the mutation to upload documents
uploadDocuments(
{
files,
search_space_id: Number(searchSpaceId),
},
{ files, search_space_id: Number(searchSpaceId) },
{
onSuccess: () => {
clearInterval(progressInterval);
setUploadProgress(100);
// Track upload success
trackDocumentUploadSuccess(Number(searchSpaceId), files.length);
toast(t("upload_initiated"), {
description: t("upload_initiated_desc"),
});
router.push(`/dashboard/${searchSpaceId}/documents`);
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
onSuccess?.() || router.push(`/dashboard/${searchSpaceId}/documents`);
},
onError: (error: any) => {
onError: (error: unknown) => {
clearInterval(progressInterval);
setUploadProgress(0);
// Track upload failure
trackDocumentUploadFailure(Number(searchSpaceId), error.message || "Upload failed");
const message = error instanceof Error ? error.message : "Upload failed";
trackDocumentUploadFailure(Number(searchSpaceId), message);
toast(t("upload_error"), {
description: `${t("upload_error_desc")}: ${error.message || "Upload failed"}`,
description: `${t("upload_error_desc")}: ${message}`,
});
},
}
);
};
const getTotalFileSize = () => {
return files.reduce((total, file) => total + file.size, 0);
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="space-y-6 max-w-4xl mx-auto"
className="space-y-3 sm:space-y-6 max-w-4xl mx-auto"
>
<Alert>
<Alert className="border border-border bg-slate-400/5 dark:bg-white/5">
<Info className="h-4 w-4" />
<AlertDescription className="text-xs sm:text-sm">{t("file_size_limit")}</AlertDescription>
</Alert>
<Card className="relative overflow-hidden">
<Card className={`relative overflow-hidden ${cardClass}`}>
<div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)] opacity-30">
<GridPattern />
</div>
<CardContent className="p-10 relative z-10">
<CardContent className="p-4 sm:p-10 relative z-10">
<div
{...getRootProps()}
className="flex flex-col items-center justify-center min-h-[300px] border-2 border-dashed border-muted-foreground/25 rounded-lg hover:border-primary/50 transition-colors cursor-pointer"
className="flex flex-col items-center justify-center min-h-[200px] sm:min-h-[300px] border-2 border-dashed border-border rounded-lg hover:border-primary/50 transition-colors cursor-pointer"
>
<input {...getInputProps()} className="hidden" />
<input
{...getInputProps()}
ref={fileInputRef}
className="hidden"
onClick={handleFileInputClick}
/>
{isDragActive ? (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center gap-4"
className="flex flex-col items-center gap-2 sm:gap-4"
>
<Upload className="h-12 w-12 text-primary" />
<p className="text-lg font-medium text-primary">{t("drop_files")}</p>
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-primary" />
<p className="text-sm sm:text-lg font-medium text-primary">{t("drop_files")}</p>
</motion.div>
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-4"
>
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="flex flex-col items-center gap-2 sm:gap-4">
<Upload className="h-8 w-8 sm:h-12 sm:w-12 text-muted-foreground" />
<div className="text-center">
<p className="text-base sm:text-lg font-medium">{t("drag_drop")}</p>
<p className="text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
<p className="text-sm sm:text-lg font-medium">{t("drag_drop")}</p>
<p className="text-xs sm:text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
</div>
</motion.div>
</div>
)}
<div className="mt-4">
<div className="mt-2 sm:mt-4">
<Button
variant="outline"
size="sm"
className="text-xs sm:text-sm"
onClick={(e) => {
e.stopPropagation();
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
if (input) input.click();
e.preventDefault();
fileInputRef.current?.click();
}}
>
{t("browse_files")}
@ -280,20 +256,21 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg sm:text-2xl">
<Card className={cardClass}>
<CardHeader className="p-4 sm:p-6">
<div className="flex items-center justify-between gap-2">
<div className="min-w-0 flex-1">
<CardTitle className="text-base sm:text-2xl">
{t("selected_files", { count: files.length })}
</CardTitle>
<CardDescription className="text-xs sm:text-sm">
{t("total_size")}: {formatFileSize(getTotalFileSize())}
{t("total_size")}: {formatFileSize(totalFileSize)}
</CardDescription>
</div>
<Button
variant="outline"
size="sm"
className="text-xs sm:text-sm shrink-0"
onClick={() => setFiles([])}
disabled={isUploading}
>
@ -301,8 +278,8 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-3 max-h-[400px] overflow-y-auto">
<CardContent className="p-4 sm:p-6 pt-0">
<div className="space-y-2 sm:space-y-3 max-h-[250px] sm:max-h-[400px] overflow-y-auto">
<AnimatePresence>
{files.map((file, index) => (
<motion.div
@ -310,7 +287,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
className="flex items-center justify-between p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
className={`flex items-center justify-between p-2 sm:p-4 rounded-lg border border-border ${cardClass} hover:bg-slate-400/10 dark:hover:bg-white/10 transition-colors`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" />
@ -329,7 +306,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<Button
variant="ghost"
size="icon"
onClick={() => removeFile(index)}
onClick={() => setFiles((prev) => prev.filter((_, i) => i !== index))}
disabled={isUploading}
className="h-8 w-8"
>
@ -344,11 +321,11 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="mt-6 space-y-3"
className="mt-3 sm:mt-6 space-y-2 sm:space-y-3"
>
<Separator />
<Separator className="bg-border" />
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center justify-between text-xs sm:text-sm">
<span>{t("uploading_files")}</span>
<span>{Math.round(uploadProgress)}%</span>
</div>
@ -358,23 +335,23 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
)}
<motion.div
className="mt-6"
className="mt-3 sm:mt-6"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
>
<Button
className="w-full py-4 sm:py-6 text-sm sm:text-base font-medium"
className="w-full py-3 sm:py-6 text-xs sm:text-base font-medium"
onClick={handleUpload}
disabled={isUploading || files.length === 0}
>
{isUploading ? (
<span className="flex items-center gap-2">
<Loader2 className="h-5 w-5 animate-spin" />
<Loader2 className="h-4 w-4 sm:h-5 sm:w-5 animate-spin" />
{t("uploading")}
</span>
) : (
<span className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5" />
<CheckCircle2 className="h-4 w-4 sm:h-5 sm:w-5" />
{t("upload_button", { count: files.length })}
</span>
)}
@ -386,24 +363,36 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
)}
</AnimatePresence>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
{t("supported_file_types")}
</CardTitle>
<CardDescription>{t("file_types_desc")}</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{supportedExtensions.map((ext) => (
<Badge key={ext} variant="outline" className="text-xs">
{ext}
</Badge>
))}
</div>
</CardContent>
</Card>
<Accordion
type="single"
collapsible
className={`w-full ${cardClass} border border-border rounded-lg`}
>
<AccordionItem value="supported-file-types" className="border-0">
<AccordionTrigger className="px-3 sm:px-6 py-3 sm:py-4 hover:no-underline">
<div className="flex items-center gap-2">
<Tag className="h-4 w-4 sm:h-5 sm:w-5 shrink-0" />
<div className="text-left min-w-0">
<div className="font-semibold text-sm sm:text-base">
{t("supported_file_types")}
</div>
<div className="text-xs sm:text-sm text-muted-foreground font-normal">
{t("file_types_desc")}
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-3 sm:px-6 pb-3 sm:pb-6">
<div className="flex flex-wrap gap-2">
{supportedExtensions.map((ext) => (
<Badge key={ext} variant="outline" className="text-xs">
{ext}
</Badge>
))}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</motion.div>
);
}

View file

@ -2,7 +2,7 @@ export function GridPattern() {
const columns = 41;
const rows = 11;
return (
<div className="flex bg-gray-100 dark:bg-neutral-900 flex-shrink-0 flex-wrap justify-center items-center gap-x-px gap-y-px scale-105">
<div className="flex bg-transparent flex-shrink-0 flex-wrap justify-center items-center gap-x-px gap-y-px scale-105">
{Array.from({ length: rows }).map((_, row) =>
Array.from({ length: columns }).map((_, col) => {
const index = row * columns + col;
@ -11,8 +11,8 @@ export function GridPattern() {
key={`${col}-${row}`}
className={`w-10 h-10 flex flex-shrink-0 rounded-[2px] ${
index % 2 === 0
? "bg-gray-50 dark:bg-neutral-950"
: "bg-gray-50 dark:bg-neutral-950 shadow-[0px_0px_1px_3px_rgba(255,255,255,1)_inset] dark:shadow-[0px_0px_1px_3px_rgba(0,0,0,1)_inset]"
? "bg-slate-200/20 dark:bg-slate-400/10"
: "bg-slate-300/30 dark:bg-slate-500/15 shadow-[0px_0px_1px_3px_rgba(255,255,255,0.1)_inset] dark:shadow-[0px_0px_1px_3px_rgba(255,255,255,0.05)_inset]"
}`}
/>
);

View file

@ -1,8 +1,4 @@
import {
IconLinkPlus,
IconSparkles,
IconUsersGroup,
} from "@tabler/icons-react";
import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react";
import {
File,
FileText,
@ -19,32 +15,18 @@ import { EnumConnectorName } from "./connector";
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
const iconProps = { className: className || "h-4 w-4" };
const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 };
// Larger props for specific services (Google services, GitHub, Linear) - scale up from size-6 to size-8
const getLargeClassName = () => {
if (!className) return "h-8 w-8";
// Replace size-6 with size-8, or h-6/w-6 with h-8/w-8
return className
.replace(/size-6/g, "size-8")
.replace(/\bh-6\b/g, "h-8")
.replace(/\bw-6\b/g, "w-8");
};
const largeImgProps = {
className: getLargeClassName(),
width: 32,
height: 32
};
switch (connectorType) {
case EnumConnectorName.LINKUP_API:
return <IconLinkPlus {...iconProps} />;
case EnumConnectorName.LINEAR_CONNECTOR:
return <Image src="/connectors/linear.svg" alt="Linear" {...largeImgProps} />;
return <Image src="/connectors/linear.svg" alt="Linear" {...imgProps} />;
case EnumConnectorName.GITHUB_CONNECTOR:
return <Image src="/connectors/github.svg" alt="GitHub" {...largeImgProps} />;
return <Image src="/connectors/github.svg" alt="GitHub" {...imgProps} />;
case EnumConnectorName.TAVILY_API:
return <Image src="/connectors/tavily.svg" alt="Tavily" {...imgProps} />;
case EnumConnectorName.SEARXNG_API:
return <Globe {...iconProps} />;
return <Image src="/connectors/searxng.svg" alt="SearXNG" {...imgProps} />;
case EnumConnectorName.BAIDU_SEARCH_API:
return <Image src="/connectors/baidu-search.svg" alt="Baidu" {...imgProps} />;
case EnumConnectorName.SLACK_CONNECTOR:
@ -56,11 +38,11 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.JIRA_CONNECTOR:
return <Image src="/connectors/jira.svg" alt="Jira" {...imgProps} />;
case EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR:
return <Image src="/connectors/google-calendar.svg" alt="Google Calendar" {...largeImgProps} />;
return <Image src="/connectors/google-calendar.svg" alt="Google Calendar" {...imgProps} />;
case EnumConnectorName.GOOGLE_GMAIL_CONNECTOR:
return <Image src="/connectors/google-gmail.svg" alt="Gmail" {...largeImgProps} />;
return <Image src="/connectors/google-gmail.svg" alt="Gmail" {...imgProps} />;
case EnumConnectorName.GOOGLE_DRIVE_CONNECTOR:
return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...largeImgProps} />;
return <Image src="/connectors/google-drive.svg" alt="Google Drive" {...imgProps} />;
case EnumConnectorName.AIRTABLE_CONNECTOR:
return <Image src="/connectors/airtable.svg" alt="Airtable" {...imgProps} />;
case EnumConnectorName.CONFLUENCE_CONNECTOR:
@ -70,7 +52,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.CLICKUP_CONNECTOR:
return <Image src="/connectors/clickup.svg" alt="ClickUp" {...imgProps} />;
case EnumConnectorName.LUMA_CONNECTOR:
return <IconSparkles {...iconProps} />;
return <Image src="/connectors/luma.svg" alt="Luma" {...imgProps} />;
case EnumConnectorName.ELASTICSEARCH_CONNECTOR:
return <Image src="/connectors/elasticsearch.svg" alt="Elasticsearch" {...imgProps} />;
case EnumConnectorName.WEBCRAWLER_CONNECTOR:
@ -96,6 +78,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <Image src="/connectors/zoom.svg" alt="Zoom" {...imgProps} />;
case "FILE":
return <File {...iconProps} />;
case "GOOGLE_DRIVE_FILE":
return <File {...iconProps} />;
case "NOTE":
return <FileText {...iconProps} />;
case "EXTENSION":

View file

@ -16,6 +16,7 @@ import {
} from "@/components/editConnector/types";
import type { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/hooks/use-search-source-connectors";
import type { UpdateConnectorResponse } from "@/contracts/types/connector.types";
import { authenticatedFetch } from "@/lib/auth-utils";
const normalizeListInput = (value: unknown): string[] => {
@ -57,7 +58,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
// State managed by the hook
const [connector, setConnector] = useState<SearchSourceConnector | null>(null);
const [originalConfig, setOriginalConfig] = useState<Record<string, any> | null>(null);
const [originalConfig, setOriginalConfig] = useState<Record<string, unknown> | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [currentSelectedRepos, setCurrentSelectedRepos] = useState<string[]>([]);
const [originalPat, setOriginalPat] = useState<string>("");
@ -161,7 +162,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
}
} else {
toast.error("Connector not found.");
router.push(`/dashboard/${searchSpaceId}/connectors`);
router.push(`/dashboard/${searchSpaceId}`);
}
}
}, [
@ -171,8 +172,10 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
router,
searchSpaceId,
connector,
editForm,
patForm,
editForm.reset,
patForm.reset,
// Note: editForm and patForm are intentionally excluded from dependencies
// to prevent infinite loops. They are stable form objects from react-hook-form.
]);
// Handlers managed by the hook
@ -219,7 +222,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
setIsSaving(true);
const updatePayload: Partial<SearchSourceConnector> = {};
let configChanged = false;
let newConfig: Record<string, any> | null = null;
let newConfig: Record<string, unknown> | null = null;
if (formData.name !== connector.name) {
updatePayload.name = formData.name;
@ -296,12 +299,18 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
return;
}
const candidateConfig: Record<string, any> = { SEARXNG_HOST: host };
let hasChanges = host !== (originalConfig.SEARXNG_HOST || "").trim();
const candidateConfig: Record<string, unknown> = { SEARXNG_HOST: host };
const originalHost =
typeof originalConfig.SEARXNG_HOST === "string" ? originalConfig.SEARXNG_HOST : "";
let hasChanges = host !== originalHost.trim();
const apiKey = (formData.SEARXNG_API_KEY || "").trim();
const originalApiKey = (originalConfig.SEARXNG_API_KEY || "").trim();
if (apiKey !== originalApiKey) {
const originalApiKey =
typeof originalConfig.SEARXNG_API_KEY === "string"
? originalConfig.SEARXNG_API_KEY
: "";
const originalApiKeyTrimmed = originalApiKey.trim();
if (apiKey !== originalApiKeyTrimmed) {
candidateConfig.SEARXNG_API_KEY = apiKey || null;
hasChanges = true;
}
@ -321,8 +330,12 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
}
const language = (formData.SEARXNG_LANGUAGE || "").trim();
const originalLanguage = (originalConfig.SEARXNG_LANGUAGE || "").trim();
if (language !== originalLanguage) {
const originalLanguage =
typeof originalConfig.SEARXNG_LANGUAGE === "string"
? originalConfig.SEARXNG_LANGUAGE
: "";
const originalLanguageTrimmed = originalLanguage.trim();
if (language !== originalLanguageTrimmed) {
candidateConfig.SEARXNG_LANGUAGE = language || null;
hasChanges = true;
}
@ -490,7 +503,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
) {
newConfig = {};
if (formData.FIRECRAWL_API_KEY && formData.FIRECRAWL_API_KEY.trim()) {
if (formData.FIRECRAWL_API_KEY?.trim()) {
if (!formData.FIRECRAWL_API_KEY.startsWith("fc-")) {
toast.warning(
"Firecrawl API keys typically start with 'fc-'. Please verify your key."
@ -504,7 +517,7 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
}
if (formData.INITIAL_URLS !== undefined) {
if (formData.INITIAL_URLS && formData.INITIAL_URLS.trim()) {
if (formData.INITIAL_URLS?.trim()) {
newConfig.INITIAL_URLS = formData.INITIAL_URLS.trim();
} else if (originalConfig.INITIAL_URLS) {
toast.info("URLs removed from crawler configuration.");
@ -530,21 +543,19 @@ export function useConnectorEditPage(connectorId: number, searchSpaceId: string)
}
try {
await updateConnector({
const updatedConnector = (await updateConnector({
id: connectorId,
data: {
...updatePayload,
connector_type: connector.connector_type as EnumConnectorName,
},
});
})) as UpdateConnectorResponse;
toast.success("Connector updated!");
const newlySavedConfig = updatePayload.config || originalConfig;
// Use the response from the API which has the full merged config
const newlySavedConfig = updatedConnector.config || originalConfig;
setOriginalConfig(newlySavedConfig);
if (updatePayload.name) {
setConnector((prev) =>
prev ? { ...prev, name: updatePayload.name!, config: newlySavedConfig } : null
);
}
// Update connector state with the full updated connector from the API
setConnector(updatedConnector);
if (configChanged) {
if (connector.connector_type === "GITHUB_CONNECTOR") {
const savedGitHubConfig = newlySavedConfig as {

View file

@ -26,4 +26,3 @@ export function useGoogleDriveFolders({
retry: 2,
});
}

View file

@ -374,7 +374,7 @@
"tip_4": "Processing may take some time depending on video length",
"preview": "Preview",
"cancel": "Cancel",
"submit": "Submit YouTube Videos",
"submit": "Add",
"processing": "Processing...",
"error_no_video": "Please add at least one YouTube video URL",
"error_invalid_urls": "Invalid YouTube URLs detected: {urls}",

View file

@ -1 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>BookStack</title><path d="M.3013 17.6146c-.1299-.3387-.5228-1.5119-.1337-2.4314l9.8273 5.6738a.329.329 0 0 0 .3299 0L24 12.9616v2.3542l-13.8401 7.9906-9.8586-5.6918zM.1911 8.9628c-.2882.8769.0149 2.0581.1236 2.4261l9.8452 5.6841L24 9.0823V6.7275L10.3248 14.623a.329.329 0 0 1-.3299 0L.1911 8.9628zm13.1698-1.9361c-.1819.1113-.4394.0015-.4852-.2064l-.2805-1.1336-2.1254-.1752a.33.33 0 0 1-.1378-.6145l5.5782-3.2207-1.7021-.9826L.6979 8.4935l9.462 5.463 13.5104-7.8004-4.401-2.5407-5.9084 3.4113zm-.1821-1.7286.2321.938 5.1984-3.0014-2.0395-1.1775-4.994 2.8834 1.3099.108a.3302.3302 0 0 1 .2931.2495zM24 9.845l-13.6752 7.8954a.329.329 0 0 1-.3299 0L.1678 12.0667c-.3891.919.003 2.0914.1332 2.4311l9.8589 5.692L24 12.1993V9.845z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#0078B9" d="M.301 17.615c-.13-.34-.522-1.512-.133-2.432l9.827 5.674a.33.33 0 0 0 .33 0L24 12.962v2.354l-13.84 7.99zm-.11-8.652c-.288.877.015 2.058.124 2.426l9.845 5.684L24 9.083V6.727l-13.675 7.895a.33.33 0 0 1-.33 0zm13.17-1.936a.332.332 0 0 1-.485-.207l-.28-1.133l-2.126-.176a.33.33 0 0 1-.138-.614l5.578-3.22l-1.702-.983l-13.51 7.8l9.462 5.462l13.51-7.8l-4.4-2.54zm-.182-1.729l.232.938l5.198-3.001l-2.04-1.178l-4.993 2.884l1.31.108a.33.33 0 0 1 .293.25M24 9.845L10.325 17.74a.33.33 0 0 1-.33 0L.168 12.067c-.39.919.003 2.091.133 2.43l9.859 5.693L24 12.2z"/></svg>

Before

Width:  |  Height:  |  Size: 812 B

After

Width:  |  Height:  |  Size: 661 B

Before After
Before After

View file

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><rect id="Icons" x="0" y="-192" width="1280" height="800" style="fill:none;"/><g id="Icons1" serif:id="Icons"><g id="Strike"></g><g id="H1"></g><g id="H2"></g><g id="H3"></g><g id="list-ul"></g><g id="hamburger-1"></g><g id="hamburger-2"></g><g id="list-ol"></g><g id="list-task"></g><g id="trash"></g><g id="vertical-menu"></g><g id="horizontal-menu"></g><g id="sidebar-2"></g><g id="Pen"></g><g id="Pen1" serif:id="Pen"></g><g id="clock"></g><g id="external-link"></g><g id="hr"></g><g id="info"></g><g id="warning"></g><g id="plus-circle"></g><g id="minus-circle"></g><g id="vue"></g><g id="cog"></g><path id="github" d="M32.029,8.345c-13.27,0 -24.029,10.759 -24.029,24.033c0,10.617 6.885,19.624 16.435,22.803c1.202,0.22 1.64,-0.522 1.64,-1.16c0,-0.569 -0.02,-2.081 -0.032,-4.086c-6.685,1.452 -8.095,-3.222 -8.095,-3.222c-1.093,-2.775 -2.669,-3.514 -2.669,-3.514c-2.182,-1.492 0.165,-1.462 0.165,-1.462c2.412,0.171 3.681,2.477 3.681,2.477c2.144,3.672 5.625,2.611 6.994,1.997c0.219,-1.553 0.838,-2.612 1.526,-3.213c-5.336,-0.606 -10.947,-2.669 -10.947,-11.877c0,-2.623 0.937,-4.769 2.474,-6.449c-0.247,-0.608 -1.072,-3.051 0.235,-6.36c0,0 2.018,-0.646 6.609,2.464c1.917,-0.533 3.973,-0.8 6.016,-0.809c2.041,0.009 4.097,0.276 6.017,0.809c4.588,-3.11 6.602,-2.464 6.602,-2.464c1.311,3.309 0.486,5.752 0.239,6.36c1.54,1.68 2.471,3.826 2.471,6.449c0,9.232 -5.62,11.263 -10.974,11.858c0.864,0.742 1.632,2.208 1.632,4.451c0,3.212 -0.029,5.804 -0.029,6.591c0,0.644 0.432,1.392 1.652,1.157c9.542,-3.185 16.421,-12.186 16.421,-22.8c0,-13.274 -10.76,-24.033 -24.034,-24.033" style="fill:#010101;"/><g id="logo"></g><g id="eye-slash"></g><g id="eye"></g><g id="toggle-off"></g><g id="shredder"></g><g id="spinner--loading--dots-" serif:id="spinner [loading, dots]"></g><g id="react"></g></g></svg>
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/></svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 963 B

Before After
Before After

View file

@ -1,44 +1 @@
<?xml version="1.0" ?><svg id="Capa_1" style="enable-background:new 0 0 150 150;" version="1.1" viewBox="0 0 150 150" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#1A73E8;}
.st1{fill:#EA4335;}
.st2{fill:#4285F4;}
.st3{fill:#FBBC04;}
.st4{fill:#34A853;}
.st5{fill:#4CAF50;}
.st6{fill:#1E88E5;}
.st7{fill:#E53935;}
.st8{fill:#C62828;}
.st9{fill:#FBC02D;}
.st10{fill:#1565C0;}
.st11{fill:#2E7D32;}
.st12{fill:#F6B704;}
.st13{fill:#E54335;}
.st14{fill:#4280EF;}
.st15{fill:#34A353;}
.st16{clip-path:url(#SVGID_2_);}
.st17{fill:#188038;}
.st18{opacity:0.2;fill:#FFFFFF;enable-background:new ;}
.st19{opacity:0.3;fill:#0D652D;enable-background:new ;}
.st20{clip-path:url(#SVGID_4_);}
.st21{opacity:0.3;fill:url(#_45_shadow_1_);enable-background:new ;}
.st22{clip-path:url(#SVGID_6_);}
.st23{fill:#FA7B17;}
.st24{opacity:0.3;fill:#174EA6;enable-background:new ;}
.st25{opacity:0.3;fill:#A50E0E;enable-background:new ;}
.st26{opacity:0.3;fill:#E37400;enable-background:new ;}
.st27{fill:url(#Finish_mask_1_);}
.st28{fill:#FFFFFF;}
.st29{fill:#0C9D58;}
.st30{opacity:0.2;fill:#004D40;enable-background:new ;}
.st31{opacity:0.2;fill:#3E2723;enable-background:new ;}
.st32{fill:#FFC107;}
.st33{opacity:0.2;fill:#1A237E;enable-background:new ;}
.st34{opacity:0.2;}
.st35{fill:#1A237E;}
.st36{fill:url(#SVGID_7_);}
.st37{fill:#FBBC05;}
.st38{clip-path:url(#SVGID_9_);fill:#E53935;}
.st39{clip-path:url(#SVGID_11_);fill:#FBC02D;}
.st40{clip-path:url(#SVGID_13_);fill:#E53935;}
.st41{clip-path:url(#SVGID_15_);fill:#FBC02D;}
</style><g><polygon class="st6" points="79.2,67.2 81.8,70.9 85.8,68 85.8,89 90.1,89 90.1,61.4 86.5,61.4 "/><path class="st6" d="M72.3,74.4c1.6-1.4,2.6-3.5,2.6-5.7c0-4.4-3.9-8-8.6-8c-4,0-7.5,2.5-8.4,6.2l4.2,1.1c0.4-1.7,2.2-2.9,4.2-2.9 c2.4,0,4.3,1.6,4.3,3.6c0,2-1.9,3.6-4.3,3.6h-2.5v4.4h2.5c2.7,0,5,1.9,5,4.1c0,2.3-2.2,4.1-4.9,4.1c-2.4,0-4.5-1.5-4.8-3.6 l-4.2,0.7c0.7,4.1,4.6,7.2,9.1,7.2c5.1,0,9.2-3.8,9.2-8.5C75.6,78.2,74.3,75.9,72.3,74.4z"/><polygon class="st9" points="100.2,120.3 49.8,120.3 49.8,100.2 100.2,100.2 "/><polygon class="st5" points="120.3,100.2 120.3,49.8 100.2,49.8 100.2,100.2 "/><path class="st6" d="M100.2,49.8V29.7h-63c-4.2,0-7.6,3.4-7.6,7.6v63h20.1V49.8H100.2z"/><polygon class="st7" points="100.2,100.2 100.2,120.3 120.3,100.2 "/><path class="st10" d="M112.8,29.7h-12.6v20.1h20.1V37.2C120.3,33,117,29.7,112.8,29.7z"/><path class="st10" d="M37.2,120.3h12.6v-20.1H29.7v12.6C29.7,117,33,120.3,37.2,120.3z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"><path fill="#fff" d="M195.368 60.632H60.632v134.736h134.736z"/><path fill="#ea4335" d="M195.368 256L256 195.368l-30.316-5.172l-30.316 5.172l-5.533 27.73z"/><path fill="#188038" d="M0 195.368v40.421C0 246.956 9.044 256 20.21 256h40.422l6.225-30.316l-6.225-30.316l-33.033-5.172z"/><path fill="#1967d2" d="M256 60.632V20.21C256 9.044 246.956 0 235.79 0h-40.422q-5.532 22.554-5.533 33.196q0 10.641 5.533 27.436q20.115 5.76 30.316 5.76T256 60.631"/><path fill="#fbbc04" d="M256 60.632h-60.632v134.736H256z"/><path fill="#34a853" d="M195.368 195.368H60.632V256h134.736z"/><path fill="#4285f4" d="M195.368 0H20.211C9.044 0 0 9.044 0 20.21v175.158h60.632V60.632h134.736z"/><path fill="#4285f4" d="M88.27 165.154c-5.036-3.402-8.523-8.37-10.426-14.94l11.689-4.816q1.59 6.063 5.558 9.398c2.627 2.223 5.827 3.318 9.566 3.318q5.734 0 9.852-3.487c2.746-2.324 4.127-5.288 4.127-8.875q0-5.508-4.345-8.994c-2.897-2.324-6.535-3.486-10.88-3.486h-6.754v-11.57h6.063q5.608 0 9.448-3.033c2.56-2.02 3.84-4.783 3.84-8.303c0-3.132-1.145-5.625-3.435-7.494c-2.29-1.87-5.188-2.813-8.708-2.813c-3.436 0-6.164.91-8.185 2.745a16.1 16.1 0 0 0-4.413 6.754l-11.57-4.817c1.532-4.345 4.345-8.185 8.471-11.503s9.398-4.985 15.798-4.985c4.733 0 8.994.91 12.767 2.745c3.772 1.836 6.736 4.379 8.875 7.613c2.14 3.25 3.2 6.888 3.2 10.93c0 4.126-.993 7.613-2.98 10.476s-4.43 5.052-7.327 6.585v.69a22.25 22.25 0 0 1 9.398 7.327c2.442 3.284 3.672 7.208 3.672 11.79c0 4.58-1.163 8.673-3.487 12.26c-2.324 3.588-5.54 6.417-9.617 8.472c-4.092 2.055-8.69 3.1-13.793 3.1c-5.912.016-11.369-1.685-16.405-5.087m71.797-58.005l-12.833 9.28l-6.417-9.734l23.023-16.607h8.825v78.333h-12.598z"/></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -1,44 +1 @@
<?xml version="1.0" ?><svg id="Capa_1" style="enable-background:new 0 0 150 150;" version="1.1" viewBox="0 0 150 150" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#1A73E8;}
.st1{fill:#EA4335;}
.st2{fill:#4285F4;}
.st3{fill:#FBBC04;}
.st4{fill:#34A853;}
.st5{fill:#4CAF50;}
.st6{fill:#1E88E5;}
.st7{fill:#E53935;}
.st8{fill:#C62828;}
.st9{fill:#FBC02D;}
.st10{fill:#1565C0;}
.st11{fill:#2E7D32;}
.st12{fill:#F6B704;}
.st13{fill:#E54335;}
.st14{fill:#4280EF;}
.st15{fill:#34A353;}
.st16{clip-path:url(#SVGID_2_);}
.st17{fill:#188038;}
.st18{opacity:0.2;fill:#FFFFFF;enable-background:new ;}
.st19{opacity:0.3;fill:#0D652D;enable-background:new ;}
.st20{clip-path:url(#SVGID_4_);}
.st21{opacity:0.3;fill:url(#_45_shadow_1_);enable-background:new ;}
.st22{clip-path:url(#SVGID_6_);}
.st23{fill:#FA7B17;}
.st24{opacity:0.3;fill:#174EA6;enable-background:new ;}
.st25{opacity:0.3;fill:#A50E0E;enable-background:new ;}
.st26{opacity:0.3;fill:#E37400;enable-background:new ;}
.st27{fill:url(#Finish_mask_1_);}
.st28{fill:#FFFFFF;}
.st29{fill:#0C9D58;}
.st30{opacity:0.2;fill:#004D40;enable-background:new ;}
.st31{opacity:0.2;fill:#3E2723;enable-background:new ;}
.st32{fill:#FFC107;}
.st33{opacity:0.2;fill:#1A237E;enable-background:new ;}
.st34{opacity:0.2;}
.st35{fill:#1A237E;}
.st36{fill:url(#SVGID_7_);}
.st37{fill:#FBBC05;}
.st38{clip-path:url(#SVGID_9_);fill:#E53935;}
.st39{clip-path:url(#SVGID_11_);fill:#FBC02D;}
.st40{clip-path:url(#SVGID_13_);fill:#E53935;}
.st41{clip-path:url(#SVGID_15_);fill:#FBC02D;}
</style><g><path class="st6" d="M104.8,113.3c-2,1.2-4.3,1.8-6.7,1.8H51.8c-2.4,0-4.7-0.6-6.7-1.8c7.3-12.6,14.4-25,14.4-25h30.8 C90.4,88.4,99.2,103.6,104.8,113.3z"/><path class="st9" d="M89.4,36.7c2,1.2,3.7,2.8,4.9,4.9l23.2,40.1c1.2,2.1,1.8,4.4,1.8,6.7c-10.5,0.1-28.8,0-28.8,0L75,61.6 C75,61.6,83,47.8,89.4,36.7z"/><path class="st7" d="M119.3,88.4c0,2.3-0.6,4.6-1.8,6.7l-8.2,14.2c-1.2,1.7-2.7,3.1-4.4,4.1l-14.4-25H119.3z"/><path class="st5" d="M30.7,88.4c0-2.3,0.6-4.6,1.8-6.7l23.2-40.1c1.2-2.1,2.9-3.7,4.9-4.9c2-1.2,14.4,25,14.4,25L59.6,88.4 C59.6,88.4,39.4,88.4,30.7,88.4z"/><path class="st10" d="M59.6,88.4l-14.4,25c-1.7-1-3.3-2.4-4.4-4.1l-8.2-14.2c-1.2-2.1-1.8-4.4-1.8-6.7H59.6z"/><path class="st11" d="M89.4,36.7L75,61.6l-14.4-25c1.7-1,3.7-1.6,5.8-1.8l16.3,0C85.1,34.9,87.4,35.5,89.4,36.7z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="229" viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before After
Before After

View file

@ -1,44 +1 @@
<?xml version="1.0" ?><svg id="Capa_1" style="enable-background:new 0 0 150 150;" version="1.1" viewBox="0 0 150 150" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><style type="text/css">
.st0{fill:#1A73E8;}
.st1{fill:#EA4335;}
.st2{fill:#4285F4;}
.st3{fill:#FBBC04;}
.st4{fill:#34A853;}
.st5{fill:#4CAF50;}
.st6{fill:#1E88E5;}
.st7{fill:#E53935;}
.st8{fill:#C62828;}
.st9{fill:#FBC02D;}
.st10{fill:#1565C0;}
.st11{fill:#2E7D32;}
.st12{fill:#F6B704;}
.st13{fill:#E54335;}
.st14{fill:#4280EF;}
.st15{fill:#34A353;}
.st16{clip-path:url(#SVGID_2_);}
.st17{fill:#188038;}
.st18{opacity:0.2;fill:#FFFFFF;enable-background:new ;}
.st19{opacity:0.3;fill:#0D652D;enable-background:new ;}
.st20{clip-path:url(#SVGID_4_);}
.st21{opacity:0.3;fill:url(#_45_shadow_1_);enable-background:new ;}
.st22{clip-path:url(#SVGID_6_);}
.st23{fill:#FA7B17;}
.st24{opacity:0.3;fill:#174EA6;enable-background:new ;}
.st25{opacity:0.3;fill:#A50E0E;enable-background:new ;}
.st26{opacity:0.3;fill:#E37400;enable-background:new ;}
.st27{fill:url(#Finish_mask_1_);}
.st28{fill:#FFFFFF;}
.st29{fill:#0C9D58;}
.st30{opacity:0.2;fill:#004D40;enable-background:new ;}
.st31{opacity:0.2;fill:#3E2723;enable-background:new ;}
.st32{fill:#FFC107;}
.st33{opacity:0.2;fill:#1A237E;enable-background:new ;}
.st34{opacity:0.2;}
.st35{fill:#1A237E;}
.st36{fill:url(#SVGID_7_);}
.st37{fill:#FBBC05;}
.st38{clip-path:url(#SVGID_9_);fill:#E53935;}
.st39{clip-path:url(#SVGID_11_);fill:#FBC02D;}
.st40{clip-path:url(#SVGID_13_);fill:#E53935;}
.st41{clip-path:url(#SVGID_15_);fill:#FBC02D;}
</style><g><path class="st5" d="M121.1,57.9L99.1,74.3v35.8h15.4c3.6,0,6.6-2.9,6.6-6.6V57.9z"/><path class="st6" d="M28.9,57.9l21.9,16.5v35.8H35.5c-3.6,0-6.6-2.9-6.6-6.6V57.9z"/><polygon class="st7" points="99.1,46.9 75,65 50.9,46.9 50.9,74.3 75,92.4 99.1,74.3 "/><path class="st8" d="M28.9,49.3v8.6l21.9,16.5V46.9L44,41.8c-1.6-1.2-3.6-1.9-5.7-1.9l0,0C33.1,39.9,28.9,44.1,28.9,49.3z"/><path class="st9" d="M121.1,49.3v8.6L99.1,74.3V46.9l6.9-5.1c1.6-1.2,3.6-1.9,5.7-1.9l0,0C116.9,39.9,121.1,44.1,121.1,49.3z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="193" viewBox="0 0 256 193"><path fill="#4285f4" d="M58.182 192.05V93.14L27.507 65.077L0 49.504v125.091c0 9.658 7.825 17.455 17.455 17.455z"/><path fill="#34a853" d="M197.818 192.05h40.727c9.659 0 17.455-7.826 17.455-17.455V49.505l-31.156 17.837l-27.026 25.798z"/><path fill="#ea4335" d="m58.182 93.14l-4.174-38.647l4.174-36.989L128 69.868l69.818-52.364l4.669 34.992l-4.669 40.644L128 145.504z"/><path fill="#fbbc04" d="M197.818 17.504V93.14L256 49.504V26.231c0-21.585-24.64-33.89-41.89-20.945z"/><path fill="#c5221f" d="m0 49.504l26.759 20.07L58.182 93.14V17.504L41.89 5.286C24.61-7.66 0 4.646 0 26.23z"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 671 B

Before After
Before After

View file

@ -1 +1,5 @@
<?xml version="1.0" ?><svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M3.03509 12.9431C3.24245 14.9227 4.10472 16.8468 5.62188 18.364C7.13904 19.8811 9.0631 20.7434 11.0428 20.9508L3.03509 12.9431Z" fill="currentColor"/><path d="M3 11.4938L12.4921 20.9858C13.2976 20.9407 14.0981 20.7879 14.8704 20.5273L3.4585 9.11548C3.19793 9.88771 3.0451 10.6883 3 11.4938Z" fill="currentColor"/><path d="M3.86722 8.10999L15.8758 20.1186C16.4988 19.8201 17.0946 19.4458 17.6493 18.9956L4.99021 6.33659C4.54006 6.89125 4.16573 7.487 3.86722 8.10999Z" fill="currentColor"/><path d="M5.66301 5.59517C9.18091 2.12137 14.8488 2.135 18.3498 5.63604C21.8508 9.13708 21.8645 14.8049 18.3907 18.3228L5.66301 5.59517Z" fill="currentColor"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="#222326" width="200" height="200" viewBox="0 0 100 100">
<path
d="M1.22541 61.5228c-.2225-.9485.90748-1.5459 1.59638-.857L39.3342 97.1782c.6889.6889.0915 1.8189-.857 1.5964C20.0515 94.4522 5.54779 79.9485 1.22541 61.5228ZM.00189135 46.8891c-.01764375.2833.08887215.5599.28957165.7606L52.3503 99.7085c.2007.2007.4773.3075.7606.2896 2.3692-.1476 4.6938-.46 6.9624-.9259.7645-.157 1.0301-1.0963.4782-1.6481L2.57595 39.4485c-.55186-.5519-1.49117-.2863-1.648174.4782-.465915 2.2686-.77832 4.5932-.92588465 6.9624ZM4.21093 29.7054c-.16649.3738-.08169.8106.20765 1.1l64.77602 64.776c.2894.2894.7262.3742 1.1.2077 1.7861-.7956 3.5171-1.6927 5.1855-2.684.5521-.328.6373-1.0867.1832-1.5407L8.43566 24.3367c-.45409-.4541-1.21271-.3689-1.54074.1832-.99132 1.6684-1.88843 3.3994-2.68399 5.1855ZM12.6587 18.074c-.3701-.3701-.393-.9637-.0443-1.3541C21.7795 6.45931 35.1114 0 49.9519 0 77.5927 0 100 22.4073 100 50.0481c0 14.8405-6.4593 28.1724-16.7199 37.3375-.3903.3487-.984.3258-1.3542-.0443L12.6587 18.074Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M21.996 12.018a10.65 10.65 0 0 0-9.98 9.98h-.04c-.32-5.364-4.613-9.656-9.976-9.98v-.04c5.363-.32 9.656-4.613 9.98-9.976h.04c.324 5.363 4.617 9.656 9.98 9.98v.036z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#919499" d="M21.996 12.018a10.65 10.65 0 0 0-9.98 9.98h-.04c-.32-5.364-4.613-9.656-9.976-9.98v-.04c5.363-.32 9.656-4.613 9.98-9.976h.04c.324 5.363 4.617 9.656 9.98 9.98v.036z"/></svg>

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 278 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#3050FF" d="m13.716 17.261l6.873 6.582L24 20.282l-6.824-6.536a9.1 9.1 0 0 0 1.143-4.43c0-5.055-4.105-9.159-9.16-9.159S0 4.261 0 9.316s4.104 9.159 9.159 9.159a9.1 9.1 0 0 0 4.557-1.214M9.159 2.773a6.546 6.546 0 0 1 6.543 6.543a6.545 6.545 0 0 1-6.543 6.542a6.545 6.545 0 0 1-6.542-6.542a6.545 6.545 0 0 1 6.542-6.543M7.26 5.713a4.065 4.065 0 0 1 4.744.747a4.06 4.06 0 0 1 .707 4.749l1.157.611a5.38 5.38 0 0 0-.935-6.282a5.38 5.38 0 0 0-6.274-.987z"/></svg>

After

Width:  |  Height:  |  Size: 550 B