Skip to main content
Glama
index.tsx•9.71 kB
/** * Chess Board Component for ChatGPT MCP * Refactored to use OpenAI Apps SDK patterns */ import React, { useEffect, useState, useMemo } from "react"; import { createRoot } from "react-dom/client"; import { Chess } from "chess.js"; import { Chessboard } from "react-chessboard"; import { useOpenAiGlobal } from "../use-openai-global"; import { useToolOutput, useToolResponseMetadata } from "../use-widget-props"; import { useWidgetState } from "../use-widget-state"; import type { ChessToolOutput, ChessMetadata, ChessWidgetState, } from "../types"; const ChessBoardWidget: React.FC = () => { // Use hooks for window.openai access const theme = useOpenAiGlobal("theme") || "light"; const toolOutput = useToolOutput<ChessToolOutput>(); const toolResponseMetadata = useToolResponseMetadata<ChessMetadata>(); // Widget state for persistent preferences const [widgetState, setWidgetState] = useWidgetState<ChessWidgetState>({ lastDepth: 15, analysisVisible: false, }); // Local component state const [position, setPosition] = useState<string>("start"); const [moveHistory, setMoveHistory] = useState<string[]>([]); const [gameStatus, setGameStatus] = useState<string>("ongoing"); const [currentTurn, setCurrentTurn] = useState<string>("white"); const [analysis, setAnalysis] = useState<string | null>(null); const [isAnalyzing, setIsAnalyzing] = useState(false); // Chess instance for local validation const chess = useMemo(() => new Chess(), []); // Update board when tool output changes useEffect(() => { if (toolOutput) { if (toolOutput.fen) { setPosition(toolOutput.fen); chess.load(toolOutput.fen); } if (toolOutput.status) { setGameStatus(toolOutput.status); } if (toolOutput.turn) { setCurrentTurn(toolOutput.turn); } } }, [toolOutput, chess]); // Update move history from metadata useEffect(() => { if (toolResponseMetadata?.move_history_list) { setMoveHistory(toolResponseMetadata.move_history_list); } }, [toolResponseMetadata]); // Request Stockfish analysis const handleStockfishAnalysis = async () => { if (!window.openai?.callTool) { console.error("window.openai.callTool not available"); return; } setIsAnalyzing(true); setAnalysis(null); try { const depth = widgetState?.lastDepth || 15; const result = await window.openai.callTool("chess_stockfish", { depth }); if (result?.content?.[0]?.text) { setAnalysis(result.content[0].text); } else if (result?.structuredContent?.best_move) { const move = result.structuredContent.best_move; const eval_text = result.structuredContent.evaluation; setAnalysis(`Best move: ${move} (${eval_text})`); } // Update widget state setWidgetState({ ...widgetState, analysisVisible: true }); } catch (error) { console.error("Error calling Stockfish:", error); setAnalysis("Error analyzing position"); } finally { setIsAnalyzing(false); } }; // Format move history for display const formattedMoveHistory = useMemo(() => { const formatted: string[] = []; for (let i = 0; i < moveHistory.length; i += 2) { const moveNum = Math.floor(i / 2) + 1; const whiteMove = moveHistory[i]; const blackMove = moveHistory[i + 1]; if (blackMove) { formatted.push(`${moveNum}. ${whiteMove} ${blackMove}`); } else { formatted.push(`${moveNum}. ${whiteMove}`); } } return formatted; }, [moveHistory]); // Get status display text const getStatusText = () => { switch (gameStatus) { case "checkmate": return `Checkmate! ${currentTurn === "white" ? "Black" : "White"} wins`; case "stalemate": return "Stalemate - Draw"; case "check": return `Check! ${currentTurn === "white" ? "White" : "Black"} to move`; case "draw_insufficient_material": return "Draw - Insufficient material"; case "puzzle": return "Puzzle: Find the mate in 1!"; default: return `${currentTurn === "white" ? "White" : "Black"} to move`; } }; // Determine board orientation const boardOrientation = "white"; // Board colors based on theme const darkSquareColor = "#b58863"; const lightSquareColor = "#f0d9b5"; return ( <div style={{ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', padding: "20px", maxWidth: "600px", margin: "0 auto", backgroundColor: theme === "dark" ? "#1a1a1a" : "#ffffff", color: theme === "dark" ? "#ffffff" : "#000000", borderRadius: "8px", }} > {/* Game Status */} <div style={{ textAlign: "center", fontSize: "18px", fontWeight: "600", marginBottom: "16px", padding: "12px", backgroundColor: theme === "dark" ? "#2a2a2a" : "#f5f5f5", borderRadius: "6px", }} > {getStatusText()} </div> {/* Chess Board */} <div style={{ marginBottom: "20px" }}> <Chessboard position={position} boardOrientation={boardOrientation} customDarkSquareStyle={{ backgroundColor: darkSquareColor }} customLightSquareStyle={{ backgroundColor: lightSquareColor }} arePiecesDraggable={false} boardWidth={Math.min(560, window.innerWidth - 80)} /> </div> {/* Stockfish Analysis Button */} <div style={{ marginBottom: "20px", textAlign: "center" }}> <button onClick={handleStockfishAnalysis} disabled={ isAnalyzing || gameStatus === "checkmate" || gameStatus === "stalemate" } style={{ padding: "12px 24px", fontSize: "16px", fontWeight: "600", backgroundColor: theme === "dark" ? "#4a9eff" : "#0066cc", color: "#ffffff", border: "none", borderRadius: "6px", cursor: isAnalyzing ? "wait" : "pointer", opacity: isAnalyzing || gameStatus === "checkmate" || gameStatus === "stalemate" ? 0.6 : 1, transition: "all 0.2s", }} onMouseOver={(e) => { if ( !isAnalyzing && gameStatus !== "checkmate" && gameStatus !== "stalemate" ) { e.currentTarget.style.backgroundColor = theme === "dark" ? "#5aafff" : "#0052a3"; } }} onMouseOut={(e) => { e.currentTarget.style.backgroundColor = theme === "dark" ? "#4a9eff" : "#0066cc"; }} > {isAnalyzing ? "Analyzing..." : "Ask Stockfish"} </button> </div> {/* Analysis Result */} {analysis && ( <div style={{ padding: "12px", backgroundColor: theme === "dark" ? "#2a4a2a" : "#e8f5e9", borderRadius: "6px", marginBottom: "20px", fontSize: "14px", }} > <strong>Engine Analysis:</strong> {analysis} </div> )} {/* Move History */} {moveHistory.length > 0 && ( <div style={{ marginTop: "20px", padding: "16px", backgroundColor: theme === "dark" ? "#2a2a2a" : "#f5f5f5", borderRadius: "6px", }} > <h3 style={{ margin: "0 0 12px 0", fontSize: "16px", fontWeight: "600", }} > Move History </h3> <div style={{ fontSize: "14px", lineHeight: "1.6", maxHeight: "150px", overflowY: "auto", fontFamily: "monospace", }} > {formattedMoveHistory.map((move, index) => ( <div key={index} style={{ padding: "4px 8px", backgroundColor: index === formattedMoveHistory.length - 1 ? theme === "dark" ? "#3a3a3a" : "#e0e0e0" : "transparent", borderRadius: "4px", }} > {move} </div> ))} </div> </div> )} {/* Instructions */} <div style={{ marginTop: "20px", padding: "12px", backgroundColor: theme === "dark" ? "#2a2a3a" : "#f0f4ff", borderRadius: "6px", fontSize: "13px", lineHeight: "1.5", }} > <strong>How to play:</strong> <ul style={{ margin: "8px 0 0 0", paddingLeft: "20px" }}> <li>Type your move in chat (e.g., "e4", "Nf3", "O-O")</li> <li>ChatGPT can suggest the next move</li> <li>Click "Ask Stockfish" for engine analysis</li> <li>Use "chess_status" to check game info</li> <li>Use "chess_puzzle" for tactical training</li> </ul> </div> </div> ); }; // Initialize the widget when the DOM is ready if (typeof document !== "undefined") { const rootElement = document.getElementById("chess-board-root"); if (rootElement) { const root = createRoot(rootElement); root.render(<ChessBoardWidget />); } } export default ChessBoardWidget;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GeneralJerel/ChessMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server