finish project
This commit is contained in:
parent
fb69bba05d
commit
3ed12c8db0
4456
package-lock.json
generated
4456
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,17 +10,24 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@privy-io/react-auth": "^2.0.3",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-avatar": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@stepperize/react": "^4.1.3",
|
||||
"@supabase/ssr": "^0.5.2",
|
||||
"@tanstack/react-query": "^5.64.1",
|
||||
"@tanstack/react-query-devtools": "^5.64.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",
|
||||
"embla-carousel-react": "^8.1.7",
|
||||
|
BIN
public/empty-data.jpg
Normal file
BIN
public/empty-data.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 174 KiB |
@ -1,11 +0,0 @@
|
||||
interface MarketingLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function Layout({ children }: MarketingLayoutProps) {
|
||||
return (
|
||||
<main className="flex flex-col items-center justify-center h-screen">
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icons } from "@/components/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl">Login</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<Button variant="outline" className="w-full">
|
||||
<Icons.google className="w-4 h-4 mr-2" />
|
||||
Login with Google
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
<Icons.github className="w-4 h-4 mr-2" />
|
||||
Login with GitHub
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs uppercase">
|
||||
<span className="bg-background px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Link href="#" className="ml-auto inline-block text-sm underline">
|
||||
Forgot your password?
|
||||
</Link>
|
||||
</div>
|
||||
<Input id="password" type="password" required />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account?{" "}
|
||||
<Link href="/signup" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
export default function LoginForm() {
|
||||
return (
|
||||
<Card className="mx-auto max-w-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Sign Up</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="first-name">First name</Label>
|
||||
<Input id="first-name" placeholder="Max" required />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="last-name">Last name</Label>
|
||||
<Input id="last-name" placeholder="Robinson" required />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" />
|
||||
</div>
|
||||
<Button type="submit" className="w-full">
|
||||
Create an account
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full">
|
||||
Sign up with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Already have an account?{" "}
|
||||
<Link href="/login" className="underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
238
src/app/(main)/agents/[agentId]/_components/ask-agent-card.tsx
Normal file
238
src/app/(main)/agents/[agentId]/_components/ask-agent-card.tsx
Normal file
@ -0,0 +1,238 @@
|
||||
"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 { Send } from "lucide-react";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import PostgrestError 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";
|
||||
|
||||
type AskAgentCardProps = {
|
||||
agentId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export default 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 <CardContent agent={agentsData.data} disabled={disabled} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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">
|
||||
<time dateTime={"2024-11-10"} className="text-xs">
|
||||
Last Active:{" "}
|
||||
<span className="font-bold">
|
||||
{last_conv ? formatDate(last_conv) : "-"}
|
||||
</span>
|
||||
</time>
|
||||
</p>
|
||||
<Badge className="" variant={"secondary"}>
|
||||
{conversation} chats
|
||||
</Badge>
|
||||
</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-[300px] min-w-full sm:h-[150px] sm:min-w-[200px] 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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,131 @@
|
||||
"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>
|
||||
<Separator className="mb-4" />
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<Link
|
||||
href={encodeURI(`https://x.com/intent/post?text=${response}`)}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
<Twitter className="h-4 w-4 mr-2" /> Share on Twitter
|
||||
</Link>
|
||||
<Link
|
||||
href={`/agents/${agent_id}`}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
<Play className="h-4 w-4 mr-2" /> Ask Another
|
||||
</Link>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
toast.info("Copied to clipboard!");
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Copy Link
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
19
src/app/(main)/agents/[agentId]/_components/title.tsx
Normal file
19
src/app/(main)/agents/[agentId]/_components/title.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import SparklesText from "@/components/ui/sparkles-text";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
export default function Title() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease }}
|
||||
>
|
||||
<SparklesText text="Ask a Question" className="mb-8 text-5xl" />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
34
src/app/(main)/agents/[agentId]/page.tsx
Normal file
34
src/app/(main)/agents/[agentId]/page.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
||||
|
||||
import Title from "./_components/title";
|
||||
import AskAgentCard from "./_components/ask-agent-card";
|
||||
import CardBackground from "@/components/card-background";
|
||||
|
||||
export default function AskAgentPage({
|
||||
params,
|
||||
}: {
|
||||
params: { agentId: string };
|
||||
}) {
|
||||
return (
|
||||
<section className="relative flex min-h-[calc(100vh-64px)] w-full py-10 items-center justify-center overflow-hidden rounded-lg border bg-background">
|
||||
<div className="layout flex flex-col items-center">
|
||||
<Title />
|
||||
<CardBackground>
|
||||
<AskAgentCard agentId={params.agentId} />
|
||||
</CardBackground>
|
||||
</div>
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12"
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
||||
import CardBackground from "@/components/card-background";
|
||||
|
||||
import Title from "../../_components/title";
|
||||
import AskAgentCard from "../../_components/ask-agent-card";
|
||||
import ResponseAgentCard from "../../_components/response-agent-card";
|
||||
|
||||
export default function AgentAsk({
|
||||
params,
|
||||
}: {
|
||||
params: { agentId: string; responseId: string };
|
||||
}) {
|
||||
return (
|
||||
<section className="relative flex min-h-[calc(100vh-64px)] w-full py-10 items-center justify-center overflow-hidden rounded-lg border bg-background">
|
||||
<div className="layout flex flex-col items-center">
|
||||
<Title />
|
||||
<CardBackground>
|
||||
<AskAgentCard agentId={params.agentId} disabled={true} />
|
||||
</CardBackground>
|
||||
<CardBackground>
|
||||
<ResponseAgentCard agentResponseId={params.responseId} />
|
||||
</CardBackground>
|
||||
</div>
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12"
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
110
src/app/(main)/agents/create/_components/agent-form.tsx
Normal file
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" />}
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
350
src/app/(main)/agents/create/_components/form-stepper.tsx
Normal file
350
src/app/(main)/agents/create/_components/form-stepper.tsx
Normal file
@ -0,0 +1,350 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
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 { createAgentSchema } from "../schemas/create-agent-schema";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
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";
|
||||
|
||||
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: "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 default function FormStepper() {
|
||||
const [agentId, setAgentId] = React.useState("");
|
||||
const [agentName, setAgenName] = React.useState("");
|
||||
|
||||
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: userId,
|
||||
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 = (values: z.infer<typeof createAgentSchema>) => {
|
||||
createAgentMutation.mutate(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardBackground>
|
||||
{/* form title */}
|
||||
<div className="flex justify-between mb-6">
|
||||
<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 className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Step {currentIndex + 1} of {steps.length}
|
||||
</span>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
{/* form nav */}
|
||||
<nav aria-label="Checkout Steps" className="group mb-6">
|
||||
<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="flex size-10 items-center justify-center rounded-full"
|
||||
>
|
||||
{index + 1}
|
||||
</Button>
|
||||
<span className="text-sm font-medium">{step.title}</span>
|
||||
</li>
|
||||
{index < array.length - 1 && (
|
||||
<Separator
|
||||
className={`flex-1 ${
|
||||
index < currentIndex ? "bg-primary" : "bg-muted"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
{/* form content */}
|
||||
<div className="space-y-4">
|
||||
{stepper.switch({
|
||||
agentType: () => <AgentTypeComponent stepper={stepper} />,
|
||||
agentForm: () => (
|
||||
<AgentFormComponent
|
||||
stepper={stepper}
|
||||
form={form}
|
||||
isPending={createAgentMutation.isPending}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
),
|
||||
complete: () => (
|
||||
<CompleteComponent agentId={agentId} agentName={agentName} />
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
</CardBackground>
|
||||
);
|
||||
}
|
||||
|
||||
const AgentTypeComponent = ({
|
||||
stepper,
|
||||
}: {
|
||||
stepper: Stepper<
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}[]
|
||||
>;
|
||||
}) => {
|
||||
const agentTypes = [
|
||||
{
|
||||
icon: MessageCircle,
|
||||
heading: "Chat",
|
||||
description: "Access from our website",
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
heading: "CLI",
|
||||
description: "Access from terminal",
|
||||
},
|
||||
{
|
||||
icon: Braces,
|
||||
heading: "API",
|
||||
description: "Access with API",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{agentTypes.map((agentType, idx) => (
|
||||
<Button
|
||||
disabled={idx !== 0}
|
||||
key={idx}
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
idx === 0 &&
|
||||
"bg-accent-foreground hover:bg-accent-foreground border-primary",
|
||||
"py-24 border-2"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center space-y-3">
|
||||
<agentType.icon className="h-9 w-9" />
|
||||
<p className="text-xl font-bold">{agentType.heading}</p>
|
||||
<p className="text-muted-foreground">{agentType.description}</p>
|
||||
</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 CompleteComponent = ({
|
||||
agentId,
|
||||
agentName,
|
||||
}: {
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col justify-center items-center py-4">
|
||||
<div className="h-12 w-12 flex flex-col justify-center items-center rounded-full bg-green-300 ">
|
||||
<Check className="h-6 w-6 text-green-800" />
|
||||
</div>
|
||||
<h3 className="text-lg py-4 font-bold">Agent succesfully created!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
You can try to have a conversation with your agent.
|
||||
</p>
|
||||
<Link
|
||||
href={`/agents/${agentId}`}
|
||||
target="blank"
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Chat with {agentName}
|
||||
</Link>
|
||||
</div>
|
||||
{/* form footer */}
|
||||
<div>
|
||||
<div className="flex justify-end gap-4">
|
||||
<Link href="/" className={buttonVariants({ variant: "outline" })}>
|
||||
Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
19
src/app/(main)/agents/create/_components/title.tsx
Normal file
19
src/app/(main)/agents/create/_components/title.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import SparklesText from "@/components/ui/sparkles-text";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
export default function Title() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease }}
|
||||
>
|
||||
<SparklesText text="Create your own agent" className="mb-8 text-5xl" />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
27
src/app/(main)/agents/create/page.tsx
Normal file
27
src/app/(main)/agents/create/page.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import AnimatedGridPattern from "@/components/ui/animated-grid-pattern";
|
||||
|
||||
import FormStepper from "./_components/form-stepper";
|
||||
import Title from "./_components/title";
|
||||
|
||||
export default function CreateAgentPage() {
|
||||
return (
|
||||
<section className="relative flex min-h-[calc(100vh-64px)] w-full py-4 items-center justify-center overflow-hidden rounded-lg border bg-background">
|
||||
<div className="layout flex flex-col h-full justify-center items-center gap-8">
|
||||
<Title />
|
||||
<FormStepper />
|
||||
</div>
|
||||
<AnimatedGridPattern
|
||||
numSquares={30}
|
||||
maxOpacity={0.1}
|
||||
duration={3}
|
||||
repeatDelay={1}
|
||||
className={cn(
|
||||
"[mask-image:radial-gradient(500px_circle_at_center,white,transparent)]",
|
||||
"inset-x-0 inset-y-[-30%] h-[200%] skew-y-12"
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
40
src/app/(main)/agents/create/schemas/create-agent-schema.tsx
Normal file
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",
|
||||
]),
|
||||
});
|
@ -1,47 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
import { z } from "zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { Play } from "lucide-react";
|
||||
|
||||
import { formatDate } from "@/lib/utils";
|
||||
|
||||
import { Tables } from "@/utils/supabase/database.types";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
import Spinner from "@/components/spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import clsx from "clsx";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { toast } from "sonner";
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
type AgentCardProps = { data: Tables<"agents"> };
|
||||
|
||||
@ -57,106 +26,6 @@ export default function AgentCard(props: AgentCardProps) {
|
||||
last_conv,
|
||||
} = props.data;
|
||||
|
||||
const [openDialogUpdate, setOpenDialogUpdate] = React.useState(false);
|
||||
|
||||
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 } = await supabase
|
||||
.from("agents")
|
||||
.select("*")
|
||||
.eq("id", id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("agents")
|
||||
.update({
|
||||
conversation: existingAgent ? existingAgent[0].conversation + 1 : 0,
|
||||
last_conv: new Date().toISOString(),
|
||||
})
|
||||
.eq("id", id)
|
||||
.select();
|
||||
|
||||
const url = encodeURI(
|
||||
`https://ai-endpoint-one.dev3vds1.link/deepinfra-ai/${name}/${model_type}/${description}/${values.question}`
|
||||
);
|
||||
|
||||
const json = await axios.get(url);
|
||||
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["agents"],
|
||||
});
|
||||
},
|
||||
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",
|
||||
});
|
||||
}
|
||||
}
|
||||
// toast.error(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: z.infer<typeof formSchema>) => {
|
||||
mutation.mutate(values);
|
||||
};
|
||||
|
||||
const {
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
return (
|
||||
<li className="flex flex-col rounded-lg p-4 border border-accent-foreground hover:shadow-sm transition-shadow duration-200">
|
||||
<figure className="overflow-hidden relative rounded-lg object-cover border mb-3 w-full h-[200px]">
|
||||
@ -184,124 +53,14 @@ export default function AgentCard(props: AgentCardProps) {
|
||||
<h3 className="text-xl font-semibold mb-2 truncate">{name}</h3>
|
||||
<p className="text-sm text-foreground line-clamp-3 mb-4">{description}</p>
|
||||
<div className="mt-auto grid">
|
||||
<Dialog
|
||||
open={openDialogUpdate}
|
||||
onOpenChange={(val) => {
|
||||
form.reset();
|
||||
mutation.reset();
|
||||
setOpenDialogUpdate(val);
|
||||
}}
|
||||
<Link
|
||||
href={`/agents/${id}`}
|
||||
target="blank"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button size={"sm"} variant={"outline"}>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
<span>Try it</span>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[90%] max-h-[90vh] max-w-xl rounded-md overflow-y-scroll">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Try Agent</DialogTitle>
|
||||
<DialogDescription>
|
||||
Try to ask a question to our agent
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<figure className="overflow-hidden relative rounded-lg object-cover border mb-3 w-full h-[200px]">
|
||||
<Image
|
||||
className="w-full object-cover"
|
||||
src={image_url ? image_url : "/placeholder.png"}
|
||||
fill={true}
|
||||
alt={name}
|
||||
/>
|
||||
<div className="h-full w-full absolute top-0 left-0" />
|
||||
</figure>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Badge className="mb-2" variant={"secondary"}>
|
||||
{conversation} chats
|
||||
</Badge>
|
||||
<p className="mb-2 text-primary">
|
||||
<time dateTime={created_at} className="text-xs">
|
||||
Last Active:{" "}
|
||||
<span className="font-bold">
|
||||
{last_conv ? formatDate(last_conv) : "-"}
|
||||
</span>
|
||||
</time>
|
||||
</p>
|
||||
</div>
|
||||
<ul className="text-sm space-y-3 mb-3">
|
||||
<li>
|
||||
<h3 className="font-bold">Name:</h3>
|
||||
<p className="text-gray-500">{name}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h3 className="font-bold">Description:</h3>
|
||||
<p className="text-gray-500">{description}</p>
|
||||
</li>
|
||||
</ul>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-3"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="question"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="font-bold">Question:</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
rows={4}
|
||||
placeholder="For answering daily questions."
|
||||
className={clsx(
|
||||
errors.question &&
|
||||
"focus-visible:ring-destructive"
|
||||
)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{mutation.isSuccess && (
|
||||
<>
|
||||
<h3 className="text-sm font-bold">Answer:</h3>
|
||||
<blockquote className="border-l-2 pl-6 italic">
|
||||
{mutation.data.response}
|
||||
</blockquote>
|
||||
</>
|
||||
)}
|
||||
<DialogFooter className="mt-4 flex justify-end space-x-2">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={mutation.isPending}
|
||||
size="sm"
|
||||
>
|
||||
{mutation.isPending && (
|
||||
<Spinner className="mr-2 h-4 w-4 " />
|
||||
)}
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutation.isPending}
|
||||
size="sm"
|
||||
>
|
||||
{mutation.isPending && (
|
||||
<Spinner className="mr-2 h-4 w-4 " />
|
||||
)}
|
||||
Submit
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Play className="h-4 w-4 mr-2" />
|
||||
<span>Try it</span>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
@ -1,25 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { parseAsInteger, useQueryState } from "nuqs";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||
import { BadgePlus, ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||
|
||||
import PostgrestError from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
} from "@/components/ui/pagination";
|
||||
|
||||
import { createClient } from "@/utils/supabase/client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import AgentCardSkeleton from "./agent-card-skeleton";
|
||||
import AgentCard from "./agent-card";
|
||||
import { ChatFloatingButton } from "@/components/chat-floating-button";
|
||||
import AgentCardSkeleton from "./agent-card-skeleton";
|
||||
|
||||
const DATA_DISPLAY = 6;
|
||||
|
||||
@ -61,85 +65,106 @@ export function AgentList() {
|
||||
paginationFilter * DATA_DISPLAY + DATA_DISPLAY - 1
|
||||
);
|
||||
|
||||
console.log(response);
|
||||
if (response.error) throw new PostgrestError(response.error);
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="group pt-8 pb-10 min-h-[50vh] ">
|
||||
<div className="layout">
|
||||
<h1 className="mb-4 text-3xl font-bold text-foreground sm:text-4xl">
|
||||
Agents
|
||||
</h1>
|
||||
<div className="mb-8 flex flex-col sm:flex-row 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>
|
||||
</div>
|
||||
<ul className="grid gap-8 mb-10 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{isLoading &&
|
||||
[...Array(6)].map((_, idx) => (
|
||||
<li key={idx}>
|
||||
<AgentCardSkeleton />
|
||||
</li>
|
||||
))}
|
||||
{agentsData &&
|
||||
agentsData.data?.map((agent) => (
|
||||
<AgentCard key={agent.name} data={agent} />
|
||||
))}
|
||||
</ul>
|
||||
{agentsData ? (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
disabled={paginationFilter === 0}
|
||||
onClick={() => setPaginationFilter((old) => old - 1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
Page {paginationFilter + 1} of{" "}
|
||||
{agentsData.count
|
||||
? Math.ceil(agentsData.count / DATA_DISPLAY)
|
||||
: 1}
|
||||
</p>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
disabled={
|
||||
paginationFilter + 1 ===
|
||||
Math.ceil(agentsData.count! / DATA_DISPLAY)
|
||||
}
|
||||
onClick={() => setPaginationFilter((old) => old + 1)}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
) : null}
|
||||
<div className="mb-8 flex flex-col sm:flex-row 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>
|
||||
</section>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL!}/agents/create`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<BadgePlus className="h-6 w-6" />
|
||||
Create new Agent
|
||||
</Link>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
<ul className="grid gap-8 mb-10 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{isLoading &&
|
||||
[...Array(6)].map((_, idx) => (
|
||||
<li key={idx}>
|
||||
<AgentCardSkeleton />
|
||||
</li>
|
||||
))}
|
||||
{agentsData &&
|
||||
agentsData.data?.map((agent) => (
|
||||
<AgentCard key={agent.name} data={agent} />
|
||||
))}
|
||||
</ul>
|
||||
{agentsData ? (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"icon"}
|
||||
disabled={paginationFilter === 0}
|
||||
onClick={() => setPaginationFilter((old) => old - 1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<p className="text-sm font-semibold text-gray-400">
|
||||
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)}
|
||||
>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -3,8 +3,15 @@ import { AgentList } from "./_components/agent-list";
|
||||
|
||||
export default function Explore() {
|
||||
return (
|
||||
<Suspense>
|
||||
<AgentList />
|
||||
</Suspense>
|
||||
<section className="group pt-8 pb-10 min-h-[50vh] ">
|
||||
<div className="layout">
|
||||
<h1 className="mb-4 text-3xl font-bold text-foreground sm:text-4xl">
|
||||
Agents
|
||||
</h1>
|
||||
<Suspense>
|
||||
<AgentList />
|
||||
</Suspense>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
@ -1,135 +0,0 @@
|
||||
import Author from "@/components/blog-author";
|
||||
import CtaSection 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({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}): Promise<Metadata | undefined> {
|
||||
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 Blog({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}) {
|
||||
let 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-gray-200 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 shadow-md"
|
||||
/>
|
||||
</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 text-gray-500"
|
||||
>
|
||||
{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>
|
||||
<CtaSection />
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -13,6 +13,8 @@ import { cn, constructMetadata } from "@/lib/utils";
|
||||
import ReactQueryProvider from "@/providers/react-query-provider";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
import MyPrivyProvider from "@/providers/privy-provider";
|
||||
|
||||
export const metadata: Metadata = constructMetadata({});
|
||||
|
||||
export const viewport: Viewport = {
|
||||
@ -40,18 +42,20 @@ export default function RootLayout({
|
||||
)}
|
||||
>
|
||||
<ReactQueryProvider>
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem={false}
|
||||
>
|
||||
{children}
|
||||
{/* <ThemeToggle /> */}
|
||||
{/* <TailwindIndicator /> */}
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
<MyPrivyProvider>
|
||||
<NuqsAdapter>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="light"
|
||||
enableSystem={false}
|
||||
>
|
||||
{children}
|
||||
{/* <ThemeToggle /> */}
|
||||
{/* <TailwindIndicator /> */}
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</NuqsAdapter>
|
||||
</MyPrivyProvider>
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
31
src/components/card-background.tsx
Normal file
31
src/components/card-background.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import ShineBorder from "@/components/ui/shine-border";
|
||||
|
||||
const ease = [0.16, 1, 0.3, 1];
|
||||
|
||||
export default function CardBackground({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2, duration: 0.4, ease }}
|
||||
className="w-full"
|
||||
>
|
||||
<ShineBorder
|
||||
className="mb-8 z-20 relative p-6 mx-auto w-full max-w-2xl flex flex-col overflow-hidden rounded-lg border bg-background md:shadow-xl"
|
||||
color={["#f8e5da", "#FE8FB5", "#812f20"]}
|
||||
>
|
||||
{children}
|
||||
</ShineBorder>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Icons } from "@/components/icons";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
@ -9,10 +9,15 @@ import {
|
||||
} from "@/components/ui/drawer";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LoginModalOptions } from "@privy-io/react-auth";
|
||||
import Link from "next/link";
|
||||
import { IoMenuSharp } from "react-icons/io5";
|
||||
|
||||
export default function drawerDemo() {
|
||||
export default function drawerDemo({
|
||||
login,
|
||||
}: {
|
||||
login: (options?: LoginModalOptions | React.MouseEvent<any, any>) => void;
|
||||
}) {
|
||||
return (
|
||||
<Drawer>
|
||||
<DrawerTrigger>
|
||||
@ -47,22 +52,12 @@ export default function drawerDemo() {
|
||||
</nav>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<Link
|
||||
href="/login"
|
||||
className={buttonVariants({ variant: "outline" })}
|
||||
<Button
|
||||
onClick={login}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get Started for Free
|
||||
</Link>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
@ -17,7 +17,7 @@ export default function CtaSection() {
|
||||
>
|
||||
<div className="flex flex-col w-full sm:flex-row items-center justify-center space-y-4 sm:space-y-0 sm:space-x-4 pt-4">
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL}/login`}
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL}/explore`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
|
@ -1,19 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import Drawer from "@/components/drawer";
|
||||
import { Icons } from "@/components/icons";
|
||||
import Menu from "@/components/menu";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { ChevronsUpDown, LogOut } from "lucide-react";
|
||||
|
||||
import { useLogin, usePrivy } from "@privy-io/react-auth";
|
||||
|
||||
import { cn, getInitials } from "@/lib/utils";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
import Drawer from "@/components/drawer";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import Spinner from "../spinner";
|
||||
|
||||
export default function Header() {
|
||||
const [addBorder, setAddBorder] = useState(false);
|
||||
const [addBorder, setAddBorder] = React.useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const { ready, authenticated, logout, user } = usePrivy();
|
||||
const router = useRouter();
|
||||
|
||||
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);
|
||||
@ -31,7 +64,7 @@ export default function Header() {
|
||||
|
||||
return (
|
||||
<header
|
||||
className={" sticky top-0 z-50 py-2 bg-background/60 backdrop-blur"}
|
||||
className={"h-16 sticky top-0 z-50 py-2 bg-background/60 backdrop-blur"}
|
||||
>
|
||||
<div className="flex justify-between items-center container">
|
||||
<Link
|
||||
@ -46,35 +79,151 @@ export default function Header() {
|
||||
alt="logo"
|
||||
className="rounded-md shadow"
|
||||
/>
|
||||
{/* <Icons.logoCrystal className="w-auto h-[28px]" /> */}
|
||||
<span className="font-bold text-xl">{siteConfig.name}</span>
|
||||
</Link>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<div className="flex items-center ">
|
||||
{/* <nav className="mr-10">
|
||||
<Menu />
|
||||
</nav> */}
|
||||
|
||||
<div className="gap-2 flex">
|
||||
<Link href="/" className={buttonVariants({ variant: "default" })}>
|
||||
Connect Wallet
|
||||
</Link>
|
||||
{/* <Link
|
||||
href="/signup"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
)}
|
||||
>
|
||||
<Icons.logo className="h-6 w-6" />
|
||||
Get Started for Free
|
||||
</Link> */}
|
||||
{!ready ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
{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 />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
logout();
|
||||
toast.success("You have been signed out");
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
className="w-full text-primary "
|
||||
>
|
||||
<LogOut />
|
||||
Log out
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
onClick={login}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 cursor-pointer block lg:hidden">
|
||||
<Drawer />
|
||||
<div className="ml-2 mt-2 cursor-pointer block lg:hidden">
|
||||
{authenticated && user ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="max-w-48 flex items-center gap-2 border p-2 rounded-md">
|
||||
<Avatar className="hidden sm:block 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 />
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
logout();
|
||||
toast.success("You have been signed out");
|
||||
}}
|
||||
asChild
|
||||
>
|
||||
<Button variant={"ghost"} className="w-full text-primary ">
|
||||
<LogOut />
|
||||
Log out
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button
|
||||
onClick={login}
|
||||
className={buttonVariants({ variant: "default" })}
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<hr
|
||||
|
@ -104,7 +104,7 @@ function HeroCTA() {
|
||||
transition={{ delay: 0.8, duration: 0.8, ease }}
|
||||
>
|
||||
<Link
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL!}`}
|
||||
href={`${process.env.NEXT_PUBLIC_DASHBOARD_URL!}/agents/create`}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "default" }),
|
||||
"w-full sm:w-auto text-background flex gap-2"
|
||||
|
47
src/components/sections/stats.tsx
Normal file
47
src/components/sections/stats.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import BlurFade from "@/components/magicui/blur-fade";
|
||||
import Section from "@/components/section";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Brain, Shield, Zap } from "lucide-react";
|
||||
|
||||
const problems = [
|
||||
{
|
||||
title: "Dynamic Goal Formation",
|
||||
description:
|
||||
"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: Brain,
|
||||
},
|
||||
{
|
||||
title: "Contextual Understanding",
|
||||
description:
|
||||
"Our agents don't operate in a vacuum. They grasp the nuances of their environment, understanding not just what's happening, but why it matters and how it affects their objectives.",
|
||||
icon: Zap,
|
||||
},
|
||||
{
|
||||
title: "Ethical Framework Integration",
|
||||
description:
|
||||
"We've built robust ethical considerations directly into our agents' decision-making processes, ensuring they operate not just effectively, but responsibly and in alignment with human values.",
|
||||
icon: Shield,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Statistic() {
|
||||
return (
|
||||
<Section title="Be Different" subtitle="Technology That Sets Us Apart">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-12">
|
||||
{problems.map((problem, index) => (
|
||||
<BlurFade key={index} delay={0.2 + index * 0.2} inView>
|
||||
<Card className="bg-background border-none shadow-none">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center">
|
||||
<problem.icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold">{problem.title}</h3>
|
||||
<p className="text-muted-foreground">{problem.description}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</BlurFade>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
148
src/components/ui/animated-grid-pattern.tsx
Normal file
148
src/components/ui/animated-grid-pattern.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface AnimatedGridPatternProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
strokeDasharray?: any;
|
||||
numSquares?: number;
|
||||
className?: string;
|
||||
maxOpacity?: number;
|
||||
duration?: number;
|
||||
repeatDelay?: number;
|
||||
}
|
||||
|
||||
export default function AnimatedGridPattern({
|
||||
width = 40,
|
||||
height = 40,
|
||||
x = -1,
|
||||
y = -1,
|
||||
strokeDasharray = 0,
|
||||
numSquares = 50,
|
||||
className,
|
||||
maxOpacity = 0.5,
|
||||
duration = 4,
|
||||
repeatDelay = 0.5,
|
||||
...props
|
||||
}: AnimatedGridPatternProps) {
|
||||
const id = useId();
|
||||
const containerRef = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [squares, setSquares] = useState(() => generateSquares(numSquares));
|
||||
|
||||
function getPos() {
|
||||
return [
|
||||
Math.floor((Math.random() * dimensions.width) / width),
|
||||
Math.floor((Math.random() * dimensions.height) / height),
|
||||
];
|
||||
}
|
||||
|
||||
// Adjust the generateSquares function to return objects with an id, x, and y
|
||||
function generateSquares(count: number) {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
pos: getPos(),
|
||||
}));
|
||||
}
|
||||
|
||||
// Function to update a single square's position
|
||||
const updateSquarePosition = (id: number) => {
|
||||
setSquares((currentSquares) =>
|
||||
currentSquares.map((sq) =>
|
||||
sq.id === id
|
||||
? {
|
||||
...sq,
|
||||
pos: getPos(),
|
||||
}
|
||||
: sq,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// Update squares to animate in
|
||||
useEffect(() => {
|
||||
if (dimensions.width && dimensions.height) {
|
||||
setSquares(generateSquares(numSquares));
|
||||
}
|
||||
}, [dimensions, numSquares]);
|
||||
|
||||
// Resize observer to update container dimensions
|
||||
useEffect(() => {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
setDimensions({
|
||||
width: entry.contentRect.width,
|
||||
height: entry.contentRect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (containerRef.current) {
|
||||
resizeObserver.unobserve(containerRef.current);
|
||||
}
|
||||
};
|
||||
}, [containerRef]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={containerRef}
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 h-full w-full fill-red-primary/30 stroke-primary/30",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={id}
|
||||
width={width}
|
||||
height={height}
|
||||
patternUnits="userSpaceOnUse"
|
||||
x={x}
|
||||
y={y}
|
||||
>
|
||||
<path
|
||||
d={`M.5 ${height}V.5H${width}`}
|
||||
fill="none"
|
||||
strokeDasharray={strokeDasharray}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill={`url(#${id})`} />
|
||||
<svg x={x} y={y} className="overflow-visible">
|
||||
{squares.map(({ pos: [x, y], id }, index) => (
|
||||
<motion.rect
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: maxOpacity }}
|
||||
transition={{
|
||||
duration,
|
||||
repeat: 1,
|
||||
delay: index * 0.1,
|
||||
repeatType: "reverse",
|
||||
}}
|
||||
onAnimationComplete={() => updateSquarePosition(id)}
|
||||
key={`${x}-${y}-${index}`}
|
||||
width={width - 1}
|
||||
height={height - 1}
|
||||
x={x * width + 1}
|
||||
y={y * height + 1}
|
||||
fill="currentColor"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
))}
|
||||
</svg>
|
||||
</svg>
|
||||
);
|
||||
}
|
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Image
|
||||
ref={ref}
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
149
src/components/ui/confetti.tsx
Normal file
149
src/components/ui/confetti.tsx
Normal file
@ -0,0 +1,149 @@
|
||||
import type { ReactNode } from "react";
|
||||
import React, {
|
||||
createContext,
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from "react";
|
||||
import type {
|
||||
GlobalOptions as ConfettiGlobalOptions,
|
||||
CreateTypes as ConfettiInstance,
|
||||
Options as ConfettiOptions,
|
||||
} from "canvas-confetti";
|
||||
import confetti from "canvas-confetti";
|
||||
|
||||
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;
|
||||
|
||||
export default Confetti;
|
200
src/components/ui/dropdown-menu.tsx
Normal file
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,
|
||||
}
|
43
src/components/ui/meteors.tsx
Normal file
43
src/components/ui/meteors.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface MeteorsProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
number?: number;
|
||||
}
|
||||
export const Meteors = ({ number = 20, ...props }: MeteorsProps) => {
|
||||
const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const styles = [...new Array(number)].map(() => ({
|
||||
top: -5,
|
||||
left: Math.floor(Math.random() * window.innerWidth) + "px",
|
||||
animationDelay: Math.random() * 1 + 0.2 + "s",
|
||||
animationDuration: Math.floor(Math.random() * 8 + 2) + "s",
|
||||
}));
|
||||
setMeteorStyles(styles);
|
||||
}, [number]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{[...meteorStyles].map((style, idx) => (
|
||||
// Meteor Head
|
||||
<span
|
||||
key={idx}
|
||||
className={cn(
|
||||
"pointer-events-none absolute left-1/2 top-1/2 size-0.5 rotate-[215deg] animate-meteor rounded-full bg-slate-500 shadow-[0_0_0_1px_#ffffff10]",
|
||||
)}
|
||||
style={style}
|
||||
{...props}
|
||||
>
|
||||
{/* Meteor Tail */}
|
||||
<div className="pointer-events-none absolute top-1/2 -z-10 h-px w-[50px] -translate-y-1/2 bg-gradient-to-r from-slate-500 to-transparent" />
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
31
src/components/ui/separator.tsx
Normal file
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 }
|
61
src/components/ui/shine-border.tsx
Normal file
61
src/components/ui/shine-border.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type TColorProp = string | string[];
|
||||
|
||||
interface ShineBorderProps {
|
||||
borderRadius?: number;
|
||||
borderWidth?: number;
|
||||
duration?: number;
|
||||
color?: TColorProp;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Shine Border
|
||||
* @description It is an animated background border effect component with easy to use and configurable props.
|
||||
* @param borderRadius defines the radius of the border.
|
||||
* @param borderWidth defines the width of the border.
|
||||
* @param duration defines the animation duration to be applied on the shining border
|
||||
* @param color a string or string array to define border color.
|
||||
* @param className defines the class name to be applied to the component
|
||||
* @param children contains react node elements.
|
||||
*/
|
||||
export default function ShineBorder({
|
||||
borderRadius = 8,
|
||||
borderWidth = 1,
|
||||
duration = 14,
|
||||
color = "#000000",
|
||||
className,
|
||||
children,
|
||||
}: ShineBorderProps) {
|
||||
return (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--border-radius": `${borderRadius}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"relative min-h-[60px] w-fit min-w-[300px] rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--border-width": `${borderWidth}px`,
|
||||
"--border-radius": `${borderRadius}px`,
|
||||
"--duration": `${duration}s`,
|
||||
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
|
||||
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${color instanceof Array ? color.join(",") : color},transparent,transparent)`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={`pointer-events-none before:bg-shine-size before:absolute before:inset-0 before:size-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:--background-radial-gradient] before:[background-size:300%_300%] before:[mask:--mask-linear-gradient] motion-safe:before:animate-shine`}
|
||||
></div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
152
src/components/ui/sparkles-text.tsx
Normal file
152
src/components/ui/sparkles-text.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
"use client";
|
||||
|
||||
import { CSSProperties, ReactElement, useEffect, useState } from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Sparkle {
|
||||
id: string;
|
||||
x: string;
|
||||
y: string;
|
||||
color: string;
|
||||
delay: number;
|
||||
scale: number;
|
||||
lifespan: number;
|
||||
}
|
||||
|
||||
interface SparklesTextProps {
|
||||
/**
|
||||
* @default <div />
|
||||
* @type ReactElement
|
||||
* @description
|
||||
* The component to be rendered as the text
|
||||
* */
|
||||
as?: ReactElement;
|
||||
|
||||
/**
|
||||
* @default ""
|
||||
* @type string
|
||||
* @description
|
||||
* The className of the text
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* @required
|
||||
* @type string
|
||||
* @description
|
||||
* The text to be displayed
|
||||
* */
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* @default 10
|
||||
* @type number
|
||||
* @description
|
||||
* The count of sparkles
|
||||
* */
|
||||
sparklesCount?: number;
|
||||
|
||||
/**
|
||||
* @default "{first: '#9E7AFF', second: '#FE8BBB'}"
|
||||
* @type string
|
||||
* @description
|
||||
* The colors of the sparkles
|
||||
* */
|
||||
colors?: {
|
||||
first: string;
|
||||
second: string;
|
||||
};
|
||||
}
|
||||
|
||||
const SparklesText: React.FC<SparklesTextProps> = ({
|
||||
text,
|
||||
colors = { first: "#9E7AFF", second: "#FE8BBB" },
|
||||
className,
|
||||
sparklesCount = 10,
|
||||
...props
|
||||
}) => {
|
||||
const [sparkles, setSparkles] = useState<Sparkle[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const generateStar = (): Sparkle => {
|
||||
const starX = `${Math.random() * 100}%`;
|
||||
const starY = `${Math.random() * 100}%`;
|
||||
const color = Math.random() > 0.5 ? colors.first : colors.second;
|
||||
const delay = Math.random() * 2;
|
||||
const scale = Math.random() * 1 + 0.3;
|
||||
const lifespan = Math.random() * 10 + 5;
|
||||
const id = `${starX}-${starY}-${Date.now()}`;
|
||||
return { id, x: starX, y: starY, color, delay, scale, lifespan };
|
||||
};
|
||||
|
||||
const initializeStars = () => {
|
||||
const newSparkles = Array.from({ length: sparklesCount }, generateStar);
|
||||
setSparkles(newSparkles);
|
||||
};
|
||||
|
||||
const updateStars = () => {
|
||||
setSparkles((currentSparkles) =>
|
||||
currentSparkles.map((star) => {
|
||||
if (star.lifespan <= 0) {
|
||||
return generateStar();
|
||||
} else {
|
||||
return { ...star, lifespan: star.lifespan - 0.1 };
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
initializeStars();
|
||||
const interval = setInterval(updateStars, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [colors.first, colors.second, sparklesCount]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("text-6xl font-bold", className)}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
"--sparkles-first-color": `${colors.first}`,
|
||||
"--sparkles-second-color": `${colors.second}`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<span className="relative inline-block">
|
||||
{sparkles.map((sparkle) => (
|
||||
<Sparkle key={sparkle.id} {...sparkle} />
|
||||
))}
|
||||
<strong>{text}</strong>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Sparkle: React.FC<Sparkle> = ({ id, x, y, color, delay, scale }) => {
|
||||
return (
|
||||
<motion.svg
|
||||
key={id}
|
||||
className="pointer-events-none absolute z-20"
|
||||
initial={{ opacity: 0, left: x, top: y }}
|
||||
animate={{
|
||||
opacity: [0, 1, 0],
|
||||
scale: [0, scale, 0],
|
||||
rotate: [75, 120, 150],
|
||||
}}
|
||||
transition={{ duration: 0.8, repeat: Infinity, delay }}
|
||||
width="21"
|
||||
height="21"
|
||||
viewBox="0 0 21 21"
|
||||
>
|
||||
<path
|
||||
d="M9.82531 0.843845C10.0553 0.215178 10.9446 0.215178 11.1746 0.843845L11.8618 2.72026C12.4006 4.19229 12.3916 6.39157 13.5 7.5C14.6084 8.60843 16.8077 8.59935 18.2797 9.13822L20.1561 9.82534C20.7858 10.0553 20.7858 10.9447 20.1561 11.1747L18.2797 11.8618C16.8077 12.4007 14.6084 12.3916 13.5 13.5C12.3916 14.6084 12.4006 16.8077 11.8618 18.2798L11.1746 20.1562C10.9446 20.7858 10.0553 20.7858 9.82531 20.1562L9.13819 18.2798C8.59932 16.8077 8.60843 14.6084 7.5 13.5C6.39157 12.3916 4.19225 12.4007 2.72023 11.8618L0.843814 11.1747C0.215148 10.9447 0.215148 10.0553 0.843814 9.82534L2.72023 9.13822C4.19225 8.59935 6.39157 8.60843 7.5 7.5C8.60843 6.39157 8.59932 4.19229 9.13819 2.72026L9.82531 0.843845Z"
|
||||
fill={color}
|
||||
/>
|
||||
</motion.svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default SparklesText;
|
@ -249,3 +249,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 `${yearsAgo} year${yearsAgo > 1 ? "s" : ""} ago`;
|
||||
}
|
||||
}
|
||||
|
||||
export const getInitials = (string: string) =>
|
||||
string
|
||||
.split(/\s/)
|
||||
.reduce((response, word) => (response += word.slice(0, 1)), "");
|
||||
|
49
src/providers/privy-provider.tsx
Normal file
49
src/providers/privy-provider.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { PrivyProvider } from "@privy-io/react-auth";
|
||||
|
||||
import { toSolanaWalletConnectors } from "@privy-io/react-auth/solana";
|
||||
|
||||
const solanaConnectors = toSolanaWalletConnectors();
|
||||
|
||||
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: "ethereum-and-solana",
|
||||
walletList: ["detected_wallets", "phantom"],
|
||||
logo: "https://supabasekong-mco40gw4sc0gs4ks40w4c4g4.dev3vds1.link/storage/v1/object/public/agent-thumbnails/Beactio%20Banner.png",
|
||||
},
|
||||
loginMethods: ["email", "wallet", "discord"],
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,23 +1,63 @@
|
||||
"use client";
|
||||
|
||||
// import { handleError } from "@/lib/utils";
|
||||
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) => {
|
||||
// handleError(error);
|
||||
if (error instanceof PostgrestError) {
|
||||
toast.error(error.code, { description: error.message });
|
||||
}
|
||||
},
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
// handleError(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",
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
@ -59,13 +59,44 @@ export type Database = {
|
||||
};
|
||||
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 | null;
|
||||
image_url: string;
|
||||
last_conv: string | null;
|
||||
model_type: Database["public"]["Enums"]["modelType"];
|
||||
name: string;
|
||||
@ -76,7 +107,7 @@ export type Database = {
|
||||
created_at?: string;
|
||||
description: string;
|
||||
id?: number;
|
||||
image_url?: string | null;
|
||||
image_url?: string;
|
||||
last_conv?: string | null;
|
||||
model_type: Database["public"]["Enums"]["modelType"];
|
||||
name: string;
|
||||
@ -87,7 +118,7 @@ export type Database = {
|
||||
created_at?: string;
|
||||
description?: string;
|
||||
id?: number;
|
||||
image_url?: string | null;
|
||||
image_url?: string;
|
||||
last_conv?: string | null;
|
||||
model_type?: Database["public"]["Enums"]["modelType"];
|
||||
name?: string;
|
||||
|
@ -10,94 +10,136 @@ const config = {
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
animation: {
|
||||
marquee: "marquee var(--duration) linear infinite",
|
||||
"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",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite",
|
||||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
from: { transform: "translateX(0)" },
|
||||
to: { transform: "translateX(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
"marquee-vertical": {
|
||||
from: { transform: "translateY(0)" },
|
||||
to: { transform: "translateY(calc(-100% - var(--gap)))" },
|
||||
},
|
||||
"border-beam": {
|
||||
"100%": {
|
||||
"offset-distance": "100%",
|
||||
},
|
||||
},
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
ripple: {
|
||||
"0%, 100%": {
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
},
|
||||
"50%": {
|
||||
transform: "translate(-50%, -50%) scale(0.9)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px'
|
||||
}
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
}
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
animation: {
|
||||
marquee: 'marquee var(--duration) linear infinite',
|
||||
'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',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
ripple: 'ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite',
|
||||
meteor: 'meteor 5s linear infinite',
|
||||
shine: 'shine var(--duration) infinite linear'
|
||||
},
|
||||
keyframes: {
|
||||
marquee: {
|
||||
from: {
|
||||
transform: 'translateX(0)'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateX(calc(-100% - var(--gap)))'
|
||||
}
|
||||
},
|
||||
'marquee-vertical': {
|
||||
from: {
|
||||
transform: 'translateY(0)'
|
||||
},
|
||||
to: {
|
||||
transform: 'translateY(calc(-100% - var(--gap)))'
|
||||
}
|
||||
},
|
||||
'border-beam': {
|
||||
'100%': {
|
||||
'offset-distance': '100%'
|
||||
}
|
||||
},
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0'
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
}
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)'
|
||||
},
|
||||
to: {
|
||||
height: '0'
|
||||
}
|
||||
},
|
||||
ripple: {
|
||||
'0%, 100%': {
|
||||
transform: 'translate(-50%, -50%) scale(1)'
|
||||
},
|
||||
'50%': {
|
||||
transform: 'translate(-50%, -50%) scale(0.9)'
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
'0%': {
|
||||
transform: 'rotate(215deg) translateX(0)',
|
||||
opacity: '1'
|
||||
},
|
||||
'70%': {
|
||||
opacity: '1'
|
||||
},
|
||||
'100%': {
|
||||
transform: 'rotate(215deg) translateX(-500px)',
|
||||
opacity: '0'
|
||||
}
|
||||
},
|
||||
shine: {
|
||||
'0%': {
|
||||
'background-position': '0% 0%'
|
||||
},
|
||||
'50%': {
|
||||
'background-position': '100% 100%'
|
||||
},
|
||||
to: {
|
||||
'background-position': '0% 0%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
|
||||
} satisfies Config;
|
||||
|
Loading…
x
Reference in New Issue
Block a user