├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.zh-CN.md ├── common ├── enum.ts ├── functions.ts └── type.d.ts ├── docs ├── CONTRIBUTING.md └── TRANSLATING.md ├── i18n ├── README.md ├── en-US │ ├── $.json │ ├── _.json │ └── _chatgpt.json └── zh-CN │ ├── $.json │ ├── _.json │ └── _chatgpt.json ├── jest.config.js ├── next-sitemap.config.js ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon │ ├── favicon-16x16.png │ ├── favicon-192x192.png │ ├── favicon-32x32.png │ ├── favicon-512x512.png │ └── favicon.ico ├── robots.txt ├── sitemap.xml ├── vite.svg └── wechat.jpg ├── server ├── constant.ts ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── src ├── app │ ├── [lang] │ │ ├── [...not_found] │ │ │ └── page.ts │ │ ├── chat-chess │ │ │ └── page.client.tsx │ │ ├── layout.tsx │ │ ├── not-found.tsx │ │ └── page.tsx │ └── globals.css ├── assets │ ├── clickprompt-light.svg │ ├── clickprompt-small.svg │ ├── icons │ │ ├── gpt.svg │ │ ├── logout.svg │ │ ├── message.svg │ │ ├── new-chat.svg │ │ ├── send.svg │ │ ├── trashcan.svg │ │ └── volume.svg │ └── images │ │ ├── 00234-2169176559.png │ │ ├── chatgpt-logo.svg │ │ └── content.png ├── components │ ├── ChakraUI │ │ ├── Provider.tsx │ │ ├── icons.ts │ │ └── index.ts │ ├── LobbyCard.tsx │ ├── LocaleSwitcher.tsx │ ├── LoginCard.tsx │ ├── RoomCard.tsx │ └── socket.client.ts ├── configs │ └── constants.ts ├── i18n │ ├── en-US.ts │ ├── index.ts │ ├── pagePath.ts │ └── zh-CN.ts ├── layout │ └── NavBar.tsx ├── middleware.ts └── types.d.ts ├── tailwind.config.js ├── tsconfig.json └── vercel.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [dev, master] 6 | pull_request: 7 | branches: [dev] 8 | 9 | jobs: 10 | build: 11 | name: Build & Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | node-version: ["lts/gallium", "lts/hydrogen", "current"] 16 | steps: 17 | - name: Checkout 🛎️ 18 | uses: actions/checkout@v3 19 | with: 20 | persist-credentials: false 21 | 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version: 16 25 | 26 | - run: npm ci 27 | 28 | - run: npm run test 29 | 30 | - run: npm run build --if-present 31 | lint: 32 | name: format and lint 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 🛎️ 36 | uses: actions/checkout@v3 37 | with: 38 | persist-credentials: false 39 | 40 | - uses: actions/setup-node@v3 41 | with: 42 | node-version: 16 43 | - run: npm ci 44 | 45 | - run: npm run format 46 | 47 | - run: npm run lint 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | pnpm-lock.yaml 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /.swc/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | .idea 40 | 41 | /dist 42 | public/sitemap-0.xml 43 | src/assets/resources/**/*.json 44 | 45 | .vercel 46 | .env 47 | server/**/*.js 48 | common/**/*.js 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Prompt Engineering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatChess - Chess AI and commentary powered by ChatGPT 2 | 3 | [![ci](https://github.com/prompt-engineering/chat-chess/actions/workflows/ci.yml/badge.svg)](https://github.com/prompt-engineering/chat-chess/actions/workflows/ci.yml) 4 | ![GitHub](https://img.shields.io/github/license/prompt-engineering/chat-chess) 5 | [![Discord](https://img.shields.io/discord/1082563233593966612)](https://discord.gg/FSWXq4DmEj) 6 | 7 | English | [简体中文](./README.zh-CN.md) 8 | 9 | Online Demo: WIP 10 | 11 | ![Screenshot](https://chatchessassets.s3.ap-east-1.amazonaws.com/screenshots/Screenshot+2023-04-26+at+19.12.40.png) 12 | 13 | Join us: 14 | 15 | [![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/FSWXq4DmEj) 16 | 17 | ## Local Usage 18 | 19 | ### Be very careful that ChatGPT will make invalid moves and the server will retry calling OpenAI API until it gets a valid move (for now), and all those requests are billed. 20 | 21 | 1. Clone [ChatChess](https://github.com/prompt-engineering/chat-chess)。 22 | 2. Run `npm install` 23 | 3. Run `npm run dev` to serve front-end 24 | 4. In a new console, cd into `server` directory 25 | 5. Run `npm install` 26 | 6. Run `npm run start` to start the backend server 27 | 28 | ## LICENSE 29 | 30 | This code is distributed under the MIT license. See [LICENSE](./LICENSE) in this directory. 31 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # ChatChess - ChatGPT 国际象棋和解说 2 | 3 | [![ci](https://github.com/prompt-engineering/chat-chess/actions/workflows/ci.yml/badge.svg)](https://github.com/prompt-engineering/chat-chess/actions/workflows/ci.yml) 4 | ![GitHub](https://img.shields.io/github/license/prompt-engineering/chat-chess) 5 | 6 | 演示: WIP 7 | 8 | ![截图](https://chatchessassets.s3.ap-east-1.amazonaws.com/screenshots/Screenshot+2023-04-26+at+19.16.29.png) 9 | 10 | [English](./README.md) | 简体中文 11 | 12 | ## 本地搭建 13 | 14 | ### 请注意 ChatGPT 会随机给出不符合规则的棋,服务器会无限重复调用 OpenAI 直到得到符合规则的结果。如果你是付费用户,这些请求都会计费。 15 | 16 | 1. 从 GitHub 克隆 [ChatChess](https://github.com/prompt-engineering/chat-chess)。 17 | 2. 执行 `npm install` 18 | 3. 运行 `npm run dev` 开启前端 19 | 4. 新建控制台,进入 `server` 文件夹 20 | 5. 执行 `npm install` 21 | 6. 运行 `npm run start` 开启后端 22 | 23 | ## LICENSE 24 | 25 | This code is distributed under the MIT license. See [LICENSE](./LICENSE) in this directory. 26 | -------------------------------------------------------------------------------- /common/enum.ts: -------------------------------------------------------------------------------- 1 | export enum RoomStatusEnum { 2 | WAITING_TO_START = 0, 3 | STARTED = 1, 4 | WAITING_PLAYER = 2, 5 | ENDED = 3 6 | }; 7 | 8 | export enum PlayerTypeEnum { 9 | HUMAN = 0, 10 | AI = 1 11 | }; 12 | 13 | export enum MessageTypeEnum { 14 | PUBLIC = 0, 15 | PRIVATE = 1 16 | }; -------------------------------------------------------------------------------- /common/functions.ts: -------------------------------------------------------------------------------- 1 | export const isDM = (id: number) => { 2 | return id == 0; 3 | }; 4 | 5 | // export const toName = (id: number) => { 6 | // return isDM(id) ? "法官" : "玩家" + id; 7 | // }; 8 | 9 | // export const toTarget = (from: number, to?: number) => { 10 | // return "[" + (to == undefined && isDM(from) ? "公告" : to == undefined ? "公聊" : ("私聊:" + toName(to))) + "]"; 11 | // }; 12 | 13 | // export const toMessageType = (from: number, to?: number) => { 14 | // return to == undefined && isDM(from) ? "公告" : to == undefined ? "公聊" : "私聊"; 15 | // }; -------------------------------------------------------------------------------- /common/type.d.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionResponseMessage } from "openai"; 2 | import { Socket } from "socket.io"; 3 | import { PlayerTypeEnum, RoomStateEnum } from "./enum"; 4 | import { Chess } from "chess.js"; 5 | 6 | export type KV = { 7 | [key: string]: T; 8 | } 9 | 10 | export type ConnectServerOptions = { 11 | apiKey: string; 12 | } 13 | 14 | export type CreateRoomOptions = { 15 | name: string; 16 | locale: string; 17 | password?: string; 18 | playerCount: number; 19 | aiCount: number; 20 | }; 21 | 22 | export type JoinRoomOptions = { 23 | id: number; 24 | password?: string; 25 | }; 26 | 27 | export type ExitRoomOptions = { 28 | id: number; 29 | }; 30 | 31 | export type StartGameOptions = { 32 | roomId: number 33 | }; 34 | 35 | export type Message = { 36 | from: number; 37 | content: string; 38 | isDelta?: boolean; 39 | }; 40 | 41 | export type RoomClient = { 42 | id: number; 43 | name: string; 44 | locale: string; 45 | isPrivate: boolean; 46 | status: RoomStatusEnum; 47 | waitingOn?: number; 48 | fen: string; 49 | players: PlayerClient[]; 50 | }; 51 | 52 | export type RoomServer = Omit & { 53 | apiKey: string; 54 | password?: string; 55 | game: Chess; 56 | players: PlayerServer[]; 57 | }; 58 | 59 | export type PlayerClient = { 60 | id: number; 61 | type: PlayerTypeEnum; 62 | }; 63 | 64 | export type PlayerServer = PlayerClient & { 65 | history: ChatCompletionRequestMessage[]; 66 | messages: Message[]; 67 | }; 68 | 69 | export type UserServer = { 70 | socket: Socket; 71 | apiKey: string; 72 | }; 73 | 74 | /** 75 | * 76 | * @export 77 | * @interface CreateChatCompletionStreamResponse 78 | */ 79 | export interface CreateChatCompletionStreamResponse { 80 | /** 81 | * 82 | * @type {string} 83 | * @memberof CreateChatCompletionStreamResponse 84 | */ 85 | id: string; 86 | /** 87 | * 88 | * @type {string} 89 | * @memberof CreateChatCompletionStreamResponse 90 | */ 91 | object: string; 92 | /** 93 | * 94 | * @type {number} 95 | * @memberof CreateChatCompletionStreamResponse 96 | */ 97 | created: number; 98 | /** 99 | * 100 | * @type {string} 101 | * @memberof CreateChatCompletionStreamResponse 102 | */ 103 | model: string; 104 | /** 105 | * 106 | * @type {Array} 107 | * @memberof CreateChatCompletionStreamResponse 108 | */ 109 | choices: Array; 110 | /** 111 | * 112 | * @type {CreateCompletionStreamResponseUsage} 113 | * @memberof CreateChatCompletionStreamResponse 114 | */ 115 | usage?: CreateCompletionStreamResponseUsage; 116 | } 117 | /** 118 | * 119 | * @export 120 | * @interface CreateChatCompletionStreamResponseChoicesInner 121 | */ 122 | export interface CreateChatCompletionStreamResponseChoicesInner { 123 | /** 124 | * 125 | * @type {number} 126 | * @memberof CreateChatCompletionStreamResponseChoicesInner 127 | */ 128 | index?: number; 129 | /** 130 | * 131 | * @type {ChatCompletionStreamResponseMessage} 132 | * @memberof CreateChatCompletionStreamResponseChoicesInner 133 | */ 134 | delta?: ChatCompletionStreamResponseMessage; 135 | /** 136 | * 137 | * @type {string} 138 | * @memberof CreateChatCompletionStreamResponseChoicesInner 139 | */ 140 | finish_reason?: string | null; 141 | } 142 | 143 | /** 144 | * 145 | * @export 146 | * @interface ChatCompletionStreamResponseMessage 147 | */ 148 | export interface ChatCompletionStreamResponseMessage { 149 | /** 150 | * @type {ChatCompletionResponseMessageRoleEnum} 151 | * @memberof ChatCompletionStreamResponseMessage 152 | */ 153 | role?: ChatCompletionResponseMessageRoleEnum; 154 | /** 155 | * @type {string} 156 | * @memberof ChatCompletionStreamResponseMessage 157 | */ 158 | content?: string; 159 | } 160 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Manual 2 | 3 | We welcome contributions of any size and skill level. As an open source project, we believe in giving back to our contributors and are happy to help with guidance on PRs, technical writing, and turning any feature idea into a reality. 4 | 5 | > **Tip for new contributors:** 6 | > Take a look at [https://github.com/firstcontributions/first-contributions](https://github.com/firstcontributions/first-contributions) for helpful information on contributing 7 | 8 | ## Quick Guide 9 | 10 | ### Prerequisite 11 | 12 | ```shell 13 | node: ">=16.0.0" 14 | npm: "^8.11.0" 15 | # otherwise, your build will fail 16 | ``` 17 | 18 | ### Setting up your local repo 19 | 20 | ```shell 21 | git clone && cd ... 22 | npm install 23 | npm run build 24 | ``` 25 | 26 | ### Development 27 | 28 | ```shell 29 | # starts a file-watching, live-reloading dev script for active development 30 | npm run dev 31 | # build the entire project, one time. 32 | npm run build 33 | ``` 34 | 35 | ### Running tests 36 | 37 | ```shell 38 | # run this in the top-level project root to run all tests 39 | npm run test 40 | ``` 41 | 42 | ### Making a Pull Request 43 | 44 | You can run the following commands before making a Pull Request 45 | 46 | ```shell 47 | # format with fix 48 | npm run format:fix 49 | # lint with fix 50 | npm run lint:fix 51 | ``` 52 | 53 | ## Code Structure 54 | 55 | TODO 56 | 57 | ## Translation 58 | 59 | See [i18n guide](TRANSLATING.md) 60 | -------------------------------------------------------------------------------- /docs/TRANSLATING.md: -------------------------------------------------------------------------------- 1 | # 🌐 i18n Guide 2 | 3 | Thanks for your interest in helping us translate ClickPrompt! 4 | 5 | TODO 6 | -------------------------------------------------------------------------------- /i18n/README.md: -------------------------------------------------------------------------------- 1 | # i18n files 2 | 3 | Inside this folder, the first folder level is locale code such as `en-US`, and in it has A LOT of json files the naming convention is: 4 | 5 | - Global data is in the `$.json` file. 6 | - For specific page data: 7 | - index page is corresponding to `_.json` file 8 | - other pages just use pathname without trailing slash and locale segment, and replace all `/` with `_`(cause in some filesystem `/` is illegal charactor in pathname). such as `_foo.json` for `/foo/`, `_foo_bar.json` for `/foo/bar/` . I think you get the idea. 9 | 10 | # HOW TO USE IN RSC(React server component) 11 | 12 | ```typescript jsx 13 | // page.server.tsx 14 | import { getAppData } from "@/i18n"; 15 | import CSC from "./component.client.tsx"; 16 | 17 | async function RscFoo() { 18 | // ... 19 | const { locale, pathname, i18n } = await getAppData(); 20 | const t = i18n.tFactory("/"); 21 | // t is a function takes key and give you value in the json file 22 | t("title"); // will be "Streamline your prompt design" 23 | 24 | // you can also access global data by 25 | const g = i18n.g; 26 | 27 | const i18nProps: GeneralI18nProps = { 28 | locale, 29 | pathname, 30 | i18n: { 31 | dict: i18n.dict, 32 | }, 33 | }; 34 | 35 | // use i18n in CSC (client side component) 36 | return ; 37 | // ... 38 | } 39 | ``` 40 | 41 | ```typescript jsx 42 | // component.client.tsx 43 | "use client"; 44 | 45 | export default function CSC({ i18n }: GeneralI18nProps) { 46 | const { dict } = i18n; 47 | 48 | // use dict like plain object here 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /i18n/en-US/$.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /i18n/en-US/_.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "Connect to server", 3 | "apikey_note": "We will not save your key, but it will be used as your id, and we will use that key to call OpenAI in the game you create.", 4 | "room": "Room", 5 | "password_protected": "Password", 6 | "waiting_to_start": "Waiting", 7 | "ended": "Ended", 8 | "started": "In progress", 9 | "player_count": "Players", 10 | "logout": "Disconnect", 11 | "input_password": "Please enter the password", 12 | "cancel": "Cancel", 13 | "confirm": "Confirm", 14 | "create_room": "Create room", 15 | "room_name": "Room name", 16 | "join": "Join", 17 | "start": "Start", 18 | "exit_room": "Leave room", 19 | "commentator": "Commentator", 20 | "player": "Player", 21 | "waiting_on": "Waiting", 22 | "join_room_success": "Room joined", 23 | "join_room_error": "Failed to join room" 24 | } -------------------------------------------------------------------------------- /i18n/en-US/_chatgpt.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /i18n/zh-CN/$.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /i18n/zh-CN/_.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "登入服务器", 3 | "apikey_note": "我们不会永久保存该 key,但它将作为您的身份认证标识。您创建的游戏中的 AI 将使用该 key 调用 OpenAI 服务。", 4 | "room": "房间", 5 | "password_protected": "密码保护", 6 | "waiting_to_start": "等待开始", 7 | "ended": "已结束", 8 | "started": "正在游戏", 9 | "player_count": "玩家人数", 10 | "logout": "登出", 11 | "input_password": "请输入房间密码", 12 | "cancel": "取消", 13 | "confirm": "确认", 14 | "create_room": "创建房间", 15 | "room_name": "房间名", 16 | "join": "加入", 17 | "start": "开始", 18 | "exit_room": "离开房间", 19 | "commentator": "解说", 20 | "player": "玩家", 21 | "waiting_on": "等待", 22 | "join_room_success": "已加入房间", 23 | "join_room_error": "加入房间失败" 24 | } 25 | -------------------------------------------------------------------------------- /i18n/zh-CN/_chatgpt.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | const nextJest = require("next/jest"); 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | /** @type {import('jest').Config} */ 11 | const customJestConfig = { 12 | // Add more setup options before each test is run 13 | // setupFilesAfterEnv: ['/jest.setup.js'], 14 | testEnvironment: "jest-environment-jsdom", 15 | moduleNameMapper: { 16 | "^jsonpath-plus": require.resolve("jsonpath-plus"), 17 | "^lodash-es$": "lodash", 18 | "^@/(.*)": "/src/$1", 19 | }, 20 | transformIgnorePatterns: [ 21 | "/node_modules/", 22 | "^.+\\.module\\.(css|sass|scss)$", 23 | ], 24 | }; 25 | 26 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 27 | module.exports = createJestConfig(customJestConfig); 28 | -------------------------------------------------------------------------------- /next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | module.exports = { 3 | siteUrl: process.env.SITE_URL || "https://chess.fluoritestudio.com/", 4 | generateRobotsTxt: true, // (optional) 5 | // ...other options 6 | }; 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | appDir: true, 6 | // TODO https://beta.nextjs.org/docs/configuring/typescript#statically-typed-links 7 | // typedRoutes: true, 8 | }, 9 | trailingSlash: true, 10 | transpilePackages: ["react-syntax-highlighter"], 11 | images: { 12 | domains: [ 13 | "prompt-engineering.github.io", 14 | ], 15 | }, 16 | webpack: (config, options) => { 17 | config.module.rules.push({ 18 | test: /\.yml/, 19 | use: "yaml-loader", 20 | }); 21 | 22 | config.module.rules.push({ 23 | test: /\.svg$/i, 24 | type: "asset", 25 | resourceQuery: /url/, // *.svg?url 26 | }); 27 | 28 | config.module.rules.push({ 29 | test: /\.svg$/i, 30 | issuer: /\.[jt]sx?$/, 31 | resourceQuery: { not: [/url/] }, // exclude react component if *.svg?url 32 | use: ["@svgr/webpack"], 33 | }); 34 | 35 | return config; 36 | }, 37 | }; 38 | 39 | module.exports = nextConfig; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-chess", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "prepare": "husky install", 7 | "dev": "cross-env NODE_ENV='development' next dev", 8 | "build": "next build", 9 | "postbuild": "next-sitemap", 10 | "start": "npm run dev", 11 | "lint": "next lint", 12 | "lint:fix": "next lint --fix", 13 | "format": "prettier --check . -u", 14 | "format:fix": "prettier --write . -u", 15 | "prepare:env": "npx vercel link && npx vercel env pull .env.local", 16 | "test": "jest --passWithNoTests", 17 | "test:watch": "jest --watch" 18 | }, 19 | "dependencies": { 20 | "@chakra-ui/icons": "^2.0.17", 21 | "@chakra-ui/react": "^2.5.1", 22 | "@chakra-ui/spinner": "^2.0.13", 23 | "@chakra-ui/system": "^2.5.1", 24 | "@emotion/react": "^11.10.6", 25 | "@emotion/styled": "^11.10.6", 26 | "@formatjs/intl-localematcher": "^0.2.32", 27 | "@types/jsonpath-plus": "^5.0.2", 28 | "autosize": "^6.0.1", 29 | "chess.js": "^1.0.0-beta.5", 30 | "client-only": "^0.0.1", 31 | "dagre": "^0.8.5", 32 | "dotparser": "^1.1.1", 33 | "encoding": "^0.1.13", 34 | "expr-eval": "^2.0.2", 35 | "formik": "^2.2.9", 36 | "framer-motion": "^10.0.1", 37 | "jsonpath-plus": "^7.2.0", 38 | "kysely": "^0.23.5", 39 | "kysely-planetscale": "^1.3.0", 40 | "lodash-es": "^4.17.21", 41 | "mermaid": "^10.0.2", 42 | "negotiator": "^0.6.3", 43 | "next": "13.2.3", 44 | "next-sitemap": "^4.0.2", 45 | "node-fetch": "^2", 46 | "openai": "^3.2.1", 47 | "react": "18.2.0", 48 | "react-chessboard": "^2.1.3", 49 | "react-dom": "18.2.0", 50 | "react-spinners": "^0.13.8", 51 | "remark-gfm": "^3.0.1", 52 | "remirror": "^2.0.26", 53 | "server-only": "^0.0.1", 54 | "sharp": "^0.31.3", 55 | "socket.io-client": "^4.6.1", 56 | "svg-pan-zoom": "^3.6.1", 57 | "typescript": "4.9.5", 58 | "use-debounce": "^9.0.3" 59 | }, 60 | "devDependencies": { 61 | "@svgr/webpack": "^6.5.1", 62 | "@testing-library/jest-dom": "^5.16.5", 63 | "@testing-library/react": "^14.0.0", 64 | "@types/autosize": "^4.0.1", 65 | "@types/chess.js": "^0.13.4", 66 | "@types/dagre": "^0.7.48", 67 | "@types/jsonpath": "^0.2.0", 68 | "@types/lodash-es": "^4.17.6", 69 | "@types/negotiator": "^0.6.1", 70 | "@types/node": "18.14.5", 71 | "@types/node-fetch": "^2.6.2", 72 | "@types/papaparse": "^5.3.7", 73 | "@types/react": "18.0.28", 74 | "@types/react-dom": "18.0.11", 75 | "@types/tunnel": "^0.0.3", 76 | "@typescript-eslint/eslint-plugin": "^5.54.1", 77 | "autoprefixer": "^10.4.13", 78 | "cross-env": "^7.0.3", 79 | "eslint": "8.35.0", 80 | "eslint-config-next": "13.2.3", 81 | "eslint-config-prettier": "^8.6.0", 82 | "eslint-plugin-prettier": "^4.2.1", 83 | "husky": "^8.0.3", 84 | "jest": "^29.4.3", 85 | "jest-environment-jsdom": "^29.4.3", 86 | "js-yaml": "^4.1.0", 87 | "lint-staged": "^13.1.2", 88 | "postcss": "^8.4.21", 89 | "prettier": "^2.8.4", 90 | "tailwindcss": "^3.2.7", 91 | "tunnel": "^0.0.6", 92 | "walkdir": "^0.4.1", 93 | "yaml-loader": "^0.8.0" 94 | }, 95 | "overrides": { 96 | "react-json-view": { 97 | "react": "$react", 98 | "react-dom": "$react-dom" 99 | }, 100 | "flux": { 101 | "react": "$react", 102 | "react-dom": "$react-dom" 103 | } 104 | }, 105 | "engines": { 106 | "npm": ">=8.11.0", 107 | "node": ">=16.19.0" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/favicon/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/favicon/favicon-512x512.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / 4 | 5 | # Host 6 | Host: https://chess.fluoritestudio.com/ 7 | 8 | # Sitemaps 9 | Sitemap: https://chess.fluoritestudio.com/sitemap.xml 10 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/public/wechat.jpg -------------------------------------------------------------------------------- /server/constant.ts: -------------------------------------------------------------------------------- 1 | import { KV } from "../common/type.js"; 2 | 3 | export const CHAT_COMPLETION_URL = "https://api.openai.com/v1/chat/completions"; 4 | export const CHAT_COMPLETION_CONFIG = { 5 | model: "gpt-3.5-turbo", 6 | }; 7 | export const DM_SYSTEM_PROMPT: KV = { 8 | "en-US": "You are a chess commentator, please provide commentary after I tell you about a move.", 9 | "zh-CN": "请扮演一名国际象棋解说,请在我告诉你一步棋之后解说这步棋。" 10 | }; 11 | export const DM_PROMPT: KV = { 12 | "en-US": "Please welcome the audience first", 13 | "zh-CN": "首先请致欢迎词。" 14 | }; 15 | export const PLAYER_SYSTEM_PROMPT = "Let's play chess. Please let me know your move using SAN format and wrap it in curly brackets. Perform your next move only after my move. Make sure you have a clear visualization of board and your move is valid based on our previous moves."; -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import express from "express"; 4 | import http from "http"; 5 | import { Server, Socket } from "socket.io"; 6 | import createHttpsProxyAgent from "https-proxy-agent"; 7 | import fetch from "node-fetch"; 8 | import { 9 | ChatCompletionResponseMessage, 10 | CreateChatCompletionRequest, 11 | } from "openai"; 12 | import { isDM } from "../common/functions.js"; 13 | import { 14 | JoinRoomOptions, 15 | CreateRoomOptions, 16 | KV, 17 | StartGameOptions, 18 | Message, 19 | PlayerServer, 20 | RoomClient, 21 | RoomServer, 22 | PlayerClient, 23 | UserServer, 24 | ConnectServerOptions, 25 | ExitRoomOptions, 26 | CreateChatCompletionStreamResponse, 27 | } from "../common/type.js"; 28 | import { RoomStatusEnum, PlayerTypeEnum } from "../common/enum.js"; 29 | import { 30 | CHAT_COMPLETION_CONFIG, 31 | CHAT_COMPLETION_URL, 32 | DM_PROMPT, 33 | DM_SYSTEM_PROMPT, 34 | PLAYER_SYSTEM_PROMPT, 35 | } from "./constant.js"; 36 | import { Chess } from "chess.js"; 37 | const app = express(); 38 | const server = http.createServer(app); 39 | const io = new Server(server, { 40 | cors: { 41 | origin: 42 | process.env.NODE_ENV == "production" 43 | ? process.env.SERVER_URL 44 | : "http://localhost:3000", 45 | credentials: true, 46 | }, 47 | }); 48 | const rooms: KV = {}; 49 | const users: KV = {}; 50 | 51 | const isLoggedIn = (socket: Socket): boolean => { 52 | return socket.id in users && users[socket.id].apiKey ? true : false; 53 | }; 54 | 55 | const toRoomDto = (room: RoomServer): RoomClient => { 56 | return { 57 | id: room.id, 58 | name: room.name, 59 | locale: room.locale, 60 | isPrivate: room.isPrivate, 61 | status: room.status, 62 | waitingOn: room.waitingOn, 63 | fen: room.game.fen(), 64 | players: room.players.map( 65 | (it) => 66 | ({ 67 | id: it.id, 68 | type: it.type, 69 | } as PlayerClient) 70 | ), 71 | } as RoomClient; 72 | }; 73 | 74 | const handleJoinRoom = (options: JoinRoomOptions, socket: Socket) => { 75 | if (!isLoggedIn(socket)) { 76 | handleLogout(socket); 77 | return; 78 | } 79 | const isAllowed = 80 | options && 81 | "id" in options && 82 | options.id in rooms && 83 | (!rooms[options.id].password || 84 | options.password == rooms[options.id].password); 85 | console.log(socket.id, "handleJoinRoom", options, isAllowed); 86 | if (isAllowed) { 87 | socket.rooms.clear(); 88 | socket.join("room" + options.id); 89 | socket.emit("JOIN_ROOM_SUCCESS", toRoomDto(rooms[options.id])); 90 | console.log(socket.id, "JOIN_ROOM_SUCCESS", options.id); 91 | } else { 92 | socket.emit("JOIN_ROOM_ERROR", {}); 93 | console.log(socket.id, "JOIN_ROOM_ERROR", options.id); 94 | } 95 | }; 96 | 97 | const handleCreateRoom = (options: CreateRoomOptions, socket: Socket) => { 98 | if (!isLoggedIn(socket)) { 99 | handleLogout(socket); 100 | return; 101 | } 102 | let i = 0; 103 | Object.keys(rooms).forEach((it) => { 104 | if (i <= parseInt(it)) i = parseInt(it) + 1; 105 | }); 106 | rooms[i] = { 107 | id: i, 108 | name: options.name, 109 | locale: options.locale, 110 | isPrivate: options.password ? true : false, 111 | apiKey: users[socket.id].apiKey, 112 | status: RoomStatusEnum.WAITING_TO_START, 113 | game: new Chess(), 114 | password: options ? options.password : undefined, 115 | players: [], 116 | }; 117 | rooms[i].game.reset(); 118 | rooms[i].players.push(initAIPlayer(0, options.playerCount, options.locale)); 119 | for (let idx = 1; idx <= options.aiCount; idx++) { 120 | rooms[i].players.push( 121 | initAIPlayer(idx, options.playerCount, options.locale) 122 | ); 123 | } 124 | for (let idx = options.aiCount; idx < options.playerCount; idx++) { 125 | rooms[i].players.push(initHumanPlayer(idx)); 126 | } 127 | console.log(socket.id, "ROOM_CREATED", i); 128 | socket.emit("ROOM_CREATED", toRoomDto(rooms[i])); 129 | }; 130 | 131 | const handleExitRoom = (options: ExitRoomOptions, socket: Socket) => { 132 | if (!isLoggedIn(socket)) { 133 | handleLogout(socket); 134 | return; 135 | } 136 | console.log(socket.id, "handleExitRoom", options.id); 137 | socket.leave("room_" + options.id); 138 | handleSendRoomList(socket); 139 | }; 140 | 141 | const handleStart = async (options: StartGameOptions, socket: Socket) => { 142 | if (!isLoggedIn(socket)) { 143 | handleLogout(socket); 144 | return; 145 | } 146 | if ( 147 | "roomId" in options && 148 | options.roomId in rooms && 149 | rooms[options.roomId].apiKey == users[socket.id].apiKey 150 | ) { 151 | console.log(socket.id, "handleStart", "Room: " + options.roomId); 152 | const room = rooms[options.roomId]; 153 | room.status = RoomStatusEnum.STARTED; 154 | while (room && room.status != RoomStatusEnum.ENDED) { 155 | for (const idx in room.players) { 156 | if (isDM(parseInt(idx)) && room.status != RoomStatusEnum.STARTED) 157 | continue; 158 | const _player = room.players[idx]; 159 | room.waitingOn = _player.id; 160 | room.status = RoomStatusEnum.WAITING_PLAYER; 161 | io.to("room" + room.id).emit("STATUS_UPDATE", toRoomDto(room)); 162 | if (_player.type == PlayerTypeEnum.AI) { 163 | // await wait(15000); 164 | await createChatCompletion(room, _player.id); 165 | } 166 | if (!isDM(_player.id)) { 167 | room.waitingOn = 0; 168 | io.to("room" + room.id).emit("STATUS_UPDATE", toRoomDto(room)); 169 | // await wait(15000); 170 | await createChatCompletion(room, 0); 171 | } 172 | if (room.game.isGameOver()) room.status = RoomStatusEnum.ENDED; 173 | if (room.status == RoomStatusEnum.ENDED) break; 174 | } 175 | room.waitingOn = undefined; 176 | io.to("room" + room.id).emit("STATUS_UPDATE", toRoomDto(room)); 177 | } 178 | } 179 | }; 180 | 181 | const handleSendRoomList = (socket: Socket) => { 182 | socket.emit( 183 | "ROOM_LIST", 184 | Object.keys(rooms).map((it) => { 185 | const _room = rooms[it]; 186 | return toRoomDto(_room); 187 | }) 188 | ); 189 | }; 190 | 191 | const handleLogin = (options: ConnectServerOptions, socket: Socket) => { 192 | console.log(socket.id, "handleLogin", options.apiKey); 193 | if (options.apiKey) { 194 | users[socket.id] = { 195 | socket, 196 | apiKey: options.apiKey, 197 | }; 198 | handleSendRoomList(socket); 199 | } else { 200 | handleLogout(socket); 201 | } 202 | }; 203 | 204 | const handleLogout = (socket: Socket) => { 205 | socket.disconnect(); 206 | delete users[socket.id]; 207 | }; 208 | 209 | io.on("connection", (socket) => { 210 | console.log(socket.id, "onConnection"); 211 | socket.on("LOGIN", (options: ConnectServerOptions) => 212 | handleLogin(options, socket) 213 | ); 214 | socket.on("JOIN_ROOM", (options: JoinRoomOptions) => 215 | handleJoinRoom(options, socket) 216 | ); 217 | socket.on("CREATE_ROOM", (options: CreateRoomOptions) => 218 | handleCreateRoom(options, socket) 219 | ); 220 | socket.on("EXIT_ROOM", (options: ExitRoomOptions) => 221 | handleExitRoom(options, socket) 222 | ); 223 | socket.on("START", (options: StartGameOptions) => 224 | handleStart(options, socket) 225 | ); 226 | }); 227 | 228 | io.on("disconnect", (socket) => { 229 | console.log(socket.id, "onDisconnect"); 230 | delete users[socket.id]; 231 | }); 232 | 233 | server.listen(3001, () => { 234 | console.log("listening on *:3001"); 235 | }); 236 | 237 | const initAIPlayer = ( 238 | id: number, 239 | playerCount: number, 240 | locale: string 241 | ): PlayerServer => { 242 | return { 243 | id, 244 | type: PlayerTypeEnum.AI, 245 | history: [ 246 | { 247 | role: "system", 248 | content: isDM(id) 249 | ? DM_SYSTEM_PROMPT[locale].replace( 250 | "${player_count}", 251 | playerCount.toString() 252 | ) 253 | : PLAYER_SYSTEM_PROMPT.replace("${key}", id.toString()).replace( 254 | "${player_count}", 255 | playerCount.toString() 256 | ), 257 | }, 258 | ...(isDM(id) 259 | ? [ 260 | { 261 | role: "user", 262 | content: DM_PROMPT[locale], 263 | }, 264 | ] 265 | : []), 266 | ], 267 | messages: [], 268 | }; 269 | }; 270 | 271 | const initHumanPlayer = (id: number): PlayerServer => { 272 | return { 273 | id, 274 | type: PlayerTypeEnum.HUMAN, 275 | history: [], 276 | messages: [], 277 | }; 278 | }; 279 | 280 | const createChatCompletion = async ( 281 | room: RoomServer, 282 | from: number, 283 | _addOn?: ChatCompletionResponseMessage 284 | ) => { 285 | try { 286 | console.log("========================"); 287 | console.log( 288 | "createChatCompletion" + "\nRoom: " + room.id + "\nFrom: " + from 289 | ); 290 | console.log("------------------------"); 291 | const _player = room.players[from]; 292 | console.log(JSON.stringify(_player.history)); 293 | const response = await fetch(CHAT_COMPLETION_URL, { 294 | method: "POST", 295 | headers: { 296 | "Content-Type": "application/json", 297 | Authorization: `Bearer ${room.apiKey}`, 298 | }, 299 | body: JSON.stringify({ 300 | ...CHAT_COMPLETION_CONFIG, 301 | messages: _addOn ? [..._player.history, _addOn] : _player.history, 302 | stream: true, 303 | } as CreateChatCompletionRequest), 304 | agent: 305 | process.env.NODE_ENV == "production" 306 | ? undefined 307 | : createHttpsProxyAgent("http://127.0.0.1:10809"), 308 | }); 309 | // const json = (await response.json()) as CreateChatCompletionResponse; 310 | // if (!response.ok) { 311 | // console.error(json); 312 | // } 313 | // const message = json.choices[0].message; 314 | // if (message) { 315 | // await handleResponse(room, from, message); 316 | // } 317 | if (!response.ok || !response.body) { 318 | throw new Error(await response.text()); 319 | } 320 | const message: ChatCompletionResponseMessage = { 321 | role: "assistant", 322 | content: "", 323 | }; 324 | for await (const chunk of response.body) { 325 | const data = chunk.toString().split("\n"); 326 | for (const lineIndex in data) { 327 | const jsonStr = data[lineIndex].replace(/^data: /g, "").trim(); 328 | if (!jsonStr) continue; 329 | if (jsonStr == "[DONE]") break; 330 | let json: CreateChatCompletionStreamResponse | undefined = undefined; 331 | try { 332 | json = JSON.parse(jsonStr) as CreateChatCompletionStreamResponse; 333 | if ( 334 | json && 335 | json.choices && 336 | json.choices.length && 337 | "delta" in json.choices[0] && 338 | json.choices[0].delta 339 | ) { 340 | if (json.choices[0].delta.role) { 341 | message.role = json.choices[0].delta.role; 342 | } 343 | if (json.choices[0].delta.content) { 344 | message.content += json.choices[0].delta.content; 345 | if (isDM(from)) handleDelta(room.id, from, message.content); 346 | } 347 | } 348 | } catch (e) { 349 | console.error(e); 350 | } 351 | } 352 | } 353 | await handleResponse(room, from, message); 354 | } catch (e) { 355 | console.error(e); 356 | console.log("RETRY in 15000"); 357 | await wait(15000); 358 | await createChatCompletion(room, from); 359 | return; 360 | } 361 | console.log("========================"); 362 | }; 363 | 364 | const wait = async (milliseconds: number) => { 365 | return new Promise((resolve) => { 366 | setTimeout(resolve, milliseconds); 367 | }); 368 | }; 369 | 370 | const handleDelta = ( 371 | roomId: number, 372 | from: number, 373 | content: string, 374 | to?: number 375 | ) => { 376 | io.to("room" + roomId).emit("MESSAGE", { 377 | from, 378 | to, 379 | content, 380 | isDelta: true, 381 | } as Message); 382 | }; 383 | 384 | const handleHistory = ( 385 | it: PlayerServer, 386 | _history: ChatCompletionResponseMessage 387 | ) => { 388 | if (it.history[it.history.length - 1].role == "user") { 389 | it.history[it.history.length - 1].content += "\n" + _history.content; 390 | } else { 391 | it.history.push(JSON.parse(JSON.stringify(_history))); 392 | } 393 | }; 394 | 395 | const handleMessage = ( 396 | roomId: number, 397 | from: number, 398 | content: string, 399 | to?: number 400 | ) => { 401 | if (!(roomId in rooms)) return; 402 | if (!isDM(from)) { 403 | const room = rooms[roomId]; 404 | const _history = { 405 | role: "user", 406 | content, 407 | } as ChatCompletionResponseMessage; 408 | if (to == undefined) { 409 | // room.players 410 | // .filter((it) => it.id != from) 411 | // .forEach((it) => handleHistory(it, _history)); 412 | room.players 413 | .filter((it) => it.id != from) 414 | .forEach((it) => { 415 | if (isDM(it.id)) { 416 | handleHistory(it, _history); 417 | } else { 418 | handleHistory(it, { 419 | role: "user", 420 | content: `{${content}}`, 421 | }); 422 | } 423 | }); 424 | } else { 425 | handleHistory(room.players[to], _history); 426 | } 427 | } 428 | io.to("room" + roomId).emit("MESSAGE", { 429 | from, 430 | to, 431 | content, 432 | } as Message); 433 | }; 434 | 435 | const handleRetry = async (room: RoomServer, from: number, message: string) => { 436 | const _addOn = { 437 | role: "user", 438 | content: message, 439 | } as ChatCompletionResponseMessage; 440 | console.log("RETRY", _addOn); 441 | await createChatCompletion(room, from, _addOn); 442 | return; 443 | }; 444 | 445 | const handleResponse = async ( 446 | room: RoomServer, 447 | from: number, 448 | message: ChatCompletionResponseMessage 449 | ) => { 450 | console.log("------------------------"); 451 | console.log("handleResponse: ", message.content); 452 | console.log("------------------------"); 453 | if (isDM(from)) { 454 | handleMessage(room.id, from, message.content); 455 | room.players[from].history.push(JSON.parse(JSON.stringify(message))); 456 | } else { 457 | const regex = /\{(?:.|\n)*?\}/g; 458 | const matches = message.content.match(regex); 459 | if (matches && matches.length) { 460 | const move = matches[0] 461 | .replace("{", "") 462 | .replace("}", "") 463 | .replace("\n", "") 464 | .trim(); 465 | console.log("Move: ", move); 466 | if (!move || move == "") { 467 | await handleRetry( 468 | room, 469 | from, 470 | "Please make a move using SAN format wrapped in curly brackets." 471 | ); 472 | return; 473 | } 474 | try { 475 | room.game.move(move); 476 | } catch (e) { 477 | console.error(e); 478 | await handleRetry( 479 | room, 480 | from, 481 | (e as Error).message + 482 | "\n" + 483 | "Current board in FEN format: " + 484 | room.game.fen() 485 | ); 486 | return; 487 | } 488 | handleMessage(room.id, from, move); 489 | room.players[from].history.push({ 490 | role: "assistant", 491 | content: `{${move}}`, 492 | } as ChatCompletionResponseMessage); 493 | } else { 494 | await handleRetry( 495 | room, 496 | from, 497 | "Please make a move using SAN format wrapped in curly brackets." 498 | ); 499 | } 500 | } 501 | }; 502 | -------------------------------------------------------------------------------- /server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-chess-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "chat-chess-server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "chess.js": "^1.0.0-beta.5", 13 | "express": "^4.18.2", 14 | "https-proxy-agent": "^5.0.1", 15 | "node-fetch": "^3.3.1", 16 | "openai": "^3.2.1", 17 | "socket.io": "^4.6.1", 18 | "typescript": "^5.0.4" 19 | }, 20 | "devDependencies": { 21 | "@types/chess.js": "^0.13.4", 22 | "@types/express": "^4.17.17" 23 | } 24 | }, 25 | "node_modules/@socket.io/component-emitter": { 26 | "version": "3.1.0", 27 | "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", 28 | "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" 29 | }, 30 | "node_modules/@types/body-parser": { 31 | "version": "1.19.2", 32 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", 33 | "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", 34 | "dev": true, 35 | "dependencies": { 36 | "@types/connect": "*", 37 | "@types/node": "*" 38 | } 39 | }, 40 | "node_modules/@types/chess.js": { 41 | "version": "0.13.4", 42 | "resolved": "https://registry.npmjs.org/@types/chess.js/-/chess.js-0.13.4.tgz", 43 | "integrity": "sha512-rPmTVffnepx/TVe9ZzOf8bXxyCkYQ+h+VTSiI4JKjoKWzCFkPgPRJL7Cz15qslksOcVGEYCPYLbG0NLO4yzbrg==", 44 | "dev": true 45 | }, 46 | "node_modules/@types/connect": { 47 | "version": "3.4.35", 48 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", 49 | "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", 50 | "dev": true, 51 | "dependencies": { 52 | "@types/node": "*" 53 | } 54 | }, 55 | "node_modules/@types/cookie": { 56 | "version": "0.4.1", 57 | "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", 58 | "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" 59 | }, 60 | "node_modules/@types/cors": { 61 | "version": "2.8.13", 62 | "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", 63 | "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", 64 | "dependencies": { 65 | "@types/node": "*" 66 | } 67 | }, 68 | "node_modules/@types/express": { 69 | "version": "4.17.17", 70 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", 71 | "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", 72 | "dev": true, 73 | "dependencies": { 74 | "@types/body-parser": "*", 75 | "@types/express-serve-static-core": "^4.17.33", 76 | "@types/qs": "*", 77 | "@types/serve-static": "*" 78 | } 79 | }, 80 | "node_modules/@types/express-serve-static-core": { 81 | "version": "4.17.33", 82 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", 83 | "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", 84 | "dev": true, 85 | "dependencies": { 86 | "@types/node": "*", 87 | "@types/qs": "*", 88 | "@types/range-parser": "*" 89 | } 90 | }, 91 | "node_modules/@types/mime": { 92 | "version": "3.0.1", 93 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", 94 | "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", 95 | "dev": true 96 | }, 97 | "node_modules/@types/node": { 98 | "version": "18.16.0", 99 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.0.tgz", 100 | "integrity": "sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==" 101 | }, 102 | "node_modules/@types/qs": { 103 | "version": "6.9.7", 104 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", 105 | "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", 106 | "dev": true 107 | }, 108 | "node_modules/@types/range-parser": { 109 | "version": "1.2.4", 110 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", 111 | "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", 112 | "dev": true 113 | }, 114 | "node_modules/@types/serve-static": { 115 | "version": "1.15.1", 116 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", 117 | "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", 118 | "dev": true, 119 | "dependencies": { 120 | "@types/mime": "*", 121 | "@types/node": "*" 122 | } 123 | }, 124 | "node_modules/accepts": { 125 | "version": "1.3.8", 126 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 127 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 128 | "dependencies": { 129 | "mime-types": "~2.1.34", 130 | "negotiator": "0.6.3" 131 | }, 132 | "engines": { 133 | "node": ">= 0.6" 134 | } 135 | }, 136 | "node_modules/agent-base": { 137 | "version": "6.0.2", 138 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 139 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 140 | "dependencies": { 141 | "debug": "4" 142 | }, 143 | "engines": { 144 | "node": ">= 6.0.0" 145 | } 146 | }, 147 | "node_modules/array-flatten": { 148 | "version": "1.1.1", 149 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 150 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 151 | }, 152 | "node_modules/asynckit": { 153 | "version": "0.4.0", 154 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 155 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 156 | }, 157 | "node_modules/axios": { 158 | "version": "0.26.1", 159 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", 160 | "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", 161 | "dependencies": { 162 | "follow-redirects": "^1.14.8" 163 | } 164 | }, 165 | "node_modules/base64id": { 166 | "version": "2.0.0", 167 | "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", 168 | "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", 169 | "engines": { 170 | "node": "^4.5.0 || >= 5.9" 171 | } 172 | }, 173 | "node_modules/body-parser": { 174 | "version": "1.20.1", 175 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 176 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 177 | "dependencies": { 178 | "bytes": "3.1.2", 179 | "content-type": "~1.0.4", 180 | "debug": "2.6.9", 181 | "depd": "2.0.0", 182 | "destroy": "1.2.0", 183 | "http-errors": "2.0.0", 184 | "iconv-lite": "0.4.24", 185 | "on-finished": "2.4.1", 186 | "qs": "6.11.0", 187 | "raw-body": "2.5.1", 188 | "type-is": "~1.6.18", 189 | "unpipe": "1.0.0" 190 | }, 191 | "engines": { 192 | "node": ">= 0.8", 193 | "npm": "1.2.8000 || >= 1.4.16" 194 | } 195 | }, 196 | "node_modules/body-parser/node_modules/debug": { 197 | "version": "2.6.9", 198 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 199 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 200 | "dependencies": { 201 | "ms": "2.0.0" 202 | } 203 | }, 204 | "node_modules/body-parser/node_modules/ms": { 205 | "version": "2.0.0", 206 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 207 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 208 | }, 209 | "node_modules/bytes": { 210 | "version": "3.1.2", 211 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 212 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 213 | "engines": { 214 | "node": ">= 0.8" 215 | } 216 | }, 217 | "node_modules/call-bind": { 218 | "version": "1.0.2", 219 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 220 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 221 | "dependencies": { 222 | "function-bind": "^1.1.1", 223 | "get-intrinsic": "^1.0.2" 224 | }, 225 | "funding": { 226 | "url": "https://github.com/sponsors/ljharb" 227 | } 228 | }, 229 | "node_modules/chess.js": { 230 | "version": "1.0.0-beta.5", 231 | "resolved": "https://registry.npmjs.org/chess.js/-/chess.js-1.0.0-beta.5.tgz", 232 | "integrity": "sha512-ZmSBraFoGrLCiC7m5vwPax02KoKFjC+3uQrlDYKoORUi8MY+7qceDXQW1ho0Gt6pylGCzTTTBa6gy55bOXp2Jw==" 233 | }, 234 | "node_modules/combined-stream": { 235 | "version": "1.0.8", 236 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 237 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 238 | "dependencies": { 239 | "delayed-stream": "~1.0.0" 240 | }, 241 | "engines": { 242 | "node": ">= 0.8" 243 | } 244 | }, 245 | "node_modules/content-disposition": { 246 | "version": "0.5.4", 247 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 248 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 249 | "dependencies": { 250 | "safe-buffer": "5.2.1" 251 | }, 252 | "engines": { 253 | "node": ">= 0.6" 254 | } 255 | }, 256 | "node_modules/content-type": { 257 | "version": "1.0.5", 258 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 259 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 260 | "engines": { 261 | "node": ">= 0.6" 262 | } 263 | }, 264 | "node_modules/cookie": { 265 | "version": "0.5.0", 266 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 267 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 268 | "engines": { 269 | "node": ">= 0.6" 270 | } 271 | }, 272 | "node_modules/cookie-signature": { 273 | "version": "1.0.6", 274 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 275 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 276 | }, 277 | "node_modules/cors": { 278 | "version": "2.8.5", 279 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 280 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 281 | "dependencies": { 282 | "object-assign": "^4", 283 | "vary": "^1" 284 | }, 285 | "engines": { 286 | "node": ">= 0.10" 287 | } 288 | }, 289 | "node_modules/data-uri-to-buffer": { 290 | "version": "4.0.1", 291 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 292 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 293 | "engines": { 294 | "node": ">= 12" 295 | } 296 | }, 297 | "node_modules/debug": { 298 | "version": "4.3.4", 299 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 300 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 301 | "dependencies": { 302 | "ms": "2.1.2" 303 | }, 304 | "engines": { 305 | "node": ">=6.0" 306 | }, 307 | "peerDependenciesMeta": { 308 | "supports-color": { 309 | "optional": true 310 | } 311 | } 312 | }, 313 | "node_modules/delayed-stream": { 314 | "version": "1.0.0", 315 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 316 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 317 | "engines": { 318 | "node": ">=0.4.0" 319 | } 320 | }, 321 | "node_modules/depd": { 322 | "version": "2.0.0", 323 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 324 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 325 | "engines": { 326 | "node": ">= 0.8" 327 | } 328 | }, 329 | "node_modules/destroy": { 330 | "version": "1.2.0", 331 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 332 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 333 | "engines": { 334 | "node": ">= 0.8", 335 | "npm": "1.2.8000 || >= 1.4.16" 336 | } 337 | }, 338 | "node_modules/ee-first": { 339 | "version": "1.1.1", 340 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 341 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 342 | }, 343 | "node_modules/encodeurl": { 344 | "version": "1.0.2", 345 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 346 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 347 | "engines": { 348 | "node": ">= 0.8" 349 | } 350 | }, 351 | "node_modules/engine.io": { 352 | "version": "6.4.1", 353 | "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz", 354 | "integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==", 355 | "dependencies": { 356 | "@types/cookie": "^0.4.1", 357 | "@types/cors": "^2.8.12", 358 | "@types/node": ">=10.0.0", 359 | "accepts": "~1.3.4", 360 | "base64id": "2.0.0", 361 | "cookie": "~0.4.1", 362 | "cors": "~2.8.5", 363 | "debug": "~4.3.1", 364 | "engine.io-parser": "~5.0.3", 365 | "ws": "~8.11.0" 366 | }, 367 | "engines": { 368 | "node": ">=10.0.0" 369 | } 370 | }, 371 | "node_modules/engine.io-parser": { 372 | "version": "5.0.6", 373 | "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz", 374 | "integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==", 375 | "engines": { 376 | "node": ">=10.0.0" 377 | } 378 | }, 379 | "node_modules/engine.io/node_modules/cookie": { 380 | "version": "0.4.2", 381 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", 382 | "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", 383 | "engines": { 384 | "node": ">= 0.6" 385 | } 386 | }, 387 | "node_modules/escape-html": { 388 | "version": "1.0.3", 389 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 390 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 391 | }, 392 | "node_modules/etag": { 393 | "version": "1.8.1", 394 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 395 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 396 | "engines": { 397 | "node": ">= 0.6" 398 | } 399 | }, 400 | "node_modules/express": { 401 | "version": "4.18.2", 402 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 403 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 404 | "dependencies": { 405 | "accepts": "~1.3.8", 406 | "array-flatten": "1.1.1", 407 | "body-parser": "1.20.1", 408 | "content-disposition": "0.5.4", 409 | "content-type": "~1.0.4", 410 | "cookie": "0.5.0", 411 | "cookie-signature": "1.0.6", 412 | "debug": "2.6.9", 413 | "depd": "2.0.0", 414 | "encodeurl": "~1.0.2", 415 | "escape-html": "~1.0.3", 416 | "etag": "~1.8.1", 417 | "finalhandler": "1.2.0", 418 | "fresh": "0.5.2", 419 | "http-errors": "2.0.0", 420 | "merge-descriptors": "1.0.1", 421 | "methods": "~1.1.2", 422 | "on-finished": "2.4.1", 423 | "parseurl": "~1.3.3", 424 | "path-to-regexp": "0.1.7", 425 | "proxy-addr": "~2.0.7", 426 | "qs": "6.11.0", 427 | "range-parser": "~1.2.1", 428 | "safe-buffer": "5.2.1", 429 | "send": "0.18.0", 430 | "serve-static": "1.15.0", 431 | "setprototypeof": "1.2.0", 432 | "statuses": "2.0.1", 433 | "type-is": "~1.6.18", 434 | "utils-merge": "1.0.1", 435 | "vary": "~1.1.2" 436 | }, 437 | "engines": { 438 | "node": ">= 0.10.0" 439 | } 440 | }, 441 | "node_modules/express/node_modules/debug": { 442 | "version": "2.6.9", 443 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 444 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 445 | "dependencies": { 446 | "ms": "2.0.0" 447 | } 448 | }, 449 | "node_modules/express/node_modules/ms": { 450 | "version": "2.0.0", 451 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 452 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 453 | }, 454 | "node_modules/fetch-blob": { 455 | "version": "3.2.0", 456 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 457 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 458 | "funding": [ 459 | { 460 | "type": "github", 461 | "url": "https://github.com/sponsors/jimmywarting" 462 | }, 463 | { 464 | "type": "paypal", 465 | "url": "https://paypal.me/jimmywarting" 466 | } 467 | ], 468 | "dependencies": { 469 | "node-domexception": "^1.0.0", 470 | "web-streams-polyfill": "^3.0.3" 471 | }, 472 | "engines": { 473 | "node": "^12.20 || >= 14.13" 474 | } 475 | }, 476 | "node_modules/finalhandler": { 477 | "version": "1.2.0", 478 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 479 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 480 | "dependencies": { 481 | "debug": "2.6.9", 482 | "encodeurl": "~1.0.2", 483 | "escape-html": "~1.0.3", 484 | "on-finished": "2.4.1", 485 | "parseurl": "~1.3.3", 486 | "statuses": "2.0.1", 487 | "unpipe": "~1.0.0" 488 | }, 489 | "engines": { 490 | "node": ">= 0.8" 491 | } 492 | }, 493 | "node_modules/finalhandler/node_modules/debug": { 494 | "version": "2.6.9", 495 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 496 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 497 | "dependencies": { 498 | "ms": "2.0.0" 499 | } 500 | }, 501 | "node_modules/finalhandler/node_modules/ms": { 502 | "version": "2.0.0", 503 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 504 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 505 | }, 506 | "node_modules/follow-redirects": { 507 | "version": "1.15.2", 508 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", 509 | "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", 510 | "funding": [ 511 | { 512 | "type": "individual", 513 | "url": "https://github.com/sponsors/RubenVerborgh" 514 | } 515 | ], 516 | "engines": { 517 | "node": ">=4.0" 518 | }, 519 | "peerDependenciesMeta": { 520 | "debug": { 521 | "optional": true 522 | } 523 | } 524 | }, 525 | "node_modules/form-data": { 526 | "version": "4.0.0", 527 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 528 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 529 | "dependencies": { 530 | "asynckit": "^0.4.0", 531 | "combined-stream": "^1.0.8", 532 | "mime-types": "^2.1.12" 533 | }, 534 | "engines": { 535 | "node": ">= 6" 536 | } 537 | }, 538 | "node_modules/formdata-polyfill": { 539 | "version": "4.0.10", 540 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 541 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 542 | "dependencies": { 543 | "fetch-blob": "^3.1.2" 544 | }, 545 | "engines": { 546 | "node": ">=12.20.0" 547 | } 548 | }, 549 | "node_modules/forwarded": { 550 | "version": "0.2.0", 551 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 552 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 553 | "engines": { 554 | "node": ">= 0.6" 555 | } 556 | }, 557 | "node_modules/fresh": { 558 | "version": "0.5.2", 559 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 560 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 561 | "engines": { 562 | "node": ">= 0.6" 563 | } 564 | }, 565 | "node_modules/function-bind": { 566 | "version": "1.1.1", 567 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 568 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 569 | }, 570 | "node_modules/get-intrinsic": { 571 | "version": "1.2.0", 572 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", 573 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", 574 | "dependencies": { 575 | "function-bind": "^1.1.1", 576 | "has": "^1.0.3", 577 | "has-symbols": "^1.0.3" 578 | }, 579 | "funding": { 580 | "url": "https://github.com/sponsors/ljharb" 581 | } 582 | }, 583 | "node_modules/has": { 584 | "version": "1.0.3", 585 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 586 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 587 | "dependencies": { 588 | "function-bind": "^1.1.1" 589 | }, 590 | "engines": { 591 | "node": ">= 0.4.0" 592 | } 593 | }, 594 | "node_modules/has-symbols": { 595 | "version": "1.0.3", 596 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 597 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 598 | "engines": { 599 | "node": ">= 0.4" 600 | }, 601 | "funding": { 602 | "url": "https://github.com/sponsors/ljharb" 603 | } 604 | }, 605 | "node_modules/http-errors": { 606 | "version": "2.0.0", 607 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 608 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 609 | "dependencies": { 610 | "depd": "2.0.0", 611 | "inherits": "2.0.4", 612 | "setprototypeof": "1.2.0", 613 | "statuses": "2.0.1", 614 | "toidentifier": "1.0.1" 615 | }, 616 | "engines": { 617 | "node": ">= 0.8" 618 | } 619 | }, 620 | "node_modules/https-proxy-agent": { 621 | "version": "5.0.1", 622 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 623 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 624 | "dependencies": { 625 | "agent-base": "6", 626 | "debug": "4" 627 | }, 628 | "engines": { 629 | "node": ">= 6" 630 | } 631 | }, 632 | "node_modules/iconv-lite": { 633 | "version": "0.4.24", 634 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 635 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 636 | "dependencies": { 637 | "safer-buffer": ">= 2.1.2 < 3" 638 | }, 639 | "engines": { 640 | "node": ">=0.10.0" 641 | } 642 | }, 643 | "node_modules/inherits": { 644 | "version": "2.0.4", 645 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 646 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 647 | }, 648 | "node_modules/ipaddr.js": { 649 | "version": "1.9.1", 650 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 651 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 652 | "engines": { 653 | "node": ">= 0.10" 654 | } 655 | }, 656 | "node_modules/media-typer": { 657 | "version": "0.3.0", 658 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 659 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 660 | "engines": { 661 | "node": ">= 0.6" 662 | } 663 | }, 664 | "node_modules/merge-descriptors": { 665 | "version": "1.0.1", 666 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 667 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 668 | }, 669 | "node_modules/methods": { 670 | "version": "1.1.2", 671 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 672 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 673 | "engines": { 674 | "node": ">= 0.6" 675 | } 676 | }, 677 | "node_modules/mime": { 678 | "version": "1.6.0", 679 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 680 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 681 | "bin": { 682 | "mime": "cli.js" 683 | }, 684 | "engines": { 685 | "node": ">=4" 686 | } 687 | }, 688 | "node_modules/mime-db": { 689 | "version": "1.52.0", 690 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 691 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 692 | "engines": { 693 | "node": ">= 0.6" 694 | } 695 | }, 696 | "node_modules/mime-types": { 697 | "version": "2.1.35", 698 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 699 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 700 | "dependencies": { 701 | "mime-db": "1.52.0" 702 | }, 703 | "engines": { 704 | "node": ">= 0.6" 705 | } 706 | }, 707 | "node_modules/ms": { 708 | "version": "2.1.2", 709 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 710 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 711 | }, 712 | "node_modules/negotiator": { 713 | "version": "0.6.3", 714 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 715 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 716 | "engines": { 717 | "node": ">= 0.6" 718 | } 719 | }, 720 | "node_modules/node-domexception": { 721 | "version": "1.0.0", 722 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 723 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 724 | "funding": [ 725 | { 726 | "type": "github", 727 | "url": "https://github.com/sponsors/jimmywarting" 728 | }, 729 | { 730 | "type": "github", 731 | "url": "https://paypal.me/jimmywarting" 732 | } 733 | ], 734 | "engines": { 735 | "node": ">=10.5.0" 736 | } 737 | }, 738 | "node_modules/node-fetch": { 739 | "version": "3.3.1", 740 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", 741 | "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", 742 | "dependencies": { 743 | "data-uri-to-buffer": "^4.0.0", 744 | "fetch-blob": "^3.1.4", 745 | "formdata-polyfill": "^4.0.10" 746 | }, 747 | "engines": { 748 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 749 | }, 750 | "funding": { 751 | "type": "opencollective", 752 | "url": "https://opencollective.com/node-fetch" 753 | } 754 | }, 755 | "node_modules/object-assign": { 756 | "version": "4.1.1", 757 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 758 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 759 | "engines": { 760 | "node": ">=0.10.0" 761 | } 762 | }, 763 | "node_modules/object-inspect": { 764 | "version": "1.12.3", 765 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 766 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 767 | "funding": { 768 | "url": "https://github.com/sponsors/ljharb" 769 | } 770 | }, 771 | "node_modules/on-finished": { 772 | "version": "2.4.1", 773 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 774 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 775 | "dependencies": { 776 | "ee-first": "1.1.1" 777 | }, 778 | "engines": { 779 | "node": ">= 0.8" 780 | } 781 | }, 782 | "node_modules/openai": { 783 | "version": "3.2.1", 784 | "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", 785 | "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", 786 | "dependencies": { 787 | "axios": "^0.26.0", 788 | "form-data": "^4.0.0" 789 | } 790 | }, 791 | "node_modules/parseurl": { 792 | "version": "1.3.3", 793 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 794 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 795 | "engines": { 796 | "node": ">= 0.8" 797 | } 798 | }, 799 | "node_modules/path-to-regexp": { 800 | "version": "0.1.7", 801 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 802 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 803 | }, 804 | "node_modules/proxy-addr": { 805 | "version": "2.0.7", 806 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 807 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 808 | "dependencies": { 809 | "forwarded": "0.2.0", 810 | "ipaddr.js": "1.9.1" 811 | }, 812 | "engines": { 813 | "node": ">= 0.10" 814 | } 815 | }, 816 | "node_modules/qs": { 817 | "version": "6.11.0", 818 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 819 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 820 | "dependencies": { 821 | "side-channel": "^1.0.4" 822 | }, 823 | "engines": { 824 | "node": ">=0.6" 825 | }, 826 | "funding": { 827 | "url": "https://github.com/sponsors/ljharb" 828 | } 829 | }, 830 | "node_modules/range-parser": { 831 | "version": "1.2.1", 832 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 833 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 834 | "engines": { 835 | "node": ">= 0.6" 836 | } 837 | }, 838 | "node_modules/raw-body": { 839 | "version": "2.5.1", 840 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 841 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 842 | "dependencies": { 843 | "bytes": "3.1.2", 844 | "http-errors": "2.0.0", 845 | "iconv-lite": "0.4.24", 846 | "unpipe": "1.0.0" 847 | }, 848 | "engines": { 849 | "node": ">= 0.8" 850 | } 851 | }, 852 | "node_modules/safe-buffer": { 853 | "version": "5.2.1", 854 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 855 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 856 | "funding": [ 857 | { 858 | "type": "github", 859 | "url": "https://github.com/sponsors/feross" 860 | }, 861 | { 862 | "type": "patreon", 863 | "url": "https://www.patreon.com/feross" 864 | }, 865 | { 866 | "type": "consulting", 867 | "url": "https://feross.org/support" 868 | } 869 | ] 870 | }, 871 | "node_modules/safer-buffer": { 872 | "version": "2.1.2", 873 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 874 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 875 | }, 876 | "node_modules/send": { 877 | "version": "0.18.0", 878 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 879 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 880 | "dependencies": { 881 | "debug": "2.6.9", 882 | "depd": "2.0.0", 883 | "destroy": "1.2.0", 884 | "encodeurl": "~1.0.2", 885 | "escape-html": "~1.0.3", 886 | "etag": "~1.8.1", 887 | "fresh": "0.5.2", 888 | "http-errors": "2.0.0", 889 | "mime": "1.6.0", 890 | "ms": "2.1.3", 891 | "on-finished": "2.4.1", 892 | "range-parser": "~1.2.1", 893 | "statuses": "2.0.1" 894 | }, 895 | "engines": { 896 | "node": ">= 0.8.0" 897 | } 898 | }, 899 | "node_modules/send/node_modules/debug": { 900 | "version": "2.6.9", 901 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 902 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 903 | "dependencies": { 904 | "ms": "2.0.0" 905 | } 906 | }, 907 | "node_modules/send/node_modules/debug/node_modules/ms": { 908 | "version": "2.0.0", 909 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 910 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 911 | }, 912 | "node_modules/send/node_modules/ms": { 913 | "version": "2.1.3", 914 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 915 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 916 | }, 917 | "node_modules/serve-static": { 918 | "version": "1.15.0", 919 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 920 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 921 | "dependencies": { 922 | "encodeurl": "~1.0.2", 923 | "escape-html": "~1.0.3", 924 | "parseurl": "~1.3.3", 925 | "send": "0.18.0" 926 | }, 927 | "engines": { 928 | "node": ">= 0.8.0" 929 | } 930 | }, 931 | "node_modules/setprototypeof": { 932 | "version": "1.2.0", 933 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 934 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 935 | }, 936 | "node_modules/side-channel": { 937 | "version": "1.0.4", 938 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 939 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 940 | "dependencies": { 941 | "call-bind": "^1.0.0", 942 | "get-intrinsic": "^1.0.2", 943 | "object-inspect": "^1.9.0" 944 | }, 945 | "funding": { 946 | "url": "https://github.com/sponsors/ljharb" 947 | } 948 | }, 949 | "node_modules/socket.io": { 950 | "version": "4.6.1", 951 | "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz", 952 | "integrity": "sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==", 953 | "dependencies": { 954 | "accepts": "~1.3.4", 955 | "base64id": "~2.0.0", 956 | "debug": "~4.3.2", 957 | "engine.io": "~6.4.1", 958 | "socket.io-adapter": "~2.5.2", 959 | "socket.io-parser": "~4.2.1" 960 | }, 961 | "engines": { 962 | "node": ">=10.0.0" 963 | } 964 | }, 965 | "node_modules/socket.io-adapter": { 966 | "version": "2.5.2", 967 | "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", 968 | "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", 969 | "dependencies": { 970 | "ws": "~8.11.0" 971 | } 972 | }, 973 | "node_modules/socket.io-parser": { 974 | "version": "4.2.2", 975 | "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz", 976 | "integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==", 977 | "dependencies": { 978 | "@socket.io/component-emitter": "~3.1.0", 979 | "debug": "~4.3.1" 980 | }, 981 | "engines": { 982 | "node": ">=10.0.0" 983 | } 984 | }, 985 | "node_modules/statuses": { 986 | "version": "2.0.1", 987 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 988 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 989 | "engines": { 990 | "node": ">= 0.8" 991 | } 992 | }, 993 | "node_modules/toidentifier": { 994 | "version": "1.0.1", 995 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 996 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 997 | "engines": { 998 | "node": ">=0.6" 999 | } 1000 | }, 1001 | "node_modules/type-is": { 1002 | "version": "1.6.18", 1003 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1004 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1005 | "dependencies": { 1006 | "media-typer": "0.3.0", 1007 | "mime-types": "~2.1.24" 1008 | }, 1009 | "engines": { 1010 | "node": ">= 0.6" 1011 | } 1012 | }, 1013 | "node_modules/typescript": { 1014 | "version": "5.0.4", 1015 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", 1016 | "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", 1017 | "bin": { 1018 | "tsc": "bin/tsc", 1019 | "tsserver": "bin/tsserver" 1020 | }, 1021 | "engines": { 1022 | "node": ">=12.20" 1023 | } 1024 | }, 1025 | "node_modules/unpipe": { 1026 | "version": "1.0.0", 1027 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1028 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1029 | "engines": { 1030 | "node": ">= 0.8" 1031 | } 1032 | }, 1033 | "node_modules/utils-merge": { 1034 | "version": "1.0.1", 1035 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1036 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 1037 | "engines": { 1038 | "node": ">= 0.4.0" 1039 | } 1040 | }, 1041 | "node_modules/vary": { 1042 | "version": "1.1.2", 1043 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1044 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1045 | "engines": { 1046 | "node": ">= 0.8" 1047 | } 1048 | }, 1049 | "node_modules/web-streams-polyfill": { 1050 | "version": "3.2.1", 1051 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 1052 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 1053 | "engines": { 1054 | "node": ">= 8" 1055 | } 1056 | }, 1057 | "node_modules/ws": { 1058 | "version": "8.11.0", 1059 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 1060 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 1061 | "engines": { 1062 | "node": ">=10.0.0" 1063 | }, 1064 | "peerDependencies": { 1065 | "bufferutil": "^4.0.1", 1066 | "utf-8-validate": "^5.0.2" 1067 | }, 1068 | "peerDependenciesMeta": { 1069 | "bufferutil": { 1070 | "optional": true 1071 | }, 1072 | "utf-8-validate": { 1073 | "optional": true 1074 | } 1075 | } 1076 | } 1077 | } 1078 | } 1079 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-chess-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "npx tsc && node index.js" 9 | }, 10 | "type": "module", 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "chess.js": "^1.0.0-beta.5", 15 | "express": "^4.18.2", 16 | "https-proxy-agent": "^5.0.1", 17 | "node-fetch": "^3.3.1", 18 | "openai": "^3.2.1", 19 | "socket.io": "^4.6.1", 20 | "typescript": "^5.0.4" 21 | }, 22 | "devDependencies": { 23 | "@types/chess.js": "^0.13.4", 24 | "@types/express": "^4.17.17" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES6", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "NodeNext", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/[lang]/[...not_found]/page.ts: -------------------------------------------------------------------------------- 1 | /// https://stackoverflow.com/a/75625136 2 | 3 | import { notFound } from "next/navigation"; 4 | 5 | export default function NotFoundCatchAll() { 6 | notFound(); 7 | return null; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/[lang]/chat-chess/page.client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { socket } from "@/components/socket.client"; 4 | import { Box, useToast } from "@chakra-ui/react"; 5 | import { useEffect, useState } from "react"; 6 | import LobbyCard from "@/components/LobbyCard"; 7 | import LoginCard from "@/components/LoginCard"; 8 | import RoomCard from "@/components/RoomCard"; 9 | import { 10 | ExitRoomOptions, 11 | JoinRoomOptions, 12 | Message, 13 | RoomClient, 14 | } from "common/type"; 15 | import Background from "@/assets/images/00234-2169176559.png"; 16 | import Image from "next/image"; 17 | 18 | function ChatChess({ i18n, locale }: GeneralI18nProps) { 19 | const [isLoggedIn, setIsLoggedIn] = useState(false); 20 | const [apiKey, setApiKey] = useState(""); 21 | const [rooms, setRooms] = useState([]); 22 | const [currentRoom, setCurrentRoom] = useState(); 23 | const toast = useToast(); 24 | 25 | useEffect(() => { 26 | socket.on("ROOM_LIST", (_rooms: RoomClient[]) => { 27 | console.log("ROOM_LIST", _rooms); 28 | setRooms(_rooms); 29 | setCurrentRoom(undefined); 30 | // setMessages([]); 31 | setIsLoggedIn(true); 32 | }); 33 | socket.on("JOIN_ROOM_SUCCESS", (_room: RoomClient) => { 34 | setCurrentRoom(_room); 35 | toast({ 36 | status: "success", 37 | title: i18n.dict["join_room_success"] + ": " + _room.id, 38 | isClosable: true, 39 | position: "top", 40 | }); 41 | }); 42 | socket.on("JOIN_ROOM_ERROR", () => { 43 | setCurrentRoom(undefined); 44 | toast({ 45 | status: "error", 46 | title: i18n.dict["join_room_error"], 47 | isClosable: true, 48 | position: "top", 49 | }); 50 | }); 51 | socket.on("STATUS_UPDATE", (_room: RoomClient) => { 52 | console.log("STATUS_UPDATE", _room); 53 | setCurrentRoom((prev) => _room); 54 | }); 55 | // socket.on("disconnect", () => { 56 | // setIsLoggedIn((prev) => false); 57 | // }); 58 | 59 | return () => { 60 | socket.off("ROOM_LIST"); 61 | socket.off("JOIN_ROOM_SUCCESS"); 62 | socket.off("JOIN_ROOM_ERROR"); 63 | socket.off("STATUS_UPDATE"); 64 | }; 65 | }, []); 66 | 67 | // useEffect(() => { 68 | // socket.on("MESSAGE", handleOnMessage); 69 | // return () => { 70 | // socket.off("MESSAGE"); 71 | // }; 72 | // }, [currentRoom]); 73 | 74 | // function handleOnMessage(message: Message) { 75 | // console.log("MESSAGE", message.content); 76 | // setMessages((prev) => [...prev, message]); 77 | // } 78 | 79 | function handleJoinRoom(options: JoinRoomOptions) { 80 | console.log("handleJoinRoom", options, socket); 81 | socket.emit("JOIN_ROOM", options); 82 | } 83 | 84 | function handleExitRoom() { 85 | socket.emit("EXIT_ROOM", { 86 | id: currentRoom?.id, 87 | } as ExitRoomOptions); 88 | setCurrentRoom(undefined); 89 | } 90 | 91 | return ( 92 | 98 | background 111 | {isLoggedIn ? ( 112 | !currentRoom ? ( 113 | 120 | ) : ( 121 | 127 | ) 128 | ) : ( 129 | 135 | )} 136 | 137 | ); 138 | } 139 | 140 | export default ChatChess; 141 | -------------------------------------------------------------------------------- /src/app/[lang]/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css"; 2 | import React from "react"; 3 | import Image from "next/image"; 4 | import NavBar from "@/layout/NavBar"; 5 | import { Container } from "@/components/ChakraUI"; 6 | import { Provider } from "@/components/ChakraUI/Provider"; 7 | 8 | type RootLayoutProps = { 9 | params: { 10 | lang: string; 11 | }; 12 | children: React.ReactNode; 13 | }; 14 | export default function RootLayout({ params, children }: RootLayoutProps) { 15 | const { lang } = params; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | ChatChess 24 | 25 | 29 | 30 | 31 | 32 | {/* https://github.com/vercel/next.js/issues/42292 */} 33 |
34 | {/* @ts-expect-error Async Server Component */} 35 | 36 |
37 | 48 | {children} 49 | 50 |
51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/[lang]/not-found.tsx: -------------------------------------------------------------------------------- 1 | /// https://stackoverflow.com/a/75625136 2 | 3 | import Link from "next/link"; 4 | 5 | export default function NotFound() { 6 | return ( 7 |
8 |

nOT foUnD – 404!

9 |
10 | 14 | Go back to Home 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/[lang]/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getAppData } from "@/i18n"; 3 | import ChatChess from "@/app/[lang]/chat-chess/page.client"; 4 | 5 | async function Page() { 6 | const { locale, pathname, i18n } = await getAppData(); 7 | const i18nProps: GeneralI18nProps = { 8 | locale, 9 | pathname, 10 | i18n: { 11 | dict: i18n.dict, 12 | }, 13 | }; 14 | 15 | return ; 16 | } 17 | 18 | export default Page; 19 | -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | /* good looking scrollbar */ 7 | .overflow-container::-webkit-scrollbar { 8 | width: 8px; 9 | } 10 | 11 | .overflow-container::-webkit-scrollbar-track { 12 | background: #f1f1f1; 13 | } 14 | 15 | .overflow-container::-webkit-scrollbar-thumb { 16 | background: #888; 17 | } 18 | 19 | .overflow-container::-webkit-scrollbar-thumb:hover { 20 | background: #555; 21 | } 22 | } 23 | 24 | #root { 25 | margin: 0 auto; 26 | text-align: center; 27 | } 28 | 29 | code { 30 | text-shadow: none !important; 31 | } 32 | 33 | .logo { 34 | height: 6em; 35 | padding: 1.5em; 36 | will-change: filter; 37 | transition: filter 300ms; 38 | } 39 | .logo:hover { 40 | filter: drop-shadow(0 0 2em #646cffaa); 41 | } 42 | .logo.react:hover { 43 | filter: drop-shadow(0 0 2em #61dafbaa); 44 | } 45 | 46 | @keyframes logo-spin { 47 | from { 48 | transform: rotate(0deg); 49 | } 50 | to { 51 | transform: rotate(360deg); 52 | } 53 | } 54 | 55 | @media (prefers-reduced-motion: no-preference) { 56 | a:nth-of-type(2) .logo { 57 | animation: logo-spin infinite 20s linear; 58 | } 59 | } 60 | 61 | .card { 62 | padding: 2em; 63 | } 64 | 65 | .read-the-docs { 66 | color: #888; 67 | } 68 | 69 | ul, 70 | ul li, 71 | p { 72 | text-align: left; 73 | } 74 | /* custom grid-cols */ 75 | .grid-cols-\[1rem_1fr\] { 76 | grid-template-columns: 1rem 1fr; 77 | } 78 | .grid-cols-\[200px_1fr\] { 79 | grid-template-columns: 200px 1fr; 80 | } 81 | -------------------------------------------------------------------------------- /src/assets/clickprompt-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/clickprompt-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/gpt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/message.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/new-chat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/send.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/trashcan.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/icons/volume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/00234-2169176559.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/src/assets/images/00234-2169176559.png -------------------------------------------------------------------------------- /src/assets/images/chatgpt-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-engineering/chat-chess/492fd70aa599b4b835fe29d0f1be7c3be16c5a27/src/assets/images/content.png -------------------------------------------------------------------------------- /src/components/ChakraUI/Provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { ChakraProvider, extendTheme } from "@chakra-ui/react"; 5 | 6 | export const Provider = ({ children }: { children: React.ReactNode }) => { 7 | const theme = extendTheme({ 8 | components: { 9 | Drawer: { 10 | sizes: { 11 | "2xl": { dialog: { maxW: "8xl" } }, 12 | }, 13 | }, 14 | }, 15 | }); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/ChakraUI/icons.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export * from "@chakra-ui/icons"; 4 | -------------------------------------------------------------------------------- /src/components/ChakraUI/index.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export { 4 | Avatar, 5 | Box, 6 | Flex, 7 | Heading, 8 | Spacer, 9 | Tooltip, 10 | Link, 11 | Breadcrumb, 12 | BreadcrumbItem, 13 | BreadcrumbLink, 14 | Button, 15 | Stack, 16 | Text, 17 | IconButton, 18 | Menu, 19 | MenuButton, 20 | MenuItem, 21 | MenuList, 22 | Input, 23 | Container, 24 | SimpleGrid, 25 | Card, 26 | CardBody, 27 | CardHeader, 28 | AlertIcon, 29 | AlertTitle, 30 | Alert, 31 | } from "@chakra-ui/react"; 32 | -------------------------------------------------------------------------------- /src/components/LobbyCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardHeader, 6 | CardBody, 7 | Button, 8 | Box, 9 | VStack, 10 | HStack, 11 | Heading, 12 | Modal, 13 | ModalOverlay, 14 | ModalBody, 15 | ModalCloseButton, 16 | ModalContent, 17 | ModalFooter, 18 | ModalHeader, 19 | useDisclosure, 20 | Input, 21 | Slider, 22 | SliderFilledTrack, 23 | SliderThumb, 24 | SliderTrack, 25 | Text, 26 | AlertDialog, 27 | AlertDialogBody, 28 | AlertDialogContent, 29 | AlertDialogFooter, 30 | AlertDialogHeader, 31 | AlertDialogOverlay, 32 | } from "@chakra-ui/react"; 33 | import { 34 | CreateRoomOptions, 35 | JoinRoomOptions, 36 | KV, 37 | RoomClient, 38 | } from "common/type"; 39 | import { PlayerTypeEnum, RoomStatusEnum } from "common/enum"; 40 | import { useEffect, useRef, useState } from "react"; 41 | import { Socket } from "socket.io-client"; 42 | import { isDM } from "common/functions"; 43 | 44 | export type LobbyCardProps = { 45 | dict: Record; 46 | locale: string; 47 | socket: Socket; 48 | rooms: RoomClient[]; 49 | handleJoinRoom: (options: JoinRoomOptions) => void; 50 | }; 51 | export default function LobbyCard(props: LobbyCardProps) { 52 | const { isOpen, onOpen, onClose } = useDisclosure(); 53 | const { 54 | isOpen: isPasswordOpen, 55 | onOpen: onPasswordOpen, 56 | onClose: onPasswordClose, 57 | } = useDisclosure(); 58 | const cancelRef = useRef(null); 59 | const [selectedRoomId, setSelectedRoomId] = useState(); 60 | const [name, setName] = useState(""); 61 | const [password, setPassword] = useState(""); 62 | const [playerCount, setPlayerCount] = useState(2); 63 | const [aiCount, setAiCount] = useState(2); 64 | // const [dmType, setDmType] = useState(PlayerTypeEnum.AI); 65 | 66 | useEffect(() => { 67 | props.socket?.on("ROOM_CREATED", (_room: RoomClient) => { 68 | props.handleJoinRoom({ 69 | id: _room.id, 70 | password: password && password != "" ? password : undefined, 71 | }); 72 | }); 73 | 74 | return () => { 75 | props.socket?.off("ROOM_CREATED"); 76 | }; 77 | }, [props.socket, password]); 78 | 79 | const handleCreateRoom = () => { 80 | const options: CreateRoomOptions = { 81 | name, 82 | locale: props.locale, 83 | password: password && password != "" ? password : undefined, 84 | playerCount, 85 | aiCount, 86 | }; 87 | console.log("handleCreateRoom", options); 88 | props.socket?.emit("CREATE_ROOM", options); 89 | }; 90 | 91 | return ( 92 | 99 | 107 | {props.rooms && 108 | props.rooms.map((room) => { 109 | return ( 110 | 115 | 116 | 117 | {props.dict["room"]}: {room.name} 118 | {room.isPrivate && ( 119 | 128 | {props.dict["password_protected"]} 129 | 130 | )} 131 | 144 | {room.status == RoomStatusEnum.WAITING_TO_START 145 | ? props.dict["waiting_to_start"] 146 | : room.status == RoomStatusEnum.ENDED 147 | ? props.dict["ended"] 148 | : props.dict["started"]} 149 | 150 | 151 | 152 | 153 | 154 | {props.dict["player_count"]}:{" "} 155 | {room.players.filter((it) => !isDM(it.id)).length}(AI:{" "} 156 | { 157 | room.players.filter( 158 | (it) => !isDM(it.id) && it.type == PlayerTypeEnum.AI 159 | ).length 160 | } 161 | ) 162 | 163 | 168 | {props.dict["commentator"]}:{" "} 169 | {room.players[0].type == PlayerTypeEnum.AI 170 | ? "AI" 171 | : props.dict["player"]} 172 | 173 | 174 | 197 | 198 | ); 199 | })} 200 | 201 | 211 | {/* */} 215 | 224 | 225 | 226 | 227 | 228 | {props.dict["create_room"]} 229 | 230 | 231 | 236 | {props.dict["room_name"]} 237 | 238 | setName(e.target.value)} 241 | > 242 | {/* 密码(可选)(公开请留空): 245 | setPassword(e.target.value)} type="password"> */} 246 | {/* 法官类型: 249 | */} 253 | {/* 玩家数量:{playerCount} 256 | { 257 | setPlayerCount(e); 258 | if (aiCount > e) 259 | setAiCount(e); 260 | }}> 261 | 262 | 263 | 264 | 265 | */} 266 | 271 | AI: {aiCount} 272 | 273 | setAiCount(e)} 279 | > 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 290 | 297 | 298 | 299 | 300 | 305 | 306 | 307 | 308 | {props.dict["input_password"]} 309 | 310 | 311 | setPassword(e.target.value)} 314 | type="password" 315 | > 316 | 317 | 318 | 328 | 345 | 346 | 347 | 348 | 349 | 350 | ); 351 | } 352 | -------------------------------------------------------------------------------- /src/components/LocaleSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SITE_LOCALE_COOKIE } from "@/configs/constants"; 4 | import { 5 | Box, 6 | Menu, 7 | MenuButton, 8 | MenuList, 9 | MenuItem, 10 | } from "@/components/ChakraUI"; 11 | import { ChevronDownIcon } from "@/components/ChakraUI/icons"; 12 | 13 | const options = [ 14 | { 15 | value: "zh-CN", 16 | label: "中文", 17 | }, 18 | { 19 | value: "en-US", 20 | label: "English", 21 | }, 22 | ]; 23 | export default function LocaleSwitcher({ locale }: { locale: string }) { 24 | const classZh = locale === "zh-CN" ? "text-blue-500" : "text-gray-500"; 25 | const classEn = locale === "en-US" ? "text-blue-500" : "text-gray-500"; 26 | function setEn() { 27 | document.cookie = `${SITE_LOCALE_COOKIE}=en-US;path=/;max-age=31536000;`; 28 | window.location.reload(); 29 | } 30 | 31 | function setZh() { 32 | document.cookie = `${SITE_LOCALE_COOKIE}=zh-CN;path=/;max-age=31536000;`; 33 | window.location.reload(); 34 | } 35 | 36 | return ( 37 |
38 | 39 | 40 | {locale === "zh-CN" ? "中文" : "English"} 41 | 42 | 43 | 44 | {options.map((child) => ( 45 | (child.value === "zh-CN" ? setZh() : setEn())} 49 | > 50 | 55 | {child.label} 56 | 57 | 58 | ))} 59 | 60 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/LoginCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardHeader, 6 | CardBody, 7 | CardFooter, 8 | Button, 9 | Text, 10 | Input, 11 | useToast, 12 | } from "@chakra-ui/react"; 13 | import { ConnectServerOptions } from "common/type"; 14 | import { Dispatch, SetStateAction, useEffect, useState } from "react"; 15 | import { Socket } from "socket.io-client"; 16 | 17 | export type LoginCardProps = { 18 | dict: Record; 19 | socket: Socket; 20 | apiKey: string; 21 | setApiKey: Dispatch>; 22 | }; 23 | export default function LoginCard(props: LoginCardProps) { 24 | const [isLoading, setIsLoading] = useState(false); 25 | const toast = useToast(); 26 | 27 | if (typeof window !== "undefined" && "sessionStorage" in window) { 28 | useEffect(() => { 29 | if (!props.apiKey) { 30 | const _apiKey = window.sessionStorage.getItem("cc:a"); 31 | if (_apiKey) { 32 | console.log("setApiKey", _apiKey); 33 | props.setApiKey(_apiKey); 34 | if (!props.socket.connected) handleConnect(_apiKey); 35 | } 36 | } 37 | }, [window.sessionStorage.getItem("cc:a")]); 38 | } 39 | 40 | const handleConnect = (_apiKey?: string) => { 41 | if (!_apiKey && props.apiKey != "") _apiKey = props.apiKey; 42 | if (!_apiKey) return; 43 | console.log("handleConnect"); 44 | setIsLoading(true); 45 | window.sessionStorage.setItem("cm:a", _apiKey); 46 | props.socket.connect(); 47 | props.socket.emit("LOGIN", { 48 | apiKey: _apiKey, 49 | } as ConnectServerOptions); 50 | }; 51 | 52 | return ( 53 | 61 | 67 | {props.dict["login"]} 68 | 69 | 70 | OpenAI API Key 71 | 78 | {props.dict["apikey_note"]} 79 | 80 | props.setApiKey(e.target.value)} 84 | > 85 | 86 | 91 | 99 | 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/RoomCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Box, 5 | Button, 6 | HStack, 7 | VStack, 8 | Text, 9 | Card, 10 | CardBody, 11 | } from "@chakra-ui/react"; 12 | import { RoomStatusEnum } from "common/enum"; 13 | import { Message, RoomClient, StartGameOptions } from "common/type"; 14 | import { isDM, toName } from "common/functions"; 15 | import { BeatLoader } from "react-spinners"; 16 | import { createRef, useEffect, useRef, useState } from "react"; 17 | import { Chess } from "chess.js"; 18 | import { Chessboard } from "react-chessboard"; 19 | import { Socket } from "socket.io-client"; 20 | 21 | export type RoomCardProps = { 22 | dict: Record; 23 | socket: Socket; 24 | room: RoomClient; 25 | handleExitRoom: () => void; 26 | }; 27 | export default function RoomCard(props: RoomCardProps) { 28 | const [game, setGame] = useState(new Chess(props.room.fen)); 29 | const [comments, setComments] = useState([]); 30 | const contentRef = createRef(); 31 | const handleStart = () => { 32 | props.socket?.emit("START", { 33 | roomId: props.room.id, 34 | } as StartGameOptions); 35 | }; 36 | const [deltaContent, setDeltaContent] = useState(); 37 | 38 | useEffect(() => { 39 | props.socket.on("MESSAGE", (message: Message) => { 40 | console.log("MESSAGE", message.content); 41 | if (isDM(message.from)) { 42 | if (message.isDelta) { 43 | setDeltaContent((prev) => message.content); 44 | } else { 45 | setDeltaContent((prev) => undefined); 46 | comments.push(message); 47 | setComments((prev) => comments); 48 | } 49 | } else if (!message.isDelta) { 50 | comments.push(message); 51 | setComments((prev) => comments); 52 | game.move(message.content); 53 | } 54 | }); 55 | return () => { 56 | props.socket.off("MESSAGE"); 57 | }; 58 | }, []); 59 | 60 | useEffect(() => { 61 | contentRef.current?.scrollIntoView({ 62 | behavior: "smooth", 63 | }); 64 | }, [comments, deltaContent]); 65 | 66 | return ( 67 | 74 | 82 | 83 | 84 | 85 | 86 | 87 | 93 | 101 | {comments && 102 | comments.map((comment, idx) => ( 103 | 104 | 114 | {isDM(comment.from) 115 | ? props.dict["commentator"] 116 | : props.dict["player"] + comment.from} 117 | :{" "} 118 | 119 | {comment.content} 120 | 121 | ))} 122 | {deltaContent != undefined && ( 123 | 124 | 130 | {props.dict["commentator"]}:{" "} 131 | 132 | {deltaContent} 133 | 134 | )} 135 |
136 | {props.room.status == RoomStatusEnum.WAITING_PLAYER && 137 | props.room.waitingOn != undefined 138 | ? `${ 139 | isDM(props.room.waitingOn) 140 | ? props.dict["commentator"] 141 | : props.dict["player"] + props.room.waitingOn 142 | }` 143 | : ""} 144 | {props.room.status == RoomStatusEnum.WAITING_PLAYER && ( 145 | 146 | )} 147 |
148 |
149 |
150 | {/* 153 | 154 | */} 155 |
156 | 157 | {props.room.status == RoomStatusEnum.WAITING_TO_START && ( 158 | 161 | )} 162 | 165 | 166 |
167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /src/components/socket.client.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { io } from "socket.io-client"; 4 | 5 | export const socket = io( 6 | process.env.NEXT_PUBLIC_SERVER_URL || "http://localhost:3001", 7 | { 8 | withCredentials: true, 9 | autoConnect: false, 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /src/configs/constants.ts: -------------------------------------------------------------------------------- 1 | export const SITE_TITLE = "ChatChess"; 2 | export const SITE_URL = "https://chess.fluoritestudio.com/"; 3 | export const SITE_LOCALE_COOKIE = "CLICKPROMPT_LOCALE"; 4 | export const SITE_USER_COOKIE = "CLICKPROMPT_USER"; 5 | export const GITHUB_URL = "https://github.com/prompt-engineering/chat-chess"; 6 | export const CP_GITHUB_ASSETS = `${GITHUB_URL}/tree/master/src/assets/`; 7 | export const SITE_INTERNAL_HEADER_URL = "$$$x-url"; 8 | export const SITE_INTERNAL_HEADER_PATHNAME = "$$$x-pathname"; 9 | export const SITE_INTERNAL_HEADER_LOCALE = "$$$x-locale"; 10 | -------------------------------------------------------------------------------- /src/i18n/en-US.ts: -------------------------------------------------------------------------------- 1 | import type { PagePath } from "./pagePath"; 2 | 3 | import _global from "@i18n/en-US/$.json"; 4 | import _index from "@i18n/en-US/_.json"; 5 | import _chatgpt from "@i18n/en-US/_chatgpt.json"; 6 | 7 | export type GlobalKey = keyof typeof _global; 8 | const pages = { 9 | "/": _index, 10 | "/chatgpt/": _chatgpt, 11 | } satisfies Record; 12 | export type PageKey

= keyof (typeof pages)[P]; 13 | 14 | const i18nDataEnUS = { 15 | "*": _global, 16 | ...pages, 17 | }; 18 | export default i18nDataEnUS; 19 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { match } from "@formatjs/intl-localematcher"; 2 | import Negotiator from "negotiator"; 3 | 4 | const dictionaries = { 5 | "en-US": () => import("./en-US").then((module) => module.default), 6 | "zh-CN": () => import("./zh-CN").then((module) => module.default), 7 | }; 8 | 9 | export type SupportedLocale = keyof typeof dictionaries; 10 | export const SupportedLocales = Object.keys(dictionaries) as SupportedLocale[]; 11 | export const DefaultLocale: SupportedLocale = "zh-CN"; 12 | 13 | export function stripLocaleInPath(pathname: string): PagePath { 14 | const splits = pathname.split("/"); 15 | const locale = splits[1]; 16 | 17 | let striped: PagePath; 18 | if (SupportedLocales.includes(locale as SupportedLocale)) { 19 | striped = pathname.replace(`/${locale}`, "") as PagePath; 20 | } else { 21 | striped = pathname as PagePath; 22 | } 23 | 24 | // todo: we read to read routes from Next.js 25 | if (splits.length == 5 && hadChildRoutes.includes(splits[2])) { 26 | striped = `/${splits[2]}/$` as PagePath; 27 | } 28 | 29 | return striped; 30 | } 31 | 32 | export function getLocaleFromPath(pathname: string): SupportedLocale { 33 | const locale = pathname.split("/")[1]; 34 | if (SupportedLocales.includes(locale as SupportedLocale)) { 35 | return locale as SupportedLocale; 36 | } 37 | 38 | return DefaultLocale; 39 | } 40 | 41 | export function replaceRouteLocale( 42 | pathname: string, 43 | locale: SupportedLocale 44 | ): string { 45 | const currentLocale = pathname.split("/")[1]; 46 | if (SupportedLocales.includes(currentLocale as SupportedLocale)) { 47 | return pathname.replace(`/${currentLocale}`, `/${locale}`); 48 | } 49 | 50 | return `/${locale}${pathname}`; 51 | } 52 | 53 | export function getLocale(headers: Headers): SupportedLocale { 54 | const languages = new Negotiator({ 55 | headers: [...headers].reduce( 56 | (pre: Record, [key, value]) => { 57 | pre[key] = value; 58 | return pre; 59 | }, 60 | {} 61 | ), 62 | }).languages(); 63 | 64 | let locale: SupportedLocale; 65 | try { 66 | locale = match( 67 | languages, 68 | SupportedLocales, 69 | DefaultLocale 70 | ) as SupportedLocale; 71 | } catch (error) { 72 | locale = DefaultLocale; 73 | } 74 | 75 | return locale; 76 | } 77 | 78 | import type { 79 | GlobalKey as GlobalKeyEnUS, 80 | PageKey as PageKeyEnUS, 81 | } from "./en-US"; 82 | import type { 83 | GlobalKey as GlobalKeyZhCN, 84 | PageKey as PageKeyZhCN, 85 | } from "./zh-CN"; 86 | 87 | export type AppData = { 88 | i18n: { 89 | g: (key: GlobalKeyEnUS | GlobalKeyZhCN) => string; 90 | tFactory:

( 91 | path: P 92 | ) => (key: PageKeyEnUS

| PageKeyZhCN

) => string; 93 | dict: Record; 94 | }; 95 | pathname: string; 96 | locale: SupportedLocale; 97 | }; 98 | export type AppDataI18n = AppData["i18n"]; 99 | 100 | import { 101 | SITE_INTERNAL_HEADER_LOCALE, 102 | SITE_INTERNAL_HEADER_PATHNAME, 103 | } from "@/configs/constants"; 104 | import { hadChildRoutes, PagePath } from "./pagePath"; 105 | 106 | export async function getAppData(): Promise { 107 | let pathname: PagePath = "/"; 108 | let locale = DefaultLocale; 109 | 110 | try { 111 | const { headers } = await import("next/headers"); 112 | pathname = (headers().get(SITE_INTERNAL_HEADER_PATHNAME) || 113 | "/") as PagePath; 114 | locale = headers().get(SITE_INTERNAL_HEADER_LOCALE) as SupportedLocale; 115 | } catch (error) { 116 | console.log(error); 117 | } 118 | 119 | const dictionary = dictionaries[locale] ?? dictionaries[DefaultLocale]; 120 | const stripedPathname = stripLocaleInPath(pathname); 121 | return dictionary().then((module) => ({ 122 | i18n: { 123 | g: (key) => module["*"][key], 124 | tFactory: (_) => (key) => 125 | (module[stripedPathname] as any)[key as any] as any, 126 | dict: module[stripedPathname], 127 | }, 128 | pathname: stripedPathname, 129 | locale, 130 | })); 131 | } 132 | -------------------------------------------------------------------------------- /src/i18n/pagePath.ts: -------------------------------------------------------------------------------- 1 | export const hadChildRoutes = [] as string[]; 2 | 3 | export const pages = ["/", "/chatgpt/"] as const; 4 | 5 | export type PagePath = (typeof pages)[number]; 6 | -------------------------------------------------------------------------------- /src/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import type { PagePath } from "./pagePath"; 2 | 3 | import _global from "@i18n/zh-CN/$.json"; 4 | import _index from "@i18n/zh-CN/_.json"; 5 | import _chatgpt from "@i18n/zh-CN/_chatgpt.json"; 6 | 7 | export type GlobalKey = keyof typeof _global; 8 | const pages = { 9 | "/": _index, 10 | "/chatgpt/": _chatgpt, 11 | } satisfies Record; 12 | export type PageKey

= keyof (typeof pages)[P]; 13 | 14 | const i18nDataZhCN = { 15 | "*": _global, 16 | ...pages, 17 | }; 18 | export default i18nDataZhCN; 19 | -------------------------------------------------------------------------------- /src/layout/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Flex, 5 | Heading, 6 | IconButton, 7 | Link as NavLink, 8 | Menu, 9 | MenuButton, 10 | MenuItem, 11 | MenuList, 12 | Spacer, 13 | } from "@/components/ChakraUI"; 14 | import { 15 | ChevronDownIcon, 16 | ExternalLinkIcon, 17 | HamburgerIcon, 18 | } from "@/components/ChakraUI/icons"; 19 | import Link from "next/link"; 20 | import { GITHUB_URL } from "@/configs/constants"; 21 | import LocaleSwitcher from "@/components/LocaleSwitcher"; 22 | import { getAppData } from "@/i18n"; 23 | 24 | export default async function NavBar({ locale }: { locale: string }) { 25 | const { pathname } = await getAppData(); 26 | 27 | const NavList = [ 28 | { 29 | title: "Home", 30 | url: `/`, 31 | }, 32 | { 33 | title: "ChatApp", 34 | url: `/chatgpt/`, 35 | }, 36 | ]; 37 | 38 | return ( 39 | 40 | 41 | 42 | ChatChess 43 | 44 | 45 |

46 | } 50 | variant="outline" 51 | display={{ md: "none", base: "block" }} 52 | mr={4} 53 | /> 54 | 55 | 56 | 57 | ChatChess 58 | 59 | 60 | 61 | 62 | GitHub 63 | 64 | 65 | 66 | 67 | 68 | 69 | 74 | GitHub 75 | 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextMiddleware, NextResponse } from "next/server"; 2 | import { 3 | SupportedLocales, 4 | getLocale, 5 | replaceRouteLocale, 6 | getLocaleFromPath, 7 | SupportedLocale, 8 | } from "@/i18n"; 9 | import { 10 | SITE_INTERNAL_HEADER_LOCALE, 11 | SITE_INTERNAL_HEADER_PATHNAME, 12 | SITE_INTERNAL_HEADER_URL, 13 | SITE_LOCALE_COOKIE, 14 | } from "@/configs/constants"; 15 | 16 | export const middleware: NextMiddleware = (request) => { 17 | // Check if there is any supported locale in the pathname 18 | const pathname = request.nextUrl.pathname; 19 | const pathnameIsMissingLocale = SupportedLocales.every( 20 | (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}` 21 | ); 22 | 23 | let locale = getLocale(request.headers); 24 | 25 | const cookie = request.cookies.get(SITE_LOCALE_COOKIE)?.value; 26 | // If there is a cookie, and it is a supported locale, use it 27 | if (SupportedLocales.includes(cookie as unknown as SupportedLocale)) { 28 | locale = cookie as unknown as SupportedLocale; 29 | } 30 | 31 | // Redirect if there is no locale 32 | if (pathnameIsMissingLocale) { 33 | // e.g. incoming request is /products 34 | // The new URL is now /en-US/products 35 | return NextResponse.redirect( 36 | new URL(`/${locale}/${pathname}`, request.url) 37 | ); 38 | } else if (getLocaleFromPath(pathname) !== locale) { 39 | return NextResponse.redirect( 40 | new URL(replaceRouteLocale(pathname, locale), request.url) 41 | ); 42 | } 43 | 44 | // ref: https://github.com/vercel/next.js/issues/43704#issuecomment-1411186664 45 | // for server component to access url and pathname 46 | // Store current request url in a custom header, which you can read later 47 | const requestHeaders = new Headers(request.headers); 48 | requestHeaders.set(SITE_INTERNAL_HEADER_URL, request.url); 49 | requestHeaders.set(SITE_INTERNAL_HEADER_PATHNAME, request.nextUrl.pathname); 50 | requestHeaders.set(SITE_INTERNAL_HEADER_LOCALE, locale); 51 | 52 | return NextResponse.next({ 53 | request: { 54 | // Apply new request headers 55 | headers: requestHeaders, 56 | }, 57 | }); 58 | }; 59 | 60 | export const config = { 61 | matcher: [ 62 | // Skip all internal paths (_next) 63 | "/((?!_next|favicon|api).*)", 64 | // Optional: only run on root (/) URL 65 | // '/' 66 | ], 67 | }; 68 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "color-name-list" { 2 | interface ColorName { 3 | name: string; 4 | hex: string; 5 | } 6 | const colorNameList: ColorName[]; 7 | export = colorNameList; 8 | } 9 | 10 | declare module "nearest-color" { 11 | interface RGB { 12 | r: number; 13 | g: number; 14 | b: number; 15 | } 16 | interface ColorSpec { 17 | name: string; 18 | value: string; 19 | rgb: RGB; 20 | distance: number; 21 | } 22 | interface ColorMatcher extends NearestColor { 23 | (needle: RGB | string): ColorSpec; 24 | or: (alternateColors: string[] | Record) => ColorMatcher; 25 | } 26 | 27 | interface NearestColor { 28 | (needle: RGB | string, colors?: ColorSpec[]): string; 29 | from: (availableColors: string[] | Record) => ColorMatcher; 30 | } 31 | 32 | const nearestColor: NearestColor; 33 | 34 | export default nearestColor; 35 | } 36 | 37 | declare module "*.svg?url" { 38 | const content: string; 39 | export default content; 40 | } 41 | 42 | type GeneralI18nProps = { 43 | i18n: { 44 | dict: import("@/i18n/index").AppData["i18n"]["dict"]; 45 | }; 46 | locale: import("@/i18n").SupportedLocale; 47 | pathname: string; 48 | }; 49 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | // generate by https://huemint.com/website-magazine/ 8 | white: "#ffffff", 9 | // Pohutukawa 10 | black: "#16245d", 11 | light: "#F8F4EA", 12 | blue: "#0A5CD6", 13 | }, 14 | }, 15 | }, 16 | plugins: [], 17 | }; 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": ".", // This has to be specified if "paths" is. 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "ES2020", 6 | "lib": ["dom", "dom.iterable", "ES2021.String", "esnext"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./src/*"], 26 | "@i18n/*": ["./i18n/*"] 27 | } 28 | }, 29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 30 | "exclude": ["node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------