diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index c0abd927f..def75b94e 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronDown, Download, Monitor } from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; +import { ChevronDown, Clock, Download, Monitor, Sparkles } from "lucide-react"; +import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import Link from "next/link"; import React, { memo, useCallback, useEffect, useRef, useState } from "react"; import Balancer from "react-wrap-balancer"; @@ -11,7 +11,18 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Empty, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@/components/ui/empty"; import { ExpandedMediaOverlay, useExpandedMedia } from "@/components/ui/expanded-gif-overlay"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { GITHUB_RELEASES_URL, @@ -50,96 +61,193 @@ const GoogleLogo = ({ className }: { className?: string }) => ( ); -const TAB_ITEMS = [ +type HeroUseCase = { + id: string; + title: string; + description: string; + src: string | null; + comingSoon?: boolean; +}; + +type HeroCategory = { + id: string; + label: string; + desktopOnly?: boolean; + useCases: HeroUseCase[]; +}; + +const HERO_TUTORIAL = "/homepage/hero_tutorial"; +const HERO_REALTIME = "/homepage/hero_realtime"; + +const CATEGORIES: HeroCategory[] = [ { - title: "General Assist", - description: "Launch SurfSense instantly from any application.", - src: "/homepage/hero_tutorial/general_assist.mp4", - featured: true, + id: "desktop", + label: "Desktop App", + desktopOnly: true, + useCases: [ + { + id: "general", + title: "General Assist", + description: "Launch SurfSense instantly from any application with a global shortcut.", + src: `${HERO_TUTORIAL}/general_assist.mp4`, + }, + { + id: "quick", + title: "Quick Assist", + description: "Select text anywhere, then ask AI to explain, rewrite, or act on it.", + src: `${HERO_TUTORIAL}/quick_assist.mp4`, + }, + { + id: "screenshot", + title: "Screenshot Assist", + description: "Capture any region of your screen and ask AI about what’s in it.", + src: `${HERO_TUTORIAL}/screenshot_assist.mp4`, + }, + { + id: "watch-folder", + title: "Watch Local Folder", + description: "Auto-sync a local folder to your knowledge base. Great for Obsidian vaults.", + src: `${HERO_TUTORIAL}/folder_watch.mp4`, + }, + ], }, { - title: "Quick Assist", - description: "Select text anywhere, then ask AI to explain, rewrite, or act on it.", - src: "/homepage/hero_tutorial/quick_assist.mp4", - featured: true, + id: "deliverables", + label: "Deliverable Studio", + useCases: [ + { + id: "report", + title: "AI Report Generator", + description: + "Generate cited research reports from your documents, then export to PDF or Markdown.", + src: `${HERO_TUTORIAL}/ReportGenGif_compressed.mp4`, + }, + { + id: "podcast", + title: "AI Podcast Generator", + description: "Turn any document or folder into a two-host AI podcast in under 20 seconds.", + src: `${HERO_TUTORIAL}/PodcastGenGif.mp4`, + }, + { + id: "presentation", + title: "AI Presentation & Video Maker", + description: "Create editable slide decks and narrated video overviews from your sources.", + src: `${HERO_TUTORIAL}/video_gen_surf.mp4`, + }, + { + id: "image", + title: "AI Image Generator", + description: "Generate high-quality images straight from your chats and documents.", + src: `${HERO_TUTORIAL}/ImageGenGif.mp4`, + }, + { + id: "resume", + title: "AI Resume Builder", + description: "Draft and format an ATS-ready resume as a polished PDF.", + src: null, + comingSoon: true, + }, + ], }, { - title: "Screenshot Assist", - description: - "Use a global shortcut to select a region on your screen and attach it to your chat message.", - src: "/homepage/hero_tutorial/screenshot_assist.mp4", - featured: true, + id: "automations", + label: "Automations", + useCases: [ + { + id: "sort", + title: "AI Document Sorting", + description: "Let AI automatically organize files into the right folders as they arrive.", + src: null, + comingSoon: true, + }, + { + id: "schedule", + title: "Scheduled AI Workflows", + description: "Run an agent on a schedule: daily briefs, weekly digests, recurring reports.", + src: null, + comingSoon: true, + }, + { + id: "event", + title: "Event-Triggered Automations", + description: + "Fire an agent the moment a document lands in a folder, then post the result to your tools.", + src: null, + comingSoon: true, + }, + { + id: "chat-built", + title: "Chat-Built Automations", + description: "Describe an automation in plain English and SurfSense builds it for you.", + src: null, + comingSoon: true, + }, + ], }, { - title: "Watch Local Folder", - description: - "Watch a local folder and automatically sync file changes to your knowledge base. Works great with Obsidian vaults.", - src: "/homepage/hero_tutorial/folder_watch.mp4", - featured: true, - }, - // { - // 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: "Video & Presentations", - description: - "Create short videos and editable presentations with AI-generated visuals and narration from your sources.", - src: "/homepage/hero_tutorial/video_gen_surf.mp4", - featured: false, + id: "search-chat", + label: "Search & Chat", + useCases: [ + { + id: "chat-docs", + title: "Chat With Your PDFs & Docs", + description: "Ask questions across all your files and get answers with inline citations.", + src: `${HERO_TUTORIAL}/BQnaGif_compressed.mp4`, + }, + { + id: "search", + title: "AI Search With Citations", + description: "Hybrid semantic and keyword search across your entire knowledge base.", + src: `${HERO_TUTORIAL}/BSNCGif.mp4`, + }, + { + id: "collab", + title: "Collaborative AI Chat", + description: "Work on AI conversations with your team in real time.", + src: `${HERO_REALTIME}/RealTimeChatGif.mp4`, + }, + { + id: "comments", + title: "Comments & Mentions", + description: "Comment and tag teammates on any AI message.", + src: `${HERO_REALTIME}/RealTimeCommentsFlow.mp4`, + }, + ], }, { - title: "Search & Citation", - description: "Ask questions and get cited responses from your knowledge base.", - src: "/homepage/hero_tutorial/BSNCGif.mp4", - featured: false, + id: "connectors", + label: "Connectors & Integrations", + useCases: [ + { + id: "connect", + title: "Connect & Sync Your Tools", + description: + "Sync Notion, Slack, Google Drive, Gmail, GitHub, Linear and 25+ sources into one searchable corpus.", + src: `${HERO_TUTORIAL}/ConnectorFlowGif.mp4`, + }, + { + id: "upload", + title: "Chat With Uploaded Files", + description: "Drop in PDFs, Office docs, images and audio. Instantly searchable.", + src: `${HERO_TUTORIAL}/DocUploadGif.mp4`, + }, + { + id: "write-back", + title: "Connector Write-Back", + description: "Let the agent post results back to Notion, Slack, Linear and Drive.", + src: null, + comingSoon: true, + }, + { + id: "obsidian", + title: "Obsidian & Knowledge Base Sync", + description: "Keep your Obsidian vault and personal knowledge base in sync.", + src: null, + comingSoon: true, + }, + ], }, - { - 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, - }, -] as const; +]; export function HeroSection() { return ( @@ -279,117 +387,15 @@ function DownloadButton() { ); } -const BrowserWindow = () => { - const [selectedIndex, setSelectedIndex] = useState(0); - const selectedItem = TAB_ITEMS[selectedIndex]; - const { expanded, open, close } = useExpandedMedia(); - - return ( - <> - -
-
-
-
-
-
-
- {TAB_ITEMS.map((item, index) => ( - - - {index !== TAB_ITEMS.length - 1 && ( -
- )} - - ))} -
-
-
- - -
-
-

- {selectedItem.title} -

-

- {selectedItem.description} -

-
-
- -
-
-
- - - - {expanded && ( - - )} - - - ); -}; - -const TabVideo = memo(function TabVideo({ src }: { src: string }) { +const TabVideo = memo(function TabVideo({ + src, + title, + reduceMotion, +}: { + src: string; + title: string; + reduceMotion: boolean; +}) { const videoRef = useRef(null); const [hasLoaded, setHasLoaded] = useState(false); @@ -398,8 +404,11 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) { const video = videoRef.current; if (!video) return; video.currentTime = 0; - video.play().catch(() => {}); - }, []); + // Respect reduced-motion: show the first frame and expose controls instead of autoplaying. + if (!reduceMotion) { + video.play().catch(() => {}); + } + }, [reduceMotion]); const handleCanPlay = useCallback(() => { setHasLoaded(true); @@ -411,7 +420,10 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) { ref={videoRef} key={src} src={src} - preload="auto" + preload={reduceMotion ? "metadata" : "auto"} + aria-label={`${title} demo`} + autoPlay={!reduceMotion} + controls={reduceMotion} loop muted playsInline @@ -419,8 +431,207 @@ const TabVideo = memo(function TabVideo({ src }: { src: string }) { className="aspect-video w-full rounded-lg sm:rounded-xl" /> {!hasLoaded && ( -
+ )}
); }); + +const UseCasePlaceholder = ({ title }: { title: string }) => ( + + + + + Demo coming soon + {`A walkthrough of ${title} is on the way.`} + + +); + +const DesktopBadge = () => ( + + + + + + Desktop app only + +); + +const UseCasePane = memo(function UseCasePane({ + useCase, + reduceMotion, +}: { + useCase: HeroUseCase; + reduceMotion: boolean; +}) { + const { expanded, open, close } = useExpandedMedia(); + const hasVideo = !useCase.comingSoon && Boolean(useCase.src); + + const media = hasVideo ? ( + + ) : ( +
+
+ +
+
+ ); + + const card = ( +
+
+
+

+ {useCase.title} +

+

+ {useCase.description} +

+
+
+ {media} +
+ ); + + return ( + <> + {reduceMotion ? ( + card + ) : ( + + {card} + + )} + + + {expanded && hasVideo && ( + + )} + + + ); +}); + +const CategoryPanel = memo(function CategoryPanel({ + category, + reduceMotion, +}: { + category: HeroCategory; + reduceMotion: boolean; +}) { + return ( +
+ {category.desktopOnly && ( +
+
+ )} + + + + {category.useCases.map((useCase) => ( + + {useCase.title} + + ))} + + + +
+ {category.useCases.map((useCase) => ( + + + + ))} +
+
+
+ ); +}); + +const BrowserWindow = () => { + const [activeCategory, setActiveCategory] = useState(CATEGORIES[0].id); + const reduceMotion = useReducedMotion() ?? false; + + return ( + +
+
+
+
+
+
+ + + {CATEGORIES.map((category, index) => ( + + + {category.label} + {category.desktopOnly && } + + {index !== CATEGORIES.length - 1 && ( + + )} + + ))} + + + +
+
+ {CATEGORIES.map((category) => ( + + + + ))} +
+ + ); +};