Files
kiss-translator/src/views/Options/Apis.js
2025-10-10 23:07:18 +08:00

863 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useMemo } from "react";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import LoadingButton from "@mui/lab/LoadingButton";
import MenuItem from "@mui/material/MenuItem";
import FormControlLabel from "@mui/material/FormControlLabel";
import Switch from "@mui/material/Switch";
import { useI18n } from "../../hooks/I18n";
import Typography from "@mui/material/Typography";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import AddIcon from "@mui/icons-material/Add";
import Alert from "@mui/material/Alert";
import Menu from "@mui/material/Menu";
import Grid from "@mui/material/Grid";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { useAlert } from "../../hooks/Alert";
import { useApiList, useApiItem } from "../../hooks/Api";
import { useConfirm } from "../../hooks/Confirm";
import { apiTranslate } from "../../apis";
import Box from "@mui/material/Box";
import { limitNumber, limitFloat } from "../../libs/utils";
import ReusableAutocomplete from "./ReusableAutocomplete";
import ShowMoreButton from "./ShowMoreButton";
import {
OPT_TRANS_DEEPLX,
OPT_TRANS_OLLAMA,
OPT_TRANS_CUSTOMIZE,
OPT_TRANS_NIUTRANS,
OPT_TRANS_BUILTINAI,
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
DEFAULT_HTTP_TIMEOUT,
DEFAULT_BATCH_INTERVAL,
DEFAULT_BATCH_SIZE,
DEFAULT_BATCH_LENGTH,
DEFAULT_CONTEXT_SIZE,
OPT_ALL_TYPES,
API_SPE_TYPES,
BUILTIN_STONES,
BUILTIN_PLACEHOLDERS,
BUILTIN_PLACETAGS,
} from "../../config";
function TestButton({ api }) {
const i18n = useI18n();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleApiTest = async () => {
try {
setLoading(true);
const [text] = await apiTranslate({
text: "hello world",
fromLang: "en",
toLang: "zh-CN",
apiSetting: { ...api },
useCache: false,
usePool: false,
});
if (!text) {
throw new Error("empty result");
}
alert.success(i18n("test_success"));
} catch (err) {
// alert.error(`${i18n("test_failed")}: ${err.message}`);
let msg = err.message;
try {
msg = JSON.stringify(JSON.parse(err.message), null, 2);
} catch (err) {
// skip
}
alert.error(
<>
<div>{i18n("test_failed")}</div>
{msg === err.message ? (
<div
style={{
maxWidth: 400,
}}
>
{msg}
</div>
) : (
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
)}
</>
);
} finally {
setLoading(false);
}
};
return (
<LoadingButton
size="small"
variant="outlined"
onClick={handleApiTest}
loading={loading}
>
{i18n("click_test")}
</LoadingButton>
);
}
function ApiFields({ apiSlug, isUserApi, deleteApi }) {
const { api, update, reset } = useApiItem(apiSlug);
const i18n = useI18n();
const [formData, setFormData] = useState({});
const [isModified, setIsModified] = useState(false);
const [showMore, setShowMore] = useState(false);
const confirm = useConfirm();
useEffect(() => {
if (api) {
setFormData(api);
}
}, [api]);
useEffect(() => {
if (!api) return;
const hasChanged = JSON.stringify(api) !== JSON.stringify(formData);
setIsModified(hasChanged);
}, [api, formData]);
const handleChange = (e) => {
let { name, value, type, checked } = e.target;
if (type === "checkbox" || type === "switch") {
value = checked;
}
// if (value === "true") value = true;
// if (value === "false") value = false;
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
case "httpTimeout":
value = limitNumber(value, 5000, 60000);
break;
case "temperature":
value = limitFloat(value, 0, 2);
break;
case "maxTokens":
value = limitNumber(value, 0, 2 ** 15);
break;
case "batchInterval":
value = limitNumber(value, 100, 10000);
break;
case "batchSize":
value = limitNumber(value, 1, 100);
break;
case "batchLength":
value = limitNumber(value, 1000, 100000);
break;
case "contextSize":
value = limitNumber(value, 1, 20);
break;
default:
}
setFormData((prevData) => ({
...prevData,
[name]: value,
}));
};
const handleSave = () => {
// 过滤掉 api 对象中不存在的字段
// const updatedFields = Object.keys(formData).reduce((acc, key) => {
// if (api && Object.keys(api).includes(key)) {
// acc[key] = formData[key];
// }
// return acc;
// }, {});
// update(updatedFields);
update(formData);
};
const handleReset = () => {
reset();
};
const handleDelete = async () => {
const isConfirmed = await confirm({
confirmText: i18n("delete"),
cancelText: i18n("cancel"),
});
if (isConfirmed) {
deleteApi(apiSlug);
}
};
const {
url = "",
key = "",
model = "",
apiType,
systemPrompt = "",
subtitlePrompt = "",
// userPrompt = "",
customHeader = "",
customBody = "",
think = false,
thinkIgnore = "",
fetchLimit = DEFAULT_FETCH_LIMIT,
fetchInterval = DEFAULT_FETCH_INTERVAL,
httpTimeout = DEFAULT_HTTP_TIMEOUT,
dictNo = "",
memoryNo = "",
reqHook = "",
resHook = "",
temperature = 0,
maxTokens = 256,
apiName = "",
isDisabled = false,
useBatchFetch = false,
batchInterval = DEFAULT_BATCH_INTERVAL,
batchSize = DEFAULT_BATCH_SIZE,
batchLength = DEFAULT_BATCH_LENGTH,
useContext = false,
contextSize = DEFAULT_CONTEXT_SIZE,
tone = "neutral",
placeholder = BUILTIN_PLACEHOLDERS[0],
placetag = BUILTIN_PLACETAGS[0],
// aiTerms = false,
} = formData;
const keyHelper = useMemo(
() => (API_SPE_TYPES.mulkeys.has(apiType) ? i18n("mulkeys_help") : ""),
[apiType, i18n]
);
return (
<Stack spacing={3}>
<TextField
size="small"
label={i18n("api_name")}
name="apiName"
value={apiName}
onChange={handleChange}
/>
{!API_SPE_TYPES.machine.has(apiType) &&
apiType !== OPT_TRANS_BUILTINAI && (
<>
<TextField
size="small"
label={"URL"}
name="url"
value={url}
onChange={handleChange}
multiline={apiType === OPT_TRANS_DEEPLX}
maxRows={10}
helperText={
apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
}
/>
<TextField
size="small"
label={"KEY"}
name="key"
value={key}
onChange={handleChange}
multiline={API_SPE_TYPES.mulkeys.has(apiType)}
maxRows={10}
helperText={keyHelper}
/>
</>
)}
{API_SPE_TYPES.ai.has(apiType) && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={6} lg={3}>
{/* todo 改成 ReusableAutocomplete 可选择和填写模型 */}
<TextField
size="small"
fullWidth
label={"MODEL"}
name="model"
value={model}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<ReusableAutocomplete
freeSolo
size="small"
fullWidth
options={BUILTIN_STONES}
name="tone"
label={i18n("translation_style")}
value={tone}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={"Temperature"}
type="number"
name="temperature"
value={temperature}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={"Max Tokens"}
type="number"
name="maxTokens"
value={maxTokens}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
<TextField
size="small"
label={"SYSTEM PROMPT"}
name="systemPrompt"
value={systemPrompt}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("system_prompt_helper")}
/>
<TextField
size="small"
label={"SUBTITLE PROMPT"}
name="subtitlePrompt"
value={subtitlePrompt}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("system_prompt_helper")}
/>
{/* <TextField
size="small"
label={"USER PROMPT"}
name="userPrompt"
value={userPrompt}
onChange={handleChange}
multiline
maxRows={10}
/> */}
</>
)}
{apiType === OPT_TRANS_OLLAMA && (
<>
<TextField
select
size="small"
name="think"
value={think}
label={i18n("if_think")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("nothink")}</MenuItem>
<MenuItem value={true}>{i18n("think")}</MenuItem>
</TextField>
<TextField
size="small"
label={i18n("think_ignore")}
name="thinkIgnore"
value={thinkIgnore}
onChange={handleChange}
/>
</>
)}
{apiType === OPT_TRANS_NIUTRANS && (
<>
<TextField
size="small"
label={"DictNo"}
name="dictNo"
value={dictNo}
onChange={handleChange}
/>
<TextField
size="small"
label={"MemoryNo"}
name="memoryNo"
value={memoryNo}
onChange={handleChange}
/>
</>
)}
{apiType === OPT_TRANS_CUSTOMIZE && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/>
</>
)}
{API_SPE_TYPES.batch.has(api.apiType) && (
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
size="small"
name="useBatchFetch"
value={useBatchFetch}
label={i18n("use_batch_fetch")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_interval")}
type="number"
name="batchInterval"
value={batchInterval}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_size")}
type="number"
name="batchSize"
value={batchSize}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("batch_length")}
type="number"
name="batchLength"
value={batchLength}
onChange={handleChange}
/>
</Grid>
</Grid>
</Box>
)}
{API_SPE_TYPES.context.has(api.apiType) && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={6} lg={3}>
{" "}
<TextField
select
size="small"
fullWidth
name="useContext"
value={useContext}
label={i18n("use_context")}
onChange={handleChange}
>
<MenuItem value={false}>{i18n("disable")}</MenuItem>
<MenuItem value={true}>{i18n("enable")}</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
{" "}
<TextField
size="small"
fullWidth
label={i18n("context_size")}
type="number"
name="contextSize"
value={contextSize}
onChange={handleChange}
/>
</Grid>
</Grid>
</Box>
</>
)}
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("fetch_limit")}
type="number"
name="fetchLimit"
value={fetchLimit}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("fetch_interval")}
type="number"
name="fetchInterval"
value={fetchInterval}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
size="small"
fullWidth
label={i18n("http_timeout")}
type="number"
name="httpTimeout"
value={httpTimeout}
onChange={handleChange}
/>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
{showMore && (
<>
<Box>
<Grid container spacing={2} columns={12}>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
size="small"
name="placeholder"
value={placeholder}
label={i18n("api_placeholder")}
onChange={handleChange}
>
{BUILTIN_PLACEHOLDERS.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={12} sm={12} md={6} lg={3}>
<TextField
select
fullWidth
size="small"
name="placetag"
value={placetag}
label={i18n("api_placetag")}
onChange={handleChange}
>
{BUILTIN_PLACETAGS.map((item) => (
<MenuItem key={item} value={item}>
{`<${item}>`}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
{apiType !== OPT_TRANS_BUILTINAI && (
<>
{" "}
<TextField
size="small"
label={i18n("custom_header")}
name="customHeader"
value={customHeader}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_header_help")}
/>
<TextField
size="small"
label={i18n("custom_body")}
name="customBody"
value={customBody}
onChange={handleChange}
multiline
maxRows={10}
helperText={i18n("custom_body_help")}
/>
</>
)}
{apiType !== OPT_TRANS_CUSTOMIZE &&
apiType !== OPT_TRANS_BUILTINAI && (
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
/>
</>
)}
</>
)}
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
<Button
size="small"
variant="contained"
onClick={handleSave}
disabled={!isModified}
>
{i18n("save")}
</Button>
<TestButton api={formData} />
<Button size="small" variant="outlined" onClick={handleReset}>
{i18n("restore_default")}
</Button>
{isUserApi && (
<Button
size="small"
variant="outlined"
color="error"
onClick={handleDelete}
>
{i18n("delete")}
</Button>
)}
<FormControlLabel
control={
<Switch
size="small"
name="isDisabled"
checked={isDisabled}
onChange={handleChange}
/>
}
label={i18n("is_disabled")}
/>
<ShowMoreButton showMore={showMore} onChange={setShowMore} />
</Stack>
{/* {apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>} */}
</Stack>
);
}
function ApiAccordion({ api, isUserApi, deleteApi }) {
const [expanded, setExpanded] = useState(false);
const handleChange = (e) => {
setExpanded((pre) => !pre);
};
return (
<Accordion expanded={expanded} onChange={handleChange}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography
sx={{
opacity: api.isDisabled ? 0.5 : 1,
overflowWrap: "anywhere",
}}
>
{`[${api.apiType}] ${api.apiName}`}
</Typography>
</AccordionSummary>
<AccordionDetails>
{expanded && (
<ApiFields
apiSlug={api.apiSlug}
isUserApi={isUserApi}
deleteApi={deleteApi}
/>
)}
</AccordionDetails>
</Accordion>
);
}
export default function Apis() {
const i18n = useI18n();
const { userApis, builtinApis, addApi, deleteApi } = useApiList();
const apiTypes = useMemo(
() =>
OPT_ALL_TYPES.map((type) => ({
type,
label: type,
})),
[]
);
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleMenuItemClick = (apiType) => {
addApi(apiType);
handleClose();
};
return (
<Box>
<Stack spacing={3}>
<Alert severity="info">
{i18n("about_api")}
<br />
{i18n("about_api_2")}
</Alert>
<Box>
<Button
size="small"
id="add-api-button"
variant="contained"
onClick={handleClick}
aria-controls={open ? "add-api-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
endIcon={<KeyboardArrowDownIcon />}
startIcon={<AddIcon />}
>
{i18n("add")}
</Button>
<Menu
id="add-api-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
"aria-labelledby": "add-api-button",
}}
>
{apiTypes.map((apiOption) => (
<MenuItem
key={apiOption.type}
onClick={() => handleMenuItemClick(apiOption.type)}
>
{apiOption.label}
</MenuItem>
))}
</Menu>
</Box>
<Box>
{userApis.map((api) => (
<ApiAccordion
key={api.apiSlug}
api={api}
isUserApi={true}
deleteApi={deleteApi}
/>
))}
</Box>
<Box>
{builtinApis.map((api) => (
<ApiAccordion key={api.apiSlug} api={api} />
))}
</Box>
</Stack>
</Box>
);
}