diff --git a/surfsense_web/app/(home)/page.tsx b/surfsense_web/app/(home)/page.tsx index e0478fce3..b5a726423 100644 --- a/surfsense_web/app/(home)/page.tsx +++ b/surfsense_web/app/(home)/page.tsx @@ -5,11 +5,13 @@ import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid"; import { FeaturesCards } from "@/components/homepage/features-card"; import { HeroSection } from "@/components/homepage/hero-section"; import ExternalIntegrations from "@/components/homepage/integrations"; +import { UseCasesGrid } from "@/components/homepage/use-cases-grid"; export default function HomePage() { return (
+ diff --git a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx index 1e48abb9d..b4e37d324 100644 --- a/surfsense_web/components/assistant-ui/inline-mention-editor.tsx +++ b/surfsense_web/components/assistant-ui/inline-mention-editor.tsx @@ -141,9 +141,7 @@ export const InlineMentionEditor = forwardRef Then chat with it in real-time, even alongside your team.

-
+
-
-
- {/* Light mode image */} - header - {/* Dark mode image */} - header -
+
+
); @@ -236,24 +210,23 @@ const BackgroundGrids = () => { ); }; -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 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; @@ -264,14 +237,14 @@ const CollisionMechanism = React.forwardRef< useEffect(() => { const checkCollision = () => { - if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) { + if (beamRef.current && parentRef.current && !cycleCollisionDetected) { const beamRect = beamRef.current.getBoundingClientRect(); - const containerRect = containerRef.current.getBoundingClientRect(); const parentRect = parentRef.current.getBoundingClientRect(); + const rightEdge = parentRect.right; - if (beamRect.bottom >= containerRect.top) { - const relativeX = beamRect.left - parentRect.left + beamRect.width / 2; - const relativeY = beamRect.bottom - parentRect.top; + if (beamRect.right >= rightEdge - 20) { + const relativeX = parentRect.width - 20; + const relativeY = beamRect.top - parentRect.top + beamRect.height / 2; setCollision({ detected: true, @@ -288,7 +261,7 @@ const CollisionMechanism = React.forwardRef< const animationInterval = setInterval(checkCollision, 100); return () => clearInterval(animationInterval); - }, [cycleCollisionDetected, containerRef]); + }, [cycleCollisionDetected, parentRef]); useEffect(() => { if (collision.detected && collision.coordinates) { @@ -354,9 +327,7 @@ const CollisionMechanism = React.forwardRef< ); -}); - -CollisionMechanism.displayName = "CollisionMechanism"; +}; const Explosion = ({ ...props }: React.HTMLProps) => { const spans = Array.from({ length: 20 }, (_, index) => ({ diff --git a/surfsense_web/components/homepage/use-cases-grid.tsx b/surfsense_web/components/homepage/use-cases-grid.tsx new file mode 100644 index 000000000..21287f587 --- /dev/null +++ b/surfsense_web/components/homepage/use-cases-grid.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { AnimatePresence, motion } from "motion/react"; +import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay"; + +const useCases = [ + { + title: "Search & Citation", + description: "Ask questions and get Perplexity-style cited responses from your knowledge base.", + src: "/homepage/hero_tutorial/BSNCGif.gif", + }, + { + title: "Document Mention QNA", + description: "Mention specific documents in your queries for targeted answers.", + src: "/homepage/hero_tutorial/BQnaGif_compressed.gif", + }, + { + title: "Report Generation", + description: "Generate and export reports in many formats.", + src: "/homepage/hero_tutorial/ReportGenGif_compressed.gif", + }, + { + title: "Podcast Generation", + description: "Turn your knowledge into podcasts in under 20 seconds.", + src: "/homepage/hero_tutorial/PodcastGenGif.gif", + }, + { + title: "Image Generation", + description: "Generate images directly from your conversations.", + src: "/homepage/hero_tutorial/ImageGenGif.gif", + }, +]; + +function UseCaseCard({ + title, + description, + src, + className, +}: { + title: string; + description: string; + src: string; + className?: string; +}) { + const { expanded, open, close } = useExpandedGif(); + + return ( + <> + +
+ {title} +
+
+

{title}

+

{description}

+
+
+ + + {expanded && } + + + ); +} + +export function UseCasesGrid() { + return ( +
+
+

+ What You Can Do +

+
+ + {/* First row: 2 larger cards */} +
+ {useCases.slice(0, 2).map((useCase) => ( + + ))} +
+ + {/* Second row: 3 equal cards */} +
+ {useCases.slice(2).map((useCase) => ( + + ))} +
+ +

+ And more coming soon. +

+
+ ); +} diff --git a/surfsense_web/components/ui/expanded-gif-overlay.tsx b/surfsense_web/components/ui/expanded-gif-overlay.tsx new file mode 100644 index 000000000..56f343ded --- /dev/null +++ b/surfsense_web/components/ui/expanded-gif-overlay.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useEffect, useState } from "react"; + +function ExpandedGifOverlay({ + src, + alt, + onClose, +}: { + src: string; + alt: string; + onClose: () => void; +}) { + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + document.addEventListener("keydown", handleKey); + return () => document.removeEventListener("keydown", handleKey); + }, [onClose]); + + return ( + + e.stopPropagation()} + /> + + ); +} + +function useExpandedGif() { + const [expanded, setExpanded] = useState(false); + const open = useCallback(() => setExpanded(true), []); + const close = useCallback(() => setExpanded(false), []); + return { expanded, open, close }; +} + +export { ExpandedGifOverlay, useExpandedGif }; diff --git a/surfsense_web/components/ui/walkthrough-scroll.tsx b/surfsense_web/components/ui/walkthrough-scroll.tsx new file mode 100644 index 000000000..c14aa968f --- /dev/null +++ b/surfsense_web/components/ui/walkthrough-scroll.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { AnimatePresence, motion, useScroll, useTransform } from "motion/react"; +import { useRef } from "react"; +import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay"; + +const walkthroughSteps = [ + { + step: 1, + title: "Login", + description: "Login to get started.", + src: "/homepage/hero_tutorial/LoginFlowGif.gif", + }, + { + step: 2, + title: "Connect & Sync", + description: "Connect your connectors and sync. Enable periodic syncing to keep them updated.", + src: "/homepage/hero_tutorial/ConnectorFlowGif.gif", + }, + { + step: 3, + title: "Upload Documents", + description: "While connectors index, upload your documents directly.", + src: "/homepage/hero_tutorial/DocUploadGif.gif", + }, +]; + +function WalkthroughCard({ + i, + step, + title, + description, + src, + progress, + range, + targetScale, +}: { + i: number; + step: number; + title: string; + description: string; + src: string; + progress: ReturnType["scrollYProgress"]; + range: [number, number]; + targetScale: number; +}) { + const container = useRef(null); + const scale = useTransform(progress, range, [1, targetScale]); + const { expanded, open, close } = useExpandedGif(); + + return ( + <> +
+ +
+ + {step} + +
+

+ {title} +

+

+ {description} +

+
+
+
+ {title} +
+
+
+ + + {expanded && } + + + ); +} + +function WalkthroughScroll() { + const container = useRef(null); + const { scrollYProgress } = useScroll({ + target: container, + offset: ["start start", "end end"], + }); + + return ( +
+ {walkthroughSteps.map((project, i) => { + const targetScale = Math.max(0.6, 1 - (walkthroughSteps.length - i - 1) * 0.05); + return ( + + ); + })} +
+ ); +} + +export { WalkthroughScroll, WalkthroughCard }; diff --git a/surfsense_web/package.json b/surfsense_web/package.json index 9a10bff02..6f9e879a2 100644 --- a/surfsense_web/package.json +++ b/surfsense_web/package.json @@ -82,6 +82,7 @@ "jotai": "^2.15.1", "jotai-tanstack-query": "^0.11.0", "katex": "^0.16.28", + "lenis": "^1.3.17", "lucide-react": "^0.477.0", "motion": "^12.23.22", "next": "^16.1.0", diff --git a/surfsense_web/pnpm-lock.yaml b/surfsense_web/pnpm-lock.yaml index fc6b3ca9f..1a7606808 100644 --- a/surfsense_web/pnpm-lock.yaml +++ b/surfsense_web/pnpm-lock.yaml @@ -191,6 +191,9 @@ importers: katex: specifier: ^0.16.28 version: 0.16.28 + lenis: + specifier: ^1.3.17 + version: 1.3.17(react@19.2.3) lucide-react: specifier: ^0.477.0 version: 0.477.0(react@19.2.3) @@ -5380,6 +5383,20 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lenis@1.3.17: + resolution: {integrity: sha512-k9T9rgcxne49ggJOvXCraWn5dt7u2mO+BNkhyu6yxuEnm9c092kAW5Bus5SO211zUvx7aCCEtzy9UWr0RB+oJw==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + react: '>=17.0.0' + vue: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + react: + optional: true + vue: + optional: true + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -6702,6 +6719,9 @@ packages: tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} + tailwind-merge@3.4.1: + resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==} + tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: @@ -11236,7 +11256,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-easy-sort: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - tailwind-merge: 3.4.0 + tailwind-merge: 3.4.1 transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -12532,6 +12552,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lenis@1.3.17(react@19.2.3): + optionalDependencies: + react: 19.2.3 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -14394,6 +14418,8 @@ snapshots: tailwind-merge@3.4.0: {} + tailwind-merge@3.4.1: {} + tailwindcss-animate@1.0.7(tailwindcss@4.1.18): dependencies: tailwindcss: 4.1.18 diff --git a/surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif b/surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif new file mode 100644 index 000000000..43b182f05 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/BQnaGif_compressed.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif b/surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif new file mode 100644 index 000000000..f60c94783 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/BSNCGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif b/surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif new file mode 100644 index 000000000..de3330f51 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/ConnectorFlowGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif b/surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif new file mode 100644 index 000000000..9e7b0ed58 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/DocUploadGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif b/surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif new file mode 100644 index 000000000..618672679 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/ImageGenGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/LoginFlowGif.gif b/surfsense_web/public/homepage/hero_tutorial/LoginFlowGif.gif new file mode 100644 index 000000000..20999f9f0 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/LoginFlowGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif b/surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif new file mode 100644 index 000000000..4d85f90a3 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/PodcastGenGif.gif differ diff --git a/surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif b/surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif new file mode 100644 index 000000000..074bd4299 Binary files /dev/null and b/surfsense_web/public/homepage/hero_tutorial/ReportGenGif_compressed.gif differ diff --git a/surfsense_web/public/homepage/temp_hero_dark.png b/surfsense_web/public/homepage/temp_hero_dark.png deleted file mode 100644 index 92f4284a7..000000000 Binary files a/surfsense_web/public/homepage/temp_hero_dark.png and /dev/null differ diff --git a/surfsense_web/public/homepage/temp_hero_light.png b/surfsense_web/public/homepage/temp_hero_light.png deleted file mode 100644 index d7a0734cf..000000000 Binary files a/surfsense_web/public/homepage/temp_hero_light.png and /dev/null differ