mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-02 22:02:13 +08:00
feat: implement the first version of landing page
This commit is contained in:
@@ -57,6 +57,7 @@
|
||||
"codemirror": "^6.0.2",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"gsap": "^3.13.0",
|
||||
"hast": "^1.0.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.26.2",
|
||||
@@ -81,6 +82,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.0.15",
|
||||
"@types/gsap": "^3.0.0",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/react": "^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:
|
||||
specifier: ^8.6.0
|
||||
version: 8.6.0(react@19.2.3)
|
||||
gsap:
|
||||
specifier: ^3.13.0
|
||||
version: 3.14.2
|
||||
hast:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@@ -198,6 +201,9 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.0.15
|
||||
version: 4.1.18
|
||||
'@types/gsap':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
'@types/node':
|
||||
specifier: ^20.14.10
|
||||
version: 20.19.29
|
||||
@@ -2008,6 +2014,10 @@ packages:
|
||||
'@types/geojson@7946.0.16':
|
||||
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':
|
||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||
|
||||
@@ -3294,6 +3304,9 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
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:
|
||||
resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==}
|
||||
|
||||
@@ -6890,6 +6903,10 @@ snapshots:
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/gsap@3.0.0':
|
||||
dependencies:
|
||||
gsap: 3.14.2
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
@@ -8407,6 +8424,8 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
gsap@3.14.2: {}
|
||||
|
||||
h3@1.15.5:
|
||||
dependencies:
|
||||
cookie-es: 1.2.2
|
||||
|
||||
@@ -1,82 +1,25 @@
|
||||
import { StarFilledIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Jumbotron } from "@/components/landing/jumbotron";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import { env } from "@/env";
|
||||
import { Footer } from "@/components/landing/footer";
|
||||
import { Header } from "@/components/landing/header";
|
||||
import { Hero } from "@/components/landing/hero";
|
||||
import { CaseStudySection } from "@/components/landing/sections/case-study-section";
|
||||
import { CommunitySection } from "@/components/landing/sections/community-section";
|
||||
import { SandboxSection } from "@/components/landing/sections/sandbox-section";
|
||||
import { SkillsSection } from "@/components/landing/sections/skills-section";
|
||||
import { WhatsNewSection } from "@/components/landing/sections/whats-new-section";
|
||||
|
||||
export default function LandingPage() {
|
||||
return (
|
||||
<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">
|
||||
<div className="font-serif text-xl">
|
||||
<h1>DeerFlow</h1>
|
||||
</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"
|
||||
>
|
||||
<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 />
|
||||
<Header />
|
||||
<main className="flex w-full flex-col">
|
||||
<Hero />
|
||||
<CaseStudySection />
|
||||
<SkillsSection />
|
||||
<SandboxSection />
|
||||
<WhatsNewSection />
|
||||
<CommunitySection />
|
||||
</main>
|
||||
<footer className="container-md mx-auto"></footer>
|
||||
<Footer />
|
||||
</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";
|
||||
|
||||
import { ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import Galaxy from "@/components/Galaxy";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FlickeringGrid } from "@/components/ui/flickering-grid";
|
||||
import Galaxy from "@/components/ui/galaxy";
|
||||
import { WordRotate } from "@/components/ui/word-rotate";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Jumbotron({ className }: { className?: string }) {
|
||||
export function Hero({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -16,30 +17,28 @@ export function Jumbotron({ className }: { className?: string }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="absolute inset-0 z-0 bg-black">
|
||||
<div className="absolute inset-0 z-0 bg-black/40">
|
||||
<Galaxy
|
||||
mouseRepulsion={false}
|
||||
starSpeed={0.2}
|
||||
density={0.6}
|
||||
glowIntensity={0.3}
|
||||
glowIntensity={0.35}
|
||||
twinkleIntensity={0.3}
|
||||
speed={0.5}
|
||||
/>
|
||||
</div>
|
||||
<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}
|
||||
gridGap={4}
|
||||
color={"white"}
|
||||
maxOpacity={0.2}
|
||||
maxOpacity={0.3}
|
||||
flickerChance={0.25}
|
||||
/>
|
||||
<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">
|
||||
<WordRotate
|
||||
words={[
|
||||
"Do Anything",
|
||||
"Learn Anything",
|
||||
"Deep Research",
|
||||
"Collect Data",
|
||||
"Analyze Data",
|
||||
@@ -51,21 +50,28 @@ export function Jumbotron({ className }: { className?: string }) {
|
||||
"Generate Videos",
|
||||
"Generate Songs",
|
||||
"Organize Emails",
|
||||
"Do Anything",
|
||||
"Learn Anything",
|
||||
]}
|
||||
/>{" "}
|
||||
<div>with DeerFlow</div>
|
||||
</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
|
||||
<br />
|
||||
creates. With the help of sandboxes, tools and skills, it handles
|
||||
<br />
|
||||
different levels of tasks that could take minutes to hours.
|
||||
</p>
|
||||
<Button className="size-lg mt-8 scale-108" size="lg">
|
||||
<span className="text-md">Get Started with 2.0</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
<Link href="/workspace">
|
||||
<Button className="size-lg mt-8 scale-108" size="lg">
|
||||
<span className="text-md">Get Started with 2.0</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</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> {
|
||||
squareSize?: number
|
||||
gridGap?: number
|
||||
flickerChance?: number
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
className?: string
|
||||
maxOpacity?: number
|
||||
squareSize?: number;
|
||||
gridGap?: number;
|
||||
flickerChance?: number;
|
||||
color?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
maxOpacity?: number;
|
||||
}
|
||||
|
||||
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
@@ -26,58 +32,58 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
maxOpacity = 0.3,
|
||||
...props
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isInView, setIsInView] = useState(false)
|
||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isInView, setIsInView] = useState(false);
|
||||
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const memoizedColor = useMemo(() => {
|
||||
const toRGBA = (color: string) => {
|
||||
if (typeof window === "undefined") {
|
||||
return `rgba(0, 0, 0,`
|
||||
return `rgba(0, 0, 0,`;
|
||||
}
|
||||
const canvas = document.createElement("canvas")
|
||||
canvas.width = canvas.height = 1
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return "rgba(255, 0, 0,"
|
||||
ctx.fillStyle = color
|
||||
ctx.fillRect(0, 0, 1, 1)
|
||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
|
||||
return `rgba(${r}, ${g}, ${b},`
|
||||
}
|
||||
return toRGBA(color)
|
||||
}, [color])
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = canvas.height = 1;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return "rgba(255, 0, 0,";
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||
return `rgba(${r}, ${g}, ${b},`;
|
||||
};
|
||||
return toRGBA(color);
|
||||
}, [color]);
|
||||
|
||||
const setupCanvas = useCallback(
|
||||
(canvas: HTMLCanvasElement, width: number, height: number) => {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
const cols = Math.floor(width / (squareSize + gridGap))
|
||||
const rows = Math.floor(height / (squareSize + gridGap))
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
canvas.style.width = `${width}px`;
|
||||
canvas.style.height = `${height}px`;
|
||||
const cols = Math.floor(width / (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++) {
|
||||
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(
|
||||
(squares: Float32Array, deltaTime: number) => {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity
|
||||
squares[i] = Math.random() * maxOpacity;
|
||||
}
|
||||
}
|
||||
},
|
||||
[flickerChance, maxOpacity]
|
||||
)
|
||||
[flickerChance, maxOpacity],
|
||||
);
|
||||
|
||||
const drawGrid = useCallback(
|
||||
(
|
||||
@@ -87,56 +93,56 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number
|
||||
dpr: number,
|
||||
) => {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.fillStyle = "transparent"
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.fillStyle = "transparent";
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j]
|
||||
ctx.fillStyle = `${memoizedColor}${opacity})`
|
||||
const opacity = squares[i * rows + j];
|
||||
ctx.fillStyle = `${memoizedColor}${opacity})`;
|
||||
ctx.fillRect(
|
||||
i * (squareSize + gridGap) * dpr,
|
||||
j * (squareSize + gridGap) * dpr,
|
||||
squareSize * dpr,
|
||||
squareSize * dpr
|
||||
)
|
||||
squareSize * dpr,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[memoizedColor, squareSize, gridGap]
|
||||
)
|
||||
[memoizedColor, squareSize, gridGap],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const container = containerRef.current
|
||||
if (!canvas || !container) return
|
||||
const canvas = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!canvas || !container) return;
|
||||
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId: number
|
||||
let gridParams: ReturnType<typeof setupCanvas>
|
||||
let animationFrameId: number;
|
||||
let gridParams: ReturnType<typeof setupCanvas>;
|
||||
|
||||
const updateCanvasSize = () => {
|
||||
const newWidth = width || container.clientWidth
|
||||
const newHeight = height || container.clientHeight
|
||||
setCanvasSize({ width: newWidth, height: newHeight })
|
||||
gridParams = setupCanvas(canvas, newWidth, newHeight)
|
||||
}
|
||||
const newWidth = width || container.clientWidth;
|
||||
const newHeight = height || container.clientHeight;
|
||||
setCanvasSize({ width: newWidth, height: newHeight });
|
||||
gridParams = setupCanvas(canvas, newWidth, newHeight);
|
||||
};
|
||||
|
||||
updateCanvasSize()
|
||||
updateCanvasSize();
|
||||
|
||||
let lastTime = 0
|
||||
let lastTime = 0;
|
||||
const animate = (time: number) => {
|
||||
if (!isInView) return
|
||||
if (!isInView) return;
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000
|
||||
lastTime = time
|
||||
const deltaTime = (time - lastTime) / 1000;
|
||||
lastTime = time;
|
||||
|
||||
updateSquares(gridParams.squares, deltaTime)
|
||||
updateSquares(gridParams.squares, deltaTime);
|
||||
drawGrid(
|
||||
ctx,
|
||||
canvas.width,
|
||||
@@ -144,36 +150,38 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
gridParams.cols,
|
||||
gridParams.rows,
|
||||
gridParams.squares,
|
||||
gridParams.dpr
|
||||
)
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
gridParams.dpr,
|
||||
);
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize()
|
||||
})
|
||||
updateCanvasSize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(container)
|
||||
resizeObserver.observe(container);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
setIsInView(entry.isIntersecting)
|
||||
if (entry) {
|
||||
setIsInView(entry.isIntersecting);
|
||||
}
|
||||
},
|
||||
{ threshold: 0 }
|
||||
)
|
||||
{ threshold: 0 },
|
||||
);
|
||||
|
||||
intersectionObserver.observe(canvas)
|
||||
intersectionObserver.observe(canvas);
|
||||
|
||||
if (isInView) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
animationFrameId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
resizeObserver.disconnect()
|
||||
intersectionObserver.disconnect()
|
||||
}
|
||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
resizeObserver.disconnect();
|
||||
intersectionObserver.disconnect();
|
||||
};
|
||||
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -190,5 +198,5 @@ export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from 'ogl';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import './Galaxy.css';
|
||||
import { Renderer, Program, Mesh, Color, Triangle } from "ogl";
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./galaxy.css";
|
||||
|
||||
const vertexShader = `
|
||||
attribute vec2 uv;
|
||||
@@ -84,7 +84,7 @@ float Star(vec2 uv, float flare) {
|
||||
vec3 StarLayer(vec2 uv) {
|
||||
vec3 col = vec3(0.0);
|
||||
|
||||
vec2 gv = fract(uv) - 0.5;
|
||||
vec2 gv = fract(uv) - 0.5;
|
||||
vec2 id = floor(uv);
|
||||
|
||||
for (int y = -1; y <= 1; y++) {
|
||||
@@ -100,7 +100,7 @@ vec3 StarLayer(vec2 uv) {
|
||||
float blu = smoothstep(STAR_COLOR_CUTOFF, 1.0, Hash21(si + 3.0)) + STAR_COLOR_CUTOFF;
|
||||
float grn = min(red, blu) * seed;
|
||||
vec3 base = vec3(red, grn, blu);
|
||||
|
||||
|
||||
float hue = atan(base.g - base.r, base.b - base.r) / (2.0 * 3.14159) + 0.5;
|
||||
hue = fract(hue + uHueShift / 360.0);
|
||||
float sat = length(base - vec3(dot(base, vec3(0.299, 0.587, 0.114)))) * uSaturation;
|
||||
@@ -115,7 +115,7 @@ vec3 StarLayer(vec2 uv) {
|
||||
float twinkle = trisn(uTime * uSpeed + seed * 6.2831) * 0.5 + 1.0;
|
||||
twinkle = mix(1.0, twinkle, uTwinkleIntensity);
|
||||
star *= twinkle;
|
||||
|
||||
|
||||
col += star * size * color;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ void main() {
|
||||
vec2 uv = (vUv * uResolution.xy - focalPx) / uResolution.y;
|
||||
|
||||
vec2 mouseNorm = uMouse - vec2(0.5);
|
||||
|
||||
|
||||
if (uAutoCenterRepulsion > 0.0) {
|
||||
vec2 centerUV = vec2(0.0, 0.0);
|
||||
float centerDist = length(uv - centerUV);
|
||||
@@ -200,7 +200,7 @@ export default function Galaxy({
|
||||
const ctn = ctnDom.current;
|
||||
const renderer = new Renderer({
|
||||
alpha: transparent,
|
||||
premultipliedAlpha: false
|
||||
premultipliedAlpha: false,
|
||||
});
|
||||
const gl = renderer.gl;
|
||||
|
||||
@@ -212,6 +212,7 @@ export default function Galaxy({
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
}
|
||||
|
||||
/** @type {Program | undefined} */
|
||||
let program;
|
||||
|
||||
function resize() {
|
||||
@@ -221,11 +222,11 @@ export default function Galaxy({
|
||||
program.uniforms.uResolution.value = new Color(
|
||||
gl.canvas.width,
|
||||
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();
|
||||
|
||||
const geometry = new Triangle(gl);
|
||||
@@ -235,7 +236,11 @@ export default function Galaxy({
|
||||
uniforms: {
|
||||
uTime: { value: 0 },
|
||||
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) },
|
||||
uRotation: { value: new Float32Array(rotation) },
|
||||
@@ -244,7 +249,10 @@ export default function Galaxy({
|
||||
uHueShift: { value: hueShift },
|
||||
uSpeed: { value: speed },
|
||||
uMouse: {
|
||||
value: new Float32Array([smoothMousePos.current.x, smoothMousePos.current.y])
|
||||
value: new Float32Array([
|
||||
smoothMousePos.current.x,
|
||||
smoothMousePos.current.y,
|
||||
]),
|
||||
},
|
||||
uGlowIntensity: { value: glowIntensity },
|
||||
uSaturation: { value: saturation },
|
||||
@@ -254,8 +262,8 @@ export default function Galaxy({
|
||||
uRepulsionStrength: { value: repulsionStrength },
|
||||
uMouseActiveFactor: { value: 0.0 },
|
||||
uAutoCenterRepulsion: { value: autoCenterRepulsion },
|
||||
uTransparent: { value: transparent }
|
||||
}
|
||||
uTransparent: { value: transparent },
|
||||
},
|
||||
});
|
||||
|
||||
const mesh = new Mesh(gl, { geometry, program });
|
||||
@@ -269,10 +277,13 @@ export default function Galaxy({
|
||||
}
|
||||
|
||||
const lerpFactor = 0.05;
|
||||
smoothMousePos.current.x += (targetMousePos.current.x - smoothMousePos.current.x) * lerpFactor;
|
||||
smoothMousePos.current.y += (targetMousePos.current.y - smoothMousePos.current.y) * lerpFactor;
|
||||
smoothMousePos.current.x +=
|
||||
(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[1] = smoothMousePos.current.y;
|
||||
@@ -296,19 +307,19 @@ export default function Galaxy({
|
||||
}
|
||||
|
||||
if (mouseInteraction) {
|
||||
ctn.addEventListener('mousemove', handleMouseMove);
|
||||
ctn.addEventListener('mouseleave', handleMouseLeave);
|
||||
ctn.addEventListener("mousemove", handleMouseMove);
|
||||
ctn.addEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animateId);
|
||||
window.removeEventListener('resize', resize);
|
||||
window.removeEventListener("resize", resize);
|
||||
if (mouseInteraction) {
|
||||
ctn.removeEventListener('mousemove', handleMouseMove);
|
||||
ctn.removeEventListener('mouseleave', handleMouseLeave);
|
||||
ctn.removeEventListener("mousemove", handleMouseMove);
|
||||
ctn.removeEventListener("mouseleave", handleMouseLeave);
|
||||
}
|
||||
ctn.removeChild(gl.canvas);
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
||||
};
|
||||
}, [
|
||||
focal,
|
||||
@@ -326,7 +337,7 @@ export default function Galaxy({
|
||||
rotationSpeed,
|
||||
repulsionStrength,
|
||||
autoCenterRepulsion,
|
||||
transparent
|
||||
transparent,
|
||||
]);
|
||||
|
||||
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 { useInView, useMotionValue, useSpring } from "motion/react"
|
||||
import { type ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
||||
import { useInView, useMotionValue, useSpring } from "motion/react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
|
||||
value: number
|
||||
startValue?: number
|
||||
direction?: "up" | "down"
|
||||
delay?: number
|
||||
decimalPlaces?: number
|
||||
value: number;
|
||||
startValue?: number;
|
||||
direction?: "up" | "down";
|
||||
delay?: number;
|
||||
decimalPlaces?: number;
|
||||
}
|
||||
|
||||
export function NumberTicker({
|
||||
@@ -22,22 +22,22 @@ export function NumberTicker({
|
||||
decimalPlaces = 0,
|
||||
...props
|
||||
}: NumberTickerProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null)
|
||||
const motionValue = useMotionValue(direction === "down" ? value : startValue)
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const motionValue = useMotionValue(direction === "down" ? value : startValue);
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping: 60,
|
||||
stiffness: 100,
|
||||
})
|
||||
const isInView = useInView(ref, { once: true, margin: "0px" })
|
||||
});
|
||||
const isInView = useInView(ref, { once: true, margin: "0px" });
|
||||
|
||||
useEffect(() => {
|
||||
if (isInView) {
|
||||
const timer = setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? startValue : value)
|
||||
}, delay * 1000)
|
||||
return () => clearTimeout(timer)
|
||||
motionValue.set(direction === "down" ? startValue : value);
|
||||
}, delay * 1000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [motionValue, isInView, delay, value, direction, startValue])
|
||||
}, [motionValue, isInView, delay, value, direction, startValue]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
@@ -46,22 +46,22 @@ export function NumberTicker({
|
||||
ref.current.textContent = Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(Number(latest.toFixed(decimalPlaces)))
|
||||
}).format(Number(latest.toFixed(decimalPlaces)));
|
||||
}
|
||||
}),
|
||||
[springValue, decimalPlaces]
|
||||
)
|
||||
[springValue, decimalPlaces],
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-block tracking-wider text-black tabular-nums dark:text-white",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{startValue}
|
||||
</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";
|
||||
|
||||
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 { AuroraText } from "./aurora-text";
|
||||
@@ -15,12 +15,12 @@ interface WordRotateProps {
|
||||
|
||||
export function WordRotate({
|
||||
words,
|
||||
duration = 2500,
|
||||
duration = 2200,
|
||||
motionProps = {
|
||||
initial: { opacity: 0, y: -50, filter: "blur(16px)" },
|
||||
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
|
||||
exit: { opacity: 0, y: 50, filter: "blur(16px)" },
|
||||
transition: { duration: 0.25, ease: "easeOut" },
|
||||
transition: { duration: 0.3, ease: "easeOut" },
|
||||
},
|
||||
className,
|
||||
}: WordRotateProps) {
|
||||
@@ -37,7 +37,7 @@ export function WordRotate({
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden py-2">
|
||||
<AnimatePresence mode="wait">
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.h1
|
||||
key={words[index]}
|
||||
className={cn(className)}
|
||||
|
||||
@@ -49,10 +49,11 @@ export function MessageList({
|
||||
);
|
||||
}
|
||||
if (group.type === "assistant:present-files") {
|
||||
const files = [];
|
||||
const files: string[] = [];
|
||||
for (const message of group.messages) {
|
||||
if (hasPresentFiles(message)) {
|
||||
files.push(...extractPresentFilesFromMessage(message));
|
||||
const presentFiles = extractPresentFilesFromMessage(message);
|
||||
files.push(...presentFiles);
|
||||
}
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -12,13 +12,10 @@
|
||||
/* Strictness */
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"checkJs": true,
|
||||
"noImplicitAny": false,
|
||||
"checkJs": false,
|
||||
/* Bundled projects */
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"ES2022"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"noEmit": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
@@ -32,9 +29,7 @@
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -46,8 +41,5 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"generated"
|
||||
]
|
||||
"exclude": ["node_modules", "generated"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user