diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
index 299cf1032..1bb28e770 100644
--- a/surfsense_web/components/homepage/hero-section.tsx
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -1,39 +1,22 @@
"use client";
import { AnimatePresence, motion } from "motion/react";
-import dynamic from "next/dynamic";
+import { Monitor } from "lucide-react";
import Link from "next/link";
-import type React from "react";
-import { useEffect, useRef, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState, memo } from "react";
import Balancer from "react-wrap-balancer";
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
+import {
+ ExpandedMediaOverlay,
+ useExpandedMedia,
+} from "@/components/ui/expanded-gif-overlay";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
-const HeroCarousel = dynamic(
- () => import("@/components/ui/hero-carousel").then((m) => ({ default: m.HeroCarousel })),
- {
- ssr: false,
- loading: () => (
-
- ),
- }
-);
-
-// Official Google "G" logo with brand colors
const GoogleLogo = ({ className }: { className?: string }) => (
);
-function useIsDesktop(breakpoint = 1024) {
- const [isDesktop, setIsDesktop] = useState(false);
- useEffect(() => {
- const mql = window.matchMedia(`(min-width: ${breakpoint}px)`);
- setIsDesktop(mql.matches);
- const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches);
- mql.addEventListener("change", handler);
- return () => mql.removeEventListener("change", handler);
- }, [breakpoint]);
- return isDesktop;
-}
+const TAB_ITEMS = [
+ {
+ title: "Connect & Sync",
+ description:
+ "Connect data sources like Notion, Drive and Gmail. Automatically sync to keep them updated.",
+ src: "/homepage/hero_tutorial/ConnectorFlowGif.mp4",
+ featured: true,
+ },
+ {
+ title: "Upload Documents",
+ description: "Upload documents directly, from images to massive PDFs.",
+ src: "/homepage/hero_tutorial/DocUploadGif.mp4",
+ featured: true,
+ },
+ {
+ title: "Search & Citation",
+ description:
+ "Ask questions and get cited responses from your knowledge base.",
+ src: "/homepage/hero_tutorial/BSNCGif.mp4",
+ featured: false,
+ },
+ {
+ title: "Document Q&A",
+ description: "Mention specific documents in chat for targeted answers.",
+ src: "/homepage/hero_tutorial/BQnaGif_compressed.mp4",
+ featured: false,
+ },
+ {
+ title: "Reports",
+ description: "Generate reports from your sources in many formats.",
+ src: "/homepage/hero_tutorial/ReportGenGif_compressed.mp4",
+ featured: false,
+ },
+ {
+ title: "Podcasts",
+ description: "Turn anything into a podcast in under 20 seconds.",
+ src: "/homepage/hero_tutorial/PodcastGenGif.mp4",
+ featured: false,
+ },
+ {
+ title: "Image Generation",
+ description:
+ "Generate high-quality images easily from your conversations.",
+ src: "/homepage/hero_tutorial/ImageGenGif.mp4",
+ featured: false,
+ },
+ {
+ title: "Collaborative Chat",
+ description:
+ "Collaborate on AI-powered conversations in realtime with your team.",
+ src: "/homepage/hero_realtime/RealTimeChatGif.mp4",
+ featured: false,
+ },
+ {
+ title: "Comments",
+ description: "Add comments and tag teammates on any message.",
+ src: "/homepage/hero_realtime/RealTimeCommentsFlow.mp4",
+ featured: false,
+ },
+ {
+ title: "Video Generation",
+ description:
+ "Create short videos with AI-generated visuals and narration from your sources.",
+ src: "/homepage/hero_tutorial/video_gen_surf.mp4",
+ featured: false,
+ },
+] as const;
export function HeroSection() {
- const containerRef = useRef(null);
- const parentRef = useRef(null);
- const isDesktop = useIsDesktop();
-
return (
-
-
- {isDesktop && (
- <>
-
-
-
-
- >
- )}
+
+
+
+ NotebookLM for Teams
+
+
+
+
+ An open source, privacy focused alternative to NotebookLM for teams with no data limits.
+
-
-
-
-
NotebookLM for Teams
+
+
+
+
-
-
- Connect any LLM to your internal knowledge sources and chat with it in real time alongside
- your team.
-
-
-
- {/* */}
-
-
-
+
);
@@ -158,193 +156,155 @@ function GetStartedButton() {
if (isGoogleAuth) {
return (
-
- {/* Animated gradient background on hover */}
-
- {/* Google logo with subtle animation */}
-
-
-
- Continue with Google
-
+
+
Continue with Google
+
);
}
return (
-
-
- Get Started
-
-
+
+ Get Started
+
);
}
-const BackgroundGrids = () => {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
+const BrowserWindow = () => {
+ const [selectedIndex, setSelectedIndex] = useState(0);
+ const selectedItem = TAB_ITEMS[selectedIndex];
+ const intervalRef = useRef
(null);
+ const { expanded, open, close } = useExpandedMedia();
-const CollisionMechanism = ({
- parentRef,
- beamOptions = {},
-}: {
- parentRef: React.RefObject;
- beamOptions?: {
- initialX?: number;
- translateX?: number;
- initialY?: number;
- translateY?: number;
- rotate?: number;
- className?: string;
- duration?: number;
- delay?: number;
- repeatDelay?: number;
- };
-}) => {
- 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);
+ const startInterval = useCallback(() => {
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
+ intervalRef.current = setInterval(() => {
+ setSelectedIndex((prev) => (prev + 1) % TAB_ITEMS.length);
+ }, 10000);
+ }, []);
useEffect(() => {
- const checkCollision = () => {
- if (beamRef.current && parentRef.current && !cycleCollisionDetected) {
- const beamRect = beamRef.current.getBoundingClientRect();
- const parentRect = parentRef.current.getBoundingClientRect();
- const rightEdge = parentRect.right;
-
- if (beamRect.right >= rightEdge - 20) {
- const relativeX = parentRect.width - 20;
- const relativeY = beamRect.top - parentRect.top + beamRect.height / 2;
-
- setCollision({
- detected: true,
- coordinates: { x: relativeX, y: relativeY },
- });
- setCycleCollisionDetected(true);
- if (beamRef.current) {
- beamRef.current.style.opacity = "0";
- }
- }
- }
- };
-
- const animationInterval = setInterval(checkCollision, 100);
-
- return () => clearInterval(animationInterval);
- }, [cycleCollisionDetected, parentRef]);
-
- useEffect(() => {
- if (!collision.detected || !collision.coordinates) return;
-
- const timer1 = setTimeout(() => {
- setCollision({ detected: false, coordinates: null });
- setCycleCollisionDetected(false);
- if (beamRef.current) {
- beamRef.current.style.opacity = "1";
- }
- }, 2000);
-
- const timer2 = setTimeout(() => {
- setBeamKey((prevKey) => prevKey + 1);
- }, 2000);
-
+ startInterval();
return () => {
- clearTimeout(timer1);
- clearTimeout(timer2);
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ }
};
- }, [collision]);
+ }, [startInterval]);
+
+ const handleTabClick = (index: number) => {
+ setSelectedIndex(index);
+ startInterval();
+ };
return (
<>
-
+
+
+
+
+ {TAB_ITEMS.map((item, index) => (
+
+
+ {index !== TAB_ITEMS.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+
+
+ {selectedItem.title}
+
+
+ {selectedItem.description}
+
+
+
+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: wrapper for video expand */}
+
+
+
+
+
+
+
+
- {collision.detected && collision.coordinates && (
-
)}
@@ -352,62 +312,92 @@ const 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),
- }));
+const TabVideo = memo(function TabVideo({ src }: { src: string }) {
+ const videoRef = useRef(null);
+ const [hasLoaded, setHasLoaded] = useState(false);
+
+ useEffect(() => {
+ setHasLoaded(false);
+ const video = videoRef.current;
+ if (!video) return;
+ video.currentTime = 0;
+ video.play().catch(() => { });
+ }, [src]);
+
+ const handleCanPlay = useCallback(() => {
+ setHasLoaded(true);
+ }, []);
return (
-
-
- {spans.map((span) => (
-
- ))}
+
+
+ {!hasLoaded && (
+
+ )}
);
-};
+});
-const GridLineVertical = ({ className, offset }: { className?: string; offset?: string }) => {
+const GITHUB_RELEASES_URL =
+ "https://github.com/MODSetter/SurfSense/releases/latest";
+
+const DownloadApp = memo(function DownloadApp() {
return (
-
+
+
+ Download the desktop app
+
+
+
);
-};
+});