+
+
+ Everything Your Team Needs to Succeed
+
+
+ Powerful features designed to enhance collaboration, boost productivity, and streamline
+ your workflow.
+
+
-
-
-
-
-
-
- Customizable
-
-
-
- Customize your research agent to your specific needs.
-
-
-
- Streamline your workflow
+ Streamlined Workflow
- Pull all your knowledge into one place, so you can find what matters and get things
- done faster.
+ Centralize all your knowledge and resources in one intelligent workspace. Find what
+ you need instantly and accelerate decision-making.
@@ -52,7 +43,26 @@ export function Features() {
- Make your company and personal content collaborative.
+
+ Work together effortlessly with real-time collaboration tools that keep your entire
+ team aligned.
+
+
+
+
+
+
+
+
+
+
+ Fully Customizable
+
+
+
+
+ Choose from 100+ leading LLMs and seamlessly call any model on demand.
+
diff --git a/surfsense_web/components/Footer.tsx b/surfsense_web/components/homepage/footer.tsx
similarity index 97%
rename from surfsense_web/components/Footer.tsx
rename to surfsense_web/components/homepage/footer.tsx
index 074e90686..88e640e81 100644
--- a/surfsense_web/components/Footer.tsx
+++ b/surfsense_web/components/homepage/footer.tsx
@@ -22,7 +22,7 @@ export function Footer() {
];
return (
-
+
diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
new file mode 100644
index 000000000..147502afe
--- /dev/null
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -0,0 +1,318 @@
+"use client";
+import { AnimatePresence, motion } from "motion/react";
+import Image from "next/image";
+import Link from "next/link";
+import React, { useEffect, useRef, useState } from "react";
+import Balancer from "react-wrap-balancer";
+import { cn } from "@/lib/utils";
+
+export function HeroSection() {
+ const containerRef = useRef
(null);
+ const parentRef = useRef(null);
+
+ return (
+
+
+
+
+
+
+
+
+
+ The AI Workspace{" "}
+
+
+
+ {/* // TODO:aCTUAL DESCRITION */}
+
+ Your multi-collaborative AI workspace, connected to external sources.
+
+
+
+ Start Free Trial
+
+
+ Explore
+
+
+
+
+ {/* Light mode image */}
+
+ {/* Dark mode image */}
+
+
+
+
+ );
+}
+
+const BackgroundGrids = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const CollisionMechanism = React.forwardRef<
+ HTMLDivElement,
+ {
+ containerRef: React.RefObject;
+ parentRef: React.RefObject;
+ beamOptions?: {
+ initialX?: number;
+ translateX?: number;
+ initialY?: number;
+ translateY?: number;
+ rotate?: number;
+ className?: string;
+ duration?: number;
+ delay?: number;
+ repeatDelay?: number;
+ };
+ }
+>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
+ const beamRef = useRef(null);
+ const [collision, setCollision] = useState<{
+ detected: boolean;
+ coordinates: { x: number; y: number } | null;
+ }>({ detected: false, coordinates: null });
+ const [beamKey, setBeamKey] = useState(0);
+ const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);
+
+ useEffect(() => {
+ const checkCollision = () => {
+ if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) {
+ const beamRect = beamRef.current.getBoundingClientRect();
+ const containerRect = containerRef.current.getBoundingClientRect();
+ const parentRect = parentRef.current.getBoundingClientRect();
+
+ if (beamRect.bottom >= containerRect.top) {
+ const relativeX = beamRect.left - parentRect.left + beamRect.width / 2;
+ const relativeY = beamRect.bottom - parentRect.top;
+
+ setCollision({
+ detected: true,
+ coordinates: { x: relativeX, y: relativeY },
+ });
+ setCycleCollisionDetected(true);
+ if (beamRef.current) {
+ beamRef.current.style.opacity = "0";
+ }
+ }
+ }
+ };
+
+ const animationInterval = setInterval(checkCollision, 50);
+
+ return () => clearInterval(animationInterval);
+ }, [cycleCollisionDetected, containerRef]);
+
+ useEffect(() => {
+ if (collision.detected && collision.coordinates) {
+ setTimeout(() => {
+ setCollision({ detected: false, coordinates: null });
+ setCycleCollisionDetected(false);
+ // Set beam opacity to 0
+ if (beamRef.current) {
+ beamRef.current.style.opacity = "1";
+ }
+ }, 2000);
+
+ // Reset the beam animation after a delay
+ setTimeout(() => {
+ setBeamKey((prevKey) => prevKey + 1);
+ }, 2000);
+ }
+ }, [collision]);
+
+ return (
+ <>
+
+
+ {collision.detected && collision.coordinates && (
+
+ )}
+
+ >
+ );
+});
+
+CollisionMechanism.displayName = "CollisionMechanism";
+
+const Explosion = ({ ...props }: React.HTMLProps) => {
+ const spans = Array.from({ length: 20 }, (_, index) => ({
+ id: index,
+ initialX: 0,
+ initialY: 0,
+ directionX: Math.floor(Math.random() * 80 - 40),
+ directionY: Math.floor(Math.random() * -50 - 10),
+ }));
+
+ return (
+
+
+ {spans.map((span) => (
+
+ ))}
+
+ );
+};
+
+const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
+ return (
+
+ );
+};
diff --git a/surfsense_web/components/homepage/integrations.tsx b/surfsense_web/components/homepage/integrations.tsx
new file mode 100644
index 000000000..47d4e7325
--- /dev/null
+++ b/surfsense_web/components/homepage/integrations.tsx
@@ -0,0 +1,185 @@
+"use client";
+import React, { useEffect, useState } from "react";
+
+interface Integration {
+ name: string;
+ icon: string;
+}
+
+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",
+ },
+
+ // Communication
+ { name: "Slack", icon: "https://cdn.simpleicons.org/slack/4A154B" },
+ { name: "Discord", icon: "https://cdn.simpleicons.org/discord/5865F2" },
+ { name: "Gmail", icon: "https://cdn.simpleicons.org/gmail/EA4335" },
+
+ // 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" },
+
+ // Documentation & Knowledge
+ { name: "Confluence", icon: "https://cdn.simpleicons.org/confluence/172B4D" },
+ { name: "Notion", icon: "https://cdn.simpleicons.org/notion/000000/ffffff" },
+
+ // 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",
+ },
+
+ // Development
+ { name: "GitHub", icon: "https://cdn.simpleicons.org/github/181717/ffffff" },
+
+ // Productivity
+ { name: "Google Calendar", icon: "https://cdn.simpleicons.org/googlecalendar/4285F4" },
+ { name: "Luma", icon: "https://images.lumacdn.com/social-images/default-social-202407.png" },
+
+ // Media
+ { name: "YouTube", icon: "https://cdn.simpleicons.org/youtube/FF0000" },
+];
+
+function SemiCircleOrbit({ radius, centerX, centerY, count, iconSize, startIndex }: any) {
+ return (
+ <>
+ {/* Semi-circle glow background */}
+
+
+ {/* 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
+
+ Connect your favourite apps to your workflow.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/surfsense_web/components/homepage/navbar.tsx b/surfsense_web/components/homepage/navbar.tsx
new file mode 100644
index 000000000..731db4469
--- /dev/null
+++ b/surfsense_web/components/homepage/navbar.tsx
@@ -0,0 +1,177 @@
+"use client";
+import { IconBrandDiscord, IconBrandGithub, IconMenu2, IconX } from "@tabler/icons-react";
+import { AnimatePresence, motion } from "motion/react";
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+import { Logo } from "@/components/Logo";
+import { ThemeTogglerComponent } from "@/components/theme/theme-toggle";
+import { cn } from "@/lib/utils";
+
+export const Navbar = () => {
+ const [isScrolled, setIsScrolled] = useState(false);
+
+ const navItems = [
+ { name: "Home", link: "/" },
+ { name: "Pricing", link: "/pricing" },
+ { name: "Sign In", link: "/login" },
+ { name: "Docs", link: "/docs" },
+ ];
+
+ useEffect(() => {
+ const handleScroll = () => {
+ setIsScrolled(window.scrollY > 20);
+ };
+
+ window.addEventListener("scroll", handleScroll);
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, []);
+
+ return (
+
+
+
+
+ );
+};
+
+const DesktopNav = ({ navItems, isScrolled }: any) => {
+ const [hovered, setHovered] = useState(null);
+ return (
+ {
+ setHovered(null);
+ }}
+ className={cn(
+ "mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start rounded-full px-4 py-2 lg:flex transition-all duration-300",
+ isScrolled
+ ? "bg-white/80 backdrop-blur-md border border-white/20 shadow-lg dark:bg-neutral-950/80 dark:border-neutral-800/50"
+ : "bg-transparent border border-transparent"
+ )}
+ >
+
+
+ SurfSense
+
+
+ {navItems.map((navItem: any, idx: number) => (
+ setHovered(idx)}
+ className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
+ key={`link=${idx}`}
+ href={navItem.link}
+ >
+ {hovered === idx && (
+
+ )}
+ {navItem.name}
+
+ ))}
+
+
+
+
+
+
+
+ 8.3k
+
+
+
+ Book a call
+
+
+
+ );
+};
+
+const MobileNav = ({ navItems, isScrolled }: any) => {
+ const [open, setOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+ SurfSense
+
+ {open ? (
+
setOpen(!open)} />
+ ) : (
+ setOpen(!open)} />
+ )}
+
+
+
+ {open && (
+
+ {navItems.map((navItem: any, idx: number) => (
+
+ {navItem.name}
+
+ ))}
+
+
+
+
+
+
+
+ 8.3k
+
+
+
+
+ Book a call
+
+
+ )}
+
+
+ >
+ );
+};
diff --git a/surfsense_web/components/pricing.tsx b/surfsense_web/components/pricing.tsx
index b666c8982..083ad0d8e 100644
--- a/surfsense_web/components/pricing.tsx
+++ b/surfsense_web/components/pricing.tsx
@@ -91,42 +91,53 @@ export function Pricing({
-
- {plans.map((plan, index) => (
-
+ {plans.map((plan, index) => (
+
+ plans.length === 3 && index === 0 && "origin-right",
+ plans.length === 3 && index === 2 && "origin-left"
+ )}
+ >
{plan.isPopular && (
@@ -136,9 +147,12 @@ export function Pricing({
)}
-
{plan.name}
-
-
+ {plan.name}
+
+
+ {isNaN(Number(plan.price)) ? (
+ {isMonthly ? plan.price : plan.yearlyPrice}
+ ) : (
-
- {plan.period !== "Next 3 months" && (
-
- / {plan.period}
-
)}
-
+
+ {plan.period && plan.period !== "Next 3 months" && (
+
+ / {plan.period}
+
+ )}
+
-
- {isMonthly ? "billed monthly" : "billed annually"}
-
+
+ {isNaN(Number(plan.price)) ? "" : isMonthly ? "billed monthly" : "billed annually"}
+
{plan.features.map((feature, idx) => (
diff --git a/surfsense_web/components/pricing/pricing-section.tsx b/surfsense_web/components/pricing/pricing-section.tsx
index c017eb1e7..5a0341359 100644
--- a/surfsense_web/components/pricing/pricing-section.tsx
+++ b/surfsense_web/components/pricing/pricing-section.tsx
@@ -4,55 +4,38 @@ import { Pricing } from "@/components/pricing";
const demoPlans = [
{
- name: "STARTER",
- price: "50",
- yearlyPrice: "40",
- period: "per month",
+ name: "COMMUNITY",
+ price: "0",
+ yearlyPrice: "0",
+ period: "forever",
features: [
- "Up to 10 projects",
- "Basic analytics",
- "48-hour support response time",
- "Limited API access",
- "Community support",
+ "Supports 100+ LLMs",
+ "Supports local Ollama or vLLM setups",
+ "6000+ Embedding Models",
+ "50+ File extensions supported.",
+ "Podcasts support with local TTS providers.",
+ "Connects with 15+ external sources.",
+ "Cross-Browser Extension for dynamic webpages including authenticated content",
+ "Upcoming: Mergeable MindMaps",
+ "Upcoming: Note Management",
],
- description: "Perfect for individuals and small projects",
- buttonText: "Start Free Trial",
- href: "/sign-up",
- isPopular: false,
- },
- {
- name: "PROFESSIONAL",
- price: "99",
- yearlyPrice: "79",
- period: "per month",
- features: [
- "Unlimited projects",
- "Advanced analytics",
- "24-hour support response time",
- "Full API access",
- "Priority support",
- "Team collaboration",
- "Custom integrations",
- ],
- description: "Ideal for growing teams and businesses",
+ description: "Open source version with powerful features",
buttonText: "Get Started",
- href: "/sign-up",
+ href: "/docs",
isPopular: true,
},
{
name: "ENTERPRISE",
- price: "299",
- yearlyPrice: "239",
- period: "per month",
+ price: "Contact Us",
+ yearlyPrice: "Contact Us",
+ period: "",
features: [
- "Everything in Professional",
- "Custom solutions",
- "Dedicated account manager",
- "1-hour support response time",
- "SSO Authentication",
- "Advanced security",
- "Custom contracts",
- "SLA agreement",
+ "Everything in Community",
+ "Priority Support",
+ "Access Controls",
+ "Multicollaborative and multiplayer features",
+ "Video generation",
+ "Advanced security features",
],
description: "For large organizations with specific needs",
buttonText: "Contact Sales",
@@ -63,11 +46,7 @@ const demoPlans = [
function PricingBasic() {
return (
-
+
);
}
diff --git a/surfsense_web/package.json b/surfsense_web/package.json
index 517e7690d..f65904784 100644
--- a/surfsense_web/package.json
+++ b/surfsense_web/package.json
@@ -74,6 +74,7 @@
"react-markdown": "^10.1.0",
"react-rough-notation": "^1.0.5",
"react-syntax-highlighter": "^15.6.1",
+ "react-wrap-balancer": "^1.1.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml
index bfda2d29d..3463929ec 100644
--- a/surfsense_web/pnpm-lock.yaml
+++ b/surfsense_web/pnpm-lock.yaml
@@ -173,6 +173,9 @@ importers:
react-syntax-highlighter:
specifier: ^15.6.1
version: 15.6.1(react@19.1.0)
+ react-wrap-balancer:
+ specifier: ^1.1.1
+ version: 1.1.1(react@19.1.0)
rehype-raw:
specifier: ^7.0.0
version: 7.0.0
@@ -4797,6 +4800,11 @@ packages:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-wrap-balancer@1.1.1:
+ resolution: {integrity: sha512-AB+l7FPRWl6uZ28VcJ8skkwLn2+UC62bjiw8tQUrZPlEWDVnR9MG0lghyn7EyxuJSsFEpht4G+yh2WikEqQ/5Q==}
+ peerDependencies:
+ react: '>=16.8.0 || ^17.0.0 || ^18'
+
react@19.1.0:
resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
engines: {node: '>=0.10.0'}
@@ -11108,6 +11116,10 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
+ react-wrap-balancer@1.1.1(react@19.1.0):
+ dependencies:
+ react: 19.1.0
+
react@19.1.0: {}
readable-stream@3.6.2:
diff --git a/surfsense_web/public/homepage/temp_hero_dark.png b/surfsense_web/public/homepage/temp_hero_dark.png
new file mode 100644
index 000000000..92f4284a7
Binary files /dev/null and b/surfsense_web/public/homepage/temp_hero_dark.png differ
diff --git a/surfsense_web/public/homepage/temp_hero_light.png b/surfsense_web/public/homepage/temp_hero_light.png
new file mode 100644
index 000000000..d7a0734cf
Binary files /dev/null and b/surfsense_web/public/homepage/temp_hero_light.png differ