Upstream franciscoBSalgueiro/en-croissant state of the touched files before 243599403a
This commit is contained in:
commit
3b3d5ed500
2 files changed
+808
No files matched your search
@@ -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 (
|
||||
<Stack h="100%">
|
||||
<AddEngine opened={opened} setOpened={setOpened} />
|
||||
<Group align="baseline" py="sm" pl="lg">
|
||||
<Title>{t("Engines.Title")}</Title>
|
||||
<OpenFolderButton base="Engines" folder="engines" />
|
||||
</Group>
|
||||
<Group grow flex={1} style={{ overflow: "hidden" }} align="start" px="md" pb="md">
|
||||
<Paper withBorder style={{ borderWidth: 2 }} h="100%">
|
||||
<Stack gap={0} h="100%" style={{ overflow: "hidden" }}>
|
||||
<Group p="xs" gap="xs">
|
||||
<Input
|
||||
size="sm"
|
||||
style={{ flexGrow: 1 }}
|
||||
leftSection={<IconSearch size="1rem" />}
|
||||
placeholder={t("Common.Search")}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||
/>
|
||||
<Tooltip label={t("Common.AddNew")}>
|
||||
<ActionIcon variant="default" size="lg" onClick={() => setOpened(true)}>
|
||||
<IconPlus size="1rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Divider />
|
||||
<ScrollArea flex={1}>
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing={{ base: "md", md: "sm" }} p="xs">
|
||||
{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 (
|
||||
<GenericCard
|
||||
id={index}
|
||||
key={item.id}
|
||||
isSelected={selected === index}
|
||||
setSelected={setSelected}
|
||||
error={undefined}
|
||||
Header={<EngineName engine={item} />}
|
||||
stats={stats}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</ScrollArea>
|
||||
{filteredEngines.length === 0 && (
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="sm">
|
||||
<ThemeIcon size={64} radius="100%" variant="light" color="gray">
|
||||
<IconCpu size={32} />
|
||||
</ThemeIcon>
|
||||
<Text c="dimmed" fw={500} ta="center">
|
||||
{hasSearch ? t("Common.NoResults") : t("Engines.Empty.NoInstalled")}
|
||||
</Text>
|
||||
{!hasSearch && !hasEngines && (
|
||||
<Text c="dimmed" size="sm" ta="center">
|
||||
{t("Engines.Empty.AddHint")}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
{!selectedEngine || selected === undefined ? (
|
||||
<Paper withBorder style={{ borderWidth: 2 }} p="md" h="100%">
|
||||
<Center h="100%">
|
||||
<Stack align="center" gap="sm">
|
||||
<ThemeIcon size={80} radius="100%" variant="light" color="gray">
|
||||
<IconCpu size={40} />
|
||||
</ThemeIcon>
|
||||
<Text c="dimmed" fw={500} size="lg">
|
||||
{t("Engines.Settings.NoEngine")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
) : (
|
||||
<Paper withBorder style={{ borderWidth: 2 }} p="md" h="100%">
|
||||
{selectedEngine.type === "local" ? (
|
||||
<EngineSettings selected={selected} setSelected={setSelected} />
|
||||
) : (
|
||||
<Stack>
|
||||
<Divider variant="dashed" label={t("Common.GeneralSettings")} />
|
||||
|
||||
<TextInput
|
||||
w="50%"
|
||||
label={t("Common.Name")}
|
||||
value={selectedEngine.name}
|
||||
onChange={(e) => {
|
||||
setEngines(async (prev) => {
|
||||
const copy = [...(await prev)];
|
||||
copy[selected].name = e.currentTarget.value;
|
||||
return copy;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider variant="dashed" label={t("Engines.Settings.AdvancedSettings")} />
|
||||
<Stack w="50%">
|
||||
<Text fw="bold">{t("Engines.Settings.NumOfLines")}</Text>
|
||||
<LinesSlider
|
||||
value={
|
||||
Number(
|
||||
selectedEngine.settings?.find((setting) => 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;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group justify="right">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
setEngines(async (prev) => {
|
||||
const copy = [...(await prev)];
|
||||
copy.splice(selected, 1);
|
||||
return copy;
|
||||
});
|
||||
setSelected(null);
|
||||
}}
|
||||
>
|
||||
{t("Common.Remove")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<ScrollArea h="100%" offsetScrollbars>
|
||||
<Stack>
|
||||
<Divider variant="dashed" label={t("Common.GeneralSettings")} />
|
||||
<Group grow align="start" wrap="nowrap">
|
||||
<Stack>
|
||||
<Group wrap="nowrap" w="100%">
|
||||
<TextInput
|
||||
flex={1}
|
||||
label={t("Common.Name")}
|
||||
value={engine.name}
|
||||
onChange={(e) => setEngine({ ...engine, name: e.currentTarget.value })}
|
||||
/>
|
||||
<TextInput
|
||||
label={t("Common.Version")}
|
||||
w="5rem"
|
||||
value={engine.version}
|
||||
placeholder="?"
|
||||
onChange={(e) => setEngine({ ...engine, version: e.currentTarget.value })}
|
||||
/>
|
||||
</Group>
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
label="ELO"
|
||||
value={engine.elo || undefined}
|
||||
min={0}
|
||||
placeholder={t("Common.Unknown")}
|
||||
onChange={(v) =>
|
||||
setEngine({
|
||||
...engine,
|
||||
elo: typeof v === "number" ? v : undefined,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Center>
|
||||
{engine.image ? (
|
||||
<Paper withBorder style={{ cursor: "pointer" }} onClick={changeImage}>
|
||||
<LocalImage
|
||||
src={engine.image}
|
||||
alt={engine.name}
|
||||
mah="10rem"
|
||||
maw="100%"
|
||||
fit="contain"
|
||||
/>
|
||||
</Paper>
|
||||
) : (
|
||||
<ActionIcon
|
||||
size="10rem"
|
||||
variant="subtle"
|
||||
styles={{
|
||||
root: {
|
||||
border: "1px dashed",
|
||||
},
|
||||
}}
|
||||
onClick={changeImage}
|
||||
>
|
||||
<IconPhotoPlus size="2.5rem" />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Center>
|
||||
</Group>
|
||||
<Divider variant="dashed" label={t("Engines.Settings.SearchSettings")} />
|
||||
<GoModeInput
|
||||
goMode={engine.go || null}
|
||||
setGoMode={(v) => setEngine({ ...engine, go: v })}
|
||||
/>
|
||||
|
||||
<Divider variant="dashed" label={t("Engines.Settings.AdvancedSettings")} />
|
||||
<SimpleGrid cols={2}>
|
||||
{completeOptions
|
||||
.filter((option: { type: string }) => option.type !== "check")
|
||||
.map((option: any) => {
|
||||
return match(option)
|
||||
.with({ type: "spin", value: P.select() }, (v: any) => {
|
||||
return (
|
||||
<NumberInput
|
||||
key={v.name}
|
||||
label={v.name}
|
||||
min={Number(v.min)}
|
||||
max={Number(v.max)}
|
||||
value={Number(v.value)}
|
||||
onChange={(e) => setSetting(v.name, e, Number(v.default))}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with({ type: "combo", value: P.select() }, (v: any) => {
|
||||
return (
|
||||
<Select
|
||||
key={v.name}
|
||||
label={v.name}
|
||||
data={v.var}
|
||||
value={v.value}
|
||||
onChange={(e) => setSetting(v.name, e, v.default)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.with({ type: "string", value: P.select() }, (v: any) => {
|
||||
if (v.name.toLowerCase() === "syzygypath") {
|
||||
return (
|
||||
<Group key={v.name} align="end" wrap="nowrap">
|
||||
<TextInput
|
||||
flex={1}
|
||||
label={v.name}
|
||||
placeholder={`/path/to/tb${syzygyPathSeparator}/path/to/tb2`}
|
||||
value={v.value || ""}
|
||||
onChange={(e) => setSetting(v.name, e.currentTarget.value, v.default)}
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
leftSection={<IconFolder size="1rem" />}
|
||||
onClick={async () => {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
directory: true,
|
||||
});
|
||||
if (!selected) return;
|
||||
|
||||
const directories = Array.isArray(selected) ? selected : [selected];
|
||||
setSetting(v.name, directories.join(syzygyPathSeparator), v.default);
|
||||
}}
|
||||
>
|
||||
{t("Common.Open")}
|
||||
</Button>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
if (v.name.toLowerCase().includes("file")) {
|
||||
const file = v.value ? new File([v.value], v.value) : null;
|
||||
return (
|
||||
<FileInput
|
||||
key={v.name}
|
||||
clearable
|
||||
label={v.name}
|
||||
value={file}
|
||||
onClick={async () => {
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
});
|
||||
if (!selected) return;
|
||||
setSetting(v.name, selected as string, v.default);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
if (e === null) {
|
||||
setSetting(v.name, null, v.default);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TextInput
|
||||
key={v.name}
|
||||
label={v.name}
|
||||
value={v.value || ""}
|
||||
onChange={(e) => setSetting(v.name, e.currentTarget.value, v.default)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.otherwise(() => null);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
<SimpleGrid cols={2}>
|
||||
{completeOptions
|
||||
.filter((option) => option.type === "check")
|
||||
.map((o) => {
|
||||
return (
|
||||
<Checkbox
|
||||
key={o.value.name}
|
||||
label={o.value.name}
|
||||
checked={!!o.value.value}
|
||||
disabled={o.value.name === "UCI_Chess960"}
|
||||
onChange={(e) =>
|
||||
setSetting(o.value.name, e.currentTarget.checked, o.value.default as boolean)
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
|
||||
<Group justify="end">
|
||||
<Button variant="default" onClick={() => toggleJSONModal(true)}>
|
||||
{t("Engines.Settings.EditJSON")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
setEngine({
|
||||
...engine,
|
||||
settings: options?.options
|
||||
.filter((option) => requiredEngineSettings.includes(option.value.name))
|
||||
.filter((option) => option.type !== "button")
|
||||
.map((option) => ({
|
||||
name: option.value.name,
|
||||
value: option.value.default as string | number | boolean | null,
|
||||
})),
|
||||
})
|
||||
}
|
||||
>
|
||||
{t("Engines.Settings.Reset")}
|
||||
</Button>
|
||||
<Button
|
||||
leftSection={<IconCopy size="1rem" />}
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
const duplicatedEngine: LocalEngine = {
|
||||
...engine,
|
||||
id: crypto.randomUUID(),
|
||||
name: `${engine.name} (Copy)`,
|
||||
};
|
||||
setEngines(async (prev) => {
|
||||
const copy = [...(await prev)];
|
||||
copy.splice(selected + 1, 0, duplicatedEngine);
|
||||
return copy;
|
||||
});
|
||||
setSelected(selected + 1);
|
||||
}}
|
||||
>
|
||||
{t("Common.Duplicate")}
|
||||
</Button>
|
||||
<Button color="red" onClick={() => toggleDeleteModal()}>
|
||||
{t("Common.Remove")}
|
||||
</Button>
|
||||
</Group>
|
||||
<ConfirmModal
|
||||
title={t("Engines.Remove.Title")}
|
||||
description={t("Engines.Remove.Message")}
|
||||
opened={deleteModal}
|
||||
onClose={toggleDeleteModal}
|
||||
onConfirm={() => {
|
||||
setEngines(async (prev) => (await prev).filter((e) => e.name !== engine.name));
|
||||
setSelected(null);
|
||||
toggleDeleteModal();
|
||||
}}
|
||||
confirmLabel={t("Common.Remove")}
|
||||
/>
|
||||
</Stack>
|
||||
<JSONModal
|
||||
key={engine.name}
|
||||
opened={jsonModal}
|
||||
toggleOpened={toggleJSONModal}
|
||||
engine={engine}
|
||||
setEngine={(v) =>
|
||||
setEngines(async (prev) => {
|
||||
const copy = [...(await prev)];
|
||||
copy[selected] = v;
|
||||
return copy;
|
||||
})
|
||||
}
|
||||
/>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
function JSONModal({
|
||||
opened,
|
||||
toggleOpened,
|
||||
engine,
|
||||
setEngine,
|
||||
}: {
|
||||
opened: boolean;
|
||||
toggleOpened: () => void;
|
||||
engine: Engine;
|
||||
setEngine: (v: Engine) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [value, setValue] = useState(JSON.stringify(engine, null, 2));
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
return (
|
||||
<Modal opened={opened} onClose={toggleOpened} title={t("Engines.Settings.EditJSON")} size="xl">
|
||||
<JsonInput
|
||||
autosize
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e);
|
||||
setError(null);
|
||||
}}
|
||||
error={error}
|
||||
/>
|
||||
<Space h="md" />
|
||||
<Button
|
||||
onClick={() => {
|
||||
const parseRes = engineSchema.safeParse(JSON.parse(value));
|
||||
if (parseRes.success) {
|
||||
setEngine(parseRes.data);
|
||||
setError(null);
|
||||
toggleOpened();
|
||||
} else {
|
||||
setError("Invalid Configuration"); // TODO: show better error message
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("Common.Save")}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function EngineName({ engine }: { engine: Engine }) {
|
||||
const { data: fileExists, isLoading } = useSWRImmutable(
|
||||
["file-exists", engine.type === "local" ? engine.path : null],
|
||||
async ([, path]) => {
|
||||
if (path === null) return false;
|
||||
if (engine.type !== "local") return true;
|
||||
const res = await commands.fileExists(path);
|
||||
return res.status === "ok";
|
||||
},
|
||||
);
|
||||
|
||||
const hasError = engine.type === "local" && !isLoading && !fileExists;
|
||||
|
||||
return (
|
||||
<Group wrap="nowrap">
|
||||
{engine.image ? (
|
||||
<LocalImage src={engine.image} alt={engine.name} h="2.5rem" fit="contain" flex={0} />
|
||||
) : engine.type !== "local" ? (
|
||||
<IconCloud size="2.5rem" />
|
||||
) : (
|
||||
<IconCpu size="2.5rem" />
|
||||
)}
|
||||
<Stack gap={0}>
|
||||
<Text fw="bold" lineClamp={1} c={hasError ? "red" : undefined}>
|
||||
{engine.name} {hasError ? "(file missing)" : ""}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" style={{ wordWrap: "break-word" }} lineClamp={1}>
|
||||
{engine.type === "local" ? engine.path.split(/\/|\\/).slice(-1)[0] : engine.url}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import type { Platform } from "@tauri-apps/plugin-os";
|
||||
import useSWR from "swr";
|
||||
import { z } from "zod";
|
||||
import { type BestMoves, commands, type EngineOptions, type GoMode } from "@/bindings";
|
||||
import { unwrap } from "./unwrap";
|
||||
|
||||
export const requiredEngineSettings = ["MultiPV", "Threads", "Hash"];
|
||||
|
||||
const goModeSchema: z.ZodSchema<GoMode> = z.union([
|
||||
z.object({
|
||||
t: z.literal("Depth"),
|
||||
c: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
t: z.literal("Time"),
|
||||
c: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
t: z.literal("Nodes"),
|
||||
c: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
t: z.literal("Infinite"),
|
||||
}),
|
||||
]);
|
||||
|
||||
const engineSettingsSchema = z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
value: z.string().or(z.number()).or(z.boolean()).nullable(),
|
||||
}),
|
||||
);
|
||||
|
||||
export type EngineSettings = z.infer<typeof engineSettingsSchema>;
|
||||
|
||||
const localEngineSchema = z.object({
|
||||
type: z.literal("local"),
|
||||
id: z.string().default(() => crypto.randomUUID()),
|
||||
name: z.string(),
|
||||
version: z.string(),
|
||||
path: z.string(),
|
||||
image: z.string().nullish(),
|
||||
elo: z.number().nullish(),
|
||||
downloadSize: z.number().nullish(),
|
||||
downloadLink: z.string().nullish(),
|
||||
loaded: z.boolean().nullish(),
|
||||
go: goModeSchema.nullish(),
|
||||
enabled: z.boolean().nullish(),
|
||||
settings: engineSettingsSchema.nullish(),
|
||||
});
|
||||
|
||||
export type LocalEngine = z.output<typeof localEngineSchema>;
|
||||
|
||||
const remoteEngineSchema = z.object({
|
||||
type: z.enum(["chessdb", "lichess"]),
|
||||
id: z.string().default(() => crypto.randomUUID()),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
image: z.string().nullish(),
|
||||
loaded: z.boolean().nullish(),
|
||||
enabled: z.boolean().nullish(),
|
||||
go: goModeSchema.nullish(),
|
||||
settings: engineSettingsSchema.nullish(),
|
||||
});
|
||||
|
||||
export type RemoteEngine = z.output<typeof remoteEngineSchema>;
|
||||
|
||||
export const engineSchema = z.union([localEngineSchema, remoteEngineSchema]);
|
||||
export type Engine = z.output<typeof engineSchema>;
|
||||
|
||||
export function stopEngine(engine: LocalEngine, tab: string): Promise<void> {
|
||||
return commands.stopEngine(engine.id, tab).then((r) => {
|
||||
unwrap(r);
|
||||
});
|
||||
}
|
||||
|
||||
export function killEngine(engine: LocalEngine, tab: string): Promise<void> {
|
||||
return commands.killEngine(engine.id, tab).then((r) => {
|
||||
unwrap(r);
|
||||
});
|
||||
}
|
||||
|
||||
export function getBestMoves(
|
||||
engine: LocalEngine,
|
||||
tab: string,
|
||||
goMode: GoMode,
|
||||
options: EngineOptions,
|
||||
): Promise<[number, BestMoves[]] | null> {
|
||||
return commands
|
||||
.getBestMoves(engine.id, engine.path, tab, goMode, options)
|
||||
.then((r) => unwrap(r));
|
||||
}
|
||||
|
||||
export function useDefaultEngines(os: Platform | undefined, opened: boolean) {
|
||||
const { data, error, isLoading } = useSWR(opened ? os : null, async (os: Platform) => {
|
||||
const bmi2: boolean = await commands.isBmi2Compatible();
|
||||
const data = await fetch(`https://www.encroissant.org/engines?os=${os}&bmi2=${bmi2}`, {
|
||||
method: "GET",
|
||||
});
|
||||
if (!data.ok) {
|
||||
throw new Error("Failed to fetch engines");
|
||||
}
|
||||
return (await data.json()).filter(
|
||||
(e: { os: Platform; bmi2: boolean }) => e.os === os && e.bmi2 === bmi2,
|
||||
);
|
||||
});
|
||||
return {
|
||||
defaultEngines: data as LocalEngine[],
|
||||
error,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user