+
{otherToolGroup.label}
{otherToolGroup.tools.map((tool) => {
diff --git a/surfsense_web/components/documents/FolderTreeView.tsx b/surfsense_web/components/documents/FolderTreeView.tsx
index 6c896b4b4..c75ac885a 100644
--- a/surfsense_web/components/documents/FolderTreeView.tsx
+++ b/surfsense_web/components/documents/FolderTreeView.tsx
@@ -324,7 +324,6 @@ export function FolderTreeView({
No matching documents
-
Try a different search term
);
}
diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
index 511c095e5..cfd970f13 100644
--- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
+++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx
@@ -2,7 +2,7 @@
import { useQuery } from "@tanstack/react-query";
import { useAtomValue, useSetAtom } from "jotai";
-import { AlarmClock, AlertTriangle, Shapes, SquareTerminal } from "lucide-react";
+import { AlarmClock, AlertTriangle, Shapes, SquareTerminal, Unplug } from "lucide-react";
import { useParams, usePathname, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useTheme } from "next-themes";
@@ -300,6 +300,7 @@ export function LayoutDataProvider({
const isAutomationsActive = pathname?.includes("/automations") === true;
const isArtifactsActive = pathname?.endsWith("/artifacts") === true;
const isPlaygroundRoute = pathname?.includes("/playground") === true;
+ const isConnectorsRoute = pathname?.includes("/connectors") === true;
const navItems: NavItem[] = useMemo(
() =>
(
@@ -316,6 +317,12 @@ export function LayoutDataProvider({
icon: Shapes,
isActive: isArtifactsActive,
},
+ {
+ title: "Connectors",
+ url: `/dashboard/${workspaceId}/connectors`,
+ icon: Unplug,
+ isActive: isConnectorsRoute,
+ },
{
title: "Playground",
url: `/dashboard/${workspaceId}/playground`,
@@ -324,7 +331,7 @@ export function LayoutDataProvider({
},
] as (NavItem | null)[]
).filter((item): item is NavItem => item !== null),
- [workspaceId, isAutomationsActive, isArtifactsActive, isPlaygroundRoute]
+ [workspaceId, isAutomationsActive, isArtifactsActive, isConnectorsRoute, isPlaygroundRoute]
);
// Handlers
@@ -631,6 +638,7 @@ export function LayoutDataProvider({
const isAutomationsPage = pathname?.includes("/automations") === true;
const isArtifactsPage = pathname?.endsWith("/artifacts") === true;
const isPlaygroundPage = pathname?.includes("/playground") === true;
+ const isConnectorsPage = pathname?.includes("/connectors") === true;
const isAllChatsPage = pathname?.endsWith("/chats") === true;
const handleChatsClick = useCallback(() => {
router.push(`/dashboard/${workspaceId}/chats`);
@@ -650,6 +658,7 @@ export function LayoutDataProvider({
isAutomationsPage ||
isArtifactsPage ||
isPlaygroundPage ||
+ isConnectorsPage ||
isAllChatsPage;
return (
@@ -702,6 +711,7 @@ export function LayoutDataProvider({
isAutomationsPage ||
isArtifactsPage ||
isPlaygroundPage ||
+ isConnectorsPage ||
isAllChatsPage
? "items-start justify-center px-6 py-8 md:px-10 md:pb-10 md:pt-16"
: undefined
diff --git a/surfsense_web/components/layout/ui/sidebar/AllChatsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/AllChatsSidebar.tsx
index bc861f443..423eecb79 100644
--- a/surfsense_web/components/layout/ui/sidebar/AllChatsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/AllChatsSidebar.tsx
@@ -487,13 +487,9 @@ function AllChatsContent({ workspaceId, className }: AllChatsContentProps) {
diff --git a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
index a24b1d619..f8b71fd2e 100644
--- a/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
+++ b/surfsense_web/components/layout/ui/sidebar/DocumentsSidebar.tsx
@@ -8,25 +8,18 @@ import {
FolderPlus,
FolderSync,
ListFilter,
- Plus,
- Settings2,
SlidersVertical,
Trash2,
- Upload,
} from "lucide-react";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { makeFolderMention, mentionedDocumentsAtom } from "@/atoms/chat/mentioned-documents.atom";
-import { importConnectorRequestAtom } 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";
import { openEditorPanelAtom } from "@/atoms/editor/editor-panel.atom";
-import { useConnectorStatus } from "@/components/assistant-ui/connector-popup/hooks/use-connector-status";
-import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
import { CreateFolderDialog } from "@/components/documents/CreateFolderDialog";
import type { DocumentNodeDoc } from "@/components/documents/DocumentNode";
import { getDocumentTypeIcon } from "@/components/documents/DocumentTypeIcon";
@@ -34,7 +27,7 @@ import type { FolderDisplay } from "@/components/documents/FolderNode";
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
import { FolderTreeView } from "@/components/documents/FolderTreeView";
import { VersionHistoryDialog } from "@/components/documents/version-history";
-import { useOptionalRuntimeConfig, useRuntimeConfig } from "@/components/providers/runtime-config";
+import { useRuntimeConfig } from "@/components/providers/runtime-config";
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
import {
DEFAULT_EXCLUDE_PATTERNS,
@@ -57,7 +50,6 @@ import {
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
@@ -67,9 +59,6 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { useAnonymousMode, useIsAnonymous } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate";
-import { EnumConnectorName } from "@/contracts/enums/connector";
-import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
-import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import type { DocumentTypeEnum } from "@/contracts/types/document.types";
import { useIsMobile } from "@/hooks/use-mobile";
import { useElectronAPI, usePlatform } from "@/hooks/use-platform";
@@ -239,11 +228,9 @@ export function EmbeddedDocumentsMenu({
}
/**
- * Import menu: local file upload plus the cloud-drive import connectors
- * (Google Drive / OneDrive / Dropbox). Drive/OneDrive/Dropbox set
- * `importConnectorRequestAtom`, which the connector dialog consumes to run
- * OAuth or open the existing account's config. In anonymous mode, `gate`
- * intercepts every item to trigger the login flow.
+ * Desktop-only "Watch Local Folder" entry point. File uploads now live in the
+ * chat composer and cloud drives in the connector catalog, so this renders
+ * nothing on web. `gate` login-gates the action in anonymous mode.
*/
export function EmbeddedImportMenu({
gate,
@@ -252,30 +239,14 @@ export function EmbeddedImportMenu({
gate?: (feature: string) => void;
onFolderWatched?: () => void;
}) {
- const { openDialog } = useDocumentUploadDialog();
- const setImportRequest = useSetAtom(importConnectorRequestAtom);
- // Provider is absent on anonymous /free pages, where every item is login-gated
- // anyway — defaulting to hosted (Composio) there is cosmetic.
- const selfHosted = useOptionalRuntimeConfig()?.deploymentMode === "self-hosted";
- const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
- const { data: connectors } = useAtomValue(connectorsAtom);
-
// Watch Local Folder is a desktop-app feature (needs the Electron folder watcher).
const { isDesktop } = usePlatform();
const params = useParams();
const workspaceId = getWorkspaceIdNumber(params) ?? 0;
const [folderWatchOpen, setFolderWatchOpen] = useState(false);
- // Native Google Drive connector self-hosted only; hosted deployments use Composio.
- const driveType = selfHosted
- ? EnumConnectorName.GOOGLE_DRIVE_CONNECTOR
- : EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR;
-
- const cloudItems = [
- { type: driveType, label: "Google Drive" },
- { type: EnumConnectorName.ONEDRIVE_CONNECTOR, label: "OneDrive" },
- { type: EnumConnectorName.DROPBOX_CONNECTOR, label: "Dropbox" },
- ];
+ // Nothing to import on web anymore — hide the button rather than show an empty menu.
+ if (!isDesktop) return null;
return (
@@ -285,99 +256,20 @@ export function EmbeddedImportMenu({
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
- aria-label="Import documents"
+ aria-label="Watch local folder"
>
- (gate ? gate("upload files") : openDialog())}>
-
- Upload Files
+ (gate ? gate("watch local folders") : setFolderWatchOpen(true))}
+ >
+
+ Watch Local Folder
- {isDesktop && (
- (gate ? gate("watch local folders") : setFolderWatchOpen(true))}
- >
-
- Watch Local Folder
-
- )}
-
- {cloudItems.map((item) => {
- const enabled = gate ? true : isConnectorEnabled(item.type);
- const statusMessage = enabled ? null : getConnectorStatusMessage(item.type);
- const icon = getConnectorIcon(item.type, "h-4 w-4");
- // gate = anonymous mode; treat every connector as unconnected so items
- // route through the login gate rather than reading workspace connectors.
- const accountCount = gate
- ? 0
- : (connectors ?? []).filter(
- (c: SearchSourceConnector) => c.connector_type === item.type
- ).length;
-
- // Unavailable (e.g. maintenance): non-actionable item explaining why.
- if (!enabled) {
- return (
-
- {icon}
- {item.label}
-
- );
- }
-
- // Connected: manage the existing account(s) or add another.
- if (accountCount > 0) {
- return (
-
-
- {icon}
- {item.label}
- {accountCount}
-
-
-
- gate
- ? gate("manage import connectors")
- : setImportRequest({ connectorType: item.type, mode: "auto" })
- }
- >
-
- {accountCount > 1 ? "Manage accounts" : "Manage"}
-
-
- gate
- ? gate("import from cloud storage")
- : setImportRequest({ connectorType: item.type, mode: "connect" })
- }
- >
-
- Add another account
-
-
-
- );
- }
-
- // Not connected: single click starts the first OAuth connect.
- return (
-
- gate
- ? gate("import from cloud storage")
- : setImportRequest({ connectorType: item.type, mode: "auto" })
- }
- >
- {icon}
- {item.label}
-
- );
- })}
- {isDesktop && !gate && (
+ {!gate && (
navItems.find((item) => item.url.endsWith("/artifacts")),
[navItems]
);
+ const connectorsItem = useMemo(
+ () => navItems.find((item) => item.url.endsWith("/connectors")),
+ [navItems]
+ );
const playgroundItem = useMemo(
() => navItems.find((item) => item.url.endsWith("/playground")),
[navItems]
@@ -150,6 +154,7 @@ export function Sidebar({
(item) =>
!item.url.endsWith("/automations") &&
!item.url.endsWith("/artifacts") &&
+ !item.url.endsWith("/connectors") &&
!item.url.endsWith("/playground")
),
[navItems]
@@ -254,6 +259,16 @@ export function Sidebar({
tooltipContent={isCollapsed ? artifactsItem.title : undefined}
/>
)}
+ {connectorsItem && (
+ onNavItemClick?.(connectorsItem)}
+ isCollapsed={isCollapsed}
+ isActive={connectorsItem.isActive}
+ tooltipContent={isCollapsed ? connectorsItem.title : undefined}
+ />
+ )}
{playgroundItem && (
+
+
+ Public chats allow anyone with the URL to view a snapshot of a chat. They do not update when
+ the original chat changes.
+
+
+);
+
export function PublicChatSnapshotsManager({
workspaceId: _workspaceId,
}: PublicChatSnapshotsManagerProps) {
@@ -81,14 +91,7 @@ export function PublicChatSnapshotsManager({
if (isLoading) {
return (
-
-
-
-
-
-
-
-
+ {infoAlert}
{/* Cards grid skeleton */}
@@ -135,13 +138,7 @@ export function PublicChatSnapshotsManager({
return (
-
-
-
- Public chats allow anyone with the URL to view a snapshot of a chat. They do not update
- when the original chat changes.
-
-
+ {infoAlert}
;
case EnumConnectorName.MCP_CONNECTOR:
- return
;
+ // Masked so the black glyph inherits currentColor (white in dark mode).
+ return (
+
+ );
case EnumConnectorName.OBSIDIAN_CONNECTOR:
return
;
case EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR:
diff --git a/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx b/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx
index 642fed1c1..809e4cf1a 100644
--- a/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx
+++ b/surfsense_web/features/artifacts-library/ui/artifacts-library.tsx
@@ -1,7 +1,7 @@
"use client";
import { useSetAtom } from "jotai";
-import { Boxes, RefreshCw, TriangleAlert } from "lucide-react";
+import { RefreshCw, Shapes, TriangleAlert } from "lucide-react";
import { useMemo, useState } from "react";
import { openReportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { MobileReportPanel } from "@/components/report-panel/report-panel";
@@ -48,7 +48,7 @@ function EmptyState() {
return (
-
+
No artifacts yet
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index 50b92b1f7..c9b73a09a 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat deleted successfully",
"error_deleting_chat": "Failed to delete chat",
"delete": "Delete",
- "try_different_search": "Try a different search term",
"updated": "Updated",
"more_options": "More options",
"clear_search": "Clear search",
diff --git a/surfsense_web/messages/es.json b/surfsense_web/messages/es.json
index ada68e841..84696e86e 100644
--- a/surfsense_web/messages/es.json
+++ b/surfsense_web/messages/es.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat eliminado correctamente",
"error_deleting_chat": "Error al eliminar el chat",
"delete": "Eliminar",
- "try_different_search": "Intenta con un término de búsqueda diferente",
"updated": "Actualizado",
"more_options": "Más opciones",
"clear_search": "Limpiar búsqueda",
diff --git a/surfsense_web/messages/hi.json b/surfsense_web/messages/hi.json
index 3b59c58dd..361284c2b 100644
--- a/surfsense_web/messages/hi.json
+++ b/surfsense_web/messages/hi.json
@@ -664,7 +664,6 @@
"chat_deleted": "चैट सफलतापूर्वक हटाया गया",
"error_deleting_chat": "चैट हटाने में विफल",
"delete": "हटाएं",
- "try_different_search": "कोई अलग खोज शब्द आज़माएं",
"updated": "अपडेट किया गया",
"more_options": "और विकल्प",
"clear_search": "खोज साफ करें",
diff --git a/surfsense_web/messages/ko.json b/surfsense_web/messages/ko.json
index 3b5d88852..5dd65166d 100644
--- a/surfsense_web/messages/ko.json
+++ b/surfsense_web/messages/ko.json
@@ -664,7 +664,6 @@
"chat_deleted": "채팅이 성공적으로 삭제되었습니다",
"error_deleting_chat": "채팅 삭제에 실패했습니다",
"delete": "삭제",
- "try_different_search": "다른 검색어를 시도해보세요",
"updated": "업데이트됨",
"more_options": "추가 옵션",
"clear_search": "검색 지우기",
diff --git a/surfsense_web/messages/pt.json b/surfsense_web/messages/pt.json
index 30fd09792..5cd735180 100644
--- a/surfsense_web/messages/pt.json
+++ b/surfsense_web/messages/pt.json
@@ -664,7 +664,6 @@
"chat_deleted": "Chat excluído com sucesso",
"error_deleting_chat": "Falha ao excluir chat",
"delete": "Excluir",
- "try_different_search": "Tente um termo de pesquisa diferente",
"updated": "Atualizado",
"more_options": "Mais opções",
"clear_search": "Limpar pesquisa",
diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json
index 0a3cd6905..669e16e2b 100644
--- a/surfsense_web/messages/zh.json
+++ b/surfsense_web/messages/zh.json
@@ -663,7 +663,6 @@
"chat_deleted": "对话删除成功",
"error_deleting_chat": "删除对话失败",
"delete": "删除",
- "try_different_search": "尝试其他搜索词",
"updated": "更新时间",
"more_options": "更多选项",
"clear_search": "清除搜索",