From cf76f6f57593719502f4043c65922e5e37d9e4e1 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 9 Feb 2026 02:49:08 +0530 Subject: [PATCH 1/3] feat: improve integration UI --- surfsense_web/app/globals.css | 19 ++ .../components/homepage/integrations.tsx | 319 +++++++++--------- surfsense_web/public/connectors/composio.svg | 12 - 3 files changed, 187 insertions(+), 163 deletions(-) delete mode 100644 surfsense_web/public/connectors/composio.svg diff --git a/surfsense_web/app/globals.css b/surfsense_web/app/globals.css index cf6f48437..3b87e4356 100644 --- a/surfsense_web/app/globals.css +++ b/surfsense_web/app/globals.css @@ -187,5 +187,24 @@ button { background-color: hsl(var(--muted-foreground) / 0.4); } +/* Integrations section — vertical column auto-scroll */ +@keyframes integrations-scroll-up { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(-50%); + } +} + +@keyframes integrations-scroll-down { + 0% { + transform: translateY(-50%); + } + 100% { + transform: translateY(0); + } +} + @source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'; @source '../node_modules/streamdown/dist/*.js'; diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx index 53aaf624a..d8361d721 100644 --- a/surfsense_web/components/homepage/integrations.tsx +++ b/surfsense_web/components/homepage/integrations.tsx @@ -1,5 +1,6 @@ "use client"; -import React, { useEffect, useState } from "react"; + +import type React from "react"; interface Integration { name: string; @@ -8,181 +9,197 @@ interface Integration { const INTEGRATIONS: Integration[] = [ // Search - { name: "Tavily", icon: "https://www.tavily.com/images/logo.svg" }, - { - name: "LinkUp", - icon: "https://framerusercontent.com/images/7zeIm6t3f1HaSltkw8upEvsD80.png?scale-down-to=512", - }, - { name: "Elasticsearch", icon: "https://cdn.simpleicons.org/elastic/00A9E5" }, + { name: "Tavily", icon: "/connectors/tavily.svg" }, + { name: "Elasticsearch", icon: "/connectors/elasticsearch.svg" }, + { name: "Baidu Search", icon: "/connectors/baidu-search.svg" }, + { name: "SearXNG", icon: "/connectors/searxng.svg" }, // Communication - { - name: "Slack", - icon: "https://upload.wikimedia.org/wikipedia/commons/d/d5/Slack_icon_2019.svg", - }, - { name: "Discord", icon: "https://cdn.simpleicons.org/discord/5865F2" }, - { name: "Gmail", icon: "https://cdn.simpleicons.org/gmail/EA4335" }, + { name: "Slack", icon: "/connectors/slack.svg" }, + { name: "Discord", icon: "/connectors/discord.svg" }, + { name: "Gmail", icon: "/connectors/google-gmail.svg" }, + { name: "Microsoft Teams", icon: "/connectors/microsoft-teams.svg" }, // Project Management - { name: "Linear", icon: "https://cdn.simpleicons.org/linear/5E6AD2" }, - { name: "Jira", icon: "https://cdn.simpleicons.org/jira/0052CC" }, - { name: "ClickUp", icon: "https://cdn.simpleicons.org/clickup/7B68EE" }, - { name: "Airtable", icon: "https://cdn.simpleicons.org/airtable/18BFFF" }, + { name: "Linear", icon: "/connectors/linear.svg" }, + { name: "Jira", icon: "/connectors/jira.svg" }, + { name: "ClickUp", icon: "/connectors/clickup.svg" }, + { name: "Airtable", icon: "/connectors/airtable.svg" }, // Documentation & Knowledge - { name: "Confluence", icon: "https://cdn.simpleicons.org/confluence/172B4D" }, - { name: "Notion", icon: "https://cdn.simpleicons.org/notion/000000/ffffff" }, - { name: "Web Pages", icon: "https://cdn.jsdelivr.net/npm/lucide-static@0.294.0/icons/globe.svg" }, + { name: "Confluence", icon: "/connectors/confluence.svg" }, + { name: "Notion", icon: "/connectors/notion.svg" }, + { name: "BookStack", icon: "/connectors/bookstack.svg" }, + { name: "Obsidian", icon: "/connectors/obsidian.svg" }, // Cloud Storage - { name: "Google Drive", icon: "https://cdn.simpleicons.org/googledrive/4285F4" }, - { name: "Dropbox", icon: "https://cdn.simpleicons.org/dropbox/0061FF" }, - { - name: "Amazon S3", - icon: "https://upload.wikimedia.org/wikipedia/commons/b/bc/Amazon-S3-Logo.svg", - }, + { name: "Google Drive", icon: "/connectors/google-drive.svg" }, // Development - { name: "GitHub", icon: "https://cdn.simpleicons.org/github/181717/ffffff" }, + { name: "GitHub", icon: "/connectors/github.svg" }, // Productivity - { name: "Google Calendar", icon: "https://cdn.simpleicons.org/googlecalendar/4285F4" }, - { name: "Luma", icon: "https://images.lumacdn.com/social-images/default-social-202407.png" }, + { name: "Google Calendar", icon: "/connectors/google-calendar.svg" }, + { name: "Luma", icon: "/connectors/luma.svg" }, // Media - { name: "YouTube", icon: "https://cdn.simpleicons.org/youtube/FF0000" }, + { name: "YouTube", icon: "/connectors/youtube.svg" }, ]; -function SemiCircleOrbit({ radius, centerX, centerY, count, iconSize, startIndex }: any) { +// 5 vertical columns — 21 icons spread across categories +const COLUMNS: number[][] = [ + [2, 5, 10, 0, 11], + [1, 7, 20, 17], + [13, 6, 4, 16], + [12, 8, 15, 18], + [3, 9, 14, 19], +]; + +// Different scroll speeds per column for organic feel (seconds) +const SCROLL_DURATIONS = [26, 32, 22, 30, 28]; + +function IntegrationCard({ integration }: { integration: Integration }) { return ( - <> - {/* Semi-circle glow background */} -
-
+
+ {integration.name} +
+ ); +} + +function ScrollingColumn({ + cards, + scrollUp, + duration, + colIndex, + isEdge, + isEdgeAdjacent, +}: { + cards: number[]; + scrollUp: boolean; + duration: number; + colIndex: number; + isEdge: boolean; + isEdgeAdjacent: boolean; +}) { + // Edge columns get a heavy vertical mask; edge-adjacent columns get a lighter one to smooth the transition + const columnMask = isEdge + ? { + maskImage: + "linear-gradient(to bottom, transparent 0%, transparent 20%, black 40%, black 60%, transparent 80%, transparent 100%)", + WebkitMaskImage: + "linear-gradient(to bottom, transparent 0%, transparent 20%, black 40%, black 60%, transparent 80%, transparent 100%)", + } + : isEdgeAdjacent + ? { + maskImage: + "linear-gradient(to bottom, transparent 0%, transparent 10%, black 30%, black 70%, transparent 90%, transparent 100%)", + WebkitMaskImage: + "linear-gradient(to bottom, transparent 0%, transparent 10%, black 30%, black 70%, transparent 90%, transparent 100%)", + } + : {}; + + const cardSet = cards.map((integrationIndex, i) => ( + + )); + + return ( +
+ {/* Outer div has NO gap — each inner copy uses pb matching the gap so both halves are identical in height → seamless -50% loop */} +
+
+ {cardSet} +
+
+ {cardSet} +
- - {/* Orbit icons */} - {Array.from({ length: count }).map((_, index) => { - const actualIndex = startIndex + index; - // Skip if we've run out of integrations - if (actualIndex >= INTEGRATIONS.length) return null; - - const angle = (index / (count - 1)) * 180; - const x = radius * Math.cos((angle * Math.PI) / 180); - const y = radius * Math.sin((angle * Math.PI) / 180); - const integration = INTEGRATIONS[actualIndex]; - - // Tooltip positioning — above or below based on angle - const tooltipAbove = angle > 90; - - return ( -
- {integration.name} - - {/* Tooltip */} - -
- ); - })} - +
); } export default function ExternalIntegrations() { - const [size, setSize] = useState({ width: 0, height: 0 }); - - useEffect(() => { - const updateSize = () => setSize({ width: window.innerWidth, height: window.innerHeight }); - updateSize(); - window.addEventListener("resize", updateSize); - return () => window.removeEventListener("resize", updateSize); - }, []); - - const baseWidth = Math.min(size.width * 0.8, 700); - const centerX = baseWidth / 2; - const centerY = baseWidth * 0.5; - - const iconSize = - size.width < 480 - ? Math.max(24, baseWidth * 0.05) - : size.width < 768 - ? Math.max(28, baseWidth * 0.06) - : Math.max(32, baseWidth * 0.07); - return ( -
-
-

Integrations

-

- Integrate with your team's most important tools -

+
+ {/* Heading */} +
+

+ Integrate with your +
+ team's most important tools +

+
-
- - - + {/* 5 scrolling columns */} +
+ {COLUMNS.map((column, colIndex) => ( + + ))}
diff --git a/surfsense_web/public/connectors/composio.svg b/surfsense_web/public/connectors/composio.svg deleted file mode 100644 index 7c06babeb..000000000 --- a/surfsense_web/public/connectors/composio.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - From 038cdb3ed32e7a77a10575431185825d8933c5dd Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:24:38 +0530 Subject: [PATCH 2/3] feat: add new integrations and update icon handling in the homepage --- .../components/homepage/integrations.tsx | 35 +++++++++---- .../contracts/enums/connectorIcons.tsx | 10 ++-- .../public/connectors/circleback.svg | 19 +++++++ surfsense_web/public/connectors/linkup.svg | 50 +++++++++++++++++++ 4 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 surfsense_web/public/connectors/circleback.svg create mode 100644 surfsense_web/public/connectors/linkup.svg diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx index d8361d721..fc250e65b 100644 --- a/surfsense_web/components/homepage/integrations.tsx +++ b/surfsense_web/components/homepage/integrations.tsx @@ -1,6 +1,7 @@ "use client"; import type React from "react"; +import Image from "next/image"; interface Integration { name: string; @@ -44,15 +45,24 @@ const INTEGRATIONS: Integration[] = [ // Media { name: "YouTube", icon: "/connectors/youtube.svg" }, + + // Search + { name: "Linkup", icon: "/connectors/linkup.svg" }, + + // Meetings + { name: "Circleback", icon: "/connectors/circleback.svg" }, + + // AI + { name: "MCP", icon: "/connectors/modelcontextprotocol.svg" }, ]; -// 5 vertical columns — 21 icons spread across categories +// 5 vertical columns — 23 icons spread across categories const COLUMNS: number[][] = [ - [2, 5, 10, 0, 11], + [2, 5, 10, 0, 21, 11], [1, 7, 20, 17], - [13, 6, 4, 16], + [13, 6, 23, 4, 16], [12, 8, 15, 18], - [3, 9, 14, 19], + [3, 9, 14, 22, 19], ]; // Different scroll speeds per column for organic feel (seconds) @@ -61,7 +71,7 @@ const SCROLL_DURATIONS = [26, 32, 22, 30, 28]; function IntegrationCard({ integration }: { integration: Integration }) { return (
- {integration.name} + {integration.name}
); } diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index 18a872d94..e29e5be6e 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -1,4 +1,4 @@ -import { IconLinkPlus, IconUsersGroup } from "@tabler/icons-react"; +import { IconUsersGroup } from "@tabler/icons-react"; import { BookOpen, File, @@ -15,11 +15,11 @@ import { EnumConnectorName } from "./connector"; export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => { const iconProps = { className: className || "h-4 w-4" }; - const imgProps = { className: className || "h-5 w-5", width: 20, height: 20 }; + const imgProps = { className: `${className || "h-5 w-5"} select-none pointer-events-none`, width: 20, height: 20, draggable: false as const }; switch (connectorType) { - case EnumConnectorName.LINKUP_API: - return ; + case EnumConnectorName.LINKUP_API: + return Linkup; case EnumConnectorName.LINEAR_CONNECTOR: return Linear; case EnumConnectorName.GITHUB_CONNECTOR: @@ -63,7 +63,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas case EnumConnectorName.YOUTUBE_CONNECTOR: return YouTube; case EnumConnectorName.CIRCLEBACK_CONNECTOR: - return ; + return Circleback; case EnumConnectorName.MCP_CONNECTOR: return MCP; case EnumConnectorName.OBSIDIAN_CONNECTOR: diff --git a/surfsense_web/public/connectors/circleback.svg b/surfsense_web/public/connectors/circleback.svg new file mode 100644 index 000000000..76bdcddd8 --- /dev/null +++ b/surfsense_web/public/connectors/circleback.svg @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/surfsense_web/public/connectors/linkup.svg b/surfsense_web/public/connectors/linkup.svg new file mode 100644 index 000000000..8b0ffb071 --- /dev/null +++ b/surfsense_web/public/connectors/linkup.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + From dfa05917404f00e2a654bba9ae970232020a8657 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:24:47 +0530 Subject: [PATCH 3/3] chore: ran linting --- .../(manage)/components/RowActions.tsx | 3 +- .../components/homepage/integrations.tsx | 61 ++++++++++--------- .../contracts/enums/connectorIcons.tsx | 11 +++- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx index eb44d114a..5c5a44964 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/RowActions.tsx @@ -208,7 +208,8 @@ export function RowActions({ Delete document? - This action cannot be undone. This will permanently delete this document from your search space. + This action cannot be undone. This will permanently delete this document from your + search space. diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx index fc250e65b..662387de5 100644 --- a/surfsense_web/components/homepage/integrations.tsx +++ b/surfsense_web/components/homepage/integrations.tsx @@ -73,21 +73,19 @@ function IntegrationCard({ integration }: { integration: Integration }) {
- {integration.name} + {integration.name}
); } @@ -132,7 +130,10 @@ function ScrollingColumn({ )); return ( -
+
{/* Outer div has NO gap — each inner copy uses pb matching the gap so both halves are identical in height → seamless -50% loop */}
{/* Heading */} @@ -203,15 +204,15 @@ export default function ExternalIntegrations() { {/* 5 scrolling columns */}
{COLUMNS.map((column, colIndex) => ( - + ))}
diff --git a/surfsense_web/contracts/enums/connectorIcons.tsx b/surfsense_web/contracts/enums/connectorIcons.tsx index e29e5be6e..c9375a5ca 100644 --- a/surfsense_web/contracts/enums/connectorIcons.tsx +++ b/surfsense_web/contracts/enums/connectorIcons.tsx @@ -15,11 +15,16 @@ import { EnumConnectorName } from "./connector"; export const getConnectorIcon = (connectorType: EnumConnectorName | string, className?: string) => { const iconProps = { className: className || "h-4 w-4" }; - const imgProps = { className: `${className || "h-5 w-5"} select-none pointer-events-none`, width: 20, height: 20, draggable: false as const }; + const imgProps = { + className: `${className || "h-5 w-5"} select-none pointer-events-none`, + width: 20, + height: 20, + draggable: false as const, + }; switch (connectorType) { - case EnumConnectorName.LINKUP_API: - return Linkup; + case EnumConnectorName.LINKUP_API: + return Linkup; case EnumConnectorName.LINEAR_CONNECTOR: return Linear; case EnumConnectorName.GITHUB_CONNECTOR: