diff --git a/app/api/compare/route.ts b/app/api/compare/route.ts new file mode 100644 index 0000000..0c599c1 --- /dev/null +++ b/app/api/compare/route.ts @@ -0,0 +1,55 @@ +import ApplicationSingleton from "@/app/app"; +import { NextResponse } from "next/server"; +import { + type PreTrainedModel, + type Processor, + RawImage, +} from "@xenova/transformers"; +import { cosineSimilarity } from "./util"; + +export async function POST(request: Request) { + try { + const formData = await request.formData(); + + // Get the two images from the form data + const query_image = formData.get("query_image") as File; + const ans_image = formData.get("ans_image") as File; + + // Create Blobs from the images + const query_image_blob = new Blob([query_image], { + type: query_image.type, + }); + const ans_image_blob = new Blob([ans_image], { type: ans_image.type }); + + const [processor, vision_model]: [Processor, PreTrainedModel] = + await ApplicationSingleton.getInstance(); + + // Read the two images as raw images + const rawImageQuery = await RawImage.fromBlob(query_image_blob); + const rawImageAns = await RawImage.fromBlob(ans_image_blob); + + // Tokenize the two images + const tokenizedImageQuery = await processor(rawImageQuery); + const tokenizedImageAns = await processor(rawImageAns); + + // Encode the two images + const { image_embeds: embedsQuery } = await vision_model( + tokenizedImageQuery + ); + const { image_embeds: embedsAns } = await vision_model(tokenizedImageAns); + + const similarity = cosineSimilarity( + Object.values(embedsQuery.data), + Object.values(embedsAns.data) + ); + + return new NextResponse(JSON.stringify({ okay: "okay", similarity }), { + status: 200, + }); + } catch (error) { + console.error(error); + return new NextResponse(JSON.stringify({ error }), { + status: 500, + }); + } +} diff --git a/app/api/compare/util.ts b/app/api/compare/util.ts new file mode 100644 index 0000000..c867be4 --- /dev/null +++ b/app/api/compare/util.ts @@ -0,0 +1,25 @@ +// Contains helper functions for comparison +export const cosineSimilarity = ( + query_embeds: number[], + ans_embeds: number[] +) => { + if (query_embeds.length !== ans_embeds.length) { + throw new Error("Embeddings must be of the same length"); + } + + let dotProduct = 0; + let normQuery = 0; + let normAns = 0; + + for (let i = 0; i < query_embeds.length; ++i) { + const queryValue = query_embeds[i]; + const dbValue = ans_embeds[i]; + + dotProduct += queryValue * dbValue; + normQuery += queryValue * queryValue; + normAns += dbValue * dbValue; + } + + const similarity = dotProduct / (Math.sqrt(normQuery) * Math.sqrt(normAns)); + return similarity; +}; diff --git a/app/app.ts b/app/app.ts new file mode 100644 index 0000000..9a1ad0e --- /dev/null +++ b/app/app.ts @@ -0,0 +1,46 @@ +import { + AutoProcessor, + CLIPVisionModelWithProjection, + type PreTrainedModel, + type Processor, +} from "@xenova/transformers"; + +// Use the Singleton pattern to enable lazy construction of the pipeline. +// NOTE: We wrap the class in a function to prevent code duplication. +const S = () => + class ApplicationSingleton { + static model_id = "Xenova/clip-vit-base-patch16"; + static processor: Promise | null = null; + static vision_model: Promise | null = null; + + static async getInstance() { + if (this.processor === null) { + this.processor = AutoProcessor.from_pretrained(this.model_id); + } + + if (this.vision_model === null) { + this.vision_model = CLIPVisionModelWithProjection.from_pretrained( + this.model_id, + { + quantized: false, + } + ); + } + + return Promise.all([this.processor, this.vision_model]); + } + }; + +let ApplicationSingleton; +if (process.env.NODE_ENV !== "production") { + // When running in development mode, attach the pipeline to the + // global object so that it's preserved between hot reloads. + // For more information, see https://vercel.com/guides/nextjs-prisma-postgres + if (!(global as any).ApplicationSingleton) { + (global as any).ApplicationSingleton = S(); + } + ApplicationSingleton = (global as any).ApplicationSingleton; +} else { + ApplicationSingleton = S(); +} +export default ApplicationSingleton as any; diff --git a/app/page.tsx b/app/page.tsx index 77ba7ed..34d6b0a 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,4 +1,3 @@ -// pages/index.tsx import React from 'react'; import AppBar from '../components/layout/AppBar'; import CustomCard from '../components/ui/CustomCard'; diff --git a/app/play/page.tsx b/app/play/page.tsx index 852c410..e3728cd 100644 --- a/app/play/page.tsx +++ b/app/play/page.tsx @@ -1,6 +1,6 @@ -"use client" -import React from 'react'; -import GameScreen from '@/components/game/GameScreen'; // Adjust the path if necessary +"use client"; +import React, { useState } from 'react'; +import GameScreen from '@/components/game/GameScreen'; import MatchScreen from '@/components/game/MatchScreen'; import MintScreen from '@/components/game/MintScreen'; import NFTScreen from '@/components/game/NFTScreen'; @@ -8,10 +8,74 @@ import Leaderboard from '@/components/game/Leaderboard'; import AppBar from '@/components/layout/AppBar'; const Play = () => { + const [currentScreen, setCurrentScreen] = useState('game'); // Default to 'game' + + const handleGameScreenComplete = () => { + setCurrentScreen('match'); + }; + + const handleMatchScreenComplete = () => { + setCurrentScreen('mint'); + }; + + const handleMintScreenComplete = () => { + setCurrentScreen('nft'); + }; + + const handleNFTScreenComplete = () => { + setCurrentScreen('leaderboard'); + }; + + const handleViewLeaderboard = () => { + setCurrentScreen('leaderboard'); + }; + + const handleGoBack = () => { + switch (currentScreen) { + case 'leaderboard': + setCurrentScreen('nft'); + break; + case 'nft': + setCurrentScreen('mint'); + break; + case 'mint': + setCurrentScreen('match'); + break; + case 'match': + setCurrentScreen('game'); + break; + default: + setCurrentScreen('game'); + break; + } + }; + + const handleRestartGame = () => { + setCurrentScreen('game'); + }; + return (
- - + {currentScreen === 'game' && } + {currentScreen === 'match' && } + {currentScreen === 'mint' && + + } + {currentScreen === 'nft' && + + } + {currentScreen === 'leaderboard' && + + }
); }; diff --git a/components/game/GameScreen.tsx b/components/game/GameScreen.tsx index f25a036..35e76a8 100644 --- a/components/game/GameScreen.tsx +++ b/components/game/GameScreen.tsx @@ -5,10 +5,12 @@ import ScoreDisplay from '../ui/scoredisplay'; import DrawingCanvas from '../ui/drawingcanvas'; import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'; import 'react-circular-progressbar/dist/styles.css'; -import MyToolBar from '../ui/toolbar'; +interface GameScreenProps { + onComplete: () => void; +} -const GameScreen: React.FC = () => { +const GameScreen: React.FC = ({ onComplete }) => { const [score, setScore] = useState(0); const [timeLeft, setTimeLeft] = useState(60); const [generatedText, setGeneratedText] = useState('Draw a cat'); @@ -24,15 +26,16 @@ const GameScreen: React.FC = () => { const handleSubmit = () => { setScore(Math.floor(Math.random() * 100)); + onComplete(); // Trigger the transition to the next screen }; const timerProgress = (timeLeft / 60) * 100; return ( -
-
+
+
-
+
{ />
-
-
{generatedText}
- +
+
{generatedText}
+
+ +
diff --git a/components/game/Leaderboard.tsx b/components/game/Leaderboard.tsx index 256eba0..43c1ec1 100644 --- a/components/game/Leaderboard.tsx +++ b/components/game/Leaderboard.tsx @@ -1,13 +1,25 @@ +"use client"; import React from 'react'; import LeaderboardTable from '../ui/leaderboardtable'; +import { Button } from '../ui/button'; -const Leaderboard: React.FC = () => { - return ( -
-

Leaderboard

- -
- ); +interface LeaderboardProps { + onGoBack: () => void; + onRestart: () => void; +} + +const Leaderboard: React.FC = ({ onGoBack, onRestart }) => { + return ( +
+

Leaderboard

+ +
+ + +
+ +
+ ); }; export default Leaderboard; diff --git a/components/game/MatchScreen.tsx b/components/game/MatchScreen.tsx index 07fefed..59b0fb6 100644 --- a/components/game/MatchScreen.tsx +++ b/components/game/MatchScreen.tsx @@ -1,32 +1,40 @@ +"use client"; import React from 'react'; import { Button } from '../ui/button'; import ImageComparison from '../ui/imagecomparison'; -const MatchScreen: React.FC = () => { - return ( -
- {/* Container for Diagonal Cards */} -
-
-
-
-

Score: 85%

-
- {/* Diagonal Image Cards */} - - {/* Score Display */} +interface MatchScreenProps { + onComplete: () => void; +} - {/* Mint Button */} -
- -
- -
-
+const MatchScreen: React.FC = ({ onComplete }) => { + const handleMintButtonClick = () => { + onComplete(); // Trigger the transition to the MintScreen + }; + + return ( +
+ {/* Container for Diagonal Cards */} +
+
+
+
+

+ Score: 85% +

+
+ {/* Diagonal Image Cards */} + + {/* Mint Button */} +
+ +
- ); +
+
+ ); }; export default MatchScreen; diff --git a/components/game/MintScreen.tsx b/components/game/MintScreen.tsx index 095a8c4..eaade1d 100644 --- a/components/game/MintScreen.tsx +++ b/components/game/MintScreen.tsx @@ -1,15 +1,26 @@ +"use client"; import React from 'react'; -import { Button } from '../ui/button'; import MintForm from '../ui/mintform'; +import { Button } from '../ui/button'; + +interface MintScreenProps { + onComplete: () => void; + onViewLeaderboard: () => void; +} + +const MintScreen: React.FC = ({ onComplete, onViewLeaderboard }) => { + const handleMintComplete = () => { + onComplete(); // Redirect to NFT Screen + }; -const MintScreen: React.FC = () => { - return ( -
-

Mint Your Drawing

- - -
- ); + return ( +
+

Your Drawing

+ + + {/* */} +
+ ); }; export default MintScreen; diff --git a/components/game/NFTScreen.tsx b/components/game/NFTScreen.tsx index c985170..90fd91b 100644 --- a/components/game/NFTScreen.tsx +++ b/components/game/NFTScreen.tsx @@ -1,31 +1,42 @@ -"use client" +"use client"; import React, { useState } from 'react'; import NFTGallery from '../ui/nftgallery'; +import { Button } from '../ui/button'; -const NFTScreen: React.FC = () => { - const [activeTab, setActiveTab] = useState<'all' | 'user'>('all'); +interface NFTScreenProps { + onViewLeaderboard: () => void; + onRestart: () => void; +} - return ( -
-
-
- - -
- -
+const NFTScreen: React.FC = ({ onViewLeaderboard, onRestart }) => { + const [activeTab, setActiveTab] = useState<'all' | 'user'>('all'); + + return ( +
+
+
+ + +
+ +
+ +
- ); + +
+
+ ); }; export default NFTScreen; diff --git a/components/ui/drawingcanvas.tsx b/components/ui/drawingcanvas.tsx index 87e711b..4354608 100644 --- a/components/ui/drawingcanvas.tsx +++ b/components/ui/drawingcanvas.tsx @@ -1,5 +1,4 @@ import React, { useState, useRef, useEffect, ForwardedRef } from 'react'; -import { FaPen, FaEraser } from 'react-icons/fa'; import MyToolBar from './toolbar'; interface DrawingCanvasProps { @@ -14,16 +13,45 @@ const DrawingCanvas = React.forwardRef( const [isErasing, setIsErasing] = useState(false); const [undoStack, setUndoStack] = useState([]); const [redoStack, setRedoStack] = useState([]); + const [activeTool, setActiveTool] = useState<'draw' | 'erase'>('draw'); const canvasRef = useRef(null); + useEffect(() => { + const handleResize = () => { + if (canvasRef.current) { + const canvas = canvasRef.current; + const ctx = canvas.getContext('2d'); + if (ctx) { + // Save current drawing + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + // Resize canvas + canvas.width = canvas.parentElement?.clientWidth || width; + canvas.height = canvas.parentElement?.clientHeight || height; + + // Restore drawing + ctx.putImageData(imageData, 0, 0); + } + } + }; + + // Set initial size + handleResize(); + + // Add resize event listener + window.addEventListener('resize', handleResize); + + return () => window.removeEventListener('resize', handleResize); + }, [width, height]); + useEffect(() => { if (canvasRef.current) { const ctx = canvasRef.current.getContext('2d'); if (ctx) { ctx.lineCap = 'round'; ctx.lineJoin = 'round'; - ctx.lineWidth = 5; + ctx.lineWidth = 3; } } }, []); @@ -43,8 +71,7 @@ const DrawingCanvas = React.forwardRef( if (ctx) { ctx.beginPath(); ctx.moveTo(offsetX, offsetY); - setIsDrawing(true); - setIsErasing(false); + setIsDrawing(true); } } }; @@ -60,28 +87,25 @@ const DrawingCanvas = React.forwardRef( }; const stopDrawing = () => { - if (canvasRef.current) { - if (isDrawing || isErasing) { - setIsDrawing(false); - setIsErasing(false); - setUndoStack([...undoStack, canvasRef.current.toDataURL()]); - setRedoStack([]); - } + if (canvasRef.current && isDrawing) { + setIsDrawing(false); + setUndoStack([...undoStack, canvasRef.current.toDataURL()]); + setRedoStack([]); } }; const handleUndo = () => { if (canvasRef.current && undoStack.length > 0) { - const ctx = canvasRef.current.getContext('2d'); + const ctx = canvasRef.current!.getContext('2d'); const lastState = undoStack.pop()!; - setRedoStack([...redoStack, canvasRef.current.toDataURL()]); + setRedoStack([...redoStack, canvasRef.current!.toDataURL()]); setUndoStack([...undoStack]); - + const img = new Image(); img.src = lastState; img.onload = () => { if (ctx) { - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height); ctx.drawImage(img, 0, 0); } }; @@ -99,7 +123,7 @@ const DrawingCanvas = React.forwardRef( img.src = nextState; img.onload = () => { if (ctx) { - ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height); ctx.drawImage(img, 0, 0); } }; @@ -110,10 +134,10 @@ const DrawingCanvas = React.forwardRef( if (canvasRef.current) { const ctx = canvasRef.current.getContext('2d'); if (ctx) { - ctx.globalCompositeOperation = 'source-over'; // Draw mode + ctx.globalCompositeOperation = 'source-over'; } - setIsDrawing(true); - setIsErasing(false); + setActiveTool('draw'); + setIsErasing(false); } }; @@ -121,16 +145,17 @@ const DrawingCanvas = React.forwardRef( if (canvasRef.current) { const ctx = canvasRef.current.getContext('2d'); if (ctx) { - ctx.globalCompositeOperation = 'destination-out'; // Erase mode - ctx.lineWidth = eraserSize; // Set eraser size + ctx.globalCompositeOperation = 'destination-out'; + ctx.lineWidth = eraserSize; } + setActiveTool('erase'); + setIsErasing(true); setIsDrawing(false); - setIsErasing(true); } }; return ( -
+
{ canvasRef.current = node; @@ -140,19 +165,19 @@ const DrawingCanvas = React.forwardRef( ref.current = node; } }} - width={width} - height={height} - className="border border-gray-300" + className={`border border-gray-300 ${isErasing ? 'cursor-cell' : 'cursor-crosshair'} + w-full h-full`} onMouseDown={startDrawing} onMouseMove={draw} onMouseUp={stopDrawing} - onMouseLeave={stopDrawing} + onMouseLeave={stopDrawing} />
); diff --git a/components/ui/mintform.tsx b/components/ui/mintform.tsx index 0fc8a58..4479097 100644 --- a/components/ui/mintform.tsx +++ b/components/ui/mintform.tsx @@ -1,38 +1,21 @@ -import React, { useState } from 'react'; +"use client"; +import React from 'react'; -const MintForm: React.FC = () => { - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - - const handleMint = () => { - // Logic to mint NFT - }; +interface MintFormProps { + imageUrl: string; // URL of the user's drawing image +} +const MintForm: React.FC = ({ imageUrl }) => { return ( -
- -