Compare commits
5 Commits
886e7557df
...
f30006a983
| Author | SHA1 | Date | |
|---|---|---|---|
| f30006a983 | |||
| 044f9cf38b | |||
| 5bd2459855 | |||
| 6c0ed3cd65 | |||
| fa0a55d846 |
10924
package-lock.json
generated
10924
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -10,34 +10,44 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.0",
|
"@headlessui/react": "^2.2.9",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@lobehub/icons": "^1.97.2",
|
||||||
"@types/node": "^20.10.8",
|
"@tabler/icons-react": "^3.35.0",
|
||||||
"@types/react": "^18.2.47",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/node": "^20.19.23",
|
||||||
|
"@types/react": "^18.3.26",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cobe": "^0.6.5",
|
"cobe": "^0.6.5",
|
||||||
"framer-motion": "^10.15.0",
|
"framer-motion": "^10.18.0",
|
||||||
"react": "^18.2.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
"motion": "^12.23.24",
|
||||||
|
"next": "^14.2.33",
|
||||||
|
"popmotion": "^11.0.5",
|
||||||
|
"react": "^18.3.1",
|
||||||
"react-countup": "^6.5.3",
|
"react-countup": "^6.5.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"react-type-animation": "^3.2.0",
|
"react-type-animation": "^3.2.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.15",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.9.3",
|
||||||
"use-debounce": "^10.0.6"
|
"use-debounce": "^10.0.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.15",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.1",
|
||||||
"prettier": "^3.3.2",
|
"eslint-config-next": "^14.2.33",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier": "^3.6.2",
|
||||||
"sharp": "0.33.1",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"sharp": "^0.33.1",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite": "^7.1.7"
|
"vite": "^7.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 718 KiB |
3
src/components/logos/Ai21.tsx
Normal file
3
src/components/logos/Ai21.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Ai21 } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <Ai21.Brand size={30} />;
|
||||||
3
src/components/logos/AlibabaCloud.tsx
Normal file
3
src/components/logos/AlibabaCloud.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { AlibabaCloud } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <AlibabaCloud.Text size={30} />;
|
||||||
3
src/components/logos/BaiduCloud.tsx
Normal file
3
src/components/logos/BaiduCloud.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { BaiduCloud } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <BaiduCloud.Combine size={30} />;
|
||||||
3
src/components/logos/ByteDance.tsx
Normal file
3
src/components/logos/ByteDance.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { ByteDance } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <ByteDance.Text size={30} />;
|
||||||
3
src/components/logos/Claude.tsx
Normal file
3
src/components/logos/Claude.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Claude } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <Claude.Combine size={30} />;
|
||||||
3
src/components/logos/DeepMind.tsx
Normal file
3
src/components/logos/DeepMind.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { DeepMind } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <DeepMind.Combine size={30} />;
|
||||||
3
src/components/logos/DeepSeek.tsx
Normal file
3
src/components/logos/DeepSeek.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { DeepSeek } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <DeepSeek.Combine size={30} />;
|
||||||
3
src/components/logos/Minimax.tsx
Normal file
3
src/components/logos/Minimax.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Minimax } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <Minimax.Combine size={30} />;
|
||||||
3
src/components/logos/Mistral.tsx
Normal file
3
src/components/logos/Mistral.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Mistral } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <Mistral.Combine size={30} />;
|
||||||
3
src/components/logos/Moonshot.tsx
Normal file
3
src/components/logos/Moonshot.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { Moonshot } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <Moonshot.Combine size={30} />;
|
||||||
0
src/components/logos/NousResearch.tsx
Normal file
0
src/components/logos/NousResearch.tsx
Normal file
3
src/components/logos/OpenAI.tsx
Normal file
3
src/components/logos/OpenAI.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { OpenAI } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <OpenAI.Combine size={30} />;
|
||||||
3
src/components/logos/TencentCloud.tsx
Normal file
3
src/components/logos/TencentCloud.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { TencentCloud } from '@lobehub/icons';
|
||||||
|
|
||||||
|
export default () => <TencentCloud.Combine size={30} />;
|
||||||
23
src/components/logos/XAI.tsx
Normal file
23
src/components/logos/XAI.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const XAILogo = () => (
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="katman_1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 841.89 595.28"
|
||||||
|
xmlSpace="preserve"
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<polygon points="557.09,211.99 565.4,538.36 631.96,538.36 640.28,93.18 " />
|
||||||
|
<polygon points="640.28,56.91 538.72,56.91 379.35,284.53 430.13,357.05 " />
|
||||||
|
<polygon points="201.61,538.36 303.17,538.36 353.96,465.84 303.17,393.31 " />
|
||||||
|
<polygon points="201.61,211.99 430.13,538.36 531.69,538.36 303.17,211.99 " />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default XAILogo;
|
||||||
105
src/components/magicui/infinite-moving-cards.tsx
Normal file
105
src/components/magicui/infinite-moving-cards.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const InfiniteMovingCards = ({
|
||||||
|
items,
|
||||||
|
direction = "left",
|
||||||
|
speed = "fast",
|
||||||
|
pauseOnHover = true,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
items: React.ReactNode[];
|
||||||
|
direction?: "left" | "right";
|
||||||
|
speed?: "fast" | "normal" | "slow";
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}): JSX.Element => {
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const scrollerRef = React.useRef<HTMLUListElement>(null);
|
||||||
|
const [start, setStart] = useState(false);
|
||||||
|
|
||||||
|
const getDirection = useCallback(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
if (direction === "left") {
|
||||||
|
containerRef.current.style.setProperty("--animation-direction", "forwards");
|
||||||
|
} else {
|
||||||
|
containerRef.current.style.setProperty("--animation-direction", "reverse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [direction]);
|
||||||
|
|
||||||
|
const getSpeed = useCallback(() => {
|
||||||
|
if (containerRef.current) {
|
||||||
|
if (speed === "fast") {
|
||||||
|
containerRef.current.style.setProperty("--animation-duration", "20s");
|
||||||
|
} else if (speed === "normal") {
|
||||||
|
containerRef.current.style.setProperty("--animation-duration", "40s");
|
||||||
|
} else {
|
||||||
|
containerRef.current.style.setProperty("--animation-duration", "80s");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [speed]);
|
||||||
|
|
||||||
|
const addAnimation = useCallback(() => {
|
||||||
|
if (containerRef.current && scrollerRef.current) {
|
||||||
|
const scrollerContent = Array.from(scrollerRef.current.children);
|
||||||
|
|
||||||
|
scrollerContent.forEach((item) => {
|
||||||
|
const duplicatedItem = item.cloneNode(true);
|
||||||
|
if (scrollerRef.current) {
|
||||||
|
scrollerRef.current.appendChild(duplicatedItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getDirection();
|
||||||
|
getSpeed();
|
||||||
|
setStart(true);
|
||||||
|
}
|
||||||
|
}, [getDirection, getSpeed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
addAnimation();
|
||||||
|
}, [addAnimation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={cn(
|
||||||
|
"scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={cn(
|
||||||
|
"flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap",
|
||||||
|
start && "animate-scroll",
|
||||||
|
pauseOnHover && "hover:[animation-play-state:paused]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<li
|
||||||
|
className="w-[350px] max-w-full relative rounded-2xl border border-b-0 flex-shrink-0 border-slate-700 px-8 py-6 md:w-[450px]"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(180deg, var(--slate-800), var(--slate-900))",
|
||||||
|
}}
|
||||||
|
key={idx}
|
||||||
|
>
|
||||||
|
<blockquote>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
|
||||||
|
></div>
|
||||||
|
<span className="relative z-20 text-sm leading-[1.6] text-gray-100 font-normal">
|
||||||
|
{item}
|
||||||
|
</span>
|
||||||
|
</blockquote>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
131
src/components/ui/Cube.tsx
Normal file
131
src/components/ui/Cube.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface CubeProps {
|
||||||
|
title: string;
|
||||||
|
descriptionTitle: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
index: number;
|
||||||
|
onHover: () => void;
|
||||||
|
onLeave: () => void;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CubeSvg: React.FC<React.SVGProps<SVGSVGElement> & { index: number }> = ({ index, ...props }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="507"
|
||||||
|
height="234"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 507 234"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={`url(#cube-gradient-${index})`}
|
||||||
|
d="M491.651 144.747L287.198 227.339C265.219 236.22 241.783 236.22 219.802 227.339L15.3486 144.747C-5.11621 136.479 -5.11621 97.5191 15.3486 89.2539L219.802 6.65884C241.783 -2.21961 265.219 -2.21961 287.198 6.65884L491.651 89.2539C512.116 97.5191 512.116 136.479 491.651 144.747Z"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id={`cube-gradient-${index}`}
|
||||||
|
x1="185.298"
|
||||||
|
x2="185.298"
|
||||||
|
y1="-27.5515"
|
||||||
|
y2="206.448"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop />
|
||||||
|
<stop offset="1" stopColor="#3F3B3E" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export function Cube({ title, descriptionTitle, description, isActive, index, onHover, onLeave, onClick }: CubeProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<motion.div
|
||||||
|
className="relative cursor-pointer"
|
||||||
|
onMouseEnter={onHover}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
zIndex: 10 - index,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: isActive ? 1.05 : 1,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* SVG Cube */}
|
||||||
|
<CubeSvg
|
||||||
|
index={index}
|
||||||
|
className="w-48 sm:w-64 lg:w-80 h-auto drop-shadow-lg opacity-50"
|
||||||
|
style={{
|
||||||
|
filter: isActive ? 'brightness(1.2) drop-shadow(0 0 20px rgba(156, 163, 175, 0.5))' : 'brightness(0.9)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Title overlay */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<h3
|
||||||
|
className="text-white text-sm lg:text-base font-medium text-center px-4 drop-shadow-lg"
|
||||||
|
style={{
|
||||||
|
transform: 'rotate(0deg) skewX(0deg)',
|
||||||
|
transformOrigin: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description with arrow line - Desktop */}
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="hidden lg:block absolute left-full top-1/2 -translate-y-1/2 z-50"
|
||||||
|
>
|
||||||
|
{/* Arrow line */}
|
||||||
|
<svg
|
||||||
|
className="absolute left-0 top-1/2 -translate-y-1/2"
|
||||||
|
width="120"
|
||||||
|
height="2"
|
||||||
|
viewBox="0 0 120 2"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="1"
|
||||||
|
x2="120"
|
||||||
|
y2="1"
|
||||||
|
stroke="white"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Description text */}
|
||||||
|
<div className="ml-32 w-80">
|
||||||
|
<h4 className="text-white text-base font-semibold mb-2">
|
||||||
|
{descriptionTitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-white text-sm leading-relaxed font-light">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description for Mobile - Below cube */}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/components/ui/CubeLight.tsx
Normal file
131
src/components/ui/CubeLight.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface CubeProps {
|
||||||
|
title: string;
|
||||||
|
descriptionTitle: string;
|
||||||
|
description: string;
|
||||||
|
isActive: boolean;
|
||||||
|
index: number;
|
||||||
|
onHover: () => void;
|
||||||
|
onLeave: () => void;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CubeSvg: React.FC<React.SVGProps<SVGSVGElement> & { index: number }> = ({ index, ...props }) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="507"
|
||||||
|
height="234"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 507 234"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill={`url(#cube-gradient-${index})`}
|
||||||
|
d="M491.651 144.747L287.198 227.339C265.219 236.22 241.783 236.22 219.802 227.339L15.3486 144.747C-5.11621 136.479 -5.11621 97.5191 15.3486 89.2539L219.802 6.65884C241.783 -2.21961 265.219 -2.21961 287.198 6.65884L491.651 89.2539C512.116 97.5191 512.116 136.479 491.651 144.747Z"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id={`cube-gradient-${index}`}
|
||||||
|
x1="185.298"
|
||||||
|
x2="185.298"
|
||||||
|
y1="-27.5515"
|
||||||
|
y2="206.448"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stopColor="#E5E7EB" />
|
||||||
|
<stop offset="1" stopColor="#9CA3AF" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export function CubeLight({ title, descriptionTitle, description, isActive, index, onHover, onLeave, onClick }: CubeProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<motion.div
|
||||||
|
className="relative cursor-pointer"
|
||||||
|
onMouseEnter={onHover}
|
||||||
|
onMouseLeave={onLeave}
|
||||||
|
onClick={onClick}
|
||||||
|
style={{
|
||||||
|
zIndex: 10 - index,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
scale: isActive ? 1.05 : 1,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 0.3,
|
||||||
|
ease: "easeOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* SVG Cube */}
|
||||||
|
<CubeSvg
|
||||||
|
index={index}
|
||||||
|
className="w-48 sm:w-64 lg:w-80 h-auto drop-shadow-lg opacity-80"
|
||||||
|
style={{
|
||||||
|
filter: isActive ? 'brightness(1.1) drop-shadow(0 0 15px rgba(0, 0, 0, 0.2))' : 'brightness(1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Title overlay */}
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<h3
|
||||||
|
className="text-black text-sm lg:text-base font-medium text-center px-4 drop-shadow-sm"
|
||||||
|
style={{
|
||||||
|
transform: 'rotate(0deg) skewX(0deg)',
|
||||||
|
transformOrigin: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description with arrow line - Desktop */}
|
||||||
|
{isActive && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="hidden lg:block absolute left-full top-1/2 -translate-y-1/2 z-50"
|
||||||
|
>
|
||||||
|
{/* Arrow line */}
|
||||||
|
<svg
|
||||||
|
className="absolute left-0 top-1/2 -translate-y-1/2"
|
||||||
|
width="120"
|
||||||
|
height="2"
|
||||||
|
viewBox="0 0 120 2"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
x1="0"
|
||||||
|
y1="1"
|
||||||
|
x2="120"
|
||||||
|
y2="1"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="1"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Description text */}
|
||||||
|
<div className="ml-32 w-80">
|
||||||
|
<h4 className="text-black text-base font-semibold mb-2">
|
||||||
|
{descriptionTitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-800 text-sm leading-relaxed font-light">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description for Mobile - Below cube */}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/components/ui/ScrollDown.tsx
Normal file
22
src/components/ui/ScrollDown.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ChevronDoubleDownIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useScroll } from '@/hooks/useScroll'
|
||||||
|
|
||||||
|
export function ScrollDown() {
|
||||||
|
const { isAtBottom, scrollToNext } = useScroll()
|
||||||
|
|
||||||
|
if (isAtBottom) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={scrollToNext}
|
||||||
|
className="fixed bottom-8 right-8 z-50 flex items-center gap-x-2 text-2xl font-medium text-white lg:text-3xl animate-blink"
|
||||||
|
>
|
||||||
|
<span>scroll</span>
|
||||||
|
<ChevronDoubleDownIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
src/components/ui/ScrollUp.tsx
Normal file
22
src/components/ui/ScrollUp.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { ChevronDoubleUpIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useScroll } from '@/hooks/useScroll'
|
||||||
|
|
||||||
|
export function ScrollUp() {
|
||||||
|
const { isAtBottom, scrollToTop } = useScroll()
|
||||||
|
|
||||||
|
if (!isAtBottom) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
className="fixed bottom-8 right-8 z-50 flex items-center gap-x-2 text-2xl font-medium text-[#1c1c49] lg:text-3xl animate-blink"
|
||||||
|
>
|
||||||
|
<span>top</span>
|
||||||
|
<ChevronDoubleUpIcon className="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
95
src/components/ui/StackedCubes.tsx
Normal file
95
src/components/ui/StackedCubes.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Cube } from "@/components/ui/Cube"
|
||||||
|
|
||||||
|
const stackData = [
|
||||||
|
{
|
||||||
|
id: "agent",
|
||||||
|
title: "Agent Layer",
|
||||||
|
descriptionTitle: "Your sovereign agent with private memory and permissioned data access—always under your control.",
|
||||||
|
description:
|
||||||
|
"Choose from a wide library of open-source LLMs, paired with built-in semantic search and retrieval.\nIt coordinates across people, apps, and other agents to plan, create, and execute.\nIt operates inside a compliant legal & financial sandbox, ready for real-world transactions and operations.\nMore than just an assistant—an intelligent partner that learns and does your way.",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "network",
|
||||||
|
title: "Network Layer",
|
||||||
|
descriptionTitle: "A global, end-to-end encrypted overlay that simply doesn’t break.",
|
||||||
|
description:
|
||||||
|
"Shortest-path routing moves your traffic the fastest way, every time.\nInstant discovery with integrated DNS, semantic search, and indexing.\nA distributed CDN and edge delivery keep content available and tamper-resistant worldwide.\nBuilt-in tool services and secure coding sandboxes—seamless on phones, desktops, and edge.",
|
||||||
|
position: "middle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cloud",
|
||||||
|
title: "Cloud Layer",
|
||||||
|
descriptionTitle: "An autonomous, stateless OS that enforces pre-deterministic deployments you define.",
|
||||||
|
description:
|
||||||
|
"Workloads are cryptographically bound to your private key—location and access are yours.\nNo cloud vendor or middleman in the path: end-to-end ownership and isolation by default.\nGeo-aware placement delivers locality, compliance, and ultra-low latency where it matters.\nEncrypted, erasure-coded storage, decentralized compute and GPU on demand—including LLMs.",
|
||||||
|
position: "bottom",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function StackedCubes() {
|
||||||
|
const [active, setActive] = useState<string | null>("agent");
|
||||||
|
const [selectedForMobile, setSelectedForMobile] = useState<string | null>("agent");
|
||||||
|
|
||||||
|
const handleCubeClick = (id: string) => {
|
||||||
|
setSelectedForMobile(prev => (prev === id ? null : id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMobileLayer = stackData.find(layer => layer.id === selectedForMobile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className="relative w-full flex items-center justify-center lg:justify-center min-h-[450px] lg:min-h-[400px]"
|
||||||
|
onMouseLeave={() => setActive("agent")}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative lg:pl-0 pl-6 h-[300px] lg:h-[400px] w-64 sm:w-80 lg:w-96 scale-120 lg:scale-100"
|
||||||
|
animate={{ y: ["-8px", "8px"] }}
|
||||||
|
transition={{
|
||||||
|
duration: 4,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stackData.map((layer, index) => (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: `calc(${index * 30}% - ${index * 10}px)`,
|
||||||
|
zIndex: active === layer.id ? 20 : 10 - index,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Cube
|
||||||
|
title={layer.title}
|
||||||
|
descriptionTitle={layer.descriptionTitle}
|
||||||
|
description={layer.description}
|
||||||
|
isActive={active === layer.id}
|
||||||
|
index={index}
|
||||||
|
onHover={() => setActive(layer.id)}
|
||||||
|
onLeave={() => {}}
|
||||||
|
onClick={() => handleCubeClick(layer.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
{selectedMobileLayer && (
|
||||||
|
<div className="lg:hidden w-full max-w-md p-6 -mt-8 bg-gray-800/50 rounded-lg">
|
||||||
|
<h4 className="text-white text-lg font-semibold mb-2 text-center">
|
||||||
|
{selectedMobileLayer.descriptionTitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-300 text-sm leading-relaxed text-center">
|
||||||
|
{selectedMobileLayer.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/components/ui/StackedCubesLight.tsx
Normal file
95
src/components/ui/StackedCubesLight.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { CubeLight } from "@/components/ui/CubeLight"
|
||||||
|
|
||||||
|
const stackData = [
|
||||||
|
{
|
||||||
|
id: "agent",
|
||||||
|
title: "Agent Layer",
|
||||||
|
descriptionTitle: "Your sovereign agent with private memory and permissioned data access—always under your control.",
|
||||||
|
description:
|
||||||
|
"Choose from a wide library of open-source LLMs, paired with built-in semantic search and retrieval.\nIt coordinates across people, apps, and other agents to plan, create, and execute.\nIt operates inside a compliant legal & financial sandbox, ready for real-world transactions and operations.\nMore than just an assistant—an intelligent partner that learns and does your way.",
|
||||||
|
position: "top",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "network",
|
||||||
|
title: "Network Layer",
|
||||||
|
descriptionTitle: "A global, end-to-end encrypted overlay that simply doesn’t break.",
|
||||||
|
description:
|
||||||
|
"Shortest-path routing moves your traffic the fastest way, every time.\nInstant discovery with integrated DNS, semantic search, and indexing.\nA distributed CDN and edge delivery keep content available and tamper-resistant worldwide.\nBuilt-in tool services and secure coding sandboxes—seamless on phones, desktops, and edge.",
|
||||||
|
position: "middle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "cloud",
|
||||||
|
title: "Cloud Layer",
|
||||||
|
descriptionTitle: "An autonomous, stateless OS that enforces pre-deterministic deployments you define.",
|
||||||
|
description:
|
||||||
|
"Workloads are cryptographically bound to your private key—location and access are yours.\nNo cloud vendor or middleman in the path: end-to-end ownership and isolation by default.\nGeo-aware placement delivers locality, compliance, and ultra-low latency where it matters.\nEncrypted, erasure-coded storage, decentralized compute and GPU on demand—including LLMs.",
|
||||||
|
position: "bottom",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function StackedCubesLight() {
|
||||||
|
const [active, setActive] = useState<string | null>("agent");
|
||||||
|
const [selectedForMobile, setSelectedForMobile] = useState<string | null>("agent");
|
||||||
|
|
||||||
|
const handleCubeClick = (id: string) => {
|
||||||
|
setSelectedForMobile(prev => (prev === id ? null : id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedMobileLayer = stackData.find(layer => layer.id === selectedForMobile);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
className="relative w-full flex items-center justify-center lg:justify-center min-h-[450px] lg:min-h-[400px]"
|
||||||
|
onMouseLeave={() => setActive("agent")}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="relative lg:pl-0 pl-6 h-[300px] lg:h-[400px] w-64 sm:w-80 lg:w-96 scale-120 lg:scale-100"
|
||||||
|
animate={{ y: ["-8px", "8px"] }}
|
||||||
|
transition={{
|
||||||
|
duration: 4,
|
||||||
|
repeat: Infinity,
|
||||||
|
repeatType: "reverse",
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stackData.map((layer, index) => (
|
||||||
|
<div
|
||||||
|
key={layer.id}
|
||||||
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top: `calc(${index * 30}% - ${index * 10}px)`,
|
||||||
|
zIndex: active === layer.id ? 20 : 10 - index,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CubeLight
|
||||||
|
title={layer.title}
|
||||||
|
descriptionTitle={layer.descriptionTitle}
|
||||||
|
description={layer.description}
|
||||||
|
isActive={active === layer.id}
|
||||||
|
index={index}
|
||||||
|
onHover={() => setActive(layer.id)}
|
||||||
|
onLeave={() => {}}
|
||||||
|
onClick={() => handleCubeClick(layer.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
{selectedMobileLayer && (
|
||||||
|
<div className="lg:hidden w-full max-w-md p-6 -mt-8 bg-gray-200/50 rounded-lg">
|
||||||
|
<h4 className="text-black text-lg font-semibold mb-2 text-center">
|
||||||
|
{selectedMobileLayer.descriptionTitle}
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-700 text-sm leading-relaxed text-center">
|
||||||
|
{selectedMobileLayer.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
src/components/ui/bento-grid.tsx
Normal file
79
src/components/ui/bento-grid.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CT, CP } from "@/components/Texts";
|
||||||
|
import Image from 'next/image';
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
export const BentoGrid = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-4 grid max-w-6xl grid-cols-1 gap-4 lg:grid-cols-3",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BentoGridItemProps {
|
||||||
|
className?: string;
|
||||||
|
title?: string | React.ReactNode;
|
||||||
|
subtitle?: string | React.ReactNode;
|
||||||
|
description?: string | React.ReactNode;
|
||||||
|
img?: string;
|
||||||
|
video?: string;
|
||||||
|
rowHeight?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BentoGridItem = React.forwardRef<HTMLDivElement, BentoGridItemProps>(
|
||||||
|
({ className, title, subtitle, description, img, video, rowHeight }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"group/bento shadow-input row-span-1 flex flex-col justify-between rounded-xl border border-black bg-black/10 backdrop-blur-md transition-all duration-300 ease-in-out hover:scale-105 hover:border-black hover:bg-black/40",
|
||||||
|
rowHeight ? rowHeight : "h-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative w-full h-[65%] min-h-[6rem] bg-transparent overflow-hidden">
|
||||||
|
{video ? (
|
||||||
|
<video
|
||||||
|
src={video}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="w-full h-full object-cover opacity-90 group-hover/bento:opacity-100 transition-opacity duration-300"
|
||||||
|
/>
|
||||||
|
) : img ? (
|
||||||
|
<Image
|
||||||
|
src={img}
|
||||||
|
alt={title as string}
|
||||||
|
width={300}
|
||||||
|
height={300}
|
||||||
|
className="w-full h-full object-cover opacity-90 group-hover/bento:opacity-100 transition-opacity duration-300"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="p-4 transition bg-white/5 hover:bg-white/7 backdrop-blur-md duration-200 group-hover/bento:translate-x-2 ">
|
||||||
|
<CT>{title}</CT>
|
||||||
|
<CP className="font-medium">{subtitle}</CP>
|
||||||
|
<CP className="mt-2">{description}</CP>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
BentoGridItem.displayName = "BentoGridItem";
|
||||||
|
|
||||||
|
export const MotionBentoGridItem = motion(BentoGridItem);
|
||||||
308
src/components/ui/dotted-glow-background.tsx
Normal file
308
src/components/ui/dotted-glow-background.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
type DottedGlowBackgroundProps = {
|
||||||
|
className?: string;
|
||||||
|
/** distance between dot centers in pixels */
|
||||||
|
gap?: number;
|
||||||
|
/** base radius of each dot in CSS px */
|
||||||
|
radius?: number;
|
||||||
|
/** dot color (will pulse by alpha) */
|
||||||
|
color?: string;
|
||||||
|
/** optional dot color for dark mode */
|
||||||
|
darkColor?: string;
|
||||||
|
/** shadow/glow color for bright dots */
|
||||||
|
glowColor?: string;
|
||||||
|
/** optional glow color for dark mode */
|
||||||
|
darkGlowColor?: string;
|
||||||
|
/** optional CSS variable name for light dot color (e.g. --color-zinc-900) */
|
||||||
|
colorLightVar?: string;
|
||||||
|
/** optional CSS variable name for dark dot color (e.g. --color-zinc-100) */
|
||||||
|
colorDarkVar?: string;
|
||||||
|
/** optional CSS variable name for light glow color */
|
||||||
|
glowColorLightVar?: string;
|
||||||
|
/** optional CSS variable name for dark glow color */
|
||||||
|
glowColorDarkVar?: string;
|
||||||
|
/** global opacity for the whole layer */
|
||||||
|
opacity?: number;
|
||||||
|
/** background radial fade opacity (0 = transparent background) */
|
||||||
|
backgroundOpacity?: number;
|
||||||
|
/** minimum per-dot speed in rad/s */
|
||||||
|
speedMin?: number;
|
||||||
|
/** maximum per-dot speed in rad/s */
|
||||||
|
speedMax?: number;
|
||||||
|
/** global speed multiplier for all dots */
|
||||||
|
speedScale?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas-based dotted background that randomly glows and dims.
|
||||||
|
* - Uses a stable grid of dots.
|
||||||
|
* - Each dot gets its own phase + speed producing organic shimmering.
|
||||||
|
* - Handles high-DPI and resizes via ResizeObserver.
|
||||||
|
*/
|
||||||
|
export function DottedGlowBackground({
|
||||||
|
className,
|
||||||
|
gap = 12,
|
||||||
|
radius = 2,
|
||||||
|
color = "rgba(0,0,0,0.7)",
|
||||||
|
darkColor,
|
||||||
|
glowColor = "rgba(0, 170, 255, 0.85)",
|
||||||
|
darkGlowColor,
|
||||||
|
colorLightVar,
|
||||||
|
colorDarkVar,
|
||||||
|
glowColorLightVar,
|
||||||
|
glowColorDarkVar,
|
||||||
|
opacity = 0.6,
|
||||||
|
backgroundOpacity = 0,
|
||||||
|
speedMin = 0.4,
|
||||||
|
speedMax = 1.3,
|
||||||
|
speedScale = 1,
|
||||||
|
}: DottedGlowBackgroundProps) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [resolvedColor, setResolvedColor] = useState<string>(color);
|
||||||
|
const [resolvedGlowColor, setResolvedGlowColor] = useState<string>(glowColor);
|
||||||
|
|
||||||
|
// Resolve CSS variable value from the container or root
|
||||||
|
const resolveCssVariable = (
|
||||||
|
el: Element,
|
||||||
|
variableName?: string,
|
||||||
|
): string | null => {
|
||||||
|
if (!variableName) return null;
|
||||||
|
const normalized = variableName.startsWith("--")
|
||||||
|
? variableName
|
||||||
|
: `--${variableName}`;
|
||||||
|
const fromEl = getComputedStyle(el as Element)
|
||||||
|
.getPropertyValue(normalized)
|
||||||
|
.trim();
|
||||||
|
if (fromEl) return fromEl;
|
||||||
|
const root = document.documentElement;
|
||||||
|
const fromRoot = getComputedStyle(root).getPropertyValue(normalized).trim();
|
||||||
|
return fromRoot || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const detectDarkMode = (): boolean => {
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (root.classList.contains("dark")) return true;
|
||||||
|
if (root.classList.contains("light")) return false;
|
||||||
|
return (
|
||||||
|
window.matchMedia &&
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keep resolved colors in sync with theme changes and prop updates
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current ?? document.documentElement;
|
||||||
|
|
||||||
|
const compute = () => {
|
||||||
|
const isDark = detectDarkMode();
|
||||||
|
|
||||||
|
let nextColor: string = color;
|
||||||
|
let nextGlow: string = glowColor;
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
const varDot = resolveCssVariable(container, colorDarkVar);
|
||||||
|
const varGlow = resolveCssVariable(container, glowColorDarkVar);
|
||||||
|
nextColor = varDot || darkColor || nextColor;
|
||||||
|
nextGlow = varGlow || darkGlowColor || nextGlow;
|
||||||
|
} else {
|
||||||
|
const varDot = resolveCssVariable(container, colorLightVar);
|
||||||
|
const varGlow = resolveCssVariable(container, glowColorLightVar);
|
||||||
|
nextColor = varDot || nextColor;
|
||||||
|
nextGlow = varGlow || nextGlow;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedColor(nextColor);
|
||||||
|
setResolvedGlowColor(nextGlow);
|
||||||
|
};
|
||||||
|
|
||||||
|
compute();
|
||||||
|
|
||||||
|
const mql = window.matchMedia
|
||||||
|
? window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
: null;
|
||||||
|
const handleMql = () => compute();
|
||||||
|
mql?.addEventListener?.("change", handleMql);
|
||||||
|
|
||||||
|
const mo = new MutationObserver(() => compute());
|
||||||
|
mo.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class", "style"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mql?.removeEventListener?.("change", handleMql);
|
||||||
|
mo.disconnect();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
color,
|
||||||
|
darkColor,
|
||||||
|
glowColor,
|
||||||
|
darkGlowColor,
|
||||||
|
colorLightVar,
|
||||||
|
colorDarkVar,
|
||||||
|
glowColorLightVar,
|
||||||
|
glowColorDarkVar,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = canvasRef.current;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!el || !container) return;
|
||||||
|
|
||||||
|
const ctx = el.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
let raf = 0;
|
||||||
|
let stopped = false;
|
||||||
|
|
||||||
|
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
el.width = Math.max(1, Math.floor(width * dpr));
|
||||||
|
el.height = Math.max(1, Math.floor(height * dpr));
|
||||||
|
el.style.width = `${Math.floor(width)}px`;
|
||||||
|
el.style.height = `${Math.floor(height)}px`;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ro = new ResizeObserver(resize);
|
||||||
|
ro.observe(container);
|
||||||
|
resize();
|
||||||
|
|
||||||
|
// Precompute dot metadata for a medium-sized grid and regenerate on resize
|
||||||
|
let dots: { x: number; y: number; phase: number; speed: number }[] = [];
|
||||||
|
|
||||||
|
const regenDots = () => {
|
||||||
|
dots = [];
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
const cols = Math.ceil(width / gap) + 2;
|
||||||
|
const rows = Math.ceil(height / gap) + 2;
|
||||||
|
const min = Math.min(speedMin, speedMax);
|
||||||
|
const max = Math.max(speedMin, speedMax);
|
||||||
|
for (let i = -1; i < cols; i++) {
|
||||||
|
for (let j = -1; j < rows; j++) {
|
||||||
|
const x = i * gap + (j % 2 === 0 ? 0 : gap * 0.5); // offset every other row
|
||||||
|
const y = j * gap;
|
||||||
|
// Randomize phase and speed slightly per dot
|
||||||
|
const phase = Math.random() * Math.PI * 2;
|
||||||
|
const span = Math.max(max - min, 0);
|
||||||
|
const speed = min + Math.random() * span; // configurable rad/s
|
||||||
|
dots.push({ x, y, phase, speed });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenThrottled = () => {
|
||||||
|
regenDots();
|
||||||
|
};
|
||||||
|
|
||||||
|
regenDots();
|
||||||
|
|
||||||
|
let last = performance.now();
|
||||||
|
|
||||||
|
const draw = (now: number) => {
|
||||||
|
if (stopped) return;
|
||||||
|
const dt = (now - last) / 1000; // seconds
|
||||||
|
last = now;
|
||||||
|
const { width, height } = container.getBoundingClientRect();
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, el.width, el.height);
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
|
||||||
|
// optional subtle background fade for depth (defaults to 0 = transparent)
|
||||||
|
if (backgroundOpacity > 0) {
|
||||||
|
const grad = ctx.createRadialGradient(
|
||||||
|
width * 0.5,
|
||||||
|
height * 0.4,
|
||||||
|
Math.min(width, height) * 0.1,
|
||||||
|
width * 0.5,
|
||||||
|
height * 0.5,
|
||||||
|
Math.max(width, height) * 0.7,
|
||||||
|
);
|
||||||
|
grad.addColorStop(0, "rgba(0,0,0,0)");
|
||||||
|
grad.addColorStop(
|
||||||
|
1,
|
||||||
|
`rgba(0,0,0,${Math.min(Math.max(backgroundOpacity, 0), 1)})`,
|
||||||
|
);
|
||||||
|
ctx.fillStyle = grad as unknown as CanvasGradient;
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// animate dots
|
||||||
|
ctx.save();
|
||||||
|
ctx.fillStyle = resolvedColor;
|
||||||
|
|
||||||
|
const time = (now / 1000) * Math.max(speedScale, 0);
|
||||||
|
for (let i = 0; i < dots.length; i++) {
|
||||||
|
const d = dots[i];
|
||||||
|
// Linear triangle wave 0..1..0 for linear glow/dim
|
||||||
|
const mod = (time * d.speed + d.phase) % 2;
|
||||||
|
const lin = mod < 1 ? mod : 2 - mod; // 0..1..0
|
||||||
|
const a = 0.25 + 0.55 * lin; // 0.25..0.8 linearly
|
||||||
|
|
||||||
|
// draw glow when bright
|
||||||
|
if (a > 0.6) {
|
||||||
|
const glow = (a - 0.6) / 0.4; // 0..1
|
||||||
|
ctx.shadowColor = resolvedGlowColor;
|
||||||
|
ctx.shadowBlur = 6 * glow;
|
||||||
|
} else {
|
||||||
|
ctx.shadowColor = "transparent";
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = a * opacity;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(d.x, d.y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
raf = requestAnimationFrame(draw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
resize();
|
||||||
|
regenThrottled();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
raf = requestAnimationFrame(draw);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
stopped = true;
|
||||||
|
cancelAnimationFrame(raf);
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
ro.disconnect();
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
gap,
|
||||||
|
radius,
|
||||||
|
resolvedColor,
|
||||||
|
resolvedGlowColor,
|
||||||
|
opacity,
|
||||||
|
backgroundOpacity,
|
||||||
|
speedMin,
|
||||||
|
speedMax,
|
||||||
|
speedScale,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
style={{ position: "absolute", inset: 0 }}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
style={{ display: "block", width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DottedGlowBackground;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AnimatedSection } from '../../components/AnimatedSection'
|
import { AnimatedSection } from '../../components/AnimatedSection'
|
||||||
import { DeploySection } from './DeploySection'
|
import { DeploySection } from './DeploySection'
|
||||||
import { GallerySection } from './GallerySection'
|
import { GallerySection } from './GallerySection'
|
||||||
|
import { Companies } from './Companies'
|
||||||
import { BentoSection } from './BentoSection'
|
import { BentoSection } from './BentoSection'
|
||||||
|
|
||||||
export default function AgentsPage() {
|
export default function AgentsPage() {
|
||||||
@@ -9,6 +10,11 @@ export default function AgentsPage() {
|
|||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<DeploySection />
|
<DeploySection />
|
||||||
</AnimatedSection>
|
</AnimatedSection>
|
||||||
|
|
||||||
|
<AnimatedSection>
|
||||||
|
<Companies />
|
||||||
|
</AnimatedSection>
|
||||||
|
|
||||||
|
|
||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<GallerySection />
|
<GallerySection />
|
||||||
|
|||||||
76
src/pages/agents/Companies.tsx
Normal file
76
src/pages/agents/Companies.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { P, Eyebrow } from '@/components/Texts';
|
||||||
|
import { InfiniteMovingCards } from "@/components/magicui/infinite-moving-cards";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import Ai21 from '@/components/logos/Ai21';
|
||||||
|
import Claude from '@/components/logos/Claude';
|
||||||
|
import BaiduCloud from '@/components/logos/BaiduCloud';
|
||||||
|
import ByteDance from '@/components/logos/ByteDance';
|
||||||
|
import DeepSeek from '@/components/logos/DeepSeek';
|
||||||
|
import DeepMind from '@/components/logos/DeepMind';
|
||||||
|
import Minimax from '@/components/logos/Minimax';
|
||||||
|
import Mistral from '@/components/logos/Mistral';
|
||||||
|
import Moonshot from '@/components/logos/Moonshot';
|
||||||
|
import TencentCloud from '@/components/logos/TencentCloud';
|
||||||
|
import OpenAI from '@/components/logos/OpenAI';
|
||||||
|
import XAI from '@/components/logos/XAI';
|
||||||
|
|
||||||
|
const logos = [
|
||||||
|
<Ai21 key="ai21" />,
|
||||||
|
<Claude key="claude" />,
|
||||||
|
<BaiduCloud key="baidu" />,
|
||||||
|
<ByteDance key="bytedance" />,
|
||||||
|
<DeepSeek key="deepseek" />,
|
||||||
|
<DeepMind key="deepmind" />,
|
||||||
|
<Minimax key="minimax" />,
|
||||||
|
<Mistral key="mistral" />,
|
||||||
|
<Moonshot key="moonshot" />,
|
||||||
|
<TencentCloud key="tencent" />,
|
||||||
|
<OpenAI key="openai" />,
|
||||||
|
<XAI key="xai" />,
|
||||||
|
];
|
||||||
|
|
||||||
|
const row1 = logos.slice(0, 6);
|
||||||
|
const row2 = logos.slice(6);
|
||||||
|
|
||||||
|
export function Companies() {
|
||||||
|
return (
|
||||||
|
<div id="companies" className="relative bg-black flex flex-col items-center justify-center w-full overflow-hidden antialiased py-4 mb-12">
|
||||||
|
<div className="relative z-10 mx-auto w-full max-w-6xl p-4">
|
||||||
|
|
||||||
|
{/* Heading */}
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col justify-center max-w-4xl items-center mb-8 mx-auto"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1 }}
|
||||||
|
>
|
||||||
|
<Eyebrow color="accent"></Eyebrow>
|
||||||
|
<P className="hidden min-xl:text-gray-100 text-center mb-6">
|
||||||
|
Mycelium Cloud allows you to deploy and scale AI agents from top global providers on a decentralized, privacy-first infrastructure.
|
||||||
|
</P>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Logos grid */}
|
||||||
|
<div className="flex flex-col items-center gap-y-6 text-white">
|
||||||
|
<InfiniteMovingCards
|
||||||
|
items={row1}
|
||||||
|
direction="right"
|
||||||
|
speed="slow"
|
||||||
|
/>
|
||||||
|
<InfiniteMovingCards
|
||||||
|
className=""
|
||||||
|
items={row2}
|
||||||
|
direction="left"
|
||||||
|
speed="slow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user