mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-04-25 11:18:22 +00:00
feat: implement the first version of landing page
This commit is contained in:
parent
307972f93e
commit
3f4bcd9433
@ -57,6 +57,7 @@
|
|||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
"hast": "^1.0.0",
|
"hast": "^1.0.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.26.2",
|
"motion": "^12.26.2",
|
||||||
@ -81,6 +82,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.0.15",
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
|
"@types/gsap": "^3.0.0",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
|
|||||||
19
frontend/pnpm-lock.yaml
generated
19
frontend/pnpm-lock.yaml
generated
@ -131,6 +131,9 @@ importers:
|
|||||||
embla-carousel-react:
|
embla-carousel-react:
|
||||||
specifier: ^8.6.0
|
specifier: ^8.6.0
|
||||||
version: 8.6.0(react@19.2.3)
|
version: 8.6.0(react@19.2.3)
|
||||||
|
gsap:
|
||||||
|
specifier: ^3.13.0
|
||||||
|
version: 3.14.2
|
||||||
hast:
|
hast:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@ -198,6 +201,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.0.15
|
specifier: ^4.0.15
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
|
'@types/gsap':
|
||||||
|
specifier: ^3.0.0
|
||||||
|
version: 3.0.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.14.10
|
specifier: ^20.14.10
|
||||||
version: 20.19.29
|
version: 20.19.29
|
||||||
@ -2008,6 +2014,10 @@ packages:
|
|||||||
'@types/geojson@7946.0.16':
|
'@types/geojson@7946.0.16':
|
||||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
|
'@types/gsap@3.0.0':
|
||||||
|
resolution: {integrity: sha512-BbWLi4WRHGze4C8NV7U7yRevuBFiPkPZZyGa0rryanvh/9HPUFXTNBXsGQxJZJq7Ix7j4RXMYodP3s+OsqCErg==}
|
||||||
|
deprecated: This is a stub types definition. gsap provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
@ -3294,6 +3304,9 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
gsap@3.14.2:
|
||||||
|
resolution: {integrity: sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA==}
|
||||||
|
|
||||||
h3@1.15.5:
|
h3@1.15.5:
|
||||||
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
|
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
|
||||||
|
|
||||||
@ -6890,6 +6903,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/geojson@7946.0.16': {}
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
|
'@types/gsap@3.0.0':
|
||||||
|
dependencies:
|
||||||
|
gsap: 3.14.2
|
||||||
|
|
||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
@ -8407,6 +8424,8 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
gsap@3.14.2: {}
|
||||||
|
|
||||||
h3@1.15.5:
|
h3@1.15.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie-es: 1.2.2
|
cookie-es: 1.2.2
|
||||||
|
|||||||
@ -1,82 +1,25 @@
|
|||||||
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
import { Footer } from "@/components/landing/footer";
|
||||||
import Link from "next/link";
|
import { Header } from "@/components/landing/header";
|
||||||
|
import { Hero } from "@/components/landing/hero";
|
||||||
import { Jumbotron } from "@/components/landing/jumbotron";
|
import { CaseStudySection } from "@/components/landing/sections/case-study-section";
|
||||||
import { Button } from "@/components/ui/button";
|
import { CommunitySection } from "@/components/landing/sections/community-section";
|
||||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
import { SandboxSection } from "@/components/landing/sections/sandbox-section";
|
||||||
import { env } from "@/env";
|
import { SkillsSection } from "@/components/landing/sections/skills-section";
|
||||||
|
import { WhatsNewSection } from "@/components/landing/sections/whats-new-section";
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-[#0a0a0a]">
|
<div className="min-h-screen w-full bg-[#0a0a0a]">
|
||||||
<header className="container-md absolute top-0 right-0 left-0 z-10 mx-auto flex h-16 items-center justify-between backdrop-blur-xs">
|
<Header />
|
||||||
<div className="font-serif text-xl">
|
<main className="flex w-full flex-col">
|
||||||
<h1>DeerFlow</h1>
|
<Hero />
|
||||||
</div>
|
<CaseStudySection />
|
||||||
<div className="relative">
|
<SkillsSection />
|
||||||
<div
|
<SandboxSection />
|
||||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-30 blur-2xl"
|
<WhatsNewSection />
|
||||||
style={{
|
<CommunitySection />
|
||||||
background: "linear-gradient(90deg, #ff80b5 0%, #9089fc 100%)",
|
|
||||||
filter: "blur(16px)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
asChild
|
|
||||||
className="group relative z-10"
|
|
||||||
>
|
|
||||||
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
|
||||||
<GitHubLogoIcon className="size-4" />
|
|
||||||
Star on GitHub
|
|
||||||
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
|
||||||
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
|
|
||||||
</header>
|
|
||||||
<main className="w-full">
|
|
||||||
<Jumbotron />
|
|
||||||
</main>
|
</main>
|
||||||
<footer className="container-md mx-auto"></footer>
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function StarCounter() {
|
|
||||||
let stars = 10000; // Default value
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
"https://api.github.com/repos/bytedance/deer-flow",
|
|
||||||
{
|
|
||||||
headers: env.GITHUB_OAUTH_TOKEN
|
|
||||||
? {
|
|
||||||
Authorization: `Bearer ${env.GITHUB_OAUTH_TOKEN}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
next: {
|
|
||||||
revalidate: 3600,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
stars = data.stargazers_count ?? stars; // Update stars if API response is valid
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching GitHub stars:", error);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StarFilledIcon className="size-4 transition-colors duration-300 group-hover:text-yellow-500" />
|
|
||||||
{stars && (
|
|
||||||
<NumberTicker className="font-mono tabular-nums" value={stars} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -0,0 +1,652 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Folder,
|
||||||
|
FileText,
|
||||||
|
Search,
|
||||||
|
Globe,
|
||||||
|
Check,
|
||||||
|
Sparkles,
|
||||||
|
Terminal,
|
||||||
|
Play,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { motion, AnimatePresence } from "motion/react";
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
type AnimationPhase =
|
||||||
|
| "idle"
|
||||||
|
| "user-input"
|
||||||
|
| "scanning"
|
||||||
|
| "load-skill"
|
||||||
|
| "load-template"
|
||||||
|
| "researching"
|
||||||
|
| "load-frontend"
|
||||||
|
| "building"
|
||||||
|
| "load-deploy"
|
||||||
|
| "deploying"
|
||||||
|
| "done";
|
||||||
|
|
||||||
|
interface FileItem {
|
||||||
|
name: string;
|
||||||
|
type: "folder" | "file";
|
||||||
|
indent: number;
|
||||||
|
highlight?: boolean;
|
||||||
|
active?: boolean;
|
||||||
|
done?: boolean;
|
||||||
|
dragging?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchSteps = [
|
||||||
|
{ type: "search", text: "mRNA lipid nanoparticle delivery 2024" },
|
||||||
|
{ type: "fetch", text: "nature.com/articles/s41587-024..." },
|
||||||
|
{ type: "search", text: "LNP ionizable lipids efficiency" },
|
||||||
|
{ type: "fetch", text: "pubs.acs.org/doi/10.1021/..." },
|
||||||
|
{ type: "search", text: "targeted mRNA tissue-specific" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Animation duration configuration - adjust the duration for each step here
|
||||||
|
const ANIMATION_DELAYS = {
|
||||||
|
"user-input": 0, // User input phase duration (milliseconds)
|
||||||
|
scanning: 2000, // Scanning phase duration
|
||||||
|
"load-skill": 1500, // Load skill phase duration
|
||||||
|
"load-template": 1200, // Load template phase duration
|
||||||
|
researching: 800, // Researching phase duration
|
||||||
|
"load-frontend": 800, // Load frontend phase duration
|
||||||
|
building: 1200, // Building phase duration
|
||||||
|
"load-deploy": 2500, // Load deploy phase duration
|
||||||
|
deploying: 1200, // Deploying phase duration
|
||||||
|
done: 2500, // Done phase duration (final step)
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default function ProgressiveSkillsAnimation() {
|
||||||
|
const [phase, setPhase] = useState<AnimationPhase>("idle");
|
||||||
|
const [searchIndex, setSearchIndex] = useState(0);
|
||||||
|
const [buildIndex, setBuildIndex] = useState(0);
|
||||||
|
const [, setChatMessages] = useState<React.ReactNode[]>([]);
|
||||||
|
const [, setShowWorkspace] = useState(false);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [hasPlayed, setHasPlayed] = useState(false);
|
||||||
|
const [hasAutoPlayed, setHasAutoPlayed] = useState(false);
|
||||||
|
const chatMessagesRef = useRef<HTMLDivElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Additional display duration after the final step (done) completes, used to show the final result
|
||||||
|
const FINAL_DISPLAY_DURATION = 3000; // milliseconds
|
||||||
|
|
||||||
|
// Play animation only when isPlaying is true
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlaying) return;
|
||||||
|
|
||||||
|
const timeline = [
|
||||||
|
{ phase: "user-input" as const, delay: ANIMATION_DELAYS["user-input"] },
|
||||||
|
{ phase: "scanning" as const, delay: ANIMATION_DELAYS.scanning },
|
||||||
|
{ phase: "load-skill" as const, delay: ANIMATION_DELAYS["load-skill"] },
|
||||||
|
{
|
||||||
|
phase: "load-template" as const,
|
||||||
|
delay: ANIMATION_DELAYS["load-template"],
|
||||||
|
},
|
||||||
|
{ phase: "researching" as const, delay: ANIMATION_DELAYS.researching },
|
||||||
|
{
|
||||||
|
phase: "load-frontend" as const,
|
||||||
|
delay: ANIMATION_DELAYS["load-frontend"],
|
||||||
|
},
|
||||||
|
{ phase: "building" as const, delay: ANIMATION_DELAYS.building },
|
||||||
|
{ phase: "load-deploy" as const, delay: ANIMATION_DELAYS["load-deploy"] },
|
||||||
|
{ phase: "deploying" as const, delay: ANIMATION_DELAYS.deploying },
|
||||||
|
{ phase: "done" as const, delay: ANIMATION_DELAYS.done },
|
||||||
|
];
|
||||||
|
|
||||||
|
let totalDelay = 0;
|
||||||
|
const timeouts: NodeJS.Timeout[] = [];
|
||||||
|
|
||||||
|
timeline.forEach(({ phase, delay }) => {
|
||||||
|
totalDelay += delay;
|
||||||
|
timeouts.push(setTimeout(() => setPhase(phase), totalDelay));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset after animation completes
|
||||||
|
// Total duration for the final step = ANIMATION_DELAYS["done"] + FINAL_DISPLAY_DURATION
|
||||||
|
timeouts.push(
|
||||||
|
setTimeout(() => {
|
||||||
|
setPhase("idle");
|
||||||
|
setChatMessages([]);
|
||||||
|
setSearchIndex(0);
|
||||||
|
setBuildIndex(0);
|
||||||
|
setShowWorkspace(false);
|
||||||
|
setIsPlaying(false);
|
||||||
|
}, totalDelay + FINAL_DISPLAY_DURATION),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => timeouts.forEach(clearTimeout);
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
setHasPlayed(true);
|
||||||
|
setPhase("idle");
|
||||||
|
setChatMessages([]);
|
||||||
|
setSearchIndex(0);
|
||||||
|
setBuildIndex(0);
|
||||||
|
setShowWorkspace(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-play when component enters viewport for the first time
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasAutoPlayed || !containerRef.current) return;
|
||||||
|
|
||||||
|
const containerElement = containerRef.current;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting && !hasAutoPlayed && !isPlaying) {
|
||||||
|
setHasAutoPlayed(true);
|
||||||
|
// Small delay before auto-playing for better UX
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
setHasPlayed(true);
|
||||||
|
setPhase("idle");
|
||||||
|
setChatMessages([]);
|
||||||
|
setSearchIndex(0);
|
||||||
|
setBuildIndex(0);
|
||||||
|
setShowWorkspace(false);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.3, // Trigger when 30% of the component is visible
|
||||||
|
rootMargin: "0px",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(containerElement);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (containerElement) {
|
||||||
|
observer.unobserve(containerElement);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [hasAutoPlayed, isPlaying]);
|
||||||
|
|
||||||
|
// Handle search animation
|
||||||
|
useEffect(() => {
|
||||||
|
if (phase === "researching" && searchIndex < searchSteps.length) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setSearchIndex((i) => i + 1);
|
||||||
|
}, 350);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [phase, searchIndex]);
|
||||||
|
|
||||||
|
// Handle build animation
|
||||||
|
useEffect(() => {
|
||||||
|
if (phase === "building" && buildIndex < 3) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setBuildIndex((i) => i + 1);
|
||||||
|
}, 600);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
if (phase === "building") {
|
||||||
|
setShowWorkspace(true);
|
||||||
|
}
|
||||||
|
}, [phase, buildIndex]);
|
||||||
|
|
||||||
|
// Auto scroll chat to bottom when messages change
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatMessagesRef.current && phase !== "idle") {
|
||||||
|
chatMessagesRef.current.scrollTo({
|
||||||
|
top: chatMessagesRef.current.scrollHeight,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [phase, searchIndex, buildIndex]);
|
||||||
|
|
||||||
|
const getFileTree = (): FileItem[] => {
|
||||||
|
const base: FileItem[] = [
|
||||||
|
{
|
||||||
|
name: "deep-search",
|
||||||
|
type: "folder",
|
||||||
|
indent: 0,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
active: ["load-skill", "load-template", "researching"].includes(phase),
|
||||||
|
done: [
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SKILL.md",
|
||||||
|
type: "file",
|
||||||
|
indent: 1,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
dragging: phase === "load-skill",
|
||||||
|
done: [
|
||||||
|
"load-template",
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "biotech.md",
|
||||||
|
type: "file",
|
||||||
|
indent: 1,
|
||||||
|
highlight: phase === "load-template",
|
||||||
|
dragging: phase === "load-template",
|
||||||
|
done: [
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase),
|
||||||
|
},
|
||||||
|
{ name: "computer-science.md", type: "file", indent: 1 },
|
||||||
|
{ name: "physics.md", type: "file", indent: 1 },
|
||||||
|
{
|
||||||
|
name: "frontend-design",
|
||||||
|
type: "folder",
|
||||||
|
indent: 0,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
active: ["load-frontend", "building"].includes(phase),
|
||||||
|
done: ["building", "load-deploy", "deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SKILL.md",
|
||||||
|
type: "file",
|
||||||
|
indent: 1,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
dragging: phase === "load-frontend",
|
||||||
|
done: ["building", "load-deploy", "deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deploy",
|
||||||
|
type: "folder",
|
||||||
|
indent: 0,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
active: ["load-deploy", "deploying"].includes(phase),
|
||||||
|
done: ["deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SKILL.md",
|
||||||
|
type: "file",
|
||||||
|
indent: 1,
|
||||||
|
highlight: phase === "scanning",
|
||||||
|
dragging: phase === "load-deploy",
|
||||||
|
done: ["deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scripts",
|
||||||
|
type: "folder",
|
||||||
|
indent: 1,
|
||||||
|
done: ["deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deploy.sh",
|
||||||
|
type: "file",
|
||||||
|
indent: 2,
|
||||||
|
done: ["deploying", "done"].includes(phase),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return base;
|
||||||
|
};
|
||||||
|
|
||||||
|
const workspaceFiles = ["index.html", "index.css", "index.js"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative flex h-[calc(100vh-280px)] w-full items-center justify-center overflow-hidden p-8"
|
||||||
|
>
|
||||||
|
{/* Overlay and Play Button */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{!isPlaying && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="absolute inset-0 z-50 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.8, opacity: 0 }}
|
||||||
|
onClick={handlePlay}
|
||||||
|
className="group flex flex-col items-center gap-4 transition-transform hover:scale-105 active:scale-95"
|
||||||
|
>
|
||||||
|
<div className="flex h-24 w-24 items-center justify-center rounded-full bg-white/10 backdrop-blur-md transition-all group-hover:bg-white/20">
|
||||||
|
<Play
|
||||||
|
size={48}
|
||||||
|
className="ml-1 text-white transition-transform group-hover:scale-110"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-lg font-medium text-white">
|
||||||
|
{hasPlayed ? "Click to replay" : "Click to play"}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<div className="flex h-full max-h-[700px] w-full max-w-6xl gap-8">
|
||||||
|
{/* Left: File Tree */}
|
||||||
|
<div className="flex flex-1 flex-col">
|
||||||
|
<motion.div
|
||||||
|
className="mb-4 font-mono text-sm text-zinc-500"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
>
|
||||||
|
/mnt/skills/
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{getFileTree().map((item, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={`${item.name}-${index}`}
|
||||||
|
className={`flex items-center gap-3 text-lg font-medium transition-all duration-300 ${
|
||||||
|
item.done
|
||||||
|
? "text-green-500"
|
||||||
|
: item.dragging
|
||||||
|
? "translate-x-8 scale-105 text-blue-400"
|
||||||
|
: item.active
|
||||||
|
? "text-white"
|
||||||
|
: item.highlight
|
||||||
|
? "text-purple-400"
|
||||||
|
: "text-zinc-600"
|
||||||
|
}`}
|
||||||
|
style={{ paddingLeft: `${item.indent * 24}px` }}
|
||||||
|
animate={
|
||||||
|
item.done
|
||||||
|
? {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.type === "folder" ? (
|
||||||
|
<Folder
|
||||||
|
size={20}
|
||||||
|
className={
|
||||||
|
item.done
|
||||||
|
? "text-green-500"
|
||||||
|
: item.highlight
|
||||||
|
? "text-purple-400"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FileText
|
||||||
|
size={20}
|
||||||
|
className={
|
||||||
|
item.done
|
||||||
|
? "text-green-500"
|
||||||
|
: item.highlight
|
||||||
|
? "text-purple-400"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{item.name}</span>
|
||||||
|
{item.done && <Check size={16} className="text-green-500" />}
|
||||||
|
{item.highlight && !item.done && (
|
||||||
|
<Sparkles size={16} className="text-purple-400" />
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Chat Interface */}
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden rounded-2xl border border-zinc-800 bg-zinc-900/50">
|
||||||
|
{/* Chat Header */}
|
||||||
|
<div className="border-b border-zinc-800 p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-3 w-3 rounded-full bg-green-500" />
|
||||||
|
<span className="text-sm text-zinc-400">DeerFlow Agent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Messages */}
|
||||||
|
<div
|
||||||
|
ref={chatMessagesRef}
|
||||||
|
className="flex-1 space-y-4 overflow-y-auto p-6"
|
||||||
|
>
|
||||||
|
{/* User Message */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{phase !== "idle" && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex justify-end"
|
||||||
|
>
|
||||||
|
<div className="max-w-[90%] rounded-2xl rounded-tr-sm bg-blue-600 px-5 py-3">
|
||||||
|
<p className="text-base">
|
||||||
|
Research mRNA delivery, build a landing page, deploy to
|
||||||
|
Vercel
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Agent Messages */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{phase !== "idle" && phase !== "user-input" && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="space-y-3"
|
||||||
|
>
|
||||||
|
{/* Found Skills */}
|
||||||
|
{[
|
||||||
|
"scanning",
|
||||||
|
"load-skill",
|
||||||
|
"load-template",
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase) && (
|
||||||
|
<div className="text-base text-zinc-300">
|
||||||
|
<span className="text-purple-400">✨</span> Found 3 skills
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Researching Section */}
|
||||||
|
{[
|
||||||
|
"load-skill",
|
||||||
|
"load-template",
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase) && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<hr className="mb-3 border-zinc-700" />
|
||||||
|
<div className="mb-3 text-zinc-300">
|
||||||
|
🔬 Researching...
|
||||||
|
</div>
|
||||||
|
<div className="mb-3 space-y-2">
|
||||||
|
{/* Loading SKILL.md */}
|
||||||
|
{[
|
||||||
|
"load-skill",
|
||||||
|
"load-template",
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase) && (
|
||||||
|
<div className="flex items-center gap-2 pl-4 text-zinc-400">
|
||||||
|
<FileText size={16} />
|
||||||
|
<span>Loading deep-search/SKILL.md...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Loading biotech.md */}
|
||||||
|
{[
|
||||||
|
"load-template",
|
||||||
|
"researching",
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase) && (
|
||||||
|
<div className="flex items-center gap-2 pl-4 text-zinc-400">
|
||||||
|
<FileText size={16} />
|
||||||
|
<span>
|
||||||
|
Found biotech related topic, loading
|
||||||
|
deep-search/biotech.md...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Search steps */}
|
||||||
|
{phase === "researching" && (
|
||||||
|
<div className="max-h-[180px] space-y-2 overflow-hidden pl-4">
|
||||||
|
{searchSteps.slice(0, searchIndex).map((step, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-500"
|
||||||
|
>
|
||||||
|
{step.type === "search" ? (
|
||||||
|
<Search size={14} className="text-blue-400" />
|
||||||
|
) : (
|
||||||
|
<Globe size={14} className="text-green-400" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">{step.text}</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{[
|
||||||
|
"load-frontend",
|
||||||
|
"building",
|
||||||
|
"load-deploy",
|
||||||
|
"deploying",
|
||||||
|
"done",
|
||||||
|
].includes(phase) && (
|
||||||
|
<div className="max-h-[180px] space-y-2 overflow-hidden pl-4">
|
||||||
|
{searchSteps.map((step, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="flex items-center gap-2 text-sm text-zinc-500"
|
||||||
|
>
|
||||||
|
{step.type === "search" ? (
|
||||||
|
<Search size={14} className="text-blue-400" />
|
||||||
|
) : (
|
||||||
|
<Globe size={14} className="text-green-400" />
|
||||||
|
)}
|
||||||
|
<span className="truncate">{step.text}</span>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Building */}
|
||||||
|
{["building", "load-deploy", "deploying", "done"].includes(
|
||||||
|
phase,
|
||||||
|
) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<hr className="mb-3 border-zinc-700" />
|
||||||
|
<div className="mb-3 text-zinc-300">🔨 Building...</div>
|
||||||
|
<div className="mb-3 flex items-center gap-2 pl-4 text-zinc-400">
|
||||||
|
<FileText size={16} />
|
||||||
|
<span>Loading frontend-design/SKILL.md...</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 pl-4">
|
||||||
|
{workspaceFiles.slice(0, buildIndex).map((file) => (
|
||||||
|
<motion.div
|
||||||
|
key={file}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="flex items-center gap-2 text-sm text-green-500"
|
||||||
|
>
|
||||||
|
<FileText size={14} />
|
||||||
|
<span>{file}</span>
|
||||||
|
<Check size={14} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deploying */}
|
||||||
|
{["load-deploy", "deploying", "done"].includes(phase) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<hr className="mb-3 border-zinc-700" />
|
||||||
|
<div className="mb-3 text-zinc-300">🚀 Deploying...</div>
|
||||||
|
<div className="mb-3 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 pl-4 text-zinc-400">
|
||||||
|
<FileText size={16} />
|
||||||
|
<span>Loading deploy/SKILL.md...</span>
|
||||||
|
</div>
|
||||||
|
{["deploying", "done"].includes(phase) && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="flex items-center gap-2 pl-4 text-zinc-400"
|
||||||
|
>
|
||||||
|
<Terminal size={16} />
|
||||||
|
<span>Executing deploy.sh</span>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{phase === "done" && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="mt-4 rounded-xl border border-green-500/30 bg-green-500/10 p-4"
|
||||||
|
>
|
||||||
|
<div className="text-lg font-medium text-green-500">
|
||||||
|
✅ Live at biotech-startup.vercel.app
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chat Input (decorative) */}
|
||||||
|
<div className="border-t border-zinc-800 p-4">
|
||||||
|
<div className="rounded-xl bg-zinc-800 px-4 py-3 text-sm text-zinc-500">
|
||||||
|
Ask DeerFlow anything...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
frontend/src/components/landing/footer.tsx
Normal file
19
frontend/src/components/landing/footer.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
const year = useMemo(() => new Date().getFullYear(), []);
|
||||||
|
return (
|
||||||
|
<footer className="container-md mx-auto mt-32 flex flex-col items-center justify-center">
|
||||||
|
<hr className="from-border/0 to-border/0 m-0 h-px w-full border-none bg-linear-to-r via-white/20" />
|
||||||
|
<div className="text-muted-foreground container flex h-20 flex-col items-center justify-center text-sm">
|
||||||
|
<p className="text-center font-serif text-lg md:text-xl">
|
||||||
|
"Originated from Open Source, give back to Open Source."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground container mb-8 flex flex-col items-center justify-center text-xs">
|
||||||
|
<p>Licensed under MIT License</p>
|
||||||
|
<p>© {year} DeerFlow</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
frontend/src/components/landing/header.tsx
Normal file
76
frontend/src/components/landing/header.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||||
|
import { env } from "@/env";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="container-md fixed top-0 right-0 left-0 z-20 mx-auto flex h-16 items-center justify-between backdrop-blur-xs">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<a href="https://github.com/bytedance/deer-flow" target="_blank">
|
||||||
|
<h1 className="font-serif text-xl">DeerFlow</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 z-0 h-full w-full rounded-full opacity-30 blur-2xl"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(90deg, #ff80b5 0%, #9089fc 100%)",
|
||||||
|
filter: "blur(16px)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
asChild
|
||||||
|
className="group relative z-10"
|
||||||
|
>
|
||||||
|
<a href="https://github.com/bytedance/deer-flow" target="_blank">
|
||||||
|
<GitHubLogoIcon className="size-4" />
|
||||||
|
Star on GitHub
|
||||||
|
{env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||||
|
env.GITHUB_OAUTH_TOKEN && <StarCounter />}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<hr className="from-border/0 via-border/70 to-border/0 absolute top-16 right-0 left-0 z-10 m-0 h-px w-full border-none bg-linear-to-r" />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function StarCounter() {
|
||||||
|
let stars = 10000; // Default value
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://api.github.com/repos/bytedance/deer-flow",
|
||||||
|
{
|
||||||
|
headers: env.GITHUB_OAUTH_TOKEN
|
||||||
|
? {
|
||||||
|
Authorization: `Bearer ${env.GITHUB_OAUTH_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
next: {
|
||||||
|
revalidate: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
stars = data.stargazers_count ?? stars; // Update stars if API response is valid
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching GitHub stars:", error);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StarFilledIcon className="size-4 transition-colors duration-300 group-hover:text-yellow-500" />
|
||||||
|
{stars && (
|
||||||
|
<NumberTicker className="font-mono tabular-nums" value={stars} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ChevronRightIcon } from "lucide-react";
|
import { ChevronRightIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
import Galaxy from "@/components/Galaxy";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
||||||
|
import Galaxy from "@/components/ui/galaxy";
|
||||||
import { WordRotate } from "@/components/ui/word-rotate";
|
import { WordRotate } from "@/components/ui/word-rotate";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export function Jumbotron({ className }: { className?: string }) {
|
export function Hero({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -16,30 +17,28 @@ export function Jumbotron({ className }: { className?: string }) {
|
|||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 z-0 bg-black">
|
<div className="absolute inset-0 z-0 bg-black/40">
|
||||||
<Galaxy
|
<Galaxy
|
||||||
mouseRepulsion={false}
|
mouseRepulsion={false}
|
||||||
starSpeed={0.2}
|
starSpeed={0.2}
|
||||||
density={0.6}
|
density={0.6}
|
||||||
glowIntensity={0.3}
|
glowIntensity={0.35}
|
||||||
twinkleIntensity={0.3}
|
twinkleIntensity={0.3}
|
||||||
speed={0.5}
|
speed={0.5}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FlickeringGrid
|
<FlickeringGrid
|
||||||
className="absolute inset-0 z-0 translate-y-[2vh] mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
className="absolute inset-0 z-0 mask-[url(/images/deer.svg)] mask-size-[100vw] mask-center mask-no-repeat md:mask-size-[72vh]"
|
||||||
squareSize={4}
|
squareSize={4}
|
||||||
gridGap={4}
|
gridGap={4}
|
||||||
color={"white"}
|
color={"white"}
|
||||||
maxOpacity={0.2}
|
maxOpacity={0.3}
|
||||||
flickerChance={0.25}
|
flickerChance={0.25}
|
||||||
/>
|
/>
|
||||||
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
|
<div className="container-md relative z-10 mx-auto flex h-screen flex-col items-center justify-center">
|
||||||
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
|
<h1 className="flex items-center gap-2 text-4xl font-bold md:text-6xl">
|
||||||
<WordRotate
|
<WordRotate
|
||||||
words={[
|
words={[
|
||||||
"Do Anything",
|
|
||||||
"Learn Anything",
|
|
||||||
"Deep Research",
|
"Deep Research",
|
||||||
"Collect Data",
|
"Collect Data",
|
||||||
"Analyze Data",
|
"Analyze Data",
|
||||||
@ -51,21 +50,28 @@ export function Jumbotron({ className }: { className?: string }) {
|
|||||||
"Generate Videos",
|
"Generate Videos",
|
||||||
"Generate Songs",
|
"Generate Songs",
|
||||||
"Organize Emails",
|
"Organize Emails",
|
||||||
|
"Do Anything",
|
||||||
|
"Learn Anything",
|
||||||
]}
|
]}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
<div>with DeerFlow</div>
|
<div>with DeerFlow</div>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-8 scale-105 text-center text-2xl opacity-70">
|
<p
|
||||||
|
className="mt-8 scale-105 text-center text-2xl text-shadow-sm"
|
||||||
|
style={{ color: "rgb(180,180,185)" }}
|
||||||
|
>
|
||||||
DeerFlow is an open-source SuperAgent that researches, codes, and
|
DeerFlow is an open-source SuperAgent that researches, codes, and
|
||||||
<br />
|
<br />
|
||||||
creates. With the help of sandboxes, tools and skills, it handles
|
creates. With the help of sandboxes, tools and skills, it handles
|
||||||
<br />
|
<br />
|
||||||
different levels of tasks that could take minutes to hours.
|
different levels of tasks that could take minutes to hours.
|
||||||
</p>
|
</p>
|
||||||
<Button className="size-lg mt-8 scale-108" size="lg">
|
<Link href="/workspace">
|
||||||
<span className="text-md">Get Started with 2.0</span>
|
<Button className="size-lg mt-8 scale-108" size="lg">
|
||||||
<ChevronRightIcon className="size-4" />
|
<span className="text-md">Get Started with 2.0</span>
|
||||||
</Button>
|
<ChevronRightIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
29
frontend/src/components/landing/section.tsx
Normal file
29
frontend/src/components/landing/section.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function Section({
|
||||||
|
className,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
title: React.ReactNode;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className={cn("mx-auto flex flex-col py-16", className)}>
|
||||||
|
<header className="flex flex-col items-center justify-between">
|
||||||
|
<div className="mb-4 bg-linear-to-r from-white via-gray-200 to-gray-400 bg-clip-text text-center text-5xl font-bold text-transparent">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
{subtitle && (
|
||||||
|
<div className="text-muted-foreground text-center text-xl">
|
||||||
|
{subtitle}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
<main className="mt-4">{children}</main>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { SparklesIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import SpotlightCard from "@/components/ui/spotlight-card";
|
||||||
|
|
||||||
|
import { Section } from "../section";
|
||||||
|
|
||||||
|
export function CaseStudySection({ className }: { className?: string }) {
|
||||||
|
const caseStudies = [
|
||||||
|
{
|
||||||
|
title: "2025 Survey",
|
||||||
|
description:
|
||||||
|
"A 12,000-word research report analyzing 47 papers on brain-inspired chips, covering Intel Loihi 2, IBM NorthPole, and SynSense's edge AI solutions.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Indie Hacker's SaaS Landing Page",
|
||||||
|
description:
|
||||||
|
"A fully responsive landing page with hero section, pricing table, testimonials, and Stripe integration — shipped in one conversation.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Transformer Architecture Explained",
|
||||||
|
description:
|
||||||
|
"A 25-slide presentation breaking down self-attention, positional encoding, and KV-cache with hand-drawn style diagrams for a university lecture.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "DeerDeer Explains RAG",
|
||||||
|
description:
|
||||||
|
"A series of 12 illustrations featuring a curious deer mascot explaining Retrieval-Augmented Generation through a library adventure story.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "AI Weekly: Your Tech Podcast",
|
||||||
|
description:
|
||||||
|
"A 20-minute podcast episode where two AI hosts debate whether AI agents will replace traditional SaaS, based on 5 articles you provided.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "How Diffusion Models Work",
|
||||||
|
description:
|
||||||
|
"A 3-minute animated explainer video visualizing the denoising process, from pure noise to a generated image, with voiceover narration.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
className={className}
|
||||||
|
title="Case Studies"
|
||||||
|
subtitle="See how DeerFlow is used in the wild"
|
||||||
|
>
|
||||||
|
<div className="mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{caseStudies.map((caseStudy) => (
|
||||||
|
<SpotlightCard className="h-64" key={caseStudy.title}>
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
<div className="flex w-75 flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<SparklesIcon className="text-primary size-8" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-2xl font-bold">{caseStudy.title}</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
{caseStudy.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SpotlightCard>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { AuroraText } from "@/components/ui/aurora-text";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
import { Section } from "../section";
|
||||||
|
|
||||||
|
export function CommunitySection() {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title={
|
||||||
|
<AuroraText colors={["#60A5FA", "#A5FA60", "#A560FA"]}>
|
||||||
|
Join the Community
|
||||||
|
</AuroraText>
|
||||||
|
}
|
||||||
|
subtitle="Contribute brilliant ideas to shape the future of DeerFlow. Collaborate, innovate, and make impacts."
|
||||||
|
>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Button className="text-xl" size="lg" asChild>
|
||||||
|
<Link href="https://github.com/bytedance/deer-flow" target="_blank">
|
||||||
|
<GitHubLogoIcon />
|
||||||
|
Contribute Now
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
frontend/src/components/landing/sections/sandbox-section.tsx
Normal file
125
frontend/src/components/landing/sections/sandbox-section.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AnimatedSpan,
|
||||||
|
Terminal,
|
||||||
|
TypingAnimation,
|
||||||
|
} from "@/components/ui/terminal";
|
||||||
|
|
||||||
|
import { Section } from "../section";
|
||||||
|
|
||||||
|
export function SandboxSection({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
className={className}
|
||||||
|
title="Sandbox"
|
||||||
|
subtitle={
|
||||||
|
<p>
|
||||||
|
We gave DeerFlow a computer. It can execute code, manage files, and
|
||||||
|
run long tasks — all in a secure Docker sandbox
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mt-8 flex w-full max-w-6xl flex-col items-center gap-12 lg:flex-row lg:gap-16">
|
||||||
|
{/* Left: Terminal */}
|
||||||
|
<div className="w-full flex-1">
|
||||||
|
<Terminal className="h-[360px] w-full">
|
||||||
|
{/* Scene 1: Build a Game */}
|
||||||
|
<TypingAnimation>$ cat requirements.txt</TypingAnimation>
|
||||||
|
<AnimatedSpan delay={800} className="text-zinc-400">
|
||||||
|
pygame==2.5.0
|
||||||
|
</AnimatedSpan>
|
||||||
|
|
||||||
|
<TypingAnimation delay={1200}>
|
||||||
|
$ pip install -r requirements.txt
|
||||||
|
</TypingAnimation>
|
||||||
|
<AnimatedSpan delay={2000} className="text-green-500">
|
||||||
|
✔ Installed pygame
|
||||||
|
</AnimatedSpan>
|
||||||
|
|
||||||
|
<TypingAnimation delay={2400}>
|
||||||
|
$ write game.py --lines 156
|
||||||
|
</TypingAnimation>
|
||||||
|
<AnimatedSpan delay={3200} className="text-blue-500">
|
||||||
|
✔ Written 156 lines
|
||||||
|
</AnimatedSpan>
|
||||||
|
|
||||||
|
<TypingAnimation delay={3600}>
|
||||||
|
$ python game.py --test
|
||||||
|
</TypingAnimation>
|
||||||
|
<AnimatedSpan delay={4200} className="text-green-500">
|
||||||
|
✔ All sprites loaded
|
||||||
|
</AnimatedSpan>
|
||||||
|
<AnimatedSpan delay={4500} className="text-green-500">
|
||||||
|
✔ Physics engine OK
|
||||||
|
</AnimatedSpan>
|
||||||
|
<AnimatedSpan delay={4800} className="text-green-500">
|
||||||
|
✔ 60 FPS stable
|
||||||
|
</AnimatedSpan>
|
||||||
|
|
||||||
|
{/* Scene 2: Data Analysis */}
|
||||||
|
<TypingAnimation delay={5400}>
|
||||||
|
$ curl -O sales-2024.csv
|
||||||
|
</TypingAnimation>
|
||||||
|
<AnimatedSpan delay={6200} className="text-zinc-400">
|
||||||
|
Downloaded 12.4 MB
|
||||||
|
</AnimatedSpan>
|
||||||
|
</Terminal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Description */}
|
||||||
|
<div className="w-full flex-1 space-y-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm font-medium tracking-wider text-purple-400 uppercase">
|
||||||
|
Open-source
|
||||||
|
</p>
|
||||||
|
<h2 className="text-4xl font-bold tracking-tight lg:text-5xl">
|
||||||
|
<a
|
||||||
|
href="https://github.com/agent-infra/sandbox"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
AIO Sandbox
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-lg text-zinc-400">
|
||||||
|
<p>
|
||||||
|
We recommend using{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/agent-infra/sandbox"
|
||||||
|
className="underline"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
All-in-One Sandbox
|
||||||
|
</a>{" "}
|
||||||
|
that combines Browser, Shell, File, MCP and VSCode Server in a
|
||||||
|
single Docker container.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature Tags */}
|
||||||
|
<div className="flex flex-wrap gap-3 pt-4">
|
||||||
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
|
Isolated
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
|
Safe
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
|
Persistent
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
|
Mountable FS
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-zinc-800 bg-zinc-900 px-4 py-2 text-sm text-zinc-300">
|
||||||
|
Long-running
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
frontend/src/components/landing/sections/skills-section.tsx
Normal file
28
frontend/src/components/landing/sections/skills-section.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import ProgressiveSkillsAnimation from "../components/progressive-skills-animation";
|
||||||
|
import { Section } from "../section";
|
||||||
|
|
||||||
|
export function SkillsSection({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
className={cn("h-[calc(100vh-64px)] w-full bg-white/7", className)}
|
||||||
|
title="Skill-based Architecture"
|
||||||
|
subtitle={
|
||||||
|
<div>
|
||||||
|
Skills are loaded progressively — only what's needed, when
|
||||||
|
it's needed.
|
||||||
|
<br />
|
||||||
|
Extend DeerFlow with your own skill files, or use our built-in
|
||||||
|
library.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<ProgressiveSkillsAnimation />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import MagicBento from "@/components/ui/magic-bento";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
import { Section } from "../section";
|
||||||
|
|
||||||
|
export function WhatsNewSection({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
className={cn("", className)}
|
||||||
|
title="Whats New in DeerFlow 2.0"
|
||||||
|
subtitle="DeerFlow is now evolving from a Deep Research agent into a full-stack Super Agent"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-center">
|
||||||
|
<MagicBento />
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,18 +1,24 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface FlickeringGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
squareSize?: number
|
squareSize?: number;
|
||||||
gridGap?: number
|
gridGap?: number;
|
||||||
flickerChance?: number
|
flickerChance?: number;
|
||||||
color?: string
|
color?: string;
|
||||||
width?: number
|
width?: number;
|
||||||
height?: number
|
height?: number;
|
||||||
className?: string
|
className?: string;
|
||||||
maxOpacity?: number
|
maxOpacity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||||
@ -26,58 +32,58 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
|||||||
maxOpacity = 0.3,
|
maxOpacity = 0.3,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isInView, setIsInView] = useState(false)
|
const [isInView, setIsInView] = useState(false);
|
||||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
|
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
const memoizedColor = useMemo(() => {
|
const memoizedColor = useMemo(() => {
|
||||||
const toRGBA = (color: string) => {
|
const toRGBA = (color: string) => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return `rgba(0, 0, 0,`
|
return `rgba(0, 0, 0,`;
|
||||||
}
|
}
|
||||||
const canvas = document.createElement("canvas")
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = canvas.height = 1
|
canvas.width = canvas.height = 1;
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return "rgba(255, 0, 0,"
|
if (!ctx) return "rgba(255, 0, 0,";
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(0, 0, 1, 1)
|
ctx.fillRect(0, 0, 1, 1);
|
||||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
|
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||||
return `rgba(${r}, ${g}, ${b},`
|
return `rgba(${r}, ${g}, ${b},`;
|
||||||
}
|
};
|
||||||
return toRGBA(color)
|
return toRGBA(color);
|
||||||
}, [color])
|
}, [color]);
|
||||||
|
|
||||||
const setupCanvas = useCallback(
|
const setupCanvas = useCallback(
|
||||||
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||||
const dpr = window.devicePixelRatio || 1
|
const dpr = window.devicePixelRatio || 1;
|
||||||
canvas.width = width * dpr
|
canvas.width = width * dpr;
|
||||||
canvas.height = height * dpr
|
canvas.height = height * dpr;
|
||||||
canvas.style.width = `${width}px`
|
canvas.style.width = `${width}px`;
|
||||||
canvas.style.height = `${height}px`
|
canvas.style.height = `${height}px`;
|
||||||
const cols = Math.floor(width / (squareSize + gridGap))
|
const cols = Math.floor(width / (squareSize + gridGap));
|
||||||
const rows = Math.floor(height / (squareSize + gridGap))
|
const rows = Math.floor(height / (squareSize + gridGap));
|
||||||
|
|
||||||
const squares = new Float32Array(cols * rows)
|
const squares = new Float32Array(cols * rows);
|
||||||
for (let i = 0; i < squares.length; i++) {
|
for (let i = 0; i < squares.length; i++) {
|
||||||
squares[i] = Math.random() * maxOpacity
|
squares[i] = Math.random() * maxOpacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cols, rows, squares, dpr }
|
return { cols, rows, squares, dpr };
|
||||||
},
|
},
|
||||||
[squareSize, gridGap, maxOpacity]
|
[squareSize, gridGap, maxOpacity],
|
||||||
)
|
);
|
||||||
|
|
||||||
const updateSquares = useCallback(
|
const updateSquares = useCallback(
|
||||||
(squares: Float32Array, deltaTime: number) => {
|
(squares: Float32Array, deltaTime: number) => {
|
||||||
for (let i = 0; i < squares.length; i++) {
|
for (let i = 0; i < squares.length; i++) {
|
||||||
if (Math.random() < flickerChance * deltaTime) {
|
if (Math.random() < flickerChance * deltaTime) {
|
||||||
squares[i] = Math.random() * maxOpacity
|
squares[i] = Math.random() * maxOpacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[flickerChance, maxOpacity]
|
[flickerChance, maxOpacity],
|
||||||
)
|
);
|
||||||
|
|
||||||
const drawGrid = useCallback(
|
const drawGrid = useCallback(
|
||||||
(
|
(
|
||||||
@ -87,56 +93,56 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
|||||||
cols: number,
|
cols: number,
|
||||||
rows: number,
|
rows: number,
|
||||||
squares: Float32Array,
|
squares: Float32Array,
|
||||||
dpr: number
|
dpr: number,
|
||||||
) => {
|
) => {
|
||||||
ctx.clearRect(0, 0, width, height)
|
ctx.clearRect(0, 0, width, height);
|
||||||
ctx.fillStyle = "transparent"
|
ctx.fillStyle = "transparent";
|
||||||
ctx.fillRect(0, 0, width, height)
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
for (let j = 0; j < rows; j++) {
|
for (let j = 0; j < rows; j++) {
|
||||||
const opacity = squares[i * rows + j]
|
const opacity = squares[i * rows + j];
|
||||||
ctx.fillStyle = `${memoizedColor}${opacity})`
|
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
||||||
ctx.fillRect(
|
ctx.fillRect(
|
||||||
i * (squareSize + gridGap) * dpr,
|
i * (squareSize + gridGap) * dpr,
|
||||||
j * (squareSize + gridGap) * dpr,
|
j * (squareSize + gridGap) * dpr,
|
||||||
squareSize * dpr,
|
squareSize * dpr,
|
||||||
squareSize * dpr
|
squareSize * dpr,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[memoizedColor, squareSize, gridGap]
|
[memoizedColor, squareSize, gridGap],
|
||||||
)
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current;
|
||||||
const container = containerRef.current
|
const container = containerRef.current;
|
||||||
if (!canvas || !container) return
|
if (!canvas || !container) return;
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d")
|
const ctx = canvas.getContext("2d");
|
||||||
if (!ctx) return
|
if (!ctx) return;
|
||||||
|
|
||||||
let animationFrameId: number
|
let animationFrameId: number;
|
||||||
let gridParams: ReturnType<typeof setupCanvas>
|
let gridParams: ReturnType<typeof setupCanvas>;
|
||||||
|
|
||||||
const updateCanvasSize = () => {
|
const updateCanvasSize = () => {
|
||||||
const newWidth = width || container.clientWidth
|
const newWidth = width || container.clientWidth;
|
||||||
const newHeight = height || container.clientHeight
|
const newHeight = height || container.clientHeight;
|
||||||
setCanvasSize({ width: newWidth, height: newHeight })
|
setCanvasSize({ width: newWidth, height: newHeight });
|
||||||
gridParams = setupCanvas(canvas, newWidth, newHeight)
|
gridParams = setupCanvas(canvas, newWidth, newHeight);
|
||||||
}
|
};
|
||||||
|
|
||||||
updateCanvasSize()
|
updateCanvasSize();
|
||||||
|
|
||||||
let lastTime = 0
|
let lastTime = 0;
|
||||||
const animate = (time: number) => {
|
const animate = (time: number) => {
|
||||||
if (!isInView) return
|
if (!isInView) return;
|
||||||
|
|
||||||
const deltaTime = (time - lastTime) / 1000
|
const deltaTime = (time - lastTime) / 1000;
|
||||||
lastTime = time
|
lastTime = time;
|
||||||
|
|
||||||
updateSquares(gridParams.squares, deltaTime)
|
updateSquares(gridParams.squares, deltaTime);
|
||||||
drawGrid(
|
drawGrid(
|
||||||
ctx,
|
ctx,
|
||||||
canvas.width,
|
canvas.width,
|
||||||
@ -144,36 +150,38 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
|||||||
gridParams.cols,
|
gridParams.cols,
|
||||||
gridParams.rows,
|
gridParams.rows,
|
||||||
gridParams.squares,
|
gridParams.squares,
|
||||||
gridParams.dpr
|
gridParams.dpr,
|
||||||
)
|
);
|
||||||
animationFrameId = requestAnimationFrame(animate)
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
}
|
};
|
||||||
|
|
||||||
const resizeObserver = new ResizeObserver(() => {
|
const resizeObserver = new ResizeObserver(() => {
|
||||||
updateCanvasSize()
|
updateCanvasSize();
|
||||||
})
|
});
|
||||||
|
|
||||||
resizeObserver.observe(container)
|
resizeObserver.observe(container);
|
||||||
|
|
||||||
const intersectionObserver = new IntersectionObserver(
|
const intersectionObserver = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
setIsInView(entry.isIntersecting)
|
if (entry) {
|
||||||
|
setIsInView(entry.isIntersecting);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ threshold: 0 }
|
{ threshold: 0 },
|
||||||
)
|
);
|
||||||
|
|
||||||
intersectionObserver.observe(canvas)
|
intersectionObserver.observe(canvas);
|
||||||
|
|
||||||
if (isInView) {
|
if (isInView) {
|
||||||
animationFrameId = requestAnimationFrame(animate)
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animationFrameId)
|
cancelAnimationFrame(animationFrameId);
|
||||||
resizeObserver.disconnect()
|
resizeObserver.disconnect();
|
||||||
intersectionObserver.disconnect()
|
intersectionObserver.disconnect();
|
||||||
}
|
};
|
||||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
|
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -190,5 +198,5 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
|
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import './Galaxy.css';
|
import "./galaxy.css";
|
||||||
|
|
||||||
const vertexShader = `
|
const vertexShader = `
|
||||||
attribute vec2 uv;
|
attribute vec2 uv;
|
||||||
@ -200,7 +200,7 @@ export default function Galaxy({
|
|||||||
const ctn = ctnDom.current;
|
const ctn = ctnDom.current;
|
||||||
const renderer = new Renderer({
|
const renderer = new Renderer({
|
||||||
alpha: transparent,
|
alpha: transparent,
|
||||||
premultipliedAlpha: false
|
premultipliedAlpha: false,
|
||||||
});
|
});
|
||||||
const gl = renderer.gl;
|
const gl = renderer.gl;
|
||||||
|
|
||||||
@ -212,6 +212,7 @@ export default function Galaxy({
|
|||||||
gl.clearColor(0, 0, 0, 1);
|
gl.clearColor(0, 0, 0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {Program | undefined} */
|
||||||
let program;
|
let program;
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
@ -221,11 +222,11 @@ export default function Galaxy({
|
|||||||
program.uniforms.uResolution.value = new Color(
|
program.uniforms.uResolution.value = new Color(
|
||||||
gl.canvas.width,
|
gl.canvas.width,
|
||||||
gl.canvas.height,
|
gl.canvas.height,
|
||||||
gl.canvas.width / gl.canvas.height
|
gl.canvas.width / gl.canvas.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', resize, false);
|
window.addEventListener("resize", resize, false);
|
||||||
resize();
|
resize();
|
||||||
|
|
||||||
const geometry = new Triangle(gl);
|
const geometry = new Triangle(gl);
|
||||||
@ -235,7 +236,11 @@ export default function Galaxy({
|
|||||||
uniforms: {
|
uniforms: {
|
||||||
uTime: { value: 0 },
|
uTime: { value: 0 },
|
||||||
uResolution: {
|
uResolution: {
|
||||||
value: new Color(gl.canvas.width, gl.canvas.height, gl.canvas.width / gl.canvas.height)
|
value: new Color(
|
||||||
|
gl.canvas.width,
|
||||||
|
gl.canvas.height,
|
||||||
|
gl.canvas.width / gl.canvas.height,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
uFocal: { value: new Float32Array(focal) },
|
uFocal: { value: new Float32Array(focal) },
|
||||||
uRotation: { value: new Float32Array(rotation) },
|
uRotation: { value: new Float32Array(rotation) },
|
||||||
@ -244,7 +249,10 @@ export default function Galaxy({
|
|||||||
uHueShift: { value: hueShift },
|
uHueShift: { value: hueShift },
|
||||||
uSpeed: { value: speed },
|
uSpeed: { value: speed },
|
||||||
uMouse: {
|
uMouse: {
|
||||||
value: new Float32Array([smoothMousePos.current.x, smoothMousePos.current.y])
|
value: new Float32Array([
|
||||||
|
smoothMousePos.current.x,
|
||||||
|
smoothMousePos.current.y,
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
uGlowIntensity: { value: glowIntensity },
|
uGlowIntensity: { value: glowIntensity },
|
||||||
uSaturation: { value: saturation },
|
uSaturation: { value: saturation },
|
||||||
@ -254,8 +262,8 @@ export default function Galaxy({
|
|||||||
uRepulsionStrength: { value: repulsionStrength },
|
uRepulsionStrength: { value: repulsionStrength },
|
||||||
uMouseActiveFactor: { value: 0.0 },
|
uMouseActiveFactor: { value: 0.0 },
|
||||||
uAutoCenterRepulsion: { value: autoCenterRepulsion },
|
uAutoCenterRepulsion: { value: autoCenterRepulsion },
|
||||||
uTransparent: { value: transparent }
|
uTransparent: { value: transparent },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mesh = new Mesh(gl, { geometry, program });
|
const mesh = new Mesh(gl, { geometry, program });
|
||||||
@ -269,10 +277,13 @@ export default function Galaxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lerpFactor = 0.05;
|
const lerpFactor = 0.05;
|
||||||
smoothMousePos.current.x += (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
|
smoothMousePos.current.x +=
|
||||||
smoothMousePos.current.y += (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
|
(targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
|
||||||
|
smoothMousePos.current.y +=
|
||||||
|
(targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
|
||||||
|
|
||||||
smoothMouseActive.current += (targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
|
smoothMouseActive.current +=
|
||||||
|
(targetMouseActive.current - smoothMouseActive.current) * lerpFactor;
|
||||||
|
|
||||||
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
|
program.uniforms.uMouse.value[0] = smoothMousePos.current.x;
|
||||||
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
|
program.uniforms.uMouse.value[1] = smoothMousePos.current.y;
|
||||||
@ -296,19 +307,19 @@ export default function Galaxy({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mouseInteraction) {
|
if (mouseInteraction) {
|
||||||
ctn.addEventListener('mousemove', handleMouseMove);
|
ctn.addEventListener("mousemove", handleMouseMove);
|
||||||
ctn.addEventListener('mouseleave', handleMouseLeave);
|
ctn.addEventListener("mouseleave", handleMouseLeave);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelAnimationFrame(animateId);
|
cancelAnimationFrame(animateId);
|
||||||
window.removeEventListener('resize', resize);
|
window.removeEventListener("resize", resize);
|
||||||
if (mouseInteraction) {
|
if (mouseInteraction) {
|
||||||
ctn.removeEventListener('mousemove', handleMouseMove);
|
ctn.removeEventListener("mousemove", handleMouseMove);
|
||||||
ctn.removeEventListener('mouseleave', handleMouseLeave);
|
ctn.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
}
|
}
|
||||||
ctn.removeChild(gl.canvas);
|
ctn.removeChild(gl.canvas);
|
||||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
focal,
|
focal,
|
||||||
@ -326,7 +337,7 @@ export default function Galaxy({
|
|||||||
rotationSpeed,
|
rotationSpeed,
|
||||||
repulsionStrength,
|
repulsionStrength,
|
||||||
autoCenterRepulsion,
|
autoCenterRepulsion,
|
||||||
transparent
|
transparent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
|
return <div ref={ctnDom} className="galaxy-container" {...rest} />;
|
||||||
217
frontend/src/components/ui/magic-bento.css
Normal file
217
frontend/src/components/ui/magic-bento.css
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
:root {
|
||||||
|
--hue: 27;
|
||||||
|
--sat: 69%;
|
||||||
|
--white: hsl(0, 0%, 100%);
|
||||||
|
--purple-primary: rgba(132, 0, 255, 1);
|
||||||
|
--purple-glow: rgba(132, 0, 255, 0.2);
|
||||||
|
--purple-border: rgba(132, 0, 255, 0.8);
|
||||||
|
--border-color: #392e4e;
|
||||||
|
--background-dark: #060010;
|
||||||
|
color-scheme: light dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.75em;
|
||||||
|
max-width: 54em;
|
||||||
|
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
min-height: 200px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 1.25em;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--background-dark);
|
||||||
|
font-weight: 300;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
--glow-x: 50%;
|
||||||
|
--glow-y: 50%;
|
||||||
|
--glow-intensity: 0;
|
||||||
|
--glow-radius: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__header,
|
||||||
|
.magic-bento-card__content {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__header {
|
||||||
|
gap: 0.75em;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__content {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__label {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__title,
|
||||||
|
.magic-bento-card__description {
|
||||||
|
--clamp-title: 1;
|
||||||
|
--clamp-desc: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__title {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card__description {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card--text-autohide .magic-bento-card__title,
|
||||||
|
.magic-bento-card--text-autohide .magic-bento-card__description {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card--text-autohide .magic-bento-card__title {
|
||||||
|
-webkit-line-clamp: var(--clamp-title);
|
||||||
|
line-clamp: var(--clamp-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card--text-autohide .magic-bento-card__description {
|
||||||
|
-webkit-line-clamp: var(--clamp-desc);
|
||||||
|
line-clamp: var(--clamp-desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 599px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.card-grid {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card:nth-child(3) {
|
||||||
|
grid-column: span 2;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card:nth-child(4) {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
grid-row: 2 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card:nth-child(6) {
|
||||||
|
grid-column: 4;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Border glow effect */
|
||||||
|
.magic-bento-card--border-glow::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
padding: 6px;
|
||||||
|
background: radial-gradient(
|
||||||
|
var(--glow-radius) circle at var(--glow-x) var(--glow-y),
|
||||||
|
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.8)) 0%,
|
||||||
|
rgba(132, 0, 255, calc(var(--glow-intensity) * 0.4)) 30%,
|
||||||
|
transparent 60%
|
||||||
|
);
|
||||||
|
border-radius: inherit;
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card--border-glow:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.magic-bento-card--border-glow:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 4px 20px rgba(46, 24, 78, 0.4),
|
||||||
|
0 0 30px var(--purple-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
background: rgba(132, 0, 255, 0.2);
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-container:hover {
|
||||||
|
box-shadow:
|
||||||
|
0 4px 20px rgba(46, 24, 78, 0.2),
|
||||||
|
0 0 30px var(--purple-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Global spotlight styles */
|
||||||
|
.global-spotlight {
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
z-index: 200 !important;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-section {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
757
frontend/src/components/ui/magic-bento.tsx
Normal file
757
frontend/src/components/ui/magic-bento.tsx
Normal file
@ -0,0 +1,757 @@
|
|||||||
|
import { gsap } from "gsap";
|
||||||
|
import React, { useRef, useEffect, useCallback, useState } from "react";
|
||||||
|
import "./magic-bento.css";
|
||||||
|
|
||||||
|
export interface BentoCardProps {
|
||||||
|
color?: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
label?: React.ReactNode;
|
||||||
|
textAutoHide?: boolean;
|
||||||
|
disableAnimations?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BentoProps {
|
||||||
|
textAutoHide?: boolean;
|
||||||
|
enableStars?: boolean;
|
||||||
|
enableSpotlight?: boolean;
|
||||||
|
enableBorderGlow?: boolean;
|
||||||
|
disableAnimations?: boolean;
|
||||||
|
spotlightRadius?: number;
|
||||||
|
particleCount?: number;
|
||||||
|
enableTilt?: boolean;
|
||||||
|
glowColor?: string;
|
||||||
|
clickEffect?: boolean;
|
||||||
|
enableMagnetism?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_PARTICLE_COUNT = 12;
|
||||||
|
const DEFAULT_SPOTLIGHT_RADIUS = 300;
|
||||||
|
const DEFAULT_GLOW_COLOR = "132, 0, 255";
|
||||||
|
const MOBILE_BREAKPOINT = 768;
|
||||||
|
|
||||||
|
const cardData: BentoCardProps[] = [
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Long/Short-term Memory",
|
||||||
|
description: (
|
||||||
|
<div>
|
||||||
|
<div>Now the agent can better understand you</div>
|
||||||
|
<div className="text-muted-foreground">Coming soon</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
label: "Context Engineering",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Planning and Reasoning",
|
||||||
|
description: "Plans ahead, reasons through complexity, then acts",
|
||||||
|
label: "Long Task Running",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Skills and Tools",
|
||||||
|
description:
|
||||||
|
"Plug, play, or even swap built-in tools. Build the agent you want.",
|
||||||
|
label: "Extensible",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Sandbox with File System",
|
||||||
|
description: "Read, write, run — like a real computer",
|
||||||
|
label: "Persistent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Multi-Model Support",
|
||||||
|
description: "Doubao, DeepSeek, OpenAI, Gemini, etc.",
|
||||||
|
label: "Flexible",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: "#0a0015",
|
||||||
|
title: "Open Source",
|
||||||
|
description: "MIT License, self-hosted, full control",
|
||||||
|
label: "Free",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const createParticleElement = (
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
color: string = DEFAULT_GLOW_COLOR,
|
||||||
|
): HTMLDivElement => {
|
||||||
|
const el = document.createElement("div");
|
||||||
|
el.className = "particle";
|
||||||
|
el.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(${color}, 1);
|
||||||
|
box-shadow: 0 0 6px rgba(${color}, 0.6);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
left: ${x}px;
|
||||||
|
top: ${y}px;
|
||||||
|
`;
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSpotlightValues = (radius: number) => ({
|
||||||
|
proximity: radius * 0.5,
|
||||||
|
fadeDistance: radius * 0.75,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateCardGlowProperties = (
|
||||||
|
card: HTMLElement,
|
||||||
|
mouseX: number,
|
||||||
|
mouseY: number,
|
||||||
|
glow: number,
|
||||||
|
radius: number,
|
||||||
|
) => {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const relativeX = ((mouseX - rect.left) / rect.width) * 100;
|
||||||
|
const relativeY = ((mouseY - rect.top) / rect.height) * 100;
|
||||||
|
|
||||||
|
card.style.setProperty("--glow-x", `${relativeX}%`);
|
||||||
|
card.style.setProperty("--glow-y", `${relativeY}%`);
|
||||||
|
card.style.setProperty("--glow-intensity", glow.toString());
|
||||||
|
card.style.setProperty("--glow-radius", `${radius}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ParticleCard: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
disableAnimations?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
particleCount?: number;
|
||||||
|
glowColor?: string;
|
||||||
|
enableTilt?: boolean;
|
||||||
|
clickEffect?: boolean;
|
||||||
|
enableMagnetism?: boolean;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
disableAnimations = false,
|
||||||
|
style,
|
||||||
|
particleCount = DEFAULT_PARTICLE_COUNT,
|
||||||
|
glowColor = DEFAULT_GLOW_COLOR,
|
||||||
|
enableTilt = true,
|
||||||
|
clickEffect = false,
|
||||||
|
enableMagnetism = false,
|
||||||
|
}) => {
|
||||||
|
const cardRef = useRef<HTMLDivElement>(null);
|
||||||
|
const particlesRef = useRef<HTMLDivElement[]>([]);
|
||||||
|
const timeoutsRef = useRef<number[]>([]);
|
||||||
|
const isHoveredRef = useRef(false);
|
||||||
|
const memoizedParticles = useRef<HTMLDivElement[]>([]);
|
||||||
|
const particlesInitialized = useRef(false);
|
||||||
|
const magnetismAnimationRef = useRef<gsap.core.Tween | null>(null);
|
||||||
|
|
||||||
|
const initializeParticles = useCallback(() => {
|
||||||
|
if (particlesInitialized.current || !cardRef.current) return;
|
||||||
|
|
||||||
|
const { width, height } = cardRef.current.getBoundingClientRect();
|
||||||
|
memoizedParticles.current = Array.from({ length: particleCount }, () =>
|
||||||
|
createParticleElement(
|
||||||
|
Math.random() * width,
|
||||||
|
Math.random() * height,
|
||||||
|
glowColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
particlesInitialized.current = true;
|
||||||
|
}, [particleCount, glowColor]);
|
||||||
|
|
||||||
|
const clearAllParticles = useCallback(() => {
|
||||||
|
timeoutsRef.current.forEach(clearTimeout);
|
||||||
|
timeoutsRef.current = [];
|
||||||
|
magnetismAnimationRef.current?.kill();
|
||||||
|
|
||||||
|
particlesRef.current.forEach((particle) => {
|
||||||
|
gsap.to(particle, {
|
||||||
|
scale: 0,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "back.in(1.7)",
|
||||||
|
onComplete: () => {
|
||||||
|
particle.parentNode?.removeChild(particle);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
particlesRef.current = [];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const animateParticles = useCallback(() => {
|
||||||
|
if (!cardRef.current || !isHoveredRef.current) return;
|
||||||
|
|
||||||
|
if (!particlesInitialized.current) {
|
||||||
|
initializeParticles();
|
||||||
|
}
|
||||||
|
|
||||||
|
memoizedParticles.current.forEach((particle, index) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (!isHoveredRef.current || !cardRef.current) return;
|
||||||
|
|
||||||
|
const clone = particle.cloneNode(true) as HTMLDivElement;
|
||||||
|
cardRef.current.appendChild(clone);
|
||||||
|
particlesRef.current.push(clone);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
clone,
|
||||||
|
{ scale: 0, opacity: 0 },
|
||||||
|
{ scale: 1, opacity: 1, duration: 0.3, ease: "back.out(1.7)" },
|
||||||
|
);
|
||||||
|
|
||||||
|
gsap.to(clone, {
|
||||||
|
x: (Math.random() - 0.5) * 100,
|
||||||
|
y: (Math.random() - 0.5) * 100,
|
||||||
|
rotation: Math.random() * 360,
|
||||||
|
duration: 2 + Math.random() * 2,
|
||||||
|
ease: "none",
|
||||||
|
repeat: -1,
|
||||||
|
yoyo: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.to(clone, {
|
||||||
|
opacity: 0.3,
|
||||||
|
duration: 1.5,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
repeat: -1,
|
||||||
|
yoyo: true,
|
||||||
|
});
|
||||||
|
}, index * 100);
|
||||||
|
|
||||||
|
timeoutsRef.current.push(timeoutId as unknown as number);
|
||||||
|
});
|
||||||
|
}, [initializeParticles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableAnimations || !cardRef.current) return;
|
||||||
|
|
||||||
|
const element = cardRef.current;
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
isHoveredRef.current = true;
|
||||||
|
animateParticles();
|
||||||
|
|
||||||
|
if (enableTilt) {
|
||||||
|
gsap.to(element, {
|
||||||
|
rotateX: 5,
|
||||||
|
rotateY: 5,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
transformPerspective: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isHoveredRef.current = false;
|
||||||
|
clearAllParticles();
|
||||||
|
|
||||||
|
if (enableTilt) {
|
||||||
|
gsap.to(element, {
|
||||||
|
rotateX: 0,
|
||||||
|
rotateY: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableMagnetism) {
|
||||||
|
gsap.to(element, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!enableTilt && !enableMagnetism) return;
|
||||||
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
|
if (enableTilt) {
|
||||||
|
const rotateX = ((y - centerY) / centerY) * -10;
|
||||||
|
const rotateY = ((x - centerX) / centerX) * 10;
|
||||||
|
|
||||||
|
gsap.to(element, {
|
||||||
|
rotateX,
|
||||||
|
rotateY,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
transformPerspective: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableMagnetism) {
|
||||||
|
const magnetX = (x - centerX) * 0.05;
|
||||||
|
const magnetY = (y - centerY) * 0.05;
|
||||||
|
|
||||||
|
magnetismAnimationRef.current = gsap.to(element, {
|
||||||
|
x: magnetX,
|
||||||
|
y: magnetY,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (!clickEffect) return;
|
||||||
|
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const maxDistance = Math.max(
|
||||||
|
Math.hypot(x, y),
|
||||||
|
Math.hypot(x - rect.width, y),
|
||||||
|
Math.hypot(x, y - rect.height),
|
||||||
|
Math.hypot(x - rect.width, y - rect.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ripple = document.createElement("div");
|
||||||
|
ripple.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: ${maxDistance * 2}px;
|
||||||
|
height: ${maxDistance * 2}px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);
|
||||||
|
left: ${x - maxDistance}px;
|
||||||
|
top: ${y - maxDistance}px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
element.appendChild(ripple);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
ripple,
|
||||||
|
{
|
||||||
|
scale: 0,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "power2.out",
|
||||||
|
onComplete: () => ripple.remove(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
element.addEventListener("mouseenter", handleMouseEnter);
|
||||||
|
element.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
element.addEventListener("mousemove", handleMouseMove);
|
||||||
|
element.addEventListener("click", handleClick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isHoveredRef.current = false;
|
||||||
|
element.removeEventListener("mouseenter", handleMouseEnter);
|
||||||
|
element.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
element.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
element.removeEventListener("click", handleClick);
|
||||||
|
clearAllParticles();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
animateParticles,
|
||||||
|
clearAllParticles,
|
||||||
|
disableAnimations,
|
||||||
|
enableTilt,
|
||||||
|
enableMagnetism,
|
||||||
|
clickEffect,
|
||||||
|
glowColor,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={cardRef}
|
||||||
|
className={`${className} particle-container`}
|
||||||
|
style={{ ...style, position: "relative", overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalSpotlight: React.FC<{
|
||||||
|
gridRef: React.RefObject<HTMLDivElement | null>;
|
||||||
|
disableAnimations?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
spotlightRadius?: number;
|
||||||
|
glowColor?: string;
|
||||||
|
}> = ({
|
||||||
|
gridRef,
|
||||||
|
disableAnimations = false,
|
||||||
|
enabled = true,
|
||||||
|
spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,
|
||||||
|
glowColor = DEFAULT_GLOW_COLOR,
|
||||||
|
}) => {
|
||||||
|
const spotlightRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const isInsideSection = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disableAnimations || !gridRef?.current || !enabled) return;
|
||||||
|
|
||||||
|
const spotlight = document.createElement("div");
|
||||||
|
spotlight.className = "global-spotlight";
|
||||||
|
spotlight.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
width: 800px;
|
||||||
|
height: 800px;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(circle,
|
||||||
|
rgba(${glowColor}, 0.15) 0%,
|
||||||
|
rgba(${glowColor}, 0.08) 15%,
|
||||||
|
rgba(${glowColor}, 0.04) 25%,
|
||||||
|
rgba(${glowColor}, 0.02) 40%,
|
||||||
|
rgba(${glowColor}, 0.01) 65%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
z-index: 200;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
`;
|
||||||
|
document.body.appendChild(spotlight);
|
||||||
|
spotlightRef.current = spotlight;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!spotlightRef.current || !gridRef.current) return;
|
||||||
|
|
||||||
|
const section = gridRef.current.closest(".bento-section");
|
||||||
|
const rect = section?.getBoundingClientRect();
|
||||||
|
const mouseInside =
|
||||||
|
rect &&
|
||||||
|
e.clientX >= rect.left &&
|
||||||
|
e.clientX <= rect.right &&
|
||||||
|
e.clientY >= rect.top &&
|
||||||
|
e.clientY <= rect.bottom;
|
||||||
|
|
||||||
|
isInsideSection.current = mouseInside ?? false;
|
||||||
|
const cards = gridRef.current.querySelectorAll(".magic-bento-card");
|
||||||
|
|
||||||
|
if (!mouseInside) {
|
||||||
|
gsap.to(spotlightRef.current, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
cards.forEach((card) => {
|
||||||
|
(card as HTMLElement).style.setProperty("--glow-intensity", "0");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { proximity, fadeDistance } =
|
||||||
|
calculateSpotlightValues(spotlightRadius);
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const cardElement = card as HTMLElement;
|
||||||
|
const cardRect = cardElement.getBoundingClientRect();
|
||||||
|
const centerX = cardRect.left + cardRect.width / 2;
|
||||||
|
const centerY = cardRect.top + cardRect.height / 2;
|
||||||
|
const distance =
|
||||||
|
Math.hypot(e.clientX - centerX, e.clientY - centerY) -
|
||||||
|
Math.max(cardRect.width, cardRect.height) / 2;
|
||||||
|
const effectiveDistance = Math.max(0, distance);
|
||||||
|
|
||||||
|
minDistance = Math.min(minDistance, effectiveDistance);
|
||||||
|
|
||||||
|
let glowIntensity = 0;
|
||||||
|
if (effectiveDistance <= proximity) {
|
||||||
|
glowIntensity = 1;
|
||||||
|
} else if (effectiveDistance <= fadeDistance) {
|
||||||
|
glowIntensity =
|
||||||
|
(fadeDistance - effectiveDistance) / (fadeDistance - proximity);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCardGlowProperties(
|
||||||
|
cardElement,
|
||||||
|
e.clientX,
|
||||||
|
e.clientY,
|
||||||
|
glowIntensity,
|
||||||
|
spotlightRadius,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
gsap.to(spotlightRef.current, {
|
||||||
|
left: e.clientX,
|
||||||
|
top: e.clientY,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
|
||||||
|
const targetOpacity =
|
||||||
|
minDistance <= proximity
|
||||||
|
? 0.8
|
||||||
|
: minDistance <= fadeDistance
|
||||||
|
? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
gsap.to(spotlightRef.current, {
|
||||||
|
opacity: targetOpacity,
|
||||||
|
duration: targetOpacity > 0 ? 0.2 : 0.5,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
isInsideSection.current = false;
|
||||||
|
gridRef.current?.querySelectorAll(".magic-bento-card").forEach((card) => {
|
||||||
|
(card as HTMLElement).style.setProperty("--glow-intensity", "0");
|
||||||
|
});
|
||||||
|
if (spotlightRef.current) {
|
||||||
|
gsap.to(spotlightRef.current, {
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
||||||
|
spotlightRef.current?.parentNode?.removeChild(spotlightRef.current);
|
||||||
|
};
|
||||||
|
}, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BentoCardGrid: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
gridRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
|
}> = ({ children, gridRef }) => (
|
||||||
|
<div className="card-grid bento-section" ref={gridRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const useMobileDetection = () => {
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () =>
|
||||||
|
setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT);
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener("resize", checkMobile);
|
||||||
|
|
||||||
|
return () => window.removeEventListener("resize", checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMobile;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MagicBento: React.FC<BentoProps> = ({
|
||||||
|
textAutoHide = true,
|
||||||
|
enableStars = true,
|
||||||
|
enableSpotlight = true,
|
||||||
|
enableBorderGlow = true,
|
||||||
|
disableAnimations = false,
|
||||||
|
spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS,
|
||||||
|
particleCount = DEFAULT_PARTICLE_COUNT,
|
||||||
|
enableTilt = false,
|
||||||
|
glowColor = DEFAULT_GLOW_COLOR,
|
||||||
|
clickEffect = true,
|
||||||
|
enableMagnetism = true,
|
||||||
|
}) => {
|
||||||
|
const gridRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isMobile = useMobileDetection();
|
||||||
|
const shouldDisableAnimations = disableAnimations || isMobile;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{enableSpotlight && (
|
||||||
|
<GlobalSpotlight
|
||||||
|
gridRef={gridRef}
|
||||||
|
disableAnimations={shouldDisableAnimations}
|
||||||
|
enabled={enableSpotlight}
|
||||||
|
spotlightRadius={spotlightRadius}
|
||||||
|
glowColor={glowColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<BentoCardGrid gridRef={gridRef}>
|
||||||
|
{cardData.map((card, index) => {
|
||||||
|
const baseClassName = `magic-bento-card ${textAutoHide ? "magic-bento-card--text-autohide" : ""} ${enableBorderGlow ? "magic-bento-card--border-glow" : ""}`;
|
||||||
|
const cardProps = {
|
||||||
|
className: baseClassName,
|
||||||
|
style: {
|
||||||
|
backgroundColor: card.color,
|
||||||
|
"--glow-color": glowColor,
|
||||||
|
} as React.CSSProperties,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enableStars) {
|
||||||
|
return (
|
||||||
|
<ParticleCard
|
||||||
|
key={index}
|
||||||
|
{...cardProps}
|
||||||
|
disableAnimations={shouldDisableAnimations}
|
||||||
|
particleCount={particleCount}
|
||||||
|
glowColor={glowColor}
|
||||||
|
enableTilt={enableTilt}
|
||||||
|
clickEffect={clickEffect}
|
||||||
|
enableMagnetism={enableMagnetism}
|
||||||
|
>
|
||||||
|
<div className="magic-bento-card__header">
|
||||||
|
<div className="magic-bento-card__label">{card.label}</div>
|
||||||
|
</div>
|
||||||
|
<div className="magic-bento-card__content">
|
||||||
|
<h2 className="magic-bento-card__title">{card.title}</h2>
|
||||||
|
<div className="magic-bento-card__description">
|
||||||
|
{card.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ParticleCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
{...cardProps}
|
||||||
|
ref={(el) => {
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (shouldDisableAnimations) return;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
|
if (enableTilt) {
|
||||||
|
const rotateX = ((y - centerY) / centerY) * -10;
|
||||||
|
const rotateY = ((x - centerX) / centerX) * 10;
|
||||||
|
gsap.to(el, {
|
||||||
|
rotateX,
|
||||||
|
rotateY,
|
||||||
|
duration: 0.1,
|
||||||
|
ease: "power2.out",
|
||||||
|
transformPerspective: 1000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableMagnetism) {
|
||||||
|
const magnetX = (x - centerX) * 0.05;
|
||||||
|
const magnetY = (y - centerY) * 0.05;
|
||||||
|
gsap.to(el, {
|
||||||
|
x: magnetX,
|
||||||
|
y: magnetY,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = () => {
|
||||||
|
if (shouldDisableAnimations) return;
|
||||||
|
|
||||||
|
if (enableTilt) {
|
||||||
|
gsap.to(el, {
|
||||||
|
rotateX: 0,
|
||||||
|
rotateY: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enableMagnetism) {
|
||||||
|
gsap.to(el, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "power2.out",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (!clickEffect || shouldDisableAnimations) return;
|
||||||
|
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Calculate the maximum distance from click point to any corner
|
||||||
|
const maxDistance = Math.max(
|
||||||
|
Math.hypot(x, y),
|
||||||
|
Math.hypot(x - rect.width, y),
|
||||||
|
Math.hypot(x, y - rect.height),
|
||||||
|
Math.hypot(x - rect.width, y - rect.height),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ripple = document.createElement("div");
|
||||||
|
ripple.style.cssText = `
|
||||||
|
position: absolute;
|
||||||
|
width: ${maxDistance * 2}px;
|
||||||
|
height: ${maxDistance * 2}px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%);
|
||||||
|
left: ${x - maxDistance}px;
|
||||||
|
top: ${y - maxDistance}px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
el.appendChild(ripple);
|
||||||
|
|
||||||
|
gsap.fromTo(
|
||||||
|
ripple,
|
||||||
|
{
|
||||||
|
scale: 0,
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: 1,
|
||||||
|
opacity: 0,
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "power2.out",
|
||||||
|
onComplete: () => ripple.remove(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener("mousemove", handleMouseMove);
|
||||||
|
el.addEventListener("mouseleave", handleMouseLeave);
|
||||||
|
el.addEventListener("click", handleClick);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="magic-bento-card__header">
|
||||||
|
<div className="magic-bento-card__label">{card.label}</div>
|
||||||
|
</div>
|
||||||
|
<div className="magic-bento-card__content">
|
||||||
|
<h2 className="magic-bento-card__title">{card.title}</h2>
|
||||||
|
<p className="magic-bento-card__description">
|
||||||
|
{card.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BentoCardGrid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MagicBento;
|
||||||
@ -1,16 +1,16 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { ComponentPropsWithoutRef, useEffect, useRef } from "react"
|
import { type ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
||||||
import { useInView, useMotionValue, useSpring } from "motion/react"
|
import { useInView, useMotionValue, useSpring } from "motion/react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
|
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
|
||||||
value: number
|
value: number;
|
||||||
startValue?: number
|
startValue?: number;
|
||||||
direction?: "up" | "down"
|
direction?: "up" | "down";
|
||||||
delay?: number
|
delay?: number;
|
||||||
decimalPlaces?: number
|
decimalPlaces?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NumberTicker({
|
export function NumberTicker({
|
||||||
@ -22,22 +22,22 @@ export function NumberTicker({
|
|||||||
decimalPlaces = 0,
|
decimalPlaces = 0,
|
||||||
...props
|
...props
|
||||||
}: NumberTickerProps) {
|
}: NumberTickerProps) {
|
||||||
const ref = useRef<HTMLSpanElement>(null)
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
const motionValue = useMotionValue(direction === "down" ? value : startValue)
|
const motionValue = useMotionValue(direction === "down" ? value : startValue);
|
||||||
const springValue = useSpring(motionValue, {
|
const springValue = useSpring(motionValue, {
|
||||||
damping: 60,
|
damping: 60,
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
})
|
});
|
||||||
const isInView = useInView(ref, { once: true, margin: "0px" })
|
const isInView = useInView(ref, { once: true, margin: "0px" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInView) {
|
if (isInView) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
motionValue.set(direction === "down" ? startValue : value)
|
motionValue.set(direction === "down" ? startValue : value);
|
||||||
}, delay * 1000)
|
}, delay * 1000);
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [motionValue, isInView, delay, value, direction, startValue])
|
}, [motionValue, isInView, delay, value, direction, startValue]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() =>
|
() =>
|
||||||
@ -46,22 +46,22 @@ export function NumberTicker({
|
|||||||
ref.current.textContent = Intl.NumberFormat("en-US", {
|
ref.current.textContent = Intl.NumberFormat("en-US", {
|
||||||
minimumFractionDigits: decimalPlaces,
|
minimumFractionDigits: decimalPlaces,
|
||||||
maximumFractionDigits: decimalPlaces,
|
maximumFractionDigits: decimalPlaces,
|
||||||
}).format(Number(latest.toFixed(decimalPlaces)))
|
}).format(Number(latest.toFixed(decimalPlaces)));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
[springValue, decimalPlaces]
|
[springValue, decimalPlaces],
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-block tracking-wider text-black tabular-nums dark:text-white",
|
"inline-block tracking-wider text-black tabular-nums dark:text-white",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{startValue}
|
{startValue}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
29
frontend/src/components/ui/spotlight-card.css
Normal file
29
frontend/src/components/ui/spotlight-card.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
.card-spotlight {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
border: 1px solid #222;
|
||||||
|
background-color: #111;
|
||||||
|
padding: 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
--mouse-x: 50%;
|
||||||
|
--mouse-y: 50%;
|
||||||
|
--spotlight-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-spotlight::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: radial-gradient(circle at var(--mouse-x) var(--mouse-y), var(--spotlight-color), transparent 80%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-spotlight:hover::before,
|
||||||
|
.card-spotlight:focus-within::before {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
46
frontend/src/components/ui/spotlight-card.tsx
Normal file
46
frontend/src/components/ui/spotlight-card.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useRef } from "react";
|
||||||
|
import "./spotlight-card.css";
|
||||||
|
|
||||||
|
interface Position {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SpotlightCardProps extends React.PropsWithChildren {
|
||||||
|
className?: string;
|
||||||
|
spotlightColor?: `rgba(${number}, ${number}, ${number}, ${number})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpotlightCard: React.FC<SpotlightCardProps> = ({
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
spotlightColor = "rgba(255, 255, 255, 0.25)",
|
||||||
|
}) => {
|
||||||
|
const divRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMouseMove: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (!divRef.current) return;
|
||||||
|
|
||||||
|
const rect = divRef.current.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
divRef.current.style.setProperty("--mouse-x", `${x}px`);
|
||||||
|
divRef.current.style.setProperty("--mouse-y", `${y}px`);
|
||||||
|
divRef.current.style.setProperty("--spotlight-color", spotlightColor);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={divRef}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
className={`card-spotlight ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpotlightCard;
|
||||||
257
frontend/src/components/ui/terminal.tsx
Normal file
257
frontend/src/components/ui/terminal.tsx
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Children,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { motion, type MotionProps, useInView } from "motion/react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
interface SequenceContextValue {
|
||||||
|
completeItem: (index: number) => void;
|
||||||
|
activeIndex: number;
|
||||||
|
sequenceStarted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceContext = createContext<SequenceContextValue | null>(null);
|
||||||
|
|
||||||
|
const useSequence = () => useContext(SequenceContext);
|
||||||
|
|
||||||
|
const ItemIndexContext = createContext<number | null>(null);
|
||||||
|
const useItemIndex = () => useContext(ItemIndexContext);
|
||||||
|
|
||||||
|
interface AnimatedSpanProps extends MotionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
className?: string;
|
||||||
|
startOnView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedSpan = ({
|
||||||
|
children,
|
||||||
|
delay = 0,
|
||||||
|
className,
|
||||||
|
startOnView = false,
|
||||||
|
...props
|
||||||
|
}: AnimatedSpanProps) => {
|
||||||
|
const elementRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sequence = useSequence();
|
||||||
|
const itemIndex = useItemIndex();
|
||||||
|
const [hasStarted, setHasStarted] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sequence || itemIndex === null) return;
|
||||||
|
if (!sequence.sequenceStarted) return;
|
||||||
|
if (hasStarted) return;
|
||||||
|
if (sequence.activeIndex === itemIndex) {
|
||||||
|
setHasStarted(true);
|
||||||
|
}
|
||||||
|
}, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex]);
|
||||||
|
|
||||||
|
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={elementRef}
|
||||||
|
initial={{ opacity: 0, y: -5 }}
|
||||||
|
animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }}
|
||||||
|
transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }}
|
||||||
|
className={cn("grid text-sm font-normal tracking-tight", className)}
|
||||||
|
onAnimationComplete={() => {
|
||||||
|
if (!sequence) return;
|
||||||
|
if (itemIndex === null) return;
|
||||||
|
sequence.completeItem(itemIndex);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children: string;
|
||||||
|
className?: string;
|
||||||
|
duration?: number;
|
||||||
|
delay?: number;
|
||||||
|
as?: React.ElementType;
|
||||||
|
startOnView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TypingAnimation = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
duration = 60,
|
||||||
|
delay = 0,
|
||||||
|
as: Component = "span",
|
||||||
|
startOnView = true,
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) => {
|
||||||
|
if (typeof children !== "string") {
|
||||||
|
throw new Error("TypingAnimation: children must be a string. Received:");
|
||||||
|
}
|
||||||
|
|
||||||
|
const MotionComponent = useMemo(
|
||||||
|
() =>
|
||||||
|
motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
}),
|
||||||
|
[Component],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>("");
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null);
|
||||||
|
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sequence = useSequence();
|
||||||
|
const itemIndex = useItemIndex();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sequence && itemIndex !== null) {
|
||||||
|
if (!sequence.sequenceStarted) return;
|
||||||
|
if (started) return;
|
||||||
|
if (sequence.activeIndex === itemIndex) {
|
||||||
|
setStarted(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startOnView) {
|
||||||
|
const startTimeout = setTimeout(() => setStarted(true), delay);
|
||||||
|
return () => clearTimeout(startTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInView) return;
|
||||||
|
|
||||||
|
const startTimeout = setTimeout(() => setStarted(true), delay);
|
||||||
|
return () => clearTimeout(startTimeout);
|
||||||
|
}, [
|
||||||
|
delay,
|
||||||
|
startOnView,
|
||||||
|
isInView,
|
||||||
|
started,
|
||||||
|
sequence?.activeIndex,
|
||||||
|
sequence?.sequenceStarted,
|
||||||
|
itemIndex,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!started) return;
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
const typingEffect = setInterval(() => {
|
||||||
|
if (i < children.length) {
|
||||||
|
setDisplayedText(children.substring(0, i + 1));
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
if (sequence && itemIndex !== null) {
|
||||||
|
sequence.completeItem(itemIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(typingEffect);
|
||||||
|
};
|
||||||
|
}, [children, duration, started]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn("text-sm font-normal tracking-tight", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
</MotionComponent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TerminalProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
sequence?: boolean;
|
||||||
|
startOnView?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Terminal = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
sequence = true,
|
||||||
|
startOnView = true,
|
||||||
|
}: TerminalProps) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const isInView = useInView(containerRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
const sequenceHasStarted = sequence ? !startOnView || isInView : false;
|
||||||
|
|
||||||
|
const contextValue = useMemo<SequenceContextValue | null>(() => {
|
||||||
|
if (!sequence) return null;
|
||||||
|
return {
|
||||||
|
completeItem: (index: number) => {
|
||||||
|
setActiveIndex((current) =>
|
||||||
|
index === current ? current + 1 : current,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
activeIndex,
|
||||||
|
sequenceStarted: sequenceHasStarted,
|
||||||
|
};
|
||||||
|
}, [sequence, activeIndex, sequenceHasStarted]);
|
||||||
|
|
||||||
|
const wrappedChildren = useMemo(() => {
|
||||||
|
if (!sequence) return children;
|
||||||
|
const array = Children.toArray(children);
|
||||||
|
return array.map((child, index) => (
|
||||||
|
<ItemIndexContext.Provider key={index} value={index}>
|
||||||
|
{child as React.ReactNode}
|
||||||
|
</ItemIndexContext.Provider>
|
||||||
|
));
|
||||||
|
}, [children, sequence]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={cn(
|
||||||
|
"border-border bg-background z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="border-border flex flex-col gap-y-2 border-b p-4">
|
||||||
|
<div className="flex flex-row gap-x-2">
|
||||||
|
<div className="h-2 w-2 rounded-full bg-red-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-yellow-500"></div>
|
||||||
|
<div className="h-2 w-2 rounded-full bg-green-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre className="p-4">
|
||||||
|
<code className="grid gap-y-1 overflow-auto">{wrappedChildren}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sequence) return content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SequenceContext.Provider value={contextValue}>
|
||||||
|
{content}
|
||||||
|
</SequenceContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { AnimatePresence, motion, MotionProps } from "motion/react";
|
import { AnimatePresence, motion, type MotionProps } from "motion/react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { AuroraText } from "./aurora-text";
|
import { AuroraText } from "./aurora-text";
|
||||||
@ -15,12 +15,12 @@ interface WordRotateProps {
|
|||||||
|
|
||||||
export function WordRotate({
|
export function WordRotate({
|
||||||
words,
|
words,
|
||||||
duration = 2500,
|
duration = 2200,
|
||||||
motionProps = {
|
motionProps = {
|
||||||
initial: { opacity: 0, y: -50, filter: "blur(16px)" },
|
initial: { opacity: 0, y: -50, filter: "blur(16px)" },
|
||||||
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
|
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||||
exit: { opacity: 0, y: 50, filter: "blur(16px)" },
|
exit: { opacity: 0, y: 50, filter: "blur(16px)" },
|
||||||
transition: { duration: 0.25, ease: "easeOut" },
|
transition: { duration: 0.3, ease: "easeOut" },
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
}: WordRotateProps) {
|
}: WordRotateProps) {
|
||||||
@ -37,7 +37,7 @@ export function WordRotate({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden py-2">
|
<div className="overflow-hidden py-2">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="popLayout">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
key={words[index]}
|
key={words[index]}
|
||||||
className={cn(className)}
|
className={cn(className)}
|
||||||
|
|||||||
@ -49,10 +49,11 @@ export function MessageList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (group.type === "assistant:present-files") {
|
if (group.type === "assistant:present-files") {
|
||||||
const files = [];
|
const files: string[] = [];
|
||||||
for (const message of group.messages) {
|
for (const message of group.messages) {
|
||||||
if (hasPresentFiles(message)) {
|
if (hasPresentFiles(message)) {
|
||||||
files.push(...extractPresentFilesFromMessage(message));
|
const presentFiles = extractPresentFilesFromMessage(message);
|
||||||
|
files.push(...presentFiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -12,13 +12,10 @@
|
|||||||
/* Strictness */
|
/* Strictness */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"checkJs": true,
|
"noImplicitAny": false,
|
||||||
|
"checkJs": false,
|
||||||
/* Bundled projects */
|
/* Bundled projects */
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"ES2022"
|
|
||||||
],
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
@ -32,9 +29,7 @@
|
|||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@ -46,8 +41,5 @@
|
|||||||
".next/types/**/*.ts",
|
".next/types/**/*.ts",
|
||||||
".next/dev/types/**/*.ts"
|
".next/dev/types/**/*.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": ["node_modules", "generated"]
|
||||||
"node_modules",
|
|
||||||
"generated"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user