refactor: streamline connector management UI and enhance document handling

- Updated the ConnectorIndicator component to accurately reflect active connectors and their document counts.
- Improved the display of standalone document types in the ActiveConnectorsTab, allowing users to view all documents easily.
- Enhanced the ConnectorCard to show last indexed dates and formatted document counts for better clarity.
- Adjusted tooltip and aria-labels for accessibility and consistency across attachment upload components.
- Preserved newlines in URL input for webcrawler configuration to ensure proper backend handling.
This commit is contained in:
Anish Sarkar 2026-01-01 20:38:12 +05:30
parent 0e93d8420f
commit 543daa0434
12 changed files with 239 additions and 116 deletions

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

@ -337,12 +337,12 @@ export const ComposerAddAttachment: FC = () => {
<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,7 +350,7 @@ 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" />

View file

@ -38,11 +38,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 +75,7 @@ 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 +85,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,35 +103,50 @@ 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 <Link
href={`/dashboard/${searchSpaceId}/connectors/add`} href={`/dashboard/${searchSpaceId}/connectors/add`}

View file

@ -159,6 +159,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 +171,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 +180,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 +189,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>
)} )}
</> </>
@ -259,7 +260,7 @@ export const ConnectorIndicator: FC = () => {
{/* Header */} {/* Header */}
<ConnectorDialogHeader <ConnectorDialogHeader
activeTab={activeTab} activeTab={activeTab}
totalSourceCount={totalSourceCount} totalSourceCount={activeConnectorsCount}
searchQuery={searchQuery} searchQuery={searchQuery}
onTabChange={handleTabChange} onTabChange={handleTabChange}
onSearchChange={setSearchQuery} onSearchChange={setSearchQuery}

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,
@ -70,18 +87,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;
@ -105,6 +120,11 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
<div className="text-[11px] text-muted-foreground mt-1"> <div className="text-[11px] text-muted-foreground mt-1">
{getStatusContent()} {getStatusContent()}
</div> </div>
{isConnected && documentCount !== undefined && (
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
)}
</div> </div>
<Button <Button
size="sm" size="sm"
@ -123,6 +143,8 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
"Syncing..." "Syncing..."
) : isConnected ? ( ) : isConnected ? (
"Manage" "Manage"
) : id === "youtube-crawler" ? (
"Add"
) : connectorType ? ( ) : connectorType ? (
"Connect" "Connect"
) : ( ) : (

View file

@ -43,9 +43,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,
}); });
} }
}; };

View file

@ -25,7 +25,7 @@ interface ConnectorEditViewProps {
onSave: () => void; onSave: () => void;
onDisconnect: () => void; onDisconnect: () => void;
onBack: () => void; onBack: () => void;
onConfigChange?: (config: Record<string, any>) => void; onConfigChange?: (config: Record<string, unknown>) => void;
onNameChange?: (name: string) => void; onNameChange?: (name: string) => void;
} }
@ -201,35 +201,37 @@ 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-4 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">Are you sure?</span>
<Button <div className="flex items-center gap-2 sm:gap-3">
variant="destructive" <Button
size="sm" variant="destructive"
onClick={handleDisconnectConfirm} size="sm"
disabled={isDisconnecting} onClick={handleDisconnectConfirm}
className="text-xs sm:text-sm" disabled={isDisconnecting}
> className="text-xs sm:text-sm flex-1 sm:flex-initial"
{isDisconnecting ? ( >
<> {isDisconnecting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <>
Disconnecting... <Loader2 className="mr-2 h-4 w-4 animate-spin" />
</> Disconnecting...
) : ( </>
"Confirm Disconnect" ) : (
)} "Confirm Disconnect"
</Button> )}
<Button </Button>
variant="ghost" <Button
size="sm" variant="ghost"
onClick={handleDisconnectCancel} size="sm"
disabled={isDisconnecting} onClick={handleDisconnectCancel}
className="text-xs sm:text-sm" disabled={isDisconnecting}
> className="text-xs sm:text-sm flex-1 sm:flex-initial"
Cancel >
</Button> Cancel
</Button>
</div>
</div> </div>
) : ( ) : (
<Button <Button
@ -237,13 +239,17 @@ export const ConnectorEditView: FC<ConnectorEditViewProps> = ({
size="sm" 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"
> >
<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"
>
{isSaving ? ( {isSaving ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />

View file

@ -1,14 +1,16 @@
"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 { getDocumentCountForConnector } from "../utils/connector-document-mapping";
import { import {
TabsContent, TabsContent,
} from "@/components/ui/tabs"; } from "@/components/ui/tabs";
@ -31,46 +33,75 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
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),
}));
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"> {connectors.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">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> Active Connectors
{activeDocumentTypes.map(([docType, count]) => ( </h3>
<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) => { {connectors.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
@ -113,6 +144,9 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
: "Never indexed"} : "Never indexed"}
</p> </p>
)} )}
<p className="text-[11px] text-muted-foreground mt-0.5">
{formatDocumentCount(documentCount)}
</p>
</div> </div>
<Button <Button
variant="secondary" variant="secondary"
@ -126,7 +160,47 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
</div> </div>
); );
})} })}
</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"
>
<div className="flex items-center justify-center">
{getConnectorIcon(doc.type, "size-3.5")}
</div>
<span className="text-[12px] font-medium">
{doc.label}
</span>
<span className="text-[11px] text-muted-foreground">
{formatDocumentCount(doc.count)}
</span>
</div>
))}
</div>
</div>
)}
</div> </div>
) : ( ) : (
<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">

View file

@ -101,6 +101,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
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={() => onConnectOAuth(connector)} onConnect={() => onConnectOAuth(connector)}
@ -162,6 +163,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
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={handleConnect}
@ -230,6 +232,7 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
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={handleConnect}

View file

@ -29,7 +29,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
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:
@ -81,6 +81,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

@ -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

@ -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