feat: docs and ui tweaks

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-07-06 19:26:35 -07:00
parent 64f2b4a6eb
commit 271a21aee6
103 changed files with 2161 additions and 2625 deletions

View file

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

View file

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

View file

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

View file

@ -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 "—";

View file

@ -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,
]);
/**

View file

@ -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);
}, []);

View file

@ -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]) => {

View file

@ -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>
);
};

View file

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