feat: add connector tools strip to DocumentsSidebar and implement AvatarGroup component for better UI representation

This commit is contained in:
Anish Sarkar 2026-03-10 14:23:19 +05:30
parent 4ebf2359b5
commit c8e36cb928
3 changed files with 78 additions and 11 deletions

View file

@ -549,6 +549,15 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<ConnectorIndicator ref={connectorRef} showTrigger={false} /> <ConnectorIndicator ref={connectorRef} showTrigger={false} />
{sidebarDocs.length > 0 && (
<button
type="button"
onClick={() => setDocumentsSidebarOpen(true)}
className="rounded-full border border-border/60 bg-accent/50 px-2.5 py-1 text-xs font-medium text-foreground/80 transition-colors hover:bg-accent"
>
{sidebarDocs.length} {sidebarDocs.length === 1 ? "source" : "sources"} selected
</button>
)}
</div> </div>
{!hasModelConfigured && ( {!hasModelConfigured && (
@ -559,15 +568,6 @@ const ComposerAction: FC<ComposerActionProps> = ({ isBlockedByOtherUser = false
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{sidebarDocs.length > 0 && (
<button
type="button"
onClick={() => setDocumentsSidebarOpen(true)}
className="rounded-full border border-border/60 bg-accent/50 px-2.5 py-1 text-xs font-medium text-foreground/80 transition-colors hover:bg-accent"
>
{sidebarDocs.length} {sidebarDocs.length === 1 ? "source" : "sources"} selected
</button>
)}
<AssistantIf condition={({ thread }) => !thread.isRunning}> <AssistantIf condition={({ thread }) => !thread.isRunning}>
<ComposerPrimitive.Send asChild disabled={isSendDisabled}> <ComposerPrimitive.Send asChild disabled={isSendDisabled}>

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@ -12,9 +12,12 @@ import {
type SortKey, type SortKey,
} from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell"; } from "@/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsTableShell";
import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom"; import { sidebarSelectedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms"; import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { DocumentTypeEnum } from "@/contracts/types/document.types"; import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useDocumentSearch } from "@/hooks/use-document-search"; import { useDocumentSearch } from "@/hooks/use-document-search";
@ -22,6 +25,14 @@ import { useDocuments } from "@/hooks/use-documents";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
const SHOWCASE_CONNECTORS = [
{ type: "GOOGLE_DRIVE_CONNECTOR", label: "Google Drive" },
{ type: "GOOGLE_GMAIL_CONNECTOR", label: "Gmail" },
{ type: "NOTION_CONNECTOR", label: "Notion" },
{ type: "YOUTUBE_CONNECTOR", label: "YouTube" },
{ type: "SLACK_CONNECTOR", label: "Slack" },
] as const;
interface DocumentsSidebarProps { interface DocumentsSidebarProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -35,6 +46,7 @@ export function DocumentsSidebar({ open, onOpenChange, isDocked = false, onDocke
const params = useParams(); const params = useParams();
const isMobile = !useMediaQuery("(min-width: 640px)"); const isMobile = !useMediaQuery("(min-width: 640px)");
const searchSpaceId = Number(params.search_space_id); const searchSpaceId = Number(params.search_space_id);
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 250); const debouncedSearch = useDebouncedValue(search, 250);
@ -201,6 +213,38 @@ export function DocumentsSidebar({ open, onOpenChange, isDocked = false, onDocke
</div> </div>
</div> </div>
{/* Connected tools strip */}
<div className="shrink-0 mx-4 mb-3 flex items-center gap-2 rounded-lg border bg-muted/50 px-3 py-2">
<button
type="button"
onClick={() => setConnectorDialogOpen(true)}
className="flex items-center gap-2 min-w-0 flex-1 text-left"
>
<span className="truncate text-xs text-muted-foreground">
Connect your tools
</span>
<AvatarGroup className="ml-auto shrink-0">
{SHOWCASE_CONNECTORS.map(({ type, label }, i) => (
<Tooltip key={type}>
<TooltipTrigger asChild>
<Avatar
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px]">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
))}
</AvatarGroup>
</button>
</div>
<div className="flex-1 min-h-0 overflow-x-hidden pt-0 flex flex-col"> <div className="flex-1 min-h-0 overflow-x-hidden pt-0 flex flex-col">
<div className="px-4 pb-2"> <div className="px-4 pb-2">
<DocumentsFilters <DocumentsFilters

View file

@ -38,4 +38,27 @@ function AvatarFallback({
); );
} }
export { Avatar, AvatarImage, AvatarFallback }; function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn("flex -space-x-2", className)}
{...props}
/>
);
}
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-group-count"
className={cn(
"relative flex size-8 shrink-0 items-center justify-center rounded-full border-2 border-background bg-muted text-xs font-medium text-muted-foreground",
className
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount };