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 */}
-
-
+
+

+
+ );
+}
+
+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 (
-
-

-
- {/* 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 (
-

+
);
}
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
;
case EnumConnectorName.LINEAR_CONNECTOR:
return
;
case EnumConnectorName.GITHUB_CONNECTOR:
@@ -63,7 +63,7 @@ export const getConnectorIcon = (connectorType: EnumConnectorName | string, clas
case EnumConnectorName.YOUTUBE_CONNECTOR:
return
;
case EnumConnectorName.CIRCLEBACK_CONNECTOR:
- return
;
+ return
;
case EnumConnectorName.MCP_CONNECTOR:
return
;
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 }) {
-
+
);
}
@@ -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
;
+ case EnumConnectorName.LINKUP_API:
+ return
;
case EnumConnectorName.LINEAR_CONNECTOR:
return
;
case EnumConnectorName.GITHUB_CONNECTOR: