Files
kiss-translator/src/views/Options/Apis.js

852 lines
23 KiB
JavaScript
Raw Normal View History

import { useState, useEffect, useMemo } from "react";
2023-09-06 00:25:46 +08:00
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
2024-04-16 16:39:11 +08:00
import LoadingButton from "@mui/lab/LoadingButton";
import MenuItem from "@mui/material/MenuItem";
2025-07-01 10:54:30 +08:00
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";
2025-09-25 23:08:39 +08:00
import ShowMoreButton from "./ShowMoreButton";
2023-09-06 00:25:46 +08:00
import {
2024-04-20 14:01:34 +08:00
OPT_TRANS_DEEPLX,
2024-04-28 21:43:20 +08:00
OPT_TRANS_OLLAMA,
2023-09-06 00:25:46 +08:00
OPT_TRANS_CUSTOMIZE,
2024-04-12 11:31:01 +08:00
OPT_TRANS_NIUTRANS,
2025-10-04 21:25:54 +08:00
OPT_TRANS_BUILTINAI,
2024-03-21 15:07:50 +08:00
DEFAULT_FETCH_LIMIT,
DEFAULT_FETCH_INTERVAL,
2025-07-01 12:38:06 +08:00
DEFAULT_HTTP_TIMEOUT,
DEFAULT_BATCH_INTERVAL,
DEFAULT_BATCH_SIZE,
DEFAULT_BATCH_LENGTH,
2025-09-03 20:43:07 +08:00
DEFAULT_CONTEXT_SIZE,
OPT_ALL_TYPES,
API_SPE_TYPES,
BUILTIN_STONES,
2025-10-01 13:22:22 +08:00
BUILTIN_PLACEHOLDERS,
BUILTIN_PLACETAGS,
2023-09-06 00:25:46 +08:00
} from "../../config";
function TestButton({ apiSlug, api }) {
2023-09-06 00:25:46 +08:00
const i18n = useI18n();
const alert = useAlert();
const [loading, setLoading] = useState(false);
const handleApiTest = async () => {
try {
setLoading(true);
const [text] = await apiTranslate({
apiSlug,
2023-09-06 14:57:02 +08:00
text: "hello world",
fromLang: "en",
2023-09-06 00:25:46 +08:00
toLang: "zh-CN",
apiSetting: api,
useCache: false,
2023-09-06 00:25:46 +08:00
});
if (!text) {
2025-07-01 17:03:52 +08:00
throw new Error("empty result");
2023-09-06 00:25:46 +08:00
}
alert.success(i18n("test_success"));
} catch (err) {
2023-11-02 23:35:36 +08:00
// alert.error(`${i18n("test_failed")}: ${err.message}`);
let msg = err.message;
try {
msg = JSON.stringify(JSON.parse(err.message), null, 2);
} catch (err) {
// skip
}
2023-11-02 23:35:36 +08:00
alert.error(
<>
<div>{i18n("test_failed")}</div>
2024-04-12 11:31:01 +08:00
{msg === err.message ? (
<div
style={{
maxWidth: 400,
}}
>
{msg}
</div>
) : (
<pre
style={{
maxWidth: 400,
overflow: "auto",
}}
>
{msg}
</pre>
)}
2023-11-02 23:35:36 +08:00
</>
);
2023-09-06 00:25:46 +08:00
} finally {
setLoading(false);
}
};
return (
2024-04-16 16:39:11 +08:00
<LoadingButton
size="small"
variant="outlined"
2024-04-16 16:39:11 +08:00
onClick={handleApiTest}
loading={loading}
>
2023-09-06 00:25:46 +08:00
{i18n("click_test")}
2024-04-16 16:39:11 +08:00
</LoadingButton>
2023-09-06 00:25:46 +08:00
);
}
function ApiFields({ apiSlug, isUserApi, deleteApi }) {
const { api, update, reset } = useApiItem(apiSlug);
2023-09-06 00:25:46 +08:00
const i18n = useI18n();
const [formData, setFormData] = useState({});
const [isModified, setIsModified] = useState(false);
2025-09-25 23:08:39 +08:00
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]);
2023-09-06 00:25:46 +08:00
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;
2024-03-21 15:07:50 +08:00
switch (name) {
case "fetchLimit":
value = limitNumber(value, 1, 100);
break;
case "fetchInterval":
value = limitNumber(value, 0, 5000);
break;
2025-07-01 12:38:06 +08:00
case "httpTimeout":
2025-09-03 20:43:07 +08:00
value = limitNumber(value, 5000, 60000);
2025-07-01 12:38:06 +08:00
break;
case "temperature":
value = limitFloat(value, 0, 2);
break;
case "maxTokens":
value = limitNumber(value, 0, 2 ** 15);
break;
case "batchInterval":
2025-09-03 12:16:41 +08:00
value = limitNumber(value, 100, 10000);
break;
case "batchSize":
value = limitNumber(value, 1, 100);
break;
case "batchLength":
2025-09-03 12:16:41 +08:00
value = limitNumber(value, 1000, 100000);
break;
2025-09-03 20:43:07 +08:00
case "contextSize":
value = limitNumber(value, 1, 20);
break;
2024-03-21 15:07:50 +08:00
default:
}
setFormData((prevData) => ({
...prevData,
2023-09-06 00:25:46 +08:00
[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"),
2023-09-06 00:25:46 +08:00
});
if (isConfirmed) {
deleteApi(apiSlug);
}
2023-09-06 00:25:46 +08:00
};
const {
url = "",
key = "",
model = "",
apiType,
systemPrompt = "",
// 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",
2025-10-01 13:22:22 +08:00
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]
);
2024-04-12 11:31:01 +08:00
2023-09-06 00:25:46 +08:00
return (
<Stack spacing={3}>
2025-07-01 10:54:30 +08:00
<TextField
size="small"
label={i18n("api_name")}
name="apiName"
value={apiName}
onChange={handleChange}
/>
2025-10-04 21:25:54 +08:00
{!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}
/>
</>
)}
2024-03-21 15:07:50 +08:00
{API_SPE_TYPES.ai.has(apiType) && (
2023-09-06 00:25:46 +08:00
<>
<Box>
<Grid container spacing={2} columns={12}>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
2023-09-06 00:25:46 +08:00
<TextField
size="small"
2024-11-30 00:41:29 +08:00
label={"SYSTEM PROMPT"}
name="systemPrompt"
value={systemPrompt}
2023-09-06 00:25:46 +08:00
onChange={handleChange}
multiline
2024-04-20 14:01:34 +08:00
maxRows={10}
helperText={i18n("system_prompt_helper")}
2023-09-06 00:25:46 +08:00
/>
{/* <TextField
2024-09-30 16:41:58 +08:00
size="small"
2024-11-30 00:41:29 +08:00
label={"USER PROMPT"}
name="userPrompt"
value={userPrompt}
2024-09-30 16:41:58 +08:00
onChange={handleChange}
multiline
maxRows={10}
/> */}
2024-09-30 16:41:58 +08:00
</>
)}
2023-09-06 00:25:46 +08:00
{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 && (
2024-04-12 11:31:01 +08:00
<>
<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 && (
2024-05-12 16:10:11 +08:00
<>
<TextField
size="small"
label={"Request Hook"}
name="reqHook"
value={reqHook}
onChange={handleChange}
multiline
maxRows={10}
2025-09-25 23:08:39 +08:00
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("request_hook_helper")}
</Box>
}
2024-05-12 16:10:11 +08:00
/>
<TextField
size="small"
label={"Response Hook"}
name="resHook"
value={resHook}
onChange={handleChange}
multiline
maxRows={10}
2025-09-25 23:08:39 +08:00
FormHelperTextProps={{
component: "div",
}}
helperText={
<Box component="pre" sx={{ overflowX: "auto" }}>
{i18n("response_hook_helper")}
</Box>
}
2024-05-12 16:10:11 +08:00
/>
</>
2024-04-17 17:38:54 +08:00
)}
{API_SPE_TYPES.batch.has(api.apiType) && (
<Box>
<Grid container spacing={2} columns={12}>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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) && (
2025-09-03 20:43:07 +08:00
<>
<Box>
<Grid container spacing={2} columns={12}>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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}>
2025-09-25 23:08:39 +08:00
<Grid item xs={12} sm={12} md={6} lg={3}>
2025-09-03 20:43:07 +08:00
<TextField
size="small"
fullWidth
label={i18n("fetch_limit")}
2025-09-03 20:43:07 +08:00
type="number"
name="fetchLimit"
value={fetchLimit}
2025-09-03 20:43:07 +08:00
onChange={handleChange}
/>
</Grid>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<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>
2025-09-25 23:08:39 +08:00
<Grid item xs={12} sm={12} md={6} lg={3}></Grid>
</Grid>
</Box>
2025-07-01 10:54:30 +08:00
2025-09-25 23:08:39 +08:00
{showMore && (
<>
2025-10-01 13:22:22 +08:00
<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}>
2025-10-01 15:35:20 +08:00
{`<${item}>`}
2025-10-01 13:22:22 +08:00
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Box>
2025-10-04 21:25:54 +08:00
{apiType !== OPT_TRANS_BUILTINAI && (
2025-09-25 23:08:39 +08:00
<>
2025-10-04 21:25:54 +08:00
{" "}
2025-09-25 23:08:39 +08:00
<TextField
size="small"
2025-10-04 21:25:54 +08:00
label={i18n("custom_header")}
name="customHeader"
value={customHeader}
2025-09-25 23:08:39 +08:00
onChange={handleChange}
multiline
maxRows={10}
2025-10-04 21:25:54 +08:00
helperText={i18n("custom_header_help")}
2025-09-25 23:08:39 +08:00
/>
<TextField
size="small"
2025-10-04 21:25:54 +08:00
label={i18n("custom_body")}
name="customBody"
value={customBody}
2025-09-25 23:08:39 +08:00
onChange={handleChange}
multiline
maxRows={10}
2025-10-04 21:25:54 +08:00
helperText={i18n("custom_body_help")}
2025-09-25 23:08:39 +08:00
/>
</>
)}
2025-10-04 21:25:54 +08:00
{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>
}
/>
</>
)}
2025-09-25 23:08:39 +08:00
</>
)}
2025-09-25 11:31:12 +08:00
<Stack
direction="row"
alignItems="center"
spacing={2}
useFlexGap
flexWrap="wrap"
>
2024-03-21 15:07:50 +08:00
<Button
size="small"
variant="contained"
onClick={handleSave}
disabled={!isModified}
2024-03-21 15:07:50 +08:00
>
{i18n("save")}
</Button>
<TestButton apiSlug={apiSlug} api={api} />
<Button size="small" variant="outlined" onClick={handleReset}>
2024-03-21 15:07:50 +08:00
{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")}
/>
2025-09-25 23:08:39 +08:00
<ShowMoreButton showMore={showMore} onChange={setShowMore} />
2023-09-06 00:25:46 +08:00
</Stack>
2023-09-06 14:57:02 +08:00
2025-09-25 23:08:39 +08:00
{/* {apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>} */}
2023-09-06 00:25:46 +08:00
</Stack>
);
}
function ApiAccordion({ api, isUserApi, deleteApi }) {
2023-09-06 00:25:46 +08:00
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}`}
2025-08-10 22:32:38 +08:00
</Typography>
2023-09-06 00:25:46 +08:00
</AccordionSummary>
<AccordionDetails>
2025-08-10 22:32:38 +08:00
{expanded && (
<ApiFields
apiSlug={api.apiSlug}
isUserApi={isUserApi}
deleteApi={deleteApi}
2025-08-10 22:32:38 +08:00
/>
)}
2023-09-06 00:25:46 +08:00
</AccordionDetails>
</Accordion>
);
}
export default function Apis() {
2023-09-06 14:57:02 +08:00
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();
};
2023-09-06 14:57:02 +08:00
return (
<Box>
<Stack spacing={3}>
2025-10-04 21:25:54 +08:00
<Alert severity="info">
{i18n("about_api")}
<br />
{i18n("about_api_2")}
</Alert>
2023-09-06 14:57:02 +08:00
<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} />
2023-09-06 14:57:02 +08:00
))}
</Box>
</Stack>
</Box>
);
2023-09-06 00:25:46 +08:00
}