From 3b3d5ed5002258582d8cb64b557ea4e3a3022512 Mon Sep 17 00:00:00 2001 From: upstream Date: Sat, 26 Sep 2026 09:42:46 +0200 Subject: [PATCH] Upstream franciscoBSalgueiro/en-croissant state of the touched files before 243599403a --- src/components/engines/EnginesPage.tsx | 695 +++++++++++++++++++++++++ src/utils/engines.ts | 113 ++++ 2 files changed, 808 insertions(+) create mode 100644 src/components/engines/EnginesPage.tsx create mode 100644 src/utils/engines.ts diff --git a/src/components/engines/EnginesPage.tsx b/src/components/engines/EnginesPage.tsx new file mode 100644 index 0000000..b4a1c41 --- /dev/null +++ b/src/components/engines/EnginesPage.tsx @@ -0,0 +1,695 @@ +import { + ActionIcon, + Button, + Center, + Checkbox, + Divider, + FileInput, + Group, + Input, + JsonInput, + Modal, + NumberInput, + Paper, + ScrollArea, + Select, + SimpleGrid, + Space, + Stack, + Text, + TextInput, + ThemeIcon, + Title, + Tooltip, +} from "@mantine/core"; +import { useToggle } from "@mantine/hooks"; +import { + IconCloud, + IconCopy, + IconCpu, + IconFolder, + IconPhotoPlus, + IconPlus, + IconSearch, +} from "@tabler/icons-react"; +import { useNavigate } from "@tanstack/react-router"; +import { platform } from "@tauri-apps/plugin-os"; +import { open } from "@tauri-apps/plugin-dialog"; +import { useAtom } from "jotai"; +import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useSWRImmutable from "swr/immutable"; +import { match, P } from "ts-pattern"; +import { commands } from "@/bindings"; +import { Route } from "@/routes/engines"; +import { enginesAtom } from "@/state/atoms"; +import { + type Engine, + engineSchema, + type LocalEngine, + requiredEngineSettings, +} from "@/utils/engines"; +import { unwrap } from "@/utils/unwrap"; +import ConfirmModal from "../common/ConfirmModal"; +import GenericCard from "../common/GenericCard"; +import GoModeInput from "../common/GoModeInput"; +import LocalImage from "../common/LocalImage"; +import OpenFolderButton from "../common/OpenFolderButton"; +import LinesSlider from "../panels/analysis/LinesSlider"; +import AddEngine from "./AddEngine"; + +export default function EnginesPage() { + const { t } = useTranslation(); + + const [engines, setEngines] = useAtom(enginesAtom); + const enginesList = useMemo(() => engines ?? [], [engines]); + const [opened, setOpened] = useState(false); + const [search, setSearch] = useState(""); + const { selected } = Route.useSearch(); + const navigate = useNavigate(); + const setSelected = (v: number | null) => { + navigate({ to: "/engines", search: { selected: v ?? undefined } }); + }; + + const selectedEngine = selected !== undefined ? enginesList[selected] : null; + const filteredEngines = useMemo(() => { + const normalizedSearch = search.trim().toLowerCase(); + const indexedEngines = enginesList.map((item, index) => ({ item, index })); + + if (!normalizedSearch) { + return indexedEngines; + } + + return indexedEngines.filter(({ item }) => { + const values = [ + item.name, + item.id, + item.type, + item.type === "local" ? item.path : "", + item.type === "local" ? (item.version ?? "") : "", + item.type === "local" && item.elo ? item.elo.toString() : "", + ]; + + return values.some((value) => value.toLowerCase().includes(normalizedSearch)); + }); + }, [enginesList, search]); + const hasSearch = search.trim().length > 0; + const hasEngines = enginesList.length > 0; + + return ( + + + + {t("Engines.Title")} + + + + + + + } + placeholder={t("Common.Search")} + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + /> + + setOpened(true)}> + + + + + + + + {filteredEngines.map(({ item, index }) => { + const stats = + item.type === "local" + ? [ + { + label: "ELO", + value: item.elo ? item.elo.toString() : "??", + }, + ] + : [{ label: "Type", value: "Cloud" }]; + if (item.type === "local" && item.version) { + stats.push({ + label: t("Common.Version"), + value: item.version, + }); + } + return ( + } + stats={stats} + /> + ); + })} + + + {filteredEngines.length === 0 && ( +
+ + + + + + {hasSearch ? t("Common.NoResults") : t("Engines.Empty.NoInstalled")} + + {!hasSearch && !hasEngines && ( + + {t("Engines.Empty.AddHint")} + + )} + +
+ )} +
+
+ {!selectedEngine || selected === undefined ? ( + +
+ + + + + + {t("Engines.Settings.NoEngine")} + + +
+
+ ) : ( + + {selectedEngine.type === "local" ? ( + + ) : ( + + + + { + setEngines(async (prev) => { + const copy = [...(await prev)]; + copy[selected].name = e.currentTarget.value; + return copy; + }); + }} + /> + + + + {t("Engines.Settings.NumOfLines")} + setting.name === "MultiPV") + ?.value, + ) || 1 + } + setValue={(v) => { + setEngines(async (prev) => { + const copy = [...(await prev)]; + const setting = copy[selected].settings?.find( + (setting) => setting.name === "MultiPV", + ); + if (setting) { + setting.value = v; + } else { + copy[selected].settings?.push({ + name: "MultiPV", + value: v, + }); + } + return copy; + }); + }} + /> + + + + + + + )} + + )} +
+
+ ); +} + +function EngineSettings({ + selected, + setSelected, +}: { + selected: number; + setSelected: (v: number | null) => void; +}) { + const { t } = useTranslation(); + + const [engines, setEngines] = useAtom(enginesAtom); + const engine = engines![selected] as LocalEngine; + const { data: options } = useSWRImmutable(["engine-config", engine.path], async ([, path]) => { + return unwrap(await commands.getEngineConfig(path)); + }); + + function setEngine(newEngine: LocalEngine) { + setEngines(async (prev) => { + const copy = [...(await prev)]; + copy[selected] = newEngine; + return copy; + }); + } + + useEffect(() => { + if (options) { + const settings = [...(engine.settings || [])]; + const missing = requiredEngineSettings.filter( + (field) => !settings.find((setting) => setting.name === field), + ); + for (const field of requiredEngineSettings) { + if (!settings.find((setting) => setting.name === field)) { + const option = options.options.find((option) => option.value.name === field); + if (option && option.type !== "button") { + settings.push({ + name: field, + value: option.value.default as string | number | boolean | null, + }); + } + } + } + if (missing.length > 0) { + setEngine({ ...engine, settings }); + } + } + }, [options]); + + const completeOptions = + options?.options + .filter((option) => option.type !== "button") + .map((option) => { + const setting = engine.settings?.find((setting) => setting.name === option.value.name); + return { + ...option, + value: { + ...option.value, + value: setting?.value !== undefined ? setting.value : option.value.default, + }, + }; + }) || []; + + function changeImage() { + open({ + title: "Select image", + }).then((res) => { + if (typeof res === "string") { + setEngine({ ...engine, image: res }); + } + }); + } + + function setSetting( + name: string, + value: string | number | boolean | null, + def: string | number | boolean | null, + ) { + const newSettings = engine.settings || []; + const setting = newSettings.find((setting) => setting.name === name); + if (setting) { + setting.value = value; + } else { + newSettings.push({ name, value }); + } + if (value !== def || requiredEngineSettings.includes(name)) { + setEngine({ + ...engine, + settings: newSettings, + }); + } else { + setEngine({ + ...engine, + settings: newSettings.filter((setting) => setting.name !== name), + }); + } + } + + const [deleteModal, toggleDeleteModal] = useToggle(); + const [jsonModal, toggleJSONModal] = useToggle(); + const syzygyPathSeparator = platform() === "windows" ? ";" : ":"; + + return ( + + + + + + + setEngine({ ...engine, name: e.currentTarget.value })} + /> + setEngine({ ...engine, version: e.currentTarget.value })} + /> + + + + setEngine({ + ...engine, + elo: typeof v === "number" ? v : undefined, + }) + } + /> + + +
+ {engine.image ? ( + + + + ) : ( + + + + )} +
+
+ + setEngine({ ...engine, go: v })} + /> + + + + {completeOptions + .filter((option: { type: string }) => option.type !== "check") + .map((option: any) => { + return match(option) + .with({ type: "spin", value: P.select() }, (v: any) => { + return ( + setSetting(v.name, e, Number(v.default))} + /> + ); + }) + .with({ type: "combo", value: P.select() }, (v: any) => { + return ( +