feat(assistant-ui): add "Manage Connectors" option to ComposerAction and clean up DocumentsFilters component

- Introduced a new dropdown menu item for managing connectors in the ComposerAction component.
- Removed unused upload button and related props from the DocumentsFilters component for improved clarity and simplicity.
- Updated LayoutDataProvider and RightPanel components to streamline state management and enhance layout consistency.
This commit is contained in:
Anish Sarkar 2026-07-06 11:09:25 +05:30
parent cda232101f
commit f11cc3868c
7 changed files with 44 additions and 248 deletions

View file

@ -1359,6 +1359,10 @@ const ComposerAction: FC<ComposerActionProps> = ({
<Camera className="h-4 w-4" />
Take a screenshot
</DropdownMenuItem>
<DropdownMenuItem onSelect={() => setConnectorDialogOpen(true)}>
<Unplug className="h-4 w-4" />
Manage Connectors
</DropdownMenuItem>
<DropdownMenuSub
open={toolsPopoverOpen}
onOpenChange={(open) => {

View file

@ -1,9 +1,8 @@
"use client";
import { FolderPlus, ListFilter, Search, Upload, X } from "lucide-react";
import { FolderPlus, ListFilter, Search, X } from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useCallback, useMemo, useRef, useState } from "react";
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
@ -21,7 +20,6 @@ export function DocumentsFilters({
onToggleType,
activeTypes,
onCreateFolder,
onUploadClick,
}: {
typeCounts: Partial<Record<DocumentTypeEnum, number>>;
onSearch: (v: string) => void;
@ -29,15 +27,11 @@ export function DocumentsFilters({
onToggleType: (type: DocumentTypeEnum, checked: boolean) => void;
activeTypes: DocumentTypeEnum[];
onCreateFolder?: () => void;
onUploadClick?: () => void;
}) {
const t = useTranslations("documents");
const id = React.useId();
const inputRef = useRef<HTMLInputElement>(null);
const { openDialog: openUploadDialog } = useDocumentUploadDialog();
const handleUpload = onUploadClick ?? openUploadDialog;
const [typeSearchQuery, setTypeSearchQuery] = useState("");
const [scrollPos, setScrollPos] = useState<"top" | "middle" | "bottom">("top");
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
@ -207,18 +201,6 @@ export function DocumentsFilters({
</Button>
)}
</div>
{/* Upload Button */}
<Button
data-joyride="upload-button"
onClick={handleUpload}
variant="outline"
size="sm"
className="h-8 shrink-0 gap-1.5 border-0 bg-white text-gray-700 shadow-none hover:bg-accent hover:text-accent-foreground dark:bg-white dark:text-gray-800"
>
<Upload size={13} />
<span>Upload</span>
</Button>
</div>
</div>
);

View file

@ -12,7 +12,6 @@ import { currentThreadAtom, resetCurrentThreadAtom } from "@/atoms/chat/current-
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
import { announcementsDialogAtom } from "@/atoms/layout/dialogs.atom";
import { rightPanelCollapsedAtom } from "@/atoms/layout/right-panel.atom";
import { deleteSearchSpaceMutationAtom } from "@/atoms/search-spaces/search-space-mutation.atoms";
import { searchSpacesAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { removeChatTabAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
@ -133,7 +132,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
// Documents sidebar state (shared atom so Composer can toggle it)
const [isDocumentsSidebarOpen, setIsDocumentsSidebarOpen] = useAtom(documentsSidebarOpenAtom);
const setIsRightPanelCollapsed = useSetAtom(rightPanelCollapsedAtom);
// Open documents sidebar by default on desktop (docked mode)
const documentsInitialized = useRef(false);
@ -335,9 +333,9 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}, [threadsData, searchSpaceId]);
// Navigation items
// Inbox, Automations, and Documents are rendered explicitly below "New chat"
// Inbox, Automations, and Artifacts are rendered explicitly below "New chat"
// in the sidebar (also surfaced in the icon rail's collapsed mode via this
// list). Announcements has been moved to the avatar dropdown.
// list). Documents is embedded below Recents; announcements live in the avatar dropdown.
const isAutomationsActive = pathname?.includes("/automations") === true;
const isArtifactsActive = pathname?.endsWith("/artifacts") === true;
const navItems: NavItem[] = useMemo(
@ -523,27 +521,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
return;
}
if (item.url === "#documents") {
if (!isMobile) {
if (!isDocumentsSidebarOpen) {
setIsDocumentsSidebarOpen(true);
setIsRightPanelCollapsed(false);
setIsDocumentsSidebarOpen((prev) => {
if (!prev) {
setActiveSlideoutPanel(null);
} else {
setIsRightPanelCollapsed((prev) => !prev);
}
} else {
setIsDocumentsSidebarOpen((prev) => {
if (!prev) {
setActiveSlideoutPanel(null);
}
return !prev;
});
}
return !prev;
});
return;
}
router.push(item.url);
},
[router, isMobile, isDocumentsSidebarOpen, setIsDocumentsSidebarOpen, setIsRightPanelCollapsed]
[router, setIsDocumentsSidebarOpen]
);
const handleNewChat = useCallback(() => {

View file

@ -3,10 +3,9 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { PanelRight } from "lucide-react";
import dynamic from "next/dynamic";
import { type MouseEvent, startTransition, useEffect } from "react";
import { startTransition, useEffect } from "react";
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
import { closeEditorPanelAtom, editorPanelAtom } from "@/atoms/editor/editor-panel.atom";
import {
type RightPanelTab,
@ -18,7 +17,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { artifactsPanelOpenAtom, closeArtifactsPanelAtom } from "@/features/chat-artifacts";
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
import { cn } from "@/lib/utils";
import { DocumentsSidebar } from "../sidebar";
const EditorPanelContent = dynamic(
() =>
@ -61,41 +59,9 @@ const ArtifactsPanelContent = dynamic(
);
interface RightPanelProps {
documentsPanel?: {
open: boolean;
onOpenChange: (open: boolean) => void;
};
showCollapseButton?: boolean;
showTopBorder?: boolean;
}
function isKeyboardClick(event: MouseEvent) {
return event.detail === 0;
}
function CollapseButton({ onClick }: { onClick: () => void }) {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
tabIndex={-1}
onClick={(event) => {
if (isKeyboardClick(event)) return;
onClick();
}}
className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
>
<PanelRight className="h-4 w-4" />
<span className="sr-only">Collapse panel</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Collapse panel</TooltipContent>
</Tooltip>
);
}
interface RightPanelToggleButtonProps {
className?: string;
iconClassName?: string;
@ -108,7 +74,6 @@ export function RightPanelToggleButton({
disabled = false,
}: RightPanelToggleButtonProps) {
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
@ -124,8 +89,7 @@ export function RightPanelToggleButton({
: !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent =
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
const hasContent = reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
const label = collapsed ? "Expand panel" : "Collapse panel";
if (!hasContent) return null;
@ -162,7 +126,6 @@ export function RightPanelToggleButton({
*/
export function RightPanelExpandButton() {
const [collapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
@ -178,8 +141,7 @@ export function RightPanelExpandButton() {
: !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent =
documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
const hasContent = reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen;
if (!collapsed || !hasContent) return null;
@ -201,8 +163,7 @@ const PANEL_WIDTHS = {
/**
* Priority order used to fall back to another open surface when the active
* tab's content closes. Artifacts sit just above the always-available sources
* tab.
* tab's content closes. The neutral "sources" tab is kept as the closed state.
*/
const TAB_FALLBACK_ORDER: RightPanelTab[] = [
"hitl-edit",
@ -221,11 +182,7 @@ function resolveEffectiveTab(
return TAB_FALLBACK_ORDER.find((tab) => openByTab[tab]) ?? "sources";
}
export function RightPanel({
documentsPanel,
showCollapseButton = true,
showTopBorder = false,
}: RightPanelProps) {
export function RightPanel({ showTopBorder = false }: RightPanelProps) {
const [activeTab] = useAtom(rightPanelTabAtom);
const reportState = useAtomValue(reportPanelAtom);
const closeReport = useSetAtom(closeReportPanelAtom);
@ -237,9 +194,8 @@ export function RightPanel({
const closeCitation = useSetAtom(closeCitationPanelAtom);
const artifactsOpen = useAtomValue(artifactsPanelOpenAtom);
const closeArtifacts = useSetAtom(closeArtifactsPanelAtom);
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
const [collapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = documentsPanel?.open ?? false;
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
editorState.isOpen &&
@ -278,11 +234,10 @@ export function RightPanel({
]);
const isVisible =
(documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen) &&
!collapsed;
(reportOpen || editorOpen || hitlEditOpen || citationOpen || artifactsOpen) && !collapsed;
const effectiveTab = resolveEffectiveTab(activeTab, {
sources: documentsOpen,
sources: false,
report: reportOpen,
editor: editorOpen,
"hitl-edit": hitlEditOpen,
@ -291,10 +246,6 @@ export function RightPanel({
});
const targetWidth = PANEL_WIDTHS[effectiveTab];
const collapseButton = showCollapseButton ? (
<CollapseButton onClick={() => setCollapsed(true)} />
) : null;
if (!isVisible) return null;
return (
@ -306,16 +257,6 @@ export function RightPanel({
)}
>
<div className="relative flex-1 min-h-0 overflow-hidden">
{effectiveTab === "sources" && documentsOpen && documentsPanel && (
<div className="h-full">
<DocumentsSidebar
open={documentsPanel.open}
onOpenChange={documentsPanel.onOpenChange}
embedded
headerAction={collapseButton}
/>
</div>
)}
{effectiveTab === "report" && reportOpen && (
<div className="h-full flex flex-col">
<ReportPanelContent

View file

@ -440,6 +440,7 @@ export function LayoutShell({
onChatArchive={onChatArchive}
onViewAllChats={onViewAllChats}
isAllChatsActive={isAllChatsPage}
documentsPanel={documentsPanel}
user={user}
onSettings={onSettings}
onManageMembers={onManageMembers}
@ -532,17 +533,8 @@ export function LayoutShell({
{children}
</MainContentPanel>
{/* Right panel — tabbed Sources/Report (desktop only) */}
{documentsPanel ? (
<RightPanel
documentsPanel={{
open: documentsPanel.open,
onOpenChange: documentsPanel.onOpenChange,
}}
showCollapseButton={!isMacDesktop}
showTopBorder={isMacDesktop}
/>
) : null}
{/* Right panel — Report/Editor/Citations/Artifacts (desktop only) */}
<RightPanel showTopBorder={isMacDesktop} />
</>
)}
</DesktopWorkspaceRegion>

View file

@ -12,7 +12,6 @@ import {
Paperclip,
Server,
Trash2,
Unplug,
Upload,
X,
} from "lucide-react";
@ -25,8 +24,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { agentFlagsAtom } from "@/atoms/agent/agent-flags-query.atom";
import { makeFolderMention, mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
import { connectorsAtom } from "@/atoms/connectors/connector-query.atoms";
import { deleteDocumentMutationAtom } from "@/atoms/documents/document-mutation.atoms";
import { expandedFolderIdsAtom } from "@/atoms/documents/folder.atoms";
import { agentCreatedDocumentsAtom } from "@/atoms/documents/ui.atoms";
@ -58,7 +55,6 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
import { Skeleton } from "@/components/ui/skeleton";
@ -67,7 +63,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useAnonymousMode, useIsAnonymous } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useMediaQuery } from "@/hooks/use-media-query";
@ -167,18 +162,6 @@ interface WatchedFolderEntry {
active: boolean;
}
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: "GOOGLE_CALENDAR_CONNECTOR", label: "Google Calendar" },
{ type: "SLACK_CONNECTOR", label: "Slack" },
{ type: "LINEAR_CONNECTOR", label: "Linear" },
{ type: "JIRA_CONNECTOR", label: "Jira" },
{ type: "GITHUB_CONNECTOR", label: "GitHub" },
] as const;
interface DocumentsSidebarProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -228,11 +211,8 @@ function AuthenticatedDocumentsSidebarBase({
const electronAPI = desktopFeaturesEnabled ? platformElectronAPI : null;
const { etlService } = useRuntimeConfig();
const searchSpaceId = getWorkspaceIdNumber(params) ?? 0;
const setConnectorDialogOpen = useSetAtom(connectorDialogOpenAtom);
const openEditorPanel = useSetAtom(openEditorPanelAtom);
const { data: agentFlags } = useAtomValue(agentFlagsAtom);
const { data: connectors } = useAtomValue(connectorsAtom);
const connectorCount = connectors?.length ?? 0;
const [search, setSearch] = useState("");
const debouncedSearch = useDebouncedValue(search, 250);
@ -1116,76 +1096,9 @@ function AuthenticatedDocumentsSidebarBase({
const showCloudSkeleton =
currentFilesystemTab === "cloud" &&
(zeroFoldersResult.type !== "complete" || zeroAllDocsResult.type !== "complete");
const connectorButtonLabel = connectorCount > 0 ? "Manage connectors" : "Connect your connectors";
const cloudContent = (
<>
{/* Connected tools strip */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setConnectorDialogOpen(true)}
className="shrink-0 mx-4 mt-6 mb-2.5 h-auto select-none justify-start gap-2 bg-muted px-3 py-1.5 text-xs text-muted-foreground"
>
<Unplug className="size-4 shrink-0" />
<span className="truncate">{connectorButtonLabel}</span>
{connectorCount > 0 && (
<span className="shrink-0 rounded-full bg-muted-foreground/15 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
{connectorCount}
</span>
)}
<AvatarGroup className="ml-auto shrink-0">
{connectorCount > 0 && connectors
? connectors.slice(0, isMobile ? 5 : 9).map((connector, i) => {
const avatar = (
<Avatar
key={connector.id}
className="size-6"
style={{ zIndex: Math.max(9 - i, 1) }}
>
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(connector.connector_type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={connector.id}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{connector.name}
</TooltipContent>
</Tooltip>
);
})
: (isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={type}
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
)}
</AvatarGroup>
</Button>
{isElectron && (
<Button
type="button"
@ -1541,7 +1454,6 @@ function AuthenticatedDocumentsSidebarBase({
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
@ -1825,44 +1737,6 @@ function AnonymousDocumentsSidebar({
</div>
</div>
{/* Connectors strip (gated) */}
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => gate("connect your data sources")}
className="shrink-0 mx-4 mt-6 mb-2.5 h-auto select-none justify-start gap-2 border bg-muted/50 px-3 py-1.5 text-xs text-muted-foreground"
>
<Unplug className="size-4 shrink-0" />
<span className="truncate">Connect your connectors</span>
<AvatarGroup className="ml-auto shrink-0">
{(isMobile ? SHOWCASE_CONNECTORS.slice(0, 5) : SHOWCASE_CONNECTORS).map(
({ type, label }, i) => {
const avatar = (
<Avatar
key={type}
className="size-6"
style={{ zIndex: SHOWCASE_CONNECTORS.length - i }}
>
<AvatarFallback className="bg-muted text-[10px] text-muted-foreground">
{getConnectorIcon(type, "size-3.5")}
</AvatarFallback>
</Avatar>
);
if (isMobile) return avatar;
return (
<Tooltip key={type}>
<TooltipTrigger asChild>{avatar}</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{label}
</TooltipContent>
</Tooltip>
);
}
)}
</AvatarGroup>
</Button>
{/* Filters & upload */}
<div className="flex-1 min-h-0 pt-0 flex flex-col">
<div className="px-4 pb-1.5">
@ -1873,7 +1747,6 @@ function AnonymousDocumentsSidebar({
onToggleType={() => {}}
activeTypes={[]}
onCreateFolder={() => gate("create folders")}
onUploadClick={handleAnonUploadClick}
/>
</div>

View file

@ -16,6 +16,7 @@ import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { ChatListItem } from "./ChatListItem";
import { CreditBalanceDisplay } from "./CreditBalanceDisplay";
import { DocumentsSidebar } from "./DocumentsSidebar";
import { NavSection } from "./NavSection";
import { SidebarButton } from "./SidebarButton";
import { SidebarCollapseButton } from "./SidebarCollapseButton";
@ -76,6 +77,10 @@ interface SidebarProps {
onChatArchive?: (chat: ChatItem) => void;
onViewAllChats?: () => void;
isAllChatsActive?: boolean;
documentsPanel?: {
open: boolean;
onOpenChange: (open: boolean) => void;
};
user: User;
onSettings?: () => void;
onManageMembers?: () => void;
@ -113,6 +118,7 @@ export function Sidebar({
onChatArchive,
onViewAllChats,
isAllChatsActive = false,
documentsPanel,
user,
onSettings,
onManageMembers,
@ -136,11 +142,9 @@ export function Sidebar({
const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
// Inbox, Automations, and Documents are rendered explicitly right below
// Inbox, Automations, and Artifacts are rendered explicitly right below
// New Chat. Pull them out of the nav items list so they don't also appear
// in the bottom NavSection. Documents is only present in navItems on
// mobile; Automations is identified by URL suffix so the same code path
// works across search spaces.
// in the bottom NavSection. Documents is embedded below Recents.
const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]);
const automationsItem = useMemo(
() => navItems.find((item) => item.url.endsWith("/automations")),
@ -280,7 +284,10 @@ export function Sidebar({
<SidebarSection
title={t("recents")}
defaultOpen={true}
fillHeight={true}
fillHeight={!documentsPanel?.open}
className={
documentsPanel?.open ? "flex shrink-0 flex-col min-h-0 max-h-[35%]" : undefined
}
alwaysShowAction={!disableTooltips && isAllChatsActive}
action={
onViewAllChats ? (
@ -328,6 +335,15 @@ export function Sidebar({
<p className="px-2 py-1 text-sm text-muted-foreground/60">{t("no_chats")}</p>
)}
</SidebarSection>
{documentsPanel?.open ? (
<div className="min-h-0 flex-1 border-t">
<DocumentsSidebar
open={documentsPanel.open}
onOpenChange={documentsPanel.onOpenChange}
embedded
/>
</div>
) : null}
</div>
)}