feat: updated homepage

- Added the 'lenis' library for smooth scrolling functionality.
- Integrated 'UseCasesGrid' component into the homepage layout.
- Updated 'HeroSection' to replace image elements with 'WalkthroughScroll' for enhanced user experience.
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-02-16 05:50:47 -08:00
parent 9e359a79b9
commit 5825ab9884
18 changed files with 346 additions and 65 deletions

View file

@ -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 (
<main className="min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 text-gray-900 dark:from-black dark:to-gray-900 dark:text-white">
<HeroSection />
<UseCasesGrid />
<FeaturesCards />
<FeaturesBentoGrid />
<ExternalIntegrations />

View file

@ -141,9 +141,7 @@ export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMent
// Preserve mention chips as inline @title tokens.
if (element.hasAttribute(CHIP_DATA_ATTR)) {
const title = element
.querySelector("[data-mention-title='true']")
?.textContent?.trim();
const title = element.querySelector("[data-mention-title='true']")?.textContent?.trim();
if (title) {
return `@${title}`;
}

View file

@ -1,10 +1,10 @@
"use client";
import { useFeatureFlagVariantKey } from "@posthog/react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useRef, useState } from "react";
import Balancer from "react-wrap-balancer";
import { WalkthroughScroll } from "@/components/ui/walkthrough-scroll";
import { AUTH_TYPE, BACKEND_URL } from "@/lib/env-config";
import { trackLoginAttempt } from "@/lib/posthog/events";
import { cn } from "@/lib/utils";
@ -40,41 +40,37 @@ export function HeroSection() {
return (
<div
ref={parentRef}
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-20 md:px-8 md:py-40"
className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden px-4 py-12 md:px-8 md:py-24"
>
<BackgroundGrids />
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: -400,
translateX: 600,
duration: 7,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: -200,
translateX: 800,
duration: 4,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
parentRef={parentRef}
beamOptions={{
initialX: 200,
translateX: 1200,
duration: 5,
repeatDelay: 3,
}}
containerRef={containerRef}
parentRef={parentRef}
/>
<CollisionMechanism
containerRef={containerRef}
parentRef={parentRef}
beamOptions={{
initialX: 400,
@ -106,34 +102,12 @@ export function HeroSection() {
<p className="relative z-50 mx-auto mt-0 max-w-lg px-4 text-center text-base/6 text-gray-600 dark:text-gray-200">
Then chat with it in real-time, even alongside your team.
</p>
<div className="mb-10 mt-8 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-20">
<div className="mb-6 mt-6 flex w-full flex-col items-center justify-center gap-4 px-8 sm:flex-row md:mb-10">
<GetStartedButton />
<ContactSalesButton />
</div>
<div
ref={containerRef}
className="relative mx-auto max-w-7xl rounded-[32px] border border-neutral-200/50 bg-neutral-100 p-2 backdrop-blur-lg md:p-4 dark:border-neutral-700 dark:bg-neutral-800/50"
>
<div className="rounded-[24px] border border-neutral-200 bg-white p-2 dark:border-neutral-700 dark:bg-black">
{/* Light mode image */}
<Image
src="/homepage/main_demo.webp"
alt="header"
width={1920}
height={1080}
className="rounded-[20px] block dark:hidden"
unoptimized
/>
{/* Dark mode image */}
<Image
src="/homepage/main_demo.webp"
alt="header"
width={1920}
height={1080}
className="rounded-[20px] hidden dark:block"
unoptimized
/>
</div>
<div ref={containerRef} className="relative w-full">
<WalkthroughScroll />
</div>
</div>
);
@ -236,24 +210,23 @@ const BackgroundGrids = () => {
);
};
const CollisionMechanism = React.forwardRef<
HTMLDivElement,
{
containerRef: React.RefObject<HTMLDivElement | null>;
parentRef: React.RefObject<HTMLDivElement | null>;
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<HTMLDivElement | null>;
beamOptions?: {
initialX?: number;
translateX?: number;
initialY?: number;
translateY?: number;
rotate?: number;
className?: string;
duration?: number;
delay?: number;
repeatDelay?: number;
};
}) => {
const beamRef = useRef<HTMLDivElement>(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<
</AnimatePresence>
</>
);
});
CollisionMechanism.displayName = "CollisionMechanism";
};
const Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
const spans = Array.from({ length: 20 }, (_, index) => ({

View file

@ -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 (
<>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className={`group overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-sm transition-shadow duration-300 hover:shadow-xl dark:border-neutral-700/60 dark:bg-neutral-900 ${className ?? ""}`}
>
<div
className="cursor-pointer overflow-hidden bg-neutral-50 p-2 dark:bg-neutral-950"
onClick={open}
>
<img
src={src}
alt={title}
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
/>
</div>
<div className="px-5 py-4">
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">{title}</h3>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">{description}</p>
</div>
</motion.div>
<AnimatePresence>
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
</AnimatePresence>
</>
);
}
export function UseCasesGrid() {
return (
<section className="relative mx-auto max-w-7xl px-4 py-4 sm:px-6 sm:py-8 lg:px-8">
<div className="mb-6 text-center">
<h2 className="text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-white">
What You Can Do
</h2>
</div>
{/* First row: 2 larger cards */}
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
{useCases.slice(0, 2).map((useCase) => (
<UseCaseCard key={useCase.title} {...useCase} />
))}
</div>
{/* Second row: 3 equal cards */}
<div className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{useCases.slice(2).map((useCase) => (
<UseCaseCard key={useCase.title} {...useCase} />
))}
</div>
<p className="mt-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
And more coming soon.
</p>
</section>
);
}

View file

@ -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 (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-100 flex items-center justify-center bg-black/70 p-4 backdrop-blur-sm sm:p-8"
onClick={onClose}
>
<motion.img
initial={{ scale: 0.85, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.85, opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
src={src}
alt={alt}
className="max-h-[90vh] max-w-[90vw] rounded-2xl shadow-2xl"
onClick={(e) => e.stopPropagation()}
/>
</motion.div>
);
}
function useExpandedGif() {
const [expanded, setExpanded] = useState(false);
const open = useCallback(() => setExpanded(true), []);
const close = useCallback(() => setExpanded(false), []);
return { expanded, open, close };
}
export { ExpandedGifOverlay, useExpandedGif };

View file

@ -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<typeof useScroll>["scrollYProgress"];
range: [number, number];
targetScale: number;
}) {
const container = useRef<HTMLDivElement>(null);
const scale = useTransform(progress, range, [1, targetScale]);
const { expanded, open, close } = useExpandedGif();
return (
<>
<div
ref={container}
className="sticky top-0 flex items-center justify-center px-4 sm:px-6 lg:px-8"
>
<motion.div
style={{
scale,
top: `calc(10vh + ${i * 30}px)`,
}}
className="relative flex origin-top flex-col overflow-hidden rounded-2xl border border-neutral-200/60 bg-white shadow-xl sm:rounded-3xl dark:border-neutral-700/60 dark:bg-neutral-900
w-full max-w-[340px] sm:max-w-[520px] md:max-w-[680px] lg:max-w-[900px]"
>
<div className="flex items-center gap-3 border-b border-neutral-200/60 px-4 py-3 sm:px-6 sm:py-4 dark:border-neutral-700/60">
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-900 text-xs font-semibold text-white sm:h-8 sm:w-8 sm:text-sm dark:bg-white dark:text-neutral-900">
{step}
</span>
<div className="min-w-0">
<h3 className="truncate text-sm font-semibold text-neutral-900 sm:text-base dark:text-white">
{title}
</h3>
<p className="hidden text-xs text-neutral-500 sm:block dark:text-neutral-400">
{description}
</p>
</div>
</div>
<div
className="cursor-pointer bg-neutral-50 p-2 sm:p-3 dark:bg-neutral-950"
onClick={open}
>
<img src={src} alt={title} className="w-full rounded-lg object-cover sm:rounded-xl" />
</div>
</motion.div>
</div>
<AnimatePresence>
{expanded && <ExpandedGifOverlay src={src} alt={title} onClose={close} />}
</AnimatePresence>
</>
);
}
function WalkthroughScroll() {
const container = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: container,
offset: ["start start", "end end"],
});
return (
<div
ref={container}
className="relative flex w-full flex-col items-center justify-center pb-[15vh] pt-[1vh] sm:pb-[18vh] sm:pt-[2vh]"
>
{walkthroughSteps.map((project, i) => {
const targetScale = Math.max(0.6, 1 - (walkthroughSteps.length - i - 1) * 0.05);
return (
<WalkthroughCard
key={`walkthrough_${i}`}
i={i}
{...project}
progress={scrollYProgress}
range={[i * (1 / walkthroughSteps.length), 1]}
targetScale={targetScale}
/>
);
})}
</div>
);
}
export { WalkthroughScroll, WalkthroughCard };

View file

@ -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",

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB