diff --git a/surfsense_web/app/(home)/page.tsx b/surfsense_web/app/(home)/page.tsx index e0478fce3..2c1a70ac9 100644 --- a/surfsense_web/app/(home)/page.tsx +++ b/surfsense_web/app/(home)/page.tsx @@ -1,10 +1,29 @@ "use client"; -import { CTAHomepage } from "@/components/homepage/cta"; -import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid"; -import { FeaturesCards } from "@/components/homepage/features-card"; +import dynamic from "next/dynamic"; import { HeroSection } from "@/components/homepage/hero-section"; -import ExternalIntegrations from "@/components/homepage/integrations"; + +const FeaturesCards = dynamic( + () => import("@/components/homepage/features-card").then((m) => ({ default: m.FeaturesCards })), + { ssr: false } +); + +const FeaturesBentoGrid = dynamic( + () => + import("@/components/homepage/features-bento-grid").then((m) => ({ + default: m.FeaturesBentoGrid, + })), + { ssr: false } +); + +const ExternalIntegrations = dynamic(() => import("@/components/homepage/integrations"), { + ssr: false, +}); + +const CTAHomepage = dynamic( + () => import("@/components/homepage/cta").then((m) => ({ default: m.CTAHomepage })), + { ssr: false } +); export default function HomePage() { return ( diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx index d29db13ae..cceae36d9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/(manage)/components/DocumentsFilters.tsx @@ -105,11 +105,19 @@ export function DocumentsFilters({ ) : ( filteredTypes.map((value: DocumentTypeEnum, i) => ( - + )) )} diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx index 5e8fa394f..6166cd714 100644 --- a/surfsense_web/app/layout.tsx +++ b/surfsense_web/app/layout.tsx @@ -108,6 +108,9 @@ export default function RootLayout({ // Locale state is managed by LocaleContext and persisted in localStorage return ( + + + diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx index ad345ebf5..9646ec036 100644 --- a/surfsense_web/components/assistant-ui/thread.tsx +++ b/surfsense_web/components/assistant-ui/thread.tsx @@ -101,7 +101,6 @@ const ThreadContent: FC = () => { > ( return { ref: localRef, isInView }; } -// --------------------------------------------------------------------------- -// Particles (for star burst effect on completion) -// --------------------------------------------------------------------------- -type ParticlesContextType = { animate: boolean; isInView: boolean }; -const [ParticlesProvider, useParticles] = - getStrictContext("ParticlesContext"); - -function Particles({ - ref, - animate = true, - inView = false, - inViewMargin = "0px", - inViewOnce = true, - children, - style, - ...props -}: Omit, "children"> & { - animate?: boolean; - children: React.ReactNode; -} & UseIsInViewOptions) { - const { ref: localRef, isInView } = useIsInView(ref as React.Ref, { - inView, - inViewOnce, - inViewMargin, - }); - return ( - - - {children} - - - ); -} - -function ParticlesEffect({ - side = "top", - align = "center", - count = 6, - radius = 30, - spread = 360, - duration = 0.8, - holdDelay = 0.05, - sideOffset = 0, - alignOffset = 0, - delay = 0, - transition, - style, - ...props -}: Omit, "children"> & { - side?: "top" | "bottom" | "left" | "right"; - align?: "start" | "center" | "end"; - count?: number; - radius?: number; - spread?: number; - duration?: number; - holdDelay?: number; - sideOffset?: number; - alignOffset?: number; - delay?: number; -}) { - const { animate, isInView } = useParticles(); - const isVertical = side === "top" || side === "bottom"; - const alignPct = align === "start" ? "0%" : align === "end" ? "100%" : "50%"; - - const top = isVertical - ? side === "top" - ? `calc(0% - ${sideOffset}px)` - : `calc(100% + ${sideOffset}px)` - : `calc(${alignPct} + ${alignOffset}px)`; - const left = isVertical - ? `calc(${alignPct} + ${alignOffset}px)` - : side === "left" - ? `calc(0% - ${sideOffset}px)` - : `calc(100% + ${sideOffset}px)`; - - const containerStyle: React.CSSProperties = { - position: "absolute", - top, - left, - transform: "translate(-50%, -50%)", - }; - const angleStep = (spread * (Math.PI / 180)) / Math.max(1, count - 1); - - return ( - - {animate && - isInView && - [...Array(count)].map((_, i) => { - const angle = i * angleStep; - const x = Math.cos(angle) * radius; - const y = Math.sin(angle) * radius; - return ( - - ); - })} - - ); -} - // --------------------------------------------------------------------------- // Per-digit scrolling wheel // --------------------------------------------------------------------------- @@ -317,18 +193,42 @@ function AnimatedStarCount({ value, itemSize = 22, isRolling = false, + animated = true, className, onComplete, }: { value: number; itemSize?: number; isRolling?: boolean; + animated?: boolean; className?: string; onComplete?: () => void; }) { const formatted = numberFormatter.format(value); const chars = formatted.split(""); + if (!animated) { + return ( +
+ {chars.map((char, idx) => ( +
= "0" && char <= "9" ? undefined : "0.3em", + }} + > + {char} +
+ ))} +
+ ); + } + let totalDigits = 0; for (const c of chars) { if (c >= "0" && c <= "9") totalDigits++; @@ -407,13 +307,13 @@ function NavbarGitHubStars({ href = "https://github.com/MODSetter/SurfSense", className, }: NavbarGitHubStarsProps) { + const [hasMounted, setHasMounted] = React.useState(false); const [stars, setStars] = React.useState(0); const [isLoading, setIsLoading] = React.useState(true); - const [isCompleted, setIsCompleted] = React.useState(false); - const fillRaw = useMotionValue(0); - const fillSpring = useSpring(fillRaw, { stiffness: 12, damping: 14 }); - const clipPath = useTransform(fillSpring, (v) => `inset(${100 - v * 100}% 0 0 0)`); + React.useEffect(() => { + setHasMounted(true); + }, []); React.useEffect(() => { const abortController = new AbortController(); @@ -424,7 +324,6 @@ function NavbarGitHubStars({ .then((data) => { if (data && typeof data.stargazers_count === "number") { setStars(data.stargazers_count); - fillRaw.set(1); } }) .catch((err) => { @@ -434,7 +333,7 @@ function NavbarGitHubStars({ }) .finally(() => setIsLoading(false)); return () => abortController.abort(); - }, [username, repo, fillRaw]); + }, [username, repo]); return ( - -
+ +
setIsCompleted(true)} /> - -
-
- -
); diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx index a1aa5ac4a..d8fd146e3 100644 --- a/surfsense_web/components/homepage/hero-section.tsx +++ b/surfsense_web/components/homepage/hero-section.tsx @@ -32,11 +32,24 @@ 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; +} + export function HeroSection() { const containerRef = useRef(null); const parentRef = useRef(null); const heroVariant = useFeatureFlagVariantKey("notebooklm_superpowers_flag"); const isNotebookLMVariant = heroVariant === "superpowers"; + const isDesktop = useIsDesktop(); return (
- - - - + {isDesktop && ( + <> + + + + + + )}

{isNotebookLMVariant ? ( diff --git a/surfsense_web/components/ui/expanded-gif-overlay.tsx b/surfsense_web/components/ui/expanded-gif-overlay.tsx index c53dbcdc4..96a1dd424 100644 --- a/surfsense_web/components/ui/expanded-gif-overlay.tsx +++ b/surfsense_web/components/ui/expanded-gif-overlay.tsx @@ -4,7 +4,11 @@ import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; -function ExpandedGifOverlay({ +function isVideoSrc(src: string) { + return /\.(mp4|webm|ogg)(\?|$)/i.test(src); +} + +function ExpandedMediaOverlay({ src, alt, onClose, @@ -21,6 +25,31 @@ function ExpandedGifOverlay({ return () => document.removeEventListener("keydown", handleKey); }, [onClose]); + const mediaElement = isVideoSrc(src) ? ( + + ) : ( + + ); + return createPortal( - + {mediaElement} , document.body ); } -function useExpandedGif() { +function useExpandedMedia() { const [expanded, setExpanded] = useState(false); const open = useCallback(() => setExpanded(true), []); const close = useCallback(() => setExpanded(false), []); return { expanded, open, close }; } -export { ExpandedGifOverlay, useExpandedGif }; +/** @deprecated Use ExpandedMediaOverlay instead */ +const ExpandedGifOverlay = ExpandedMediaOverlay; +/** @deprecated Use useExpandedMedia instead */ +const useExpandedGif = useExpandedMedia; + +export { ExpandedMediaOverlay, useExpandedMedia, ExpandedGifOverlay, useExpandedGif }; diff --git a/surfsense_web/components/ui/hero-carousel.tsx b/surfsense_web/components/ui/hero-carousel.tsx index 141583d67..b6ae85cb3 100644 --- a/surfsense_web/components/ui/hero-carousel.tsx +++ b/surfsense_web/components/ui/hero-carousel.tsx @@ -9,59 +9,57 @@ const carouselItems = [ title: "Connect & Sync", description: "Connect data sources like Notion, Drive and Gmail. Automatically sync to keep them updated.", - src: "/homepage/hero_tutorial/ConnectorFlowGif.gif", + src: "/homepage/hero_tutorial/ConnectorFlowGif.mp4", }, { title: "Upload Documents", description: "Upload documents directly, from images to massive PDFs.", - src: "/homepage/hero_tutorial/DocUploadGif.gif", + src: "/homepage/hero_tutorial/DocUploadGif.mp4", }, { title: "Search & Citation", description: "Ask questions and get cited responses from your knowledge base.", - src: "/homepage/hero_tutorial/BSNCGif.gif", + src: "/homepage/hero_tutorial/BSNCGif.mp4", }, { title: "Targeted Document Q&A", description: "Mention specific documents in chat for targeted answers.", - src: "/homepage/hero_tutorial/BQnaGif_compressed.gif", + src: "/homepage/hero_tutorial/BQnaGif_compressed.mp4", }, { title: "Produce Reports Instantly", description: "Generate reports from your sources in many formats.", - src: "/homepage/hero_tutorial/ReportGenGif_compressed.gif", + src: "/homepage/hero_tutorial/ReportGenGif_compressed.mp4", }, { title: "Create Podcasts", description: "Turn anything into a podcast in under 20 seconds.", - src: "/homepage/hero_tutorial/PodcastGenGif.gif", + src: "/homepage/hero_tutorial/PodcastGenGif.mp4", }, { title: "Image Generation", description: "Generate high-quality images easily from your conversations.", - src: "/homepage/hero_tutorial/ImageGenGif.gif", + src: "/homepage/hero_tutorial/ImageGenGif.mp4", }, { title: "Collaborative AI Chat", description: "Collaborate on AI-powered conversations in realtime with your team.", - src: "/homepage/hero_realtime/RealTimeChatGif.gif", + src: "/homepage/hero_realtime/RealTimeChatGif.mp4", }, { title: "Realtime Comments", description: "Add comments and tag teammates on any message.", - src: "/homepage/hero_realtime/RealTimeCommentsFlow.gif", + src: "/homepage/hero_realtime/RealTimeCommentsFlow.mp4", }, ]; function HeroCarouselCard({ - index, title, description, src, isActive, onExpandedChange, }: { - index: number; title: string; description: string; src: string; @@ -69,53 +67,50 @@ function HeroCarouselCard({ onExpandedChange?: (expanded: boolean) => void; }) { const { expanded, open, close } = useExpandedGif(); + const videoRef = useRef(null); + const [frozenFrame, setFrozenFrame] = useState(null); + const [hasLoaded, setHasLoaded] = useState(false); useEffect(() => { onExpandedChange?.(expanded); }, [expanded, onExpandedChange]); - const imgRef = useRef(null); - const [frozenFrame, setFrozenFrame] = useState(null); - const [playKey, setPlayKey] = useState(0); - const captureFrame = useCallback((img: HTMLImageElement) => { + const captureFrame = useCallback((video: HTMLVideoElement) => { try { const canvas = document.createElement("canvas"); - canvas.width = img.naturalWidth; - canvas.height = img.naturalHeight; - canvas.getContext("2d")?.drawImage(img, 0, 0); - setFrozenFrame(canvas.toDataURL()); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext("2d")?.drawImage(video, 0, 0); + setFrozenFrame(canvas.toDataURL("image/jpeg", 0.85)); } catch { - /* cross-origin or other issue */ + /* tainted canvas */ } }, []); useEffect(() => { + const video = videoRef.current; if (isActive) { - setPlayKey((k) => k + 1); - setFrozenFrame(null); + setHasLoaded(false); + if (video) { + video.currentTime = 0; + video.play().catch(() => {}); + } } else { - const img = imgRef.current; - if (img && img.complete && img.naturalWidth > 0) { - captureFrame(img); + if (video) { + if (video.readyState >= 2) captureFrame(video); + video.pause(); } } }, [isActive, captureFrame]); - useEffect(() => { - if (!isActive && !frozenFrame) { - const img = new Image(); - img.onload = () => captureFrame(img); - img.src = src; - } - }, [isActive, frozenFrame, src, captureFrame]); + const handleCanPlay = useCallback(() => { + setHasLoaded(true); + }, []); return ( <>
- {/* - {index + 1} - */}

{title} @@ -130,13 +125,28 @@ function HeroCarouselCard({ onClick={isActive ? open : undefined} > {isActive ? ( - {title} +
+