mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-08 22:22:17 +02:00
feat: docs and ui tweaks
This commit is contained in:
parent
64f2b4a6eb
commit
271a21aee6
103 changed files with 2161 additions and 2625 deletions
|
|
@ -189,7 +189,7 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
|
|||
}}
|
||||
className="max-w-3xl w-[95vw] sm:w-full h-[75vh] sm:h-[85vh] flex flex-col p-0 gap-0 overflow-hidden ring-0 dark:ring-0 [&>button]:right-4 sm:[&>button]:right-12 [&>button]:top-6 sm:[&>button]:top-10 [&>button]:opacity-80 [&>button]:hover:opacity-100 [&>button]:hover:bg-accent [&>button]:hover:text-accent-foreground [&>button>svg]:size-5 select-none"
|
||||
>
|
||||
<DialogTitle className="sr-only">Manage Connectors</DialogTitle>
|
||||
<DialogTitle className="sr-only">Manage External MCP Connectors</DialogTitle>
|
||||
{/* YouTube Crawler View - shown when adding YouTube videos */}
|
||||
{isYouTubeView && workspaceId ? (
|
||||
<YouTubeCrawlerView workspaceId={workspaceId} onBack={handleBackFromYouTube} />
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const ConnectorDialogHeader: FC<ConnectorDialogHeaderProps> = ({
|
|||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl sm:text-3xl font-semibold tracking-tight">
|
||||
Manage Connectors
|
||||
Manage External MCP Connectors
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-xs sm:text-base text-muted-foreground/80 mt-1 sm:mt-1.5">
|
||||
Connect Surfsense to your favorite tools and services.
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ export const GithubConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting
|
|||
{/* Documentation Link */}
|
||||
<div>
|
||||
<Link
|
||||
href="/docs/connectors/github"
|
||||
href="/docs/connectors/external/github"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs sm:text-sm font-medium underline underline-offset-4 hover:text-primary transition-colors inline-flex items-center gap-1.5"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|||
import { connectorsApiService, type ObsidianStats } from "@/lib/apis/connectors-api.service";
|
||||
import type { ConnectorConfigProps } from "../index";
|
||||
|
||||
const OBSIDIAN_SETUP_DOCS_URL = "/docs/connectors/obsidian";
|
||||
const OBSIDIAN_SETUP_DOCS_URL = "/docs/connectors/external/obsidian";
|
||||
|
||||
function formatTimestamp(value: unknown): string {
|
||||
if (typeof value !== "string" || !value) return "—";
|
||||
|
|
|
|||
|
|
@ -18,6 +18,23 @@ export const DEPRECATED_CONNECTOR_TYPES = new Set<string>([
|
|||
EnumConnectorName.SEARXNG_API,
|
||||
EnumConnectorName.LINKUP_API,
|
||||
EnumConnectorName.BAIDU_SEARCH_API,
|
||||
// Legacy content crawlers/search retired in favor of the file Import menu and
|
||||
// hosted MCP tooling. Existing rows stay manageable; new connections refused.
|
||||
EnumConnectorName.YOUTUBE_CONNECTOR,
|
||||
EnumConnectorName.WEBCRAWLER_CONNECTOR,
|
||||
EnumConnectorName.ELASTICSEARCH_CONNECTOR,
|
||||
]);
|
||||
|
||||
/**
|
||||
* File-import connectors surfaced through the Documents sidebar "Import" menu
|
||||
* instead of the external-MCP connector catalog. They index remote files into
|
||||
* the knowledge base, so they belong with uploads, not with live MCP tools.
|
||||
*/
|
||||
export const IMPORT_CONNECTOR_TYPES = new Set<string>([
|
||||
EnumConnectorName.GOOGLE_DRIVE_CONNECTOR,
|
||||
EnumConnectorName.COMPOSIO_GOOGLE_DRIVE_CONNECTOR,
|
||||
EnumConnectorName.ONEDRIVE_CONNECTOR,
|
||||
EnumConnectorName.DROPBOX_CONNECTOR,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { format } from "date-fns";
|
|||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { connectorDialogOpenAtom } from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import {
|
||||
connectorDialogOpenAtom,
|
||||
importConnectorRequestAtom,
|
||||
} from "@/atoms/connector-dialog/connector-dialog.atoms";
|
||||
import {
|
||||
createConnectorMutationAtom,
|
||||
deleteConnectorMutationAtom,
|
||||
|
|
@ -67,6 +70,8 @@ export const useConnectorDialog = () => {
|
|||
|
||||
// Use global atom for dialog open state so it can be controlled from anywhere
|
||||
const [isOpen, setIsOpen] = useAtom(connectorDialogOpenAtom);
|
||||
// Import-flow request from the Documents sidebar "Import" menu
|
||||
const [importConnectorRequest, setImportConnectorRequest] = useAtom(importConnectorRequestAtom);
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [connectingId, setConnectingId] = useState<string | null>(null);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
|
@ -1372,6 +1377,47 @@ export const useConnectorDialog = () => {
|
|||
[isStartingIndexing, isDisconnecting, isSaving, isCreatingConnector, setIsOpen]
|
||||
);
|
||||
|
||||
// Consume an import request from the Documents sidebar "Import" menu.
|
||||
// mode "connect" -> always OAuth connect (add account). mode "auto" routes by
|
||||
// account count: none -> connect, one -> edit view, many -> accounts list.
|
||||
useEffect(() => {
|
||||
if (!importConnectorRequest || !workspaceId) return;
|
||||
|
||||
const { connectorType, mode } = importConnectorRequest;
|
||||
setImportConnectorRequest(null);
|
||||
|
||||
const connectorDef =
|
||||
OAUTH_CONNECTORS.find((c) => c.connectorType === connectorType) ||
|
||||
COMPOSIO_CONNECTORS.find((c) => c.connectorType === connectorType);
|
||||
|
||||
const existing = (allConnectors || []).filter(
|
||||
(c: SearchSourceConnector) => c.connector_type === connectorType
|
||||
);
|
||||
|
||||
if (mode === "connect" || existing.length === 0) {
|
||||
if (connectorDef) {
|
||||
handleConnectOAuth(connectorDef);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setIsOpen(true);
|
||||
if (existing.length === 1) {
|
||||
handleStartEdit(existing[0]);
|
||||
} else {
|
||||
handleViewAccountsList(connectorType, connectorDef?.title);
|
||||
}
|
||||
}, [
|
||||
importConnectorRequest,
|
||||
workspaceId,
|
||||
allConnectors,
|
||||
setImportConnectorRequest,
|
||||
handleConnectOAuth,
|
||||
handleStartEdit,
|
||||
handleViewAccountsList,
|
||||
setIsOpen,
|
||||
]);
|
||||
|
||||
const handleTabChange = useCallback((value: string) => {
|
||||
setActiveTab(value);
|
||||
}, []);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getDocumentTypeLabel } from "@/lib/documents/document-type-labels";
|
|||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
COMPOSIO_CONNECTORS,
|
||||
IMPORT_CONNECTOR_TYPES,
|
||||
LIVE_CONNECTOR_TYPES,
|
||||
OAUTH_CONNECTORS,
|
||||
} from "../constants/connector-constants";
|
||||
|
|
@ -33,12 +34,17 @@ export const ActiveConnectorsTab: FC<ActiveConnectorsTabProps> = ({
|
|||
searchQuery,
|
||||
hasSources,
|
||||
activeDocumentTypes,
|
||||
connectors,
|
||||
connectors: allActiveConnectors,
|
||||
indexingConnectorIds,
|
||||
onTabChange: _onTabChange,
|
||||
onManage,
|
||||
onViewAccountsList,
|
||||
}) => {
|
||||
// Import connectors (Drive/OneDrive/Dropbox) are managed via the Documents
|
||||
// sidebar "Import" menu, so they are excluded from the MCP connector list.
|
||||
const connectors = allActiveConnectors.filter(
|
||||
(c) => !IMPORT_CONNECTOR_TYPES.has(c.connector_type)
|
||||
);
|
||||
// Convert activeDocumentTypes array to Record for utility function
|
||||
const documentTypeCounts = activeDocumentTypes.reduce(
|
||||
(acc, [docType, count]) => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
CRAWLERS,
|
||||
DEPRECATED_CONNECTOR_TYPES,
|
||||
getConnectorCategory,
|
||||
IMPORT_CONNECTOR_TYPES,
|
||||
OAUTH_CONNECTORS,
|
||||
OTHER_CONNECTORS,
|
||||
} from "../constants/connector-constants";
|
||||
|
|
@ -82,30 +83,41 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
const passesDeploymentFilter = (c: DeploymentFilterableConnector) =>
|
||||
(!c.selfHostedOnly || selfHosted) && (!c.desktopOnly || isDesktop);
|
||||
|
||||
// Filter connectors based on search and deployment mode
|
||||
// Import connectors (Drive/OneDrive/Dropbox) are managed via the Documents
|
||||
// sidebar "Import" menu, so they never appear in the MCP connector catalog.
|
||||
const notImport = (c: { connectorType?: string }) =>
|
||||
!c.connectorType || !IMPORT_CONNECTOR_TYPES.has(c.connectorType);
|
||||
|
||||
// Filter connectors based on search, deployment mode, and import exclusion
|
||||
const filteredOAuth = OAUTH_CONNECTORS.filter(
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c)
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c) && notImport(c)
|
||||
);
|
||||
|
||||
const filteredCrawlers = CRAWLERS.filter(
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c)
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c) && notImport(c)
|
||||
);
|
||||
|
||||
const filteredOther = OTHER_CONNECTORS.filter(
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c)
|
||||
(c) => matchesSearch(c.title, c.description) && passesDeploymentFilter(c) && notImport(c)
|
||||
);
|
||||
|
||||
// Filter Composio connectors
|
||||
const filteredComposio = COMPOSIO_CONNECTORS.filter(
|
||||
(c) =>
|
||||
c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.description.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
(c.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
c.description.toLowerCase().includes(searchQuery.toLowerCase())) &&
|
||||
notImport(c)
|
||||
);
|
||||
|
||||
const isDeprecated = (c: { connectorType?: string }) =>
|
||||
!!c.connectorType && DEPRECATED_CONNECTOR_TYPES.has(c.connectorType);
|
||||
|
||||
const inCategory =
|
||||
(category: ConnectorCategory) =>
|
||||
<T extends { connectorType?: string }>(connector: T): boolean =>
|
||||
!!connector.connectorType && getConnectorCategory(connector.connectorType) === category;
|
||||
!isDeprecated(connector) &&
|
||||
!!connector.connectorType &&
|
||||
getConnectorCategory(connector.connectorType) === category;
|
||||
|
||||
const knowledgeBase = {
|
||||
oauth: filteredOAuth.filter(inCategory("knowledge_base")),
|
||||
|
|
@ -120,6 +132,16 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
crawlers: filteredCrawlers.filter(inCategory("tools_live")),
|
||||
};
|
||||
|
||||
// Deprecated cards render in a single section at the very bottom. Discord and
|
||||
// Teams come from OAUTH_CONNECTORS; Luma, Tavily, Linkup, Baidu, Elasticsearch
|
||||
// from OTHER_CONNECTORS (SearXNG has no catalog card); YouTube and Web Pages
|
||||
// from CRAWLERS.
|
||||
const deprecated = {
|
||||
oauth: filteredOAuth.filter(isDeprecated),
|
||||
other: filteredOther.filter(isDeprecated),
|
||||
crawlers: filteredCrawlers.filter(isDeprecated),
|
||||
};
|
||||
|
||||
const renderOAuthCard = (connector: OAuthConnector | ComposioConnector) => {
|
||||
const isConnected = connectedTypes.has(connector.connectorType);
|
||||
const isConnecting = connectingId === connector.id;
|
||||
|
|
@ -245,6 +267,9 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
isConnecting={isConnecting}
|
||||
documentCount={documentCount}
|
||||
isIndexing={isIndexing}
|
||||
deprecated={
|
||||
!!crawler.connectorType && DEPRECATED_CONNECTOR_TYPES.has(crawler.connectorType)
|
||||
}
|
||||
onConnect={handleConnect}
|
||||
onManage={actualConnector && onManage ? () => onManage(actualConnector) : undefined}
|
||||
/>
|
||||
|
|
@ -261,8 +286,10 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
toolsLive.composio.length > 0 ||
|
||||
toolsLive.other.length > 0 ||
|
||||
toolsLive.crawlers.length > 0;
|
||||
const hasDeprecated =
|
||||
deprecated.oauth.length > 0 || deprecated.other.length > 0 || deprecated.crawlers.length > 0;
|
||||
|
||||
const hasAnyResults = hasKnowledgeBase || hasToolsLive;
|
||||
const hasAnyResults = hasKnowledgeBase || hasToolsLive || hasDeprecated;
|
||||
|
||||
if (!hasAnyResults && searchQuery) {
|
||||
return (
|
||||
|
|
@ -307,6 +334,19 @@ export const AllConnectorsTab: FC<AllConnectorsTabProps> = ({
|
|||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{hasDeprecated && (
|
||||
<section>
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<h3 className="text-sm font-semibold text-muted-foreground">Deprecated</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{deprecated.oauth.map(renderOAuthCard)}
|
||||
{deprecated.crawlers.map(renderCrawlerCard)}
|
||||
{deprecated.other.map(renderOtherCard)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
Wrench,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { useParams } from "next/navigation";
|
||||
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -56,7 +56,6 @@ import { currentUserAtom } from "@/atoms/user/user-query.atoms";
|
|||
import { AssistantMessage } from "@/components/assistant-ui/assistant-message";
|
||||
import { ChatSessionStatus } from "@/components/assistant-ui/chat-session-status";
|
||||
import { ChatViewport } from "@/components/assistant-ui/chat-viewport";
|
||||
import { ConnectorIndicator } from "@/components/assistant-ui/connector-popup";
|
||||
import { useDocumentUploadDialog } from "@/components/assistant-ui/document-upload-popup";
|
||||
import {
|
||||
InlineMentionEditor,
|
||||
|
|
@ -94,6 +93,7 @@ import {
|
|||
import { Popover, PopoverAnchor } from "@/components/ui/popover";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
|
||||
import {
|
||||
CONNECTOR_ICON_TO_TYPES,
|
||||
|
|
@ -105,9 +105,11 @@ import { useBatchCommentsPreload } from "@/hooks/use-comments";
|
|||
import { useCommentsSync } from "@/hooks/use-comments-sync";
|
||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||
import { useElectronAPI } from "@/hooks/use-platform";
|
||||
import { useScraperCapabilities } from "@/hooks/use-scraper-capabilities";
|
||||
import { captureDisplayToPngDataUrl } from "@/lib/chat/display-media-capture";
|
||||
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
|
||||
import { slideoutOpenedTickAtom } from "@/lib/layout-events";
|
||||
import { findPlatform, type PlaygroundPlatform } from "@/lib/playground/catalog";
|
||||
import { getWorkspaceIdNumber } from "@/lib/route-params";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
|
|
@ -965,7 +967,6 @@ const Composer: FC = () => {
|
|||
workspaceId={workspaceId ?? 0}
|
||||
onChatModelSelected={handleChatModelSelected}
|
||||
/>
|
||||
<ConnectorIndicator showTrigger={false} />
|
||||
</div>
|
||||
<ConnectToolsBanner
|
||||
isThreadEmpty={isThreadEmpty}
|
||||
|
|
@ -981,6 +982,66 @@ const Composer: FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Full-color brand marks for the platform-native scraper APIs (web, Google
|
||||
* Search, Google Maps, Reddit, YouTube) available in this workspace, shown beside the
|
||||
* composer "+" so the user can see these native endpoints are connected. Laid
|
||||
* out as a clean row (not stacked) after a hairline divider that separates them
|
||||
* from the composer actions. The capability registry is the source of truth;
|
||||
* icons are display-only with a status tooltip. One-time staggered entrance,
|
||||
* reduced-motion aware.
|
||||
*/
|
||||
const ConnectedScraperIcons: FC<{ workspaceId: number }> = ({ workspaceId }) => {
|
||||
const { data: capabilities } = useScraperCapabilities(workspaceId);
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
const platforms = useMemo<PlaygroundPlatform[]>(() => {
|
||||
if (!capabilities?.length) return [];
|
||||
const seen = new Set<string>();
|
||||
const result: PlaygroundPlatform[] = [];
|
||||
for (const cap of capabilities) {
|
||||
const platformId = cap.name.split(".")[0];
|
||||
if (seen.has(platformId)) continue;
|
||||
seen.add(platformId);
|
||||
const platform = findPlatform(platformId);
|
||||
if (platform) result.push(platform);
|
||||
}
|
||||
return result;
|
||||
}, [capabilities]);
|
||||
|
||||
if (platforms.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="hidden items-center gap-1 sm:flex">
|
||||
<div aria-hidden className="h-5 w-px shrink-0 bg-border" />
|
||||
<div className="flex items-center gap-0.5" aria-label="Connected data sources">
|
||||
{platforms.map((platform, i) => {
|
||||
const Icon = platform.icon;
|
||||
return (
|
||||
<Tooltip key={platform.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: reduceMotion ? 0 : 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
ease: [0.23, 1, 0.32, 1],
|
||||
delay: reduceMotion ? 0 : i * 0.04,
|
||||
}}
|
||||
className="flex size-5 items-center justify-center"
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
</motion.span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{platform.label} · Connected</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ComposerActionProps {
|
||||
isBlockedByOtherUser?: boolean;
|
||||
workspaceId: number;
|
||||
|
|
@ -1142,7 +1203,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setConnectorDialogOpen(true)}>
|
||||
<Unplug className="size-4" />
|
||||
Manage Connectors
|
||||
Manage External MCP Connectors
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setToolsPopoverOpen(true)}>
|
||||
<Settings2 className="size-4" />
|
||||
|
|
@ -1362,7 +1423,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
|||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => setConnectorDialogOpen(true)}>
|
||||
<Unplug className="h-4 w-4" />
|
||||
Manage Connectors
|
||||
Manage External MCP Connectors
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub
|
||||
open={toolsPopoverOpen}
|
||||
|
|
@ -1556,6 +1617,7 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<ConnectedScraperIcons workspaceId={workspaceId} />
|
||||
</div>
|
||||
{!hasModelConfigured && (
|
||||
<div className="flex items-center gap-1.5 text-amber-600 dark:text-amber-400 text-xs">
|
||||
|
|
|
|||
|
|
@ -313,7 +313,7 @@ export function FolderTreeView({
|
|||
<div className="flex flex-1 flex-col items-center justify-center gap-1 px-4 py-12 text-muted-foreground select-none">
|
||||
<p className="text-sm font-medium">No documents found</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
Use the plus menu to upload files or manage connectors
|
||||
Use the Import button above to add files, or the plus menu to manage connectors
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,16 +2,29 @@
|
|||
|
||||
import { useQuery } from "@rocicorp/zero/react";
|
||||
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
||||
import { FolderPlus, ListFilter, SlidersVertical, Trash2 } from "lucide-react";
|
||||
import {
|
||||
FolderInput,
|
||||
FolderPlus,
|
||||
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";
|
||||
|
|
@ -19,7 +32,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 { useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { useIsSelfHosted, useRuntimeConfig } from "@/components/providers/runtime-config";
|
||||
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
|
||||
import { DEFAULT_EXCLUDE_PATTERNS } from "@/components/sources/FolderWatchDialog";
|
||||
import {
|
||||
|
|
@ -38,6 +51,7 @@ import {
|
|||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
|
|
@ -47,6 +61,9 @@ 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 { useElectronAPI, usePlatform } from "@/hooks/use-platform";
|
||||
import { documentsApiService } from "@/lib/apis/documents-api.service";
|
||||
|
|
@ -163,6 +180,127 @@ 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.
|
||||
*/
|
||||
export function EmbeddedImportMenu({ gate }: { gate?: (feature: string) => void }) {
|
||||
const { openDialog } = useDocumentUploadDialog();
|
||||
const setImportRequest = useSetAtom(importConnectorRequestAtom);
|
||||
const selfHosted = useIsSelfHosted();
|
||||
const { isConnectorEnabled, getConnectorStatusMessage } = useConnectorStatus();
|
||||
const { data: connectors } = useAtomValue(connectorsAtom);
|
||||
|
||||
// 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" },
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
aria-label="Import documents"
|
||||
>
|
||||
<FolderInput className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-56">
|
||||
<DropdownMenuItem onSelect={() => (gate ? gate("upload files") : openDialog())}>
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Files
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{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 (
|
||||
<DropdownMenuItem key={item.type} disabled title={statusMessage ?? undefined}>
|
||||
{icon}
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
// Connected: manage the existing account(s) or add another.
|
||||
if (accountCount > 0) {
|
||||
return (
|
||||
<DropdownMenuSub key={item.type}>
|
||||
<DropdownMenuSubTrigger>
|
||||
{icon}
|
||||
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">{accountCount}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-48">
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
gate
|
||||
? gate("manage import connectors")
|
||||
: setImportRequest({ connectorType: item.type, mode: "auto" })
|
||||
}
|
||||
>
|
||||
<Settings2 className="h-4 w-4" />
|
||||
{accountCount > 1 ? "Manage accounts" : "Manage"}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
gate
|
||||
? gate("import from cloud storage")
|
||||
: setImportRequest({ connectorType: item.type, mode: "connect" })
|
||||
}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add another account
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
// Not connected: single click starts the first OAuth connect.
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={item.type}
|
||||
onSelect={() =>
|
||||
gate
|
||||
? gate("import from cloud storage")
|
||||
: setImportRequest({ connectorType: item.type, mode: "auto" })
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function CloudDocumentsSkeleton() {
|
||||
const rows = [
|
||||
{ id: "row-1", widthClass: "w-44" },
|
||||
|
|
@ -1041,12 +1179,15 @@ function AuthenticatedDocumentsSidebarBase({
|
|||
defaultOpen={true}
|
||||
contentClassName="px-0"
|
||||
persistentAction={
|
||||
<EmbeddedDocumentsMenu
|
||||
typeCounts={typeCounts}
|
||||
activeTypes={activeTypes}
|
||||
onToggleType={onToggleType}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
/>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<EmbeddedImportMenu />
|
||||
<EmbeddedDocumentsMenu
|
||||
typeCounts={typeCounts}
|
||||
activeTypes={activeTypes}
|
||||
onToggleType={onToggleType}
|
||||
onCreateFolder={() => handleCreateFolder(null)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{renderDocumentTree()}
|
||||
|
|
@ -1214,12 +1355,15 @@ function AnonymousDocumentsSidebar({ embedded = false }: DocumentsSidebarProps)
|
|||
defaultOpen={true}
|
||||
contentClassName="px-0"
|
||||
persistentAction={
|
||||
<EmbeddedDocumentsMenu
|
||||
typeCounts={hasDoc ? { FILE: 1 } : {}}
|
||||
activeTypes={[]}
|
||||
onToggleType={() => {}}
|
||||
onCreateFolder={() => gate("create folders")}
|
||||
/>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<EmbeddedImportMenu gate={gate} />
|
||||
<EmbeddedDocumentsMenu
|
||||
typeCounts={hasDoc ? { FILE: 1 } : {}}
|
||||
activeTypes={[]}
|
||||
onToggleType={() => {}}
|
||||
onCreateFolder={() => gate("create folders")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FolderTreeView
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue