diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 72e9aa42..00000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -Dockerfile -.dockerignore -node_modules -npm-debug.log -README.md -.next -.git \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 340cc708..7ff09b78 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,13 +19,21 @@ "react/react-in-jsx-scope": "off", "react-hooks/exhaustive-deps": "off", "@typescript-eslint/no-unused-vars": "off", + "@next/next/no-html-link-for-pages": "off", "import/no-named-as-default": "off", + "@next/next/no-img-element": "off", + "@next/next/no-page-custom-font": "off", + "@next/next/no-duplicate-head": "off", + "@next/next/no-typos": "off", + "@next/next/no-before-interactive-script-outside-document": "off", + "@next/next/no-styled-jsx-in-document": "off", + "@next/next/no-head-import-in-document": "off", }, "overrides": [ { "files": ["*.js"], "rules": { - "typescript/no-var-requires": "off", + "@typescript-eslint/no-var-requires": "off", }, }, ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 210420a8..3789cca2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,19 +11,19 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 - - run: yarn - - run: yarn lint + - run: npm i + - run: npm run lint build: runs-on: ubuntu-latest strategy: matrix: - node: ['18', '20'] + node: ['18'] name: Node ${{ matrix.node }} Build steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} - - run: yarn - - run: yarn build + - run: npm i + - run: npm run build diff --git a/.gitignore b/.gitignore index 88b6f0d9..373c2c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +.env # dependencies /node_modules diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b1ddb04d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md - AI Assistant Guide + +## Project Overview + +This is the Maia Chess Platform Frontend - a sophisticated chess analysis and training application featuring human-like chess AI (Maia) alongside traditional Stockfish engine capabilities. + +## Technology Stack + +- **Next.js 15.1.6** with App Router +- **React 18.3.1** with TypeScript 5.1.6 +- **Tailwind CSS 3.4.10** for styling +- **Stockfish WebAssembly** + **ONNX Runtime Web** for chess engines +- **Framer Motion** for animations +- **Recharts** for data visualization + +## Essential Commands + +### Development + +```bash +npm run dev # Start development server on localhost:3000 +npm run build # Build for production +npm run start # Start production server +npm run lint # Run ESLint (ALWAYS run before committing) +npx tsc --noEmit # Check TypeScript errors +``` + +## Project Structure + +``` +src/ +├── api/ # API client functions organized by feature +├── components/ # UI components organized by domain +├── contexts/ # React Context providers +├── hooks/ # Custom React hooks (business logic) +├── pages/ # Next.js pages/routes +├── providers/ # Context provider wrappers +├── styles/ # Global styles and Tailwind config +├── types/ # TypeScript type definitions +├── utils/ # Utility functions +└── workers/ # Web Worker files +``` + +## Architecture Patterns + +### State Management + +- **Controller Hooks** for business logic (useAnalysisController, usePlayController, etc.) +- **React Context** for global state (AuthContext, ThemeContext, ModalContext) +- **Presentational Components** that receive data via props + +### Component Organization + +- Organized by feature domain +- Barrel exports (index.ts files) for clean imports +- Absolute imports from `src/` (configured in tsconfig.json) + +## Code Conventions + +### TypeScript + +- Strict TypeScript configuration +- Interface-based type definitions in `src/types/` +- Proper typing for all components and hooks + +### Component Pattern + +```typescript +interface Props { + // Props definition +} + +export const ComponentName = (props: Props) => { + // Component implementation +} +``` + +### Hook Pattern + +```typescript +export const useFeatureController = () => { + // State and logic + return { + // Return interface + } +} +``` + +## Chess Engine Integration + +### Stockfish Engine + +- WebAssembly integration via `lila-stockfish-web` +- Files in `public/stockfish/` +- Managed through `useStockfishEngine` hook + +### Maia Engine + +- ONNX neural network model +- Client-side inference using `onnxruntime-web` +- Multiple model variants (1100-1900 rating levels) +- Weights in `public/maia2/` + +## API Structure + +- Backend proxy to `https://dock2.csslab.ca/api/` +- Feature-based API organization in `src/api/` +- Type-safe API client functions +- Base URL: `/api/v1/` + +## Theme System + +- Responsive design with custom breakpoints with breakpoints: 3xl (1920px), 4xl (2560px) (and also mobile) + +## Key Files for Understanding + +1. `src/hooks/useAnalysisController/` - Core analysis logic +2. `src/components/Analysis/` - Analysis UI components +3. `src/types/` - Type definitions +4. `src/api/` - API client functions +5. `src/pages/` - Route definitions +6. `tailwind.config.js` - Styling configuration +7. `next.config.js` - Build configuration + +## Development Workflow + +- Feature branches for development +- ESLint and Prettier for code quality +- **IMPORTANT**: Always run `npm run lint` before committing +- Vercel integration for deployments diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 861a87c4..00000000 --- a/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM node:16-alpine AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app -COPY package.json yarn.lock ./ -RUN yarn install --frozen-lockfile - -FROM node:16-alpine AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN yarn build - -FROM node:16-alpine AS runner -WORKDIR /app - -ENV NODE_ENV production -ENV NEXT_TELEMETRY_DISABLED 1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -COPY --from=builder /app/public ./public -COPY --from=builder /app/package.json ./package.json - -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT 3000 - -CMD ["node", "server.js"] \ No newline at end of file diff --git a/README.md b/README.md index 1638bd8a..ea8a686b 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,208 @@ -# maia platform +

+

+ Logo +

+

Maia Chess

+

+ A human-like chess engine. +
+ maiachess.com » +
+
+

+

-This is the code that powers the[ Maia Chess platform](https://www.maiachess.com). - -It is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with the additions of [`typescript`](https://www.typescriptlang.org/), [`eslint`](https://eslint.org/), [`storybook`](https://storybook.js.org/), [`prettier`](https://prettier.io/), [`sass`](https://sass-lang.com/), [`testing-library`](https://testing-library.com/) ,and [`jest`](https://jestjs.io/). +This repository contains the source code for the [Maia Chess Platform](https://www.maiachess.com), a modern web application designed for chess training and analysis. It leverages the Maia chess engine, developed by the University of Toronto's Computational Social Science Lab, to provide human-like move predictions and insights. The platform is built with Next.js, TypeScript, and Tailwind CSS. Join our [Discord server](https://discord.gg/hHb6gqFpxZ) for discussions, questions, and to connect with the community. -Initialize a new `next-app` with this project as a template by running +## Getting Started + +Follow these instructions to set up the development environment on your local machine. + +### Prerequisites + +- Node.js (v17+ recommended) +- npm (comes bundled with Node.js) + +### Installation + +1. Clone the repository to your local machine: + + ```bash + git clone https://github.com/maia-chess/maia-platform-frontend.git + cd maia-platform-frontend + ``` + +2. Install the project dependencies using npm: + ```bash + npm install + ``` + +### Running the Development Server + +To start the local development server, run the following command. This will launch the application on `http://localhost:3000` with hot-reloading enabled. + +```bash +npm run dev +``` + +### Building for Production + +To create a production-ready build of the application, use the following command. This will compile and optimize the code, outputting the final assets to the `.next` directory. ```bash -npx create next-app --example https://github.com/datadeque/next-app -# or -yarn create next-app --example https://github.com/datadeque/next-app +npm run build ``` +You can then start the production server with `npm run start`. + ## Development Guide -This project uses [`yarn`](https://yarnpkg.com/) and is developed & maintained for [`node`](https://nodejs.org/en/) version 17+ (however CI will try builds using versions 12, 14, and 16 as well). +This section provides guidelines for contributing to the platform's development. -The recommended code editor is [`vscode`](https://code.visualstudio.com/) along with the following extensions (see `.vscode/extensions.json`): +### Branching Strategy -- eslint (Linter) -- prettier (Formatter) -- mdx (Syntax Highlighting for Storybook) +The repository follows a simple branching model: -These extensions are highly recommended along with vscode because the project is pre-configured to format and fix ALL fixable issues on save. Furthermore, please open this project by running `code .` in the root directory from your terminal since there are known issues with environment variables when opening the project from gui. +- `main`: This branch is synced with the live deployment on Vercel. All code on this branch is considered production-ready. +- **Feature Branches**: All development work, including new features and bug fixes, should be done on separate feature branches. These branches are then merged into `main` via pull requests. -It also helps to know the following tools: +### Conventional Commits -- [`sass`](https://sass-lang.com/) -- [`react contexts`](https://reactjs.org/docs/context.html) -- [`react custom hooks`](https://reactjs.org/docs/hooks-custom.html) +We use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for our commit messages. This standard creates a more readable and structured commit history. Each commit message should follow the format: -### Design +`{type}: {description}` -Each page of the platform can be found in `src/pages`. Inside, a wrapper component fetches the data from the api and renders the page itself when the data is received. An example of this is in the `train` page where `TrainPage` fetches the game, and renders `Train`. +Common types include: -We'll refer the component rendered by the wrapper as the main component. Inside the main component a custom hook is used to isolate all the logic, and two variables `mobileLayout` and `desktopLayout` are initialized to represent the corresponding layouts. +- `feat`: A new feature +- `fix`: A bug fix +- `chore`: Changes to the build process or auxiliary tools +- `style`: Code style changes (formatting, etc.) +- `refactor`: A code change that neither fixes a bug nor adds a feature +- `docs`: Documentation only changes -The `GameControllerContext` is used to simply pass data down, as any child component of the main component can easily consume the controller without passing it down multiple layers using props. +### Architectural Overview -Each main component typically contains a `GameBoard` component which renders the lichess chessboard UI, and a family of surrounding components that are either interactive or show information such as analysis. +The platform is architected around a modular and scalable structure, leveraging modern React patterns. -### Getting Started +#### File Structure -First, install the dependencies by running +The `src/` directory contains all the core application code, organized as follows: -```bash -yarn -# or -yarn install +``` +src/ +├── api/ # Backend API client functions, organized by feature +├── components/ # Reusable React components, structured by feature or domain +├── contexts/ # React Context providers for global state management +├── hooks/ # Custom React Hooks containing business logic and state +├── pages/ # Next.js pages, defining the application's routes +├── providers/ # Wrappers for context providers +├── styles/ # Global styles and Tailwind CSS configuration +├── types/ # TypeScript type definitions, organized by feature +└── utils/ # Utility functions and helpers ``` -Then start the development server by running +#### Core Concepts and Interactions -```bash -yarn dev -``` +The application's logic is primarily driven by a combination of custom hooks, React contexts, and components, creating a clear separation of concerns. -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +- **Components (`src/components/`)**: These are the building blocks of the UI. They are designed to be "dumb" or presentational, receiving data and callbacks via props. Major features like `Analysis`, `Play`, `Openings`, and `Training` have their own dedicated component directories. -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. +- **Hooks (`src/hooks/`)**: This is where the majority of the application's business logic resides. Each major feature has a corresponding "controller" hook (e.g., `usePlayController`, `useAnalysisController`). These hooks encapsulate state management, interactions with the chess engines, and API calls. They effectively act as state machines for their respective features. -### Storybook +- **Contexts (`src/contexts/`)**: To avoid prop drilling, we use React Context to provide the state and methods from our controller hooks to the component tree. For example, `PlayControllerContext` will expose the state and functions from the `usePlayController` hook to any child component that needs it, such as the `GameBoard` or `PlayControls`. -To start storybook development server by running +This architecture allows for a decoupled system where the UI (components) is a function of the state managed by the hooks, and the state is shared efficiently through contexts. For example, a page component under `src/pages` will initialize a controller hook. That hook's state is then provided to the component tree via a Context Provider. Child components can then consume that context to access state and dispatch actions without passing props down multiple levels. -```bash -yarn storybook -``` +#### Learning Resources -Open http://localhost:6006/ with your browser and see the result. +To better understand the patterns used in this codebase, we recommend reviewing the official documentation for these core React and Next.js features: -### Linting & Testing +- [React Hooks](https://react.dev/reference/react/hooks) +- [React Context](https://react.dev/reference/react/createContext) +- [Next.js App Router](https://nextjs.org/docs) -You can lint the entire project using the built-in `eslint` config by running +### Client-Side Chess Engines -```bash -yarn lint -``` +A key feature of the platform is its ability to run both Stockfish and Maia directly in the user's browser. This is accomplished using WebAssembly and ONNX Runtime Web. -This is the same command that runs during the lint step of the initial CI. +- **Stockfish (`src/hooks/useStockfishEngine/`)**: We use a WebAssembly (WASM) version of Stockfish for standard chess analysis. The `useStockfishEngine` hook provides a simple interface to interact with the engine, allowing for move evaluation streams. This provides the "objective" best moves in any given position. -You can run your test files by running +- **Maia (`src/hooks/useMaiaEngine/`)**: The Maia engine is a neural network provided as an ONNX (Open Neural Network Exchange) model. We use the `onnxruntime-web` library to load and run Maia models on the client-side. The `useMaiaEngine` hook manages the download, initialization, and execution of the various Maia models (e.g., `maia_kdd_1100` to `maia_kdd_1900`). This engine provides the "human-like" move predictions that are central to the platform's mission. -```bash -yarn test -``` +These hooks are consumed by higher-level controller hooks (like `useAnalysisController`) to provide the dual-engine analysis that powers many of the platform's features. -This is the same command that runs during the test step of the initial CI. +## Contributing Guidelines -## Development Practices +Before contributing to the Maia Chess Platform, please review these guidelines to ensure consistency and quality. -### Committing +### Code Style and Formatting -Conventional commits is a pretty simple convention, you can learm more about it [here](https://www.conventionalcommits.org/en/v1.0.0/). +The project uses automated code formatting and linting to maintain consistency: -Basically commit messages follow the following format: `{action}: {description}`, where action is one of `feat`, `chore`, `fix`, `style`... and description is description of the change in present tense. +- **ESLint**: Configured with Next.js, TypeScript, and Prettier integration +- **Prettier**: Enforces consistent formatting with the following settings: + - No semicolons (`semi: false`) + - Single quotes (`singleQuote: true`) + - 2-space indentation (`tabWidth: 2`) + - Automatic Tailwind CSS class sorting -### Testing +### Development Workflow -Every react component should atleast have one test, to see if it renders. Test files are to be placed in `__test__` and follow the same file structure as the project root. +1. **Setup**: Install recommended VS Code extensions for optimal development experience: -Suppose you built a new component under `src/components/button/button.tsx`, a test should be placed in `__tests__/components/button/button.test.tsx` with the following: + - `dbaeumer.vscode-eslint` + - `esbenp.prettier-vscode` + - `silvenon.mdx` -```tsx -import { render } from '@testing-library/react' -import Button from 'components/button' +2. **Code Quality**: Run linting before committing: + ```bash + npm run lint + ``` -describe('Button', () => { - it('renders', () => { - render( + + + )} + +
+ {loading ? ( +
+
+
+ ) : ( + <> + {selected === 'tournament' ? ( + <> + {listKeys.map((id, i) => ( + + } + setOpenIndex={setOpenIndex} + loadingIndex={loadingIndex} + setLoadingIndex={setLoadingIndex} + selectedGameElement={ + selectedGameElement as React.RefObject + } + loadNewTournamentGame={loadNewTournamentGame} + analysisTournamentList={analysisTournamentList} + /> + ))} + + ) : ( + <> + {getCurrentGames().map((game, index) => { + const selectedGame = currentId && currentId[0] === game.id + return ( + + ) + })} + {(selected === 'play' || selected === 'hb') && + totalPages > 1 && ( +
+ + + + Page {currentPage} of {totalPages} + + + +
+ )} + + )} + )} - - -
- {selected === 'tournament' ? ( -
- {listKeys.map((id, i) => ( - - ))}
- ) : ( - - )} + {onCustomAnalysis && ( + + )} + ) : null } + +function Header({ + name, + label, + selected, + setSelected, +}: { + label: string + name: 'tournament' | 'play' | 'hb' | 'custom' | 'lichess' + selected: 'tournament' | 'play' | 'hb' | 'custom' | 'lichess' + setSelected: ( + name: 'tournament' | 'play' | 'hb' | 'custom' | 'lichess', + ) => void +}) { + return ( + + ) +} diff --git a/src/components/Analysis/BlunderMeter.tsx b/src/components/Analysis/BlunderMeter.tsx index e298c671..c1ad1d0e 100644 --- a/src/components/Analysis/BlunderMeter.tsx +++ b/src/components/Analysis/BlunderMeter.tsx @@ -1,58 +1,417 @@ -import React from 'react' +import React, { useContext, useState } from 'react' import { motion } from 'framer-motion' -import { Tooltip } from 'react-tooltip' + +import { BlunderMeterResult, ColorSanMapping } from 'src/types' +import { WindowSizeContext } from 'src/contexts' +import { MoveTooltip } from './MoveTooltip' interface Props { - blunderMoveChance: number - okMoveChance: number - goodMoveChance: number + data: BlunderMeterResult + colorSanMapping: ColorSanMapping + hover: (move?: string) => void + makeMove: (move: string) => void + showContainer?: boolean + moveEvaluation?: { + maia?: { policy: { [key: string]: number } } + stockfish?: { + cp_vec: { [key: string]: number } + winrate_vec?: { [key: string]: number } + winrate_loss_vec?: { [key: string]: number } + } + } | null } export const BlunderMeter: React.FC = ({ - blunderMoveChance, - okMoveChance, - goodMoveChance, + data, + hover, + makeMove, + colorSanMapping, + showContainer = true, + moveEvaluation, +}: Props) => { + const { isMobile } = useContext(WindowSizeContext) + + return isMobile ? ( + + ) : ( + + ) +} + +const DesktopBlunderMeter: React.FC = ({ + data, + hover, + makeMove, + colorSanMapping, + showContainer = true, + moveEvaluation, }: Props) => { return ( -
- -

- Good –{' '} - OK –{' '} - Blunder Move Probability -

-
- - - {Math.round(goodMoveChance)}% - - - - - {Math.round(okMoveChance)}% - - - - - {Math.round(blunderMoveChance)}% - - +
+

Blunder Meter

+
+
+ + + +
+
+
+ ) +} + +const MobileBlunderMeter: React.FC = ({ + data, + hover, + makeMove, + colorSanMapping, + showContainer, + moveEvaluation, +}: Props) => { + return ( +
+

Blunder Meter

+
+
+
+ + + +
+
+
+ + + +
+
+
+ ) +} + +function MovesList({ + title, + textColor, + moves, + hover, + makeMove, + colorSanMapping, + moveEvaluation, +}: { + title: string + textColor: string + moves: { move: string; probability: number }[] + hover: (move?: string) => void + makeMove: (move: string) => void + colorSanMapping: ColorSanMapping + moveEvaluation?: { + maia?: { policy: { [key: string]: number } } + stockfish?: { + cp_vec: { [key: string]: number } + winrate_vec?: { [key: string]: number } + winrate_loss_vec?: { [key: string]: number } + } + } | null +}) { + const [tooltipData, setTooltipData] = useState<{ + move: string + position: { x: number; y: number } + } | null>(null) + + const filteredMoves = () => { + return moves.slice(0, 6).filter((move) => move.probability >= 8) + } + + const handleMouseEnter = (move: string, event: React.MouseEvent) => { + hover(move) + setTooltipData({ + move, + position: { x: event.clientX, y: event.clientY }, + }) + } + + const handleMouseLeave = () => { + hover() + setTooltipData(null) + } + + return ( +
+

{title}

+
+ {filteredMoves().map((move) => ( + + ))}
+ + {/* Tooltip */} + {tooltipData && moveEvaluation && ( + + )}
) } + +function HorizontalMeter({ + moves, + bgColor, + title, + hover, + makeMove, + textColor, + probability, + colorSanMapping, +}: { + title: string + textColor: string + bgColor: string + probability: number + hover: (move?: string) => void + makeMove: (move: string) => void + colorSanMapping: ColorSanMapping + moves: { move: string; probability: number }[] +}) { + return ( + + + + {Math.round(probability)}% + + + + ) +} + +function Meter({ + moves, + bgColor, + title, + hover, + makeMove, + textColor, + probability, + colorSanMapping, + moveEvaluation, +}: { + title: string + textColor: string + bgColor: string + probability: number + hover: (move?: string) => void + makeMove: (move: string) => void + colorSanMapping: ColorSanMapping + moves: { move: string; probability: number }[] + moveEvaluation?: { + maia?: { policy: { [key: string]: number } } + stockfish?: { + cp_vec: { [key: string]: number } + winrate_vec?: { [key: string]: number } + winrate_loss_vec?: { [key: string]: number } + } + } | null +}) { + const [tooltipData, setTooltipData] = useState<{ + move: string + position: { x: number; y: number } + } | null>(null) + + const filteredMoves = () => { + return moves.slice(0, 6).filter((move) => move.probability >= 8) + } + + const handleMouseEnter = (move: string, event: React.MouseEvent) => { + hover(move) + setTooltipData({ + move, + position: { x: event.clientX, y: event.clientY }, + }) + } + + const handleMouseLeave = () => { + hover() + setTooltipData(null) + } + + return ( + + + + {Math.round(probability)}% + + +
+

{title}

+
+ {filteredMoves().map((move) => ( + + ))} +
+
+ + {/* Tooltip */} + {tooltipData && moveEvaluation && ( + + )} +
+ ) +} diff --git a/src/components/Analysis/ConfigurableScreens.tsx b/src/components/Analysis/ConfigurableScreens.tsx new file mode 100644 index 00000000..1eef706f --- /dev/null +++ b/src/components/Analysis/ConfigurableScreens.tsx @@ -0,0 +1,95 @@ +import { motion } from 'framer-motion' +import React, { useState } from 'react' +import { ConfigureAnalysis } from 'src/components/Analysis/ConfigureAnalysis' +import { ExportGame } from 'src/components/Misc/ExportGame' +import { AnalyzedGame, GameNode } from 'src/types' + +interface Props { + currentMaiaModel: string + setCurrentMaiaModel: (model: string) => void + launchContinue: () => void + MAIA_MODELS: string[] + game: AnalyzedGame + currentNode: GameNode + onDeleteCustomGame?: () => void +} + +export const ConfigurableScreens: React.FC = ({ + currentMaiaModel, + setCurrentMaiaModel, + launchContinue, + MAIA_MODELS, + game, + currentNode, + onDeleteCustomGame, +}) => { + const screens = [ + { + id: 'configure', + name: 'Configure', + }, + { + id: 'export', + name: 'Export', + }, + ] + + const [screen, setScreen] = useState(screens[0]) + + return ( +
+
+ {screens.map((s) => { + const selected = s.id === screen.id + return ( +
{ + if (e.key === 'Enter') setScreen(s) + }} + onClick={() => setScreen(s)} + className={`relative flex cursor-pointer select-none flex-row px-3 py-1.5 ${selected ? 'bg-white/5' : 'hover:bg-white hover:bg-opacity-[0.02]'} transition duration-200`} + > +

+ {s.name} +

+ {selected ? ( + + ) : null} +
+ ) + })} +
+
+ {screen.id === 'configure' ? ( + + ) : screen.id === 'export' ? ( +
+ +
+ ) : null} +
+
+ ) +} diff --git a/src/components/Analysis/ConfigureAnalysis.tsx b/src/components/Analysis/ConfigureAnalysis.tsx new file mode 100644 index 00000000..6ed65d46 --- /dev/null +++ b/src/components/Analysis/ConfigureAnalysis.tsx @@ -0,0 +1,59 @@ +import Link from 'next/link' +import React from 'react' +import { useLocalStorage } from 'src/hooks' +import { AnalyzedGame } from 'src/types' + +import { ContinueAgainstMaia } from 'src/components' + +interface Props { + currentMaiaModel: string + setCurrentMaiaModel: (model: string) => void + launchContinue: () => void + MAIA_MODELS: string[] + game: AnalyzedGame + onDeleteCustomGame?: () => void +} + +export const ConfigureAnalysis: React.FC = ({ + currentMaiaModel, + setCurrentMaiaModel, + launchContinue, + MAIA_MODELS, + game, + onDeleteCustomGame, +}: Props) => { + const isCustomGame = game.type === 'custom-pgn' || game.type === 'custom-fen' + + return ( +
+
+

Analyze using:

+ +
+ + {isCustomGame && onDeleteCustomGame && ( +
+ +
+ )} +
+ ) +} diff --git a/src/components/Analysis/CustomAnalysisModal.tsx b/src/components/Analysis/CustomAnalysisModal.tsx new file mode 100644 index 00000000..e85e431d --- /dev/null +++ b/src/components/Analysis/CustomAnalysisModal.tsx @@ -0,0 +1,176 @@ +import { useState } from 'react' +import { Chess } from 'chess.ts' +import toast from 'react-hot-toast' +import { ModalContainer } from '../Misc/ModalContainer' + +interface Props { + onSubmit: (type: 'pgn' | 'fen', data: string, name?: string) => void + onClose: () => void +} + +export const CustomAnalysisModal: React.FC = ({ onSubmit, onClose }) => { + const [mode, setMode] = useState<'pgn' | 'fen'>('pgn') + const [input, setInput] = useState('') + const [name, setName] = useState('') + + const validateAndSubmit = () => { + if (!input.trim()) { + toast.error('Please enter some data') + return + } + + if (mode === 'fen') { + const chess = new Chess() + const validation = chess.validateFen(input.trim()) + if (!validation.valid) { + toast.error('Invalid FEN position: ' + validation.error) + return + } + } else { + try { + const chess = new Chess() + chess.loadPgn(input.trim()) + } catch (error) { + toast.error('Invalid PGN format: ' + (error as Error).message) + return + } + } + + onSubmit(mode, input.trim(), name.trim() || undefined) + } + + const examplePGN = `1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Bb7 10. d4 Re8` + const exampleFEN = `r1bqkb1r/pppp1ppp/2n2n2/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 4 4` + + return ( + +
+ {/* Header */} +
+
+

Custom Analysis

+

+ Import a chess game from PGN notation or analyze a specific + position using FEN notation +

+
+ +
+ + {/* Content */} +
+
+ {/* Mode selector */} +
+ +
+ + +
+
+ +
+ + setName(e.target.value)} + /> +
+ +
+ +