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 */}
-
- {/* Dark mode image */}
-
-
+
+
);
@@ -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}
+
{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}
+
+
+
+
+

+
+
+
+
+
+ {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