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

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 type React from "react";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
type IconComponent = React.ComponentType<{ size?: number; className?: string }>;
export function getDocumentTypeIcon(type: string): React.ReactNode { export function getDocumentTypeIcon(type: string): React.ReactNode {
return getConnectorIcon(type); return getConnectorIcon(type);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,9 @@
"use client"; "use client";
import { Search } from "lucide-react"; import { Search, X } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import { import { DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
DialogDescription, import { TabsList, TabsTrigger } from "@/components/ui/tabs";
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface ConnectorDialogHeaderProps { interface ConnectorDialogHeaderProps {
@ -74,14 +67,26 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
<input <input
type="text" type="text"
placeholder="Search" 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} value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)} 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>
</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"> <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> <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"> <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> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Start Date */} {/* Start Date */}
<div className="space-y-2"> <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> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -77,7 +80,9 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
{/* End Date */} {/* End Date */}
<div className="space-y-2"> <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> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@ -137,4 +142,3 @@ export const DateRangeSelector: FC<DateRangeSelectorProps> = ({
</div> </div>
); );
}; };

View file

@ -37,9 +37,11 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
</div> </div>
{enabled && ( {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"> <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}> <Select value={frequencyMinutes} onValueChange={onFrequencyChange}>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
@ -48,12 +50,24 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
<SelectValue placeholder="Select frequency" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -62,4 +76,3 @@ export const PeriodicSyncConfig: FC<PeriodicSyncConfigProps> = ({
</div> </div>
); );
}; };

View file

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

View file

@ -53,10 +53,7 @@ const bookstackConnectorFormSchema = z.object({
type BookStackConnectorFormValues = z.infer<typeof bookstackConnectorFormSchema>; type BookStackConnectorFormValues = z.infer<typeof bookstackConnectorFormSchema>;
export const BookStackConnectForm: FC<ConnectFormProps> = ({ export const BookStackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined); const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -110,14 +107,19 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1"> <div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">API Token Required</AlertTitle> <AlertTitle className="text-xs sm:text-sm">API Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0"> <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> </AlertDescription>
</div> </div>
</Alert> </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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -125,11 +127,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My BookStack Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -147,16 +149,17 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">BookStack Base URL</FormLabel> <FormLabel className="text-xs sm:text-sm">BookStack Base URL</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="url" type="url"
placeholder="https://your-bookstack-instance.com" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -170,11 +173,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Token ID</FormLabel> <FormLabel className="text-xs sm:text-sm">Token ID</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="Your Token ID" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -192,12 +195,12 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Token Secret</FormLabel> <FormLabel className="text-xs sm:text-sm">Token Secret</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="Your Token Secret" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -211,7 +214,7 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -229,14 +232,24 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -264,7 +289,9 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */} {/* What you get section */}
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_CONNECTOR) && ( {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"> <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"> <ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.BOOKSTACK_CONNECTOR)?.map((benefit) => ( {getConnectorBenefits(EnumConnectorName.BOOKSTACK_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li> <li key={benefit}>{benefit}</li>
@ -274,7 +301,11 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -283,14 +314,17 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <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>
<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> </li>
</ul> </ul>
</div> </div>
@ -302,13 +336,16 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<Info 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-[10px] sm:text-xs">API Token Required</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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>Log in to your BookStack instance</li>
<li>Click on your profile icon Edit Profile</li> <li>Click on your profile icon Edit Profile</li>
@ -320,15 +357,19 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Rate Limiting</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Rate Limiting</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -341,13 +382,16 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<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>
<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>
<li> <li>
Click <strong>Connect</strong> to establish the connection. Click <strong>Connect</strong> to establish the connection.
@ -376,4 +420,3 @@ export const BookStackConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

@ -29,10 +29,7 @@ const circlebackFormSchema = z.object({
type CirclebackFormValues = z.infer<typeof circlebackFormSchema>; type CirclebackFormValues = z.infer<typeof circlebackFormSchema>;
export const CirclebackConnectForm: FC<ConnectFormProps> = ({ export const CirclebackConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const form = useForm<CirclebackFormValues>({ const form = useForm<CirclebackFormValues>({
resolver: zodResolver(circlebackFormSchema), resolver: zodResolver(circlebackFormSchema),
@ -71,14 +68,19 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1"> <div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Webhook-Based Integration</AlertTitle> <AlertTitle className="text-xs sm:text-sm">Webhook-Based Integration</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0"> <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> </AlertDescription>
</div> </div>
</Alert> </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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -86,11 +88,11 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My Circleback Connector" placeholder="My Circleback Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40" className="border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -118,4 +120,3 @@ export const CirclebackConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

@ -49,10 +49,7 @@ const clickupConnectorFormSchema = z.object({
type ClickUpConnectorFormValues = z.infer<typeof clickupConnectorFormSchema>; type ClickUpConnectorFormValues = z.infer<typeof clickupConnectorFormSchema>;
export const ClickUpConnectForm: FC<ConnectFormProps> = ({ export const ClickUpConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -125,11 +126,11 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My ClickUp Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -147,12 +148,12 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">ClickUp API Token</FormLabel> <FormLabel className="text-xs sm:text-sm">ClickUp API Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="pk_..." 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -166,7 +167,7 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -184,14 +185,24 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -229,7 +252,11 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -238,14 +265,17 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <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>
<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> </li>
</ul> </ul>
</div> </div>
@ -257,19 +287,23 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<Info 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-[10px] sm:text-xs">API Token Required</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">API Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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>Log in to your ClickUp account</li>
<li>Click your avatar in the upper-right corner and select "Settings"</li> <li>Click your avatar in the upper-right corner and select "Settings"</li>
<li>In the sidebar, click "Apps"</li> <li>In the sidebar, click "Apps"</li>
<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>
<li>Copy the generated token (it typically starts with "pk_")</li> <li>Copy the generated token (it typically starts with "pk_")</li>
<li> <li>
@ -288,15 +322,20 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -309,7 +348,8 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<li> <li>
Place your <strong>API Token</strong> in the form field. Place your <strong>API Token</strong> in the form field.
@ -341,4 +381,3 @@ export const ClickUpConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

@ -51,10 +51,7 @@ const confluenceConnectorFormSchema = z.object({
type ConfluenceConnectorFormValues = z.infer<typeof confluenceConnectorFormSchema>; type ConfluenceConnectorFormValues = z.infer<typeof confluenceConnectorFormSchema>;
export const ConfluenceConnectForm: FC<ConnectFormProps> = ({ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -131,11 +132,11 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My Confluence Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -153,16 +154,17 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Confluence Base URL</FormLabel> <FormLabel className="text-xs sm:text-sm">Confluence Base URL</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="url" type="url"
placeholder="https://your-domain.atlassian.net" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -176,13 +178,13 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel> <FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="email" type="email"
placeholder="your-email@example.com" placeholder="your-email@example.com"
autoComplete="email" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -200,12 +202,12 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel> <FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="Your API Token" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -219,7 +221,7 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -237,14 +239,24 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -272,7 +296,9 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */} {/* What you get section */}
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR) && ( {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"> <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"> <ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR)?.map((benefit) => ( {getConnectorBenefits(EnumConnectorName.CONFLUENCE_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li> <li key={benefit}>{benefit}</li>
@ -282,7 +308,11 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -291,14 +321,17 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <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>
<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> </li>
</ul> </ul>
</div> </div>
@ -308,15 +341,20 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3> <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"> <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" /> <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]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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>Log in to your Atlassian account</li>
<li> <li>
@ -343,15 +381,20 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -364,10 +407,12 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<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>
<li> <li>
Enter your <strong>Email Address</strong> associated with your Atlassian account Enter your <strong>Email Address</strong> associated with your Atlassian account
@ -402,4 +447,3 @@ export const ConfluenceConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

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

View file

@ -73,10 +73,7 @@ const elasticsearchConnectorFormSchema = z
type ElasticsearchConnectorFormValues = z.infer<typeof elasticsearchConnectorFormSchema>; type ElasticsearchConnectorFormValues = z.infer<typeof elasticsearchConnectorFormSchema>;
export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const authBasicId = useId(); const authBasicId = useId();
const authApiKeyId = 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -195,11 +196,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My Elasticsearch Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -231,7 +232,8 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -241,7 +243,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Show parsed URL details */} {/* Show parsed URL details */}
{form.watch("endpoint_url") && ( {form.watch("endpoint_url") && (
<div className="rounded-lg border border-border bg-muted/50 p-3"> <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"> <div className="text-[10px] sm:text-xs text-muted-foreground space-y-1">
{(() => { {(() => {
try { try {
@ -305,7 +309,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" /> <div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator> </RadioGroup.Indicator>
</RadioGroup.Item> </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>
<div className="flex items-center space-x-2"> <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" /> <div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator> </RadioGroup.Indicator>
</RadioGroup.Item> </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> </div>
</RadioGroup.Root> </RadioGroup.Root>
</FormControl> </FormControl>
@ -337,12 +345,12 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Username</FormLabel> <FormLabel className="text-xs sm:text-sm">Username</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="elastic" placeholder="elastic"
autoComplete="username" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@ -392,7 +400,8 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -409,11 +418,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Index Selection</FormLabel> <FormLabel className="text-xs sm:text-sm">Index Selection</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="logs-*, documents-*, app-logs" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -454,7 +463,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Advanced Configuration */} {/* Advanced Configuration */}
<Accordion type="single" collapsible className="w-full"> <Accordion type="single" collapsible className="w-full">
<AccordionItem value="advanced"> <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"> <AccordionContent className="space-y-4">
{/* Default Search Query */} {/* Default Search Query */}
<FormField <FormField
@ -467,15 +478,16 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<span className="text-muted-foreground">(Optional)</span> <span className="text-muted-foreground">(Optional)</span>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="*" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -489,19 +501,19 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm"> <FormLabel className="text-xs sm:text-sm">
Search Fields{" "} Search Fields <span className="text-muted-foreground">(Optional)</span>
<span className="text-muted-foreground">(Optional)</span>
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="title, content, description" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -542,15 +554,14 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{...field} {...field}
onChange={(e) => onChange={(e) =>
field.onChange( field.onChange(
e.target.value === "" e.target.value === "" ? undefined : parseInt(e.target.value, 10)
? undefined
: parseInt(e.target.value, 10)
) )
} }
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -563,7 +574,7 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -581,14 +592,24 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -616,7 +649,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
{/* What you get section */} {/* What you get section */}
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR) && ( {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"> <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"> <ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR)?.map((benefit) => ( {getConnectorBenefits(EnumConnectorName.ELASTICSEARCH_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li> <li key={benefit}>{benefit}</li>
@ -626,7 +661,11 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -635,7 +674,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
</div> </div>
@ -644,43 +685,73 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Connection Setup</h3> <h3 className="text-sm sm:text-base font-semibold mb-2">Connection Setup</h3>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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> </p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground mb-4"> <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>
<li>Self-hosted: <code className="bg-muted px-1 py-0.5 rounded">https://elasticsearch.example.com:9200</code></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> </ul>
</div> </div>
<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"> <p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Elasticsearch requires authentication. You can use either: Elasticsearch requires authentication. You can use either:
</p> </p>
<ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4"> <ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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"> <pre className="bg-muted p-2 rounded mt-1 text-[9px] overflow-x-auto">
<code>POST /_security/api_key</code> <code>POST /_security/api_key</code>
</pre> </pre>
</li> </li>
<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> </li>
</ul> </ul>
</div> </div>
<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"> <p className="text-[10px] sm:text-xs text-muted-foreground mb-3">
Specify which indices to search. You can: Specify which indices to search. You can:
</p> </p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground"> <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>
<li>List specific indices: <code className="bg-muted px-1 py-0.5 rounded">logs-2024, documents-2024</code></li> Use wildcards: <code className="bg-muted px-1 py-0.5 rounded">logs-*</code>{" "}
<li>Leave empty to search all accessible indices (not recommended for performance)</li> 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> </ul>
</div> </div>
</div> </div>
@ -694,19 +765,30 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Query</h4> <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"> <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> </p>
</div> </div>
<div> <div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Fields</h4> <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"> <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> </p>
<ul className="list-disc pl-5 space-y-1 text-[10px] sm:text-xs text-muted-foreground"> <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>
<li><code className="bg-muted px-1 py-0.5 rounded">content</code> - Main content</li> <code className="bg-muted px-1 py-0.5 rounded">title</code> - Document
<li><code className="bg-muted px-1 py-0.5 rounded">description</code> - Descriptions</li> 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> </ul>
<p className="text-[10px] sm:text-xs text-muted-foreground mt-2"> <p className="text-[10px] sm:text-xs text-muted-foreground mt-2">
Leave empty to search all fields in your documents. Leave empty to search all fields in your documents.
@ -716,7 +798,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Maximum Documents</h4> <h4 className="text-[10px] sm:text-xs font-medium mb-2">Maximum Documents</h4>
<p className="text-[10px] sm:text-xs text-muted-foreground"> <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> </p>
</div> </div>
</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> <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"> <ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li> <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>
<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>
<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> </li>
</ul> </ul>
</div> </div>
<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"> <ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li> <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>
<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>
<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> </li>
</ul> </ul>
</div> </div>
@ -761,13 +855,16 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<h4 className="text-[10px] sm:text-xs font-medium mb-2">Search Issues</h4> <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"> <ul className="list-disc pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li> <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>
<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>
<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> </li>
</ul> </ul>
</div> </div>
@ -776,7 +873,9 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
<Info 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-[10px] sm:text-xs">Need More Help?</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Need More Help?</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -788,4 +887,3 @@ export const ElasticsearchConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

@ -57,10 +57,7 @@ const githubConnectorFormSchema = z.object({
type GithubConnectorFormValues = z.infer<typeof githubConnectorFormSchema>; type GithubConnectorFormValues = z.infer<typeof githubConnectorFormSchema>;
export const GithubConnectForm: FC<ConnectFormProps> = ({ export const GithubConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined); const [endDate, setEndDate] = useState<Date | undefined>(undefined);
@ -92,7 +89,7 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
isSubmittingRef.current = true; isSubmittingRef.current = true;
try { try {
const repoList = stringToArray(values.repo_full_names); const repoList = stringToArray(values.repo_full_names);
await onSubmit({ await onSubmit({
name: values.name, name: values.name,
connector_type: EnumConnectorName.GITHUB_CONNECTOR, connector_type: EnumConnectorName.GITHUB_CONNECTOR,
@ -122,7 +119,8 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<div className="-ml-1"> <div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Personal Access Token Required</AlertTitle> <AlertTitle className="text-xs sm:text-sm">Personal Access Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0"> <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 <a
href="https://github.com/settings/tokens" href="https://github.com/settings/tokens"
target="_blank" 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -145,11 +147,11 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My GitHub Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -167,16 +169,17 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">GitHub Personal Access Token</FormLabel> <FormLabel className="text-xs sm:text-sm">GitHub Personal Access Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="ghp_..." 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -190,15 +193,16 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Repository Names</FormLabel> <FormLabel className="text-xs sm:text-sm">Repository Names</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="owner/repo1, owner/repo2" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <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> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@ -222,7 +226,7 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -240,14 +244,24 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -285,7 +311,11 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -294,7 +324,10 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <li>
@ -303,7 +336,8 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<li>Large files (over 1MB) are skipped during indexing.</li> <li>Large files (over 1MB) are skipped during indexing.</li>
<li>Only specified repositories are indexed.</li> <li>Only specified repositories are indexed.</li>
<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> </li>
</ul> </ul>
</div> </div>
@ -313,15 +347,20 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3> <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"> <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" /> <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]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li> <li>
Go to your GitHub{" "} Go to your GitHub{" "}
@ -336,39 +375,46 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
</li> </li>
<li> <li>
Click on <strong>Personal access tokens</strong>, then choose{" "} 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>
<li> <li>
Click <strong>Generate new token</strong> (and choose the appropriate type). Click <strong>Generate new token</strong> (and choose the appropriate type).
</li> </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> <li>
Give your token a descriptive name (e.g., "SurfSense Connector"). Under <strong>Select scopes</strong> (for classic tokens) or{" "}
</li> <strong>Repository access</strong> (for fine-grained), grant the necessary
<li> permissions. At minimum, the <strong>`repo`</strong> scope (or equivalent
Set an expiration date for the token (recommended for security). read access to repositories for fine-grained tokens) is required to read
</li> repository content.
<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.
</li> </li>
<li> <li>
Click <strong>Generate token</strong>. Click <strong>Generate token</strong>.
</li> </li>
<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> </li>
</ol> </ol>
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Repository Access</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Repository Access</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -381,13 +427,15 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<li> <li>
Enter your <strong>GitHub Personal Access Token</strong> in the form field. Enter your <strong>GitHub Personal Access Token</strong> in the form field.
</li> </li>
<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>
<li> <li>
Click <strong>Connect</strong> to establish the connection. Click <strong>Connect</strong> to establish the connection.
@ -416,4 +464,3 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

@ -51,10 +51,7 @@ const jiraConnectorFormSchema = z.object({
type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>; type JiraConnectorFormValues = z.infer<typeof jiraConnectorFormSchema>;
export const JiraConnectForm: FC<ConnectFormProps> = ({ export const JiraConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -131,11 +132,11 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My Jira Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -153,12 +154,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Jira Base URL</FormLabel> <FormLabel className="text-xs sm:text-sm">Jira Base URL</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="url" type="url"
placeholder="https://your-domain.atlassian.net" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -176,13 +177,13 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Email Address</FormLabel> <FormLabel className="text-xs sm:text-sm">Email Address</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="email" type="email"
placeholder="your-email@example.com" placeholder="your-email@example.com"
autoComplete="email" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -200,12 +201,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">API Token</FormLabel> <FormLabel className="text-xs sm:text-sm">API Token</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="Your API Token" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -219,7 +220,7 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -237,14 +238,24 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -282,7 +305,11 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -291,14 +318,17 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <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>
<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> </li>
</ul> </ul>
</div> </div>
@ -308,15 +338,20 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3> <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"> <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" /> <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]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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>Log in to your Atlassian account</li>
<li> <li>
@ -343,15 +378,20 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -364,10 +404,12 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<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>
<li> <li>
Enter your <strong>Email Address</strong> associated with your Atlassian account Enter your <strong>Email Address</strong> associated with your Atlassian account
@ -404,4 +446,3 @@ export const JiraConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

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

View file

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

View file

@ -49,10 +49,7 @@ const lumaConnectorFormSchema = z.object({
type LumaConnectorFormValues = z.infer<typeof lumaConnectorFormSchema>; type LumaConnectorFormValues = z.infer<typeof lumaConnectorFormSchema>;
export const LumaConnectForm: FC<ConnectFormProps> = ({ export const LumaConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
onSubmit,
isSubmitting,
}) => {
const isSubmittingRef = useRef(false); const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined); const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = 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"> <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 {...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 <FormField
control={form.control} control={form.control}
name="name" name="name"
@ -125,11 +126,11 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel> <FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="My Luma Connector" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -147,12 +148,12 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<FormItem> <FormItem>
<FormLabel className="text-xs sm:text-sm">Luma API Key</FormLabel> <FormLabel className="text-xs sm:text-sm">Luma API Key</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="password" type="password"
placeholder="Your API Key" 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" 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} disabled={isSubmitting}
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormDescription className="text-[10px] sm:text-xs"> <FormDescription className="text-[10px] sm:text-xs">
@ -166,7 +167,7 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
{/* Indexing Configuration */} {/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20"> <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> <h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */} {/* Date Range Selector */}
<DateRangeSelector <DateRangeSelector
startDate={startDate} startDate={startDate}
@ -184,14 +185,24 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
Automatically re-index at regular intervals Automatically re-index at regular intervals
</p> </p>
</div> </div>
<Switch checked={periodicEnabled} onCheckedChange={setPeriodicEnabled} disabled={isSubmitting} /> <Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div> </div>
{periodicEnabled && ( {periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3"> <div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2"> <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">
<Select value={frequencyMinutes} onValueChange={setFrequencyMinutes} disabled={isSubmitting}> Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger <SelectTrigger
id="frequency" id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm" 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" /> <SelectValue placeholder="Select frequency" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="z-[100]"> <SelectContent className="z-[100]">
<SelectItem value="15" className="text-xs sm:text-sm">Every 15 minutes</SelectItem> <SelectItem value="15" className="text-xs sm:text-sm">
<SelectItem value="60" className="text-xs sm:text-sm">Every hour</SelectItem> Every 15 minutes
<SelectItem value="360" className="text-xs sm:text-sm">Every 6 hours</SelectItem> </SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">Every 12 hours</SelectItem> <SelectItem value="60" className="text-xs sm:text-sm">
<SelectItem value="1440" className="text-xs sm:text-sm">Daily</SelectItem> Every hour
<SelectItem value="10080" className="text-xs sm:text-sm">Weekly</SelectItem> </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> </SelectContent>
</Select> </Select>
</div> </div>
@ -229,7 +252,11 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
)} )}
{/* Documentation Section */} {/* 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"> <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"> <AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation Documentation
@ -238,14 +265,17 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<div> <div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3> <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"> <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> </p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1"> <ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li> <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>
<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> </li>
</ul> </ul>
</div> </div>
@ -257,13 +287,16 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<Info 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-[10px] sm:text-xs">API Key Required</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">API Key Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<div> <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"> <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>Log into your Luma account</li>
<li>Navigate to your account settings</li> <li>Navigate to your account settings</li>
@ -286,15 +319,20 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
</div> </div>
<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"> <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> </p>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle> <AlertTitle className="text-[10px] sm:text-xs">Data Privacy</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]"> <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> </AlertDescription>
</Alert> </Alert>
</div> </div>
@ -307,7 +345,8 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3> <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"> <ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li> <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>
<li> <li>
Place your <strong>API Key</strong> in the form field. Place your <strong>API Key</strong> in the form field.
@ -339,4 +378,3 @@ export const LumaConnectForm: FC<ConnectFormProps> = ({
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

@ -112,4 +112,3 @@ export function getConnectorBenefits(connectorType: string): string[] | null {
return benefits[connectorType] || 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 * Factory function to get the appropriate connect form component for a connector type
*/ */
export function getConnectFormComponent( export function getConnectFormComponent(connectorType: string): ConnectFormComponent | null {
connectorType: string
): ConnectFormComponent | null {
switch (connectorType) { switch (connectorType) {
case "TAVILY_API": case "TAVILY_API":
return TavilyApiConnectForm; return TavilyApiConnectForm;
@ -82,4 +80,3 @@ export function getConnectFormComponent(
return null; return null;
} }
} }

View file

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

View file

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

View file

@ -1,8 +1,9 @@
"use client"; "use client";
import { Copy, Webhook, Check } from "lucide-react"; import { Copy, Webhook, Check, Info } from "lucide-react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import type { FC } from "react"; import type { FC } from "react";
import { z } from "zod";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -14,13 +15,22 @@ export interface CirclebackConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void; onNameChange?: (name: string) => void;
} }
export const CirclebackConfig: FC<CirclebackConfigProps> = ({ // Type-safe schema for webhook info response
connector, const circlebackWebhookInfoSchema = z.object({
onNameChange, 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 [name, setName] = useState<string>(connector.name || "");
const [webhookUrl, setWebhookUrl] = useState<string>(""); 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 [isLoading, setIsLoading] = useState(true);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -33,19 +43,31 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
useEffect(() => { useEffect(() => {
const fetchWebhookInfo = async () => { const fetchWebhookInfo = async () => {
if (!connector.search_space_id) return; 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); setIsLoading(true);
try { try {
const response = await authenticatedFetch( 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) { if (response.ok) {
const data = await response.json(); const data: unknown = await response.json();
setWebhookInfo(data); // Runtime validation with zod schema
setWebhookUrl(data.webhook_url || ""); const validatedData = circlebackWebhookInfoSchema.parse(data);
setWebhookInfo(validatedData);
setWebhookUrl(validatedData.webhook_url);
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch webhook info:", error); console.error("Failed to fetch webhook info:", error);
// Reset state on error
setWebhookInfo(null);
setWebhookUrl("");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -107,11 +129,12 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
<Input <Input
value={webhookUrl} value={webhookUrl}
readOnly 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 <Button
type="button" type="button"
variant="outline" variant="secondary"
size="sm" size="sm"
onClick={handleCopyWebhookUrl} onClick={handleCopyWebhookUrl}
className="shrink-0" className="shrink-0"
@ -141,11 +164,12 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
{webhookInfo && ( {webhookInfo && (
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20"> <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> <AlertTitle className="text-xs sm:text-sm">Configuration Instructions</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0 mt-1"> <AlertDescription className="text-[10px] sm:text-xs !pl-0 mt-1">
Configure this URL in Circleback Settings Automations Create automation Send webhook request. Configure this URL in Circleback Settings Automations Create automation Send
The webhook will automatically send meeting notes, transcripts, and action items to this search space. webhook request. The webhook will automatically send meeting notes, transcripts, and
action items to this search space.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@ -153,4 +177,3 @@ export const CirclebackConfig: FC<CirclebackConfigProps> = ({
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

@ -271,7 +271,9 @@ export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
<div className="h-2.5 w-2.5 rounded-full bg-current" /> <div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator> </RadioGroup.Indicator>
</RadioGroup.Item> </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>
<div className="flex items-center space-x-2"> <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" /> <div className="h-2.5 w-2.5 rounded-full bg-current" />
</RadioGroup.Indicator> </RadioGroup.Indicator>
</RadioGroup.Item> </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> </div>
</RadioGroup.Root> </RadioGroup.Root>
@ -435,4 +439,3 @@ export const ElasticsearchConfig: FC<ElasticsearchConfigProps> = ({
</div> </div>
); );
}; };

View file

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

View file

@ -13,19 +13,21 @@ interface SelectedFolder {
name: string; name: string;
} }
export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
connector, // Initialize with existing selected folders and files from connector config
onConfigChange, const existingFolders =
}) => { (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
// Initialize with existing selected folders from connector config const existingFiles = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
const existingFolders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders); const [selectedFolders, setSelectedFolders] = useState<SelectedFolder[]>(existingFolders);
const [selectedFiles, setSelectedFiles] = useState<SelectedFolder[]>(existingFiles);
const [showFolderSelector, setShowFolderSelector] = useState(false); const [showFolderSelector, setShowFolderSelector] = useState(false);
// Update selected folders when connector config changes // Update selected folders and files when connector config changes
useEffect(() => { useEffect(() => {
const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || []; const folders = (connector.config?.selected_folders as SelectedFolder[] | undefined) || [];
const files = (connector.config?.selected_files as SelectedFolder[] | undefined) || [];
setSelectedFolders(folders); setSelectedFolders(folders);
setSelectedFiles(files);
}, [connector.config]); }, [connector.config]);
const handleSelectFolders = (folders: SelectedFolder[]) => { const handleSelectFolders = (folders: SelectedFolder[]) => {
@ -35,28 +37,61 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
onConfigChange({ onConfigChange({
...connector.config, ...connector.config,
selected_folders: folders, 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 ( 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="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"> <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"> <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> </p>
</div> </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"> <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"> <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> </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) => ( {selectedFolders.map((folder) => (
<p key={folder.id} className="text-xs sm:text-sm text-muted-foreground truncate" title={folder.name}> <p
{folder.name} 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> </p>
))} ))}
</div> </div>
@ -69,6 +104,8 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
connectorId={connector.id} connectorId={connector.id}
selectedFolders={selectedFolders} selectedFolders={selectedFolders}
onSelectFolders={handleSelectFolders} onSelectFolders={handleSelectFolders}
selectedFiles={selectedFiles}
onSelectFiles={handleSelectFiles}
/> />
<Button <Button
type="button" type="button"
@ -87,17 +124,17 @@ export const GoogleDriveConfig: FC<ConnectorConfigProps> = ({
onClick={() => setShowFolderSelector(true)} 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" 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> </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"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0" />
<AlertDescription className="text-[10px] sm:text-xs !pl-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> </AlertDescription>
</Alert> </Alert>
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,15 +34,9 @@ export const SearxngConfig: FC<SearxngConfigProps> = ({
onConfigChange, onConfigChange,
onNameChange, onNameChange,
}) => { }) => {
const [host, setHost] = useState<string>( const [host, setHost] = useState<string>((connector.config?.SEARXNG_HOST as 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 [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>( const [categories, setCategories] = useState<string>(
arrayToString(connector.config?.SEARXNG_CATEGORIES) 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" className="border-slate-400/20 focus-visible:border-slate-400/40"
/> />
<p className="text-[10px] sm:text-xs text-muted-foreground"> <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> </p>
</div> </div>
</div> </div>
@ -319,4 +314,3 @@ export const SearxngConfig: FC<SearxngConfigProps> = ({
</div> </div>
); );
}; };

View file

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

View file

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

View file

@ -10,14 +10,11 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import type { ConnectorConfigProps } from "../index"; import type { ConnectorConfigProps } from "../index";
export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({ connector, onConfigChange }) => {
connector,
onConfigChange,
}) => {
// Initialize with existing config values // Initialize with existing config values
const existingApiKey = (connector.config?.FIRECRAWL_API_KEY as string | undefined) || ""; const existingApiKey = (connector.config?.FIRECRAWL_API_KEY as string | undefined) || "";
const existingUrls = (connector.config?.INITIAL_URLS as string | undefined) || ""; const existingUrls = (connector.config?.INITIAL_URLS as string | undefined) || "";
const [apiKey, setApiKey] = useState(existingApiKey); const [apiKey, setApiKey] = useState(existingApiKey);
const [initialUrls, setInitialUrls] = useState(existingUrls); const [initialUrls, setInitialUrls] = useState(existingUrls);
const [showApiKey, setShowApiKey] = useState(false); const [showApiKey, setShowApiKey] = useState(false);
@ -43,9 +40,11 @@ export const WebcrawlerConfig: FC<ConnectorConfigProps> = ({
const handleUrlsChange = (value: string) => { const handleUrlsChange = (value: string) => {
setInitialUrls(value); setInitialUrls(value);
if (onConfigChange) { if (onConfigChange) {
// Preserve newlines for multi-line URL input
// Backend will handle trimming individual URLs when splitting by newline
onConfigChange({ onConfigChange({
...connector.config, ...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"> <div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Web Crawler Configuration</h3> <h3 className="font-medium text-sm sm:text-base">Web Crawler Configuration</h3>
<p className="text-xs sm:text-sm text-muted-foreground"> <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> </p>
</div> </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"> <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" /> <Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0" />
<AlertDescription className="text-[10px] sm:text-xs !pl-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> </AlertDescription>
</Alert> </Alert>
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,19 +1,20 @@
"use client"; "use client";
import { format } from "date-fns"; 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 type { FC } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon"; import { getDocumentTypeLabel } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentTypeIcon";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { SearchSourceConnector } from "@/contracts/types/connector.types"; import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types"; import type { LogSummary, LogActiveTask } from "@/contracts/types/log.types";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import { getDocumentCountForConnector } from "../utils/connector-document-mapping";
TabsContent, import { TabsContent } from "@/components/ui/tabs";
} from "@/components/ui/tabs";
interface ActiveConnectorsTabProps { interface ActiveConnectorsTabProps {
searchQuery: string;
hasSources: boolean; hasSources: boolean;
totalSourceCount: number; totalSourceCount: number;
activeDocumentTypes: Array<[string, number]>; activeDocumentTypes: Array<[string, number]>;
@ -26,107 +27,186 @@ interface ActiveConnectorsTabProps {
} }
export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
searchQuery,
hasSources, hasSources,
activeDocumentTypes, activeDocumentTypes,
connectors, connectors,
indexingConnectorIds, indexingConnectorIds,
logsSummary, logsSummary,
searchSpaceId,
onTabChange, onTabChange,
onManage, 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 ( return (
<TabsContent value="active" className="m-0"> <TabsContent value="active" className="m-0">
{hasSources ? ( {hasSources ? (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-2 mb-4"> {/* Active Connectors Section */}
<h3 className="text-sm font-semibold text-muted-foreground"> {filteredConnectors.length > 0 && (
Currently Active <div className="space-y-4">
</h3> <div className="flex items-center gap-2">
</div> <h3 className="text-sm font-semibold text-muted-foreground">Active Connectors</h3>
<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>
</div> </div>
))} <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{connectors.map((connector) => { {filteredConnectors.map((connector) => {
const isIndexing = indexingConnectorIds.has(connector.id); const isIndexing = indexingConnectorIds.has(connector.id);
const activeTask = logsSummary?.active_tasks?.find( const activeTask = logsSummary?.active_tasks?.find(
(task: LogActiveTask) => task.connector_id === connector.id (task: LogActiveTask) => task.connector_id === connector.id
); );
const documentCount = getDocumentCountForConnector(
connector.connector_type,
documentTypeCounts
);
return ( return (
<div <div
key={`connector-${connector.id}`} key={`connector-${connector.id}`}
className={cn( className={cn(
"flex items-center gap-4 p-4 rounded-xl border border-border transition-all", "flex items-center gap-4 p-4 rounded-xl border border-border transition-all",
isIndexing isIndexing
? "bg-primary/5 border-primary/20" ? "bg-primary/5 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10" : "bg-slate-400/5 dark:bg-white/5 hover:bg-slate-400/10 dark:hover:bg-white/10"
)} )}
> >
<div <div
className={cn( className={cn(
"flex h-12 w-12 items-center justify-center rounded-lg border", "flex h-12 w-12 items-center justify-center rounded-lg border",
isIndexing isIndexing
? "bg-primary/10 border-primary/20" ? "bg-primary/10 border-primary/20"
: "bg-slate-400/5 dark:bg-white/5 border-slate-400/5 dark:border-white/5" : "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>
)} )}
</p> >
) : ( {getConnectorIcon(connector.connector_type, "size-6")}
<p className="text-[11px] text-muted-foreground mt-1"> </div>
{connector.last_indexed_at <div className="flex-1 min-w-0">
? `Last indexed: ${format(new Date(connector.last_indexed_at), "MMM d, yyyy")}` <p className="text-[14px] font-semibold leading-tight truncate">
: "Never indexed"} {connector.name}
</p> </p>
)} {isIndexing ? (
</div> <p className="text-[11px] text-primary mt-1 flex items-center gap-1.5">
<Button <Loader2 className="size-3 animate-spin" />
variant="secondary" Indexing...
size="sm" {activeTask?.message && (
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" <span className="text-muted-foreground truncate max-w-[150px]">
onClick={onManage ? () => onManage(connector) : undefined} {activeTask.message}
disabled={isIndexing} </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"} <div className="flex items-center justify-center">
</Button> {getConnectorIcon(doc.type, "size-3.5")}
</div> </div>
); <span className="text-[12px] font-medium">{doc.label}</span>
})} <span className="text-[11px] text-muted-foreground">
</div> {formatDocumentCount(doc.count)}
</span>
</div>
))}
</div>
</div>
)}
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-20 text-center"> <div className="flex flex-col items-center justify-center py-20 text-center">
@ -149,4 +229,3 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</TabsContent> </TabsContent>
); );
}; };

View file

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

View file

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

View file

@ -22,10 +22,7 @@ interface YouTubeCrawlerViewProps {
onBack: () => void; onBack: () => void;
} }
export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({ searchSpaceId, onBack }) => {
searchSpaceId,
onBack,
}) => {
const t = useTranslations("add_youtube"); const t = useTranslations("add_youtube");
const router = useRouter(); const router = useRouter();
const [videoTags, setVideoTags] = useState<TagType[]>([]); const [videoTags, setVideoTags] = useState<TagType[]>([]);
@ -133,12 +130,8 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
{getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")} {getConnectorIcon(EnumConnectorName.YOUTUBE_CONNECTOR, "h-7 w-7")}
</div> </div>
<div> <div>
<h2 className="text-xl sm:text-2xl font-semibold tracking-tight"> <h2 className="text-xl sm:text-2xl font-semibold tracking-tight">{t("title")}</h2>
{t("title")} <p className="text-xs sm:text-base text-muted-foreground mt-1">{t("subtitle")}</p>
</h2>
<p className="text-xs sm:text-base text-muted-foreground mt-1">
{t("subtitle")}
</p>
</div> </div>
</div> </div>
</div> </div>
@ -159,7 +152,8 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
styleClasses={{ styleClasses={{
inlineTagsContainer: 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", "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: { tag: {
body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex", body: "h-7 relative bg-background border border-input hover:bg-background rounded-md font-medium text-xs ps-2 pe-7 flex",
closeButton: closeButton:
@ -172,11 +166,7 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
<p className="text-xs text-muted-foreground mt-1">{t("hint")}</p> <p className="text-xs text-muted-foreground mt-1">{t("hint")}</p>
</div> </div>
{error && ( {error && <div className="text-sm text-red-500 mt-2">{error}</div>}
<div className="text-sm text-red-500 mt-2">
{error}
</div>
)}
<div className="bg-muted/50 rounded-lg p-4 text-sm"> <div className="bg-muted/50 rounded-lg p-4 text-sm">
<h4 className="font-medium mb-2">{t("tips_title")}</h4> <h4 className="font-medium mb-2">{t("tips_title")}</h4>
@ -244,4 +234,3 @@ export const YouTubeCrawlerView: FC<YouTubeCrawlerViewProps> = ({
</div> </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> </MessagePrimitive.Root>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -223,9 +223,17 @@ export function GoogleDriveFolderTree({
const childFiles = children?.filter((c) => !c.isFolder) || []; const childFiles = children?.filter((c) => !c.isFolder) || [];
const indentSize = 0.75; // Smaller indent for mobile const indentSize = 0.75; // Smaller indent for mobile
return ( 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 <div
className={cn( 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", "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); 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()} 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"> <div className="shrink-0">
{isFolder ? ( {isFolder ? (
@ -310,7 +310,9 @@ export function GoogleDriveFolderTree({
{childFiles.map((child) => renderItem(child, level + 1))} {childFiles.map((child) => renderItem(child, level + 1))}
{children.length === 0 && ( {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> </div>
)} )}
@ -319,15 +321,15 @@ export function GoogleDriveFolderTree({
}; };
return ( 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"> <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="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"> <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 <Checkbox
checked={isFolderSelected("root")} checked={isFolderSelected("root")}
onCheckedChange={() => toggleFolderSelection("root", "My Drive")} 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" /> <HardDrive className="h-3 w-3 sm:h-4 sm:w-4 text-primary shrink-0" />
<button <button

View file

@ -97,7 +97,6 @@ export function DashboardBreadcrumb() {
const sectionLabels: Record<string, string> = { const sectionLabels: Record<string, string> = {
"new-chat": t("chat") || "Chat", "new-chat": t("chat") || "Chat",
documents: t("documents"), documents: t("documents"),
connectors: t("connectors"),
logs: t("logs"), logs: t("logs"),
settings: t("settings"), settings: t("settings"),
editor: t("editor"), editor: t("editor"),
@ -156,62 +155,12 @@ export function DashboardBreadcrumb() {
return breadcrumbs; 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 // Handle other sub-sections
let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1); let subSectionLabel = subSection.charAt(0).toUpperCase() + subSection.slice(1);
const subSectionLabels: Record<string, string> = { const subSectionLabels: Record<string, string> = {
upload: t("upload_documents"), upload: t("upload_documents"),
youtube: t("add_youtube"), youtube: t("add_youtube"),
webpage: t("add_webpages"), webpage: t("add_webpages"),
add: t("add_connector"),
edit: t("edit_connector"),
manage: t("manage"), manage: t("manage"),
}; };

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; 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 { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { import {
@ -31,7 +31,6 @@ import {
useSidebar, useSidebar,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { useLogsSummary } from "@/hooks/use-logs"; import { useLogsSummary } from "@/hooks/use-logs";
import { useIsMobile } from "@/hooks/use-mobile";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { AllNotesSidebar } from "./all-notes-sidebar"; import { AllNotesSidebar } from "./all-notes-sidebar";
@ -55,7 +54,6 @@ interface NavNotesProps {
onAddNote?: () => void; onAddNote?: () => void;
defaultOpen?: boolean; defaultOpen?: boolean;
searchSpaceId?: string; searchSpaceId?: string;
isSourcesExpanded?: boolean;
} }
// Map of icon names to their components // Map of icon names to their components
@ -65,17 +63,10 @@ const actionIconMap: Record<string, LucideIcon> = {
MoreHorizontal, MoreHorizontal,
}; };
export function NavNotes({ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) {
notes,
onAddNote,
defaultOpen = true,
searchSpaceId,
isSourcesExpanded = false,
}: NavNotesProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const isMobile = useIsMobile();
const { setOpenMobile } = useSidebar(); const { setOpenMobile } = useSidebar();
const [isDeleting, setIsDeleting] = useState<number | null>(null); const [isDeleting, setIsDeleting] = useState<number | null>(null);
const [isOpen, setIsOpen] = useState(defaultOpen); const [isOpen, setIsOpen] = useState(defaultOpen);
@ -98,13 +89,6 @@ export function NavNotes({
); );
}, [summary?.active_tasks]); }, [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 // Handle note deletion with loading state
const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => { const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => {
setIsDeleting(noteId); 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 { AnimatePresence, motion } from "motion/react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCallback, useState } from "react"; import { useCallback, useMemo, useState, useRef } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { toast } from "sonner"; import { toast } from "sonner";
import { uploadDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; 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 { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -24,115 +30,106 @@ import { GridPattern } from "./GridPattern";
interface DocumentUploadTabProps { interface DocumentUploadTabProps {
searchSpaceId: string; 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 t = useTranslations("upload_documents");
const router = useRouter(); const router = useRouter();
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [uploadProgress, setUploadProgress] = useState(0); const [uploadProgress, setUploadProgress] = useState(0);
// Use the uploadDocumentMutationAtom
const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom); const [uploadDocumentMutation] = useAtom(uploadDocumentMutationAtom);
const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation; const { mutate: uploadDocuments, isPending: isUploading } = uploadDocumentMutation;
const fileInputRef = useRef<HTMLInputElement>(null);
const audioFileTypes = { const acceptedFileTypes = useMemo(() => {
"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 etlService = process.env.NEXT_PUBLIC_ETL_SERVICE; const etlService = process.env.NEXT_PUBLIC_ETL_SERVICE;
return FILE_TYPE_CONFIG[etlService || "default"] || FILE_TYPE_CONFIG.default;
}, []);
if (etlService === "LLAMACLOUD") { const supportedExtensions = useMemo(
return { () => Array.from(new Set(Object.values(acceptedFileTypes).flat())).sort(),
"application/pdf": [".pdf"], [acceptedFileTypes]
"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 onDrop = useCallback((acceptedFiles: File[]) => { const onDrop = useCallback((acceptedFiles: File[]) => {
setFiles((prevFiles) => [...prevFiles, ...acceptedFiles]); setFiles((prev) => [...prev, ...acceptedFiles]);
}, []); }, []);
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
@ -140,12 +137,12 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
accept: acceptedFileTypes, accept: acceptedFileTypes,
maxSize: 50 * 1024 * 1024, maxSize: 50 * 1024 * 1024,
noClick: false, noClick: false,
noKeyboard: false,
}); });
const removeFile = (index: number) => { // Handle file input click to prevent event bubbling that might reopen dialog
setFiles((prevFiles) => prevFiles.filter((_, i) => i !== index)); const handleFileInputClick = useCallback((e: React.MouseEvent<HTMLInputElement>) => {
}; e.stopPropagation();
}, []);
const formatFileSize = (bytes: number) => { const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
@ -155,114 +152,93 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`; return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
}; };
const totalFileSize = files.reduce((total, file) => total + file.size, 0);
const handleUpload = async () => { const handleUpload = async () => {
setUploadProgress(0); 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(() => { const progressInterval = setInterval(() => {
setUploadProgress((prev) => { setUploadProgress((prev) => (prev >= 90 ? prev : prev + Math.random() * 10));
if (prev >= 90) return prev;
return prev + Math.random() * 10;
});
}, 200); }, 200);
// Use the mutation to upload documents
uploadDocuments( uploadDocuments(
{ { files, search_space_id: Number(searchSpaceId) },
files,
search_space_id: Number(searchSpaceId),
},
{ {
onSuccess: () => { onSuccess: () => {
clearInterval(progressInterval); clearInterval(progressInterval);
setUploadProgress(100); setUploadProgress(100);
// Track upload success
trackDocumentUploadSuccess(Number(searchSpaceId), files.length); trackDocumentUploadSuccess(Number(searchSpaceId), files.length);
toast(t("upload_initiated"), { description: t("upload_initiated_desc") });
toast(t("upload_initiated"), { onSuccess?.() || router.push(`/dashboard/${searchSpaceId}/documents`);
description: t("upload_initiated_desc"),
});
router.push(`/dashboard/${searchSpaceId}/documents`);
}, },
onError: (error: any) => { onError: (error: unknown) => {
clearInterval(progressInterval); clearInterval(progressInterval);
setUploadProgress(0); setUploadProgress(0);
const message = error instanceof Error ? error.message : "Upload failed";
// Track upload failure trackDocumentUploadFailure(Number(searchSpaceId), message);
trackDocumentUploadFailure(Number(searchSpaceId), error.message || "Upload failed");
toast(t("upload_error"), { 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 ( return (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }} 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" /> <Info className="h-4 w-4" />
<AlertDescription className="text-xs sm:text-sm">{t("file_size_limit")}</AlertDescription> <AlertDescription className="text-xs sm:text-sm">{t("file_size_limit")}</AlertDescription>
</Alert> </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"> <div className="absolute inset-0 [mask-image:radial-gradient(ellipse_at_center,white,transparent)] opacity-30">
<GridPattern /> <GridPattern />
</div> </div>
<CardContent className="p-4 sm:p-10 relative z-10">
<CardContent className="p-10 relative z-10">
<div <div
{...getRootProps()} {...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 ? ( {isDragActive ? (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} 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" /> <Upload className="h-8 w-8 sm:h-12 sm:w-12 text-primary" />
<p className="text-lg font-medium text-primary">{t("drop_files")}</p> <p className="text-sm sm:text-lg font-medium text-primary">{t("drop_files")}</p>
</motion.div> </motion.div>
) : ( ) : (
<motion.div <div className="flex flex-col items-center gap-2 sm:gap-4">
initial={{ opacity: 0 }} <Upload className="h-8 w-8 sm:h-12 sm:w-12 text-muted-foreground" />
animate={{ opacity: 1 }}
className="flex flex-col items-center gap-4"
>
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="text-center"> <div className="text-center">
<p className="text-base sm:text-lg font-medium">{t("drag_drop")}</p> <p className="text-sm 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-xs sm:text-sm text-muted-foreground mt-1">{t("or_browse")}</p>
</div> </div>
</motion.div> </div>
)} )}
<div className="mt-2 sm:mt-4">
<div className="mt-4">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-xs sm:text-sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
const input = document.querySelector('input[type="file"]') as HTMLInputElement; e.preventDefault();
if (input) input.click(); fileInputRef.current?.click();
}} }}
> >
{t("browse_files")} {t("browse_files")}
@ -280,20 +256,21 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
exit={{ opacity: 0, height: 0 }} exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
> >
<Card> <Card className={cardClass}>
<CardHeader> <CardHeader className="p-4 sm:p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-2">
<div> <div className="min-w-0 flex-1">
<CardTitle className="text-lg sm:text-2xl"> <CardTitle className="text-base sm:text-2xl">
{t("selected_files", { count: files.length })} {t("selected_files", { count: files.length })}
</CardTitle> </CardTitle>
<CardDescription className="text-xs sm:text-sm"> <CardDescription className="text-xs sm:text-sm">
{t("total_size")}: {formatFileSize(getTotalFileSize())} {t("total_size")}: {formatFileSize(totalFileSize)}
</CardDescription> </CardDescription>
</div> </div>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-xs sm:text-sm shrink-0"
onClick={() => setFiles([])} onClick={() => setFiles([])}
disabled={isUploading} disabled={isUploading}
> >
@ -301,8 +278,8 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="p-4 sm:p-6 pt-0">
<div className="space-y-3 max-h-[400px] overflow-y-auto"> <div className="space-y-2 sm:space-y-3 max-h-[250px] sm:max-h-[400px] overflow-y-auto">
<AnimatePresence> <AnimatePresence>
{files.map((file, index) => ( {files.map((file, index) => (
<motion.div <motion.div
@ -310,7 +287,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }} 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"> <div className="flex items-center gap-3 flex-1 min-w-0">
<FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" /> <FileType className="h-5 w-5 text-muted-foreground flex-shrink-0" />
@ -329,7 +306,7 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => removeFile(index)} onClick={() => setFiles((prev) => prev.filter((_, i) => i !== index))}
disabled={isUploading} disabled={isUploading}
className="h-8 w-8" className="h-8 w-8"
> >
@ -344,11 +321,11 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
<motion.div <motion.div
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} 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="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>{t("uploading_files")}</span>
<span>{Math.round(uploadProgress)}%</span> <span>{Math.round(uploadProgress)}%</span>
</div> </div>
@ -358,23 +335,23 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
)} )}
<motion.div <motion.div
className="mt-6" className="mt-3 sm:mt-6"
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
> >
<Button <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} onClick={handleUpload}
disabled={isUploading || files.length === 0} disabled={isUploading || files.length === 0}
> >
{isUploading ? ( {isUploading ? (
<span className="flex items-center gap-2"> <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")} {t("uploading")}
</span> </span>
) : ( ) : (
<span className="flex items-center gap-2"> <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 })} {t("upload_button", { count: files.length })}
</span> </span>
)} )}
@ -386,24 +363,36 @@ export function DocumentUploadTab({ searchSpaceId }: DocumentUploadTabProps) {
)} )}
</AnimatePresence> </AnimatePresence>
<Card> <Accordion
<CardHeader> type="single"
<CardTitle className="flex items-center gap-2"> collapsible
<Tag className="h-5 w-5" /> className={`w-full ${cardClass} border border-border rounded-lg`}
{t("supported_file_types")} >
</CardTitle> <AccordionItem value="supported-file-types" className="border-0">
<CardDescription>{t("file_types_desc")}</CardDescription> <AccordionTrigger className="px-3 sm:px-6 py-3 sm:py-4 hover:no-underline">
</CardHeader> <div className="flex items-center gap-2">
<CardContent> <Tag className="h-4 w-4 sm:h-5 sm:w-5 shrink-0" />
<div className="flex flex-wrap gap-2"> <div className="text-left min-w-0">
{supportedExtensions.map((ext) => ( <div className="font-semibold text-sm sm:text-base">
<Badge key={ext} variant="outline" className="text-xs"> {t("supported_file_types")}
{ext} </div>
</Badge> <div className="text-xs sm:text-sm text-muted-foreground font-normal">
))} {t("file_types_desc")}
</div> </div>
</CardContent> </div>
</Card> </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> </motion.div>
); );
} }

View file

@ -2,7 +2,7 @@ export function GridPattern() {
const columns = 41; const columns = 41;
const rows = 11; const rows = 11;
return ( 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: rows }).map((_, row) =>
Array.from({ length: columns }).map((_, col) => { Array.from({ length: columns }).map((_, col) => {
const index = row * columns + col; const index = row * columns + col;
@ -11,8 +11,8 @@ export function GridPattern() {
key={`${col}-${row}`} key={`${col}-${row}`}
className={`w-10 h-10 flex flex-shrink-0 rounded-[2px] ${ className={`w-10 h-10 flex flex-shrink-0 rounded-[2px] ${
index % 2 === 0 index % 2 === 0
? "bg-gray-50 dark:bg-neutral-950" ? "bg-slate-200/20 dark:bg-slate-400/10"
: "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-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 { import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react";
IconLinkPlus,
IconSparkles,
IconUsersGroup,
} from "@tabler/icons-react";
import { import {
File, File,
FileText, FileText,
@ -19,32 +15,18 @@ import { EnumConnectorName } from "./connector";
export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => { export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => {
const iconProps = { className: className || "h-4 w-4" }; const iconProps = { className: className || "h-4 w-4" };
const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 }; 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) { switch (connectorType) {
case EnumConnectorName.LINKUP_API: case EnumConnectorName.LINKUP_API:
return <IconLinkPlus {...iconProps} />; return <IconLinkPlus {...iconProps} />;
case EnumConnectorName.LINEAR_CONNECTOR: 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: 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: case EnumConnectorName.TAVILY_API:
return <Image src="/connectors/tavily.svg" alt="Tavily" {...imgProps} />; return <Image src="/connectors/tavily.svg" alt="Tavily" {...imgProps} />;
case EnumConnectorName.SEARXNG_API: case EnumConnectorName.SEARXNG_API:
return <Globe {...iconProps} />; return <Image src="/connectors/searxng.svg" alt="SearXNG" {...imgProps} />;
case EnumConnectorName.BAIDU_SEARCH_API: case EnumConnectorName.BAIDU_SEARCH_API:
return <Image src="/connectors/baidu-search.svg" alt="Baidu" {...imgProps} />; return <Image src="/connectors/baidu-search.svg" alt="Baidu" {...imgProps} />;
case EnumConnectorName.SLACK_CONNECTOR: case EnumConnectorName.SLACK_CONNECTOR:
@ -56,11 +38,11 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.JIRA_CONNECTOR: case EnumConnectorName.JIRA_CONNECTOR:
return <Image src="/connectors/jira.svg" alt="Jira" {...imgProps} />; return <Image src="/connectors/jira.svg" alt="Jira" {...imgProps} />;
case EnumConnectorName.GOOGLE_CALENDAR_CONNECTOR: 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: 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: 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: case EnumConnectorName.AIRTABLE_CONNECTOR:
return <Image src="/connectors/airtable.svg" alt="Airtable" {...imgProps} />; return <Image src="/connectors/airtable.svg" alt="Airtable" {...imgProps} />;
case EnumConnectorName.CONFLUENCE_CONNECTOR: case EnumConnectorName.CONFLUENCE_CONNECTOR:
@ -70,7 +52,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.CLICKUP_CONNECTOR: case EnumConnectorName.CLICKUP_CONNECTOR:
return <Image src="/connectors/clickup.svg" alt="ClickUp" {...imgProps} />; return <Image src="/connectors/clickup.svg" alt="ClickUp" {...imgProps} />;
case EnumConnectorName.LUMA_CONNECTOR: case EnumConnectorName.LUMA_CONNECTOR:
return <IconSparkles {...iconProps} />; return <Image src="/connectors/luma.svg" alt="Luma" {...imgProps} />;
case EnumConnectorName.ELASTICSEARCH_CONNECTOR: case EnumConnectorName.ELASTICSEARCH_CONNECTOR:
return <Image src="/connectors/elasticsearch.svg" alt="Elasticsearch" {...imgProps} />; return <Image src="/connectors/elasticsearch.svg" alt="Elasticsearch" {...imgProps} />;
case EnumConnectorName.WEBCRAWLER_CONNECTOR: case EnumConnectorName.WEBCRAWLER_CONNECTOR:
@ -96,6 +78,8 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
return <Image src="/connectors/zoom.svg" alt="Zoom" {...imgProps} />; return <Image src="/connectors/zoom.svg" alt="Zoom" {...imgProps} />;
case "FILE": case "FILE":
return <File {...iconProps} />; return <File {...iconProps} />;
case "GOOGLE_DRIVE_FILE":
return <File {...iconProps} />;
case "NOTE": case "NOTE":
return <FileText {...iconProps} />; return <FileText {...iconProps} />;
case "EXTENSION": case "EXTENSION":

View file

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

View file

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

View file

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

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"> <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>
.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>

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"> <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>
.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>

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