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:
Anish Sarkar 2025-12-31 16:47:19 +05:30
parent 4d6186a43a
commit 3ac806dcdf
4 changed files with 221 additions and 220 deletions

View file

@ -38,7 +38,6 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar"; import { Calendar } from "@/components/ui/calendar";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -348,221 +347,213 @@ export default function ConnectorsPage() {
</Button> </Button>
</motion.div> </motion.div>
<Card> {isLoading ? (
<CardHeader className="pb-3"> <div className="flex justify-center py-8">
<CardTitle>{t("your_connectors")}</CardTitle> <div className="animate-pulse text-center">
<CardDescription>{t("view_manage")}</CardDescription> <div className="h-6 w-32 bg-muted rounded mx-auto mb-2"></div>
</CardHeader> <div className="h-4 w-48 bg-muted rounded mx-auto"></div>
<CardContent> </div>
{isLoading ? ( </div>
<div className="flex justify-center py-8"> ) : connectors.length === 0 ? (
<div className="animate-pulse text-center"> <div className="text-center py-12">
<div className="h-6 w-32 bg-muted rounded mx-auto mb-2"></div> <h3 className="text-lg font-medium mb-2">{t("no_connectors")}</h3>
<div className="h-4 w-48 bg-muted rounded mx-auto"></div> <p className="text-muted-foreground mb-6">{t("no_connectors_desc")}</p>
</div> <Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}>
</div> <Plus className="mr-2 h-4 w-4" />
) : connectors.length === 0 ? ( {t("add_first")}
<div className="text-center py-12"> </Button>
<h3 className="text-lg font-medium mb-2">{t("no_connectors")}</h3> </div>
<p className="text-muted-foreground mb-6">{t("no_connectors_desc")}</p> ) : (
<Button onClick={() => router.push(`/dashboard/${searchSpaceId}/connectors/add`)}> <div className="rounded-md border">
<Plus className="mr-2 h-4 w-4" /> <Table>
{t("add_first")} <TableHeader>
</Button> <TableRow>
</div> <TableHead>{t("name")}</TableHead>
) : ( <TableHead>{t("type")}</TableHead>
<div className="rounded-md border"> <TableHead>{t("last_indexed")}</TableHead>
<Table> <TableHead>{t("periodic")}</TableHead>
<TableHeader> <TableHead className="text-right">{t("actions")}</TableHead>
<TableRow> </TableRow>
<TableHead>{t("name")}</TableHead> </TableHeader>
<TableHead>{t("type")}</TableHead> <TableBody>
<TableHead>{t("last_indexed")}</TableHead> {connectors.map((connector) => (
<TableHead>{t("periodic")}</TableHead> <TableRow key={connector.id}>
<TableHead className="text-right">{t("actions")}</TableHead> <TableCell className="font-medium">{connector.name}</TableCell>
</TableRow> <TableCell>{getConnectorIcon(connector.connector_type)}</TableCell>
</TableHeader> <TableCell>
<TableBody> {connector.is_indexable
{connectors.map((connector) => ( ? formatDateTime(connector.last_indexed_at)
<TableRow key={connector.id}> : t("not_indexable")}
<TableCell className="font-medium">{connector.name}</TableCell> </TableCell>
<TableCell>{getConnectorIcon(connector.connector_type)}</TableCell> <TableCell>
<TableCell> {connector.is_indexable ? (
{connector.is_indexable connector.periodic_indexing_enabled ? (
? formatDateTime(connector.last_indexed_at) <TooltipProvider>
: t("not_indexable")} <Tooltip>
</TableCell> <TooltipTrigger asChild>
<TableCell> <div className="flex items-center gap-1 text-green-600 dark:text-green-400">
{connector.is_indexable ? ( <Clock className="h-4 w-4" />
connector.periodic_indexing_enabled ? ( <span className="text-sm font-medium">
<TooltipProvider> {connector.indexing_frequency_minutes
<Tooltip> ? formatFrequency(connector.indexing_frequency_minutes)
<TooltipTrigger asChild> : "Enabled"}
<div className="flex items-center gap-1 text-green-600 dark:text-green-400"> </span>
<Clock className="h-4 w-4" /> </div>
<span className="text-sm font-medium"> </TooltipTrigger>
{connector.indexing_frequency_minutes <TooltipContent>
? formatFrequency(connector.indexing_frequency_minutes) <p>
: "Enabled"} Runs every {connector.indexing_frequency_minutes} minutes
</span> {connector.next_scheduled_at && (
</div> <>
</TooltipTrigger> <br />
<TooltipContent> Next: {formatDateTime(connector.next_scheduled_at)}
<p> </>
Runs every {connector.indexing_frequency_minutes} minutes )}
{connector.next_scheduled_at && ( </p>
<> </TooltipContent>
<br /> </Tooltip>
Next: {formatDateTime(connector.next_scheduled_at)} </TooltipProvider>
</> ) : (
)} <span className="text-sm text-muted-foreground">Disabled</span>
</p> )
</TooltipContent> ) : (
</Tooltip> <span className="text-sm text-muted-foreground">-</span>
</TooltipProvider> )}
) : ( </TableCell>
<span className="text-sm text-muted-foreground">Disabled</span> <TableCell className="text-right">
) <div className="flex justify-end gap-2">
) : ( {connector.is_indexable && (
<span className="text-sm text-muted-foreground">-</span> <div className="flex gap-1">
)} <TooltipProvider>
</TableCell> <Tooltip>
<TableCell className="text-right"> <TooltipTrigger asChild>
<div className="flex justify-end gap-2"> <Button
{connector.is_indexable && ( variant="outline"
<div className="flex gap-1"> size="sm"
<TooltipProvider> onClick={() => handleOpenDatePicker(connector.id)}
<Tooltip> disabled={indexingConnectorId === connector.id}
<TooltipTrigger asChild> >
<Button {indexingConnectorId === connector.id ? (
variant="outline" <RefreshCw className="h-4 w-4 animate-spin" />
size="sm" ) : connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR ? (
onClick={() => handleOpenDatePicker(connector.id)} <Folder className="h-4 w-4" />
disabled={indexingConnectorId === connector.id} ) : (
> <CalendarIcon className="h-4 w-4" />
{indexingConnectorId === connector.id ? ( )}
<RefreshCw className="h-4 w-4 animate-spin" /> <span className="sr-only">
) : connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR ? ( {connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
<Folder className="h-4 w-4" /> ? "Select folder to index"
) : ( : t("index_date_range")}
<CalendarIcon className="h-4 w-4" /> </span>
)} </Button>
<span className="sr-only"> </TooltipTrigger>
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR <TooltipContent>
? "Select folder to index" <p>
: t("index_date_range")} {connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
</span> ? "Select folder to index"
</Button> : t("index_date_range")}
</TooltipTrigger> </p>
<TooltipContent> </TooltipContent>
<p> </Tooltip>
{connector.connector_type === EnumConnectorName.GOOGLE_DRIVE_CONNECTOR </TooltipProvider>
? "Select folder to index" {/* Hide quick index button for Google Drive (requires folder selection) */}
: t("index_date_range")} {connector.connector_type !== EnumConnectorName.GOOGLE_DRIVE_CONNECTOR && (
</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> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => handleOpenPeriodicDialog(connector.id)} onClick={() => handleQuickIndexConnector(connector.id)}
disabled={indexingConnectorId === connector.id}
> >
<Clock className="h-4 w-4" /> {indexingConnectorId === connector.id ? (
<span className="sr-only">Configure Periodic Indexing</span> <RefreshCw className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
<span className="sr-only">{t("quick_index")}</span>
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>Configure Periodic Indexing</p> <p>{t("quick_index_auto")}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
<Button </div>
variant="outline" )}
size="sm" {connector.is_indexable && (
onClick={() => <TooltipProvider>
router.push( <Tooltip>
`/dashboard/${searchSpaceId}/connectors/${connector.id}/edit` <TooltipTrigger asChild>
)
}
>
<Edit className="h-4 w-4" />
<span className="sr-only">{tCommon("edit")}</span>
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="text-destructive-foreground hover:bg-destructive/10" onClick={() => handleOpenPeriodicDialog(connector.id)}
onClick={() => setConnectorToDelete(connector.id)}
> >
<Trash2 className="h-4 w-4" /> <Clock className="h-4 w-4" />
<span className="sr-only">{tCommon("delete")}</span> <span className="sr-only">Configure Periodic Indexing</span>
</Button> </Button>
</AlertDialogTrigger> </TooltipTrigger>
<AlertDialogContent> <TooltipContent>
<AlertDialogHeader> <p>Configure Periodic Indexing</p>
<AlertDialogTitle>{t("delete_connector")}</AlertDialogTitle> </TooltipContent>
<AlertDialogDescription> </Tooltip>
{t("delete_confirm")} </TooltipProvider>
</AlertDialogDescription> )}
</AlertDialogHeader> <Button
<AlertDialogFooter> variant="outline"
<AlertDialogCancel onClick={() => setConnectorToDelete(null)}> size="sm"
{tCommon("cancel")} onClick={() =>
</AlertDialogCancel> router.push(
<AlertDialogAction `/dashboard/${searchSpaceId}/connectors/${connector.id}/edit`
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" )
onClick={handleDeleteConnector} }
> >
{tCommon("delete")} <Edit className="h-4 w-4" />
</AlertDialogAction> <span className="sr-only">{tCommon("edit")}</span>
</AlertDialogFooter> </Button>
</AlertDialogContent> <AlertDialog>
</AlertDialog> <AlertDialogTrigger asChild>
</div> <Button
</TableCell> variant="outline"
</TableRow> size="sm"
))} className="text-destructive-foreground hover:bg-destructive/10"
</TableBody> onClick={() => setConnectorToDelete(connector.id)}
</Table> >
</div> <Trash2 className="h-4 w-4" />
)} <span className="sr-only">{tCommon("delete")}</span>
</CardContent> </Button>
</Card> </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 */} {/* Date Picker Dialog */}
<Dialog open={datePickerOpen} onOpenChange={setDatePickerOpen}> <Dialog open={datePickerOpen} onOpenChange={setDatePickerOpen}>

View file

@ -1,16 +1,36 @@
"use client"; "use client";
import { useParams, useRouter } from "next/navigation"; import { Upload } from "lucide-react";
import { useEffect } from "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 params = useParams();
const router = useRouter();
const search_space_id = params.search_space_id as string; const search_space_id = params.search_space_id as string;
useEffect(() => { return (
router.replace(`/dashboard/${search_space_id}/sources/add?tab=documents`); <div className="container mx-auto py-8 px-4 min-h-[calc(100vh-64px)]">
}, [search_space_id, router]); <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>
);
} }

View file

@ -1,12 +1,11 @@
"use client"; "use client";
import { IconBrandYoutube } from "@tabler/icons-react"; 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 { motion } from "motion/react";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ConnectorsTab } from "@/components/sources/ConnectorsTab"; import { ConnectorsTab } from "@/components/sources/ConnectorsTab";
import { DocumentUploadTab } from "@/components/sources/DocumentUploadTab";
import { YouTubeTab } from "@/components/sources/YouTubeTab"; import { YouTubeTab } from "@/components/sources/YouTubeTab";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { trackSourcesTabViewed } from "@/lib/posthog/events"; import { trackSourcesTabViewed } from "@/lib/posthog/events";
@ -16,12 +15,12 @@ export default function AddSourcesPage() {
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const search_space_id = params.search_space_id as string; 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 // Handle tab from query parameter
useEffect(() => { useEffect(() => {
const tabParam = searchParams.get("tab"); const tabParam = searchParams.get("tab");
if (tabParam && ["documents", "youtube", "connectors"].includes(tabParam)) { if (tabParam && ["youtube", "connectors"].includes(tabParam)) {
setActiveTab(tabParam); setActiveTab(tabParam);
} }
}, [searchParams]); }, [searchParams]);
@ -62,12 +61,7 @@ export default function AddSourcesPage() {
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={handleTabChange} className="w-full"> <Tabs value={activeTab} onValueChange={handleTabChange} className="w-full">
<TabsList className="grid w-full max-w-3xl mx-auto grid-cols-4 h-12"> <TabsList className="grid w-full max-w-2xl mx-auto grid-cols-3 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>
<TabsTrigger value="youtube" className="flex items-center gap-2"> <TabsTrigger value="youtube" className="flex items-center gap-2">
<IconBrandYoutube className="h-4 w-4" /> <IconBrandYoutube className="h-4 w-4" />
YouTube YouTube
@ -85,10 +79,6 @@ export default function AddSourcesPage() {
</TabsList> </TabsList>
<div className="mt-8"> <div className="mt-8">
<TabsContent value="documents" className="space-y-6">
<DocumentUploadTab searchSpaceId={search_space_id} />
</TabsContent>
<TabsContent value="youtube" className="space-y-6"> <TabsContent value="youtube" className="space-y-6">
<YouTubeTab searchSpaceId={search_space_id} /> <YouTubeTab searchSpaceId={search_space_id} />
</TabsContent> </TabsContent>

View file

@ -325,7 +325,7 @@ export const ComposerAddAttachment: FC = () => {
const chatAttachmentInputRef = useRef<HTMLInputElement>(null); const chatAttachmentInputRef = useRef<HTMLInputElement>(null);
const handleFileUpload = () => { const handleFileUpload = () => {
router.push(`/dashboard/${searchSpaceId}/sources/add?tab=documents`); router.push(`/dashboard/${searchSpaceId}/documents/upload`);
}; };
const handleChatAttachment = () => { const handleChatAttachment = () => {