diff --git a/frontend/debate-ai/app/create-debate/page.tsx b/frontend/debate-ai/app/create-debate/page.tsx new file mode 100644 index 0000000..f244dcd --- /dev/null +++ b/frontend/debate-ai/app/create-debate/page.tsx @@ -0,0 +1,48 @@ +"use client"; + +import React from "react"; +import { CreateDebate } from "../../components/CreateDebate"; +import Image from "next/image"; + +const CreateDebatePage = () => { + return ( +
+ {/* Content container */} +
+ {/* Left Image - Hidden on mobile */} +
+ Julius Caesar Left +
+ + {/* Center Content - Full width on mobile, centered on desktop */} +
+ +
+ + {/* Right Image - Hidden on mobile */} +
+ Julius Caesar Right +
+
+
+ ); +}; + +export default CreateDebatePage; diff --git a/frontend/debate-ai/app/create-gladiator/page.tsx b/frontend/debate-ai/app/create-gladiator/page.tsx new file mode 100644 index 0000000..9d2df40 --- /dev/null +++ b/frontend/debate-ai/app/create-gladiator/page.tsx @@ -0,0 +1,479 @@ +"use client"; +import React, { useEffect, useRef } from "react"; +import { CreateDebate } from "../../components/CreateDebate"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Toaster, toast } from "sonner"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Check, + ChevronRight, + Code, + Copy, + Loader2, + MessageSquare, + Shield, + Sword, + Swords, +} from "lucide-react"; +import Image from "next/image"; +import { + useAccount, + useWriteContract, + useWaitForTransactionReceipt, +} from "wagmi"; +import { GLADIATOR_NFT_ADDRESS, GLADIATOR_NFT_ABI } from "@/config/contracts"; +import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from "@/config/contracts"; +import { Label } from "../../components/ui/label"; +import { Badge } from "../../components/ui/badge"; +import { Switch } from "../../components/ui/switch"; + +interface GeneratedGladiator { + name: string; + image: string; + description: string; + speciality: string; + stats: { + strength: number; + agility: number; + intelligence: number; + }; + ipfsUrl?: string; +} + +const CreateGladiator = () => { + const [gladiator, setGladiator] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [showMintingModal, setShowMintingModal] = useState(false); + const [mintError, setMintError] = useState(null); + + const [showJson, setShowJson] = useState(false); + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(JSON.stringify(gladiator, null, 2)); + setCopied(true); + toast.success("JSON copied to clipboard", { + description: "The gladiator data has been copied to your clipboard.", + }); + setTimeout(() => setCopied(false), 2000); + }; + + // Wagmi hooks for minting + const { address, isConnected } = useAccount(); + const { + writeContract, + isPending: isMintPending, + data: txHash, + } = useWriteContract(); + const { + isLoading: isMintConfirming, + isSuccess: isMintSuccess, + error: mintTxError, + } = useWaitForTransactionReceipt({ + hash: txHash, + }); + const coordinatorUrl = process.env.NEXT_PUBLIC_COORDINATOR_URL; + + async function handleSubmit(formData: FormData) { + const twitterHandle = formData.get("twitter")?.toString().replace("@", ""); + + if (!twitterHandle) { + toast.error("Twitter handle required", { + description: + "Please enter a valid Twitter handle to generate a gladiator.", + }); + return; + } + + setIsLoading(true); + setError(null); + + const promise = async () => { + try { + const response = await fetch( + `${coordinatorUrl}/api/character/generate`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: twitterHandle }), + } + ); + + if (!response.ok) { + throw new Error("Failed to generate gladiator"); + } + + const characterData = await response.json(); + const gladiatorData: GeneratedGladiator = { + name: characterData.name, + image: + process.env.NEXT_PUBLIC_DEFAULT_GLADIATOR_IMAGE || + "/placeholder-gladiator.png", + description: characterData.bio.join(" "), + speciality: characterData.topics[0] || "General Philosophy", + stats: { + strength: Math.min(100, characterData.topics.length * 20), + agility: Math.min(100, characterData.postExamples.length * 5), + intelligence: Math.min(100, characterData.adjectives.length * 25), + }, + ipfsUrl: characterData.ipfsUrl, + }; + + setGladiator(gladiatorData); + return gladiatorData; + } catch (e) { + const errorMessage = + e instanceof Error ? e.message : "Failed to generate gladiator"; + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + toast.promise(promise(), { + loading: "Generating your gladiator...", + success: (data) => `${data.name} has been generated successfully!`, + error: (err) => err.message, + }); + } + + const handleMint = async () => { + if (!gladiator || !address || !isConnected) { + toast.error("Cannot mint gladiator", { + description: + "Please ensure your wallet is connected and a gladiator has been generated.", + }); + return; + } + + try { + setMintError(null); + + const publicKey = Math.floor(Math.random() * 1000000).toString(16); + + // toast.promise( + // writeContract({ + // address: MARKET_FACTORY_ADDRESS, + // abi: MARKET_FACTORY_ABI, + // functionName: "registerGladiator", + // args: [ + // gladiator.name, + // gladiator.ipfsUrl || "", + // publicKey, + // ], + // }), + // { + // loading: 'Minting your gladiator NFT...', + // success: 'Gladiator NFT minted successfully!', + // error: (err) => `Failed to mint: ${err.message}`, + // } + // ); + // -------- + + // ---------- + await writeContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "registerGladiator", + args: [gladiator.name, gladiator.ipfsUrl || "", publicKey], + }); + } catch (e) { + const errorMessage = + e instanceof Error ? e.message : "Failed to mint NFT"; + setMintError(errorMessage); + toast.error("Minting failed", { + description: errorMessage, + }); + } + }; + + const toastShown = useRef({ + confirming: false, + success: false, + error: false, + }); + + useEffect(() => { + if (isMintConfirming && !toastShown.current.confirming) { + toast.info("Transaction is being confirmed..."); + toastShown.current.confirming = true; + } + + if (isMintSuccess && !toastShown.current.success) { + toast.success("Minting successful!"); + toastShown.current.success = true; + } + + if (mintTxError && !toastShown.current.error) { + toast.error(`Transaction failed: ${mintTxError.message}`); + toastShown.current.error = true; + } + }, [isMintConfirming, isMintSuccess, mintTxError]); + + return ( +
+ +
+
+ Julius Caesar Right +
+ +
+ {!gladiator && ( +
+ + +
+
+ CREATE GLADIATOR +
+
+ The arena awaits your brilliance +
+
+
+ +
+
+ + + +
+ +
+ {!isConnected ? ( +
+ Please connect your wallet to create a gladiator +
+ ) : ( +
+
+ {"Background"} +
+ + +
+ )} +
+
+
+
+
+ )} + {error &&

{error}

} + {gladiator && ( +
+ +
+ {!showJson ? ( + <> +
+
+
+ {gladiator.name} +
+
+
+ + JSON + +
+
+
+ +
+
+ +
+
+

+ {gladiator.name} +

+

+ {gladiator.description} +

+
+ +
+
+
+ +
+
+ + Speciality + + + {gladiator.speciality} + +
+
+
+
+ +
+
+ + Class + + + Warrior + +
+
+
+ +
+

+ Combat Stats +

+
+ {Object.entries(gladiator.stats).map( + ([stat, value]) => ( +
+
+ + {stat} + + + {value}% + +
+
+
+
+
+ ) + )} +
+
+ + +
+
+ + ) : ( +
+
+
+ + JSON + +
+ +
+                          {JSON.stringify(gladiator, null, 2)}
+                        
+
+
+ )} +
+ +
+ )} +
+ +
+ Julius Caesar Right +
+
+
+ ); +}; + +export default CreateGladiator; diff --git a/frontend/debate-ai/app/debate/[id]/page.tsx b/frontend/debate-ai/app/debate/[id]/page.tsx index 6aad1c6..6889e14 100644 --- a/frontend/debate-ai/app/debate/[id]/page.tsx +++ b/frontend/debate-ai/app/debate/[id]/page.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; -import { DebateView } from '@/components/DebateView'; +import { DebateView2 } from "../../../components/debate-details/DebateView2"; export default function DebatePage({ params }: { params: { id: string } }) { return ( -
- +
+
); -} \ No newline at end of file +} diff --git a/frontend/debate-ai/app/gladiators/page.tsx b/frontend/debate-ai/app/gladiators/page.tsx new file mode 100644 index 0000000..b24be05 --- /dev/null +++ b/frontend/debate-ai/app/gladiators/page.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { GladiatorsGrid } from "../../components/gladiators/gladiators-grid"; + +const Gladiators = () => { + return ( + <> + + + ); +}; + +export default Gladiators; diff --git a/frontend/debate-ai/app/globals.css b/frontend/debate-ai/app/globals.css index f9267b4..d78f6db 100644 --- a/frontend/debate-ai/app/globals.css +++ b/frontend/debate-ai/app/globals.css @@ -150,3 +150,73 @@ body { .animate-blink { animation: blink 2s infinite ease-in-out; } + +.custom-scrollbar::-webkit-scrollbar { + width: 8px; +} + +.custom-scrollbar::-webkit-scrollbar-track { + background: #2a1b15; + border-radius: 4px; +} + +.custom-scrollbar::-webkit-scrollbar-thumb { + background: #d1bb9e20; + border-radius: 4px; + border: 2px solid #2a1b15; +} + +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: #d1bb9e40; +} + +.custom-scrollbar-main::-webkit-scrollbar { + width: 8px; +} + +.custom-scrollbar-main::-webkit-scrollbar-track { + background: #4d3328; + border-radius: 4px; +} + +.custom-scrollbar-main::-webkit-scrollbar-thumb { + background: #e3e2e2; + border-radius: 4px; + border: 2px solid #4d3328; +} + +.custom-scrollbar-main::-webkit-scrollbar-thumb:hover { + background: #e3e2e2; +} + +.navbar { + height: 3.5rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + /* bg-gray-900 */ + background-image: url("../public/navbar.png"); + background-repeat: no-repeat; + background-size: cover; +} + +@media (max-width: 1040px) { + .navbar { + background-image: none; + background: #2a1b15 !important; + } +} + +@media (max-width: 768px) { + .width-set { + width: 100%; + } +} + +@media (max-width: 1024px) { + .width-debate-sidebar { + width: 90%; + left: -10px; + } +} diff --git a/frontend/debate-ai/app/home/page.tsx b/frontend/debate-ai/app/home/page.tsx index 9853d92..c5dcf11 100644 --- a/frontend/debate-ai/app/home/page.tsx +++ b/frontend/debate-ai/app/home/page.tsx @@ -1,13 +1,13 @@ "use client"; -import { DebateList } from "../../components/DebateList"; -import HomeCenter from "../../components/HomeCenter"; -import Navbar from "../../components/Navbar"; +import { DebateList } from "../../components/home-page/DebateList"; +import HomeCenter from "../../components/home-page/HomeCenter"; +import Navbar from "../../components/common/Navbar"; export default function Home() { return ( <> - + {/* */} {/* home screen start */} diff --git a/frontend/debate-ai/app/layout.tsx b/frontend/debate-ai/app/layout.tsx index 4a7ff2c..eb4b4c0 100644 --- a/frontend/debate-ai/app/layout.tsx +++ b/frontend/debate-ai/app/layout.tsx @@ -1,14 +1,17 @@ import "./globals.css"; import "@rainbow-me/rainbowkit/styles.css"; import { Providers } from "./providers"; -import LoaderWrapper from "../components/LoaderWrapper"; +import LoaderWrapper from "../components/common/LoaderWrapper"; +import NavbarWrapper from "../components/common/NavbarWrapper"; function RootLayout({ children }: { children: React.ReactNode }) { return ( - + - {children} + + {children} + diff --git a/frontend/debate-ai/app/leaderboard/page.tsx b/frontend/debate-ai/app/leaderboard/page.tsx new file mode 100644 index 0000000..e0c152c --- /dev/null +++ b/frontend/debate-ai/app/leaderboard/page.tsx @@ -0,0 +1,335 @@ +"use client"; +import React, { useState } from "react"; +import { + Trophy, + Users, + ChevronLeft, + ChevronRight, + Medal, + Coins, +} from "lucide-react"; + +type Gladiator = { + aiAddress: string; + name: string; + model: string; + index: bigint; + isActive: boolean; + publicKey: string; +}; + +type LeaderboardEntry = { + gladiatorIndex: bigint; + totalScore: bigint; + gladiator?: { + name: string; + image: string; + earnings: string; + winStreak: number; + rank: string; + }; +}; + +type UserEntry = { + address: string; + score: bigint; + image: string; + earnings: string; + rank: string; +}; + +function Leaderboard() { + const [activeTab, setActiveTab] = useState<"gladiators" | "users">( + "gladiators" + ); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 10; + + // Simulated data with enhanced profile information + const leaderboardData = [ + [BigInt(1000), BigInt(800), BigInt(600)], + [BigInt(0), BigInt(1), BigInt(2)], + ]; + + const gladiators = [ + { + name: "Maximus", + image: + "https://images.unsplash.com/photo-1590086782957-93c06ef21604?w=200&h=200&fit=crop", + earnings: "5,234.50", + winStreak: 12, + rank: "Legendary", + }, + { + name: "Spartacus", + image: + "https://images.unsplash.com/photo-1618077360395-f3068be8e001?w=200&h=200&fit=crop", + earnings: "4,150.75", + winStreak: 8, + rank: "Elite", + }, + { + name: "Crixus", + image: + "https://images.unsplash.com/photo-1566492031773-4f4e44671857?w=200&h=200&fit=crop", + earnings: "3,890.25", + winStreak: 5, + rank: "Veteran", + }, + ]; + + const leaderboard: LeaderboardEntry[] = leaderboardData + ? (leaderboardData as [bigint[], bigint[]])[0].map( + (score: bigint, i: number) => ({ + gladiatorIndex: (leaderboardData as [bigint[], bigint[]])[1][i], + totalScore: score, + gladiator: (gladiators as any[])?.[ + Number((leaderboardData as [bigint[], bigint[]])[1][i]) + ], + }) + ) + : []; + + // Enhanced user data with multiple entries for pagination + const users: UserEntry[] = Array(15) + .fill(null) + .map((_, index) => ({ + address: `0x${Math.random().toString(16).slice(2, 10)}...${Math.random() + .toString(16) + .slice(2, 6)}`, + score: BigInt(Math.floor(Math.random() * 1000)), + image: [ + "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop", + "https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?w=200&h=200&fit=crop", + "https://images.unsplash.com/photo-1633332755192-727a05c4013d?w=200&h=200&fit=crop", + ][index % 3], + earnings: (Math.random() * 5000 + 1000).toFixed(2), + rank: ["Diamond", "Platinum", "Gold"][index % 3], + })); + + // Sort users by score + users.sort((a, b) => Number(b.score - a.score)); + + const currentData = activeTab === "gladiators" ? leaderboard : users; + const totalPages = Math.ceil(currentData.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentItems = currentData.slice(startIndex, endIndex); + + // Reset to first page when switching tabs + React.useEffect(() => { + setCurrentPage(1); + }, [activeTab]); + + const getMedalColor = (index: number) => { + switch (index) { + case 0: + return "text-[#FFD700]"; // Gold + case 1: + return "text-[#C0C0C0]"; // Silver + case 2: + return "text-[#CD7F32]"; // Bronze + default: + return "text-[#E6D5C3]"; + } + }; + + // Function to generate page numbers with ellipsis + const getPageNumbers = () => { + const pageNumbers = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + + // Always show first page + pageNumbers.push(1); + + if (currentPage > 3) { + pageNumbers.push("..."); + } + + // Show pages around current page + for ( + let i = Math.max(2, currentPage - 1); + i <= Math.min(totalPages - 1, currentPage + 1); + i++ + ) { + pageNumbers.push(i); + } + + if (currentPage < totalPages - 2) { + pageNumbers.push("..."); + } + + // Always show last page + if (totalPages > 1) { + pageNumbers.push(totalPages); + } + + return pageNumbers; + }; + + return ( +
+
+
+ {/* Header */} +
+

+ + Arena Leaderboard +

+

+ Top warriors competing for glory and riches in the arena +

+
+ + {/* Tabs */} +
+ + +
+ + {/* Leaderboard Table */} +
+
+ {currentItems.map((entry, index) => { + const globalIndex = startIndex + index; + const isGladiator = activeTab === "gladiators"; + const entryData = isGladiator + ? (entry as LeaderboardEntry).gladiator + : (entry as UserEntry); + + return ( +
+
+
+ {globalIndex <= 2 ? ( + + ) : ( + + {globalIndex + 1} + + )} +
+
+ {isGladiator +
+
+ + {isGladiator + ? entryData?.name + : (entry as UserEntry).address} + +
+
+
+
+
+ + ${entryData?.earnings} + +
+ + {isGladiator + ? (entry as LeaderboardEntry).totalScore.toString() + : (entry as UserEntry).score.toString()}{" "} + pts + +
+
+
+ ); + })} +
+ + {/* Enhanced Pagination */} + {totalPages > 1 && ( +
+ +
+ {getPageNumbers().map((pageNum, index) => + pageNum === "..." ? ( + + ... + + ) : ( + + ) + )} +
+ +
+ )} +
+
+
+
+ ); +} + +export default Leaderboard; diff --git a/frontend/debate-ai/app/page.tsx b/frontend/debate-ai/app/page.tsx index af981c7..a1dbb07 100644 --- a/frontend/debate-ai/app/page.tsx +++ b/frontend/debate-ai/app/page.tsx @@ -1,90 +1,14 @@ -'use client'; +"use client"; -import { Suspense } from 'react'; -import { CreateDebate } from '@/components/CreateDebate'; -import { DebateList } from '@/components/DebateList'; -import { GladiatorsGrid } from '@/components/gladiators-grid'; -import { Card } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { CreateGladiatorForm } from '@/components/CreateGladiatorForm'; -import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { DebateList } from "../components/home-page/DebateList"; +import HomeCenter from "../components/home-page/HomeCenter"; export default function Home() { return ( -
- {/* Header */} -
-
-
-

agora.ai

- -
-
-
+ <> + - {/* Main Content */} -
- - - - - Active Debates - - - Gladiators - - - Create Debate - - - Create Gladiator - - - - - -
- Loading debates... -
- }> - - - - - - -
- Loading gladiators... -
- }> - - - - - - - - - - - - -
- + + ); } - diff --git a/frontend/debate-ai/components/CreateDebate.tsx b/frontend/debate-ai/components/CreateDebate.tsx index 62caeba..26003ca 100644 --- a/frontend/debate-ai/components/CreateDebate.tsx +++ b/frontend/debate-ai/components/CreateDebate.tsx @@ -1,22 +1,39 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { useWriteContract, useWaitForTransactionReceipt, useAccount, useChainId } from 'wagmi'; -import { DEBATE_FACTORY_ADDRESS, DEBATE_FACTORY_ABI, MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI, MOCK_TOKEN_ADDRESS } from '@/config/contracts'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from './ui/card'; -import { Label } from './ui/label'; -import { Dialog, DialogContent } from './ui/dialog'; -import { decodeEventLog, Log } from 'viem'; - -// Define gladiator names and addresses +import { useState, useEffect } from "react"; +import { + useWriteContract, + useWaitForTransactionReceipt, + useAccount, + useChainId, +} from "wagmi"; +import { + DEBATE_FACTORY_ADDRESS, + DEBATE_FACTORY_ABI, + MARKET_FACTORY_ADDRESS, + MARKET_FACTORY_ABI, + MOCK_TOKEN_ADDRESS, +} from "@/config/contracts"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "./ui/card"; +import { Label } from "./ui/label"; +import { Dialog, DialogContent } from "./ui/dialog"; +import { decodeEventLog, Log } from "viem"; +import { Sword, Clock, Users, MessageSquare } from "lucide-react"; +import Image from "next/image"; +import { Toaster, toast } from "sonner"; + const GLADIATOR_NAMES = [ "Socrates", "Plato", "Aristotle", "Marcus Aurelius", - "Seneca" + "Seneca", ]; const GLADIATOR_ADDRESSES = [ @@ -24,155 +41,192 @@ const GLADIATOR_ADDRESSES = [ "0x2222222222222222222222222222222222222222", "0x3333333333333333333333333333333333333333", "0x4444444444444444444444444444444444444444", - "0x5555555555555555555555555555555555555555" + "0x5555555555555555555555555555555555555555", ] as const; -// Simple public keys for testing const GLADIATOR_PUBLIC_KEYS = [ "0x0001", "0x0002", "0x0003", "0x0004", - "0x0005" + "0x0005", ] as const; -// Market parameters -const DEFAULT_BONDING_TARGET = BigInt(1000) * BigInt(10**18); // 1000 tokens -const DEFAULT_BONDING_DURATION = 7 * 24 * 60 * 60; // 7 days -const DEFAULT_BASE_PRICE = 100; // $0.01 in basis points +const DEFAULT_BONDING_TARGET = BigInt(1000) * BigInt(10 ** 18); +const DEFAULT_BONDING_DURATION = 7 * 24 * 60 * 60; +const DEFAULT_BASE_PRICE = 100; +const ROUNDS = "3"; +const JUDGEAI = "0x6AaE19C60cB5f933E9061d81a1C915c4A920abCC"; export function CreateDebate() { const { address, isConnected } = useAccount(); const chainId = useChainId(); - const [topic, setTopic] = useState(''); - const [duration, setDuration] = useState('1'); - const [rounds, setRounds] = useState('3'); - const [judgeAI, setJudgeAI] = useState(''); + const [topic, setTopic] = useState(""); + const [duration, setDuration] = useState("1"); - const { data: debateHash, writeContract: writeDebate, error: writeError, isPending: isDebatePending } = useWriteContract(); + const { + data: debateHash, + writeContract: writeDebate, + error: writeError, + isPending: isDebatePending, + } = useWriteContract(); - const { - data: marketHash, - writeContract: writeMarket, + const { + data: marketHash, + writeContract: writeMarket, error: marketError, - isPending: isMarketPending + isPending: isMarketPending, } = useWriteContract(); - const { isLoading: isConfirmingDebate, isSuccess: isDebateSuccess, data: receipt } = useWaitForTransactionReceipt({ + const { + isLoading: isConfirmingDebate, + isSuccess: isDebateSuccess, + data: receipt, + } = useWaitForTransactionReceipt({ hash: debateHash, }); - const { isLoading: isConfirmingMarket } = useWaitForTransactionReceipt({ - hash: marketHash, - }); + const { isLoading: isConfirmingMarket, isSuccess: isMarketSuccess } = + useWaitForTransactionReceipt({ + hash: marketHash, + }); - // Log any errors useEffect(() => { if (writeError) { - console.error('Debate creation error:', writeError); + console.error("Debate creation error:", writeError); + toast.error(`Debate creation error:`); } if (marketError) { - console.error('Market creation error:', marketError); + console.error("Market creation error:", marketError); + toast.error(`Market creation error:`); } }, [writeError, marketError]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + writeDebate({ address: DEBATE_FACTORY_ADDRESS, abi: DEBATE_FACTORY_ABI, - functionName: 'createDebate', - args: [topic, BigInt(Number(duration) * 24 * 60 * 60), BigInt(rounds), [judgeAI]], + functionName: "createDebate", + args: [ + topic, + BigInt(Number(duration) * 24 * 60 * 60), + BigInt(ROUNDS), + [JUDGEAI], + ], }); }; - // Create market when debate is created useEffect(() => { - console.log('Effect triggered:', { - isDebateSuccess, - hasReceipt: !!receipt, - hasLogs: !!receipt?.logs, - judgeAI, - writeMarket: !!writeMarket - }); - if (isDebateSuccess && receipt?.logs && writeMarket) { + console.log("hit"); try { - // Find DebateCreated event const log = receipt.logs.find((log: Log) => { try { const event = decodeEventLog({ abi: DEBATE_FACTORY_ABI, ...log, }); - console.log('Decoded log event:', event); - return event.eventName === 'DebateCreated'; + console.log("event", event); + return event.eventName === "DebateCreated"; } catch (error) { - console.log('Error decoding log:', error); return false; } }); - if (!log) { - console.log('DebateCreated event not found in logs'); - return; - } + if (!log) return; const decodedEvent = decodeEventLog({ abi: DEBATE_FACTORY_ABI, ...log, }); - console.log('Final decoded event:', decodedEvent); - - if (decodedEvent.eventName === 'DebateCreated' && - decodedEvent.args && - 'debateId' in decodedEvent.args) { + if ( + decodedEvent.eventName === "DebateCreated" && + decodedEvent.args && + "debateId" in decodedEvent.args + ) { writeMarket({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'createMarket', + functionName: "createMarket", args: [ MOCK_TOKEN_ADDRESS as `0x${string}`, decodedEvent.args.debateId, - judgeAI as `0x${string}`, + JUDGEAI as `0x${string}`, DEFAULT_BONDING_TARGET, DEFAULT_BONDING_DURATION, - DEFAULT_BASE_PRICE - ] as const + DEFAULT_BASE_PRICE, + ] as const, }); } } catch (error) { - console.error('Error creating market:', error); + console.error("Error creating market:", error); } } - }, [isDebateSuccess, receipt, writeMarket, judgeAI]); + }, [isDebateSuccess, receipt, writeMarket, JUDGEAI]); - const isPending = isDebatePending || isMarketPending || isConfirmingDebate || isConfirmingMarket; + useEffect(() => { + if (isDebateSuccess) { + toast.success("Debate successfully created!"); + } + if (isMarketSuccess) { + toast.success("Market successfully created!"); + } + }, [isDebateSuccess, isMarketSuccess]); + + const isPending = + isDebatePending || + isMarketPending || + isConfirmingDebate || + isConfirmingMarket; return ( - <> - - - Create a New Debate - Set up a new debate topic and define its parameters +
+ + + +
+
+ CREATE DEBATE +
+
+ Initiate a new battle of minds in the arena +
+
-
- +
+ setTopic(e.target.value)} placeholder="Enter debate topic" required - className="bg-[#1C2128] border-white/10 text-white placeholder:text-gray-500" + className="bg-[#483535] border-[#D1BB9E]/20 text-[#f0ecec] placeholder:text-[#D1BB9E]/50 focus:border-[#cfcece] " />
-
- +
+ setDuration(e.target.value)} min="1" required - className="bg-[#1C2128] border-white/10 text-white" - /> -
-
- - setRounds(e.target.value)} - min="1" - required - className="bg-[#1C2128] border-white/10 text-white" + className="bg-[#483535] border-[#D1BB9E]/20 text-[#f0ecec] placeholder:text-[#D1BB9E]/50 focus:border-[#cfcece] " />
-
- - setJudgeAI(e.target.value)} - placeholder="Enter Judge AI address" - required - className="bg-[#1C2128] border-white/10 text-white placeholder:text-gray-500" - /> + +
+ {!isConnected ? ( +
+ Please connect your wallet to create a debate +
+ ) : ( +
+ {/* Background image */} +
+ {"Background"} +
+ + {/* Transparent button */} + +
+ )}
- {!isConnected ? ( -
- Please connect your wallet to create a debate -
- ) : ( - - )} - {}}> - + +
-
-

Transaction Being Processed

-

- {isDebatePending || isConfirmingDebate - ? 'Creating debate...' - : 'Creating market...'} +

+

+ Transaction Being Processed +

+

+ {isDebatePending || isConfirmingDebate + ? "Creating debate..." + : "Creating market..."}

- +
); -} \ No newline at end of file +} diff --git a/frontend/debate-ai/components/CreateGladiatorForm.tsx b/frontend/debate-ai/components/CreateGladiatorForm.tsx index c3aa794..f8b622c 100644 --- a/frontend/debate-ai/components/CreateGladiatorForm.tsx +++ b/frontend/debate-ai/components/CreateGladiatorForm.tsx @@ -1,108 +1,151 @@ -'use client' - -import { useState, useTransition } from "react" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Loader2, Swords } from 'lucide-react' -import Image from "next/image" -import { useAccount, useWriteContract, useWaitForTransactionReceipt } from 'wagmi' -import { GLADIATOR_NFT_ADDRESS, GLADIATOR_NFT_ABI } from '@/config/contracts' -import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from '@/config/contracts' +"use client"; + +import { useState, useTransition } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Loader2, Swords } from "lucide-react"; +import Image from "next/image"; +import { + useAccount, + useWriteContract, + useWaitForTransactionReceipt, +} from "wagmi"; +import { GLADIATOR_NFT_ADDRESS, GLADIATOR_NFT_ABI } from "@/config/contracts"; +import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from "@/config/contracts"; interface GeneratedGladiator { - name: string - image: string - description: string - speciality: string + name: string; + image: string; + description: string; + speciality: string; stats: { - strength: number - agility: number - intelligence: number - } - ipfsUrl?: string + strength: number; + agility: number; + intelligence: number; + }; + ipfsUrl?: string; } export function CreateGladiatorForm() { - const [gladiator, setGladiator] = useState(null) - const [error, setError] = useState(null) - const [isPending, startTransition] = useTransition() - const [showMintingModal, setShowMintingModal] = useState(false) - const [mintError, setMintError] = useState(null) + const [gladiator, setGladiator] = useState(null); + const [error, setError] = useState(null); + const [isPending, startTransition] = useTransition(); + const [showMintingModal, setShowMintingModal] = useState(false); + const [mintError, setMintError] = useState(null); // Wagmi hooks for minting - const { address, isConnected } = useAccount() - const { writeContract, isPending: isMintPending, data: txHash } = useWriteContract() - const { isLoading: isMintConfirming, isSuccess: isMintSuccess, error: mintTxError } = useWaitForTransactionReceipt({ + const { address, isConnected } = useAccount(); + const { + writeContract, + isPending: isMintPending, + data: txHash, + } = useWriteContract(); + const { + isLoading: isMintConfirming, + isSuccess: isMintSuccess, + error: mintTxError, + } = useWaitForTransactionReceipt({ hash: txHash, - }) + }); const coordinatorUrl = process.env.NEXT_PUBLIC_COORDINATOR_URL; async function handleSubmit(formData: FormData) { - const twitterHandle = formData.get('twitter')?.toString().replace('@', '') // Remove @ if present - - if (!twitterHandle) return + const twitterHandle = formData.get("twitter")?.toString().replace("@", ""); // Remove @ if present + + if (!twitterHandle) return; startTransition(async () => { try { // Call the character generation endpoint - const response = await fetch(`${coordinatorUrl}/api/character/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username: twitterHandle }), - }) - + const response = await fetch( + `${coordinatorUrl}/api/character/generate`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: twitterHandle }), + } + ); + if (!response.ok) { - throw new Error('Failed to generate gladiator') + throw new Error("Failed to generate gladiator"); + } + + const characterData = await response.json(); + + // Add validation + if (!characterData?.data?.name) { + throw new Error("Invalid character data structure"); } - - const characterData = await response.json() - + // Transform the character data into the GeneratedGladiator format const gladiatorData: GeneratedGladiator = { - name: characterData.data.name, - image: process.env.NEXT_PUBLIC_DEFAULT_GLADIATOR_IMAGE || '/placeholder-gladiator.png', - description: characterData.data.bio.join(' '), - speciality: characterData.data.topics[0] || 'General Philosophy', + name: characterData.data.name || "Unknown Gladiator", + image: + process.env.NEXT_PUBLIC_DEFAULT_GLADIATOR_IMAGE || + "/placeholder-gladiator.png", + description: + characterData.data.bio?.join(" ") || "No description available", + speciality: characterData.data.topics?.[0] || "General Philosophy", stats: { - strength: Math.min(100, (characterData.data.topics.length * 20)), - agility: Math.min(100, (characterData.data.postExamples.length * 5)), - intelligence: Math.min(100, (characterData.data.adjectives.length * 25)) + strength: Math.min( + 100, + (characterData.data.topics?.length || 0) * 20 + ), + agility: Math.min( + 100, + (characterData.data.postExamples?.length || 0) * 5 + ), + intelligence: Math.min( + 100, + (characterData.data.adjectives?.length || 0) * 25 + ), }, - ipfsUrl: characterData.data.ipfsUrl - } - - setGladiator(gladiatorData) - setError(null) + ipfsUrl: characterData.data.ipfsUrl, + }; + + setGladiator(gladiatorData); + setError(null); } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to generate gladiator') + setError( + e instanceof Error ? e.message : "Failed to generate gladiator" + ); } - }) + }); } const handleMint = async () => { + console.log("gladiator", gladiator); + console.log("address", address); + console.log("isConnected", isConnected); if (!gladiator || !address || !isConnected) return; try { - setShowMintingModal(true) - setMintError(null) + setShowMintingModal(true); + setMintError(null); const publicKey = Math.floor(Math.random() * 1000000).toString(16); await writeContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'registerGladiator', + functionName: "registerGladiator", args: [ gladiator.name, - gladiator.ipfsUrl || '', // Use IPFS URL instead of raw data - publicKey + gladiator.ipfsUrl || "", // Use IPFS URL instead of raw data + publicKey, ], - }) + }); } catch (e) { - setMintError(e instanceof Error ? e.message : 'Failed to mint NFT') + setMintError(e instanceof Error ? e.message : "Failed to mint NFT"); } - } + }; return (
@@ -128,9 +171,7 @@ export function CreateGladiatorForm() { - {error && ( -

{error}

- )} + {error &&

{error}

} {gladiator && (
@@ -147,7 +188,9 @@ export function CreateGladiatorForm() {

{gladiator.name}

-

{gladiator.description}

+

+ {gladiator.description} +

Speciality

@@ -159,7 +202,7 @@ export function CreateGladiatorForm() {
Strength
-
@@ -168,7 +211,7 @@ export function CreateGladiatorForm() {
Agility
-
@@ -177,7 +220,7 @@ export function CreateGladiatorForm() {
Intelligence
-
@@ -188,8 +231,8 @@ export function CreateGladiatorForm() {
- @@ -222,6 +265,5 @@ export function CreateGladiatorForm() {
)}
- ) + ); } - diff --git a/frontend/debate-ai/components/DebateView.tsx b/frontend/debate-ai/components/DebateView.tsx index 98258c1..8b4da21 100644 --- a/frontend/debate-ai/components/DebateView.tsx +++ b/frontend/debate-ai/components/DebateView.tsx @@ -1,19 +1,42 @@ -'use client'; - -import { Card, CardContent } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { DEBATE_FACTORY_ADDRESS, DEBATE_FACTORY_ABI, MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from '@/config/contracts'; -import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt, useClient } from 'wagmi'; -import { formatEther, formatAddress } from '@/lib/utils'; -import { useState, useEffect } from 'react'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Input } from '@/components/ui/input'; -import { waitForTransactionReceipt } from 'viem/actions'; -import { config } from '@/config/wallet-config'; -import { ChevronDown } from 'lucide-react'; -import { BribeSubmission } from './BribeSubmission'; -import { NominateGladiatorCard } from './NominateGladiatorCard'; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + DEBATE_FACTORY_ADDRESS, + DEBATE_FACTORY_ABI, + MARKET_FACTORY_ADDRESS, + MARKET_FACTORY_ABI, + GLADIATOR_NFT_ABI, + GLADIATOR_NFT_ADDRESS, +} from "@/config/contracts"; +import { + useAccount, + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, + usePublicClient, +} from "wagmi"; +import { formatEther, formatAddress } from "@/lib/utils"; +import { useState, useEffect, useMemo } from "react"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Input } from "@/components/ui/input"; +import { waitForTransactionReceipt } from "viem/actions"; +import { config } from "@/config/wallet-config"; +import { ChevronDown, Menu } from "lucide-react"; +import { BribeSubmission } from "./debate-details/BribeSubmission"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Clock, + Users, + Calendar, + BarChart3, + Trophy, + Activity, + AlertCircle, +} from "lucide-react"; +import Navbar from "./common/Navbar"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import { NominateGladiatorCard } from "./debate-details/NominateGladiatorCard"; interface Message { sender: string; @@ -25,39 +48,40 @@ interface Message { const BASIS_POINTS = 10000n; const MIN_PRICE = 1n; // $0.01 in basis points const MAX_PRICE = 9900n; // $0.99 in basis points -const MIN_ORDER_SIZE = BigInt(10**18); // 1 full token +const MIN_ORDER_SIZE = BigInt(10 ** 18); // 1 full token // Type definitions matching MarketFactory.sol structs type BondingCurveStruct = { - target: bigint; // Target amount to reach - current: bigint; // Current amount raised - basePrice: bigint; // Starting price - currentPrice: bigint; // Current price - isFulfilled: boolean; // Whether target is reached - endTime: bigint; // When bonding period ends + target: bigint; // Target amount to reach + current: bigint; // Current amount raised + basePrice: bigint; // Starting price + currentPrice: bigint; // Current price + isFulfilled: boolean; // Whether target is reached + endTime: bigint; // When bonding period ends }; // Type for how bonding curve is returned from contract type BondingCurve = [ - bigint, // target - bigint, // current - bigint, // basePrice - bigint, // currentPrice - boolean, // isFulfilled - bigint // endTime + bigint, // target + bigint, // current + bigint, // basePrice + bigint, // currentPrice + boolean, // isFulfilled + bigint, // endTime ]; type Gladiator = { - aiAddress: string; // Address of the AI agent - name: string; // Name of the gladiator - index: bigint; // Index in gladiators array - isActive: boolean; // Whether still in competition - publicKey: string; // Public key for encrypted bribes + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional }; type JudgeVerdict = { - scores: bigint[]; // Scores for each gladiator - timestamp: bigint; // When verdict was given + scores: bigint[]; // Scores for each gladiator + timestamp: bigint; // When verdict was given }; type Round = { @@ -68,10 +92,10 @@ type Round = { }; type Order = { - price: bigint; // Price in basis points (100 = 1%) - amount: bigint; // Amount of shares + price: bigint; // Price in basis points (100 = 1%) + amount: bigint; // Amount of shares outcomeIndex: bigint; // Which outcome this order is for - owner: string; // Order creator + owner: string; // Order creator }; type Position = { @@ -88,39 +112,39 @@ type Bribe = { // Return types for contract read functions type MarketDetails = [ - string, // token - bigint, // debateId - boolean, // resolved - bigint, // winningGladiator - BondingCurve,// bondingCurve - bigint // totalBondingAmount + string, // token + bigint, // debateId + boolean, // resolved + bigint, // winningGladiator + BondingCurve, // bondingCurve + bigint, // totalBondingAmount ]; type RoundInfo = [ - bigint, // roundIndex - bigint, // startTime - bigint, // endTime - boolean // isComplete + bigint, // roundIndex + bigint, // startTime + bigint, // endTime + boolean, // isComplete ]; type LeaderboardInfo = [ - bigint[], // totalScores - bigint[] // gladiatorIndexes + bigint[], // totalScores + bigint[], // gladiatorIndexes ]; type DebateDetails = [ - topic: string, // topic - startTime: bigint, // startTime - duration: bigint, // duration - debateEndTime: bigint, // debateEndTime - currentRound: bigint, // currentRound - totalRounds: bigint, // totalRounds - isActive: boolean, // isActive - creator: string, // creator - market: string, // market - judges: string[], // judges - hasOutcome: boolean, // hasOutcome - finalOutcome: bigint // finalOutcome + topic: string, // topic + startTime: bigint, // startTime + duration: bigint, // duration + debateEndTime: bigint, // debateEndTime + currentRound: bigint, // currentRound + totalRounds: bigint, // totalRounds + isActive: boolean, // isActive + creator: string, // creator + market: string, // market + judges: string[], // judges + hasOutcome: boolean, // hasOutcome + finalOutcome: bigint, // finalOutcome ]; // Type for arrays that will be indexed @@ -132,59 +156,52 @@ interface DebateViewProps { debateId: number; } -// Add sorting function after Message interface -function sortMessagesByTimestamp(messages: Message[]): Message[] { - return [...messages].sort((a, b) => { - const timeA = new Date(a.timestamp).getTime(); - const timeB = new Date(b.timestamp).getTime(); - return timeB - timeA; // Sort in descending order (newest first) - }); -} - -export function DebateView({ debateId }: DebateViewProps) { +export function DebateView2({ debateId }: DebateViewProps) { const { isConnected, address } = useAccount(); - const publicClient = useClient({ config }); + const publicClient = usePublicClient(); // Component state const [pendingTx, setPendingTx] = useState(false); - const [orderType, setOrderType] = useState<'buy' | 'sell'>('buy'); - const [selectedGladiator, setSelectedGladiator] = useState(null); - const [amount, setAmount] = useState('0'); - const [potentialReturn, setPotentialReturn] = useState('0.00'); + const [orderType, setOrderType] = useState<"buy" | "sell">("buy"); + const [selectedGladiator, setSelectedGladiator] = useState( + null + ); + const [amount, setAmount] = useState("0"); + const [potentialReturn, setPotentialReturn] = useState("0.00"); const [chatMessages, setChatMessages] = useState([]); // Transaction state - const { + const { data: approveHash, isPending: isApprovePending, - writeContract: approveToken + writeContract: approveToken, } = useWriteContract(); const { data: orderHash, isPending: isOrderPending, - writeContract: placeLimitOrder + writeContract: placeLimitOrder, } = useWriteContract(); - const { isLoading: isApproveConfirming, isSuccess: isApproveConfirmed } = useWaitForTransactionReceipt({ - hash: approveHash, - }); - - const { isLoading: isOrderConfirming, isSuccess: isOrderConfirmed } = useWaitForTransactionReceipt({ - hash: orderHash, - }); + const { isLoading: isApproveConfirming, isSuccess: isApproveConfirmed } = + useWaitForTransactionReceipt({ + hash: approveHash, + }); + const { isLoading: isOrderConfirming, isSuccess: isOrderConfirmed } = + useWaitForTransactionReceipt({ + hash: orderHash, + }); - // Effect for handling order confirmation useEffect(() => { if (isOrderConfirmed) { - console.log('Order confirmed, refreshing data...'); + console.log("Order confirmed, refreshing data..."); // Reset form - setAmount('0'); - setPotentialReturn('0.00'); + setAmount("0"); + setPotentialReturn("0.00"); setSelectedGladiator(null); - + // Refetch all data refetchAllData(); } @@ -196,133 +213,140 @@ export function DebateView({ debateId }: DebateViewProps) { bondingCurve: true, debateInfo: true, gladiators: true, - leaderboard: true + leaderboard: true, }); const toggleCard = (cardName: keyof typeof expandedCards) => { - setExpandedCards(prev => ({ + setExpandedCards((prev) => ({ ...prev, - [cardName]: !prev[cardName] + [cardName]: !prev[cardName], })); }; - /// Get Debate Details - const { data: debateDetails, refetch: refetchDebateDetails } = useReadContract({ - address: DEBATE_FACTORY_ADDRESS, - abi: DEBATE_FACTORY_ABI, - functionName: 'getDebateDetails', - args: [BigInt(debateId)], - }) as { data: DebateDetails | undefined, refetch: () => void }; + const { data: debateDetails, refetch: refetchDebateDetails } = + useReadContract({ + address: DEBATE_FACTORY_ADDRESS, + abi: DEBATE_FACTORY_ABI, + functionName: "getDebateDetails", + args: [BigInt(debateId)], + }) as { data: DebateDetails | undefined; refetch: () => void }; // Get market ID from debate ID const { data: marketId, refetch: refetchMarketId } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'debateIdToMarketId', + functionName: "debateIdToMarketId", args: [BigInt(debateId)], }); // Log market ID changes useEffect(() => { - console.log('[DebateView] marketId changed:', marketId?.toString()); + console.log("[DebateView] marketId changed:", marketId?.toString()); }, [marketId]); // Get market details - const { data: marketDetails, refetch: refetchMarketDetails } = useReadContract({ - address: MARKET_FACTORY_ADDRESS, - abi: MARKET_FACTORY_ABI, - functionName: 'getMarketDetails', - args: marketId ? [marketId] : undefined, - }) as { data: MarketDetails | undefined, refetch: () => void }; + const { data: marketDetails, refetch: refetchMarketDetails } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getMarketDetails", + args: marketId ? [marketId] : undefined, + }) as { data: MarketDetails | undefined; refetch: () => void }; // Get gladiators const { data: gladiators, refetch: refetchGladiators } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getGladiators', + functionName: "getGladiators", args: marketId ? [marketId] : undefined, - }) as { data: Gladiator[] | undefined, refetch: () => void }; + }) as { data: Gladiator[] | undefined; refetch: () => void }; // Get round info const { data: roundInfo, refetch: refetchRoundInfo } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getCurrentRound', + functionName: "getCurrentRound", args: marketId ? [marketId] : undefined, - }) as { data: RoundInfo | undefined, refetch: () => void }; + }) as { data: RoundInfo | undefined; refetch: () => void }; // Get leaderboard const { data: leaderboard, refetch: refetchLeaderboard } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getLeaderboard', + functionName: "getLeaderboard", args: marketId ? [marketId] : undefined, - }) as { data: LeaderboardInfo | undefined, refetch: () => void }; + }) as { data: LeaderboardInfo | undefined; refetch: () => void }; // Get market prices const { data: gladiatorPrices, refetch: refetchPrices } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getMarketPrices', + functionName: "getMarketPrices", args: marketId ? [marketId] : undefined, - }) as { data: bigint[] | undefined, refetch: () => void }; + }) as { data: bigint[] | undefined; refetch: () => void }; // Get market volumes const { data: gladiatorVolumes, refetch: refetchVolumes } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getMarketVolumes', + functionName: "getMarketVolumes", args: marketId ? [marketId] : undefined, - }) as { data: bigint[] | undefined, refetch: () => void }; + }) as { data: bigint[] | undefined; refetch: () => void }; // Get total volume const { data: totalVolume, refetch: refetchTotalVolume } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getTotalVolume', + functionName: "getTotalVolume", args: marketId ? [marketId] : undefined, - }) as { data: bigint | undefined, refetch: () => void }; + }) as { data: bigint | undefined; refetch: () => void }; // Get bonding curve details - const { data: bondingCurveDetails, refetch: refetchBondingCurve } = useReadContract({ - address: MARKET_FACTORY_ADDRESS, - abi: MARKET_FACTORY_ABI, - functionName: 'getBondingCurveDetails', - args: marketId ? [marketId] : undefined, - }) as { data: BondingCurve | undefined, refetch: () => void }; + const { data: bondingCurveDetails, refetch: refetchBondingCurve } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getBondingCurveDetails", + args: marketId ? [marketId] : undefined, + }) as { data: BondingCurve | undefined; refetch: () => void }; // Get user positions if connected const { data: userPositions } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getUserPositions', + functionName: "getUserPositions", args: marketId && address ? [marketId, address] : undefined, }) as { data: bigint[] | undefined }; - // Add allowance check const { data: currentAllowance } = useReadContract({ address: marketDetails?.[0] as `0x${string}`, - abi: [{ - name: 'allowance', - type: 'function', - stateMutability: 'view', - inputs: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' } - ], - outputs: [{ type: 'uint256' }] - }], - functionName: 'allowance', - args: address && marketDetails ? [address, MARKET_FACTORY_ADDRESS] : undefined, + abi: [ + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ type: "uint256" }], + }, + ], + functionName: "allowance", + args: + address && marketDetails ? [address, MARKET_FACTORY_ADDRESS] : undefined, }); - // Add state for chat messages + // Add state for chat messages // Effect to fetch and subscribe to chat messages useEffect(() => { - console.log("[DebateView] Chat effect triggered with marketId:", marketId?.toString()); + console.log( + "[DebateView] Chat effect triggered with marketId:", + marketId?.toString() + ); if (!marketId) { console.log("[DebateView] No valid marketId yet"); return; @@ -336,31 +360,40 @@ export function DebateView({ debateId }: DebateViewProps) { const fetchMessages = async () => { try { - console.log("[DebateView] Fetching messages for market:", marketIdBigInt.toString()); + console.log( + "[DebateView] Fetching messages for market:", + marketIdBigInt.toString() + ); const response = await fetch(`/api/chat/${marketIdBigInt}`, { - method: 'GET', + method: "GET", headers: { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', - 'X-Request-Time': Date.now().toString() + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + // Add a timestamp to force unique requests + "X-Request-Time": Date.now().toString(), }, - cache: 'no-store', - next: { revalidate: 0 } + // Disable caching at fetch level + cache: "no-store", + next: { revalidate: 0 }, }); if (!response.ok) { - console.error("[DebateView] HTTP error from API:", response.status, response.statusText); + console.error( + "[DebateView] HTTP error from API:", + response.status, + response.statusText + ); throw new Error(`HTTP error! status: ${response.status}`); } const messages = await response.json(); - if ('error' in messages) { + if ("error" in messages) { console.error("[DebateView] API returned error:", messages.error); return; } console.log("[DebateView] Received messages:", messages); - setChatMessages(sortMessagesByTimestamp(messages)); + setChatMessages(messages); } catch (error) { - console.error('[DebateView] Error fetching chat messages:', error); + console.error("[DebateView] Error fetching chat messages:", error); } }; @@ -371,11 +404,14 @@ export function DebateView({ debateId }: DebateViewProps) { return; } - console.log("[DebateView] Setting up WebSocket for market:", marketIdBigInt.toString()); + console.log( + "[DebateView] Setting up WebSocket for market:", + marketIdBigInt.toString() + ); const wsUrl = `${process.env.NEXT_PUBLIC_WS_URL}/chat/${marketIdBigInt}`; console.log("[DebateView] WebSocket URL:", wsUrl); ws = new WebSocket(wsUrl); - + ws.onopen = () => { console.log("[DebateView] WebSocket connected"); isWsConnected = true; @@ -387,7 +423,7 @@ export function DebateView({ debateId }: DebateViewProps) { console.error("[DebateView] WebSocket error:", error); isWsConnected = false; }; - + ws.onclose = () => { console.log("[DebateView] WebSocket closed"); isWsConnected = false; @@ -399,12 +435,12 @@ export function DebateView({ debateId }: DebateViewProps) { } }, 3000); }; - + ws.onmessage = (event) => { console.log("[DebateView] Received WebSocket message:", event.data); try { const message = JSON.parse(event.data); - setChatMessages(prev => sortMessagesByTimestamp([message, ...prev])); + setChatMessages((prev) => [...prev, message]); } catch (error) { console.error("[DebateView] Error parsing WebSocket message:", error); } @@ -435,7 +471,7 @@ export function DebateView({ debateId }: DebateViewProps) { refetchVolumes(), refetchTotalVolume(), refetchBondingCurve(), - refetchDebateDetails() + refetchDebateDetails(), ]); }; @@ -452,68 +488,80 @@ export function DebateView({ debateId }: DebateViewProps) { market, judges, hasOutcome, - finalOutcome + finalOutcome, ] = debateDetails || []; // Loading check for market data - const isMarketDataLoading = !marketDetails || !gladiators || !gladiatorPrices || !bondingCurveDetails || !debateDetails; + const isMarketDataLoading = + !marketDetails || + !gladiators || + !gladiatorPrices || + !bondingCurveDetails || + !debateDetails; // Format total volume const totalVolumeFormatted = formatEther(totalVolume || 0n); // Calculate end date - const endDate = debateEndTime ? new Date(Number(debateEndTime) * 1000) : new Date(); + const endDate = debateEndTime + ? new Date(Number(debateEndTime) * 1000) + : new Date(); // Format bonding curve data - const bondingCurve = bondingCurveDetails ? { - target: bondingCurveDetails[0], - current: bondingCurveDetails[1], - basePrice: bondingCurveDetails[2], - currentPrice: bondingCurveDetails[3], - isFulfilled: bondingCurveDetails[4], - endTime: bondingCurveDetails[5] - } : null; + const bondingCurve = bondingCurveDetails + ? { + target: bondingCurveDetails[0], + current: bondingCurveDetails[1], + basePrice: bondingCurveDetails[2], + currentPrice: bondingCurveDetails[3], + isFulfilled: bondingCurveDetails[4], + endTime: bondingCurveDetails[5], + } + : null; // Extract round info - const [roundIndex, roundStartTime, roundEndTime, isRoundComplete] = roundInfo || [0n, 0n, 0n, false]; + const [roundIndex, roundStartTime, roundEndTime, isRoundComplete] = + roundInfo || [0n, 0n, 0n, false]; // Calculate time remaining - const timeRemaining = bondingCurve ? Math.max(0, Number(bondingCurve.endTime) - Math.floor(Date.now() / 1000)) : 0; + const timeRemaining = bondingCurve + ? Math.max(0, Number(bondingCurve.endTime) - Math.floor(Date.now() / 1000)) + : 0; const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60)); const hoursRemaining = Math.floor((timeRemaining % (24 * 60 * 60)) / 3600); - - const handleApproveToken = async (amountInWei: bigint) => { if (!marketDetails || !address) return false; - + try { // Check if we already have sufficient allowance if (currentAllowance && currentAllowance >= amountInWei) { - console.log('Already approved sufficient amount'); + console.log("Already approved sufficient amount"); return true; } // If not, proceed with approval approveToken({ address: marketDetails[0] as `0x${string}`, - abi: [{ - name: 'approve', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'spender', type: 'address' }, - { name: 'amount', type: 'uint256' } - ], - outputs: [{ type: 'bool' }] - }], - functionName: 'approve', - args: [MARKET_FACTORY_ADDRESS, amountInWei] + abi: [ + { + name: "approve", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ type: "bool" }], + }, + ], + functionName: "approve", + args: [MARKET_FACTORY_ADDRESS, amountInWei], }); // Wait for approval hash while (!approveHash) { - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } // Wait for approval confirmation @@ -524,54 +572,61 @@ export function DebateView({ debateId }: DebateViewProps) { return true; } } catch (error) { - console.error('Error in approval:', error); + console.error("Error in approval:", error); return false; } return false; }; - - - - const handlePlaceLimitOrder = async (outcomeIndex: bigint, isLong: boolean) => { + console.log("Bonding curve", bondingCurve); + const handlePlaceLimitOrder = async ( + outcomeIndex: bigint, + isLong: boolean + ) => { if (!marketId || !bondingCurve) { - console.error('Market data not loaded'); + console.error("Market data not loaded"); return; } try { setPendingTx(true); console.log("Creating order for", amount, "of", isLong ? "Yes" : "No"); - const amountInWei = BigInt(Math.floor(parseFloat(amount) * 10**18)); - + const amountInWei = BigInt(Math.floor(parseFloat(amount) * 10 ** 18)); + // First approve const approved = await handleApproveToken(amountInWei); if (!approved) { - console.error('Approval failed'); + console.error("Approval failed"); setPendingTx(false); return; } // Then place order - const price = isLong ? bondingCurve.basePrice : (10000n - bondingCurve.basePrice); - console.log('Base price:', bondingCurve.basePrice.toString()); - console.log('Calculated price:', price.toString()); - - console.log('Placing order with params:', { - marketId: marketId.toString(), - outcomeIndex: outcomeIndex.toString(), - price: price.toString(), - amountInWei: amountInWei.toString() + const price = isLong + ? bondingCurve.basePrice + : 10000n - bondingCurve.basePrice; + if (!marketId) { + console.error("Market ID is required"); + return; + } + // Ensure marketId is properly converted to bigint + const marketIdBigInt = BigInt(marketId as string); + + // Safe logging with null checks + console.log("Placing order with params:", { + marketId: marketIdBigInt, + outcomeIndex: outcomeIndex, + price: price, + amountInWei: amountInWei, }); await placeLimitOrder({ address: MARKET_FACTORY_ADDRESS as `0x${string}`, abi: MARKET_FACTORY_ABI, - functionName: 'placeLimitOrder', - args: [marketId, outcomeIndex, price, amountInWei] + functionName: "placeLimitOrder", + args: [marketIdBigInt, outcomeIndex, price, amountInWei], }); - } catch (error) { - console.error('Error placing order:', error); + console.error("Error placing order:", error); } finally { setPendingTx(false); } @@ -582,10 +637,12 @@ export function DebateView({ debateId }: DebateViewProps) { setAmount(value); // Calculate potential return based on current price if (selectedGladiator && gladiatorPrices) { - const price = Number(gladiatorPrices[Number(selectedGladiator.index)]) / 100; - const return_value = orderType === 'buy' - ? numValue * (100 / price - 1) - : numValue * (100 / (100 - price) - 1); + const price = + Number(gladiatorPrices[Number(selectedGladiator.index)]) / 100; + const return_value = + orderType === "buy" + ? numValue * (100 / price - 1) + : numValue * (100 / (100 - price) - 1); setPotentialReturn(return_value.toFixed(2)); } }; @@ -596,441 +653,1087 @@ export function DebateView({ debateId }: DebateViewProps) { handleAmountChange(newAmount.toString()); }; - console.log("marketDetails", marketDetails); - console.log("gladiators", gladiators); - console.log("gladiatorPrices", gladiatorPrices); - console.log("bondingCurveDetails", bondingCurveDetails); - console.log("debateDetails", debateDetails); + // Add new state for nomination modal + const [isNominationModalOpen, setIsNominationModalOpen] = useState(false); + + // Add state for user's gladiators + const [userGladiators, setUserGladiators] = useState([]); + + // Get all gladiators + const { data: allGladiators, refetch: refetchAllGladiators } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getAllGladiators", + }) as { data: Gladiator[] | undefined; refetch: () => void }; + + // Add contract write for nomination + const { + data: nominateHash, + isPending: isNominatePending, + writeContract: nominateGladiator, + } = useWriteContract(); + + // Add hook for checking ownership + const { data: ownershipData } = useReadContract({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI, + functionName: "ownerOf", + args: selectedGladiator?.tokenId + ? [BigInt(selectedGladiator.tokenId)] + : undefined, + }); + + // Effect to filter user's gladiators + useEffect(() => { + if (allGladiators && address && publicClient) { + const fetchUserGladiators = async () => { + try { + const userOwnedGladiators = await Promise.all( + allGladiators.map(async (gladiator, index) => { + try { + if (!gladiator) return null; + + // Use multicall to batch ownership checks + const ownerData = await publicClient.readContract({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI, + functionName: "ownerOf", + args: [BigInt(index + 1)], // tokenIds start from 1 + }); + + const owner = ownerData as string; + + if (owner && owner.toLowerCase() === address.toLowerCase()) { + const tokenId = index + 1; + return { + ...gladiator, + tokenId, + name: gladiator.name || `Gladiator #${tokenId}`, + } as Gladiator; + } + } catch (error) { + console.error( + `Error checking ownership for gladiator ${index + 1}:`, + error + ); + } + return null; + }) + ); + + const validGladiators = userOwnedGladiators.filter( + (g): g is Gladiator => + g !== null && + typeof g === "object" && + "tokenId" in g && + typeof g.tokenId === "number" + ); + + setUserGladiators(validGladiators); + } catch (error) { + console.error("Error fetching user gladiators:", error); + setUserGladiators([]); + } + }; + + fetchUserGladiators(); + } else { + setUserGladiators([]); + } + }, [allGladiators, address, publicClient]); + + const handleNominate = async (tokenId: number | undefined) => { + if (!marketId || !tokenId) return; + + try { + await nominateGladiator({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "nominateGladiator", + args: [BigInt(tokenId), marketId], + }); + setIsNominationModalOpen(false); + } catch (error) { + console.error("Error nominating gladiator:", error); + } + }; + + // Add index mapping + const indexedGladiators = useMemo(() => { + return ( + gladiators?.map((g, index) => ({ ...g, index: BigInt(index) })) || [] + ); + }, [gladiators]); return ( -
- {/* Main content */} -
- {/* AI Discussion */} - -
toggleCard('aiDiscussion')} - > -
AI Agents Discussion
-
- {bondingCurve?.isFulfilled ? ( - <> -
-
- Live -
- - - ) : ( -
- Locked 🔒 -
- )} -
-
- {expandedCards.aiDiscussion && ( - - {bondingCurve?.isFulfilled ? ( -
- {chatMessages.map((message, index) => ( -
-
- {message.sender.charAt(0)} -
-
-
-
{message.sender}
-

{message.content}

+ <> + {/* */} + +
+ {/* Main content */} + +
+ {/* Debate Info Section */} + {/* Debate Info Section */} +
+ {expandedCards.debateInfo && ( + +
+ {/* Header Section */} +
+
+

+ {topic || "Loading..."} +

+
+
+ + {formatAddress(creator || "")} +
+ +
+ + ${totalVolumeFormatted} + +
+ +
+ + + {currentRound?.toString() || "0"}/ + {totalRounds?.toString() || "0"} +
- ))} -
- ) : ( -
-
-
🤖
-
-
-

AI Agents are waiting to start

-

- Once the bonding curve target is reached, three expert AI agents will begin analyzing and debating this topic in real-time. -

-
- Progress: {bondingCurve ? ((Number(bondingCurve.current) * 100) / Number(bondingCurve.target)).toFixed(1) : '0'}% to unlock + +
+
+
+ {isActive ? "Active" : "Ended"} +
+ + {isActive && ( +
+ + + {daysRemaining}d {hoursRemaining}h remaining + +
+ )}
- )} - - )} - - - {/* Rest of the components */} - {isMarketDataLoading ? ( -
Loading market details...
- ) : ( - <> - {/* Bribe Submission */} - {bondingCurve?.isFulfilled && marketId && ( - + )} +
- {/* Bonding Curve Progress */} - -
toggleCard('bondingCurve')} - > -
Bonding Curve Progress
-
-
- {formatEther(bondingCurve?.current || 0n)}/${formatEther(bondingCurve?.target || 0n)} -
- -
+ {/* AI Discussion */} + +
toggleCard("aiDiscussion")} + > +
+ AI Agents Discussion
- {expandedCards.bondingCurve && ( - -
-
-
Bonding Curve Progress
-
- {formatEther(bondingCurve?.current || 0n)}/{formatEther(bondingCurve?.target || 0n)} -
-
-
-
-
-
-
Current: ${formatEther(bondingCurve?.current || 0n)}
-
Target: ${formatEther(bondingCurve?.target || 0n)}
+
+ {bondingCurve?.isFulfilled ? ( + <> +
+
+ + Live +
+ + + ) : ( +
+ + Locked 🔒 +
- - )} - - - {/* Debate Information */} - -
toggleCard('debateInfo')} - > -
Debate Information
- + )}
- {expandedCards.debateInfo && ( - -
-
-
-

{topic || 'Loading...'}

-
Created by {formatAddress(creator || '')}
-
-
-
- {isActive ? ( - Active - ) : ( - Ended - )} +
+ + {expandedCards.aiDiscussion && ( + + {bondingCurve?.isFulfilled ? ( +
+ {chatMessages.map((message, index) => ( +
+
+ {message.sender.charAt(0)}
- {isActive && ( -
- {daysRemaining}d {hoursRemaining}h remaining +
+
+ {message.sender} +
- )} +
+

+ {message.content} +

+
+
+ ))} +
+ ) : ( +
+
+
🤖
-
-
-
Total Volume
-
${totalVolumeFormatted}
-
-
-
Round
-
{currentRound?.toString() || '0'}/{totalRounds?.toString() || '0'}
-
-
-
End Date
-
{endDate.toLocaleDateString()}
-
-
-
Judges
-
{judges?.length || 0}
+
+

+ AI Agents are waiting to start +

+

+ Once the bonding curve target is reached, three expert + AI agents will begin analyzing and debating this topic + in real-time. +

+
+
+ Progress:{" "} + {bondingCurve + ? ( + (Number(bondingCurve.current) * 100) / + Number(bondingCurve.target) + ).toFixed(1) + : "0"} + % to unlock +
- - )} - + )} + + )} + + + {/* Rest of the components */} + {isMarketDataLoading ? ( +
Loading market details...
+ ) : ( + <> + {/* Bribe Submission */} + {/* {bondingCurve?.isFulfilled && marketId && ( + + )} */} + + {/* Bonding Curve Progress */} + +
+
+ + Bonding Curve + +
+ + ${formatEther(bondingCurve?.current || 0n)} / $ + {formatEther(bondingCurve?.target || 0n)} + +
+
- {/* Gladiator List */} - -
toggleCard('gladiators')} - > -
GLADIATOR
-
-
% CHANCE ↻
- -
-
- {expandedCards.gladiators && ( - -
-
GLADIATOR
-
% CHANCE ↻
+
+
+
-
- {gladiators?.map((gladiator, index) => { - const currentPrice = Number(gladiatorPrices?.[index] || 0n) / Number(BASIS_POINTS); - const volume = gladiatorVolumes ? formatEther(gladiatorVolumes[index]) : '0'; - const totalVolumeFormatted = formatEther(totalVolume || 0n); - const volumePercentage = totalVolumeFormatted !== '0' - ? ((Number(volume) / Number(totalVolumeFormatted)) * 100).toFixed(1) - : '0'; - - // Calculate implied probability based on volume - const impliedProbability = totalVolumeFormatted !== '0' - ? ((Number(volume) / Number(totalVolumeFormatted)) * 100).toFixed(1) - : currentPrice.toFixed(1); - - // Calculate prices based on probability - const yesPrice = (Number(impliedProbability) / 100).toFixed(2); - const noPrice = (1 - Number(impliedProbability) / 100).toFixed(2); - - return ( -
-
-
-
{gladiator.name}
-
- ${volume} Vol. ({volumePercentage}%) -
-
-
-
{impliedProbability}%
-
- Implied Probability -
-
-
-
- -
+
+ + {/* Rows */} +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + const currentPrice = + Number( + gladiatorPrices?.[Number(gladiator.index)] || 0n + ) / Number(BASIS_POINTS); + const volume = gladiatorVolumes + ? formatEther( + gladiatorVolumes[Number(gladiator.index)] + ) + : "0"; + const totalVolumeFormatted = formatEther( + totalVolume || 0n + ); + const volumePercentage = + totalVolumeFormatted !== "0" + ? ( + (Number(volume) / + Number(totalVolumeFormatted)) * + 100 + ).toFixed(1) + : currentPrice.toFixed(1); + + const impliedProbability = + totalVolumeFormatted !== "0" + ? ( + (Number(volume) / + Number(totalVolumeFormatted)) * + 100 + ).toFixed(1) + : currentPrice.toFixed(1); + + const yesPrice = ( + Number(impliedProbability) / 100 + ).toFixed(2); + const noPrice = ( + 1 - + Number(impliedProbability) / 100 + ).toFixed(2); + + return ( +
+
+
+
+ {gladiator.name} +
+
+ +
+
+ ${volume} +
+
+ {volumePercentage}% +
+
+ +
+
+ {impliedProbability}% +
+
+ +
+ + +
+
+
+ ); + })} +
+
+ + )} + + + )} +
+
+ {/* Desktop Sidebar - Visible on large screens */} +
+ + + +
+ {/* Order Type Tabs */} + setOrderType(v as "buy" | "sell")} + className="w-full" + > + + + Buy + + + Sell + + + + + {/* Gladiator Selection */} +
+ +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + + return ( + -
+ ); + })} +
+
+ + {/* Amount Input */} +
+ +
+
+ $
- ); - })} -
- - )} - - - )} -
+ handleAmountChange(e.target.value)} + className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20" + min="0" + step="0.1" + /> +
+ + +
+
+
- {/* Side Panel */} -
- - -
- {/* Order Type Tabs */} - setOrderType(v as 'buy' | 'sell')}> - - Buy - Sell - - - - {/* Gladiator Selection */} -
- -
- {gladiators?.map((gladiator) => ( - - ))} -
-
+ {/* Stats */} +
+
+ Avg price + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + return orderType === "sell" + ? `${impliedProbability.toFixed(2)}¢` + : `${(1 - impliedProbability).toFixed(2)}¢`; + })() + : "-"} + +
+
+ Shares + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted && + parseFloat(amount) + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + const avgPrice = + orderType === "sell" + ? impliedProbability + : 1 - impliedProbability; + return (parseFloat(amount) / avgPrice).toFixed( + 2 + ); + })() + : "0.00"} + +
+
+ Potential return + + ${potentialReturn} ( + {( + (parseFloat(potentialReturn) / + parseFloat(amount || "1")) * + 100 || 0 + ).toFixed(2)} + %) + +
+
- {/* Amount Input */} -
- -
-
- $ -
- handleAmountChange(e.target.value)} - className="pl-6" - min="0" - step="0.1" - /> -
- + {/* Place Order Button */} + +

+ By trading, you agree to the Terms of Use. +

-
-
+ + + +
- {/* Stats */} -
-
- Avg price - {selectedGladiator && gladiatorPrices && gladiatorVolumes && totalVolumeFormatted ? - (() => { - const volume = formatEther(gladiatorVolumes[Number(selectedGladiator.index)]); - const impliedProbability = totalVolumeFormatted !== '0' - ? (Number(volume) / Number(totalVolumeFormatted)) - : Number(gladiatorPrices[Number(selectedGladiator.index)]) / Number(BASIS_POINTS); - return orderType === 'sell' - ? `${impliedProbability.toFixed(2)}¢` - : `${(1 - impliedProbability).toFixed(2)}¢`; - })() - : '-'} -
-
- Shares - {selectedGladiator && gladiatorPrices && gladiatorVolumes && totalVolumeFormatted && parseFloat(amount) ? - (() => { - const volume = formatEther(gladiatorVolumes[Number(selectedGladiator.index)]); - const impliedProbability = totalVolumeFormatted !== '0' - ? (Number(volume) / Number(totalVolumeFormatted)) - : Number(gladiatorPrices[Number(selectedGladiator.index)]) / Number(BASIS_POINTS); - const avgPrice = orderType === 'sell' ? impliedProbability : (1 - impliedProbability); - return (parseFloat(amount) / avgPrice).toFixed(2); - })() - : '0.00'} -
-
- Potential return - ${potentialReturn} ({((parseFloat(potentialReturn) / parseFloat(amount || '1') * 100) || 0).toFixed(2)}%) -
-
+ {/* Mobile/Tablet Drawer - Visible on smaller screens */} +
+ + + + + +
+ + +
+ {/* Order Type Tabs */} + + setOrderType(v as "buy" | "sell") + } + className="w-full" + > + + + Buy + + + Sell + + + + + {/* Gladiator Selection */} +
+ +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + return ( + + ); + })} +
+
- {/* Place Order Button */} - - -

- By trading, you agree to the Terms of Use. -

-
-
- + {/* Amount Input */} +
+ +
+
+ $ +
+ + handleAmountChange(e.target.value) + } + className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20" + min="0" + step="0.1" + /> +
+ + +
+
+
+ + {/* Stats */} +
+
+ Avg price + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + return orderType === "sell" + ? `${impliedProbability.toFixed(2)}¢` + : `${(1 - impliedProbability).toFixed( + 2 + )}¢`; + })() + : "-"} + +
+
+ Shares + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted && + parseFloat(amount) + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + const avgPrice = + orderType === "sell" + ? impliedProbability + : 1 - impliedProbability; + return ( + parseFloat(amount) / avgPrice + ).toFixed(2); + })() + : "0.00"} + +
+
+ Potential return + + ${potentialReturn} ( + {( + (parseFloat(potentialReturn) / + parseFloat(amount || "1")) * + 100 || 0 + ).toFixed(2)} + %) + +
+
+ + {/* Place Order Button */} + +

+ By trading, you agree to the Terms of Use. +

+
+ + +
+ + +
+
{/* Nomination Card */} - -
- {/* Add the confirmation dialog */} - {}}> - -
-
-

Transaction Being Processed

-

- {isApprovePending - ? 'Preparing approval...' - : isApproveConfirming - ? 'Confirming approval...' - : isOrderPending - ? 'Preparing order...' - : isOrderConfirming - ? 'Confirming order...' - : 'Processing...'} -

-
-
-
-
+ {/* Add the confirmation dialog */} + {}} + > + +
+
+

+ Transaction Being Processed +

+

+ {isApprovePending + ? "Preparing approval..." + : isApproveConfirming + ? "Confirming approval..." + : isOrderPending + ? "Preparing order..." + : isOrderConfirming + ? "Confirming order..." + : "Processing..."} +

+
+
+
+ + {/* Nomination Modal */} + + +
+

+ Select a Gladiator to Nominate +

+ + {userGladiators.length === 0 ? ( +
+

+ You don't own any Gladiator NFTs yet. +

+ +
+ ) : ( +
+ {userGladiators.filter(Boolean).map( + (gladiator) => + gladiator && ( +
handleNominate(gladiator.tokenId)} + > +
+
+ 🤖 +
+
+

+ {gladiator.name || "Unnamed Gladiator"} +

+

+ Token #{gladiator.tokenId || "Unknown"} +

+
+
+
+ ) + )} +
+ )} +
+
+
+
+ ); } diff --git a/frontend/debate-ai/components/GladiatorCard.tsx b/frontend/debate-ai/components/GladiatorCard.tsx deleted file mode 100644 index 7aa574d..0000000 --- a/frontend/debate-ai/components/GladiatorCard.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import Image from "next/image" -import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" -import { Badge } from "@/components/ui/badge" -import { Shield, Sword } from 'lucide-react' - -interface GladiatorProps { - id: string - name: string - image: string - wins: number - losses: number - speciality: string - level: number -} - -interface Gladiator { - aiAddress: string; - name: string; - isActive: boolean; - publicKey: string; -} - -export function GladiatorCard({ aiAddress, name, isActive, publicKey }: Gladiator) { - return ( - - - - -
- {name} -
-
- -

{name}

-

Level 1

-
- -
- - 10 -
-
- - 10 -
-
-
-
- - - {name} - -
-
- {name} -
-
-
- Level 1 - Speciality -
-
- Wins: 10 - Losses: 10 -
-

- Win Rate: {((10 / (10 + 10)) * 100).toFixed(1)}% -

-
-
-
-
- ) -} - diff --git a/frontend/debate-ai/components/GladiatorList.tsx b/frontend/debate-ai/components/GladiatorList.tsx index 036010a..640e5a3 100644 --- a/frontend/debate-ai/components/GladiatorList.tsx +++ b/frontend/debate-ai/components/GladiatorList.tsx @@ -1,10 +1,10 @@ -import { useReadContract, useReadContracts } from 'wagmi'; -import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from '@/config/contracts'; -import { Card, CardContent } from './ui/card'; -import { Button } from './ui/button'; -import { formatAddress } from '@/lib/utils'; -import { useRouter } from 'next/navigation'; -import { Abi } from 'viem'; +import { useReadContract, useReadContracts } from "wagmi"; +import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from "@/config/contracts"; +import { Card, CardContent } from "./ui/card"; +import { Button } from "./ui/button"; +import { formatAddress } from "@/lib/utils"; +import { useRouter } from "next/navigation"; +import { Abi } from "viem"; interface Gladiator { aiAddress: string; @@ -21,11 +21,11 @@ export function GladiatorList() { const { data: marketCount } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'marketCount', + functionName: "marketCount", }); // Create an array of market IDs from 0 to marketCount-1 - const marketIds = marketCount + const marketIds = marketCount ? Array.from({ length: Number(marketCount) }, (_, i) => BigInt(i)) : []; @@ -34,9 +34,15 @@ export function GladiatorList() { contracts: marketIds.map((id) => ({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getGladiators', + functionName: "getGladiators", args: [id], - })) as { abi?: Abi | undefined; functionName?: string | undefined; args?: readonly unknown[] | undefined; address?: `0x${string}` | undefined; chainId?: number | undefined }[], + })) as { + abi?: Abi | undefined; + functionName?: string | undefined; + args?: readonly unknown[] | undefined; + address?: `0x${string}` | undefined; + chainId?: number | undefined; + }[], }); // Get market details for each market @@ -44,9 +50,15 @@ export function GladiatorList() { contracts: marketIds.map((id) => ({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getMarketDetails', + functionName: "getMarketDetails", args: [id], - })) as { abi?: Abi | undefined; functionName?: string | undefined; args?: readonly unknown[] | undefined; address?: `0x${string}` | undefined; chainId?: number | undefined }[], + })) as { + abi?: Abi | undefined; + functionName?: string | undefined; + args?: readonly unknown[] | undefined; + address?: `0x${string}` | undefined; + chainId?: number | undefined; + }[], }); const handleGladiatorClick = (marketId: string) => { @@ -57,13 +69,21 @@ export function GladiatorList() {
{gladiatorsPerMarket?.map((result, marketIndex) => { const gladiators = result.result as Gladiator[] | undefined; - const marketDetail = marketDetails?.[marketIndex]?.result as [string, bigint, boolean, bigint, bigint] | undefined; + const marketDetail = marketDetails?.[marketIndex]?.result as + | [string, bigint, boolean, bigint, bigint] + | undefined; if (!gladiators || !marketDetail) return null; - const [token, debateId, resolved, winningGladiator, totalBondingAmount] = marketDetail; + const [ + token, + debateId, + resolved, + winningGladiator, + totalBondingAmount, + ] = marketDetail; return gladiators.map((gladiator) => ( - handleGladiatorClick(marketIndex.toString())} @@ -72,17 +92,24 @@ export function GladiatorList() {

{gladiator.name}

-

Address: {formatAddress(gladiator.aiAddress)}

-

Market ID: #{marketIndex}

- Status: {gladiator.isActive ? ( + Address: {formatAddress(gladiator.aiAddress)} +

+

+ Market ID: #{marketIndex} +

+

+ Status:{" "} + {gladiator.isActive ? ( Active ) : ( Inactive )}

{resolved && winningGladiator === BigInt(gladiator.index) && ( -

Winner!

+

+ Winner! +

)}
); -} \ No newline at end of file +} diff --git a/frontend/debate-ai/components/GladiatorView.tsx b/frontend/debate-ai/components/GladiatorView.tsx index f98ef72..3ca9799 100644 --- a/frontend/debate-ai/components/GladiatorView.tsx +++ b/frontend/debate-ai/components/GladiatorView.tsx @@ -1,10 +1,10 @@ -import { useEffect, useState } from 'react'; -import { useAccount, useReadContract } from 'wagmi'; -import { Card } from './ui/card'; -import { Button } from './ui/button'; -import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from '@/config/contracts'; -import { formatEther } from '@/lib/utils'; -import { BribeSubmission } from './BribeSubmission'; +import { useEffect, useState } from "react"; +import { useAccount, useReadContract } from "wagmi"; +import { Card } from "./ui/card"; +import { Button } from "./ui/button"; +import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from "@/config/contracts"; +import { formatEther } from "@/lib/utils"; +import { BribeSubmission } from "./debate-details/BribeSubmission"; type Gladiator = { aiAddress: string; @@ -40,14 +40,16 @@ interface GladiatorViewProps { } export function GladiatorView({ marketId }: GladiatorViewProps) { - const [selectedGladiator, setSelectedGladiator] = useState(null); + const [selectedGladiator, setSelectedGladiator] = useState( + null + ); const { isConnected } = useAccount(); // Get gladiators const { data: gladiators } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getGladiators', + functionName: "getGladiators", args: [marketId], }); @@ -55,7 +57,7 @@ export function GladiatorView({ marketId }: GladiatorViewProps) { const { data: currentRound } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getCurrentRound', + functionName: "getCurrentRound", args: [marketId], }); @@ -63,23 +65,29 @@ export function GladiatorView({ marketId }: GladiatorViewProps) { const { data: leaderboardData } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getLeaderboard', + functionName: "getLeaderboard", args: [marketId], }); // Format leaderboard data - const leaderboard: LeaderboardEntry[] = leaderboardData - ? (leaderboardData as [bigint[], bigint[]]) [0].map((score: bigint, i: number) => ({ - gladiatorIndex: (leaderboardData as [bigint[], bigint[]])[1][i], - totalScore: score, - gladiator: (gladiators as Gladiator[])?.[Number((leaderboardData as [bigint[], bigint[]])[1][i])] - })) + const leaderboard: LeaderboardEntry[] = leaderboardData + ? (leaderboardData as [bigint[], bigint[]])[0].map( + (score: bigint, i: number) => ({ + gladiatorIndex: (leaderboardData as [bigint[], bigint[]])[1][i], + totalScore: score, + gladiator: (gladiators as Gladiator[])?.[ + Number((leaderboardData as [bigint[], bigint[]])[1][i]) + ], + }) + ) : []; // Calculate time remaining in current round - const timeRemaining = currentRound && (currentRound as { endTime: bigint }).endTime > 0n - ? Number((currentRound as { endTime: bigint }).endTime) - Math.floor(Date.now() / 1000) - : 0; + const timeRemaining = + currentRound && (currentRound as { endTime: bigint }).endTime > 0n + ? Number((currentRound as { endTime: bigint }).endTime) - + Math.floor(Date.now() / 1000) + : 0; return (
@@ -90,7 +98,12 @@ export function GladiatorView({ marketId }: GladiatorViewProps) {

Round {(currentRound as RoundData).roundIndex.toString()}

Time Remaining: {Math.max(0, timeRemaining)} seconds

-

Status: {(currentRound as RoundData).isComplete ? 'Complete' : 'In Progress'}

+

+ Status:{" "} + {(currentRound as RoundData).isComplete + ? "Complete" + : "In Progress"} +

) : (

No active round

@@ -104,7 +117,11 @@ export function GladiatorView({ marketId }: GladiatorViewProps) { {(gladiators as Gladiator[])?.map((gladiator: Gladiator) => (
@@ -133,7 +151,9 @@ export function GladiatorView({ marketId }: GladiatorViewProps) { {index + 1}. {entry.gladiator?.name}
- {entry.totalScore.toString()} pts + + {entry.totalScore.toString()} pts +
))}
@@ -152,4 +172,4 @@ export function GladiatorView({ marketId }: GladiatorViewProps) { )}
); -} \ No newline at end of file +} diff --git a/frontend/debate-ai/components/NominateGladiatorCard.tsx b/frontend/debate-ai/components/NominateGladiatorCard.tsx deleted file mode 100644 index 9a5b36c..0000000 --- a/frontend/debate-ai/components/NominateGladiatorCard.tsx +++ /dev/null @@ -1,122 +0,0 @@ -'use client'; - -import { Card, CardContent } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { useAccount, useReadContract, useReadContracts, useWriteContract, useWaitForTransactionReceipt } from 'wagmi'; -import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI, GLADIATOR_NFT_ADDRESS, GLADIATOR_NFT_ABI } from '@/config/contracts'; -import { formatAddress } from '@/lib/utils'; - -type Gladiator = { - aiAddress: string; - name: string; - index: bigint; - isActive: boolean; - publicKey: string; -}; - -interface NominateGladiatorCardProps { - marketId: bigint | undefined; - isBondingCurveFulfilled: boolean; -} - -export function NominateGladiatorCard({ marketId, isBondingCurveFulfilled }: NominateGladiatorCardProps) { - const { isConnected, address } = useAccount(); - - // Get user's NFTs - const { data: totalSupply } = useReadContract({ - address: GLADIATOR_NFT_ADDRESS, - abi: GLADIATOR_NFT_ABI, - functionName: 'totalSupply', - }); - - const tokenIndices = totalSupply - ? Array.from({ length: Number(totalSupply) }, (_, i) => i) - : []; - - // Get owner of each token - const { data: tokenOwners } = useReadContracts({ - contracts: tokenIndices.map(index => ({ - address: GLADIATOR_NFT_ADDRESS, - abi: GLADIATOR_NFT_ABI as any, - functionName: 'ownerOf', - args: [BigInt(index + 1)], - })), - }); - - // Get gladiator details for owned NFTs - const { data: myGladiators } = useReadContract({ - address: MARKET_FACTORY_ADDRESS, - abi: MARKET_FACTORY_ABI, - functionName: 'getAllGladiators', - }) as { data: Gladiator[] }; - - // Nominate gladiator transaction - const { - data: nominateHash, - isPending: isNominatePending, - writeContract: nominateGladiator - } = useWriteContract(); - - const { isLoading: isNominateConfirming } = useWaitForTransactionReceipt({ - hash: nominateHash, - }); - - // Filter gladiators owned by the current user - const ownedGladiators = myGladiators?.filter((_, index) => { - const ownerResult = tokenOwners?.[index]; - return ownerResult?.result === address; - }) || []; - - const handleNominate = async (tokenId: bigint) => { - if (!marketId) return; - - try { - await nominateGladiator({ - address: MARKET_FACTORY_ADDRESS, - abi: MARKET_FACTORY_ABI, - functionName: 'nominateGladiator', - args: [marketId, tokenId], - }); - } catch (error) { - console.error('Error nominating gladiator:', error); - } - }; - - // Don't show the card if conditions aren't met - if (isBondingCurveFulfilled || !isConnected || ownedGladiators.length === 0) { - return null; - } - - return ( - - -

Nominate Your Gladiator

-

As an NFT holder, you can nominate your gladiator to participate in this debate.

-
- {ownedGladiators.map((gladiator, index) => ( -
-
-

{gladiator.name}

-

{formatAddress(gladiator.aiAddress)}

-
- -
- ))} -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/debate-ai/components/Loader.tsx b/frontend/debate-ai/components/common/Loader.tsx similarity index 91% rename from frontend/debate-ai/components/Loader.tsx rename to frontend/debate-ai/components/common/Loader.tsx index 20ed62e..8e08531 100644 --- a/frontend/debate-ai/components/Loader.tsx +++ b/frontend/debate-ai/components/common/Loader.tsx @@ -1,4 +1,4 @@ -import "../app/globals.css"; +import "../../app/globals.css"; function Loader() { return ( diff --git a/frontend/debate-ai/components/LoaderWrapper.tsx b/frontend/debate-ai/components/common/LoaderWrapper.tsx similarity index 94% rename from frontend/debate-ai/components/LoaderWrapper.tsx rename to frontend/debate-ai/components/common/LoaderWrapper.tsx index a4b387d..8a24b3c 100644 --- a/frontend/debate-ai/components/LoaderWrapper.tsx +++ b/frontend/debate-ai/components/common/LoaderWrapper.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react"; import { usePathname } from "next/navigation"; -import Loader from "@/components/Loader"; +import Loader from "@/components/common/Loader"; export default function LoaderWrapper({ children, diff --git a/frontend/debate-ai/components/MiniChatWindow.tsx b/frontend/debate-ai/components/common/MiniChatWindow.tsx similarity index 95% rename from frontend/debate-ai/components/MiniChatWindow.tsx rename to frontend/debate-ai/components/common/MiniChatWindow.tsx index 3da8246..f06684b 100644 --- a/frontend/debate-ai/components/MiniChatWindow.tsx +++ b/frontend/debate-ai/components/common/MiniChatWindow.tsx @@ -147,7 +147,8 @@ export function ChatWindow({ // Get the host from coordinator URL and use port 3004 const wsHost = new URL(coordinatorUrl).hostname; - const ws = new WebSocket(`ws://${wsHost}:3004/${marketId}`); + console.log(wsHost); + const ws = new WebSocket(`wss://${wsHost}/${marketId}`); wsRef.current = ws; ws.onopen = () => { diff --git a/frontend/debate-ai/components/Navbar.tsx b/frontend/debate-ai/components/common/Navbar.tsx similarity index 53% rename from frontend/debate-ai/components/Navbar.tsx rename to frontend/debate-ai/components/common/Navbar.tsx index b92560d..8b76885 100644 --- a/frontend/debate-ai/components/Navbar.tsx +++ b/frontend/debate-ai/components/common/Navbar.tsx @@ -1,7 +1,16 @@ import { useState, useEffect } from "react"; import navbarImg from "../public/navbar.png"; import { ConnectButton } from "@rainbow-me/rainbowkit"; -import { ChevronDown, ExternalLink, Menu, Wallet } from "lucide-react"; +import { + ChevronDown, + ExternalLink, + Menu, + Wallet, + X, + Swords, + Users, + PlusCircle, +} from "lucide-react"; const Navbar = () => { const [isDrawerOpen, setDrawerOpen] = useState(false); @@ -12,6 +21,22 @@ const Navbar = () => { setDrawerOpen(!isDrawerOpen); }; + const handleGladiatorsClick = () => { + window.location.href = `/gladiators`; + }; + + const handleDebatesClick = () => { + window.location.href = `/create-debate`; + }; + + const handleCreateGladiatorsClick = () => { + window.location.href = `/create-gladiator`; + }; + + const handleHomeClick = () => { + window.location.href = `/`; + }; + useEffect(() => { const controlNavbar = () => { if (typeof window !== "undefined") { @@ -39,18 +64,14 @@ const Navbar = () => { return ( <> -
-
+
+
{/* Logo and Hamburger Container */}
-
+
handleHomeClick()} + > AIgora
{/* Hamburger Menu (Visible on small screens) */} @@ -65,25 +86,28 @@ const Navbar = () => {
{/* Navigation Buttons (Hidden on small screens) */}
- - {/* */} - +
{/* Wallet Connection Button */}
- {/* */} {({ account, @@ -119,7 +143,7 @@ const Navbar = () => { - {/* Account Button - Simplified for mobile */} + {/* Account Button */} + + +
{/* Drawer Content */} -
- - -
diff --git a/frontend/debate-ai/components/common/NavbarWrapper.tsx b/frontend/debate-ai/components/common/NavbarWrapper.tsx new file mode 100644 index 0000000..71463a5 --- /dev/null +++ b/frontend/debate-ai/components/common/NavbarWrapper.tsx @@ -0,0 +1,16 @@ +"use client"; + +import Loader from "@/components/common/Loader"; +import Navbar from "./Navbar"; + +export default function NavbarWrapper({ + children, +}: { + children: React.ReactNode; +}) { + return ( + <> + {children} + + ); +} diff --git a/frontend/debate-ai/components/PixelCard.tsx b/frontend/debate-ai/components/common/PixelCard.tsx similarity index 100% rename from frontend/debate-ai/components/PixelCard.tsx rename to frontend/debate-ai/components/common/PixelCard.tsx diff --git a/frontend/debate-ai/components/debate-details/BondingCurve.tsx b/frontend/debate-ai/components/debate-details/BondingCurve.tsx new file mode 100644 index 0000000..7f32143 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/BondingCurve.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { formatEther, formatAddress } from "@/lib/utils"; +import { Card, CardContent } from "@/components/ui/card"; + +type BondingCurveStruct = { + target: bigint; // Target amount to reach + current: bigint; // Current amount raised + basePrice: bigint; // Starting price + currentPrice: bigint; // Current price + isFulfilled: boolean; // Whether target is reached + endTime: bigint; // When bonding period ends +}; +type ExpandedCardStruct = { + aiDiscussion: boolean; + bondingCurve: boolean; + debateInfo: boolean; + gladiators: boolean; + leaderboard: boolean; +}; + +type BondingCurveProps = { + bondingCurve: BondingCurveStruct | null; + expandedCards: ExpandedCardStruct; +}; + +const BondingCurveComp = ({ + bondingCurve, + expandedCards, +}: BondingCurveProps) => { + return ( +
+ +
+
+ + Bonding Curve + +
+ + ${formatEther(bondingCurve?.current || 0n)} / $ + {formatEther(bondingCurve?.target || 0n)} + +
+
+ +
+
+
+
+
+ + {expandedCards.bondingCurve && ( + +
+
+
+ Current: ${formatEther(bondingCurve?.current || 0n)} +
+
+
+ Target: ${formatEther(bondingCurve?.target || 0n)} +
+
+ + )} + +
+ ); +}; + +export default BondingCurveComp; diff --git a/frontend/debate-ai/components/BribeSubmission.tsx b/frontend/debate-ai/components/debate-details/BribeSubmission.tsx similarity index 65% rename from frontend/debate-ai/components/BribeSubmission.tsx rename to frontend/debate-ai/components/debate-details/BribeSubmission.tsx index a3bdee7..5044a28 100644 --- a/frontend/debate-ai/components/BribeSubmission.tsx +++ b/frontend/debate-ai/components/debate-details/BribeSubmission.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Card } from './ui/card'; -import { useAccount, useWriteContract, useReadContract } from 'wagmi'; -import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from '@/config/contracts'; -import { formatEther } from '@/lib/utils'; +import { useState } from "react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Card } from "../ui/card"; +import { useAccount, useWriteContract, useReadContract } from "wagmi"; +import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI } from "@/config/contracts"; +import { formatEther } from "@/lib/utils"; // Add type definition for bribes type Bribe = { @@ -24,9 +24,16 @@ interface BribeSubmissionProps { onBribeSubmitted?: () => void; } -export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitted }: BribeSubmissionProps) { - const [information, setInformation] = useState(''); - const [selectedGladiator, setSelectedGladiator] = useState(null); +export function BribeSubmission({ + marketId, + roundId, + gladiators, + onBribeSubmitted, +}: BribeSubmissionProps) { + const [information, setInformation] = useState(""); + const [selectedGladiator, setSelectedGladiator] = useState( + null + ); const { isConnected } = useAccount(); const { writeContract: submitBribe, isPending } = useWriteContract(); @@ -35,7 +42,7 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte const { data: bribesData } = useReadContract({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'getBribesForRound', + functionName: "getBribesForRound", args: [marketId, roundId], }); @@ -50,25 +57,28 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte submitBribe({ address: MARKET_FACTORY_ADDRESS, abi: MARKET_FACTORY_ABI, - functionName: 'submitBribe', + functionName: "submitBribe", args: [marketId, roundId, information, selectedGladiator], }); // Clear form after successful submission - setInformation(''); + setInformation(""); setSelectedGladiator(null); - + // Notify parent component if (onBribeSubmitted) { onBribeSubmitted(); } } catch (error) { // Keep error logging for production debugging - console.error('Error submitting bribe:', error); + console.error("Error submitting bribe:", error); } }; - const handleGladiatorSelect = (gladiator: { name: string; index: bigint }) => { + const handleGladiatorSelect = (gladiator: { + name: string; + index: bigint; + }) => { const newSelection = BigInt(gladiator.index.toString()); setSelectedGladiator(newSelection); }; @@ -80,22 +90,30 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte // Check if submit should be enabled const isSubmitDisabled = () => { - return !isConnected || !selectedGladiator || !information.trim() || isPending; + return ( + !isConnected || !selectedGladiator || !information.trim() || isPending + ); }; return (
-

Submit Information with Bribe

- +

+ Submit Information with Bribe +

+
- +
{gladiators.map((gladiator) => (
- + setInformation(e.target.value)} @@ -124,11 +144,11 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte disabled={isSubmitDisabled()} onClick={handleSubmit} > - {!isConnected - ? 'Connect Wallet' - : isPending - ? 'Submitting...' - : 'Submit Bribe (1 Token)'} + {!isConnected + ? "Connect Wallet" + : isPending + ? "Submitting..." + : "Submit Bribe (1 Token)"}
@@ -141,12 +161,23 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte {typedBribesData[0].map((address: string, index: number) => (
- From: {address.slice(0, 6)}...{address.slice(-4)} - {formatEther(typedBribesData[1][index])} tokens + + From: {address.slice(0, 6)}...{address.slice(-4)} + + + {formatEther(typedBribesData[1][index])} tokens +

{typedBribesData[2][index]}

- Supporting: {gladiators.find(g => g.index.toString() === typedBribesData[4][index].toString())?.name} + Supporting:{" "} + { + gladiators.find( + (g) => + g.index.toString() === + typedBribesData[4][index].toString() + )?.name + }
))} @@ -155,4 +186,4 @@ export function BribeSubmission({ marketId, roundId, gladiators, onBribeSubmitte )}
); -} \ No newline at end of file +} diff --git a/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx b/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx new file mode 100644 index 0000000..a5c63d8 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/DebateDiscussion.tsx @@ -0,0 +1,137 @@ +import React from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { ChevronDown, Menu } from "lucide-react"; + +interface Message { + sender: string; + content: string; + timestamp: string; +} + +type BondingCurveStruct = { + target: bigint; // Target amount to reach + current: bigint; // Current amount raised + basePrice: bigint; // Starting price + currentPrice: bigint; // Current price + isFulfilled: boolean; // Whether target is reached + endTime: bigint; // When bonding period ends +}; +type ExpandedCardStruct = { + aiDiscussion: boolean; + bondingCurve: boolean; + debateInfo: boolean; + gladiators: boolean; + leaderboard: boolean; +}; + +type ToggleCardFunction = (cardName: keyof ExpandedCardStruct) => void; + +type DebateDiscussionProps = { + bondingCurve: BondingCurveStruct | null; + expandedCards: ExpandedCardStruct; + toggleCard: ToggleCardFunction; + chatMessages: Message[]; +}; + +export const DebateDiscussion = ({ + bondingCurve, + expandedCards, + toggleCard, + chatMessages, +}: DebateDiscussionProps) => { + return ( +
+ +
toggleCard("aiDiscussion")} + > +
+ AI Agents Discussion +
+
+ {bondingCurve?.isFulfilled ? ( + <> +
+
+ + Live + +
+ + + ) : ( +
+ + Locked 🔒 + +
+ )} +
+
+ + {expandedCards.aiDiscussion && ( + + {bondingCurve?.isFulfilled ? ( +
+ {chatMessages.map((message, index) => ( +
+
+ {message.sender.charAt(0)} +
+
+
+ {message.sender} + +
+
+

+ {message.content} +

+
+
+
+ ))} +
+ ) : ( +
+
+
🤖
+
+
+

+ AI Agents are waiting to start +

+

+ Once the bonding curve target is reached, three expert AI + agents will begin analyzing and debating this topic in + real-time. +

+
+
+ Progress:{" "} + {bondingCurve + ? ( + (Number(bondingCurve.current) * 100) / + Number(bondingCurve.target) + ).toFixed(1) + : "0"} + % to unlock +
+
+
+
+ )} +
+ )} +
+
+ ); +}; diff --git a/frontend/debate-ai/components/debate-details/DebateInfo.tsx b/frontend/debate-ai/components/debate-details/DebateInfo.tsx new file mode 100644 index 0000000..c2f446d --- /dev/null +++ b/frontend/debate-ai/components/debate-details/DebateInfo.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { formatEther, formatAddress } from "@/lib/utils"; +import { Clock, Users, Trophy } from "lucide-react"; + +type ExpandedCardStruct = { + aiDiscussion: boolean; + bondingCurve: boolean; + debateInfo: boolean; + gladiators: boolean; + leaderboard: boolean; +}; + +type DebateDetails = [ + topic: string, // topic + startTime: bigint, // startTime + duration: bigint, // duration + debateEndTime: bigint, // debateEndTime + currentRound: bigint, // currentRound + totalRounds: bigint, // totalRounds + isActive: boolean, // isActive + creator: string, // creator + market: string, // market + judges: string[], // judges + hasOutcome: boolean, // hasOutcome + finalOutcome: bigint, // finalOutcome +]; + +type DebateInfoProps = { + expandedCards: ExpandedCardStruct; + debateDetails: DebateDetails | undefined; + totalVolume: bigint | undefined; + timeRemaining: number; +}; + +function DebateInfo({ + expandedCards, + debateDetails, + totalVolume, + timeRemaining, +}: DebateInfoProps) { + // Format total volume + const totalVolumeFormatted = formatEther(totalVolume || 0n); + + const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60)); + const hoursRemaining = Math.floor((timeRemaining % (24 * 60 * 60)) / 3600); + + let isDummyActive = true; + if (daysRemaining === 0 && hoursRemaining === 0) { + isDummyActive = false; + } + + const [ + topic, + startTime, + duration, + debateEndTime, + currentRound, + totalRounds, + isActive, // TODO: change it later on from contract + creator, + market, + judges, + hasOutcome, + finalOutcome, + ] = debateDetails || []; + + return ( +
+
+ {expandedCards.debateInfo && ( + +
+ {/* Header Section */} +
+
+

+ {topic || "Loading..."} +

+
+
+ + {formatAddress(creator || "")} +
+ +
+ + ${totalVolumeFormatted} + +
+ +
+ + + {currentRound?.toString() || "0"}/ + {totalRounds?.toString() || "0"} + +
+
+
+ +
+
+
+ {isDummyActive ? "Active" : "Ended"} +
+ + {isDummyActive && ( +
+ + + {daysRemaining}d {hoursRemaining}h remaining + +
+ )} +
+
+
+ + )} +
+
+ ); +} + +export default DebateInfo; diff --git a/frontend/debate-ai/components/debate-details/DebateView2.tsx b/frontend/debate-ai/components/debate-details/DebateView2.tsx new file mode 100644 index 0000000..5f705c9 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/DebateView2.tsx @@ -0,0 +1,945 @@ +import { + DEBATE_FACTORY_ADDRESS, + DEBATE_FACTORY_ABI, + MARKET_FACTORY_ADDRESS, + MARKET_FACTORY_ABI, + GLADIATOR_NFT_ABI, + GLADIATOR_NFT_ADDRESS, +} from "@/config/contracts"; +import { + useAccount, + useReadContract, + useWriteContract, + useWaitForTransactionReceipt, + usePublicClient, +} from "wagmi"; + +import { useState, useEffect, useMemo } from "react"; + +import { waitForTransactionReceipt } from "viem/actions"; + +import BondingCurveComp from "./BondingCurve"; +import DebateInfo from "./DebateInfo"; +import { DebateDiscussion } from "./DebateDiscussion"; + +import GladiatorListComp from "./GladiatorList"; +import NominationCard from "./NominationCard"; +import DesktopSideDrawer from "./DesktopSideDrawer"; +import MobileSideDrawer from "./MobileSideDrawer"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Toaster, toast } from "sonner"; + +interface Message { + sender: string; + content: string; + timestamp: string; +} + +// Type for how bonding curve is returned from contract +type BondingCurve = [ + bigint, // target + bigint, // current + bigint, // basePrice + bigint, // currentPrice + boolean, // isFulfilled + bigint, // endTime +]; + +type Gladiator = { + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional +}; + +type JudgeVerdict = { + scores: bigint[]; // Scores for each gladiator + timestamp: bigint; // When verdict was given +}; + +type Round = { + startTime: bigint; + endTime: bigint; + isComplete: boolean; + verdict: JudgeVerdict; +}; + +type Order = { + price: bigint; // Price in basis points (100 = 1%) + amount: bigint; // Amount of shares + outcomeIndex: bigint; // Which outcome this order is for + owner: string; // Order creator +}; + +type Position = { + shares: { [gladiatorIndex: string]: bigint }; // gladiatorIndex => number of shares +}; + +type Bribe = { + briber: string; + amount: bigint; + information: string; + timestamp: bigint; + outcomeIndex: bigint; +}; + +// Return types for contract read functions +type MarketDetails = [ + string, // token + bigint, // debateId + boolean, // resolved + bigint, // winningGladiator + BondingCurve, // bondingCurve + bigint, // totalBondingAmount +]; + +type RoundInfo = [ + bigint, // roundIndex + bigint, // startTime + bigint, // endTime + boolean, // isComplete +]; + +type LeaderboardInfo = [ + bigint[], // totalScores + bigint[], // gladiatorIndexes +]; + +type DebateDetails = [ + topic: string, // topic + startTime: bigint, // startTime + duration: bigint, // duration + debateEndTime: bigint, // debateEndTime + currentRound: bigint, // currentRound + totalRounds: bigint, // totalRounds + isActive: boolean, // isActive + creator: string, // creator + market: string, // market + judges: string[], // judges + hasOutcome: boolean, // hasOutcome + finalOutcome: bigint, // finalOutcome +]; + +interface DebateViewProps { + debateId: number; +} + +export function DebateView2({ debateId }: DebateViewProps) { + const { isConnected, address } = useAccount(); + const publicClient = usePublicClient(); + + // Component state + const [pendingTx, setPendingTx] = useState(false); + const [orderType, setOrderType] = useState<"buy" | "sell">("buy"); + const [selectedGladiator, setSelectedGladiator] = useState( + null + ); + const [amount, setAmount] = useState("0"); + const [potentialReturn, setPotentialReturn] = useState("0.00"); + const [chatMessages, setChatMessages] = useState([]); + + // Transaction state + const { + data: approveHash, + isPending: isApprovePending, + writeContract: approveToken, + } = useWriteContract(); + + const { + data: orderHash, + isPending: isOrderPending, + writeContract: placeLimitOrder, + } = useWriteContract(); + + const { isLoading: isApproveConfirming, isSuccess: isApproveConfirmed } = + useWaitForTransactionReceipt({ + hash: approveHash, + }); + + const { + isLoading: isOrderConfirming, + isSuccess: isOrderConfirmed, + error: errorTxnOrder, + } = useWaitForTransactionReceipt({ + hash: orderHash, + }); + + // Effect for handling order confirmation + useEffect(() => { + if (isOrderConfirmed) { + console.log("Order confirmed, refreshing data..."); + toast.success("Order confirmed!"); + // Reset form + setAmount("0"); + setPotentialReturn("0.00"); + setSelectedGladiator(null); + + // Refetch all data + refetchAllData(); + } + }, [isOrderConfirmed]); + + // Add state for tracking expanded cards + const [expandedCards, setExpandedCards] = useState({ + aiDiscussion: true, + bondingCurve: true, + debateInfo: true, + gladiators: true, + leaderboard: true, + }); + + const toggleCard = (cardName: keyof typeof expandedCards) => { + setExpandedCards((prev) => ({ + ...prev, + [cardName]: !prev[cardName], + })); + }; + + /// Get Debate Details + const { data: debateDetails, refetch: refetchDebateDetails } = + useReadContract({ + address: DEBATE_FACTORY_ADDRESS, + abi: DEBATE_FACTORY_ABI, + functionName: "getDebateDetails", + args: [BigInt(debateId)], + }) as { data: DebateDetails | undefined; refetch: () => void }; + + // Get market ID from debate ID + const { data: marketId, refetch: refetchMarketId } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "debateIdToMarketId", + args: [BigInt(debateId)], + }); + + // Log market ID changes + useEffect(() => { + console.log("[DebateView] marketId changed:", marketId?.toString()); + }, [marketId]); + + // Get market details + const { data: marketDetails, refetch: refetchMarketDetails } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getMarketDetails", + args: marketId ? [marketId] : undefined, + }) as { data: MarketDetails | undefined; refetch: () => void }; + + // Get gladiators + const { data: gladiators, refetch: refetchGladiators } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getGladiators", + args: marketId ? [marketId] : undefined, + }) as { data: Gladiator[] | undefined; refetch: () => void }; + + // Get round info + const { data: roundInfo, refetch: refetchRoundInfo } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getCurrentRound", + args: marketId ? [marketId] : undefined, + }) as { data: RoundInfo | undefined; refetch: () => void }; + + // Get leaderboard + const { data: leaderboard, refetch: refetchLeaderboard } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getLeaderboard", + args: marketId ? [marketId] : undefined, + }) as { data: LeaderboardInfo | undefined; refetch: () => void }; + + // Get market prices + const { data: gladiatorPrices, refetch: refetchPrices } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getMarketPrices", + args: marketId ? [marketId] : undefined, + }) as { data: bigint[] | undefined; refetch: () => void }; + + // Get market volumes + const { data: gladiatorVolumes, refetch: refetchVolumes } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getMarketVolumes", + args: marketId ? [marketId] : undefined, + }) as { data: bigint[] | undefined; refetch: () => void }; + + // Get total volume + const { data: totalVolume, refetch: refetchTotalVolume } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getTotalVolume", + args: marketId ? [marketId] : undefined, + }) as { data: bigint | undefined; refetch: () => void }; + + // Get bonding curve details + const { data: bondingCurveDetails, refetch: refetchBondingCurve } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getBondingCurveDetails", + args: marketId ? [marketId] : undefined, + }) as { data: BondingCurve | undefined; refetch: () => void }; + + // Get user positions if connected + const { data: userPositions } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getUserPositions", + args: marketId && address ? [marketId, address] : undefined, + }) as { data: bigint[] | undefined }; + + // Add allowance check + const { data: currentAllowance } = useReadContract({ + address: marketDetails?.[0] as `0x${string}`, + abi: [ + { + name: "allowance", + type: "function", + stateMutability: "view", + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + outputs: [{ type: "uint256" }], + }, + ], + functionName: "allowance", + args: + address && marketDetails ? [address, MARKET_FACTORY_ADDRESS] : undefined, + }); + + // Add state for chat messages + + // Effect to fetch and subscribe to chat messages + useEffect(() => { + console.log( + "[DebateView] Chat effect triggered with marketId:", + marketId?.toString() + ); + if (!marketId) { + console.log("[DebateView] No valid marketId yet"); + return; + } + + // Convert marketId to bigint to ensure type safety + const marketIdBigInt = BigInt(marketId.toString()); + + let ws: WebSocket; + let isWsConnected = false; + + const fetchMessages = async () => { + try { + console.log( + "[DebateView] Fetching messages for market:", + marketIdBigInt.toString() + ); + const response = await fetch(`/api/chat/${marketIdBigInt}`, { + method: "GET", + headers: { + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + // Add a timestamp to force unique requests + "X-Request-Time": Date.now().toString(), + }, + // Disable caching at fetch level + cache: "no-store", + next: { revalidate: 0 }, + }); + if (!response.ok) { + console.error( + "[DebateView] HTTP error from API:", + response.status, + response.statusText + ); + throw new Error(`HTTP error! status: ${response.status}`); + } + const messages = await response.json(); + if ("error" in messages) { + console.error("[DebateView] API returned error:", messages.error); + return; + } + console.log("[DebateView] Received messages:", messages); + setChatMessages(messages); + } catch (error) { + console.error("[DebateView] Error fetching chat messages:", error); + } + }; + + // Set up WebSocket connection + const setupWebSocket = () => { + if (!process.env.NEXT_PUBLIC_WS_URL) { + console.error("[DebateView] NEXT_PUBLIC_WS_URL not set"); + return; + } + + console.log( + "[DebateView] Setting up WebSocket for market:", + marketIdBigInt.toString() + ); + const wsUrl = `${process.env.NEXT_PUBLIC_WS_URL}/chat/${marketIdBigInt}`; + console.log("[DebateView] WebSocket URL:", wsUrl); + ws = new WebSocket(wsUrl); + + ws.onopen = () => { + console.log("[DebateView] WebSocket connected"); + isWsConnected = true; + // Fetch messages after WebSocket is connected + fetchMessages(); + }; + + ws.onerror = (error) => { + console.error("[DebateView] WebSocket error:", error); + isWsConnected = false; + }; + + ws.onclose = () => { + console.log("[DebateView] WebSocket closed"); + isWsConnected = false; + // Try to reconnect after a delay + setTimeout(() => { + if (!isWsConnected) { + console.log("[DebateView] Attempting to reconnect WebSocket..."); + setupWebSocket(); + } + }, 3000); + }; + + ws.onmessage = (event) => { + console.log("[DebateView] Received WebSocket message:", event.data); + try { + const message = JSON.parse(event.data); + setChatMessages((prev) => [...prev, message]); + } catch (error) { + console.error("[DebateView] Error parsing WebSocket message:", error); + } + }; + }; + + // Start WebSocket connection + setupWebSocket(); + + return () => { + console.log("[DebateView] Cleaning up WebSocket connection"); + isWsConnected = false; + if (ws) { + ws.close(); + } + }; + }, [marketId]); + + // Function to refetch all data + const refetchAllData = async () => { + await Promise.all([ + refetchMarketId(), + refetchMarketDetails(), + refetchGladiators(), + refetchRoundInfo(), + refetchLeaderboard(), + refetchPrices(), + refetchVolumes(), + refetchTotalVolume(), + refetchBondingCurve(), + refetchDebateDetails(), + ]); + }; + + // Extract debate details + const [debateEndTime] = debateDetails || []; + + // Loading check for market data + const isMarketDataLoading = + !marketDetails || + !gladiators || + !gladiatorPrices || + !bondingCurveDetails || + !debateDetails; + + // Calculate end date + const endDate = debateEndTime + ? new Date(Number(debateEndTime) * 1000) + : new Date(); + + // Format bonding curve data + const bondingCurve = bondingCurveDetails + ? { + target: bondingCurveDetails[0], + current: bondingCurveDetails[1], + basePrice: bondingCurveDetails[2], + currentPrice: bondingCurveDetails[3], + isFulfilled: bondingCurveDetails[4], + endTime: bondingCurveDetails[5], + } + : null; + + // Extract round info + const [roundIndex, roundStartTime, roundEndTime, isRoundComplete] = + roundInfo || [0n, 0n, 0n, false]; + + // Calculate time remaining + const timeRemaining = bondingCurve + ? Math.max(0, Number(bondingCurve.endTime) - Math.floor(Date.now() / 1000)) + : 0; + + const handleApproveToken = async (amountInWei: bigint) => { + if (!marketDetails || !address) return false; + + try { + // Check if we already have sufficient allowance + if (currentAllowance && currentAllowance >= amountInWei) { + console.log("Already approved sufficient amount"); + return true; + } + + // If not, proceed with approval + approveToken({ + address: marketDetails[0] as `0x${string}`, + abi: [ + { + name: "approve", + type: "function", + stateMutability: "nonpayable", + inputs: [ + { name: "spender", type: "address" }, + { name: "amount", type: "uint256" }, + ], + outputs: [{ type: "bool" }], + }, + ], + functionName: "approve", + args: [MARKET_FACTORY_ADDRESS, amountInWei], + }); + + // Wait for approval hash + while (!approveHash) { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + // Wait for approval confirmation + if (approveHash && publicClient) { + await waitForTransactionReceipt(publicClient as any, { + hash: approveHash, + }); + return true; + } + } catch (error) { + console.error("Error in approval:", error); + return false; + } + return false; + }; + console.log("Bonding curve", bondingCurve); + const handlePlaceLimitOrder = async ( + outcomeIndex: bigint, + isLong: boolean + ) => { + if (!marketId || !bondingCurve) { + console.error("Market data not loaded"); + return; + } + + try { + setPendingTx(true); + console.log("Creating order for", amount, "of", isLong ? "Yes" : "No"); + const amountInWei = BigInt(Math.floor(parseFloat(amount) * 10 ** 18)); + + // First approve + const approved = await handleApproveToken(amountInWei); + if (!approved) { + console.error("Approval failed"); + setPendingTx(false); + return; + } + + // Then place order + const price = isLong + ? bondingCurve.basePrice + : 10000n - bondingCurve.basePrice; + if (!marketId) { + console.error("Market ID is required"); + return; + } + // Ensure marketId is properly converted to bigint + const marketIdBigInt = BigInt(marketId as string); + + // Safe logging with null checks + console.log("Placing order with params:", { + marketId: marketIdBigInt, + outcomeIndex: outcomeIndex, + price: price, + amountInWei: amountInWei, + }); + + await placeLimitOrder({ + address: MARKET_FACTORY_ADDRESS as `0x${string}`, + abi: MARKET_FACTORY_ABI, + functionName: "placeLimitOrder", + args: [marketIdBigInt, outcomeIndex, price, amountInWei], + }); + } catch (error) { + console.error("Error placing order:", error); + } finally { + setPendingTx(false); + } + }; + + const handleAmountChange = (value: string) => { + const numValue = parseFloat(value) || 0; + setAmount(value); + // Calculate potential return based on current price + if (selectedGladiator && gladiatorPrices) { + const price = + Number(gladiatorPrices[Number(selectedGladiator.index)]) / 100; + const return_value = + orderType === "buy" + ? numValue * (100 / price - 1) + : numValue * (100 / (100 - price) - 1); + setPotentialReturn(return_value.toFixed(2)); + } + }; + + const adjustAmount = (delta: number) => { + const currentAmount = parseFloat(amount) || 0; + const newAmount = Math.max(0, currentAmount + delta); + handleAmountChange(newAmount.toString()); + }; + + // Add new state for nomination modal + const [isNominationModalOpen, setIsNominationModalOpen] = useState(false); + + // Add state for user's gladiators + const [userGladiators, setUserGladiators] = useState([]); + + // Get all gladiators + const { data: allGladiators, refetch: refetchAllGladiators } = + useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getAllGladiators", + }) as { data: Gladiator[] | undefined; refetch: () => void }; + + // Add contract write for nomination + const { + data: nominateHash, + isPending: isNominatePending, + writeContract: nominateGladiator, + } = useWriteContract(); + + const { + isLoading: isNominateConfirming, + isSuccess: isNominateConfirmed, + error: nominateTxError, + } = useWaitForTransactionReceipt({ + hash: nominateHash, + }); + + useEffect(() => { + let toastId: any; + if (isNominateConfirming) { + console.log("Nomination is loading!"); + toastId = toast.loading("Nomination transaction is getting confirmed..", { + id: "nomination-loading", + }); + } + + return () => { + if (toastId) { + toast.dismiss(toastId); + } + }; + }, [isNominateConfirming]); + + useEffect(() => { + if (isNominateConfirmed) { + console.log("Nomination confirmed"); + toast.success("Nomination for your gladiator is confirmed!", { + id: "nomination-success", + }); + } + }, [isNominateConfirmed]); + + useEffect(() => { + if (nominateTxError) { + console.log("Error in Nomination"); + toast.error("Error in Nomination for your gladiator!", { + id: "nomination-error", + }); + } + }, [nominateTxError]); + + useEffect(() => { + if (errorTxnOrder) { + console.log("Error in placing order"); + toast.error("Some error occurred while placing order"); + } + }, [errorTxnOrder]); + + // Add hook for checking ownership + const { data: ownershipData } = useReadContract({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI, + functionName: "ownerOf", + args: selectedGladiator?.tokenId + ? [BigInt(selectedGladiator.tokenId)] + : undefined, + }); + + // Effect to filter user's gladiators + useEffect(() => { + if (allGladiators && address && publicClient) { + const fetchUserGladiators = async () => { + try { + const userOwnedGladiators = await Promise.all( + allGladiators.map(async (gladiator, index) => { + try { + if (!gladiator) return null; + + // Use multicall to batch ownership checks + const ownerData = await publicClient.readContract({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI, + functionName: "ownerOf", + args: [BigInt(index + 1)], // tokenIds start from 1 + }); + + const owner = ownerData as string; + + if (owner && owner.toLowerCase() === address.toLowerCase()) { + const tokenId = index + 1; + return { + ...gladiator, + tokenId, + name: gladiator.name || `Gladiator #${tokenId}`, + } as Gladiator; + } + } catch (error) { + console.error( + `Error checking ownership for gladiator ${index + 1}:`, + error + ); + } + return null; + }) + ); + + const validGladiators = userOwnedGladiators.filter( + (g): g is Gladiator => + g !== null && + typeof g === "object" && + "tokenId" in g && + typeof g.tokenId === "number" + ); + + setUserGladiators(validGladiators); + } catch (error) { + console.error("Error fetching user gladiators:", error); + setUserGladiators([]); + } + }; + + fetchUserGladiators(); + } else { + setUserGladiators([]); + } + }, [allGladiators, address, publicClient]); + + const handleNominate = async (tokenId: number | undefined) => { + if (!marketId || !tokenId) return; + + try { + await nominateGladiator({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "nominateGladiator", + args: [BigInt(tokenId), marketId], + }); + setIsNominationModalOpen(false); + } catch (error) { + console.error("Error nominating gladiator:", error); + } + }; + + // Add index mapping + const indexedGladiators = useMemo(() => { + return ( + gladiators?.map((g, index) => ({ ...g, index: BigInt(index) })) || [] + ); + }, [gladiators]); + + // TODO: replace it with isActive when recieved correct from contract + const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60)); + const hoursRemaining = Math.floor((timeRemaining % (24 * 60 * 60)) / 3600); + + let isDummyActive = true; + if (daysRemaining === 0 && hoursRemaining === 0) { + isDummyActive = false; + } + + return ( + <> +
+
+ + {/* Debate Info Section */} + + + {/* AI Discussion */} + + + {/* Rest of the components */} + {isMarketDataLoading ? ( +
Loading market details...
+ ) : ( + <> + {/* Bribe Submission */} + {/* {bondingCurve?.isFulfilled && marketId && ( + + )} */} + + {/* Bonding Curve Progress */} + + + {/* Gladiator List */} + + + )} +
+ +
+ {/* Desktop Sidebar - Visible on large screens */} + + + + + {/* Mobile/Tablet Drawer - Visible on smaller screens */} +
+ + {/* Nomination Card */} + + + {/* Add the confirmation dialog */} + {}} + > + +
+
+

+ Transaction Being Processed +

+

+ {isApprovePending + ? "Preparing approval..." + : isApproveConfirming + ? "Confirming approval..." + : isOrderPending + ? "Preparing order..." + : isOrderConfirming + ? "Confirming order..." + : "Processing..."} +

+
+
+
+
+ + ); +} diff --git a/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx b/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx new file mode 100644 index 0000000..e48182a --- /dev/null +++ b/frontend/debate-ai/components/debate-details/DesktopSideDrawer.tsx @@ -0,0 +1,301 @@ +import React from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { formatEther, formatAddress } from "@/lib/utils"; + +const BASIS_POINTS = 10000n; +type Gladiator = { + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional +}; + +type DesktopSideDrawerProps = { + setOrderType: React.Dispatch>; + indexedGladiators: Gladiator[]; + selectedGladiator: Gladiator | null; + setSelectedGladiator: React.Dispatch>; + handleAmountChange: (value: string) => void; + amount: string; + adjustAmount: (delta: number) => void; + gladiatorPrices: bigint[] | undefined; + gladiatorVolumes: bigint[] | undefined; + totalVolume: bigint | undefined; + orderType: string; + potentialReturn: string; + isConnected: boolean; + isApproveConfirming: boolean; + isApprovePending: boolean; + isOrderPending: boolean; + isOrderConfirming: boolean; + handlePlaceLimitOrder: ( + outcomeIndex: bigint, + isLong: boolean + ) => Promise; + isActive: boolean; +}; + +const DesktopSideDrawer = ({ + setOrderType, + indexedGladiators, + selectedGladiator, + setSelectedGladiator, + handleAmountChange, + amount, + adjustAmount, + gladiatorPrices, + gladiatorVolumes, + totalVolume, + orderType, + potentialReturn, + isConnected, + isApproveConfirming, + isApprovePending, + isOrderPending, + isOrderConfirming, + handlePlaceLimitOrder, + isActive, +}: DesktopSideDrawerProps) => { + const totalVolumeFormatted = formatEther(totalVolume || 0n); + + return ( +
+
+ + + +
+ {/* Order Type Tabs */} + setOrderType(v as "buy" | "sell")} + className="w-full" + > + + + Buy + + + Sell + + + + + {/* Gladiator Selection */} +
+ +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + + return ( + + ); + })} +
+
+ + {/* Amount Input */} +
+ +
+
+ $ +
+ handleAmountChange(e.target.value)} + className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20" + min="0" + step="0.1" + /> +
+ + +
+
+
+ + {/* Stats */} +
+
+ Avg price + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted + ? (() => { + const volume = formatEther( + gladiatorVolumes[Number(selectedGladiator.index)] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + return orderType === "sell" + ? `${(1 - impliedProbability).toFixed(2)}¢` + : `${impliedProbability.toFixed(2)}¢`; + })() + : "-"} + +
+
+ Shares + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted && + parseFloat(amount) + ? (() => { + const volume = formatEther( + gladiatorVolumes[Number(selectedGladiator.index)] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + const avgPrice = + orderType === "sell" + ? 1 - impliedProbability + : impliedProbability; + return (parseFloat(amount) / avgPrice).toFixed(2); + })() + : "0.00"} + +
+
+ Potential return + + ${potentialReturn} ( + {( + (parseFloat(potentialReturn) / + parseFloat(amount || "1")) * + 100 || 0 + ).toFixed(2)} + %) + +
+
+ + {/* Place Order Button */} + + +

+ By trading, you agree to the Terms of Use. +

+
+
+
+
+
+
+ ); +}; + +export default DesktopSideDrawer; diff --git a/frontend/debate-ai/components/debate-details/GladiatorList.tsx b/frontend/debate-ai/components/debate-details/GladiatorList.tsx new file mode 100644 index 0000000..9ace83e --- /dev/null +++ b/frontend/debate-ai/components/debate-details/GladiatorList.tsx @@ -0,0 +1,192 @@ +import React from "react"; +import { Card, CardContent } from "@/components/ui/card"; +import { ChevronDown, Menu } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { formatEther, formatAddress } from "@/lib/utils"; + +const BASIS_POINTS = 10000n; + +type ExpandedCardStruct = { + aiDiscussion: boolean; + bondingCurve: boolean; + debateInfo: boolean; + gladiators: boolean; + leaderboard: boolean; +}; + +type Gladiator = { + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional +}; + +type GladiatorListProps = { + expandedCards: ExpandedCardStruct; + setIsNominationModalOpen: React.Dispatch>; + isConnected: boolean; + indexedGladiators: Gladiator[]; + gladiatorPrices: bigint[] | undefined; + gladiatorVolumes: bigint[] | undefined; + totalVolume: bigint | undefined; + handleAmountChange: (value: string) => void; + setAmount: React.Dispatch>; + setOrderType: React.Dispatch>; + setSelectedGladiator: React.Dispatch>; +}; + +const GladiatorListComp = ({ + expandedCards, + setIsNominationModalOpen, + isConnected, + indexedGladiators, + gladiatorPrices, + gladiatorVolumes, + totalVolume, + handleAmountChange, + setAmount, + setOrderType, + setSelectedGladiator, +}: GladiatorListProps) => { + return ( +
+ {" "} + + {expandedCards.gladiators && ( + +
+ {/* Header with dividers */} +
+
+
+ Gladiator + +
+
Volume
+
Probability
+
+
+ + {/* Rows */} +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + const currentPrice = + Number(gladiatorPrices?.[Number(gladiator.index)] || 0n) / + Number(BASIS_POINTS); + const volume = gladiatorVolumes + ? formatEther(gladiatorVolumes[Number(gladiator.index)]) + : "0"; + const totalVolumeFormatted = formatEther(totalVolume || 0n); + const volumePercentage = + totalVolumeFormatted !== "0" + ? ( + (Number(volume) / Number(totalVolumeFormatted)) * + 100 + ).toFixed(1) + : currentPrice.toFixed(1); + + const impliedProbability = + totalVolumeFormatted !== "0" + ? ( + (Number(volume) / Number(totalVolumeFormatted)) * + 100 + ).toFixed(1) + : currentPrice.toFixed(1); + + const yesPrice = (Number(impliedProbability) / 100).toFixed( + 2 + ); + const noPrice = ( + 1 - + Number(impliedProbability) / 100 + ).toFixed(2); + + return ( +
+
+
+
+ {gladiator.name} +
+
+ +
+
+ ${volume} +
+
+ {volumePercentage}% +
+
+ +
+
+ {impliedProbability}% +
+
+ +
+ + +
+
+
+ ); + })} +
+
+
+ )} +
+
+ ); +}; + +export default GladiatorListComp; diff --git a/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx b/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx new file mode 100644 index 0000000..cef0444 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/MobileSideDrawer.tsx @@ -0,0 +1,321 @@ +import React from "react"; + +import { Card, CardContent } from "@/components/ui/card"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { formatEther, formatAddress } from "@/lib/utils"; +import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; +import { ChevronDown, Menu } from "lucide-react"; + +const BASIS_POINTS = 10000n; +type Gladiator = { + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional +}; + +type MobileSideDrawerProps = { + setOrderType: React.Dispatch>; + indexedGladiators: Gladiator[]; + selectedGladiator: Gladiator | null; + setSelectedGladiator: React.Dispatch>; + handleAmountChange: (value: string) => void; + amount: string; + adjustAmount: (delta: number) => void; + gladiatorPrices: bigint[] | undefined; + gladiatorVolumes: bigint[] | undefined; + totalVolume: bigint | undefined; + orderType: string; + potentialReturn: string; + isConnected: boolean; + isApproveConfirming: boolean; + isApprovePending: boolean; + isOrderPending: boolean; + isOrderConfirming: boolean; + handlePlaceLimitOrder: ( + outcomeIndex: bigint, + isLong: boolean + ) => Promise; + isActive: boolean; +}; + +const MobileSideDrawer = ({ + setOrderType, + indexedGladiators, + selectedGladiator, + setSelectedGladiator, + handleAmountChange, + amount, + adjustAmount, + gladiatorPrices, + gladiatorVolumes, + totalVolume, + orderType, + potentialReturn, + isConnected, + isApproveConfirming, + isApprovePending, + isOrderPending, + isOrderConfirming, + handlePlaceLimitOrder, + isActive, +}: MobileSideDrawerProps) => { + const totalVolumeFormatted = formatEther(totalVolume || 0n); + return ( +
+ {/*
*/} +
+ + + + + +
+ + +
+ {/* Order Type Tabs */} + setOrderType(v as "buy" | "sell")} + className="w-full" + > + + + Buy + + + Sell + + + + + {/* Gladiator Selection */} +
+ +
+ {indexedGladiators.map((gladiator, index) => { + if (!gladiator) return null; + return ( + + ); + })} +
+
+ + {/* Amount Input */} +
+ +
+
+ $ +
+ handleAmountChange(e.target.value)} + className="pl-6 bg-[#D1BB9E]/50 border-[#D1BB9E]/30 focus:border-[#D1BB9E] text-white focus:ring-[#D1BB9E]/20" + min="0" + step="0.1" + /> +
+ + +
+
+
+ + {/* Stats */} +
+
+ Avg price + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + return orderType === "sell" + ? `${impliedProbability.toFixed(2)}¢` + : `${(1 - impliedProbability).toFixed(2)}¢`; + })() + : "-"} + +
+
+ Shares + + {selectedGladiator && + gladiatorPrices && + gladiatorVolumes && + totalVolumeFormatted && + parseFloat(amount) + ? (() => { + const volume = formatEther( + gladiatorVolumes[ + Number(selectedGladiator.index) + ] + ); + const impliedProbability = + totalVolumeFormatted !== "0" + ? Number(volume) / + Number(totalVolumeFormatted) + : Number( + gladiatorPrices[ + Number(selectedGladiator.index) + ] + ) / Number(BASIS_POINTS); + const avgPrice = + orderType === "sell" + ? impliedProbability + : 1 - impliedProbability; + return (parseFloat(amount) / avgPrice).toFixed( + 2 + ); + })() + : "0.00"} + +
+
+ Potential return + + ${potentialReturn} ( + {( + (parseFloat(potentialReturn) / + parseFloat(amount || "1")) * + 100 || 0 + ).toFixed(2)} + %) + +
+
+ + {/* Place Order Button */} + +

+ By trading, you agree to the Terms of Use. +

+
+
+
+
+
+
+
+
+ ); +}; + +export default MobileSideDrawer; diff --git a/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx b/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx new file mode 100644 index 0000000..c011dc1 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/NominateGladiatorCard.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + useAccount, + useReadContract, + useReadContracts, + useWriteContract, + useWaitForTransactionReceipt, +} from "wagmi"; +import { + MARKET_FACTORY_ADDRESS, + MARKET_FACTORY_ABI, + GLADIATOR_NFT_ADDRESS, + GLADIATOR_NFT_ABI, +} from "@/config/contracts"; +import { formatAddress } from "@/lib/utils"; +import { Sword } from "lucide-react"; + +type Gladiator = { + aiAddress: string; + name: string; + index: bigint; + isActive: boolean; + publicKey: string; +}; + +interface NominateGladiatorCardProps { + marketId: bigint | undefined; + isBondingCurveFulfilled: boolean; +} + +export function NominateGladiatorCard({ + marketId, + isBondingCurveFulfilled, +}: NominateGladiatorCardProps) { + const { isConnected, address } = useAccount(); + + // Get user's NFTs + const { data: totalSupply } = useReadContract({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI, + functionName: "totalSupply", + }); + + const tokenIndices = totalSupply + ? Array.from({ length: Number(totalSupply) }, (_, i) => i) + : []; + + // Get owner of each token + const { data: tokenOwners } = useReadContracts({ + contracts: tokenIndices.map((index) => ({ + address: GLADIATOR_NFT_ADDRESS, + abi: GLADIATOR_NFT_ABI as any, + functionName: "ownerOf", + args: [BigInt(index + 1)], + })), + }); + + // Get gladiator details for owned NFTs + const { data: myGladiators } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getAllGladiators", + }) as { data: Gladiator[] }; + + // Nominate gladiator transaction + const { + data: nominateHash, + isPending: isNominatePending, + writeContract: nominateGladiator, + } = useWriteContract(); + + const { isLoading: isNominateConfirming } = useWaitForTransactionReceipt({ + hash: nominateHash, + }); + + // Filter gladiators owned by the current user + const ownedGladiators = + myGladiators?.filter((_, index) => { + const ownerResult = tokenOwners?.[index]; + return ownerResult?.result === address; + }) || []; + + const handleNominate = async (tokenId: bigint) => { + if (!marketId) return; + + try { + await nominateGladiator({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "nominateGladiator", + args: [marketId, tokenId], + }); + } catch (error) { + console.error("Error nominating gladiator:", error); + } + }; + + // Don't show the card if conditions aren't met + if (isBondingCurveFulfilled || !isConnected || ownedGladiators.length === 0) { + return null; + } + + return ( + + +
+
+ +
+

+ Nominate Your Gladiator +

+
+ +

+ As an NFT holder, you can nominate your gladiator to participate in + this debate. +

+ +
+ {ownedGladiators.map((gladiator, index) => ( +
+
+

{gladiator.name}

+

+ {formatAddress(gladiator.aiAddress)} +

+
+ + +
+ ))} +
+
+
+ ); +} diff --git a/frontend/debate-ai/components/debate-details/NominationCard.tsx b/frontend/debate-ai/components/debate-details/NominationCard.tsx new file mode 100644 index 0000000..303cca0 --- /dev/null +++ b/frontend/debate-ai/components/debate-details/NominationCard.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { NominateGladiatorCard } from "./NominateGladiatorCard"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +type BondingCurveStruct = { + target: bigint; // Target amount to reach + current: bigint; // Current amount raised + basePrice: bigint; // Starting price + currentPrice: bigint; // Current price + isFulfilled: boolean; // Whether target is reached + endTime: bigint; // When bonding period ends +}; + +type Gladiator = { + aiAddress: string; // Address of the AI agent + name: string; // Name of the gladiator + index: bigint; // Index in gladiators array + isActive: boolean; // Whether still in competition + publicKey: string; // Public key for encrypted bribes + tokenId: number; // Token ID of the NFT - making this required instead of optional +}; + +type NominationCardProps = { + bondingCurve: BondingCurveStruct | null; + marketId: unknown; + isApproveConfirming: boolean; + isApprovePending: boolean; + isOrderPending: boolean; + isOrderConfirming: boolean; + isNominationModalOpen: boolean; + setIsNominationModalOpen: React.Dispatch>; + userGladiators: Gladiator[]; + handleNominate: (tokenId: number | undefined) => void; + isActive: boolean; +}; + +const NominationCard = ({ + bondingCurve, + marketId, + isApprovePending, + isOrderPending, + isApproveConfirming, + isOrderConfirming, + isNominationModalOpen, + setIsNominationModalOpen, + userGladiators, + handleNominate, + isActive, +}: NominationCardProps) => { + return ( +
+ + + {/* Nomination Modal */} + + +
+

+ Select a Gladiator to Nominate +

+ + {userGladiators.length === 0 ? ( +
+

+ You don't own any Gladiator NFTs yet. +

+ +
+ ) : ( +
+ {userGladiators.filter(Boolean).map( + (gladiator) => + gladiator && ( +
handleNominate(gladiator.tokenId)} + > +
+
+ 🤖 +
+
+

+ {gladiator.name || "Unnamed Gladiator"} +

+

+ Token #{gladiator.tokenId || "Unknown"} +

+
+
+
+ ) + )} +
+ )} +
+
+
+
+ ); +}; + +export default NominationCard; diff --git a/frontend/debate-ai/components/gladiators-grid.tsx b/frontend/debate-ai/components/gladiators-grid.tsx deleted file mode 100644 index 7d6e0bf..0000000 --- a/frontend/debate-ai/components/gladiators-grid.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Abi } from "viem"; -import { useReadContract } from "wagmi"; -import { MARKET_FACTORY_ABI, MARKET_FACTORY_ADDRESS } from "@/config/contracts"; -import { GladiatorCard } from "./GladiatorCard" -import { useRouter } from 'next/navigation'; - -export function GladiatorsGrid() { - interface Gladiator { - aiAddress: string; - name: string; - isActive: boolean; - publicKey: string; - } - - const router = useRouter(); - - const { data: gladiators } = useReadContract({ - address: MARKET_FACTORY_ADDRESS, - abi: MARKET_FACTORY_ABI, - functionName: 'getAllGladiators', - }) as { data: Gladiator[] }; - - if (!gladiators) { - return
Loading gladiators...
; - } - - console.log("Gladiators", gladiators); - - return ( -
-

Gladiators

-
- {gladiators.map((gladiator: Gladiator) => ( - - ))} -
-
- ) -} diff --git a/frontend/debate-ai/components/gladiators/GladiatorCard.tsx b/frontend/debate-ai/components/gladiators/GladiatorCard.tsx new file mode 100644 index 0000000..133dab0 --- /dev/null +++ b/frontend/debate-ai/components/gladiators/GladiatorCard.tsx @@ -0,0 +1,138 @@ +import Image from "next/image"; +import { + Card, + CardContent, + CardFooter, + CardHeader, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Badge } from "@/components/ui/badge"; +import { ChevronRight, Shield, Sword } from "lucide-react"; +import { Button } from "../ui/button"; + +interface GladiatorProps { + id: string; + name: string; + image: string; + wins: number; + losses: number; + speciality: string; + level: number; +} + +interface Gladiator { + aiAddress: string; + name: string; + isActive: boolean; + publicKey: string; +} + +export function GladiatorCard({ + aiAddress, + name, + isActive, + publicKey, +}: Gladiator) { + return ( + + + + {/*
*/} + +
+
+ {name} +
+ +
+

+ {name} +

+

Level 1

+
+ +
+
+ + 10 +
+
+ + 10 +
+
+ +
+
+
+ +
+
+ Win Rate + 50% +
+
+ +
+
+ + + + + + {name} + +
+
+ {name} +
+
+
+ Level 1 + + Speciality + +
+
+
+ + Wins: 10 +
+
+ + Losses: 10 +
+
+

+ Win Rate: {((10 / (10 + 10)) * 100).toFixed(1)}% +

+
+
+
+
+ ); +} diff --git a/frontend/debate-ai/components/gladiators/gladiators-grid.tsx b/frontend/debate-ai/components/gladiators/gladiators-grid.tsx new file mode 100644 index 0000000..e0462d1 --- /dev/null +++ b/frontend/debate-ai/components/gladiators/gladiators-grid.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useReadContract } from "wagmi"; +import { MARKET_FACTORY_ABI, MARKET_FACTORY_ADDRESS } from "@/config/contracts"; +import { GladiatorCard } from "./GladiatorCard"; + +import { Sword } from "lucide-react"; +import Image from "next/image"; + +export function GladiatorsGrid() { + interface Gladiator { + aiAddress: string; + name: string; + isActive: boolean; + publicKey: string; + } + + const { data: gladiators } = useReadContract({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI, + functionName: "getAllGladiators", + }) as { data: Gladiator[] }; + + return ( +
+
+ {/* Header Section */} +
+
+
+ Example image +
+
+
+
+ ARENA OF GLADIATORS +
+
+ Witness the finest warriors in our digital colosseum. Each + gladiator brings unique skills and strategies to the arena. +
+
+
+
+ Example image +
+
+
+ + {/* Grid Section */} + +
+ {gladiators?.map((gladiator: Gladiator) => ( + + ))} +
+ + {/* Empty State */} + {gladiators?.length === 0 && ( +
+ +

+ No Gladiators Found +

+

+ The arena awaits its first warriors. +

+
+ )} +
+
+ ); +} diff --git a/frontend/debate-ai/components/DebateList.tsx b/frontend/debate-ai/components/home-page/DebateList.tsx similarity index 64% rename from frontend/debate-ai/components/DebateList.tsx rename to frontend/debate-ai/components/home-page/DebateList.tsx index 610a554..014e529 100644 --- a/frontend/debate-ai/components/DebateList.tsx +++ b/frontend/debate-ai/components/home-page/DebateList.tsx @@ -6,10 +6,10 @@ import { MARKET_FACTORY_ADDRESS, MARKET_FACTORY_ABI, } from "@/config/contracts"; -import { Card } from "./ui/card"; +import { Card } from "../ui/card"; import { formatAddress } from "@/lib/utils"; -import { Button } from "./ui/button"; -import { ChatWindow } from "./MiniChatWindow"; +import { Button } from "../ui/button"; +import { ChatWindow } from "../common/MiniChatWindow"; import { type Abi } from "viem"; import { Award, @@ -20,6 +20,16 @@ import { Users, } from "lucide-react"; +// Type for how bonding curve is returned from contract +type BondingCurve = [ + bigint, // target + bigint, // current + bigint, // basePrice + bigint, // currentPrice + boolean, // isFulfilled + bigint, // endTime +]; + type DebateDetails = [ string, // topic bigint, // startTime @@ -32,7 +42,7 @@ type DebateDetails = [ string, // market string[], // judges boolean, // hasOutcome - bigint // finalOutcome + bigint, // finalOutcome ]; interface DebateWithId { @@ -83,7 +93,7 @@ export function DebateList() { window.location.href = `/debate/${debateId}`; }; - // Combine all data and sort + // Combined all data and sort const debates: DebateWithId[] = []; debateIds?.forEach((id, index) => { const details = debateDetails?.[index]?.result as DebateDetails | undefined; @@ -102,10 +112,31 @@ export function DebateList() { setVisibleItems((prev) => prev + 6); }; + const { data: bondingCurveDetailsList } = useReadContracts({ + contracts: (marketIdsData || []).map((marketIdData) => ({ + address: MARKET_FACTORY_ADDRESS, + abi: MARKET_FACTORY_ABI as unknown as Abi, + functionName: "getBondingCurveDetails", + args: marketIdData?.result ? [marketIdData.result] : undefined, + })), + }); + + const bondingCurveMap = new Map(); + + (marketIdsData || []).forEach((marketIdData, index) => { + const marketId = marketIdData.result?.toString(); + const details = bondingCurveDetailsList?.[index]?.result as + | BondingCurve + | undefined; + if (marketId && details) { + bondingCurveMap.set(marketId, details); + } + }); + return (
- {visibleDebates.map(({ id: debateId, details, marketId }) => { + {visibleDebates.map(({ id: debateId, details, marketId }, index) => { if (!details) return null; const [ @@ -119,15 +150,36 @@ export function DebateList() { finalOutcome, ] = details; + // Fetch bonding curve details + const bondingCurveDetails = bondingCurveMap.get(marketId.toString()); + + const isFulfilled = bondingCurveDetails + ? bondingCurveDetails[4] + : false; + + const endTime = bondingCurveDetails ? bondingCurveDetails[5] : 0n; + + const timeRemaining = endTime + ? Math.max(0, Number(endTime) - Math.floor(Date.now() / 1000)) + : 0; + + const daysRemaining = Math.floor(timeRemaining / (24 * 60 * 60)); + const hoursRemaining = Math.floor( + (timeRemaining % (24 * 60 * 60)) / 3600 + ); + + let isDummyActive = true; + if (daysRemaining === 0 && hoursRemaining === 0) { + isDummyActive = false; + } + return (
@@ -145,15 +197,15 @@ export function DebateList() {
- {isActive && ( + {isDummyActive && (
)} - {isActive ? "Active" : "Completed"} + {isDummyActive ? "Active" : "Completed"}
@@ -172,7 +224,37 @@ export function DebateList() {
- + {!isFulfilled ? ( +
+
+
🤖
+
+
+

+ AI Agents are waiting to start +

+

+ Once the bonding curve target is reached, three expert + AI agents will begin analyzing and debating this topic + in real-time. +

+
+
+ Progress:{" "} + {bondingCurveDetails + ? ( + (Number(bondingCurveDetails[1]) * 100) / + Number(bondingCurveDetails[0]) + ).toFixed(1) + : "0"} + % to unlock +
+
+
+
+ ) : ( + + )}
diff --git a/frontend/debate-ai/components/HomeCenter.tsx b/frontend/debate-ai/components/home-page/HomeCenter.tsx similarity index 81% rename from frontend/debate-ai/components/HomeCenter.tsx rename to frontend/debate-ai/components/home-page/HomeCenter.tsx index 973a843..50bb5b7 100644 --- a/frontend/debate-ai/components/HomeCenter.tsx +++ b/frontend/debate-ai/components/home-page/HomeCenter.tsx @@ -1,17 +1,17 @@ import React from "react"; import Image from "next/image"; -import PixelCard from "../components/PixelCard"; +import PixelCard from "../common/PixelCard"; const HomeCenter = () => { return ( <> -
-
+
+
Example image @@ -57,12 +57,12 @@ const HomeCenter = () => {
-
+
Example image diff --git a/frontend/debate-ai/components/ui/drawer.tsx b/frontend/debate-ai/components/ui/drawer.tsx new file mode 100644 index 0000000..5dad120 --- /dev/null +++ b/frontend/debate-ai/components/ui/drawer.tsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; + +import { cn } from "@/lib/utils"; + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps) => ( + +); +Drawer.displayName = "Drawer"; + +const DrawerTrigger = DrawerPrimitive.Trigger; + +const DrawerPortal = DrawerPrimitive.Portal; + +const DrawerClose = DrawerPrimitive.Close; + +const DrawerOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; + +const DrawerContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ {children} + + +)); +DrawerContent.displayName = "DrawerContent"; + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerHeader.displayName = "DrawerHeader"; + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DrawerFooter.displayName = "DrawerFooter"; + +const DrawerTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; + +const DrawerDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/frontend/debate-ai/components/wagmi.tsx b/frontend/debate-ai/components/wagmi.tsx new file mode 100644 index 0000000..3276a12 --- /dev/null +++ b/frontend/debate-ai/components/wagmi.tsx @@ -0,0 +1,30 @@ +import { getDefaultConfig } from "@rainbow-me/rainbowkit"; +import { + arbitrum, + base, + mainnet, + optimism, + polygon, + sepolia, + holesky, +} from "wagmi/chains"; + +if (!process.env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID) { + throw new Error("RAINBOWKIT_PROJECT_ID is not set"); +} +export const rainbowKitProjectId = + process.env.NEXT_PUBLIC_RAINBOWKIT_PROJECT_ID; +export const config = getDefaultConfig({ + appName: "RainbowKit demo", + projectId: rainbowKitProjectId, + chains: [ + mainnet, + polygon, + optimism, + arbitrum, + base, + holesky, + ...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === "true" ? [sepolia] : []), + ], + ssr: true, +}); diff --git a/frontend/debate-ai/package.json b/frontend/debate-ai/package.json index 55e9aa4..456fcc3 100644 --- a/frontend/debate-ai/package.json +++ b/frontend/debate-ai/package.json @@ -31,14 +31,18 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "ethers": "5.7.2", + "framer-motion": "^12.4.1", "lucide-react": "^0.469.0", "next": "14.2.16", "openai-edge": "^1.2.2", + "pino-pretty": "^13.0.0", "react": "^18", "react-chartjs-2": "^5.2.0", "react-dom": "^18", + "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", "viem": "^2.21.59", "wagmi": "^2.14.6" }, diff --git a/frontend/debate-ai/public/glad_left.webp b/frontend/debate-ai/public/glad_left.webp new file mode 100644 index 0000000..a551585 Binary files /dev/null and b/frontend/debate-ai/public/glad_left.webp differ diff --git a/frontend/debate-ai/public/glad_right.webp b/frontend/debate-ai/public/glad_right.webp new file mode 100644 index 0000000..4f28a9f Binary files /dev/null and b/frontend/debate-ai/public/glad_right.webp differ diff --git a/frontend/debate-ai/public/gladiator_1.jpg b/frontend/debate-ai/public/gladiator_1.jpg new file mode 100644 index 0000000..5a05e3a Binary files /dev/null and b/frontend/debate-ai/public/gladiator_1.jpg differ diff --git a/frontend/debate-ai/public/julius_with_a_gun.webp b/frontend/debate-ai/public/julius_with_a_gun.webp new file mode 100644 index 0000000..20b4500 Binary files /dev/null and b/frontend/debate-ai/public/julius_with_a_gun.webp differ diff --git a/frontend/debate-ai/public/rock_container.webp b/frontend/debate-ai/public/rock_container.webp new file mode 100644 index 0000000..5917f34 Binary files /dev/null and b/frontend/debate-ai/public/rock_container.webp differ diff --git a/frontend/debate-ai/public/side_1.webp b/frontend/debate-ai/public/side_1.webp new file mode 100644 index 0000000..f3e3096 Binary files /dev/null and b/frontend/debate-ai/public/side_1.webp differ diff --git a/frontend/debate-ai/public/side_2.webp b/frontend/debate-ai/public/side_2.webp new file mode 100644 index 0000000..6e0497f Binary files /dev/null and b/frontend/debate-ai/public/side_2.webp differ diff --git a/frontend/debate-ai/public/swords.webp b/frontend/debate-ai/public/swords.webp new file mode 100644 index 0000000..f3137ac Binary files /dev/null and b/frontend/debate-ai/public/swords.webp differ diff --git a/frontend/debate-ai/tailwind.config.ts b/frontend/debate-ai/tailwind.config.ts index ebc9f38..1c63261 100644 --- a/frontend/debate-ai/tailwind.config.ts +++ b/frontend/debate-ai/tailwind.config.ts @@ -1,62 +1,62 @@ import type { Config } from "tailwindcss"; const config: Config = { - darkMode: ["class"], - content: [ + darkMode: ["class"], + content: [ "./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", ], theme: { - extend: { - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))' - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))' - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))' - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))' - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))' - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))' - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))' - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))' - } - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)' - } - } + extend: { + colors: { + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + chart: { + "1": "hsl(var(--chart-1))", + "2": "hsl(var(--chart-2))", + "3": "hsl(var(--chart-3))", + "4": "hsl(var(--chart-4))", + "5": "hsl(var(--chart-5))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + }, }, plugins: [require("tailwindcss-animate")], };