finish project
@ -1,9 +1,20 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
images: {
|
||||
remotePatterns: [{ hostname: "localhost" }, { hostname: "randomuser.me" }],
|
||||
remotePatterns: [
|
||||
{ hostname: "localhost" },
|
||||
{ hostname: "randomuser.me" },
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "minio-s3.dev3vds1.link",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "supabasekong-swwwcowsg8kso0cg4swosw48.dev3vds1.link",
|
||||
},
|
||||
],
|
||||
},
|
||||
transpilePackages: ["geist"],
|
||||
// transpilePackages: ["geist"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
4849
package-lock.json
generated
26
package.json
@ -9,22 +9,36 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@privy-io/react-auth": "^2.2.1",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.5",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@splinetool/react-spline": "^4.0.0",
|
||||
"@splinetool/runtime": "^1.9.37",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"@stepperize/react": "^5.0.1",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@tanstack/react-query": "^5.65.1",
|
||||
"@tanstack/react-query-devtools": "^5.65.1",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"axios": "^1.7.9",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^11.3.21",
|
||||
"geist": "^1.3.1",
|
||||
"lucide-react": "^0.417.0",
|
||||
"motion": "^12.0.11",
|
||||
"next": "15.0.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"nuqs": "^2.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-icons": "^5.2.1",
|
||||
"rehype-pretty-code": "^0.14.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
@ -32,11 +46,13 @@
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"shiki": "^1.22.0",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unified": "^11.0.5",
|
||||
"usehooks-ts": "^3.1.0",
|
||||
"vaul": "^0.9.1"
|
||||
"vaul": "^0.9.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
|
BIN
public/agent.png
Normal file
After Width: | Height: | Size: 667 KiB |
BIN
public/background-1.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
public/background-2.png
Normal file
After Width: | Height: | Size: 259 KiB |
BIN
public/background-3.png
Normal file
After Width: | Height: | Size: 285 KiB |
BIN
public/background-4.png
Normal file
After Width: | Height: | Size: 804 KiB |
BIN
public/background-5.png
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
public/empty-data.jpg
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
public/logo.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
public/marquee-dot.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
@ -10,7 +10,7 @@ export async function Blog() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Section id="blog" title="Blog">
|
||||
<Section id="blog" >
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 border border-b-0">
|
||||
{articles.map((data, idx) => (
|
||||
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
|
37
src/app/(main)/_components/cta.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { Rocket } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import BlurFade from "@/components/blur-fade";
|
||||
|
||||
export function CTA() {
|
||||
return (
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6">
|
||||
<BlurFade
|
||||
inView
|
||||
className="flex flex-col items-center justify-center gap-8 py-20"
|
||||
>
|
||||
<h3 className="text-center mx-auto mt-4 text-2xl font-semibold sm:text-3xl md:text-4xl">
|
||||
Join us and try our Agentic AI in your operations
|
||||
</h3>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL}/explore`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Rocket className="h-6 w-6" />
|
||||
Get started
|
||||
</Link>
|
||||
</BlurFade>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
102
src/app/(main)/_components/features.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { BrainCog, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import BlurFade from "@/components/blur-fade";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
|
||||
const FEATURE_LIST = [
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="32"
|
||||
height="29"
|
||||
viewBox="0 0 32 29"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M31.4375 26.875C31.4375 27.1899 31.3124 27.492 31.0897 27.7147C30.867 27.9374 30.5649 28.0625 30.25 28.0625H1.75C1.43506 28.0625 1.13301 27.9374 0.910311 27.7147C0.687611 27.492 0.5625 27.1899 0.5625 26.875C0.5625 26.5601 0.687611 26.258 0.910311 26.0353C1.13301 25.8126 1.43506 25.6875 1.75 25.6875H2.9375V16.1875C2.9375 15.8726 3.06261 15.5705 3.28531 15.3478C3.50801 15.1251 3.81006 15 4.125 15H7.6875C8.00244 15 8.30449 15.1251 8.52719 15.3478C8.74989 15.5705 8.875 15.8726 8.875 16.1875V25.6875H11.25V9.0625C11.25 8.74756 11.3751 8.44551 11.5978 8.22281C11.8205 8.00011 12.1226 7.875 12.4375 7.875H17.1875C17.5024 7.875 17.8045 8.00011 18.0272 8.22281C18.2499 8.44551 18.375 8.74756 18.375 9.0625V25.6875H20.75V1.9375C20.75 1.62256 20.8751 1.32051 21.0978 1.09781C21.3205 0.875111 21.6226 0.75 21.9375 0.75H27.875C28.1899 0.75 28.492 0.875111 28.7147 1.09781C28.9374 1.32051 29.0625 1.62256 29.0625 1.9375V25.6875H30.25C30.5649 25.6875 30.867 25.8126 31.0897 26.0353C31.3124 26.258 31.4375 26.5601 31.4375 26.875Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "Dynamic Goal Formation",
|
||||
desc: "Unlike traditional AI systems that rely on predefined objectives, our agents can formulate and prioritize their own goals based on their understanding of the broader context and desired outcomes.",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="31"
|
||||
height="32"
|
||||
viewBox="0 0 31 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M30.7 29.536C30.8327 29.7125 30.9137 29.9224 30.9337 30.1423C30.9538 30.3621 30.9121 30.5833 30.8134 30.7808C30.7147 30.9783 30.563 31.1444 30.3751 31.2604C30.1873 31.3764 29.9708 31.4377 29.75 31.4375H1.25C1.02947 31.4375 0.813291 31.3761 0.625695 31.2602C0.438098 31.1442 0.286493 30.9783 0.187868 30.7811C0.0892428 30.5838 0.0474938 30.363 0.0672989 30.1434C0.087104 29.9237 0.167681 29.714 0.300001 29.5375C1.35151 28.1278 2.75371 27.0178 4.36719 26.3179C3.48276 25.5107 2.86306 24.455 2.58943 23.2892C2.31579 22.1235 2.401 20.9023 2.83387 19.7859C3.26675 18.6695 4.02707 17.71 5.01502 17.0335C6.00297 16.3569 7.1724 15.9949 8.3698 15.9949C9.56721 15.9949 10.7366 16.3569 11.7246 17.0335C12.7125 17.71 13.4729 18.6695 13.9057 19.7859C14.3386 20.9023 14.4238 22.1235 14.1502 23.2892C13.8765 24.455 13.2568 25.5107 12.3724 26.3179C13.5365 26.8211 14.5943 27.5409 15.4896 28.4391C16.3849 27.5409 17.4427 26.8211 18.6068 26.3179C17.7224 25.5107 17.1027 24.455 16.829 23.2892C16.5554 22.1235 16.6406 20.9023 17.0735 19.7859C17.5064 18.6695 18.2667 17.71 19.2546 17.0335C20.2426 16.3569 21.412 15.9949 22.6094 15.9949C23.8068 15.9949 24.9762 16.3569 25.9642 17.0335C26.9522 17.71 27.7125 18.6695 28.1453 19.7859C28.5782 20.9023 28.6634 22.1235 28.3898 23.2892C28.1162 24.455 27.4965 25.5107 26.612 26.3179C28.233 27.0141 29.6427 28.1238 30.7 29.536ZM0.537501 15.7625C0.662256 15.8561 0.80422 15.9242 0.955286 15.9629C1.10635 16.0016 1.26356 16.0101 1.41794 15.9881C1.57232 15.966 1.72084 15.9138 1.85502 15.8343C1.98921 15.7549 2.10643 15.6498 2.2 15.525C2.91898 14.5664 3.85128 13.7883 4.92307 13.2524C5.99486 12.7165 7.1767 12.4375 8.375 12.4375C9.5733 12.4375 10.7551 12.7165 11.8269 13.2524C12.8987 13.7883 13.831 14.5664 14.55 15.525C14.6606 15.6725 14.804 15.7922 14.9689 15.8747C15.1338 15.9571 15.3156 16 15.5 16C15.6844 16 15.8662 15.9571 16.0311 15.8747C16.196 15.7922 16.3394 15.6725 16.45 15.525C17.169 14.5664 18.1013 13.7883 19.1731 13.2524C20.2449 12.7165 21.4267 12.4375 22.625 12.4375C23.8233 12.4375 25.0051 12.7165 26.0769 13.2524C27.1487 13.7883 28.081 14.5664 28.8 15.525C28.8937 15.6498 29.011 15.7549 29.1453 15.8343C29.2795 15.9137 29.4281 15.9659 29.5826 15.9879C29.737 16.0099 29.8943 16.0012 30.0454 15.9624C30.1965 15.9236 30.3385 15.8554 30.4632 15.7618C30.588 15.6681 30.6931 15.5508 30.7725 15.4165C30.8519 15.2822 30.9041 15.1336 30.9261 14.9792C30.9481 14.8247 30.9394 14.6675 30.9006 14.5164C30.8618 14.3653 30.7937 14.2233 30.7 14.0985C29.6485 12.6892 28.2462 11.5797 26.6328 10.8804C27.5172 10.0732 28.1369 9.01747 28.4106 7.85175C28.6842 6.68603 28.599 5.46481 28.1661 4.34839C27.7332 3.23197 26.9729 2.27252 25.985 1.59596C24.997 0.919409 23.8276 0.557373 22.6302 0.557373C21.4328 0.557373 20.2634 0.919409 19.2754 1.59596C18.2875 2.27252 17.5271 3.23197 17.0943 4.34839C16.6614 5.46481 16.5762 6.68603 16.8498 7.85175C17.1235 9.01747 17.7432 10.0732 18.6276 10.8804C17.4635 11.3836 16.4057 12.1034 15.5104 13.0016C14.6151 12.1034 13.5573 11.3836 12.3932 10.8804C13.2776 10.0732 13.8973 9.01747 14.171 7.85175C14.4446 6.68603 14.3594 5.46481 13.9265 4.34839C13.4936 3.23197 12.7333 2.27252 11.7454 1.59596C10.7574 0.919409 9.58799 0.557373 8.39059 0.557373C7.19318 0.557373 6.02376 0.919409 5.0358 1.59596C4.04785 2.27252 3.28753 3.23197 2.85466 4.34839C2.42178 5.46481 2.33657 6.68603 2.61021 7.85175C2.88384 9.01747 3.50355 10.0732 4.38797 10.8804C2.76692 11.5772 1.35725 12.6874 0.300001 14.1C0.206434 14.2248 0.138355 14.3667 0.0996529 14.5178C0.0609504 14.6689 0.0523816 14.8261 0.0744355 14.9805C0.0964895 15.1348 0.148734 15.2834 0.228187 15.4175C0.30764 15.5517 0.412745 15.669 0.537501 15.7625Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "Contextual Understanding",
|
||||
desc: "Unlike traditional AI systems that rely on predefined objectives, our agents can formulate and prioritize their own goals based on their understanding of the broader context and desired outcomes.",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.8125 26.6192V30.7369C14.8131 30.8209 14.7958 30.9042 14.7617 30.981C14.7277 31.0579 14.6778 31.1267 14.6152 31.1828C14.5526 31.239 14.4788 31.2812 14.3987 31.3066C14.3186 31.3321 14.234 31.3403 14.1505 31.3306C10.7216 30.9113 7.5317 29.3565 5.08907 26.9139C2.64644 24.4713 1.09161 21.2814 0.672337 17.8525C0.662667 17.769 0.670841 17.6844 0.696319 17.6043C0.721796 17.5241 0.763996 17.4504 0.820126 17.3878C0.876256 17.3252 0.945036 17.2752 1.02192 17.2412C1.09879 17.2072 1.18202 17.1899 1.26609 17.1905H5.38077C5.65398 19.5975 6.73556 21.8406 8.44876 23.5532C10.1619 25.2659 12.4054 26.3468 14.8125 26.6192ZM24.2264 17.1875H17.1875V24.2264C18.9623 23.9674 20.606 23.1425 21.8742 21.8742C23.1425 20.606 23.9674 18.9623 24.2264 17.1875ZM17.1875 7.77359V14.8125H24.2264C23.9674 13.0377 23.1425 11.394 21.8742 10.1258C20.606 8.85753 18.9623 8.03257 17.1875 7.77359ZM30.7369 17.1875H26.6192C26.3466 19.595 25.2653 21.8388 23.5521 23.5521C21.8388 25.2653 19.595 26.3466 17.1875 26.6192V30.7369C17.1869 30.8209 17.2042 30.9042 17.2383 30.981C17.2723 31.0579 17.3222 31.1267 17.3848 31.1828C17.4474 31.239 17.5212 31.2812 17.6013 31.3066C17.6814 31.3321 17.766 31.3403 17.8495 31.3306C21.2784 30.9113 24.4683 29.3565 26.9109 26.9139C29.3536 24.4713 30.9084 21.2814 31.3277 17.8525C31.3377 17.769 31.33 17.6843 31.3049 17.6041C31.2798 17.5238 31.2379 17.4498 31.1821 17.3869C31.1262 17.324 31.0576 17.2737 30.9809 17.2394C30.9041 17.205 30.821 17.1873 30.7369 17.1875ZM26.6192 14.8125H30.7369C30.8209 14.8131 30.9042 14.7958 30.981 14.7617C31.0579 14.7277 31.1267 14.6778 31.1828 14.6152C31.239 14.5526 31.2812 14.4788 31.3066 14.3987C31.3321 14.3186 31.3403 14.234 31.3306 14.1505C30.9113 10.7216 29.3565 7.5317 26.9139 5.08907C24.4713 2.64644 21.2814 1.09161 17.8525 0.672337C17.769 0.662667 17.6844 0.670841 17.6043 0.696319C17.5241 0.721796 17.4504 0.763996 17.3878 0.820126C17.3252 0.876256 17.2752 0.945036 17.2412 1.02192C17.2072 1.09879 17.1899 1.18202 17.1905 1.26609V5.38077C19.5975 5.65398 21.8406 6.73556 23.5532 8.44876C25.2659 10.1619 26.3468 12.4054 26.6192 14.8125ZM14.1505 0.672337C10.7216 1.09161 7.5317 2.64644 5.08907 5.08907C2.64644 7.5317 1.09161 10.7216 0.672337 14.1505C0.662667 14.234 0.670841 14.3186 0.696319 14.3987C0.721796 14.4788 0.763996 14.5526 0.820126 14.6152C0.876256 14.6778 0.945036 14.7277 1.02192 14.7617C1.09879 14.7958 1.18202 14.8131 1.26609 14.8125H5.38077C5.65335 12.405 6.73466 10.1612 8.44791 8.44791C10.1612 6.73466 12.405 5.65335 14.8125 5.38077V1.26312C14.8126 1.1793 14.795 1.09641 14.7608 1.01988C14.7266 0.943362 14.6766 0.874948 14.6141 0.819141C14.5515 0.763335 14.4779 0.721401 14.398 0.696099C14.3181 0.670796 14.2337 0.662698 14.1505 0.672337ZM7.77359 14.8125H14.8125V7.77359C13.0377 8.03257 11.394 8.85753 10.1258 10.1258C8.85753 11.394 8.03257 13.0377 7.77359 14.8125ZM14.8125 24.2264V17.1875H7.77359C8.03257 18.9623 8.85753 20.606 10.1258 21.8742C11.394 23.1425 13.0377 23.9674 14.8125 24.2264Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "Human Values at Core",
|
||||
desc: "Unlike traditional AI systems that rely on predefined objectives, our agents can formulate and prioritize their own goals based on their understanding of the broader context and desired outcomes.",
|
||||
},
|
||||
];
|
||||
|
||||
export function Features() {
|
||||
return (
|
||||
<section id="features" className="">
|
||||
<div className="layout h-8 border-x"></div>
|
||||
<BlurFade inView className="flex items-center justify-center">
|
||||
<hr className="w-full border-black" />
|
||||
<Badge className="hover:bg-primary rounded-md font-light">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
<span className="text-lg mx-2">Features</span>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Badge>
|
||||
<hr className="w-full border-black" />
|
||||
</BlurFade>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 ">
|
||||
{FEATURE_LIST.map((feature, index) => (
|
||||
<BlurFade key={index} delay={0.2 + index * 0.2} inView>
|
||||
<Card className="bg-background border-none shadow-[0_0_23.1px_0_rgba(0,0,0,0.08)_inset]">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-14 h-14 rounded-full flex items-center justify-center shadow-[0_-2px_4.4px_0_rgba(0,0,0,0.4)_inset]">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">{feature.heading}</h3>
|
||||
<p className="text-muted-foreground">{feature.desc}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
219
src/app/(main)/_components/hero.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { BadgePlus, ChevronRight, Compass } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import Marquee from "@/components/ui/marquee";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { montserrat, spaceGrotesk } from "@/app/fonts";
|
||||
import { PrivyInterface, usePrivy } from "@privy-io/react-auth";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
function HeroPill() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: ANIMATION_EASE }}
|
||||
>
|
||||
<Link
|
||||
href={"/explore"}
|
||||
className="flex w-auto items-center space-x-2 rounded-md bg-primary px-3 py-2 ring-1 ring-accent whitespace-pre"
|
||||
>
|
||||
<div className="w-fit rounded-md bg-accent text-center text-xs font-medium text-primary sm:text-sm">
|
||||
<ChevronRight className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-lg font-bold text-white sm:text-sm pr-2">
|
||||
Introducing soviro.ai
|
||||
</p>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroTitles() {
|
||||
return (
|
||||
<div className="flex w-full max-w-5xl flex-col space-y-4 overflow-hidden mt-12">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-medium leading-tight text-foreground sm:text-5xl md:text-6xl",
|
||||
spaceGrotesk.className
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
staggerChildren: 0.2,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
{["Seamless", "AI", "Integration", "for", "a", "Next-Gen", "SDK"].map(
|
||||
(text, index) => (
|
||||
<motion.span
|
||||
key={index}
|
||||
className="inline-block px-1 md:px-2 text-balance font-semibold"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: index * 0.2,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</motion.span>
|
||||
)
|
||||
)}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className={cn(
|
||||
"mx-auto max-w-3xl text-center text-sm leading-7 text-muted-foreground sm:text-lg sm:leading-9 text-balance"
|
||||
// montserrat.className
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 1.5,
|
||||
duration: 0.8,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
Effortlessly build powerful AI agent workflows with minimal code,
|
||||
unlocking seamless automation and intelligent decision-making for
|
||||
complex tasks. Accelerate your development and drive efficiency with
|
||||
cutting-edge AI technology.
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCTA({ privyAuth }: { privyAuth: PrivyInterface }) {
|
||||
const { ready, authenticated } = privyAuth;
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="mx-auto mt-12 pb-12 flex w-full max-w-2xl flex-col items-center justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 1.75, duration: 0.8, ease: ANIMATION_EASE }}
|
||||
>
|
||||
{!ready ? (
|
||||
<Skeleton className="h-10 w-32" />
|
||||
) : (
|
||||
<>
|
||||
{!authenticated ? (
|
||||
<Button disabled className="gap-2">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.55189 0.333435H17.4369C21.4036 0.333435 23.6669 2.57343 23.6669 6.55177V17.4484C23.6669 21.4034 21.4152 23.6668 17.4486 23.6668H6.55189C2.57356 23.6668 0.333557 21.4034 0.333557 17.4484V6.55177C0.333557 2.57343 2.57356 0.333435 6.55189 0.333435ZM12.9569 12.9684H16.2702C16.8069 12.9568 17.2386 12.5251 17.2386 11.9884C17.2386 11.4518 16.8069 11.0201 16.2702 11.0201H12.9569V7.7301C12.9569 7.19344 12.5252 6.76177 11.9886 6.76177C11.4519 6.76177 11.0202 7.19344 11.0202 7.7301V11.0201H7.71856C7.46189 11.0201 7.21689 11.1251 7.03022 11.3001C6.85522 11.4868 6.75022 11.7306 6.75022 11.9884C6.75022 12.5251 7.18189 12.9568 7.71856 12.9684H11.0202V16.2701C11.0202 16.8068 11.4519 17.2384 11.9886 17.2384C12.5252 17.2384 12.9569 16.8068 12.9569 16.2701V12.9684Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Connect your wallet first
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/agents/create`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.55189 0.333435H17.4369C21.4036 0.333435 23.6669 2.57343 23.6669 6.55177V17.4484C23.6669 21.4034 21.4152 23.6668 17.4486 23.6668H6.55189C2.57356 23.6668 0.333557 21.4034 0.333557 17.4484V6.55177C0.333557 2.57343 2.57356 0.333435 6.55189 0.333435ZM12.9569 12.9684H16.2702C16.8069 12.9568 17.2386 12.5251 17.2386 11.9884C17.2386 11.4518 16.8069 11.0201 16.2702 11.0201H12.9569V7.7301C12.9569 7.19344 12.5252 6.76177 11.9886 6.76177C11.4519 6.76177 11.0202 7.19344 11.0202 7.7301V11.0201H7.71856C7.46189 11.0201 7.21689 11.1251 7.03022 11.3001C6.85522 11.4868 6.75022 11.7306 6.75022 11.9884C6.75022 12.5251 7.18189 12.9568 7.71856 12.9684H11.0202V16.2701C11.0202 16.8068 11.4519 17.2384 11.9886 17.2384C12.5252 17.2384 12.9569 16.8068 12.9569 16.2701V12.9684Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Create new Agent
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
href="/agents"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"w-full sm:w-auto flex gap-2"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="26"
|
||||
height="20"
|
||||
viewBox="0 0 26 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24.9459 12.6141V12.6031C24.9441 12.5949 24.9415 12.5869 24.9383 12.5791C24.8657 12.381 24.7813 12.1874 24.6856 11.9994L20.1389 1.66563C20.0951 1.56609 20.0332 1.47559 19.9563 1.39875C19.6312 1.07366 19.2454 0.815782 18.8207 0.63984C18.396 0.463899 17.9408 0.373342 17.4811 0.373342C17.0214 0.373342 16.5662 0.463899 16.1415 0.63984C15.7168 0.815782 15.331 1.07366 15.0059 1.39875C14.8424 1.56245 14.7504 1.78425 14.75 2.01563V4.75H11.25V2.01563C11.2501 1.90069 11.2275 1.78685 11.1836 1.68063C11.1397 1.57441 11.0753 1.47788 10.9941 1.39656C10.6691 1.07147 10.2832 0.813594 9.85849 0.637653C9.4338 0.461711 8.97861 0.371155 8.51892 0.371155C8.05922 0.371155 7.60403 0.461711 7.17934 0.637653C6.75465 0.813594 6.36878 1.07147 6.04376 1.39656C5.96682 1.4734 5.90488 1.5639 5.8611 1.66344L1.31876 11.9994C1.22308 12.1874 1.13871 12.381 1.0661 12.5791C1.06306 12.5865 1.0605 12.5942 1.05845 12.602C1.05845 12.602 1.05845 12.6108 1.05845 12.6141C0.606717 13.9193 0.687536 15.35 1.28339 16.596C1.87924 17.8421 2.94213 18.8031 4.24169 19.2709C5.54126 19.7386 6.97281 19.6754 8.2261 19.095C9.4794 18.5145 10.4535 17.4636 10.9372 16.1698C11.1502 15.5887 11.2576 14.9742 11.2544 14.3553V6.5H14.7544V14.3564C14.7512 14.9753 14.8586 15.5898 15.0716 16.1709C15.5553 17.4646 16.5294 18.5156 17.7827 19.0961C19.036 19.6765 20.4675 19.7397 21.7671 19.272C23.0666 18.8042 24.1295 17.8432 24.7254 16.5971C25.3212 15.3511 25.4021 13.9204 24.9503 12.6152L24.9459 12.6141ZM9.28892 15.5672C8.96581 16.4293 8.31599 17.1294 7.48028 17.5158C6.64457 17.9022 5.69027 17.9437 4.82417 17.6313C3.95806 17.319 3.24991 16.678 2.85316 15.8471C2.45641 15.0163 2.40301 14.0626 2.70454 13.1927L2.90251 12.7409C3.26637 12.0493 3.85125 11.4993 4.56394 11.1787C5.27664 10.858 6.07616 10.7851 6.8351 10.9716C7.59404 11.158 8.26874 11.5931 8.7517 12.2076C9.23466 12.822 9.49809 13.5804 9.50001 14.3619V14.375C9.49995 14.7831 9.4285 15.1881 9.28892 15.5716V15.5672ZM21.2031 17.6639C20.6741 17.8576 20.1061 17.9207 19.5474 17.8478C18.9888 17.775 18.4559 17.5683 17.9942 17.2454C17.5325 16.9226 17.1556 16.493 16.8955 15.9933C16.6353 15.4935 16.4997 14.9384 16.5 14.375V14.363C16.5029 13.5818 16.767 12.8241 17.2503 12.2104C17.7336 11.5968 18.4083 11.1624 19.167 10.9765C19.9257 10.7906 20.7248 10.8638 21.4371 11.1846C22.1493 11.5053 22.7338 12.0551 23.0975 12.7464L23.2955 13.1981C23.6067 14.068 23.5614 15.0256 23.1694 15.8621C22.7775 16.6987 22.0707 17.3464 21.2031 17.6639Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
Explore Agents
|
||||
</Link>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroMarquee() {
|
||||
return (
|
||||
<div className="relative overflow-hidden bg-[#D7D7D7B2] bg-opacity-70">
|
||||
<Marquee pauseOnHover className="[--duration:20s]">
|
||||
{[...Array(5)].map((review, idx) => (
|
||||
<div key={idx} className="flex items-center">
|
||||
<Image
|
||||
src="/marquee-dot.png"
|
||||
height={45}
|
||||
width={45}
|
||||
alt=""
|
||||
className="mr-2"
|
||||
/>
|
||||
<p className={spaceGrotesk.className}>
|
||||
Available for all major programming languages
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Hero() {
|
||||
const privyAuth = usePrivy();
|
||||
|
||||
return (
|
||||
<section id="hero" className="">
|
||||
<div className="pt-8 px-3 sm:px-6 layout flex flex-col justify-center items-center border-x">
|
||||
<HeroPill />
|
||||
<HeroTitles />
|
||||
<HeroCTA privyAuth={privyAuth} />
|
||||
</div>
|
||||
<HeroMarquee />
|
||||
</section>
|
||||
);
|
||||
}
|
97
src/app/(main)/_components/roadmap.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
import { Book, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import BlurFade from "@/components/blur-fade";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { spaceGrotesk } from "@/app/fonts";
|
||||
import Link from "next/link";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
|
||||
const ROADMAPS = [
|
||||
"Lorem ipsum dolor sit amet consectetur. Et dictum at egestas nisl. Aenean ut a augue viverra adipiscing mi nisl ullamcorper mauris. Ipsum varius ullamcorper mi suscipit. Justo eget faucibus aliquet dui pellentesque sit vitae pellentesque.",
|
||||
"Lorem ipsum dolor sit amet consectetur. Et dictum at egestas nisl. Aenean ut a augue viverra adipiscing mi nisl ullamcorper mauris. Ipsum varius ullamcorper mi suscipit. Justo eget faucibus aliquet dui pellentesque sit vitae pellentesque.",
|
||||
"Lorem ipsum dolor sit amet consectetur. Et dictum at egestas nisl. Aenean ut a augue viverra adipiscing mi nisl ullamcorper mauris. Ipsum varius ullamcorper mi suscipit. Justo eget faucibus aliquet dui pellentesque sit vitae pellentesque.",
|
||||
"Lorem ipsum dolor sit amet consectetur. Et dictum at egestas nisl. Aenean ut a augue viverra adipiscing mi nisl ullamcorper mauris. Ipsum varius ullamcorper mi suscipit. Justo eget faucibus aliquet dui pellentesque sit vitae pellentesque.",
|
||||
];
|
||||
|
||||
export function Roadmap() {
|
||||
return (
|
||||
<section className="">
|
||||
<div
|
||||
className="layout"
|
||||
style={{
|
||||
backgroundImage: "url(/background-4.png)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.75)",
|
||||
backgroundBlendMode: "lighten",
|
||||
}}
|
||||
>
|
||||
<div className="border-x border-b">
|
||||
<BlurFade className="p-6" inView>
|
||||
<Badge className="hover:bg-primary rounded-md font-light mb-6">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
<span className="text-lg mx-2">Roadmap</span>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Badge>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="sm:col-span-2">
|
||||
<h2
|
||||
className={cn(
|
||||
"text-3xl sm:text-6xl/[80px] mb-2",
|
||||
spaceGrotesk.className
|
||||
)}
|
||||
>
|
||||
WE HAVE BIG PLANS FOR SOVIRO.AI IN 2025
|
||||
</h2>
|
||||
<p className="mb-4 text-muted-foreground">
|
||||
Discover our strategic roadmap for 2025, In 2025, we'll
|
||||
transform our SDK AI platform to be faster, smarter, and more
|
||||
user-friendly. With key updates, new features, and seamless
|
||||
integration, we're making it easier for developers to
|
||||
build powerful with SOVIRO.AI
|
||||
</p>
|
||||
<Link
|
||||
href={"https://soviro.gitbook.io/soviro"}
|
||||
target="_blank"
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<Book className="h-4 w-4 mr-2" />
|
||||
Open Gitbook
|
||||
</Link>
|
||||
</div>
|
||||
<div className="h-[267px] sm:h-[367px] w-full relative">
|
||||
<Image
|
||||
src="/background-5.png"
|
||||
fill={true}
|
||||
className="h-full w-full object-cover"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BlurFade>
|
||||
</div>
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 border-x ">
|
||||
{ROADMAPS.map((roadmap, idx) => (
|
||||
<BlurFade
|
||||
key={idx}
|
||||
delay={0.2 * idx}
|
||||
inView
|
||||
className="border-r last:border-r-0 p-6 space-y-4 border-b last:border-b-0 sm:[&:nth-child(3)]:border-b-0 sm:[&:nth-child(4)]:border-b-0 lg:border-b-0"
|
||||
>
|
||||
<li>
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="text-5xl">{idx + 1}</p>
|
||||
<p className="text-lg">Q{idx + 1} 2025</p>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold">Phase 0{idx + 1}</h2>
|
||||
<p className="text-muted-foreground">{roadmap}</p>
|
||||
</li>
|
||||
</BlurFade>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -3,22 +3,29 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { BorderText } from "@/components/ui/border-number";
|
||||
import { NumberTicker } from "@/components/ui/number-ticker";
|
||||
import Link from "next/link";
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: "10K+",
|
||||
subtitle: "Stars on GitHub",
|
||||
value: 50,
|
||||
unit: "+",
|
||||
title: "Specialized Agents",
|
||||
subtitle: "Purpose-built AI agents for specific tasks and industries.",
|
||||
icon: <Icons.github className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "50K+",
|
||||
subtitle: "Discord Members",
|
||||
value: 65,
|
||||
unit: "x",
|
||||
title: "Processing Speed",
|
||||
subtitle: "Faster than human operators in complex decision scenarios.",
|
||||
icon: <Icons.discord className="h-5 w-5" />,
|
||||
},
|
||||
{
|
||||
title: "1M+",
|
||||
subtitle: "Downloads",
|
||||
value: 100,
|
||||
unit: "+",
|
||||
title: "Daily Decisions",
|
||||
subtitle: "Autonomous conversations made by our systems every day.",
|
||||
icon: <Icons.npm className="h-5 w-5" />,
|
||||
},
|
||||
];
|
||||
@ -33,41 +40,30 @@ export function Statistics() {
|
||||
"radial-gradient(circle at bottom center, hsl(var(--secondary) / 0.4), hsl(var(--background)))",
|
||||
}}
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3">
|
||||
<ul className="grid grid-cols-1 sm:grid-cols-3">
|
||||
{stats.map((stat, idx) => (
|
||||
<Link
|
||||
href="#"
|
||||
<li
|
||||
key={idx}
|
||||
className="flex flex-col items-center justify-center py-8 px-4 border-b sm:border-b-0 last:border-b-0 sm:border-r sm:last:border-r-0 [&:nth-child(-n+2)]:border-t-0 sm:[&:nth-child(-n+3)]:border-t-0 relative group overflow-hidden"
|
||||
>
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-all transform translate-x-full -translate-y-full group-hover:translate-x-0 group-hover:translate-y-0 duration-300 ease-in-out">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="7" y1="17" x2="17" y2="7"></line>
|
||||
<polyline points="7 7 17 7 17 17"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="text-center relative">
|
||||
<BorderText text={stat.title} />
|
||||
<div className="flex items-center justify-center gap-2 mt-2">
|
||||
{stat.icon}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{/* <BorderText text="eheh"/> */}
|
||||
<div className="flex items-center justify-center text-primary text-[6rem] font-bold leading-none">
|
||||
<NumberTicker value={stat.value} />
|
||||
<span>{stat.unit}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center gap-2 mt-2">
|
||||
<h3 className="text-xl font-medium text-card-foreground text-center text-balance">
|
||||
{stat.title}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground text-balance text-center max-w-md mx-auto">
|
||||
{stat.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
82
src/app/(main)/_components/tokenomics.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import BlurFade from "@/components/blur-fade";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
|
||||
const STATS = [
|
||||
{ value: "95%", desc: "Circulating Supply" },
|
||||
{ value: "2%", desc: "Team Allocations" },
|
||||
{ value: "1%", desc: "Marketing Operations" },
|
||||
{ value: "2%", desc: "Treasury" },
|
||||
];
|
||||
|
||||
export function Tokenomics() {
|
||||
return (
|
||||
<section id="tokenomics">
|
||||
<div className="layout h-8 border-x"></div>
|
||||
<BlurFade className="flex items-center justify-center" inView>
|
||||
<hr className="w-full border-black" />
|
||||
<Badge className="hover:bg-primary rounded-md font-light">
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
<span className="text-lg mx-2">Tokenomics</span>
|
||||
<ChevronRight className="h-6 w-6" />
|
||||
</Badge>
|
||||
<hr className="w-full border-black" />
|
||||
</BlurFade>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<Accordion type="multiple" className="w-full space-y-6">
|
||||
{STATS.map((stat, idx) => (
|
||||
<AccordionItem key={`item-${idx}`} value={`item-${idx}`} asChild className="border-b-0">
|
||||
<BlurFade delay={0.2 + idx * 0.2} inView>
|
||||
<Card className="border-primary rounded-lg">
|
||||
<AccordionTrigger className="py-0 [&[data-state=open]>div>svg]:rotate-180 [&[data-state=closed]>div]:bg-primary [&[data-state=closed]>div]:text-white [&[data-state=open]>div]:mb-0 [&[data-state=open]>div]:sm:mb-0 [&[data-state=open]>div]:bg-[url(/background-3.png)] [&[data-state=open]>div]:text-primary hover:no-underline">
|
||||
<div className="transition-all grow m-2 p-3 sm:m-4 sm:p-6 flex justify-between items-center gap-3 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<p className="font-bold text-2xl sm:text-5xl">
|
||||
{stat.value}
|
||||
</p>
|
||||
<h2 className="text-xl sm:text-3xl">{stat.desc}</h2>
|
||||
</div>
|
||||
<ChevronDown className="h-6 w-6 sm:h-8 sm:w-8 shrink-0 transition-transform duration-200" />
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="m-2 mt-0 p-3 sm:m-4 sm:mt-0 sm:p-6 bg-[url(/background-3.png)] text-muted-foreground text-sm sm:text-lg">
|
||||
<p className="mb-4">
|
||||
The circulating supply represents the total amount of
|
||||
tokens that are currently available in the market. This
|
||||
ensures liquidity and accessibility for all participants.
|
||||
Our commitment to transparency and fairness is reflected
|
||||
in the high percentage of tokens allocated for
|
||||
circulation.
|
||||
</p>
|
||||
<ul className="list-disc">
|
||||
<li className="ml-6">
|
||||
Ensures liquidity and accessibility for all
|
||||
participants.
|
||||
</li>
|
||||
<li className="ml-6">
|
||||
Reflects our commitment to transparency and fairness.
|
||||
</li>
|
||||
<li className="ml-6">
|
||||
Supports the growth and sustainability of our community.
|
||||
</li>
|
||||
</ul>
|
||||
</AccordionContent>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
254
src/app/(main)/agents/[agentId]/_components/ask-agent-card.tsx
Normal file
@ -0,0 +1,254 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { Send } from "lucide-react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import PostgrestError, { ANIMATION_EASE } from "@/lib/config";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
type AskAgentCardProps = {
|
||||
agentId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function AskAgentCard({ agentId, disabled = false }: AskAgentCardProps) {
|
||||
const {
|
||||
data: agentsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
retry: 0,
|
||||
queryKey: ["agents", agentId],
|
||||
queryFn: async ({ signal }) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const response = await supabase
|
||||
.from("agents")
|
||||
.select("*")
|
||||
.eq("id", parseInt(agentId))
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (response.error) throw new PostgrestError(response.error);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
if (error) return <p className="text-center">{error.message}</p>;
|
||||
|
||||
if (isLoading || !agentsData || !agentsData.data)
|
||||
return <Spinner className="mx-auto" />;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<CardContent agent={agentsData.data} disabled={disabled} />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const CardContent = ({
|
||||
agent,
|
||||
disabled,
|
||||
}: {
|
||||
agent: Tables<"agents">;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
const {
|
||||
id,
|
||||
image_url,
|
||||
name,
|
||||
model_type,
|
||||
description,
|
||||
conversation,
|
||||
last_conv,
|
||||
} = agent;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const formSchema = z.object({
|
||||
question: z
|
||||
.string({
|
||||
required_error: "Question is required",
|
||||
})
|
||||
.min(2, {
|
||||
message: "Minimum Question is 2 characters.",
|
||||
}),
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
question: "",
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof formSchema>) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data: existingAgent, error: error1 } = await supabase
|
||||
.from("agents")
|
||||
.select("*")
|
||||
.eq("id", id);
|
||||
|
||||
if (error1) throw new PostgrestError(error1);
|
||||
|
||||
const { error: error2 } = await supabase
|
||||
.from("agents")
|
||||
.update({
|
||||
conversation: existingAgent ? existingAgent[0].conversation + 1 : 0,
|
||||
last_conv: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", id)
|
||||
.select();
|
||||
|
||||
if (error2) throw new PostgrestError(error2);
|
||||
|
||||
const botResponseJson = await axios.get(
|
||||
encodeURI(
|
||||
`https://ai-endpoint-one.dev3vds1.link/deepinfra-ai/${name}/${model_type}/${description}/${values.question}`
|
||||
)
|
||||
);
|
||||
|
||||
const { data, error: error3 } = await supabase
|
||||
.from("agent_responses")
|
||||
.upsert({
|
||||
agent_id: id,
|
||||
question: values.question,
|
||||
response: botResponseJson.data.response,
|
||||
})
|
||||
.select()
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (error3) throw new PostgrestError(error3);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data: Tables<"agent_responses">) => {
|
||||
router.push(`/agents/${id}/responses/${data.id}`);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["agents", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
mutation.mutate(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-transparent">
|
||||
<div className="flex flex-col-reverse sm:flex-row justify-between gap-3 mb-4">
|
||||
<div className="mr-auto">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<p className="text-primary text-sm ">
|
||||
<time dateTime={"2024-11-10"}>
|
||||
Last Active:{" "}
|
||||
<span className="font-bold">
|
||||
{last_conv ? formatDate(last_conv) : "-"}
|
||||
</span>
|
||||
</time>
|
||||
</p>
|
||||
<div
|
||||
className="text-sm mb-2 text-white px-2 py-0.5 rounded-md"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #FF512F 0%, #F09819 100%)",
|
||||
}}
|
||||
>
|
||||
{conversation} chats
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-primary font-bold text-xl mb-2">{name}</h1>
|
||||
<p className="text-gray-500 text-sm">{description}</p>
|
||||
</div>
|
||||
<figure className="relative h-[333px] min-w-full sm:h-[150px] sm:min-w-[250px] rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src={image_url}
|
||||
fill={true}
|
||||
alt="test"
|
||||
className="object-cover"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full flex gap-3"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="question"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
disabled={disabled}
|
||||
placeholder="Ask our agent"
|
||||
className={clsx(
|
||||
"h-9",
|
||||
form.formState.errors.question &&
|
||||
"focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button disabled={mutation.isPending || disabled} size={"sm"}>
|
||||
{mutation.isPending ? (
|
||||
<Spinner className="h-4 w-4" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
31
src/app/(main)/agents/[agentId]/_components/heading.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Section } from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Heading() {
|
||||
return (
|
||||
<section id="heading">
|
||||
<div className="layout py-6 relative w-full border-x overflow-hidden">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">Ask a Question</AuroraText>
|
||||
</motion.h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { Copy, Play, Twitter } from "lucide-react";
|
||||
|
||||
import PostgrestError from "@/lib/config";
|
||||
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
|
||||
type ResponseAgentCardProps = {
|
||||
agentResponseId: string;
|
||||
};
|
||||
|
||||
export default function ResponseAgentCard({
|
||||
agentResponseId,
|
||||
}: ResponseAgentCardProps) {
|
||||
const {
|
||||
data: responseData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
retry: 0,
|
||||
queryKey: ["agent-responses", agentResponseId],
|
||||
queryFn: async ({ signal }) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const response = await supabase
|
||||
.from("agent_responses")
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
agents (
|
||||
agent_id:id,
|
||||
name,
|
||||
image_url
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq("id", agentResponseId)
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (response.error) throw new PostgrestError(response.error);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
if (error) return <p className="text-center">{error.message}</p>;
|
||||
|
||||
if (isLoading || !responseData || !responseData.data)
|
||||
return <Spinner className="mx-auto" />;
|
||||
|
||||
return <CardContent agentResponse={responseData.data} />;
|
||||
}
|
||||
|
||||
const CardContent = ({
|
||||
agentResponse,
|
||||
}: {
|
||||
agentResponse: Tables<"agent_responses"> & {
|
||||
agents: {
|
||||
agent_id: number;
|
||||
name: string;
|
||||
image_url: string;
|
||||
};
|
||||
};
|
||||
}) => {
|
||||
const { id, agent_id, question, response, agents } = agentResponse;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<figure className="relative h-16 w-16 rounded-full overflow-hidden">
|
||||
<Image
|
||||
src={agents.image_url}
|
||||
fill={true}
|
||||
alt="test"
|
||||
className="object-cover"
|
||||
/>
|
||||
</figure>
|
||||
<div>
|
||||
<h1 className="text-primary">{agents.name}</h1>
|
||||
<p className="text-gray-400 text-sm">{id}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="mb-3" />
|
||||
<h2 className="text-sm font-bold text-primary mb-2">Question: </h2>
|
||||
<p className="text-gray-500 text-sm mb-3">{question}</p>
|
||||
<h2 className="text-sm font-bold text-primary mb-2">Response: </h2>
|
||||
<p className="text-gray-500 text-sm mb-3">{response}</p>
|
||||
<div className="mt-24 grid grid-cols-3 gap-3">
|
||||
<Link
|
||||
href={encodeURI(`https://x.com/intent/post?text=${response}`)}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M19.5582 0H23.2208L15.2208 10.1867L24.6666 24H17.2449L11.4618 15.6267L4.8112 24H1.14855L9.72687 13.12L0.666626 0H8.28108L13.5341 7.68L19.5582 0ZM18.257 21.5467H20.2811L7.17265 2.29333H4.95578L18.257 21.5467Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
Share on Twitter
|
||||
</Link>
|
||||
<Link
|
||||
href={`/agents/${agent_id}`}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 17 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M16.5 10.0005C16.5006 10.2551 16.4353 10.5056 16.3105 10.7275C16.1856 10.9495 16.0055 11.1353 15.7875 11.267L2.28 19.5302C2.05227 19.6696 1.79144 19.7457 1.52445 19.7507C1.25746 19.7556 0.993989 19.6892 0.76125 19.5583C0.530728 19.4294 0.338696 19.2414 0.204904 19.0137C0.0711107 18.786 0.000385179 18.5268 0 18.2627V1.73828C0.000385179 1.47417 0.0711107 1.21493 0.204904 0.987222C0.338696 0.759511 0.530728 0.571545 0.76125 0.442655C0.993989 0.311732 1.25746 0.245313 1.52445 0.250257C1.79144 0.255201 2.05227 0.33133 2.28 0.470781L15.7875 8.7339C16.0055 8.8656 16.1856 9.05145 16.3105 9.2734C16.4353 9.49534 16.5006 9.74582 16.5 10.0005Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Ask Another
|
||||
</Link>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
toast.info("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 19 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M15.3333 3.75V17.25C15.3333 17.4489 15.2542 17.6397 15.1136 17.7803C14.9729 17.921 14.7822 18 14.5833 18H1.08325C0.88434 18 0.693574 17.921 0.552922 17.7803C0.41227 17.6397 0.333252 17.4489 0.333252 17.25V3.75C0.333252 3.55109 0.41227 3.36032 0.552922 3.21967C0.693574 3.07902 0.88434 3 1.08325 3H14.5833C14.7822 3 14.9729 3.07902 15.1136 3.21967C15.2542 3.36032 15.3333 3.55109 15.3333 3.75ZM17.5833 0H4.08325C3.88434 0 3.69357 0.0790178 3.55292 0.21967C3.41227 0.360322 3.33325 0.551088 3.33325 0.75C3.33325 0.948912 3.41227 1.13968 3.55292 1.28033C3.69357 1.42098 3.88434 1.5 4.08325 1.5H16.8333V14.25C16.8333 14.4489 16.9123 14.6397 17.0529 14.7803C17.1936 14.921 17.3843 15 17.5833 15C17.7822 15 17.9729 14.921 18.1136 14.7803C18.2542 14.6397 18.3333 14.4489 18.3333 14.25V0.75C18.3333 0.551088 18.2542 0.360322 18.1136 0.21967C17.9729 0.0790178 17.7822 0 17.5833 0Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,355 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { MessageCircle, Send } from "lucide-react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import PostgrestError, { ANIMATION_EASE } from "@/lib/config";
|
||||
import { cn, formatDate } from "@/lib/utils";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
import { Card } from "@/components/ui/card";
|
||||
|
||||
import { editAgentSchema } from "../schemas/edit-agent-schema";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type EditAgentCardProps = {
|
||||
agentId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export function EditAgentCard({
|
||||
agentId,
|
||||
disabled = false,
|
||||
}: EditAgentCardProps) {
|
||||
const {
|
||||
data: agentsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
retry: 0,
|
||||
queryKey: ["agents", agentId],
|
||||
queryFn: async ({ signal }) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const response = await supabase
|
||||
.from("agents")
|
||||
.select("*")
|
||||
.eq("id", parseInt(agentId))
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (response.error) throw new PostgrestError(response.error);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
if (error) return <p className="text-center">{error.message}</p>;
|
||||
|
||||
if (isLoading || !agentsData || !agentsData.data)
|
||||
return <Spinner className="mx-auto" />;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<CardContent agent={agentsData.data} disabled={disabled} />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const CardContent = ({
|
||||
agent,
|
||||
disabled,
|
||||
}: {
|
||||
agent: Tables<"agents">;
|
||||
disabled: boolean;
|
||||
}) => {
|
||||
const { id, image_url, name, model_type, description, user_id } = agent;
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<z.infer<typeof editAgentSchema>>({
|
||||
resolver: zodResolver(editAgentSchema),
|
||||
defaultValues: {
|
||||
userId: user_id,
|
||||
name: name,
|
||||
description: description,
|
||||
modelType: model_type,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof editAgentSchema>) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("agents")
|
||||
.update({
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
})
|
||||
.eq("id", id)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw new PostgrestError(error);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data: Tables<"agents">) => {
|
||||
console.log(data);
|
||||
toast.success("Data has successfully updated");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["agents", id],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof editAgentSchema>) => {
|
||||
mutation.mutate(values);
|
||||
};
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
if (mutation.isSuccess)
|
||||
return (
|
||||
<CompleteComponent
|
||||
agentId={mutation.data.id.toString()}
|
||||
agentName={mutation.data.name}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className="p-6 bg-transparent">
|
||||
<div className="flex flex-col sm:flex-row justify-between gap-3 mb-4">
|
||||
<figure className="relative h-[275px] min-w-full sm:h-[215px] sm:min-w-[275px] rounded-xl overflow-hidden">
|
||||
<Image
|
||||
src={image_url}
|
||||
fill={true}
|
||||
alt="test"
|
||||
className="object-cover"
|
||||
/>
|
||||
</figure>
|
||||
<div className="grow sm:ml-auto">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Agent Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="astrobot"
|
||||
className={cn(
|
||||
errors.name && "focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder="For answering daily questions."
|
||||
className={cn(
|
||||
errors.description && "focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={mutation.isPending}
|
||||
onClick={() => router.back()}
|
||||
type="button"
|
||||
>
|
||||
{mutation.isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
{mutation.isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const CompleteComponent = ({
|
||||
agentId,
|
||||
agentName,
|
||||
}: {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-center items-center py-4">
|
||||
<div className="h-20 w-20 flex flex-col justify-center items-center rounded-full bg-primary">
|
||||
<svg
|
||||
width="61"
|
||||
height="41"
|
||||
viewBox="0 0 61 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="14.6426"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="6.35693"
|
||||
y="19.0342"
|
||||
width="7.32129"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="13.6787"
|
||||
y="27.8223"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="19.5354"
|
||||
y="32.2148"
|
||||
width="7.32129"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="26.8567"
|
||||
y="27.8223"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="32.7136"
|
||||
y="19.0342"
|
||||
width="8.78555"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="41.4993"
|
||||
y="14.6426"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="47.3562"
|
||||
y="5.85645"
|
||||
width="8.78555"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="56.1418" width="4.39277" height="5.85703" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl py-4 font-bold">Agent succesfully created!</h3>
|
||||
<p className="text-lg text-muted-foreground mb-3">
|
||||
You can try to have a conversation with your agent.
|
||||
</p>
|
||||
<Link
|
||||
href={`/agents/${agentId}`}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M8.50006 0.875C7.0973 0.874694 5.71836 1.23757 4.49745 1.9283C3.27654 2.61904 2.25526 3.6141 1.53301 4.81664C0.810765 6.01918 0.412159 7.38822 0.375993 8.79052C0.339826 10.1928 0.66733 11.5806 1.32663 12.8188L0.439908 15.4789C0.366459 15.6992 0.355799 15.9355 0.409125 16.1615C0.462451 16.3874 0.577655 16.5941 0.741823 16.7582C0.905992 16.9224 1.11264 17.0376 1.3386 17.0909C1.56456 17.1443 1.80091 17.1336 2.02116 17.0602L4.68131 16.1734C5.77098 16.753 6.97836 17.0767 8.21181 17.12C9.44525 17.1633 10.6724 16.925 11.8 16.4232C12.9276 15.9215 13.926 15.1694 14.7196 14.2241C15.5132 13.2789 16.0809 12.1652 16.3799 10.9678C16.6788 9.77029 16.7009 8.52047 16.4447 7.31315C16.1885 6.10584 15.6605 4.97276 14.901 3.99993C14.1415 3.02711 13.1703 2.24009 12.0612 1.69864C10.9521 1.15718 9.73427 0.875506 8.50006 0.875ZM5.06256 9.9375C4.87714 9.9375 4.69589 9.88252 4.54172 9.7795C4.38755 9.67649 4.26739 9.53007 4.19643 9.35876C4.12547 9.18746 4.1069 8.99896 4.14308 8.8171C4.17925 8.63525 4.26854 8.4682 4.39965 8.33709C4.53076 8.20598 4.69781 8.11669 4.87967 8.08051C5.06152 8.04434 5.25002 8.06291 5.42133 8.13386C5.59264 8.20482 5.73905 8.32498 5.84207 8.47915C5.94508 8.63332 6.00006 8.81458 6.00006 9C6.00006 9.24864 5.90129 9.4871 5.72548 9.66291C5.54966 9.83873 5.31121 9.9375 5.06256 9.9375ZM8.50006 9.9375C8.31464 9.9375 8.13339 9.88252 7.97922 9.7795C7.82505 9.67649 7.70489 9.53007 7.63393 9.35876C7.56297 9.18746 7.5444 8.99896 7.58058 8.8171C7.61675 8.63525 7.70604 8.4682 7.83715 8.33709C7.96826 8.20598 8.13531 8.11669 8.31717 8.08051C8.49902 8.04434 8.68752 8.06291 8.85883 8.13386C9.03014 8.20482 9.17655 8.32498 9.27957 8.47915C9.38258 8.63332 9.43756 8.81458 9.43756 9C9.43756 9.24864 9.33879 9.4871 9.16298 9.66291C8.98716 9.83873 8.74871 9.9375 8.50006 9.9375ZM11.9376 9.9375C11.7521 9.9375 11.5709 9.88252 11.4167 9.7795C11.2625 9.67649 11.1424 9.53007 11.0714 9.35876C11.0005 9.18746 10.9819 8.99896 11.0181 8.8171C11.0543 8.63525 11.1435 8.4682 11.2747 8.33709C11.4058 8.20598 11.5728 8.11669 11.7547 8.08051C11.9365 8.04434 12.125 8.06291 12.2963 8.13386C12.4676 8.20482 12.6141 8.32498 12.7171 8.47915C12.8201 8.63332 12.8751 8.81458 12.8751 9C12.8751 9.24864 12.7763 9.4871 12.6005 9.66291C12.4247 9.83873 12.1862 9.9375 11.9376 9.9375Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Chat with {agentName}
|
||||
</Link>
|
||||
</div>
|
||||
{/* form footer */}
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Link
|
||||
href="/agents"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
Back to agent list
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
31
src/app/(main)/agents/[agentId]/edit/_components/heading.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Section } from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Heading() {
|
||||
return (
|
||||
<section id="heading">
|
||||
<div className="layout py-6 relative w-full border-x overflow-hidden">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">Edit Agent</AuroraText>
|
||||
</motion.h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
27
src/app/(main)/agents/[agentId]/edit/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { CheckSessionProvider } from "@/providers/check-session-provider";
|
||||
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Heading } from "./_components/heading";
|
||||
import { EditAgentCard } from "./_components/edit-agent-card";
|
||||
|
||||
export default async function EditAgentPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ agentId: string }>;
|
||||
}) {
|
||||
const { agentId } = await params;
|
||||
|
||||
return (
|
||||
<CheckSessionProvider>
|
||||
<Heading />
|
||||
<hr className="" />
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<EditAgentCard agentId={agentId} />
|
||||
</div>
|
||||
</section>
|
||||
<hr className="" />
|
||||
<Footer />
|
||||
</CheckSessionProvider>
|
||||
);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const editAgentSchema = z.object({
|
||||
userId: z.string(),
|
||||
name: z
|
||||
.string({
|
||||
required_error: "Name is required",
|
||||
})
|
||||
.min(2, {
|
||||
message: "Minimum Name is 2 characters.",
|
||||
})
|
||||
.max(30, {
|
||||
message: "Maximum Name is 30 characters.",
|
||||
}),
|
||||
description: z
|
||||
.string({
|
||||
required_error: "Description is required",
|
||||
})
|
||||
.min(2, {
|
||||
message: "Minimum Description is 2 characters.",
|
||||
})
|
||||
.max(200, {
|
||||
message: "Maximum Description is 200 characters.",
|
||||
}),
|
||||
modelType: z.enum([
|
||||
"roleplay",
|
||||
"programming",
|
||||
"marketing",
|
||||
"marketing_seo",
|
||||
"technology",
|
||||
"science",
|
||||
"translation",
|
||||
"legal",
|
||||
"finance",
|
||||
"health",
|
||||
"trivia",
|
||||
"academia",
|
||||
"general",
|
||||
]),
|
||||
});
|
25
src/app/(main)/agents/[agentId]/page.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Heading } from "./_components/heading";
|
||||
import { AskAgentCard } from "./_components/ask-agent-card";
|
||||
|
||||
export default async function ChatToAgent({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ agentId: string }>;
|
||||
}) {
|
||||
const { agentId } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading />
|
||||
<hr className="" />
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<AskAgentCard agentId={agentId} />
|
||||
</div>
|
||||
</section>
|
||||
<hr className="" />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Section } from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Heading() {
|
||||
return (
|
||||
<section id="heading">
|
||||
<div className="layout py-6 relative w-full border-x overflow-hidden">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">Ask a Question</AuroraText>
|
||||
</motion.h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Heading } from "./_components/heading";
|
||||
import { AskAgentCard } from "../../_components/ask-agent-card";
|
||||
import ResponseAgentCard from "../../_components/response-agent-card";
|
||||
|
||||
export default async function AgentResponse({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ agentId: string; responseId: string }>;
|
||||
}) {
|
||||
const { agentId, responseId } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading />
|
||||
<hr className="" />
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<AskAgentCard agentId={agentId} disabled={true} />
|
||||
</div>
|
||||
</section>
|
||||
<hr className="" />
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<ResponseAgentCard agentResponseId={responseId} />
|
||||
</div>
|
||||
</section>
|
||||
<hr className="" />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
20
src/app/(main)/agents/_components/agent-card-skeleton.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AgentCardSkeleton() {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col p-4",
|
||||
"bg-background rounded-lg transition-all duration-300 border hover:shadow-[0_0_23.1px_0_rgba(0,0,0,0.08)_inset]"
|
||||
)}
|
||||
>
|
||||
<Skeleton className="h-[260px] w-full mb-3" />
|
||||
<Skeleton className="h-4 w-full mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-3" />
|
||||
<Skeleton className="h-3 w-full mb-3" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
166
src/app/(main)/agents/_components/agent-card.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { User } from "@privy-io/react-auth";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Pencil, Play, Share2 } from "lucide-react";
|
||||
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
|
||||
import { cn, formatDate } from "@/lib/utils";
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
|
||||
type AgentCardProps = {
|
||||
data: Tables<"agents">;
|
||||
idx: number;
|
||||
user: User | null;
|
||||
};
|
||||
|
||||
export default function AgentCard(props: AgentCardProps) {
|
||||
const {
|
||||
id,
|
||||
image_url,
|
||||
name,
|
||||
description,
|
||||
model_type,
|
||||
created_at,
|
||||
conversation,
|
||||
last_conv,
|
||||
user_id,
|
||||
} = props.data;
|
||||
|
||||
return (
|
||||
<motion.li
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: props.idx * 0.2,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
className={cn(
|
||||
"flex flex-col p-4",
|
||||
"bg-background rounded-lg transition-all duration-300 border hover:shadow-[0_0_23.1px_0_rgba(0,0,0,0.08)_inset]"
|
||||
)}
|
||||
>
|
||||
{image_url && (
|
||||
<Image
|
||||
className="object-cover mb-3 rounded-lg overflow-hidden"
|
||||
src={"/agent.png"}
|
||||
width={1200}
|
||||
height={630}
|
||||
alt={name}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xl font-bold mb-2">{name}</h3>
|
||||
<div
|
||||
className="mb-2 text-white px-2 py-0.5 rounded-md"
|
||||
style={{
|
||||
background: "linear-gradient(90deg, #FF512F 0%, #F09819 100%)",
|
||||
}}
|
||||
>
|
||||
{conversation} chats
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-2">{description}</p>
|
||||
<p className="mb-2 text-primary">
|
||||
<time dateTime={created_at} className="text-xs">
|
||||
<span className="font-bold">Last Active: </span>
|
||||
{last_conv ? formatDate(last_conv) : "-"}
|
||||
</time>
|
||||
</p>
|
||||
<div className="mt-auto grid sm:grid-cols-2 gap-2">
|
||||
<Link
|
||||
href={`/agents/${id}`}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 17 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M16.75 9.99998C16.7506 10.2546 16.6853 10.5051 16.5605 10.727C16.4356 10.949 16.2555 11.1348 16.0375 11.2665L2.53 19.5297C2.30227 19.6691 2.04144 19.7452 1.77445 19.7502C1.50746 19.7551 1.24399 19.6887 1.01125 19.5578C0.780728 19.4289 0.588696 19.2409 0.454904 19.0132C0.321111 18.7855 0.250385 18.5263 0.25 18.2622V1.73779C0.250385 1.47368 0.321111 1.21445 0.454904 0.986734C0.588696 0.759022 0.780728 0.571057 1.01125 0.442167C1.24399 0.311244 1.50746 0.244825 1.77445 0.249769C2.04144 0.254713 2.30227 0.330842 2.53 0.470292L16.0375 8.73342C16.2555 8.86511 16.4356 9.05097 16.5605 9.27291C16.6853 9.49485 16.7506 9.74533 16.75 9.99998Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span>Try it</span>
|
||||
</Link>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
`${process.env.NEXT_PUBLIC_SITE_URL}/agents/${id}`
|
||||
);
|
||||
toast.info("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 19 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M18.125 17.7502C18.1251 18.2428 18.0173 18.7294 17.8093 19.1759C17.6012 19.6225 17.298 20.018 16.9208 20.3349C16.5436 20.6517 16.1016 20.8821 15.6259 21.01C15.1502 21.1378 14.6522 21.16 14.167 21.0749C13.6818 20.9898 13.2211 20.7995 12.8173 20.5174C12.4135 20.2353 12.0763 19.8682 11.8294 19.442C11.5825 19.0157 11.432 18.5405 11.3883 18.0499C11.3446 17.5592 11.4089 17.0649 11.5766 16.6017L6.60782 13.4095C6.13327 13.875 5.53182 14.19 4.87893 14.315C4.22604 14.44 3.55079 14.3693 2.93788 14.112C2.32497 13.8547 1.8017 13.422 1.43373 12.8684C1.06575 12.3148 0.869446 11.6649 0.869446 11.0002C0.869446 10.3354 1.06575 9.68549 1.43373 9.13188C1.8017 8.57828 2.32497 8.14568 2.93788 7.88833C3.55079 7.63099 4.22604 7.56038 4.87893 7.68534C5.53182 7.81031 6.13327 8.1253 6.60782 8.59079L11.5766 5.40329C11.292 4.62146 11.3056 3.7622 11.6145 2.98971C11.9235 2.21722 12.5063 1.58565 13.2515 1.2157C13.9967 0.845739 14.8521 0.76333 15.6543 0.984215C16.4564 1.2051 17.149 1.71379 17.5998 2.41309C18.0506 3.11239 18.2279 3.95326 18.0979 4.77503C17.9678 5.5968 17.5396 6.34185 16.8949 6.86781C16.2503 7.39378 15.4344 7.66378 14.6033 7.62622C13.7721 7.58867 12.984 7.24619 12.3894 6.66423L7.42063 9.85642C7.68917 10.5985 7.68917 11.4112 7.42063 12.1533L12.3894 15.3455C12.8638 14.8812 13.4646 14.5671 14.1165 14.4425C14.7685 14.3179 15.4428 14.3884 16.055 14.645C16.6671 14.9017 17.19 15.3331 17.5582 15.8855C17.9264 16.4378 18.1236 17.0864 18.125 17.7502Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
<span>Share</span>
|
||||
</Button>
|
||||
</div>
|
||||
{!props.user || user_id !== props.user.id ? (
|
||||
<Button className="mt-2" variant={"dots"} disabled={true}>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 19 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M18.3103 4.87866L14.1216 0.688973C13.9823 0.549649 13.8169 0.439129 13.6349 0.363725C13.4529 0.288322 13.2578 0.249512 13.0608 0.249512C12.8638 0.249512 12.6687 0.288322 12.4867 0.363725C12.3047 0.439129 12.1393 0.549649 12 0.688973L0.439695 12.2502C0.299801 12.389 0.188889 12.5542 0.113407 12.7362C0.0379245 12.9183 -0.000621974 13.1135 7.58902e-06 13.3105V17.5002C7.58902e-06 17.898 0.158043 18.2796 0.439347 18.5609C0.720652 18.8422 1.10218 19.0002 1.50001 19.0002H5.6897C5.88675 19.0009 6.08197 18.9623 6.26399 18.8868C6.44602 18.8113 6.61122 18.7004 6.75001 18.5605L18.3103 7.00022C18.4496 6.86093 18.5602 6.69556 18.6356 6.51355C18.711 6.33153 18.7498 6.13645 18.7498 5.93944C18.7498 5.74243 18.711 5.54735 18.6356 5.36534C18.5602 5.18333 18.4496 5.01795 18.3103 4.87866ZM15 8.18897L10.8103 4.00022L13.0603 1.75022L17.25 5.93897L15 8.18897Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
<Link
|
||||
href={`/agents/${id}/edit`}
|
||||
className={cn("mt-2", buttonVariants({ variant: "dots" }))}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 19 19"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M18.3103 4.87866L14.1216 0.688973C13.9823 0.549649 13.8169 0.439129 13.6349 0.363725C13.4529 0.288322 13.2578 0.249512 13.0608 0.249512C12.8638 0.249512 12.6687 0.288322 12.4867 0.363725C12.3047 0.439129 12.1393 0.549649 12 0.688973L0.439695 12.2502C0.299801 12.389 0.188889 12.5542 0.113407 12.7362C0.0379245 12.9183 -0.000621974 13.1135 7.58902e-06 13.3105V17.5002C7.58902e-06 17.898 0.158043 18.2796 0.439347 18.5609C0.720652 18.8422 1.10218 19.0002 1.50001 19.0002H5.6897C5.88675 19.0009 6.08197 18.9623 6.26399 18.8868C6.44602 18.8113 6.61122 18.7004 6.75001 18.5605L18.3103 7.00022C18.4496 6.86093 18.5602 6.69556 18.6356 6.51355C18.711 6.33153 18.7498 6.13645 18.7498 5.93944C18.7498 5.74243 18.711 5.54735 18.6356 5.36534C18.5602 5.18333 18.4496 5.01795 18.3103 4.87866ZM15 8.18897L10.8103 4.00022L13.0603 1.75022L17.25 5.93897L15 8.18897Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</Link>
|
||||
)}
|
||||
</motion.li>
|
||||
);
|
||||
}
|
277
src/app/(main)/agents/_components/agent-list.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { parseAsInteger, useQueryState } from "nuqs";
|
||||
|
||||
import { PrivyInterface, usePrivy } from "@privy-io/react-auth";
|
||||
|
||||
import { BadgePlus, ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import PostgrestError, { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Section } from "@/components/section";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
} from "@/components/ui/pagination";
|
||||
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import AgentCard from "./agent-card";
|
||||
import AgentCardSkeleton from "./agent-card-skeleton";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
const DATA_DISPLAY = 9;
|
||||
|
||||
function AgentListFilter({ privyAuth }: { privyAuth: PrivyInterface }) {
|
||||
const { ready, authenticated } = privyAuth;
|
||||
|
||||
const [searchFilter, setSearchFilter] = useQueryState("search", {
|
||||
defaultValue: "",
|
||||
throttleMs: 1000,
|
||||
shallow: false,
|
||||
history: "push",
|
||||
});
|
||||
|
||||
const [_, setPaginationFilter] = useQueryState(
|
||||
"page",
|
||||
parseAsInteger
|
||||
.withOptions({
|
||||
shallow: false,
|
||||
history: "push",
|
||||
scroll: false,
|
||||
})
|
||||
.withDefault(0)
|
||||
);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: ANIMATION_EASE }}
|
||||
className="pt-1 px-3 sm:px-6 mb-8 flex flex-col sm:flex-row gap-3 justify-between"
|
||||
>
|
||||
<div className="relative">
|
||||
<Input
|
||||
aria-label="search"
|
||||
id="search"
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
className="min-w-64 pl-7"
|
||||
value={searchFilter}
|
||||
onChange={(e) => {
|
||||
setPaginationFilter(0);
|
||||
e.target.value === ""
|
||||
? setSearchFilter(null)
|
||||
: setSearchFilter(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Search className="absolute w-4 h-4 left-2 top-1/2 -translate-y-1/2 text-gray-300" />
|
||||
</div>
|
||||
{!ready ? (
|
||||
<Skeleton className="h-10 w-32" />
|
||||
) : (
|
||||
<>
|
||||
{authenticated ? (
|
||||
<Link
|
||||
href={`/agents/create`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.55189 0.333435H17.4369C21.4036 0.333435 23.6669 2.57343 23.6669 6.55177V17.4484C23.6669 21.4034 21.4152 23.6668 17.4486 23.6668H6.55189C2.57356 23.6668 0.333557 21.4034 0.333557 17.4484V6.55177C0.333557 2.57343 2.57356 0.333435 6.55189 0.333435ZM12.9569 12.9684H16.2702C16.8069 12.9568 17.2386 12.5251 17.2386 11.9884C17.2386 11.4518 16.8069 11.0201 16.2702 11.0201H12.9569V7.7301C12.9569 7.19344 12.5252 6.76177 11.9886 6.76177C11.4519 6.76177 11.0202 7.19344 11.0202 7.7301V11.0201H7.71856C7.46189 11.0201 7.21689 11.1251 7.03022 11.3001C6.85522 11.4868 6.75022 11.7306 6.75022 11.9884C6.75022 12.5251 7.18189 12.9568 7.71856 12.9684H11.0202V16.2701C11.0202 16.8068 11.4519 17.2384 11.9886 17.2384C12.5252 17.2384 12.9569 16.8068 12.9569 16.2701V12.9684Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Create new Agent
|
||||
</Link>
|
||||
) : (
|
||||
<Button
|
||||
disabled
|
||||
className="w-full sm:w-auto text-background flex gap-2"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.55189 0.333435H17.4369C21.4036 0.333435 23.6669 2.57343 23.6669 6.55177V17.4484C23.6669 21.4034 21.4152 23.6668 17.4486 23.6668H6.55189C2.57356 23.6668 0.333557 21.4034 0.333557 17.4484V6.55177C0.333557 2.57343 2.57356 0.333435 6.55189 0.333435ZM12.9569 12.9684H16.2702C16.8069 12.9568 17.2386 12.5251 17.2386 11.9884C17.2386 11.4518 16.8069 11.0201 16.2702 11.0201H12.9569V7.7301C12.9569 7.19344 12.5252 6.76177 11.9886 6.76177C11.4519 6.76177 11.0202 7.19344 11.0202 7.7301V11.0201H7.71856C7.46189 11.0201 7.21689 11.1251 7.03022 11.3001C6.85522 11.4868 6.75022 11.7306 6.75022 11.9884C6.75022 12.5251 7.18189 12.9568 7.71856 12.9684H11.0202V16.2701C11.0202 16.8068 11.4519 17.2384 11.9886 17.2384C12.5252 17.2384 12.9569 16.8068 12.9569 16.2701V12.9684Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Connect your wallet first
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentListCards({ privyAuth }: { privyAuth: PrivyInterface }) {
|
||||
const { user } = privyAuth;
|
||||
|
||||
const [searchFilter] = useQueryState("search", {
|
||||
defaultValue: "",
|
||||
throttleMs: 1000,
|
||||
shallow: false,
|
||||
history: "push",
|
||||
});
|
||||
|
||||
const [paginationFilter, setPaginationFilter] = useQueryState(
|
||||
"page",
|
||||
parseAsInteger
|
||||
.withOptions({
|
||||
shallow: false,
|
||||
history: "push",
|
||||
scroll: false,
|
||||
})
|
||||
.withDefault(0)
|
||||
);
|
||||
|
||||
const {
|
||||
data: agentsData,
|
||||
isLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ["agents", searchFilter, paginationFilter],
|
||||
queryFn: async ({ signal }) => {
|
||||
const supabase = createClient();
|
||||
|
||||
const response = await supabase
|
||||
.from("agents")
|
||||
.select("*", { count: "exact" })
|
||||
.ilike("name", `%${searchFilter}%`)
|
||||
.order("id", { ascending: false })
|
||||
.range(
|
||||
paginationFilter * DATA_DISPLAY,
|
||||
paginationFilter * DATA_DISPLAY + DATA_DISPLAY - 1
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
if (response.error) throw new PostgrestError(response.error);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{agentsData && agentsData.data.length === 0 && (
|
||||
<div className="flex flex-col justify-center items-center ">
|
||||
<figure className="relative w-full sm:w-1/2 h-72 sm:h-96 mb-6">
|
||||
<Image
|
||||
className="object-cover"
|
||||
src="/empty-data.jpg"
|
||||
fill={true}
|
||||
alt="Empty data"
|
||||
/>
|
||||
</figure>
|
||||
<h1 className="text-xl font-bold">No data found</h1>
|
||||
</div>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<motion.ul
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease: ANIMATION_EASE }}
|
||||
className="px-3 sm:px-6 mb-10 grid grid-cols-1 lg:grid-cols-3 gap-6"
|
||||
>
|
||||
{[...Array(9)].map((_, idx) => (
|
||||
<li key={idx}>
|
||||
<AgentCardSkeleton />
|
||||
</li>
|
||||
))}
|
||||
</motion.ul>
|
||||
) : (
|
||||
<ul className="px-3 sm:px-6 mb-10 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{agentsData &&
|
||||
agentsData.data?.map((agent, idx) => (
|
||||
<AgentCard key={agent.name} data={agent} idx={idx} user={user} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{agentsData ? (
|
||||
<Pagination className="mb-8">
|
||||
<PaginationContent className="bg-primary rounded-md p-2">
|
||||
<PaginationItem>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
disabled={paginationFilter === 0}
|
||||
onClick={() => setPaginationFilter((old) => old - 1)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<p className="text-sm font-semibold text-white mx-2">
|
||||
Page {paginationFilter + 1} of{" "}
|
||||
{agentsData.count
|
||||
? Math.ceil(agentsData.count / DATA_DISPLAY)
|
||||
: 1}
|
||||
</p>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
disabled={
|
||||
agentsData.count === 0
|
||||
? true
|
||||
: paginationFilter + 1 ===
|
||||
Math.ceil(agentsData.count! / DATA_DISPLAY)
|
||||
}
|
||||
onClick={() => setPaginationFilter((old) => old + 1)}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentList() {
|
||||
const privyAuth = usePrivy();
|
||||
|
||||
return (
|
||||
<section id="agent-list" className="relative z-20">
|
||||
<div className="layout border-x">
|
||||
<div className="relative w-full overflow-hidden pt-4">
|
||||
<AgentListFilter privyAuth={privyAuth} />
|
||||
</div>
|
||||
<AgentListCards privyAuth={privyAuth} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
31
src/app/(main)/agents/_components/heading.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Section } from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Heading() {
|
||||
return (
|
||||
<section id="heading">
|
||||
<div className="layout py-6 relative w-full border-x overflow-hidden">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">Explore Agents</AuroraText>
|
||||
</motion.h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
110
src/app/(main)/agents/create/_components/agent-form.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { Stepper } from "@stepperize/react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { createAgentSchema } from "../schemas/create-agent-schema";
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
|
||||
type AgentFormProps = {
|
||||
stepper: Stepper<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
form: UseFormReturn<z.infer<typeof createAgentSchema>, any, undefined>;
|
||||
onSubmit: (values: z.infer<typeof createAgentSchema>) => void;
|
||||
isPending: boolean;
|
||||
};
|
||||
|
||||
export default function AgentForm(props: AgentFormProps) {
|
||||
const { stepper, form, onSubmit, isPending } = props;
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof createAgentSchema>) => {
|
||||
onSubmit(values);
|
||||
};
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-3">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Agent Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="astrobot"
|
||||
className={cn(
|
||||
errors.name && "focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder="For answering daily questions."
|
||||
className={cn(
|
||||
errors.description && "focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={stepper.prev}
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
570
src/app/(main)/agents/create/_components/form-stepper.tsx
Normal file
@ -0,0 +1,570 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { useLogin, usePrivy } from "@privy-io/react-auth";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm, UseFormReturn } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
import { Braces, Check, Code, MessageCircle } from "lucide-react";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import confetti from "canvas-confetti";
|
||||
import { defineStepper, Stepper } from "@stepperize/react";
|
||||
import {
|
||||
AnimatedSpan,
|
||||
Terminal,
|
||||
TypingAnimation,
|
||||
} from "@/components/ui/terminal";
|
||||
|
||||
import { createAgentSchema } from "../schemas/create-agent-schema";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
// import CardBackground from "@/components/card-background";
|
||||
import AgentForm from "./agent-form";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: "agentType",
|
||||
title: "Agent Type",
|
||||
description: "Select how your Agent will interact",
|
||||
},
|
||||
{
|
||||
id: "agentForm",
|
||||
title: "Agent Form",
|
||||
description: "Enter your agent informations",
|
||||
},
|
||||
{
|
||||
id: "confirmation",
|
||||
title: "Form Confirmation",
|
||||
description: "Enter your agent informations",
|
||||
},
|
||||
{ id: "complete", title: "Complete", description: "Checkout complete" },
|
||||
];
|
||||
|
||||
const { useStepper, steps, utils } = defineStepper(...data);
|
||||
|
||||
const triggerConfetti = () => {
|
||||
const duration = 5 * 1000;
|
||||
const animationEnd = Date.now() + duration;
|
||||
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
||||
|
||||
const randomInRange = (min: number, max: number) =>
|
||||
Math.random() * (max - min) + min;
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval);
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
|
||||
});
|
||||
confetti({
|
||||
...defaults,
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
|
||||
});
|
||||
}, 250);
|
||||
};
|
||||
|
||||
export function FormStepper() {
|
||||
const [agentId, setAgentId] = React.useState("");
|
||||
const [agentName, setAgenName] = React.useState("");
|
||||
|
||||
const { ready, authenticated, logout, user } = usePrivy();
|
||||
|
||||
const stepper = useStepper();
|
||||
|
||||
const currentIndex = utils.getIndex(stepper.current.id);
|
||||
|
||||
const form = useForm<z.infer<typeof createAgentSchema>>({
|
||||
resolver: zodResolver(createAgentSchema),
|
||||
defaultValues: {
|
||||
userId: "",
|
||||
name: "",
|
||||
description: "",
|
||||
modelType: "general",
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createAgentMutation = useMutation({
|
||||
mutationFn: async (values: z.infer<typeof createAgentSchema>) => {
|
||||
const { modelType, userId, ...rest } = values;
|
||||
|
||||
const supabase = createClient();
|
||||
|
||||
const url = encodeURI(
|
||||
`https://ai-endpoint-one.dev3vds1.link/ai-image/${values.description}`
|
||||
);
|
||||
|
||||
const json = await axios.get(url);
|
||||
|
||||
const response = await supabase
|
||||
.from("agents")
|
||||
.insert({
|
||||
...rest,
|
||||
model_type: "general",
|
||||
image_url: json.data.url,
|
||||
user_id: user!.id,
|
||||
conversation: 0,
|
||||
last_conv: null,
|
||||
})
|
||||
.select()
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (response.error) throw new Error(response.error.message);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: (data: Tables<"agents">) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["agents"],
|
||||
});
|
||||
triggerConfetti();
|
||||
stepper.next();
|
||||
|
||||
setAgentId(data.id.toString());
|
||||
setAgenName(data.name);
|
||||
|
||||
stepper.next();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!user) {
|
||||
toast.error("You are not login yet");
|
||||
return;
|
||||
}
|
||||
const values = form.getValues();
|
||||
createAgentMutation.mutate(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="layout border-x px-3 sm:px-6 py-8">
|
||||
<motion.div
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
delay: 0.5,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
{/* form title */}
|
||||
<div className="flex justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-primary text-lg font-bold mb-1">
|
||||
Agent Form
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{stepper.current.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* form nav */}
|
||||
<nav aria-label="Checkout Steps" className="group mb-8">
|
||||
<ol
|
||||
className="flex items-center justify-between gap-2"
|
||||
aria-orientation="horizontal"
|
||||
>
|
||||
{stepper.all.map((step, index, array) => (
|
||||
<React.Fragment key={step.id}>
|
||||
<li className="flex items-center gap-4 flex-shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
role="tab"
|
||||
variant={index <= currentIndex ? "default" : "outline"}
|
||||
aria-current={
|
||||
stepper.current.id === step.id ? "step" : undefined
|
||||
}
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={steps.length}
|
||||
aria-selected={stepper.current.id === step.id}
|
||||
className={cn(
|
||||
"flex size-8 items-center justify-center rounded-full",
|
||||
index > currentIndex && "border border-dashed"
|
||||
)}
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<span className="text-sm font-bold">{step.title}</span>
|
||||
</li>
|
||||
{index < array.length - 1 && (
|
||||
<Separator
|
||||
className={`border-t border-dashed flex-1 ${
|
||||
index < currentIndex ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
{/* form content */}
|
||||
<div className="space-y-8">
|
||||
{stepper.switch({
|
||||
agentType: () => <AgentTypeComponent stepper={stepper} />,
|
||||
agentForm: () => (
|
||||
<AgentFormComponent
|
||||
stepper={stepper}
|
||||
form={form}
|
||||
isPending={createAgentMutation.isPending}
|
||||
// onSubmit={handleSubmit}
|
||||
onSubmit={() => stepper.next()}
|
||||
/>
|
||||
),
|
||||
confirmation: () => (
|
||||
<AgentConfirmationComponent
|
||||
stepper={stepper}
|
||||
form={form}
|
||||
isPending={createAgentMutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
),
|
||||
complete: () => (
|
||||
<CompleteComponent agentId={agentId} agentName={agentName} />
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const AgentTypeComponent = ({
|
||||
stepper,
|
||||
}: {
|
||||
stepper: Stepper<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
}) => {
|
||||
const agentTypes = [
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="65"
|
||||
height="56"
|
||||
viewBox="0 0 65 56"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M60 0.5H5C3.67392 0.5 2.40215 1.02678 1.46447 1.96447C0.526784 2.90215 0 4.17392 0 5.5V50.5C0 51.8261 0.526784 53.0979 1.46447 54.0355C2.40215 54.9732 3.67392 55.5 5 55.5H60C61.3261 55.5 62.5979 54.9732 63.5355 54.0355C64.4732 53.0979 65 51.8261 65 50.5V5.5C65 4.17392 64.4732 2.90215 63.5355 1.96447C62.5979 1.02678 61.3261 0.5 60 0.5ZM24.2688 38.7312C24.501 38.9635 24.6853 39.2393 24.811 39.5428C24.9367 39.8462 25.0014 40.1715 25.0014 40.5C25.0014 40.8285 24.9367 41.1538 24.811 41.4572C24.6853 41.7607 24.501 42.0365 24.2688 42.2688C24.0365 42.501 23.7607 42.6853 23.4572 42.811C23.1538 42.9367 22.8285 43.0014 22.5 43.0014C22.1715 43.0014 21.8462 42.9367 21.5428 42.811C21.2393 42.6853 20.9635 42.501 20.7312 42.2688L8.23125 29.7688C7.99881 29.5366 7.81441 29.2608 7.6886 28.9574C7.56279 28.6539 7.49803 28.3285 7.49803 28C7.49803 27.6715 7.56279 27.3461 7.6886 27.0426C7.81441 26.7392 7.99881 26.4634 8.23125 26.2312L20.7312 13.7312C21.2003 13.2621 21.8366 12.9986 22.5 12.9986C23.1634 12.9986 23.7997 13.2621 24.2688 13.7312C24.7379 14.2004 25.0014 14.8366 25.0014 15.5C25.0014 16.1634 24.7379 16.7997 24.2688 17.2688L13.5344 28L24.2688 38.7312ZM56.7688 29.7688L44.2688 42.2688C43.7996 42.7379 43.1634 43.0014 42.5 43.0014C41.8366 43.0014 41.2004 42.7379 40.7312 42.2688C40.2621 41.7996 39.9986 41.1634 39.9986 40.5C39.9986 39.8366 40.2621 39.2004 40.7312 38.7312L51.4656 28L40.7312 17.2688C40.2621 16.7997 39.9986 16.1634 39.9986 15.5C39.9986 14.8366 40.2621 14.2004 40.7312 13.7312C41.2004 13.2621 41.8366 12.9986 42.5 12.9986C43.1634 12.9986 43.7996 13.2621 44.2688 13.7312L56.7688 26.2312C57.0012 26.4634 57.1856 26.7392 57.3114 27.0426C57.4372 27.3461 57.502 27.6715 57.502 28C57.502 28.3285 57.4372 28.6539 57.3114 28.9574C57.1856 29.2608 57.0012 29.5366 56.7688 29.7688Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "Chat",
|
||||
description: "Access from our website",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="65"
|
||||
height="56"
|
||||
viewBox="0 0 65 56"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M60 0.5H5C3.67392 0.5 2.40215 1.02678 1.46447 1.96447C0.526784 2.90215 0 4.17392 0 5.5V50.5C0 51.8261 0.526784 53.0979 1.46447 54.0355C2.40215 54.9732 3.67392 55.5 5 55.5H60C61.3261 55.5 62.5979 54.9732 63.5355 54.0355C64.4732 53.0979 65 51.8261 65 50.5V5.5C65 4.17392 64.4732 2.90215 63.5355 1.96447C62.5979 1.02678 61.3261 0.5 60 0.5ZM24.2688 38.7312C24.501 38.9635 24.6853 39.2393 24.811 39.5428C24.9367 39.8462 25.0014 40.1715 25.0014 40.5C25.0014 40.8285 24.9367 41.1538 24.811 41.4572C24.6853 41.7607 24.501 42.0365 24.2688 42.2688C24.0365 42.501 23.7607 42.6853 23.4572 42.811C23.1538 42.9367 22.8285 43.0014 22.5 43.0014C22.1715 43.0014 21.8462 42.9367 21.5428 42.811C21.2393 42.6853 20.9635 42.501 20.7312 42.2688L8.23125 29.7688C7.99881 29.5366 7.81441 29.2608 7.6886 28.9574C7.56279 28.6539 7.49803 28.3285 7.49803 28C7.49803 27.6715 7.56279 27.3461 7.6886 27.0426C7.81441 26.7392 7.99881 26.4634 8.23125 26.2312L20.7312 13.7312C21.2003 13.2621 21.8366 12.9986 22.5 12.9986C23.1634 12.9986 23.7997 13.2621 24.2688 13.7312C24.7379 14.2004 25.0014 14.8366 25.0014 15.5C25.0014 16.1634 24.7379 16.7997 24.2688 17.2688L13.5344 28L24.2688 38.7312ZM56.7688 29.7688L44.2688 42.2688C43.7996 42.7379 43.1634 43.0014 42.5 43.0014C41.8366 43.0014 41.2004 42.7379 40.7312 42.2688C40.2621 41.7996 39.9986 41.1634 39.9986 40.5C39.9986 39.8366 40.2621 39.2004 40.7312 38.7312L51.4656 28L40.7312 17.2688C40.2621 16.7997 39.9986 16.1634 39.9986 15.5C39.9986 14.8366 40.2621 14.2004 40.7312 13.7312C41.2004 13.2621 41.8366 12.9986 42.5 12.9986C43.1634 12.9986 43.7996 13.2621 44.2688 13.7312L56.7688 26.2312C57.0012 26.4634 57.1856 26.7392 57.3114 27.0426C57.4372 27.3461 57.502 27.6715 57.502 28C57.502 28.3285 57.4372 28.6539 57.3114 28.9574C57.1856 29.2608 57.0012 29.5366 56.7688 29.7688Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "CLI",
|
||||
description: "Access from terminal",
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg
|
||||
width="60"
|
||||
height="66"
|
||||
viewBox="0 0 60 66"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M30 0.5C13.1781 0.5 0 8.1875 0 18V48C0 57.8125 13.1781 65.5 30 65.5C46.8219 65.5 60 57.8125 60 48V18C60 8.1875 46.8219 0.5 30 0.5ZM55 33C55 36.0062 52.5375 39.0719 48.2469 41.4125C43.4156 44.0469 36.9344 45.5 30 45.5C23.0656 45.5 16.5844 44.0469 11.7531 41.4125C7.4625 39.0719 5 36.0062 5 33V27.8C10.3313 32.4875 19.4469 35.5 30 35.5C40.5531 35.5 49.6688 32.475 55 27.8V33ZM48.2469 56.4125C43.4156 59.0469 36.9344 60.5 30 60.5C23.0656 60.5 16.5844 59.0469 11.7531 56.4125C7.4625 54.0719 5 51.0062 5 48V42.8C10.3313 47.4875 19.4469 50.5 30 50.5C40.5531 50.5 49.6688 47.475 55 42.8V48C55 51.0062 52.5375 54.0719 48.2469 56.4125Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
heading: "API",
|
||||
description: "Access with API",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
{agentTypes.map((agentType, idx) => (
|
||||
<Button
|
||||
disabled={idx !== 0}
|
||||
key={idx}
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
idx === 0 &&
|
||||
"bg-background rounded-xl transition-all duration-300 border shadow-[0_0_23.1px_0_rgba(0,0,0,0.08)_inset]",
|
||||
"py-14 px-6 border-2"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{agentType.icon}
|
||||
<div className="space-y-3 text-left">
|
||||
<p className="text-xl font-bold">{agentType.heading}</p>
|
||||
<p className="text-muted-foreground">{agentType.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{/* form footer */}
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="outline" disabled={true}>
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={stepper.next}>Next</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentFormComponent = ({
|
||||
stepper,
|
||||
form,
|
||||
onSubmit,
|
||||
isPending,
|
||||
}: {
|
||||
stepper: Stepper<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
form: UseFormReturn<z.infer<typeof createAgentSchema>, any, undefined>;
|
||||
onSubmit: (values: z.infer<typeof createAgentSchema>) => void;
|
||||
isPending: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<AgentForm
|
||||
stepper={stepper}
|
||||
form={form}
|
||||
onSubmit={onSubmit}
|
||||
isPending={isPending}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const AgentConfirmationComponent = ({
|
||||
stepper,
|
||||
form,
|
||||
onSubmit,
|
||||
isPending,
|
||||
}: {
|
||||
stepper: Stepper<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
form: UseFormReturn<z.infer<typeof createAgentSchema>, any, undefined>;
|
||||
onSubmit: () => void;
|
||||
isPending: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Terminal className="max-w-none max-h-none">
|
||||
<TypingAnimation duration={30}>
|
||||
> This action will create your agent with following details:
|
||||
</TypingAnimation>
|
||||
<AnimatedSpan delay={2000}>
|
||||
<p>
|
||||
> Agent Name:{" "}
|
||||
<span className="text-green-500">{form.getValues("name")}</span>
|
||||
</p>
|
||||
</AnimatedSpan>
|
||||
<AnimatedSpan delay={2500}>
|
||||
<p>
|
||||
> Agent Desccription:{" "}
|
||||
<span className="text-green-500">
|
||||
{form.getValues("description")}
|
||||
</span>
|
||||
</p>
|
||||
</AnimatedSpan>
|
||||
<TypingAnimation delay={3000}>
|
||||
> Click "Submit" to create the agent
|
||||
</TypingAnimation>
|
||||
</Terminal>
|
||||
{/* form footer */}
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="outline" onClick={stepper.prev} disabled={isPending}>
|
||||
{isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={() => onSubmit()} disabled={isPending}>
|
||||
{isPending && <Spinner className="h-4 w-4 mr-2" />}
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CompleteComponent = ({
|
||||
agentId,
|
||||
agentName,
|
||||
}: {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-center items-center py-4">
|
||||
<div className="h-20 w-20 flex flex-col justify-center items-center rounded-full bg-primary">
|
||||
<svg
|
||||
width="61"
|
||||
height="41"
|
||||
viewBox="0 0 61 41"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="0.5"
|
||||
y="14.6426"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="6.35693"
|
||||
y="19.0342"
|
||||
width="7.32129"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="13.6787"
|
||||
y="27.8223"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="19.5354"
|
||||
y="32.2148"
|
||||
width="7.32129"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="26.8567"
|
||||
y="27.8223"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="32.7136"
|
||||
y="19.0342"
|
||||
width="8.78555"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="41.4993"
|
||||
y="14.6426"
|
||||
width="5.85703"
|
||||
height="4.39277"
|
||||
fill="white"
|
||||
/>
|
||||
<rect
|
||||
x="47.3562"
|
||||
y="5.85645"
|
||||
width="8.78555"
|
||||
height="8.78555"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="56.1418" width="4.39277" height="5.85703" fill="white" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl py-4 font-bold">Agent succesfully created!</h3>
|
||||
<p className="text-lg text-muted-foreground mb-3">
|
||||
You can try to have a conversation with your agent.
|
||||
</p>
|
||||
<Link
|
||||
href={`/agents/${agentId}`}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="mr-2"
|
||||
>
|
||||
<path
|
||||
d="M8.50006 0.875C7.0973 0.874694 5.71836 1.23757 4.49745 1.9283C3.27654 2.61904 2.25526 3.6141 1.53301 4.81664C0.810765 6.01918 0.412159 7.38822 0.375993 8.79052C0.339826 10.1928 0.66733 11.5806 1.32663 12.8188L0.439908 15.4789C0.366459 15.6992 0.355799 15.9355 0.409125 16.1615C0.462451 16.3874 0.577655 16.5941 0.741823 16.7582C0.905992 16.9224 1.11264 17.0376 1.3386 17.0909C1.56456 17.1443 1.80091 17.1336 2.02116 17.0602L4.68131 16.1734C5.77098 16.753 6.97836 17.0767 8.21181 17.12C9.44525 17.1633 10.6724 16.925 11.8 16.4232C12.9276 15.9215 13.926 15.1694 14.7196 14.2241C15.5132 13.2789 16.0809 12.1652 16.3799 10.9678C16.6788 9.77029 16.7009 8.52047 16.4447 7.31315C16.1885 6.10584 15.6605 4.97276 14.901 3.99993C14.1415 3.02711 13.1703 2.24009 12.0612 1.69864C10.9521 1.15718 9.73427 0.875506 8.50006 0.875ZM5.06256 9.9375C4.87714 9.9375 4.69589 9.88252 4.54172 9.7795C4.38755 9.67649 4.26739 9.53007 4.19643 9.35876C4.12547 9.18746 4.1069 8.99896 4.14308 8.8171C4.17925 8.63525 4.26854 8.4682 4.39965 8.33709C4.53076 8.20598 4.69781 8.11669 4.87967 8.08051C5.06152 8.04434 5.25002 8.06291 5.42133 8.13386C5.59264 8.20482 5.73905 8.32498 5.84207 8.47915C5.94508 8.63332 6.00006 8.81458 6.00006 9C6.00006 9.24864 5.90129 9.4871 5.72548 9.66291C5.54966 9.83873 5.31121 9.9375 5.06256 9.9375ZM8.50006 9.9375C8.31464 9.9375 8.13339 9.88252 7.97922 9.7795C7.82505 9.67649 7.70489 9.53007 7.63393 9.35876C7.56297 9.18746 7.5444 8.99896 7.58058 8.8171C7.61675 8.63525 7.70604 8.4682 7.83715 8.33709C7.96826 8.20598 8.13531 8.11669 8.31717 8.08051C8.49902 8.04434 8.68752 8.06291 8.85883 8.13386C9.03014 8.20482 9.17655 8.32498 9.27957 8.47915C9.38258 8.63332 9.43756 8.81458 9.43756 9C9.43756 9.24864 9.33879 9.4871 9.16298 9.66291C8.98716 9.83873 8.74871 9.9375 8.50006 9.9375ZM11.9376 9.9375C11.7521 9.9375 11.5709 9.88252 11.4167 9.7795C11.2625 9.67649 11.1424 9.53007 11.0714 9.35876C11.0005 9.18746 10.9819 8.99896 11.0181 8.8171C11.0543 8.63525 11.1435 8.4682 11.2747 8.33709C11.4058 8.20598 11.5728 8.11669 11.7547 8.08051C11.9365 8.04434 12.125 8.06291 12.2963 8.13386C12.4676 8.20482 12.6141 8.32498 12.7171 8.47915C12.8201 8.63332 12.8751 8.81458 12.8751 9C12.8751 9.24864 12.7763 9.4871 12.6005 9.66291C12.4247 9.83873 12.1862 9.9375 11.9376 9.9375Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
Chat with {agentName}
|
||||
</Link>
|
||||
</div>
|
||||
{/* form footer */}
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Link href="/" className={buttonVariants({ variant: "default" })}>
|
||||
Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
31
src/app/(main)/agents/create/_components/heading.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { ANIMATION_EASE } from "@/lib/config";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Section } from "@/components/section";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Heading() {
|
||||
return (
|
||||
<section id="heading">
|
||||
<div className="layout py-6 relative w-full border-x overflow-hidden">
|
||||
<motion.h1
|
||||
className={cn(
|
||||
"text-center text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
)}
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease: ANIMATION_EASE,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">Create you Own Agent</AuroraText>
|
||||
</motion.h1>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
17
src/app/(main)/agents/create/page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { CheckSessionProvider } from "@/providers/check-session-provider";
|
||||
|
||||
import { Footer } from "@/components/footer";
|
||||
import { FormStepper } from "./_components/form-stepper";
|
||||
import { Heading } from "./_components/heading";
|
||||
|
||||
export default function CreateAgent() {
|
||||
return (
|
||||
<CheckSessionProvider>
|
||||
<Heading />
|
||||
<hr className="" />
|
||||
<FormStepper />
|
||||
<hr className="" />
|
||||
<Footer />
|
||||
</CheckSessionProvider>
|
||||
);
|
||||
}
|
40
src/app/(main)/agents/create/schemas/create-agent-schema.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const createAgentSchema = z.object({
|
||||
userId: z.string(),
|
||||
name: z
|
||||
.string({
|
||||
required_error: "Name is required",
|
||||
})
|
||||
.min(2, {
|
||||
message: "Minimum Name is 2 characters.",
|
||||
})
|
||||
.max(30, {
|
||||
message: "Maximum Name is 30 characters.",
|
||||
}),
|
||||
description: z
|
||||
.string({
|
||||
required_error: "Description is required",
|
||||
})
|
||||
.min(2, {
|
||||
message: "Minimum Description is 2 characters.",
|
||||
})
|
||||
.max(200, {
|
||||
message: "Maximum Description is 200 characters.",
|
||||
}),
|
||||
modelType: z.enum([
|
||||
"roleplay",
|
||||
"programming",
|
||||
"marketing",
|
||||
"marketing_seo",
|
||||
"technology",
|
||||
"science",
|
||||
"translation",
|
||||
"legal",
|
||||
"finance",
|
||||
"health",
|
||||
"trivia",
|
||||
"academia",
|
||||
"general",
|
||||
]),
|
||||
});
|
26
src/app/(main)/agents/page.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { Section } from "@/components/section";
|
||||
|
||||
import { Heading } from "./_components/heading";
|
||||
|
||||
// import { Blog } from "../_components/blog";
|
||||
import { AgentList } from "./_components/agent-list";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function Explore() {
|
||||
return (
|
||||
<>
|
||||
<Heading />
|
||||
<hr className="" />
|
||||
<div className="relative">
|
||||
<Suspense>
|
||||
<AgentList />
|
||||
</Suspense>
|
||||
<hr className="" />
|
||||
<div className="absolute -bottom-64 left-0 w-full bg-[url(/background-2.png)] bg-center pt-72">
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
14
src/app/(main)/layout.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { ChatFloatingButton } from "@/components/chat-floating-button";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: LayoutProps) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{/* <ChatFloatingButton /> */}
|
||||
</>
|
||||
);
|
||||
}
|
28
src/app/(main)/page.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Hero } from "./_components/hero";
|
||||
import { Features } from "./_components/features";
|
||||
import { CTA } from "./_components/cta";
|
||||
import { Tokenomics } from "./_components/tokenomics";
|
||||
import { Roadmap } from "./_components/roadmap";
|
||||
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div className="bg-[url(/background-1.png)]">
|
||||
<Hero />
|
||||
<Features />
|
||||
<hr className="" />
|
||||
</div>
|
||||
<Tokenomics />
|
||||
<hr className="" />
|
||||
<Roadmap />
|
||||
<hr className="" />
|
||||
<div className="bg-[url(/background-2.png)] bg-bottom">
|
||||
<CTA />
|
||||
<hr className="" />
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
import Author from "@/components/blog-author";
|
||||
import { CTA } from "@/components/sections/cta";
|
||||
import { getPost } from "@/lib/blog";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata | undefined> {
|
||||
const params = await props.params;
|
||||
let post = await getPost(params.slug);
|
||||
let {
|
||||
title,
|
||||
publishedAt: publishedTime,
|
||||
summary: description,
|
||||
image,
|
||||
} = post.metadata;
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title,
|
||||
description,
|
||||
type: "article",
|
||||
publishedTime,
|
||||
url: `${siteConfig.url}/blog/${post.slug}`,
|
||||
images: [
|
||||
{
|
||||
url: image,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title,
|
||||
description,
|
||||
images: [image],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ slug: string }>;
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
const post = await getPost(params.slug);
|
||||
if (!post) {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<section id="blog">
|
||||
<script
|
||||
type="application/ld+json"
|
||||
suppressHydrationWarning
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: post.metadata.title,
|
||||
datePublished: post.metadata.publishedAt,
|
||||
dateModified: post.metadata.publishedAt,
|
||||
description: post.metadata.summary,
|
||||
image: post.metadata.image
|
||||
? `${siteConfig.url}${post.metadata.image}`
|
||||
: `${siteConfig.url}/blog/${post.slug}/opengraph-image`,
|
||||
url: `${siteConfig.url}/blog/${post.slug}`,
|
||||
author: {
|
||||
"@type": "Person",
|
||||
name: siteConfig.name,
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<div className="mx-auto w-full max-w-[800px] px-4 sm:px-6 lg:px-8 space-y-4 my-12">
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="mb-8 w-full h-64 bg-muted animate-pulse rounded-lg"></div>
|
||||
}
|
||||
>
|
||||
{post.metadata.image && (
|
||||
<div className="mb-8">
|
||||
<Image
|
||||
width={1920}
|
||||
height={1080}
|
||||
src={post.metadata.image}
|
||||
alt={post.metadata.title}
|
||||
className="w-full h-auto rounded-lg border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Suspense>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="title font-medium text-3xl tracking-tighter">
|
||||
{post.metadata.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<Suspense fallback={<p className="h-5" />}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<time dateTime={post.metadata.publishedAt} className="text-sm">
|
||||
{formatDate(post.metadata.publishedAt)}
|
||||
</time>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Author
|
||||
twitterUsername={post.metadata.author}
|
||||
name={post.metadata.author}
|
||||
image={"/author.jpg"}
|
||||
/>
|
||||
</div>
|
||||
<article
|
||||
className="prose dark:prose-invert mx-auto max-w-full"
|
||||
dangerouslySetInnerHTML={{ __html: post.source }}
|
||||
></article>
|
||||
</div>
|
||||
<CTA />
|
||||
</section>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Footer } from "@/components/sections/footer";
|
||||
import { Header } from "@/components/sections/header";
|
||||
|
||||
interface MarketingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: MarketingLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import BlogCard from "@/components/blog-card";
|
||||
import { getBlogPosts } from "@/lib/blog";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { constructMetadata } from "@/lib/utils";
|
||||
|
||||
export const metadata = constructMetadata({
|
||||
title: "Blog",
|
||||
description: `Latest news and updates from ${siteConfig.name}.`,
|
||||
});
|
||||
|
||||
export default async function Blog() {
|
||||
const allPosts = await getBlogPosts();
|
||||
|
||||
const articles = await Promise.all(
|
||||
allPosts.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto w-full max-w-screen-xl px-2.5 lg:px-20 mt-24">
|
||||
<div className="text-center py-16">
|
||||
<h1 className="text-3xl font-bold text-foreground sm:text-4xl">
|
||||
Articles
|
||||
</h1>
|
||||
<p className="mt-4 text-xl text-muted-foreground">
|
||||
Latest news and updates from {siteConfig.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[50vh] bg-white/50 shadow-[inset_10px_-50px_94px_0_rgb(199,199,199,0.2)] backdrop-blur-lg">
|
||||
<div className="mx-auto grid w-full max-w-screen-xl grid-cols-1 gap-8 px-2.5 py-10 lg:px-20 lg:grid-cols-3">
|
||||
{articles.map((data, idx) => (
|
||||
<BlogCard key={data.slug} data={data} priority={idx <= 1} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
11
src/app/fonts.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Montserrat, Space_Grotesk } from "next/font/google";
|
||||
|
||||
export const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const spaceGrotesk = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
@ -4,18 +4,18 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 210 20% 98%;
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 210 20% 98%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 210 20% 98%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 261 75.8% 75.7%;
|
||||
--primary: 0 0% 13%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 215 20% 95%;
|
||||
--secondary-foreground: 215 25% 10%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--muted-foreground: 0 0% 46%;
|
||||
--accent: 215 20% 95%;
|
||||
--accent-foreground: 215 25% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
@ -123,3 +123,15 @@
|
||||
background: hsl(var(--border));
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
width: 90%;
|
||||
max-width: 1168px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-padding-top: 5.5rem;
|
||||
}
|
@ -1,13 +1,27 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
||||
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { Header } from "@/components/header";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn, constructMetadata } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
|
||||
import ReactQueryProvider from "@/providers/react-query-provider";
|
||||
import MyPrivyProvider from "@/providers/privy-provider";
|
||||
|
||||
// import { GeistMono } from "geist/font/mono";
|
||||
// import { GeistSans } from "geist/font/sans";
|
||||
import { montserrat } from "./fonts";
|
||||
|
||||
import "./globals.css";
|
||||
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
export const metadata: Metadata = constructMetadata({
|
||||
title: `${siteConfig.name} | ${siteConfig.description}`,
|
||||
});
|
||||
@ -26,25 +40,32 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
suppressHydrationWarning
|
||||
className={`${GeistSans.variable} ${GeistMono.variable}`}
|
||||
>
|
||||
<html lang="en" suppressHydrationWarning className={montserrat.className}>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background antialiased w-full mx-auto scroll-smooth font-sans"
|
||||
"min-h-screen bg-background antialiased w-full mx-auto scroll-smooth",
|
||||
montserrat.className
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem={false}
|
||||
>
|
||||
{children}
|
||||
<ThemeToggle />
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
<ReactQueryProvider>
|
||||
<MyPrivyProvider>
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
enableSystem={false}
|
||||
>
|
||||
<Header />
|
||||
{/* <main></main> */}
|
||||
{children}
|
||||
{/* <Footer /> */}
|
||||
<ThemeToggle />
|
||||
<TailwindIndicator />
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
</MyPrivyProvider>
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -1,33 +0,0 @@
|
||||
import { Blog } from "@/components/sections/blog";
|
||||
import { Community } from "@/components/sections/community";
|
||||
import { CTA } from "@/components/sections/cta";
|
||||
import { Examples } from "@/components/sections/examples";
|
||||
import { Features } from "@/components/sections/features";
|
||||
import { Footer } from "@/components/sections/footer";
|
||||
import { Header } from "@/components/sections/header";
|
||||
import { Hero } from "@/components/sections/hero";
|
||||
import { Logos } from "@/components/sections/logos";
|
||||
import { Pricing } from "@/components/sections/pricing";
|
||||
import { Statistics } from "@/components/sections/statistics";
|
||||
import { Testimonials } from "@/components/sections/testimonials";
|
||||
import { UseCases } from "@/components/sections/use-cases";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<Header />
|
||||
<Hero />
|
||||
<Logos />
|
||||
<Examples />
|
||||
<UseCases />
|
||||
<Features />
|
||||
<Statistics />
|
||||
<Testimonials />
|
||||
<Pricing />
|
||||
<Community />
|
||||
<Blog />
|
||||
<CTA />
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
60
src/components/blur-fade.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, useInView, Variants } from "framer-motion";
|
||||
import { useRef } from "react";
|
||||
|
||||
type MarginType = `${number}${"px" | "%"}`;
|
||||
|
||||
interface BlurFadeProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: {
|
||||
hidden: { y: number };
|
||||
visible: { y: number };
|
||||
};
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
yOffset?: number;
|
||||
inView?: boolean;
|
||||
inViewMargin?: MarginType;
|
||||
blur?: string;
|
||||
}
|
||||
export default function BlurFade({
|
||||
children,
|
||||
className,
|
||||
variant,
|
||||
duration = 0.4,
|
||||
delay = 0,
|
||||
yOffset = 6,
|
||||
inView = false,
|
||||
inViewMargin = "-50px",
|
||||
blur = "6px",
|
||||
}: BlurFadeProps) {
|
||||
const ref = useRef(null);
|
||||
const inViewResult = useInView(ref, { once: true, margin: inViewMargin });
|
||||
const isInView = !inView || inViewResult;
|
||||
const defaultVariants: Variants = {
|
||||
hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` },
|
||||
visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` },
|
||||
};
|
||||
const combinedVariants = variant || defaultVariants;
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial="hidden"
|
||||
animate={isInView ? "visible" : "hidden"}
|
||||
exit="hidden"
|
||||
variants={combinedVariants}
|
||||
transition={{
|
||||
delay: 0.04 + delay,
|
||||
duration,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
142
src/components/chat-floating-button.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import { useState, FormEvent } from "react";
|
||||
import { Send, Bot, Paperclip, Mic, CornerDownLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ChatBubble,
|
||||
ChatBubbleAvatar,
|
||||
ChatBubbleMessage,
|
||||
} from "@/components/ui/chat-bubble";
|
||||
import { ChatInput } from "@/components/ui/chat-input";
|
||||
import {
|
||||
ExpandableChat,
|
||||
ExpandableChatHeader,
|
||||
ExpandableChatBody,
|
||||
ExpandableChatFooter,
|
||||
} from "@/components/ui/expandable-chat";
|
||||
import { ChatMessageList } from "@/components/ui/chat-message-list";
|
||||
|
||||
export function ChatFloatingButton() {
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
id: 1,
|
||||
content: "Hello! How can I help you today?",
|
||||
sender: "ai",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: "I have a question about the component library.",
|
||||
sender: "user",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: "Sure! I'd be happy to help. What would you like to know?",
|
||||
sender: "ai",
|
||||
},
|
||||
]);
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim()) return;
|
||||
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length + 1,
|
||||
content: input,
|
||||
sender: "user",
|
||||
},
|
||||
]);
|
||||
setInput("");
|
||||
setIsLoading(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length + 1,
|
||||
content: "This is an AI response to your message.",
|
||||
sender: "ai",
|
||||
},
|
||||
]);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[600px] relative">
|
||||
<ExpandableChat
|
||||
size="lg"
|
||||
position="bottom-right"
|
||||
icon={<Bot className="h-6 w-6" />}
|
||||
className="sm:h-[80vh]"
|
||||
>
|
||||
<ExpandableChatHeader className="flex-col text-center justify-center">
|
||||
<h1 className="text-xl font-semibold">Chat with AI ✨ bro</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Ask me anything about the components
|
||||
</p>
|
||||
</ExpandableChatHeader>
|
||||
<ExpandableChatBody>
|
||||
<ChatMessageList>
|
||||
{messages.map((message) => (
|
||||
<ChatBubble
|
||||
key={message.id}
|
||||
variant={message.sender === "user" ? "sent" : "received"}
|
||||
>
|
||||
<ChatBubbleAvatar
|
||||
className="h-8 w-8 shrink-0"
|
||||
src={
|
||||
message.sender === "user"
|
||||
? "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=64&h=64&q=80&crop=faces&fit=crop"
|
||||
: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&q=80&crop=faces&fit=crop"
|
||||
}
|
||||
fallback={message.sender === "user" ? "US" : "AI"}
|
||||
/>
|
||||
<ChatBubbleMessage
|
||||
variant={message.sender === "user" ? "sent" : "received"}
|
||||
>
|
||||
{message.content}
|
||||
</ChatBubbleMessage>
|
||||
</ChatBubble>
|
||||
))}
|
||||
|
||||
{isLoading && (
|
||||
<ChatBubble variant="received">
|
||||
<ChatBubbleAvatar
|
||||
className="h-8 w-8 shrink-0"
|
||||
src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=64&h=64&q=80&crop=faces&fit=crop"
|
||||
fallback="AI"
|
||||
/>
|
||||
<ChatBubbleMessage isLoading />
|
||||
</ChatBubble>
|
||||
)}
|
||||
</ChatMessageList>
|
||||
</ExpandableChatBody>
|
||||
<ExpandableChatFooter>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="relative rounded-lg border bg-background focus-within:ring-1 focus-within:ring-ring p-1"
|
||||
>
|
||||
<ChatInput
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Type your message..."
|
||||
className="min-h-12 resize-none rounded-lg bg-background border-0 p-3 shadow-none focus-visible:ring-0"
|
||||
/>
|
||||
<div className="flex items-center p-3 pt-0 justify-between">
|
||||
<Button type="submit" size="sm" className="ml-auto gap-1.5">
|
||||
Send Message
|
||||
<CornerDownLeft className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ExpandableChatFooter>
|
||||
</ExpandableChat>
|
||||
</div>
|
||||
);
|
||||
}
|
57
src/components/footer.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { Twitter } from "lucide-react";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
|
||||
import { siteConfig } from "@/lib/config";
|
||||
|
||||
import BlurFade from "./blur-fade";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<BlurFade inView>
|
||||
<footer className="pb-16 pt-8">
|
||||
<div className="layout ">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-x-2 p-2 rounded-md bg-[#D7D7D799] bg-opacity-60 shadow-[0_4px_7px_0_rgba(255,255,255,0.12)_inset] backdrop-blur-xs">
|
||||
<figure className="relative h-[23px] w-[18px] sm:h-[43px] sm:w-[38px]">
|
||||
<Image src={"/logo.png"} alt="logo" fill={true} />
|
||||
</figure>
|
||||
<h2 className="text-base sm:text-3xl font-bold">
|
||||
{siteConfig.name}
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="flex items-center gap-5 p-3 rounded-md bg-[#D7D7D799] bg-opacity-60 shadow-[0_4px_7px_0_rgba(255,255,255,0.12)_inset] backdrop-blur-xs">
|
||||
<li>
|
||||
<Link href={"https://x.com/home?lang=id"}>
|
||||
<GitHubLogoIcon className="h-6 w-6 sm:h-10 sm:w-10" />
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href={"https://x.com/home?lang=id"}>
|
||||
<svg
|
||||
// width="53"
|
||||
// height="54"
|
||||
viewBox="0 0 53 54"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6 sm:h-10 sm:w-10"
|
||||
>
|
||||
<path
|
||||
d="M41.7189 0.314453H49.8072L32.1406 22.81L53 53.3145H36.6104L23.8394 34.8233L9.15261 53.3145H1.06426L20.008 29.2878L0 0.314453H16.8153L28.4157 17.2745L41.7189 0.314453ZM38.8454 47.8967H43.3153L14.3675 5.3789H9.47189L38.8454 47.8967Z"
|
||||
fill="#222222"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex justify-center p-2 rounded-md bg-[#D7D7D799] bg-opacity-60 shadow-[0_4px_7px_0_rgba(255,255,255,0.12)_inset] backdrop-blur-xs">
|
||||
<p className="text-center font-light">All rights reserved</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</BlurFade>
|
||||
);
|
||||
}
|
210
src/components/header.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useLogin, usePrivy } from "@privy-io/react-auth";
|
||||
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn, getInitials } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { MobileDrawer } from "@/components/mobile-drawer";
|
||||
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
import { Avatar, AvatarFallback } from "./ui/avatar";
|
||||
import { ChevronsUpDown, Copy, LogOut } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Header() {
|
||||
const [addBorder, setAddBorder] = React.useState(false);
|
||||
|
||||
const { ready, authenticated, logout, user } = usePrivy();
|
||||
|
||||
const { login } = useLogin({
|
||||
onComplete: ({ user, isNewUser, wasAlreadyAuthenticated, loginMethod }) => {
|
||||
if (!wasAlreadyAuthenticated) toast.success("Login success");
|
||||
|
||||
// router.replace("/");
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
toast.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 20) {
|
||||
setAddBorder(true);
|
||||
} else {
|
||||
setAddBorder(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 h-[var(--header-height)] z-50 p-0 pb-16 bg-background/60 backdrop-blur">
|
||||
<div className="flex justify-between items-center layout mx-auto py-2 px-3 sm:px-6 border-x">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
{/* <Icons.logo className="w-auto" /> */}
|
||||
<Image src="/logo.png" height={40} width={36} alt="logo" />
|
||||
<span className="font-bold text-xl">{siteConfig.name}</span>
|
||||
</Link>
|
||||
<nav className="hidden lg:block">
|
||||
<ul className="flex gap-4">
|
||||
<li>
|
||||
<Link
|
||||
href={"/#features"}
|
||||
className={cn(
|
||||
"transition-all hover:font-bold hover:border-b hover:pb-1"
|
||||
)}
|
||||
>
|
||||
Features
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={"/#tokenomics"}
|
||||
className={cn(
|
||||
"transition-all hover:font-bold hover:border-b hover:pb-1"
|
||||
)}
|
||||
>
|
||||
Tokenomics
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href={"https://soviro.gitbook.io/soviro"}
|
||||
target="_blank"
|
||||
className={cn(
|
||||
"transition-all hover:font-bold hover:border-b hover:pb-1"
|
||||
)}
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="hidden lg:block">
|
||||
{!ready ? (
|
||||
<Skeleton className="h-10 w-32" />
|
||||
) : (
|
||||
<>
|
||||
{authenticated && user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="max-w-48 flex items-center gap-2 border p-2 rounded-md">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user.email ? getInitials(user.email.address) : "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{user.email && user.email.address}
|
||||
{user.wallet && user.wallet.address}
|
||||
</span>
|
||||
<span className="truncate text-xs">{user.id}</span>
|
||||
</div>
|
||||
<ChevronsUpDown className="ml-auto size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
side={"bottom"}
|
||||
align="end"
|
||||
sideOffset={4}
|
||||
>
|
||||
<DropdownMenuLabel className="p-0 font-normal">
|
||||
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<Avatar className="h-8 w-8 rounded-lg">
|
||||
<AvatarFallback className="rounded-lg">
|
||||
{user.email ? getInitials(user.email.address) : "U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold">
|
||||
{user.email?.address}
|
||||
</span>
|
||||
<span className="truncate text-xs">{user.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{user.wallet !== undefined && (
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
className="focus:bg-accent focus:text-primary"
|
||||
>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="flex-start w-full"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.wallet!.address);
|
||||
toast.info("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
<Copy />
|
||||
Copy wallet code
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
logout();
|
||||
toast.success("You have been signed out");
|
||||
}}
|
||||
asChild
|
||||
className="focus:bg-accent focus:text-primary"
|
||||
>
|
||||
<Button variant={"ghost"} className="flex-start w-full">
|
||||
<LogOut />
|
||||
Sign out
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
onClick={login}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 cursor-pointer block lg:hidden">
|
||||
<MobileDrawer />
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
className={cn(
|
||||
"absolute w-full bottom-0 transition-opacity duration-300 ease-in-out"
|
||||
)}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
}
|
11
src/components/loading-page.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export function LoadingPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center ">
|
||||
<span className="relative flex h-20 w-20 mb-7">
|
||||
<span className="animate-ping absolute h-full w-full rounded-full bg-primary opacity-75"></span>
|
||||
<span className="relative flex justify-center items-center rounded-full h-20 w-20 bg-primary"></span>
|
||||
</span>
|
||||
<p className="text-2xl font-semibold text-black">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -31,7 +31,7 @@ const Section = forwardRef<HTMLElement, SectionProps>(
|
||||
|
||||
return (
|
||||
<section id={id} ref={ref}>
|
||||
<div className={cn("relative mx-auto container", className)}>
|
||||
<div className={cn("relative mx-auto layout", className)}>
|
||||
{(title || subtitle || description) && (
|
||||
<div
|
||||
className={cn(
|
||||
@ -40,7 +40,7 @@ const Section = forwardRef<HTMLElement, SectionProps>(
|
||||
)}
|
||||
>
|
||||
{title && (
|
||||
<h2 className="text-sm text-muted-foreground text-balance font-semibold tracking-tigh uppercase">
|
||||
<h2 className="text-lg text-muted-foreground text-balance font-semibold tracking-tigh uppercase">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Ripple } from "@/components/ui/ripple";
|
||||
|
||||
const contributors = [
|
||||
{
|
||||
name: "Alice Johnson",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8NHx8cG9ydHJhaXR8ZW58MHx8MHx8fDA%3D",
|
||||
},
|
||||
{
|
||||
name: "Bob Brown",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Charlie Davis",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MTJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Diana Evans",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8Mjh8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
{
|
||||
name: "Ethan Ford",
|
||||
avatar:
|
||||
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MzJ8fHBvcnRyYWl0fGVufDB8fDB8fHww",
|
||||
},
|
||||
];
|
||||
|
||||
export function Community() {
|
||||
return (
|
||||
<Section id="community" title="Community">
|
||||
<div className="border-x border-t overflow-hidden relative">
|
||||
<Ripple />
|
||||
<div className="p-6 text-center py-12">
|
||||
<p className="text-muted-foreground mb-6 text-balance max-w-prose mx-auto font-medium">
|
||||
We're grateful for the amazing open-source community that helps
|
||||
make our project better every day.
|
||||
</p>
|
||||
<div className="flex justify-center -space-x-6 mb-8">
|
||||
{contributors.map((contributor, index) => (
|
||||
<div key={index}>
|
||||
<Avatar className="size-12 relative border-2 border-background bg-muted">
|
||||
<AvatarImage
|
||||
src={contributor.avatar}
|
||||
alt={contributor.name}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="text-lg font-semibold">
|
||||
{contributor.name.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button variant="secondary" className="flex items-center gap-2">
|
||||
<Icons.github className="h-5 w-5" />
|
||||
Become a contributor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import { Section } from "@/components/section";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function CTA() {
|
||||
return (
|
||||
<Section id="cta">
|
||||
<div className="border overflow-hidden relative text-center py-16 mx-auto">
|
||||
<p className="max-w-3xl text-foreground mb-6 text-balance mx-auto font-medium text-3xl">
|
||||
Ready to build your next AI agent?
|
||||
</p>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button className="flex items-center gap-2">Get Started</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import { Section } from "@/components/section";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
export function Features() {
|
||||
const services = siteConfig.features;
|
||||
return (
|
||||
<Section id="features" title="Features">
|
||||
<div className="border-x border-t">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
{services.map(({ name, description, icon: Icon }, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex flex-col gap-y-2 items-center justify-center py-8 px-4 border-b transition-colors hover:bg-secondary/20",
|
||||
"last:border-b-0",
|
||||
"md:[&:nth-child(2n+1)]:border-r md:[&:nth-child(n+5)]:border-b-0",
|
||||
"lg:[&:nth-child(3n)]:border-r-0 lg:[&:nth-child(n+4)]:border-b-0 lg:border-r"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2 items-center">
|
||||
<div className="bg-gradient-to-b from-primary to-primary/80 p-2 rounded-lg text-white transition-colors group-hover:from-secondary group-hover:to-secondary/80">
|
||||
{Icon}
|
||||
</div>
|
||||
<h2 className="text-xl font-medium text-card-foreground text-center text-balance">
|
||||
{name}
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground text-balance text-center max-w-md mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
<Link
|
||||
href="#"
|
||||
className="text-sm text-primary hover:underline underline-offset-4 transition-colors hover:text-secondary-foreground"
|
||||
>
|
||||
Learn more >
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { BorderText } from "@/components/ui/border-number";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="flex flex-col gap-y-5 rounded-lg px-7 py-5 container">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Icons.logo className="h-5 w-5" />
|
||||
<h2 className="text-lg font-bold text-foreground">
|
||||
{siteConfig.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-x-2">
|
||||
{siteConfig.footer.socialLinks.map((link, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href={link.url}
|
||||
className="flex h-5 w-5 items-center justify-center text-muted-foreground transition-all duration-100 ease-linear hover:text-foreground hover:underline hover:underline-offset-4"
|
||||
>
|
||||
{link.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between gap-y-5 md:flex-row md:items-center">
|
||||
<ul className="flex flex-col gap-x-5 gap-y-2 text-muted-foreground md:flex-row md:items-center">
|
||||
{siteConfig.footer.links.map((link, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="text-[15px]/normal font-medium text-muted-foreground transition-all duration-100 ease-linear hover:text-foreground hover:underline hover:underline-offset-4"
|
||||
>
|
||||
<a href={link.url}>{link.text}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex items-center justify-between text-sm font-medium tracking-tight text-muted-foreground">
|
||||
<p>{siteConfig.footer.bottomText}</p>
|
||||
</div>
|
||||
</div>
|
||||
<BorderText
|
||||
text={siteConfig.footer.brandText}
|
||||
className="text-[clamp(3rem,15vw,10rem)] overflow-hidden font-mono tracking-tighter font-medium"
|
||||
/>
|
||||
</footer>
|
||||
);
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { MobileDrawer } from "@/components/mobile-drawer";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { easeInOutCubic } from "@/lib/animation";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion, useAnimation } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="sticky top-0 h-[var(--header-height)] z-50 p-0 bg-background/60 backdrop-blur">
|
||||
<div className="flex justify-between items-center container mx-auto p-2">
|
||||
<Link
|
||||
href="/"
|
||||
title="brand-logo"
|
||||
className="relative mr-6 flex items-center space-x-2"
|
||||
>
|
||||
<Icons.logo className="w-auto" />
|
||||
<span className="font-semibold text-lg">{siteConfig.name}</span>
|
||||
</Link>
|
||||
<div className="hidden lg:block">
|
||||
<Link
|
||||
href="#"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"h-8 text-primary-foreground rounded-lg group tracking-tight font-medium"
|
||||
)}
|
||||
>
|
||||
{siteConfig.cta}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-2 cursor-pointer block lg:hidden">
|
||||
<MobileDrawer />
|
||||
</div>
|
||||
</div>
|
||||
<hr className="absolute w-full bottom-0" />
|
||||
</header>
|
||||
);
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { AuroraText } from "@/components/aurora-text";
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Section } from "@/components/section";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { lazy, Suspense, useEffect, useState } from "react";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
function HeroPill() {
|
||||
return (
|
||||
<motion.a
|
||||
href="/blog/introducing-dev-ai"
|
||||
className="flex w-auto items-center space-x-2 rounded-full bg-primary/20 px-2 py-1 ring-1 ring-accent whitespace-pre"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, ease }}
|
||||
>
|
||||
<div className="w-fit rounded-full bg-accent px-2 py-0.5 text-left text-xs font-medium text-primary sm:text-sm">
|
||||
🛠️ New
|
||||
</div>
|
||||
<p className="text-xs font-medium text-primary sm:text-sm">
|
||||
Introducing AI Agent SDK
|
||||
</p>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
className="ml-1"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8.78141 5.33312L5.20541 1.75712L6.14808 0.814453L11.3334 5.99979L6.14808 11.1851L5.20541 10.2425L8.78141 6.66645H0.666748V5.33312H8.78141Z"
|
||||
fill="hsl(var(--primary))"
|
||||
/>
|
||||
</svg>
|
||||
</motion.a>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroTitles() {
|
||||
return (
|
||||
<div className="flex w-full max-w-3xl flex-col overflow-hidden pt-8">
|
||||
<motion.h1
|
||||
className="text-left text-4xl font-semibold leading-tighter text-foreground sm:text-5xl md:text-6xl tracking-tighter"
|
||||
initial={{ filter: "blur(10px)", opacity: 0, y: 50 }}
|
||||
animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 1,
|
||||
ease,
|
||||
staggerChildren: 0.2,
|
||||
}}
|
||||
>
|
||||
<motion.span
|
||||
className="inline-block text-balance"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.8,
|
||||
delay: 0.5,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
<AuroraText className="leading-normal">
|
||||
{siteConfig.hero.title}
|
||||
</AuroraText>
|
||||
</motion.span>
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
className="text-left max-w-xl leading-normal text-muted-foreground sm:text-lg sm:leading-normal text-balance"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
delay: 0.6,
|
||||
duration: 0.8,
|
||||
ease,
|
||||
}}
|
||||
>
|
||||
{siteConfig.hero.description}
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HeroCTA() {
|
||||
return (
|
||||
<div className="relative mt-6">
|
||||
<motion.div
|
||||
className="flex w-full max-w-2xl flex-col items-start justify-start space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.8, duration: 0.8, ease }}
|
||||
>
|
||||
<Link
|
||||
href="/download"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2 rounded-lg"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
{siteConfig.hero.cta}
|
||||
</Link>
|
||||
</motion.div>
|
||||
<motion.p
|
||||
className="mt-3 text-sm text-muted-foreground text-left"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.0, duration: 0.8 }}
|
||||
>
|
||||
{siteConfig.hero.ctaDescription}
|
||||
</motion.p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const LazySpline = lazy(() => import("@splinetool/react-spline"));
|
||||
|
||||
export function Hero() {
|
||||
const [showSpline, setShowSpline] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024); // Assuming 1024px is the breakpoint for lg
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
return () => window.removeEventListener("resize", checkMobile);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Don't show on mobile
|
||||
if (!isMobile) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowSpline(true);
|
||||
}, 1000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<Section id="hero">
|
||||
<div className="relative grid grid-cols-1 lg:grid-cols-2 gap-x-8 w-full p-6 lg:p-12 border-x overflow-hidden">
|
||||
<div className="flex flex-col justify-start items-start lg:col-span-1">
|
||||
<HeroPill />
|
||||
<HeroTitles />
|
||||
<HeroCTA />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className="relative lg:h-full lg:col-span-1">
|
||||
<Suspense>
|
||||
{showSpline && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8, delay: 1 }}
|
||||
>
|
||||
<LazySpline
|
||||
scene="https://prod.spline.design/mZBrYNcnoESGlTUG/scene.splinecode"
|
||||
className="absolute inset-0 w-full h-full origin-top-left flex items-center justify-center"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
21
src/components/spinner.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type SpinnerProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function Spinner({ className }: SpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-block h-5 w-5 animate-spin rounded-full border-2 border-solid border-gray-300 border-e-black align-[-0.125em] text-surface motion-reduce:animate-[spin_1.5s_linear_infinite] dark:text-white",
|
||||
className
|
||||
)}
|
||||
role="status"
|
||||
>
|
||||
<span className="!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -34,7 +34,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
{/* <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> */}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
|
@ -13,7 +13,7 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
"border border-primary bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
@ -21,6 +21,8 @@ const buttonVariants = cva(
|
||||
|
||||
orange:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
dots:
|
||||
"border border-primary bg-[url(/background-2.png)] hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
|
121
src/components/ui/chat-bubble.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MessageLoading } from "@/components/ui/message-loading";
|
||||
|
||||
interface ChatBubbleProps {
|
||||
variant?: "sent" | "received";
|
||||
layout?: "default" | "ai";
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ChatBubble({
|
||||
variant = "received",
|
||||
layout = "default",
|
||||
className,
|
||||
children,
|
||||
}: ChatBubbleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-start gap-2 mb-4",
|
||||
variant === "sent" && "flex-row-reverse",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatBubbleMessageProps {
|
||||
variant?: "sent" | "received";
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ChatBubbleMessage({
|
||||
variant = "received",
|
||||
isLoading,
|
||||
className,
|
||||
children,
|
||||
}: ChatBubbleMessageProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg p-3",
|
||||
variant === "sent" ? "bg-primary text-primary-foreground" : "bg-muted",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageLoading />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatBubbleAvatarProps {
|
||||
src?: string;
|
||||
fallback?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ChatBubbleAvatar({
|
||||
src,
|
||||
fallback = "AI",
|
||||
className,
|
||||
}: ChatBubbleAvatarProps) {
|
||||
return (
|
||||
<Avatar className={cn("h-8 w-8", className)}>
|
||||
{src && <AvatarImage src={src} />}
|
||||
<AvatarFallback>{fallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatBubbleActionProps {
|
||||
icon?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function ChatBubbleAction({
|
||||
icon,
|
||||
onClick,
|
||||
className,
|
||||
}: ChatBubbleActionProps) {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatBubbleActionWrapper({
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 mt-2", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
24
src/components/ui/chat-input.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import * as React from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ChatInputProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<Textarea
|
||||
autoComplete="off"
|
||||
ref={ref}
|
||||
name="message"
|
||||
className={cn(
|
||||
"max-h-12 px-4 py-3 bg-background text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 w-full rounded-md flex items-center h-16 resize-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
ChatInput.displayName = "ChatInput";
|
||||
|
||||
export { ChatInput };
|
55
src/components/ui/chat-message-list.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import * as React from "react";
|
||||
import { ArrowDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAutoScroll } from "@/hooks/use-auto-scroll";
|
||||
|
||||
interface ChatMessageListProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
smooth?: boolean;
|
||||
}
|
||||
|
||||
const ChatMessageList = React.forwardRef<HTMLDivElement, ChatMessageListProps>(
|
||||
({ className, children, smooth = false, ...props }, _ref) => {
|
||||
const {
|
||||
scrollRef,
|
||||
isAtBottom,
|
||||
autoScrollEnabled,
|
||||
scrollToBottom,
|
||||
disableAutoScroll,
|
||||
} = useAutoScroll({
|
||||
smooth,
|
||||
content: children,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
<div
|
||||
className={`flex flex-col w-full h-full p-4 overflow-y-auto ${className}`}
|
||||
ref={scrollRef}
|
||||
onWheel={disableAutoScroll}
|
||||
onTouchMove={disableAutoScroll}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col gap-6">{children}</div>
|
||||
</div>
|
||||
|
||||
{!isAtBottom && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
scrollToBottom();
|
||||
}}
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="absolute bottom-2 left-1/2 transform -translate-x-1/2 inline-flex rounded-full shadow-md"
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChatMessageList.displayName = "ChatMessageList";
|
||||
|
||||
export { ChatMessageList };
|
147
src/components/ui/confetti.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import type {
|
||||
GlobalOptions as ConfettiGlobalOptions,
|
||||
CreateTypes as ConfettiInstance,
|
||||
Options as ConfettiOptions,
|
||||
} from "canvas-confetti";
|
||||
import confetti from "canvas-confetti";
|
||||
import type { ReactNode } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
|
||||
type Api = {
|
||||
fire: (options?: ConfettiOptions) => void;
|
||||
};
|
||||
|
||||
type Props = React.ComponentPropsWithRef<"canvas"> & {
|
||||
options?: ConfettiOptions;
|
||||
globalOptions?: ConfettiGlobalOptions;
|
||||
manualstart?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export type ConfettiRef = Api | null;
|
||||
|
||||
const ConfettiContext = createContext<Api>({} as Api);
|
||||
|
||||
// Define component first
|
||||
const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => {
|
||||
const {
|
||||
options,
|
||||
globalOptions = { resize: true, useWorker: true },
|
||||
manualstart = false,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const instanceRef = useRef<ConfettiInstance | null>(null);
|
||||
|
||||
const canvasRef = useCallback(
|
||||
(node: HTMLCanvasElement) => {
|
||||
if (node !== null) {
|
||||
if (instanceRef.current) return;
|
||||
instanceRef.current = confetti.create(node, {
|
||||
...globalOptions,
|
||||
resize: true,
|
||||
});
|
||||
} else {
|
||||
if (instanceRef.current) {
|
||||
instanceRef.current.reset();
|
||||
instanceRef.current = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
[globalOptions],
|
||||
);
|
||||
|
||||
const fire = useCallback(
|
||||
async (opts = {}) => {
|
||||
try {
|
||||
await instanceRef.current?.({ ...options, ...opts });
|
||||
} catch (error) {
|
||||
console.error("Confetti error:", error);
|
||||
}
|
||||
},
|
||||
[options],
|
||||
);
|
||||
|
||||
const api = useMemo(
|
||||
() => ({
|
||||
fire,
|
||||
}),
|
||||
[fire],
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => api, [api]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manualstart) {
|
||||
(async () => {
|
||||
try {
|
||||
await fire();
|
||||
} catch (error) {
|
||||
console.error("Confetti effect error:", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [manualstart, fire]);
|
||||
|
||||
return (
|
||||
<ConfettiContext.Provider value={api}>
|
||||
<canvas ref={canvasRef} {...rest} />
|
||||
{children}
|
||||
</ConfettiContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
// Set display name immediately
|
||||
ConfettiComponent.displayName = "Confetti";
|
||||
|
||||
// Export as Confetti
|
||||
export const Confetti = ConfettiComponent;
|
||||
|
||||
interface ConfettiButtonProps extends ButtonProps {
|
||||
options?: ConfettiOptions &
|
||||
ConfettiGlobalOptions & { canvas?: HTMLCanvasElement };
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ConfettiButtonComponent = ({
|
||||
options,
|
||||
children,
|
||||
...props
|
||||
}: ConfettiButtonProps) => {
|
||||
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
try {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + rect.height / 2;
|
||||
await confetti({
|
||||
...options,
|
||||
origin: {
|
||||
x: x / window.innerWidth,
|
||||
y: y / window.innerHeight,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Confetti button error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} {...props}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
ConfettiButtonComponent.displayName = "ConfettiButton";
|
||||
|
||||
export const ConfettiButton = ConfettiButtonComponent;
|
200
src/components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
153
src/components/ui/expandable-chat.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { X, MessageCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export type ChatPosition = "bottom-right" | "bottom-left";
|
||||
export type ChatSize = "sm" | "md" | "lg" | "xl" | "full";
|
||||
|
||||
const chatConfig = {
|
||||
dimensions: {
|
||||
sm: "sm:max-w-sm sm:max-h-[500px]",
|
||||
md: "sm:max-w-md sm:max-h-[600px]",
|
||||
lg: "sm:max-w-lg sm:max-h-[700px]",
|
||||
xl: "sm:max-w-xl sm:max-h-[800px]",
|
||||
full: "sm:w-full sm:h-full",
|
||||
},
|
||||
positions: {
|
||||
"bottom-right": "bottom-5 right-5",
|
||||
"bottom-left": "bottom-5 left-5",
|
||||
},
|
||||
chatPositions: {
|
||||
"bottom-right": "sm:bottom-[calc(100%+10px)] sm:right-0",
|
||||
"bottom-left": "sm:bottom-[calc(100%+10px)] sm:left-0",
|
||||
},
|
||||
states: {
|
||||
open: "pointer-events-auto opacity-100 visible scale-100 translate-y-0",
|
||||
closed:
|
||||
"pointer-events-none opacity-0 invisible scale-100 sm:translate-y-5",
|
||||
},
|
||||
};
|
||||
|
||||
interface ExpandableChatProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
position?: ChatPosition;
|
||||
size?: ChatSize;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ExpandableChat: React.FC<ExpandableChatProps> = ({
|
||||
className,
|
||||
position = "bottom-right",
|
||||
size = "md",
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const chatRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleChat = () => setIsOpen(!isOpen);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(`fixed ${chatConfig.positions[position]} z-50`, className)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
ref={chatRef}
|
||||
className={cn(
|
||||
"flex flex-col bg-background border sm:rounded-lg shadow-md overflow-hidden transition-all duration-250 ease-out sm:absolute sm:w-[90vw] sm:h-[80vh] fixed inset-0 w-full h-full sm:inset-auto",
|
||||
chatConfig.chatPositions[position],
|
||||
chatConfig.dimensions[size],
|
||||
isOpen ? chatConfig.states.open : chatConfig.states.closed,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 sm:hidden"
|
||||
onClick={toggleChat}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ExpandableChatToggle
|
||||
icon={icon}
|
||||
isOpen={isOpen}
|
||||
toggleChat={toggleChat}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandableChat.displayName = "ExpandableChat";
|
||||
|
||||
const ExpandableChatHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex items-center justify-between p-4 border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
ExpandableChatHeader.displayName = "ExpandableChatHeader";
|
||||
|
||||
const ExpandableChatBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <div className={cn("flex-grow overflow-y-auto", className)} {...props} />;
|
||||
|
||||
ExpandableChatBody.displayName = "ExpandableChatBody";
|
||||
|
||||
const ExpandableChatFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <div className={cn("border-t p-4", className)} {...props} />;
|
||||
|
||||
ExpandableChatFooter.displayName = "ExpandableChatFooter";
|
||||
|
||||
interface ExpandableChatToggleProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
toggleChat: () => void;
|
||||
}
|
||||
|
||||
const ExpandableChatToggle: React.FC<ExpandableChatToggleProps> = ({
|
||||
className,
|
||||
icon,
|
||||
isOpen,
|
||||
toggleChat,
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={toggleChat}
|
||||
className={cn(
|
||||
"w-14 h-14 rounded-full shadow-md flex items-center justify-center hover:shadow-lg hover:shadow-black/30 transition-all duration-300",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
icon || <MessageCircle className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
ExpandableChatToggle.displayName = "ExpandableChatToggle";
|
||||
|
||||
export {
|
||||
ExpandableChat,
|
||||
ExpandableChatHeader,
|
||||
ExpandableChatBody,
|
||||
ExpandableChatFooter,
|
||||
};
|
178
src/components/ui/form.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
47
src/components/ui/message-loading.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
// @hidden
|
||||
function MessageLoading() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-foreground"
|
||||
>
|
||||
<circle cx="4" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
id="spinner_qFRN"
|
||||
begin="0;spinner_OcgL.end+0.25s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="12" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
begin="spinner_qFRN.begin+0.1s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="20" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
id="spinner_OcgL"
|
||||
begin="spinner_qFRN.begin+0.2s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export { MessageLoading };
|
58
src/components/ui/number-ticker.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useInView, useMotionValue, useSpring } from "motion/react";
|
||||
import { ComponentPropsWithoutRef, useEffect, useRef } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> {
|
||||
value: number;
|
||||
direction?: "up" | "down";
|
||||
delay?: number; // delay in s
|
||||
decimalPlaces?: number;
|
||||
}
|
||||
|
||||
export function NumberTicker({
|
||||
value,
|
||||
direction = "up",
|
||||
delay = 0,
|
||||
className,
|
||||
decimalPlaces = 0,
|
||||
...props
|
||||
}: NumberTickerProps) {
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const motionValue = useMotionValue(direction === "down" ? value : 0);
|
||||
const springValue = useSpring(motionValue, {
|
||||
damping: 60,
|
||||
stiffness: 100,
|
||||
});
|
||||
const isInView = useInView(ref, { once: true, margin: "0px" });
|
||||
|
||||
useEffect(() => {
|
||||
isInView &&
|
||||
setTimeout(() => {
|
||||
motionValue.set(direction === "down" ? 0 : value);
|
||||
}, delay * 1000);
|
||||
}, [motionValue, isInView, delay, value, direction]);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
springValue.on("change", (latest) => {
|
||||
if (ref.current) {
|
||||
ref.current.textContent = Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(Number(latest.toFixed(decimalPlaces)));
|
||||
}
|
||||
}),
|
||||
[springValue, decimalPlaces]
|
||||
);
|
||||
|
||||
return (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn("inline-block tabular-nums tracking-wider", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
117
src/components/ui/pagination.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ButtonProps, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Pagination.displayName = "Pagination"
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
PaginationContent.displayName = "PaginationContent"
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("", className)} {...props} />
|
||||
))
|
||||
PaginationItem.displayName = "PaginationItem"
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<ButtonProps, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
PaginationLink.displayName = "PaginationLink"
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationPrevious.displayName = "PaginationPrevious"
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</PaginationLink>
|
||||
)
|
||||
PaginationNext.displayName = "PaginationNext"
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
PaginationEllipsis.displayName = "PaginationEllipsis"
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
}
|
31
src/components/ui/separator.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
15
src/components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
27
src/components/ui/sonner.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
return (
|
||||
<Sonner
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
119
src/components/ui/terminal.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, MotionProps } from "motion/react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface AnimatedSpanProps extends MotionProps {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AnimatedSpan = ({
|
||||
children,
|
||||
delay = 0,
|
||||
className,
|
||||
...props
|
||||
}: AnimatedSpanProps) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -5 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: delay / 1000 }}
|
||||
className={cn("grid text-sm font-normal tracking-tight", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
interface TypingAnimationProps extends MotionProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export const TypingAnimation = ({
|
||||
children,
|
||||
className,
|
||||
duration = 60,
|
||||
delay = 0,
|
||||
as: Component = "span",
|
||||
...props
|
||||
}: TypingAnimationProps) => {
|
||||
if (typeof children !== "string") {
|
||||
throw new Error("TypingAnimation: children must be a string. Received:");
|
||||
}
|
||||
|
||||
const MotionComponent = motion.create(Component, {
|
||||
forwardMotionProps: true,
|
||||
});
|
||||
|
||||
const [displayedText, setDisplayedText] = useState<string>("");
|
||||
const [started, setStarted] = useState(false);
|
||||
const elementRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const startTimeout = setTimeout(() => {
|
||||
setStarted(true);
|
||||
}, delay);
|
||||
return () => clearTimeout(startTimeout);
|
||||
}, [delay]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!started) return;
|
||||
|
||||
let i = 0;
|
||||
const typingEffect = setInterval(() => {
|
||||
if (i < children.length) {
|
||||
setDisplayedText(children.substring(0, i + 1));
|
||||
i++;
|
||||
} else {
|
||||
clearInterval(typingEffect);
|
||||
}
|
||||
}, 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;
|
||||
}
|
||||
|
||||
export const Terminal = ({ children, className }: TerminalProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border border-border bg-background",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-y-2 border-b border-border 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">{children}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
22
src/components/ui/textarea.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
135
src/hooks/use-auto-scroll.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
// @hidden
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
interface ScrollState {
|
||||
isAtBottom: boolean;
|
||||
autoScrollEnabled: boolean;
|
||||
}
|
||||
|
||||
interface UseAutoScrollOptions {
|
||||
offset?: number;
|
||||
smooth?: boolean;
|
||||
content?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function useAutoScroll(options: UseAutoScrollOptions = {}) {
|
||||
const { offset = 20, smooth = false, content } = options;
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const lastContentHeight = useRef(0);
|
||||
const userHasScrolled = useRef(false);
|
||||
|
||||
const [scrollState, setScrollState] = useState<ScrollState>({
|
||||
isAtBottom: true,
|
||||
autoScrollEnabled: true,
|
||||
});
|
||||
|
||||
const checkIsAtBottom = useCallback(
|
||||
(element: HTMLElement) => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = element;
|
||||
const distanceToBottom = Math.abs(
|
||||
scrollHeight - scrollTop - clientHeight
|
||||
);
|
||||
return distanceToBottom <= offset;
|
||||
},
|
||||
[offset]
|
||||
);
|
||||
|
||||
const scrollToBottom = useCallback(
|
||||
(instant?: boolean) => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
const targetScrollTop =
|
||||
scrollRef.current.scrollHeight - scrollRef.current.clientHeight;
|
||||
|
||||
if (instant) {
|
||||
scrollRef.current.scrollTop = targetScrollTop;
|
||||
} else {
|
||||
scrollRef.current.scrollTo({
|
||||
top: targetScrollTop,
|
||||
behavior: smooth ? "smooth" : "auto",
|
||||
});
|
||||
}
|
||||
|
||||
setScrollState({
|
||||
isAtBottom: true,
|
||||
autoScrollEnabled: true,
|
||||
});
|
||||
userHasScrolled.current = false;
|
||||
},
|
||||
[smooth]
|
||||
);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!scrollRef.current) return;
|
||||
|
||||
const atBottom = checkIsAtBottom(scrollRef.current);
|
||||
|
||||
setScrollState((prev) => ({
|
||||
isAtBottom: atBottom,
|
||||
// Re-enable auto-scroll if at the bottom
|
||||
autoScrollEnabled: atBottom ? true : prev.autoScrollEnabled,
|
||||
}));
|
||||
}, [checkIsAtBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollRef.current;
|
||||
if (!element) return;
|
||||
|
||||
element.addEventListener("scroll", handleScroll, { passive: true });
|
||||
return () => element.removeEventListener("scroll", handleScroll);
|
||||
}, [handleScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollElement = scrollRef.current;
|
||||
if (!scrollElement) return;
|
||||
|
||||
const currentHeight = scrollElement.scrollHeight;
|
||||
const hasNewContent = currentHeight !== lastContentHeight.current;
|
||||
|
||||
if (hasNewContent) {
|
||||
if (scrollState.autoScrollEnabled) {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToBottom(lastContentHeight.current === 0);
|
||||
});
|
||||
}
|
||||
lastContentHeight.current = currentHeight;
|
||||
}
|
||||
}, [content, scrollState.autoScrollEnabled, scrollToBottom]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = scrollRef.current;
|
||||
if (!element) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (scrollState.autoScrollEnabled) {
|
||||
scrollToBottom(true);
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(element);
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [scrollState.autoScrollEnabled, scrollToBottom]);
|
||||
|
||||
const disableAutoScroll = useCallback(() => {
|
||||
const atBottom = scrollRef.current
|
||||
? checkIsAtBottom(scrollRef.current)
|
||||
: false;
|
||||
|
||||
// Only disable if not at bottom
|
||||
if (!atBottom) {
|
||||
userHasScrolled.current = true;
|
||||
setScrollState((prev) => ({
|
||||
...prev,
|
||||
autoScrollEnabled: false,
|
||||
}));
|
||||
}
|
||||
}, [checkIsAtBottom]);
|
||||
|
||||
return {
|
||||
scrollRef,
|
||||
isAtBottom: scrollState.isAtBottom,
|
||||
autoScrollEnabled: scrollState.autoScrollEnabled,
|
||||
scrollToBottom: () => scrollToBottom(false),
|
||||
disableAutoScroll,
|
||||
};
|
||||
}
|
@ -10,8 +10,10 @@ import {
|
||||
|
||||
export const BLUR_FADE_DELAY = 0.15;
|
||||
|
||||
export const ANIMATION_EASE = [0.16, 1, 0.3, 1];
|
||||
|
||||
export const siteConfig = {
|
||||
name: "AI Agent SDK",
|
||||
name: "soviro.ai",
|
||||
description: "Create AI Agents with just a few lines of code.",
|
||||
cta: "Get Started",
|
||||
url: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
|
||||
@ -29,7 +31,7 @@ export const siteConfig = {
|
||||
instagram: "https://instagram.com/aiagentsdk",
|
||||
},
|
||||
hero: {
|
||||
title: "AI Agent SDK",
|
||||
title: "soviro.ai",
|
||||
description:
|
||||
"Create powerful AI agent workflows with just a few lines of code, enabling complex task automation and decision-making processes.",
|
||||
cta: "Get Started",
|
||||
@ -135,7 +137,7 @@ export const siteConfig = {
|
||||
{ text: "Contact", url: "#" },
|
||||
],
|
||||
bottomText: "All rights reserved.",
|
||||
brandText: "AGENT SDK",
|
||||
brandText: "soviro.ai",
|
||||
},
|
||||
|
||||
testimonials: [
|
||||
@ -263,3 +265,22 @@ export const siteConfig = {
|
||||
};
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
|
||||
export default class PostgrestError extends Error {
|
||||
details: string;
|
||||
hint: string;
|
||||
code: string;
|
||||
|
||||
constructor(context: {
|
||||
message: string;
|
||||
details: string;
|
||||
hint: string;
|
||||
code: string;
|
||||
}) {
|
||||
super(context.message);
|
||||
this.name = "PostgrestError";
|
||||
this.details = context.details;
|
||||
this.hint = context.hint;
|
||||
this.code = context.code;
|
||||
}
|
||||
}
|
||||
|
@ -87,3 +87,8 @@ export function formatDate(date: string) {
|
||||
return `${fullDate} (${yearsAgo}y ago)`;
|
||||
}
|
||||
}
|
||||
|
||||
export const getInitials = (string: string) =>
|
||||
string
|
||||
.split(/\s/)
|
||||
.reduce((response, word) => (response += word.slice(0, 1)), "");
|
||||
|
20
src/providers/check-session-provider.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingPage } from "@/components/loading-page";
|
||||
import { usePrivy } from "@privy-io/react-auth";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function CheckSessionProvider({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const { ready, authenticated } = usePrivy();
|
||||
const router = useRouter();
|
||||
|
||||
if (!ready) return <LoadingPage />;
|
||||
|
||||
if (ready && !authenticated) router.replace("/");
|
||||
|
||||
if (ready && authenticated) return children;
|
||||
}
|
52
src/providers/privy-provider.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { PrivyProvider } from "@privy-io/react-auth";
|
||||
|
||||
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
|
||||
|
||||
const solanaConnectors = toSolanaWalletConnectors({
|
||||
// By default, shouldAutoConnect is enabled
|
||||
shouldAutoConnect: false,
|
||||
});
|
||||
|
||||
export default function MyPrivyProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<PrivyProvider
|
||||
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
|
||||
config={{
|
||||
// Customize Privy's appearance in your app
|
||||
appearance: {
|
||||
accentColor: "#812f20",
|
||||
theme: "light",
|
||||
showWalletLoginFirst: false,
|
||||
walletChainType: "solana-only",
|
||||
// walletList: ["phantom"],
|
||||
logo: "https://supabasekong-swwwcowsg8kso0cg4swosw48.dev3vds1.link/storage/v1/object/public/logo/banner.png",
|
||||
},
|
||||
loginMethods: ["email", "wallet"],
|
||||
fundingMethodConfig: {
|
||||
moonpay: {
|
||||
useSandbox: true,
|
||||
},
|
||||
},
|
||||
// Create embedded wallets for users who don't have a wallet
|
||||
embeddedWallets: {
|
||||
createOnLogin: "all-users",
|
||||
showWalletUIs: true,
|
||||
},
|
||||
mfa: {
|
||||
noPromptOnMfaRequired: false,
|
||||
},
|
||||
externalWallets: {
|
||||
solana: { connectors: solanaConnectors },
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PrivyProvider>
|
||||
);
|
||||
}
|
76
src/providers/react-query-provider.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import PostgrestError from "@/lib/config";
|
||||
import {
|
||||
MutationCache,
|
||||
QueryCache,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => {
|
||||
if (error instanceof PostgrestError) {
|
||||
toast.error(error.code, { description: error.message });
|
||||
}
|
||||
},
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
const errResponse = error.response.data;
|
||||
console.log({ errResponse });
|
||||
// if (errResponse.errors && Array.isArray(errResponse.errors)) {
|
||||
// errResponse.errors.forEach(
|
||||
// (inputErr: { field: string; message: string }) => {
|
||||
// toast.error(`Error field : ${inputErr.field}`, {
|
||||
// description: inputErr.message,
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// if (errResponse.code) {
|
||||
// toast.error(errResponse?.code, {
|
||||
// description: errResponse?.message,
|
||||
// });
|
||||
// }
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
toast.error("No response from the server", {
|
||||
description: "Failed to fetch the data, server returns null.",
|
||||
});
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
toast.error("Failed to set up the request", {
|
||||
description: "There is something wrong when setting up the request",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export default function ReactQueryProvider({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
9
src/utils/supabase/client.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { Database } from "./database.types";
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient<Database>(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
}
|
564
src/utils/supabase/database.types.ts
Normal file
@ -0,0 +1,564 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[];
|
||||
|
||||
export type Database = {
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
operationName?: string;
|
||||
query?: string;
|
||||
variables?: Json;
|
||||
extensions?: Json;
|
||||
};
|
||||
Returns: Json;
|
||||
};
|
||||
};
|
||||
Enums: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
pgbouncer: {
|
||||
Tables: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
get_auth: {
|
||||
Args: {
|
||||
p_usename: string;
|
||||
};
|
||||
Returns: {
|
||||
username: string;
|
||||
password: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
Enums: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
public: {
|
||||
Tables: {
|
||||
agent_responses: {
|
||||
Row: {
|
||||
agent_id: number;
|
||||
created_at: string;
|
||||
id: string;
|
||||
question: string;
|
||||
response: string;
|
||||
};
|
||||
Insert: {
|
||||
agent_id: number;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
question: string;
|
||||
response: string;
|
||||
};
|
||||
Update: {
|
||||
agent_id?: number;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
question?: string;
|
||||
response?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "agent_responses_agent_id_fkey";
|
||||
columns: ["agent_id"];
|
||||
referencedRelation: "agents";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
agents: {
|
||||
Row: {
|
||||
conversation: number;
|
||||
created_at: string;
|
||||
description: string;
|
||||
id: number;
|
||||
image_url: string;
|
||||
last_conv: string | null;
|
||||
model_type: Database["public"]["Enums"]["modelType"];
|
||||
name: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
conversation: number;
|
||||
created_at?: string;
|
||||
description: string;
|
||||
id?: number;
|
||||
image_url: string;
|
||||
last_conv?: string | null;
|
||||
model_type: Database["public"]["Enums"]["modelType"];
|
||||
name: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
conversation?: number;
|
||||
created_at?: string;
|
||||
description?: string;
|
||||
id?: number;
|
||||
image_url?: string;
|
||||
last_conv?: string | null;
|
||||
model_type?: Database["public"]["Enums"]["modelType"];
|
||||
name?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
modelType:
|
||||
| "roleplay"
|
||||
| "programming"
|
||||
| "marketing"
|
||||
| "marketing_seo"
|
||||
| "technology"
|
||||
| "science"
|
||||
| "translation"
|
||||
| "legal"
|
||||
| "finance"
|
||||
| "health"
|
||||
| "trivia"
|
||||
| "academia"
|
||||
| "general";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
storage: {
|
||||
Tables: {
|
||||
buckets: {
|
||||
Row: {
|
||||
allowed_mime_types: string[] | null;
|
||||
avif_autodetection: boolean | null;
|
||||
created_at: string | null;
|
||||
file_size_limit: number | null;
|
||||
id: string;
|
||||
name: string;
|
||||
owner: string | null;
|
||||
owner_id: string | null;
|
||||
public: boolean | null;
|
||||
updated_at: string | null;
|
||||
};
|
||||
Insert: {
|
||||
allowed_mime_types?: string[] | null;
|
||||
avif_autodetection?: boolean | null;
|
||||
created_at?: string | null;
|
||||
file_size_limit?: number | null;
|
||||
id: string;
|
||||
name: string;
|
||||
owner?: string | null;
|
||||
owner_id?: string | null;
|
||||
public?: boolean | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
Update: {
|
||||
allowed_mime_types?: string[] | null;
|
||||
avif_autodetection?: boolean | null;
|
||||
created_at?: string | null;
|
||||
file_size_limit?: number | null;
|
||||
id?: string;
|
||||
name?: string;
|
||||
owner?: string | null;
|
||||
owner_id?: string | null;
|
||||
public?: boolean | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
migrations: {
|
||||
Row: {
|
||||
executed_at: string | null;
|
||||
hash: string;
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
Insert: {
|
||||
executed_at?: string | null;
|
||||
hash: string;
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
Update: {
|
||||
executed_at?: string | null;
|
||||
hash?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
objects: {
|
||||
Row: {
|
||||
bucket_id: string | null;
|
||||
created_at: string | null;
|
||||
id: string;
|
||||
last_accessed_at: string | null;
|
||||
metadata: Json | null;
|
||||
name: string | null;
|
||||
owner: string | null;
|
||||
owner_id: string | null;
|
||||
path_tokens: string[] | null;
|
||||
updated_at: string | null;
|
||||
user_metadata: Json | null;
|
||||
version: string | null;
|
||||
};
|
||||
Insert: {
|
||||
bucket_id?: string | null;
|
||||
created_at?: string | null;
|
||||
id?: string;
|
||||
last_accessed_at?: string | null;
|
||||
metadata?: Json | null;
|
||||
name?: string | null;
|
||||
owner?: string | null;
|
||||
owner_id?: string | null;
|
||||
path_tokens?: string[] | null;
|
||||
updated_at?: string | null;
|
||||
user_metadata?: Json | null;
|
||||
version?: string | null;
|
||||
};
|
||||
Update: {
|
||||
bucket_id?: string | null;
|
||||
created_at?: string | null;
|
||||
id?: string;
|
||||
last_accessed_at?: string | null;
|
||||
metadata?: Json | null;
|
||||
name?: string | null;
|
||||
owner?: string | null;
|
||||
owner_id?: string | null;
|
||||
path_tokens?: string[] | null;
|
||||
updated_at?: string | null;
|
||||
user_metadata?: Json | null;
|
||||
version?: string | null;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "objects_bucketId_fkey";
|
||||
columns: ["bucket_id"];
|
||||
referencedRelation: "buckets";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
s3_multipart_uploads: {
|
||||
Row: {
|
||||
bucket_id: string;
|
||||
created_at: string;
|
||||
id: string;
|
||||
in_progress_size: number;
|
||||
key: string;
|
||||
owner_id: string | null;
|
||||
upload_signature: string;
|
||||
user_metadata: Json | null;
|
||||
version: string;
|
||||
};
|
||||
Insert: {
|
||||
bucket_id: string;
|
||||
created_at?: string;
|
||||
id: string;
|
||||
in_progress_size?: number;
|
||||
key: string;
|
||||
owner_id?: string | null;
|
||||
upload_signature: string;
|
||||
user_metadata?: Json | null;
|
||||
version: string;
|
||||
};
|
||||
Update: {
|
||||
bucket_id?: string;
|
||||
created_at?: string;
|
||||
id?: string;
|
||||
in_progress_size?: number;
|
||||
key?: string;
|
||||
owner_id?: string | null;
|
||||
upload_signature?: string;
|
||||
user_metadata?: Json | null;
|
||||
version?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_bucket_id_fkey";
|
||||
columns: ["bucket_id"];
|
||||
referencedRelation: "buckets";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
s3_multipart_uploads_parts: {
|
||||
Row: {
|
||||
bucket_id: string;
|
||||
created_at: string;
|
||||
etag: string;
|
||||
id: string;
|
||||
key: string;
|
||||
owner_id: string | null;
|
||||
part_number: number;
|
||||
size: number;
|
||||
upload_id: string;
|
||||
version: string;
|
||||
};
|
||||
Insert: {
|
||||
bucket_id: string;
|
||||
created_at?: string;
|
||||
etag: string;
|
||||
id?: string;
|
||||
key: string;
|
||||
owner_id?: string | null;
|
||||
part_number: number;
|
||||
size?: number;
|
||||
upload_id: string;
|
||||
version: string;
|
||||
};
|
||||
Update: {
|
||||
bucket_id?: string;
|
||||
created_at?: string;
|
||||
etag?: string;
|
||||
id?: string;
|
||||
key?: string;
|
||||
owner_id?: string | null;
|
||||
part_number?: number;
|
||||
size?: number;
|
||||
upload_id?: string;
|
||||
version?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_parts_bucket_id_fkey";
|
||||
columns: ["bucket_id"];
|
||||
referencedRelation: "buckets";
|
||||
referencedColumns: ["id"];
|
||||
},
|
||||
{
|
||||
foreignKeyName: "s3_multipart_uploads_parts_upload_id_fkey";
|
||||
columns: ["upload_id"];
|
||||
referencedRelation: "s3_multipart_uploads";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
can_insert_object: {
|
||||
Args: {
|
||||
bucketid: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
metadata: Json;
|
||||
};
|
||||
Returns: undefined;
|
||||
};
|
||||
extension: {
|
||||
Args: {
|
||||
name: string;
|
||||
};
|
||||
Returns: string;
|
||||
};
|
||||
filename: {
|
||||
Args: {
|
||||
name: string;
|
||||
};
|
||||
Returns: string;
|
||||
};
|
||||
foldername: {
|
||||
Args: {
|
||||
name: string;
|
||||
};
|
||||
Returns: string[];
|
||||
};
|
||||
get_size_by_bucket: {
|
||||
Args: Record<PropertyKey, never>;
|
||||
Returns: {
|
||||
size: number;
|
||||
bucket_id: string;
|
||||
}[];
|
||||
};
|
||||
list_multipart_uploads_with_delimiter: {
|
||||
Args: {
|
||||
bucket_id: string;
|
||||
prefix_param: string;
|
||||
delimiter_param: string;
|
||||
max_keys?: number;
|
||||
next_key_token?: string;
|
||||
next_upload_token?: string;
|
||||
};
|
||||
Returns: {
|
||||
key: string;
|
||||
id: string;
|
||||
created_at: string;
|
||||
}[];
|
||||
};
|
||||
list_objects_with_delimiter: {
|
||||
Args: {
|
||||
bucket_id: string;
|
||||
prefix_param: string;
|
||||
delimiter_param: string;
|
||||
max_keys?: number;
|
||||
start_after?: string;
|
||||
next_token?: string;
|
||||
};
|
||||
Returns: {
|
||||
name: string;
|
||||
id: string;
|
||||
metadata: Json;
|
||||
updated_at: string;
|
||||
}[];
|
||||
};
|
||||
operation: {
|
||||
Args: Record<PropertyKey, never>;
|
||||
Returns: string;
|
||||
};
|
||||
search: {
|
||||
Args: {
|
||||
prefix: string;
|
||||
bucketname: string;
|
||||
limits?: number;
|
||||
levels?: number;
|
||||
offsets?: number;
|
||||
search?: string;
|
||||
sortcolumn?: string;
|
||||
sortorder?: string;
|
||||
};
|
||||
Returns: {
|
||||
name: string;
|
||||
id: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
last_accessed_at: string;
|
||||
metadata: Json;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
Enums: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type PublicSchema = Database[Extract<keyof Database, "public">];
|
||||
|
||||
export type Tables<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])
|
||||
? (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesInsert<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesUpdate<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type Enums<
|
||||
PublicEnumNameOrOptions extends
|
||||
| keyof PublicSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never
|
||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
||||
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
||||
: never;
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof PublicSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
|
||||
? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never;
|
@ -18,10 +18,6 @@ const config = {
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["var(--font-geist-sans)"],
|
||||
mono: ["var(--font-geist-mono)"],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
@ -63,7 +59,7 @@ const config = {
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
animation: {
|
||||
marquee: "marquee var(--duration) linear infinite",
|
||||
marquee: "marquee var(--duration) infinite linear",
|
||||
"marquee-vertical": "marquee-vertical var(--duration) linear infinite",
|
||||
"border-beam": "border-beam calc(var(--duration)*1s) infinite linear",
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
|