diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index f4290f8..8f0f311 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -37,14 +37,13 @@ import { useMatchmakingContext } from "@/context/matchmakingContext";
import { ChessVariantSelector } from "@/components/ChessVariantSelector";
import { getChessVariantById } from "@/lib/chessVariants";
import { HeroBranding } from "@/components/HeroBranding";
-import { GameResultOverlay } from "@/components/GameResultOverlay";
-import type { GameResult } from "@/components/GameResultOverlay";
import { WalletConnectModal } from "@/components/WalletConnectModal";
+import { endpoints } from "@/lib/api";
+import { ErrorBoundary } from "@/components/ErrorBoundary";
import { CapturedPieces } from "@/components/chess/CapturedPieces";
import { EvaluationBar } from "@/components/chess/EvaluationBar";
import { MoveHistory } from "@/components/chess/MoveHistory";
-import { ErrorBoundary } from "@/components/ErrorBoundary";
-import { endpoints } from "@/lib/api";
+import GameResultOverlay, { type GameResult } from "@/components/GameResultOverlay";
export default function Home() {
const [game] = useState(new Chess());
@@ -78,7 +77,6 @@ export default function Home() {
joinMatchmaking,
cancelMatchmaking,
sendMove: matchmakingSendMove,
- lastOpponentMove,
gameId,
} = useMatchmaking();
@@ -136,25 +134,6 @@ export default function Home() {
}
}, [matchmakingStatus, gameId, router]);
- // Apply opponent's move in online mode
- useEffect(() => {
- if (!lastOpponentMove || gameMode !== "online") return;
- try {
- const move = game.move({
- from: lastOpponentMove.from,
- to: lastOpponentMove.to,
- promotion: lastOpponentMove.promotion ?? "q",
- });
- if (move) {
- setPosition(game.fen());
- setViewIndex(null);
- }
- } catch {
- // illegal move from server — ignore
- }
- }, [lastOpponentMove, game, gameMode]);
-
- // Fetch online player count
useEffect(() => {
let isMounted = true;
const fetchOnlinePlayers = async () => {
diff --git a/frontend/app/play/[slug]/page.tsx b/frontend/app/play/[slug]/page.tsx
index d918cda..a73c14e 100644
--- a/frontend/app/play/[slug]/page.tsx
+++ b/frontend/app/play/[slug]/page.tsx
@@ -1,499 +1,60 @@
"use client";
-import React, { useState, useEffect, useCallback } from "react";
+import React from "react";
import dynamic from "next/dynamic";
-import { Chess } from "chess.js";
-import { useParams, useRouter } from "next/navigation";
-import { useChessSocket } from "@/hook/useChessSocket";
-import { FaUser, FaClock, FaSignal } from "react-icons/fa";
-import { Web3StatusBar } from "@/components/Web3StatusBar";
-import { useCheatDetection } from "@/hook/useCheatDetection";
-import { GameResultOverlay } from "@/components/GameResultOverlay";
-import type { GameResult } from "@/components/GameResultOverlay";
-import { CheatDetectionPanel } from "@/components/chess/CheatDetectionPanel";
-import { useIsMobile } from "@/hook/use-mobile";
-import { ErrorBoundary } from "@/components/ErrorBoundary";
-const ChessboardComponent = dynamic(
- () => import("@/components/chess/ChessboardComponent"),
+const PlayGameEngine = dynamic(
+ () => import("@/components/chess/PlayGameEngine"),
{
ssr: false,
loading: () => (
-
-
- {Array.from({ length: 64 }).map((_, i) => (
-
- ))}
-
-
- ),
- },
-);
-
-type GameStatus = "playing" | "checkmate" | "stalemate" | "draw" | "resigned";
-
-export default function PlayOnlinePage() {
- const params = useParams();
- const router = useRouter();
- const gameId = params.slug as string;
-
- const [game] = useState(new Chess());
- const [position, setPosition] = useState("start");
- const [moveHistory, setMoveHistory] = useState([]);
- const [whiteTime, setWhiteTime] = useState(600); // 10 min in seconds
- const [blackTime, setBlackTime] = useState(600);
- const [playerColor] = useState<"white" | "black">("white");
- const [gameStatus, setGameStatus] = useState("playing");
- const [isCheatPanelExpanded, setIsCheatPanelExpanded] = useState(false);
- const [isMoveHistoryOpen, setIsMoveHistoryOpen] = useState(false);
- const [boardOrientation, setBoardOrientation] = useState<"white" | "black">("white");
- const isMobile = useIsMobile();
-
- const handleFlipBoard = useCallback(() => {
- setBoardOrientation((prev) => (prev === "white" ? "black" : "white"));
- }, []);
-
- const {
- status: socketStatus,
- sendMove,
- disconnect,
- reconnect,
- lastOpponentMove,
- } = useChessSocket(gameId);
-
- const checkGameStatus = useCallback(() => {
- if (game.isCheckmate()) {
- setGameStatus("checkmate");
- } else if (game.isStalemate()) {
- setGameStatus("stalemate");
- } else if (game.isDraw()) {
- setGameStatus("draw");
- }
- }, [game]);
-
- // ── FE-04: Countdown clocks ───────────────────────────────────────────────
- // Tick the active side's clock down by 1 second every second while the game
- // is live. The interval is cleared the moment the game ends.
- useEffect(() => {
- if (socketStatus !== "connected" || gameStatus !== "playing") return;
-
- const id = setInterval(() => {
- const activeColor = game.turn(); // "w" | "b"
- if (activeColor === "w") {
- setWhiteTime((t) => Math.max(0, t - 1));
- } else {
- setBlackTime((t) => Math.max(0, t - 1));
- }
- }, 1000);
-
- return () => clearInterval(id);
- }, [socketStatus, gameStatus, game]);
-
- // Sync clocks from authoritative server clock messages.
- // The socket emits { type: "clock", whiteTime: number, blackTime: number }
- // after each move so both sides stay in sync.
- useEffect(() => {
- // useChessSocket only exposes lastOpponentMove; clock data arrives via the
- // raw WebSocket. We listen for a "clock" message relayed through a custom
- // DOM event dispatched by a thin shim (or we handle it here directly).
- // For now we seed from lastOpponentMove's accompanying clock field when
- // the server includes it.
- if (!lastOpponentMove) return;
- const raw = lastOpponentMove as typeof lastOpponentMove & {
- whiteTime?: number;
- blackTime?: number;
- };
- if (typeof raw.whiteTime === "number") setWhiteTime(raw.whiteTime);
- if (typeof raw.blackTime === "number") setBlackTime(raw.blackTime);
- }, [lastOpponentMove]);
-
- // Cheat detection
- const {
- opponentAnalysis,
- playerAnalysis,
- recordMove: recordCheatMove,
- reset: resetCheatDetection,
- isActive: isCheatDetectionActive,
- } = useCheatDetection(playerColor);
-
- // Apply opponent's move to local chess state
- useEffect(() => {
- if (!lastOpponentMove) return;
- try {
- const fenBeforeOpponentMove = game.fen();
- const move = game.move({
- from: lastOpponentMove.from,
- to: lastOpponentMove.to,
- promotion: lastOpponentMove.promotion ?? "q",
- });
- if (move) {
- setPosition(game.fen());
- setMoveHistory((prev: string[]) => [...prev, move.san]);
- recordCheatMove(
- move.san,
- move,
- fenBeforeOpponentMove,
- playerColor === "white" ? "b" : "w",
- Math.ceil(game.moveNumber() / 2),
- );
- checkGameStatus();
- }
- } catch {
- // illegal move from server — ignore
- }
- }, [lastOpponentMove, game, checkGameStatus, recordCheatMove, playerColor]);
-
- const isMyTurn =
- socketStatus === "connected" &&
- ((playerColor === "white" && game.turn() === "w") ||
- (playerColor === "black" && game.turn() === "b"));
-
- const handleMove = useCallback(
- ({
- sourceSquare,
- targetSquare,
- }: {
- sourceSquare: string;
- targetSquare: string;
- }) => {
- if (!isMyTurn || gameStatus !== "playing") return false;
-
- try {
- const fenBeforeMyMove = game.fen();
- const move = game.move({
- from: sourceSquare,
- to: targetSquare,
- promotion: "q",
- });
- if (move === null) return false;
-
- requestAnimationFrame(() => setPosition(game.fen()));
- setMoveHistory((prev: string[]) => [...prev, move.san]);
- sendMove({ from: sourceSquare, to: targetSquare, promotion: "q" });
- recordCheatMove(
- move.san,
- move,
- fenBeforeMyMove,
- playerColor === "white" ? "w" : "b",
- Math.ceil(game.moveNumber() / 2),
- );
- checkGameStatus();
- return true;
- } catch {
- return false;
- }
- },
- [isMyTurn, game, gameStatus, sendMove, checkGameStatus, recordCheatMove, playerColor],
- );
-
- const handleResign = useCallback(() => {
- setGameStatus("resigned");
- resetCheatDetection();
- disconnect();
- }, [disconnect, resetCheatDetection]);
-
- const formatTime = (seconds: number) => {
- const m = Math.floor(seconds / 60);
- const s = seconds % 60;
- return `${m}:${s.toString().padStart(2, "0")}`;
- };
-
- const socketStatusLabel = () => {
- switch (socketStatus) {
- case "connected":
- return "Live";
- case "connecting":
- return "Connecting...";
- case "reconnecting":
- return "Reconnecting...";
- case "disconnected":
- return "Disconnected";
- case "error":
- return "Error";
- default:
- return "Idle";
- }
- };
-
- const socketStatusColor = () => {
- switch (socketStatus) {
- case "connected":
- return "text-emerald-400";
- case "connecting":
- case "reconnecting":
- return "text-yellow-400";
- default:
- return "text-red-400";
- }
- };
-
- // Group moves into pairs for display
- const movePairs = moveHistory.reduce(
- (acc: string[][], move: string, i: number) => {
- if (i % 2 === 0) acc.push([move]);
- else acc[acc.length - 1].push(move);
- return acc;
- },
- [],
- );
-
- let overlayResult: GameResult | null = null;
- if (gameStatus === "checkmate") {
- overlayResult = game.turn() === "b" ? "white_wins" : "black_wins";
- } else if (gameStatus === "stalemate") {
- overlayResult = "stalemate";
- } else if (gameStatus === "draw") {
- overlayResult = "draw";
- }
-
- return (
-
-
- {/* Top bar */}
-
-
-
-
-
-
- {/* Chessboard Section */}
-
- {/* Opponent info bar */}
-
-
-
-
-
-
-
Opponent
-
- {playerColor === "white" ? "Black" : "White"}
-
-
-
-
-
-
- {formatTime(playerColor === "white" ? blackTime : whiteTime)}
-
-
-
-
- {/* Board */}
-
-
-
-
-
-
- {/* Player info bar */}
-
-
-
-
-
-
-
You
-
- {playerColor}
- {isMyTurn && (
-
- (Your turn)
-
- )}
-
-
-
-
-
-
- {formatTime(playerColor === "white" ? whiteTime : blackTime)}
-
-
-
-
- {/* Mobile controls — visible only on mobile, directly below board */}
-
-
-
-
-
-
- {socketStatusLabel()}
-
-
-
+
+
+
-
- {/* Game Sidebar - Move History & Controls */}
-
- {/* Game Status Card */}
-
-
-
- Game Status
-
-
-
-
- {socketStatusLabel()}
-
+
+
+
-
- {gameStatus !== "playing" && (
-
-
- {gameStatus === "checkmate" && "Checkmate!"}
- {gameStatus === "stalemate" && "Stalemate!"}
- {gameStatus === "draw" && "Draw!"}
- {gameStatus === "resigned" && "Resigned!"}
-
-
- )}
-
- {game.isCheck() && gameStatus === "playing" && (
-
-
Check!
+
- )}
-
-
- Game ID: {gameId?.slice(0, 12)}...
-
-
- {/* Move History */}
-
-
-
- {movePairs.length === 0 ? (
-
- No moves yet.{" "}
- {isMyTurn ? "Your turn to move!" : "Waiting for opponent..."}
-
- ) : (
- movePairs.map((pair, i) => (
+
+
+ {Array.from({ length: 64 }).map((_, i) => (
-
- {i + 1}.
-
-
- {pair[0]}
-
-
- {pair[1] ?? ""}
-
-
- ))
- )}
+ className={`${
+ (Math.floor(i / 8) + (i % 8)) % 2 === 0
+ ? "bg-gray-700/30"
+ : "bg-gray-600/20"
+ } rounded-sm shimmer-bg`}
+ />
+ ))}
+
-
- {/* Cheat Detection Panel */}
-
setIsCheatPanelExpanded((prev: boolean) => !prev)}
- />
-
- {/* Controls — desktop only, mobile controls are above the sidebar */}
-
-
-
+
-
- {/* Reconnection button */}
- {socketStatus === "disconnected" && gameStatus === "playing" && (
-
- )}
+ ),
+ },
+);
- {overlayResult && (
-
router.push("/")}
- onPlayOnline={() => router.push("/")}
- />
- )}
-
- );
+export default function PlayOnlinePage() {
+ return
;
}
diff --git a/frontend/components/chess/HeroChessGame.tsx b/frontend/components/chess/HeroChessGame.tsx
new file mode 100644
index 0000000..0679969
--- /dev/null
+++ b/frontend/components/chess/HeroChessGame.tsx
@@ -0,0 +1,591 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import dynamic from "next/dynamic";
+import { Chess } from "chess.js";
+import { FaUser } from "react-icons/fa";
+import { RiAliensFill } from "react-icons/ri";
+import { useChessSocket } from "@/hook/useChessSocket";
+import { useMatchmaking } from "@/hook/useMatchmaking";
+import { useStockfishWASM, AnalysisResult } from "@/components/chess/StockfishWASM";
+import { useRouter } from "next/navigation";
+import { useMatchmakingContext } from "@/context/matchmakingContext";
+import { getChessVariantById } from "@/lib/chessVariants";
+import { GameResultOverlay } from "@/components/GameResultOverlay";
+import type { GameResult } from "@/components/GameResultOverlay";
+import { CapturedPieces } from "@/components/chess/CapturedPieces";
+import { EvaluationBar } from "@/components/chess/EvaluationBar";
+import { MoveHistory } from "@/components/chess/MoveHistory";
+import { ErrorBoundary } from "@/components/ErrorBoundary";
+
+const ChessboardComponent = dynamic(
+ () => import("@/components/chess/ChessboardComponent"),
+ { ssr: false },
+);
+const GameModeButtons = dynamic(() => import("@/components/GameModeButtons"), {
+ ssr: false,
+});
+const AiPersonalityModal = dynamic(
+ () =>
+ import("@/app/components/matchmaking/AiPersonalityModal").then((m) => ({
+ default: m.AiPersonalityModal,
+ })),
+ { ssr: false },
+);
+const MatchmakingModal = dynamic(
+ () =>
+ import("@/app/components/matchmaking/MatchmakingModal").then((m) => ({
+ default: m.MatchmakingModal,
+ })),
+ { ssr: false },
+);
+const ChessVariantSelector = dynamic(
+ () =>
+ import("@/components/ChessVariantSelector").then((m) => ({
+ default: m.ChessVariantSelector,
+ })),
+ { ssr: false },
+);
+
+interface HeroChessGameProps {
+ onlinePlayerCount: number | null;
+ heroBranding: React.ReactNode;
+}
+
+export default function HeroChessGame({
+ onlinePlayerCount,
+ heroBranding,
+}: HeroChessGameProps) {
+ const [game] = useState(new Chess());
+ const [position, setPosition] = useState("start");
+ const [viewIndex, setViewIndex] = useState
(null);
+ const [gameMode, setGameMode] = useState<"online" | "bot" | null>(null);
+ const router = useRouter();
+ const [isPersonalityModalOpen, setIsPersonalityModalOpen] = useState(false);
+ const [isMatchmakingModalOpen, setIsMatchmakingModalOpen] = useState(false);
+ const [botAnalysis, setBotAnalysis] = useState(null);
+
+ const [heroGameResult, setHeroGameResult] = useState(null);
+ const [heroMoveCount, setHeroMoveCount] = useState(0);
+
+ const { aiPersonality, chessVariant, setChessVariant } =
+ useMatchmakingContext();
+ const selectedVariant = getChessVariantById(chessVariant);
+
+ const {
+ status: matchmakingStatus,
+ playerColor,
+ error: matchmakingError,
+ joinMatchmaking,
+ cancelMatchmaking,
+ sendMove: matchmakingSendMove,
+ lastOpponentMove,
+ gameId,
+ } = useMatchmaking();
+
+ const {
+ status: socketStatus,
+ sendMove: socketSendMove,
+ disconnect: disconnectSocket,
+ reconnect: reconnectSocket,
+ } = useChessSocket(gameId);
+
+ const { analyzePosition, isReady: stockfishReady, isAnalyzing } = useStockfishWASM({
+ jsBridgePath: "/assets/stockfish.js",
+ defaultTimeLimit: 250,
+ });
+
+ const currentDisplayPosition = React.useMemo(() => {
+ if (viewIndex === null) return position;
+ const tempGame = new Chess();
+ const history = game.history();
+ for (let i = 0; i <= viewIndex; i++) {
+ try { tempGame.move(history[i]); } catch {}
+ }
+ return tempGame.fen();
+ }, [position, viewIndex, game]);
+
+ const handleMoveClick = useCallback((index: number) => {
+ setViewIndex(index === game.history().length - 1 ? null : index);
+ }, [game]);
+
+ const sendMove = useCallback(
+ (from: string, to: string, promotion?: string) => {
+ if (gameId) {
+ socketSendMove({ from, to, promotion: promotion || "q" });
+ } else {
+ matchmakingSendMove(from, to, promotion);
+ }
+ },
+ [gameId, socketSendMove, matchmakingSendMove],
+ );
+
+ useEffect(() => {
+ if (matchmakingStatus === "match_found" && gameId) {
+ router.push(`/play/${gameId}`);
+ }
+ }, [matchmakingStatus, gameId, router]);
+
+ useEffect(() => {
+ if (!lastOpponentMove || gameMode !== "online") return;
+ try {
+ const move = game.move({
+ from: lastOpponentMove.from,
+ to: lastOpponentMove.to,
+ promotion: lastOpponentMove.promotion ?? "q",
+ });
+ if (move) {
+ setPosition(game.fen());
+ setViewIndex(null);
+ }
+ } catch {
+ // illegal move from server — ignore
+ }
+ }, [lastOpponentMove, game, gameMode]);
+
+ useEffect(() => {
+ let active = true;
+
+ const playBotMove = async () => {
+ const isHeroMode = gameMode === null;
+ const isBotMode = gameMode === "bot";
+ if (!stockfishReady || isAnalyzing || !(isHeroMode || isBotMode)) return;
+ if (game.turn() !== "b" || game.isGameOver()) return;
+
+ try {
+ let depth = 5;
+ if (isBotMode) {
+ depth = 10;
+ if (aiPersonality === "aggressive") depth = 15;
+ if (aiPersonality === "defensive") depth = 18;
+ }
+
+ const fenBeforeAnalysis = game.fen();
+ const result = await analyzePosition(fenBeforeAnalysis, depth);
+ if (
+ active &&
+ result.bestMove &&
+ game.fen() === fenBeforeAnalysis &&
+ !game.isGameOver()
+ ) {
+ setBotAnalysis(result);
+ const from = result.bestMove.substring(0, 2);
+ const to = result.bestMove.substring(2, 4);
+ const promotion =
+ result.bestMove.length > 4 ? result.bestMove.substring(4, 5) : undefined;
+ game.move({ from, to, promotion });
+ setPosition(game.fen());
+ setViewIndex(null);
+ setHeroMoveCount((c) => c + 1);
+ }
+ } catch (e) {
+ console.error("Bot failed to move:", e);
+ }
+ };
+
+ const timer = setTimeout(playBotMove, 400);
+ return () => {
+ active = false;
+ clearTimeout(timer);
+ };
+ }, [position, gameMode, analyzePosition, aiPersonality, game, stockfishReady, isAnalyzing]);
+
+ useEffect(() => {
+ if (!game.isGameOver()) return;
+ if (gameMode !== null && gameMode !== "bot") return;
+
+ if (game.isCheckmate()) {
+ setHeroGameResult(game.turn() === "w" ? "black_wins" : "white_wins");
+ } else if (game.isStalemate()) {
+ setHeroGameResult("stalemate");
+ } else {
+ setHeroGameResult("draw");
+ }
+ }, [position, game, gameMode]);
+
+ const isMyTurn = (() => {
+ if (gameMode === null) {
+ return game.turn() === "w";
+ }
+ if (gameMode === "bot") {
+ return game.turn() === "w";
+ }
+ if (gameMode === "online") {
+ return (
+ socketStatus === "connected" &&
+ ((playerColor === "white" && game.turn() === "w") ||
+ (playerColor === "black" && game.turn() === "b"))
+ );
+ }
+ return true;
+ })();
+
+ const handleMove = useCallback(
+ ({
+ sourceSquare,
+ targetSquare,
+ }: {
+ sourceSquare: string;
+ targetSquare: string;
+ }) => {
+ if (!isMyTurn || game.isGameOver()) return false;
+ if (viewIndex !== null) {
+ setViewIndex(null);
+ return false;
+ }
+
+ try {
+ const move = game.move({
+ from: sourceSquare,
+ to: targetSquare,
+ promotion: "q",
+ });
+ if (move === null) return false;
+
+ setBotAnalysis(null);
+ setHeroMoveCount((c) => c + 1);
+ requestAnimationFrame(() => setPosition(game.fen()));
+ if (gameMode === "online") {
+ sendMove(sourceSquare, targetSquare, "q");
+ }
+
+ setPosition(game.fen());
+ setViewIndex(null);
+ return true;
+ } catch {
+ return false;
+ }
+ },
+ [isMyTurn, game, gameMode, sendMove, viewIndex],
+ );
+
+ const handleHeroPlayAgain = useCallback(() => {
+ game.reset();
+ setPosition("start");
+ setBotAnalysis(null);
+ setHeroGameResult(null);
+ setHeroMoveCount(0);
+ }, [game]);
+
+ const handleExit = () => {
+ if (gameMode === "online") {
+ cancelMatchmaking();
+ disconnectSocket();
+ }
+ game.reset();
+ setPosition("start");
+ setGameMode(null);
+ setBotAnalysis(null);
+ setHeroGameResult(null);
+ setHeroMoveCount(0);
+ };
+
+ const handleSetGameMode = (mode: "online" | "bot" | null) => {
+ if (mode === "online") {
+ setGameMode(mode);
+ setIsMatchmakingModalOpen(true);
+ } else if (mode === "bot") {
+ setGameMode(mode);
+ setIsPersonalityModalOpen(true);
+ } else {
+ setGameMode(mode);
+ }
+ };
+
+ const handleMatchmakingConfirm = (type: "Rated" | "Casual") => {
+ setIsMatchmakingModalOpen(false);
+ joinMatchmaking(type, chessVariant);
+ };
+
+ const handleMatchmakingClose = () => {
+ setIsMatchmakingModalOpen(false);
+ setGameMode(null);
+ };
+
+ const handlePersonalityConfirm = () => {
+ setIsPersonalityModalOpen(false);
+ game.reset();
+ setPosition("start");
+ setBotAnalysis(null);
+ setHeroGameResult(null);
+ setHeroMoveCount(0);
+ };
+
+ const handlePersonalityClose = () => {
+ setIsPersonalityModalOpen(false);
+ setGameMode(null);
+ };
+
+ const handlePlayOnlineFromResult = () => {
+ setHeroGameResult(null);
+ handleSetGameMode("online");
+ };
+
+ const onlineStatusLabel = () => {
+ if (socketStatus === "reconnecting") return "🔄 Reconnecting...";
+ if (matchmakingStatus === "match_found") return "✅ Match found! Starting…";
+ if (socketStatus === "connected")
+ return `🟢 Online Match (you are ${playerColor})`;
+ if (matchmakingStatus === "error" || socketStatus === "error")
+ return `❌ ${matchmakingError ?? "Connection error"}`;
+ return "Online Match";
+ };
+
+ return (
+
+
+
+
+
+
+
+ {botAnalysis && (
+
+
+ Eval:{" "}
+ {botAnalysis.evaluation != null
+ ? `${botAnalysis.evaluation > 0 ? "+" : ""}${botAnalysis.evaluation.toFixed(2)}`
+ : "N/A"}
+
+ |
+ D{botAnalysis.depth}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {heroMoveCount > 0
+ ? `Move ${Math.ceil(heroMoveCount / 2)}`
+ : "Make your first move!"}
+
+ {!stockfishReady && (
+
+
+ Loading engine...
+
+ )}
+ {stockfishReady && heroMoveCount === 0 && (
+
+
+ Engine ready
+
+ )}
+
+
+
+
+
+
+
+
+ {heroBranding}
+
+
+
+
+
+
+
+ Choose Your Arena
+
+
+ Play online, train with bots, or solve puzzles to earn XLM
+
+
+
+
+
+
+ {gameMode && (
+
+
+
+ {gameMode === "online" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {gameMode === "online"
+ ? onlineStatusLabel()
+ : "Playing vs Bot"}
+
+
+ {selectedVariant.label} / {selectedVariant.averageGameTime}
+
+
+
+
+
+ )}
+
+ {gameMode === "online" && matchmakingStatus === "searching" && (
+
+
+
+
+ Looking for opponent...
+
+
+
+
+
+
+ {onlinePlayerCount} Players online
+
+
+ Queueing for {selectedVariant.label}
+
+
+
+
+
+ )}
+
+ {gameMode === "online" && socketStatus === "reconnecting" && (
+
+
+
+
+
+ Reconnecting...
+
+
+ Attempting to restore connection
+
+
+
+
+
+ )}
+
+ {heroGameResult && (
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/components/chess/PlayGameEngine.tsx b/frontend/components/chess/PlayGameEngine.tsx
new file mode 100644
index 0000000..ca9b83c
--- /dev/null
+++ b/frontend/components/chess/PlayGameEngine.tsx
@@ -0,0 +1,473 @@
+"use client";
+
+import React, { useState, useEffect, useCallback } from "react";
+import dynamic from "next/dynamic";
+import { Chess } from "chess.js";
+import { useParams, useRouter } from "next/navigation";
+import { useChessSocket } from "@/hook/useChessSocket";
+import { FaUser, FaClock, FaSignal } from "react-icons/fa";
+import { Web3StatusBar } from "@/components/Web3StatusBar";
+import { useCheatDetection } from "@/hook/useCheatDetection";
+import { GameResultOverlay } from "@/components/GameResultOverlay";
+import type { GameResult } from "@/components/GameResultOverlay";
+import { CheatDetectionPanel } from "@/components/chess/CheatDetectionPanel";
+import { useIsMobile } from "@/hook/use-mobile";
+import { ErrorBoundary } from "@/components/ErrorBoundary";
+
+const ChessboardComponent = dynamic(
+ () => import("@/components/chess/ChessboardComponent"),
+ {
+ ssr: false,
+ loading: () => (
+
+
+ {Array.from({ length: 64 }).map((_, i) => (
+
+ ))}
+
+
+ ),
+ },
+);
+
+type GameStatus = "playing" | "checkmate" | "stalemate" | "draw" | "resigned";
+
+export default function PlayGameEngine() {
+ const params = useParams();
+ const router = useRouter();
+ const gameId = params.slug as string;
+
+ const [game] = useState(new Chess());
+ const [position, setPosition] = useState("start");
+ const [moveHistory, setMoveHistory] = useState([]);
+ const [whiteTime, setWhiteTime] = useState(600);
+ const [blackTime, setBlackTime] = useState(600);
+ const [playerColor] = useState<"white" | "black">("white");
+ const [gameStatus, setGameStatus] = useState("playing");
+ const [isCheatPanelExpanded, setIsCheatPanelExpanded] = useState(false);
+ const [isMoveHistoryOpen, setIsMoveHistoryOpen] = useState(false);
+ const [boardOrientation, setBoardOrientation] = useState<"white" | "black">("white");
+ const isMobile = useIsMobile();
+
+ const handleFlipBoard = useCallback(() => {
+ setBoardOrientation((prev) => (prev === "white" ? "black" : "white"));
+ }, []);
+
+ const {
+ status: socketStatus,
+ sendMove,
+ disconnect,
+ reconnect,
+ lastOpponentMove,
+ } = useChessSocket(gameId);
+
+ const checkGameStatus = useCallback(() => {
+ if (game.isCheckmate()) {
+ setGameStatus("checkmate");
+ } else if (game.isStalemate()) {
+ setGameStatus("stalemate");
+ } else if (game.isDraw()) {
+ setGameStatus("draw");
+ }
+ }, [game]);
+
+ useEffect(() => {
+ if (socketStatus !== "connected" || gameStatus !== "playing") return;
+
+ const id = setInterval(() => {
+ const activeColor = game.turn();
+ if (activeColor === "w") {
+ setWhiteTime((t) => Math.max(0, t - 1));
+ } else {
+ setBlackTime((t) => Math.max(0, t - 1));
+ }
+ }, 1000);
+
+ return () => clearInterval(id);
+ }, [socketStatus, gameStatus, game]);
+
+ useEffect(() => {
+ if (!lastOpponentMove) return;
+ const raw = lastOpponentMove as typeof lastOpponentMove & {
+ whiteTime?: number;
+ blackTime?: number;
+ };
+ if (typeof raw.whiteTime === "number") setWhiteTime(raw.whiteTime);
+ if (typeof raw.blackTime === "number") setBlackTime(raw.blackTime);
+ }, [lastOpponentMove]);
+
+ const {
+ opponentAnalysis,
+ playerAnalysis,
+ recordMove: recordCheatMove,
+ reset: resetCheatDetection,
+ isActive: isCheatDetectionActive,
+ } = useCheatDetection(playerColor);
+
+ useEffect(() => {
+ if (!lastOpponentMove) return;
+ try {
+ const fenBeforeOpponentMove = game.fen();
+ const move = game.move({
+ from: lastOpponentMove.from,
+ to: lastOpponentMove.to,
+ promotion: lastOpponentMove.promotion ?? "q",
+ });
+ if (move) {
+ setPosition(game.fen());
+ setMoveHistory((prev: string[]) => [...prev, move.san]);
+ recordCheatMove(
+ move.san,
+ move,
+ fenBeforeOpponentMove,
+ playerColor === "white" ? "b" : "w",
+ Math.ceil(game.moveNumber() / 2),
+ );
+ checkGameStatus();
+ }
+ } catch {
+ // illegal move from server — ignore
+ }
+ }, [lastOpponentMove, game, checkGameStatus, recordCheatMove, playerColor]);
+
+ const isMyTurn =
+ socketStatus === "connected" &&
+ ((playerColor === "white" && game.turn() === "w") ||
+ (playerColor === "black" && game.turn() === "b"));
+
+ const handleMove = useCallback(
+ ({
+ sourceSquare,
+ targetSquare,
+ }: {
+ sourceSquare: string;
+ targetSquare: string;
+ }) => {
+ if (!isMyTurn || gameStatus !== "playing") return false;
+
+ try {
+ const fenBeforeMyMove = game.fen();
+ const move = game.move({
+ from: sourceSquare,
+ to: targetSquare,
+ promotion: "q",
+ });
+ if (move === null) return false;
+
+ requestAnimationFrame(() => setPosition(game.fen()));
+ setMoveHistory((prev: string[]) => [...prev, move.san]);
+ sendMove({ from: sourceSquare, to: targetSquare, promotion: "q" });
+ recordCheatMove(
+ move.san,
+ move,
+ fenBeforeMyMove,
+ playerColor === "white" ? "w" : "b",
+ Math.ceil(game.moveNumber() / 2),
+ );
+ checkGameStatus();
+ return true;
+ } catch {
+ return false;
+ }
+ },
+ [isMyTurn, game, gameStatus, sendMove, checkGameStatus, recordCheatMove, playerColor],
+ );
+
+ const handleResign = useCallback(() => {
+ setGameStatus("resigned");
+ resetCheatDetection();
+ disconnect();
+ }, [disconnect, resetCheatDetection]);
+
+ const formatTime = (seconds: number) => {
+ const m = Math.floor(seconds / 60);
+ const s = seconds % 60;
+ return `${m}:${s.toString().padStart(2, "0")}`;
+ };
+
+ const socketStatusLabel = () => {
+ switch (socketStatus) {
+ case "connected":
+ return "Live";
+ case "connecting":
+ return "Connecting...";
+ case "reconnecting":
+ return "Reconnecting...";
+ case "disconnected":
+ return "Disconnected";
+ case "error":
+ return "Error";
+ default:
+ return "Idle";
+ }
+ };
+
+ const socketStatusColor = () => {
+ switch (socketStatus) {
+ case "connected":
+ return "text-emerald-400";
+ case "connecting":
+ case "reconnecting":
+ return "text-yellow-400";
+ default:
+ return "text-red-400";
+ }
+ };
+
+ const movePairs = moveHistory.reduce(
+ (acc: string[][], move: string, i: number) => {
+ if (i % 2 === 0) acc.push([move]);
+ else acc[acc.length - 1].push(move);
+ return acc;
+ },
+ [],
+ );
+
+ let overlayResult: GameResult | null = null;
+ if (gameStatus === "checkmate") {
+ overlayResult = game.turn() === "b" ? "white_wins" : "black_wins";
+ } else if (gameStatus === "stalemate") {
+ overlayResult = "stalemate";
+ } else if (gameStatus === "draw") {
+ overlayResult = "draw";
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Opponent
+
+ {playerColor === "white" ? "Black" : "White"}
+
+
+
+
+
+
+ {formatTime(playerColor === "white" ? blackTime : whiteTime)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
You
+
+ {playerColor}
+ {isMyTurn && (
+
+ (Your turn)
+
+ )}
+
+
+
+
+
+
+ {formatTime(playerColor === "white" ? whiteTime : blackTime)}
+
+
+
+
+
+
+
+
+
+
+ {socketStatusLabel()}
+
+
+
+
+
+
+
+
+
+ Game Status
+
+
+
+
+ {socketStatusLabel()}
+
+
+
+
+ {gameStatus !== "playing" && (
+
+
+ {gameStatus === "checkmate" && "Checkmate!"}
+ {gameStatus === "stalemate" && "Stalemate!"}
+ {gameStatus === "draw" && "Draw!"}
+ {gameStatus === "resigned" && "Resigned!"}
+
+
+ )}
+
+ {game.isCheck() && gameStatus === "playing" && (
+
+ )}
+
+
+ Game ID: {gameId?.slice(0, 12)}...
+
+
+
+
+
+
+ {movePairs.length === 0 ? (
+
+ No moves yet.{" "}
+ {isMyTurn ? "Your turn to move!" : "Waiting for opponent..."}
+
+ ) : (
+ movePairs.map((pair, i) => (
+
+
+ {i + 1}.
+
+
+ {pair[0]}
+
+
+ {pair[1] ?? ""}
+
+
+ ))
+ )}
+
+
+
+
setIsCheatPanelExpanded((prev: boolean) => !prev)}
+ />
+
+
+
+
+
+
+ {socketStatus === "disconnected" && gameStatus === "playing" && (
+
+ )}
+
+
+
+
+ {overlayResult && (
+
router.push("/")}
+ onPlayOnline={() => router.push("/")}
+ />
+ )}
+
+ );
+}
diff --git a/frontend/components/route.md' b/frontend/components/route.md'
deleted file mode 100644
index 0fe453d..0000000
--- a/frontend/components/route.md'
+++ /dev/null
@@ -1,440 +0,0 @@
-# Route
-
-## Features Implemented
-
-This document summarizes the Route of the requested AI/Infra features for the KnightVerse chess platform.
-
----
-
-## ✅ 1. Natural Language Agent Interface
-
-### Overview
-A conversational interface that allows users to interact with chess engines using plain English, providing intelligent analysis, suggestions, and explanations.
-
-### Files Created
-
-#### Core Modules
-- **`agent-engines/gpu_worker/nl_models.py`** (119 lines)
- - Data models for NL requests and responses
- - Intent type enumeration (7 intent types)
- - Complexity level support (beginner, intermediate, advanced)
- - Move analysis and intent recognition models
-
-- **`agent-engines/gpu_worker/nl_intent_parser.py`** (184 lines)
- - Pattern-based intent recognition system
- - Regex-based entity extraction
- - FEN string detection and parsing
- - Chess move extraction from natural language
- - Confidence scoring algorithm
-
-- **`agent-engines/gpu_worker/nl_agent.py`** (571 lines)
- - Main NaturalLanguageAgent service class
- - Intent-based request routing (7 handlers)
- - Multi-level complexity responses
- - Integration with chess engine worker pool
- - Request history tracking
- - Natural language response generation
-
-#### Tests
-- **`agent-engines/tests/test_nl_agent.py`** (428 lines)
- - Unit tests for all models
- - Intent recognition tests (20+ test cases)
- - Complexity detection tests
- - Entity extraction tests
- - Full agent workflow tests
- - Edge case coverage
-
-### Features
-- ✅ Intent Recognition: 7 distinct intent types
- - Analyze position
- - Suggest move
- - Explain move
- - Get hint
- - Compare moves
- - Learn concept
- - Unknown intent handling
-
-- ✅ Complexity Levels
- - Beginner: Simple, jargon-free explanations
- - Intermediate: Balanced technical detail
- - Advanced: Full engine output with variations
-
-- ✅ Entity Extraction
- - FEN string detection
- - Algebraic notation move extraction
- - Context preservation
-
-- ✅ Response Generation
- - Position evaluations in natural language
- - Move suggestions with reasoning
- - Tactical and strategic hints
- - Concept explanations (forks, pins, skewers)
-
-### Usage Example
-```python
-from gpu_worker.nl_agent import NaturalLanguageAgent
-
-agent = NaturalLanguageAgent(worker_pool)
-
-# Move suggestion
-response = await agent.process_request(
- user_input="What's the best move?",
- fen="rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
-)
-print(response.natural_language_response)
-print(f"Best move: {response.best_move}")
-```
-
----
-
-## ✅ 2. CI/CD Pipeline for Soroban Deployments
-
-### Overview
-Automated deployment pipeline for Stellar Soroban smart contracts with rollback support, verification, and multi-environment deployment.
-
-### Files Created
-
-#### GitHub Actions Workflow
-- **`.github/workflows/soroban-deploy.yml`** (286 lines)
- - Multi-stage pipeline (validate → deploy → verify → notify)
- - Support for testnet and futurenet
- - Manual and automatic triggers
- - WASM validation and inspection
- - Artifact preservation (30-day retention)
- - Deployment summary generation
-
-#### Deployment Scripts
-- **`contracts/deploy_advanced.sh`** (359 lines)
- - Production-ready deployment with rollback
- - Automatic backup creation (max 10 backups)
- - Deployment verification with retry logic
- - Contract ID persistence
- - Deployment history logging
- - Multiple commands: deploy, rollback, verify, history, backups, build
-
-### Features
-- ✅ Automated Testing & Validation
- - Contract unit tests
- - WASM file validation
- - Soroban contract inspection
-
-- ✅ Multi-Environment Support
- - Testnet (automatic on main branch)
- - Futurenet (manual trigger)
- - Network configuration management
-
-- ✅ Rollback Mechanism
- - Automatic backups before deployment
- - One-command rollback to previous version
- - Backup cleanup (keeps last 10)
- - Deployment history tracking
-
-- ✅ Verification System
- - Post-deployment health checks
- - Contract accessibility verification
- - Retry logic (5 attempts)
- - Automatic rollback on verification failure
-
-- ✅ CI/CD Integration
- - GitHub Actions workflow
- - Secret management for deployer keys
- - Environment protection rules
- - Deployment notifications
-
-### Usage Examples
-
-#### Automated Deployment (GitHub Actions)
-```yaml
-# Triggers automatically on push to main
-push:
- branches: [main]
- paths: ['contracts/**']
-
-# Or manual trigger
-workflow_dispatch:
- inputs:
- environment:
- type: choice
- options: [testnet, futurenet]
-```
-
-#### Manual Deployment
-```bash
-# Deploy all contracts
-./deploy_advanced.sh deploy testnet
-
-# Deploy specific contract
-./deploy_advanced.sh deploy testnet game_contract testnet-deployer
-
-# Rollback
-./deploy_advanced.sh rollback testnet game_contract
-
-# Verify
-./deploy_advanced.sh verify testnet game_contract
-
-# History
-./deploy_advanced.sh history
-```
-
-### Required GitHub Secrets
-- `SOROBAN_SECRET_KEY`: Testnet deployer key
-- `SOROBAN_FUTURENET_SECRET_KEY`: Futurenet deployer key
-
----
-
-## ✅ 3. Stockfish 16.1 Integration via WASM
-
-### Overview
-WebAssembly-based Stockfish integration enabling browser-compatible chess engine analysis without server dependencies.
-
-### Files Created
-
-#### Core Modules
-- **`agent-engines/gpu_worker/stockfish_wasm.py`** (408 lines)
- - StockfishWASMEngine class
- - WASM engine configuration
- - Async analysis interface
- - Concurrent position analysis
- - JavaScript bridge code generator
- - WASM download information
-
-- **`agent-engines/gpu_worker/stockfish_wasm_bridge.py`** (389 lines)
- - TypeScript bridge code generator
- - React hook (useStockfishWASM)
- - Analysis display component
- - Engine status indicator
-
-#### Frontend Component
-- **`frontend/components/chess/StockfishWASM.tsx`** (358 lines)
- - React TypeScript component
- - useStockfishWASM hook
- - AnalysisDisplay component
- - EngineStatus component
- - Web Worker integration
- - Error handling and timeouts
-
-#### Tests
-- **`agent-engines/tests/test_stockfish_wasm.py`** (381 lines)
- - Configuration model tests
- - Analysis result tests
- - Engine lifecycle tests
- - Concurrent analysis tests
- - Error handling tests
- - Resource cleanup tests
-
-### Features
-- ✅ WASM Engine Integration
- - Stockfish 16.1 support
- - WebAssembly loading and initialization
- - Configurable threads and hash size
- - Skill level adjustment (0-20)
-
-- ✅ Analysis Capabilities
- - Single position analysis
- - Multiple concurrent analyses
- - Configurable depth and time limits
- - Principal variation extraction
- - Evaluation scoring
-
-- ✅ Browser Compatibility
- - Web Worker integration
- - Shared Array Buffer support
- - Memory management
- - Graceful error handling
-
-- ✅ React Integration
- - Custom hook (useStockfishWASM)
- - Status indicators
- - Analysis display components
- - Loading states
- - Error boundaries
-
-- ✅ Resource Management
- - Proper cleanup on shutdown
- - Timeout handling
- - Worker termination
- - Memory limit enforcement
-
-### Usage Examples
-
-#### Python Backend
-```python
-from gpu_worker.stockfish_wasm import StockfishWASMEngine, WASMEngineConfig
-
-engine = StockfishWASMEngine(
- WASMEngineConfig(threads=2, hash_size_mb=32)
-)
-
-await engine.initialize()
-
-result = await engine.analyze_position(
- fen="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
- depth=20
-)
-
-print(f"Best move: {result.best_move}")
-print(f"Evaluation: {result.evaluation}")
-
-await engine.shutdown()
-```
-
-#### React Frontend
-```typescript
-import { useStockfishWASM, AnalysisDisplay } from '@/components/chess/StockfishWASM';
-
-function ChessPanel() {
- const { isReady, analyzePosition } = useStockfishWASM({
- defaultDepth: 18,
- threads: 2,
- });
-
- const handleAnalyze = async () => {
- const result = await analyzePosition(fen);
- console.log('Best:', result.bestMove);
- };
-
- return ;
-}
-```
-
-### WASM File Setup
-```bash
-# Download Stockfish WASM
-# Source: https://github.com/nmrugg/stockfish.js
-
-# Place in public directory
-cp stockfish.js frontend/public/assets/
-cp stockfish.wasm frontend/public/assets/
-```
-
----
-
-## Testing
-
-### Run All Tests
-```bash
-cd agent-engines
-
-# Run all tests
-pytest
-
-# Natural Language Agent tests
-pytest tests/test_nl_agent.py -v
-
-# Stockfish WASM tests
-pytest tests/test_stockfish_wasm.py -v
-
-# With coverage
-pytest --cov=gpu_worker --cov-report=html
-```
-
-### Test Coverage
-- ✅ Natural Language Agent: 25+ test cases
-- ✅ Intent Parser: 15+ test cases
-- ✅ Stockfish WASM: 20+ test cases
-- ✅ Edge cases and error handling
-- ✅ Concurrent operations
-- ✅ Resource cleanup
-
----
-
-## Documentation
-
-### Updated Files
-- **`agent-engines/README.md`**
- - Added Natural Language Agent section
- - Added Stockfish WASM section
- - Added Soroban CI/CD section
- - Usage examples for all features
- - Testing instructions
-
-### Documentation Quality
-- ✅ Comprehensive API documentation
-- ✅ Code comments and docstrings
-- ✅ Usage examples
-- ✅ Installation instructions
-- ✅ Configuration guides
-
----
-
-## Acceptance Criteria Checklist
-
-### Code Quality
-- ✅ Well-documented code with docstrings
-- ✅ Follows existing design patterns
-- ✅ Type hints throughout
-- ✅ Clean code structure
-- ✅ Error handling
-
-### Testing
-- ✅ Unit tests for all new modules
-- ✅ Edge case coverage
-- ✅ Integration tests
-- ✅ Async test support
-- ✅ Mocked external dependencies
-
-### Integration
-- ✅ Fully integrated with existing codebase
-- ✅ Compatible with cargo test (Rust backend)
-- ✅ Compatible with pytest (Python agents)
-- ✅ Frontend components ready for npm
-- ✅ CI/CD workflows functional
-
-### Resource Efficiency
-- ✅ Configurable resource limits
-- ✅ Proper cleanup and shutdown
-- ✅ Concurrent operation support
-- ✅ Memory management
-- ✅ Timeout handling
-
----
-
-## File Summary
-
-### New Files Created: 10
-1. `agent-engines/gpu_worker/nl_models.py` - NL agent data models
-2. `agent-engines/gpu_worker/nl_intent_parser.py` - Intent recognition
-3. `agent-engines/gpu_worker/nl_agent.py` - NL agent service
-4. `agent-engines/gpu_worker/stockfish_wasm.py` - WASM engine integration
-5. `agent-engines/gpu_worker/stockfish_wasm_bridge.py` - TypeScript generator
-6. `agent-engines/tests/test_nl_agent.py` - NL agent tests
-7. `agent-engines/tests/test_stockfish_wasm.py` - WASM tests
-8. `.github/workflows/soroban-deploy.yml` - CI/CD pipeline
-9. `contracts/deploy_advanced.sh` - Deployment script
-10. `frontend/components/chess/StockfishWASM.tsx` - React component
-
-### Modified Files: 1
-1. `agent-engines/README.md` - Updated documentation
-
-### Total Lines Added: ~3,500+
-- Python: ~2,400 lines
-- TypeScript: ~700 lines
-- YAML/Shell: ~650 lines
-- Documentation: ~300 lines
-
----
-
-## Next Steps
-
-### Deployment
-1. Set up GitHub secrets for Soroban deployment
-2. Configure GitHub environments (testnet, futurenet)
-3. Download Stockfish WASM files for frontend
-4. Run test suite to verify all integrations
-
-### Optional Enhancements
-- Add LLM integration for enhanced NL responses
-- Implement real-time analysis streaming
-- Add puzzle generation using WASM engine
-- Create deployment dashboard
-- Add performance benchmarks
-
----
-
-## Support
-
-For questions or issues:
-- Check the updated README.md in agent-engines/
-- Review test files for usage examples
-- Consult inline code documentation
-- Refer to CI/CD workflow files for deployment details
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 80b1a6d..9d20b5b 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -191,6 +191,7 @@
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -2035,6 +2036,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -2058,35 +2060,11 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
},
- "node_modules/@emnapi/core": {
- "version": "2.0.0-alpha.3",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
- "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@emnapi/wasi-threads": "2.0.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "2.0.0-alpha.3",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
- "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@emnapi/wasi-threads": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
@@ -2094,7 +2072,6 @@
"dev": true,
"license": "MIT",
"optional": true,
- "peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -2986,6 +2963,7 @@
"integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"playwright": "1.62.0"
},
@@ -3438,9 +3416,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3458,9 +3433,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3478,9 +3450,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3498,9 +3467,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3518,9 +3484,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3538,9 +3501,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -3862,6 +3822,7 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -4471,6 +4432,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -4489,6 +4451,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz",
"integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4499,6 +4462,7 @@
"integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4572,6 +4536,7 @@
"integrity": "sha512-XGwIabPallYipmcOk45DpsBSgLC64A0yvdAkrwEzwZ2viqGqRUJ8eEYoPz0CWnutgAFbNMPdsGGvzjSmcWVlEA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.27.0",
"@typescript-eslint/types": "8.27.0",
@@ -5269,6 +5234,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5803,6 +5769,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -6998,6 +6965,7 @@
"integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7172,6 +7140,7 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -8656,6 +8625,7 @@
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"mdn-data": "2.27.1",
"source-map-js": "^1.2.1"
@@ -9845,6 +9815,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -10017,6 +9988,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10119,6 +10091,7 @@
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.6.0.tgz",
"integrity": "sha512-2KHNpeg2SyaxXYq+xO1TM+tOtN9hViI41otJuiYiu6DRYGw+WMvDFDMP4aw7zIKRRm1xd0gizXuKWhb8iJYHBw==",
"license": "BSD-3-Clause",
+ "peer": true,
"dependencies": {
"disposables": "^1.0.1",
"dnd-core": "^2.6.0",
@@ -10209,6 +10182,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
@@ -10229,7 +10203,8 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-remove-scroll": {
"version": "2.6.3",
@@ -10387,6 +10362,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -10409,7 +10385,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/recharts/node_modules/redux-thunk": {
"version": "3.1.0",
@@ -10830,6 +10807,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -11543,7 +11521,8 @@
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.15.tgz",
"integrity": "sha512-6ZMg+hHdMJpjpeCCFasX7K+U615U9D+7k5/cDK/iRwl6GptF24+I/AbKgOnXhVKePzrEyIXutLv36n4cRsq3Sg==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -11689,6 +11668,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -11928,6 +11908,7 @@
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -12151,6 +12132,7 @@
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
@@ -12345,9 +12327,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12369,9 +12348,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12393,9 +12369,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -12417,9 +12390,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [