mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-04 13:22:41 +02:00
feat: Refactor document upload and connector management UI, enhance user experience with improved loading states, and streamline source addition process by removing unnecessary components.
This commit is contained in:
parent
4d6186a43a
commit
3ac806dcdf
4 changed files with 221 additions and 220 deletions
|
|
@ -38,7 +38,6 @@ import {
|
|||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -348,221 +347,213 @@ export default function ConnectorsPage() {
|
|||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>{t("your_connectors")}</CardTitle>
|
||||
<CardDescription>{t("view_manage")}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{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 && (
|
||||
{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={() => handleOpenPeriodicDialog(connector.id)}
|
||||
onClick={() => handleQuickIndexConnector(connector.id)}
|
||||
disabled={indexingConnectorId === connector.id}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="sr-only">Configure Periodic Indexing</span>
|
||||
{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>Configure Periodic Indexing</p>
|
||||
<p>{t("quick_index_auto")}</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>
|
||||
</div>
|
||||
)}
|
||||
{connector.is_indexable && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-destructive-foreground hover:bg-destructive/10"
|
||||
onClick={() => setConnectorToDelete(connector.id)}
|
||||
onClick={() => handleOpenPeriodicDialog(connector.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="sr-only">{tCommon("delete")}</span>
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="sr-only">Configure Periodic Indexing</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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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}>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,36 @@
|
|||
"use client";
|
||||
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { Upload } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
|
||||
|
||||
export default function UploadDocumentsRedirect() {
|
||||
export default function UploadDocumentsPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const search_space_id = params.search_space_id as string;
|
||||
|
||||
useEffect(() => {
|
||||
router.replace(`/dashboard/${search_space_id}/sources/add?tab=documents`);
|
||||
}, [search_space_id, router]);
|
||||
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>
|
||||
|
||||
return null;
|
||||
{/* Document Upload */}
|
||||
<DocumentUploadTab searchSpaceId={search_space_id} />
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { IconBrandYoutube } from "@tabler/icons-react";
|
||||
import { Cable, Database, Globe, Upload } from "lucide-react";
|
||||
import { Cable, Database, Globe } from "lucide-react";
|
||||
import { motion } from "motion/react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ConnectorsTab } from "@/components/sources/ConnectorsTab";
|
||||
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
|
||||
import { YouTubeTab } from "@/components/sources/YouTubeTab";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { trackSourcesTabViewed } from "@/lib/posthog/events";
|
||||
|
|
@ -16,12 +15,12 @@ export default function AddSourcesPage() {
|
|||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const search_space_id = params.search_space_id as string;
|
||||
const [activeTab, setActiveTab] = useState("documents");
|
||||
const [activeTab, setActiveTab] = useState("youtube");
|
||||
|
||||
// Handle tab from query parameter
|
||||
useEffect(() => {
|
||||
const tabParam = searchParams.get("tab");
|
||||
if (tabParam && ["documents", "youtube", "connectors"].includes(tabParam)) {
|
||||
if (tabParam && ["youtube", "connectors"].includes(tabParam)) {
|
||||
setActiveTab(tabParam);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
|
@ -62,12 +61,7 @@ export default function AddSourcesPage() {
|
|||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
|
||||
<TabsList className="grid w-full max-w-3xl mx-auto grid-cols-4 h-12">
|
||||
<TabsTrigger value="documents" className="flex items-center gap-2">
|
||||
<Upload className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Documents</span>
|
||||
<span className="sm:hidden">Docs</span>
|
||||
</TabsTrigger>
|
||||
<TabsList className="grid w-full max-w-2xl mx-auto grid-cols-3 h-12">
|
||||
<TabsTrigger value="youtube" className="flex items-center gap-2">
|
||||
<IconBrandYoutube className="h-4 w-4" />
|
||||
YouTube
|
||||
|
|
@ -85,10 +79,6 @@ export default function AddSourcesPage() {
|
|||
</TabsList>
|
||||
|
||||
<div className="mt-8">
|
||||
<TabsContent value="documents" className="space-y-6">
|
||||
<DocumentUploadTab searchSpaceId={search_space_id} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="youtube" className="space-y-6">
|
||||
<YouTubeTab searchSpaceId={search_space_id} />
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -325,7 +325,7 @@ export const ComposerAddAttachment: FC = () => {
|
|||
const chatAttachmentInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileUpload = () => {
|
||||
router.push(`/dashboard/${searchSpaceId}/sources/add?tab=documents`);
|
||||
router.push(`/dashboard/${searchSpaceId}/documents/upload`);
|
||||
};
|
||||
|
||||
const handleChatAttachment = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue