one-api/web/berry/src/views/Channel/component/EditModal.js
Buer cf4e33cb12
fix: fix bugs with theme berry (#931)
* fix: home page & logo style issue

* improve: Enhanced user experience by improving the channel selection box

* fix: key cannot be activated after expiration
2024-01-14 13:22:31 +08:00

685 lines
22 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 PropTypes from "prop-types";
import { useState, useEffect } from "react";
import { CHANNEL_OPTIONS } from "constants/ChannelConstants";
import { useTheme } from "@mui/material/styles";
import { API } from "utils/api";
import { showError, showSuccess } from "utils/common";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
TextField,
Button,
Divider,
Select,
MenuItem,
FormControl,
InputLabel,
OutlinedInput,
ButtonGroup,
Container,
Autocomplete,
FormHelperText,
Checkbox
} from "@mui/material";
import { Formik } from "formik";
import * as Yup from "yup";
import { defaultConfig, typeConfig } from "../type/Config"; //typeConfig
import { createFilterOptions } from "@mui/material/Autocomplete";
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const filter = createFilterOptions();
const validationSchema = Yup.object().shape({
is_edit: Yup.boolean(),
name: Yup.string().required("名称 不能为空"),
type: Yup.number().required("渠道 不能为空"),
key: Yup.string().when("is_edit", {
is: false,
then: Yup.string().required("密钥 不能为空"),
}),
other: Yup.string(),
models: Yup.array().min(1, "模型 不能为空"),
groups: Yup.array().min(1, "用户组 不能为空"),
base_url: Yup.string().when("type", {
is: (value) => [3, 8].includes(value),
then: Yup.string().required("渠道API地址 不能为空"), // base_url 是必需的
otherwise: Yup.string(), // 在其他情况下base_url 可以是任意字符串
}),
model_mapping: Yup.string().test(
"is-json",
"必须是有效的JSON字符串",
function (value) {
try {
if (value === "" || value === null || value === undefined) {
return true;
}
const parsedValue = JSON.parse(value);
if (typeof parsedValue === "object") {
return true;
}
} catch (e) {
return false;
}
return false;
}
),
});
const EditModal = ({ open, channelId, onCancel, onOk }) => {
const theme = useTheme();
// const [loading, setLoading] = useState(false);
const [initialInput, setInitialInput] = useState(defaultConfig.input);
const [inputLabel, setInputLabel] = useState(defaultConfig.inputLabel); //
const [inputPrompt, setInputPrompt] = useState(defaultConfig.prompt);
const [groupOptions, setGroupOptions] = useState([]);
const [modelOptions, setModelOptions] = useState([]);
const initChannel = (typeValue) => {
if (typeConfig[typeValue]?.inputLabel) {
setInputLabel({
...defaultConfig.inputLabel,
...typeConfig[typeValue].inputLabel,
});
} else {
setInputLabel(defaultConfig.inputLabel);
}
if (typeConfig[typeValue]?.prompt) {
setInputPrompt({
...defaultConfig.prompt,
...typeConfig[typeValue].prompt,
});
} else {
setInputPrompt(defaultConfig.prompt);
}
return typeConfig[typeValue]?.input;
};
const handleTypeChange = (setFieldValue, typeValue, values) => {
const newInput = initChannel(typeValue);
if (newInput) {
Object.keys(newInput).forEach((key) => {
if (
(!Array.isArray(values[key]) &&
values[key] !== null &&
values[key] !== undefined &&
values[key] !== "") ||
(Array.isArray(values[key]) && values[key].length > 0)
) {
return;
}
if (key === "models") {
setFieldValue(key, initialModel(newInput[key]));
return;
}
setFieldValue(key, newInput[key]);
});
}
};
const basicModels = (channelType) => {
let modelGroup =
typeConfig[channelType]?.modelGroup || defaultConfig.modelGroup;
// 循环 modelOptions找到 modelGroup 对应的模型
let modelList = [];
modelOptions.forEach((model) => {
if (model.group === modelGroup) {
modelList.push(model);
}
});
return modelList;
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data);
} catch (error) {
showError(error.message);
}
};
const fetchModels = async () => {
try {
let res = await API.get(`/api/channel/models`);
const { data } = res.data;
data.forEach(item => {
if (!item.owned_by) {
item.owned_by = "未知";
}
});
// 先对data排序
data.sort((a, b) => {
const ownedByComparison = a.owned_by.localeCompare(b.owned_by);
if (ownedByComparison === 0) {
return a.id.localeCompare(b.id);
}
return ownedByComparison;
});
setModelOptions(
data.map((model) => {
return {
id: model.id,
group: model.owned_by,
};
})
);
} catch (error) {
showError(error.message);
}
};
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
setSubmitting(true);
if (values.base_url && values.base_url.endsWith("/")) {
values.base_url = values.base_url.slice(0, values.base_url.length - 1);
}
if (values.type === 3 && values.other === "") {
values.other = "2023-09-01-preview";
}
if (values.type === 18 && values.other === "") {
values.other = "v2.1";
}
let res;
const modelsStr = values.models.map((model) => model.id).join(",");
values.group = values.groups.join(",");
if (channelId) {
res = await API.put(`/api/channel/`, {
...values,
id: parseInt(channelId),
models: modelsStr,
});
} else {
res = await API.post(`/api/channel/`, { ...values, models: modelsStr });
}
const { success, message } = res.data;
if (success) {
if (channelId) {
showSuccess("渠道更新成功!");
} else {
showSuccess("渠道创建成功!");
}
setSubmitting(false);
setStatus({ success: true });
onOk(true);
} else {
setStatus({ success: false });
showError(message);
setErrors({ submit: message });
}
};
function initialModel(channelModel) {
if (!channelModel) {
return [];
}
// 如果 channelModel 是一个字符串
if (typeof channelModel === "string") {
channelModel = channelModel.split(",");
}
let modelList = channelModel.map((model) => {
const modelOption = modelOptions.find((option) => option.id === model);
if (modelOption) {
return modelOption;
}
return { id: model, group: "自定义:点击或回车输入" };
});
return modelList;
}
const loadChannel = async () => {
let res = await API.get(`/api/channel/${channelId}`);
const { success, message, data } = res.data;
if (success) {
if (data.models === "") {
data.models = [];
} else {
data.models = initialModel(data.models);
}
if (data.group === "") {
data.groups = [];
} else {
data.groups = data.group.split(",");
}
if (data.model_mapping !== "") {
data.model_mapping = JSON.stringify(
JSON.parse(data.model_mapping),
null,
2
);
}
data.base_url = data.base_url ?? '';
data.is_edit = true;
initChannel(data.type);
setInitialInput(data);
} else {
showError(message);
}
};
useEffect(() => {
fetchGroups().then();
fetchModels().then();
}, []);
useEffect(() => {
if (channelId) {
loadChannel().then();
} else {
initChannel(1);
setInitialInput({ ...defaultConfig.input, is_edit: false });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [channelId]);
return (
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={"md"}>
<DialogTitle
sx={{
margin: "0px",
fontWeight: 700,
lineHeight: "1.55556",
padding: "24px",
fontSize: "1.125rem",
}}
>
{channelId ? "编辑渠道" : "新建渠道"}
</DialogTitle>
<Divider />
<DialogContent>
<Formik
initialValues={initialInput}
enableReinitialize
validationSchema={validationSchema}
onSubmit={submit}
>
{({
errors,
handleBlur,
handleChange,
handleSubmit,
isSubmitting,
touched,
values,
setFieldValue,
}) => (
<form noValidate onSubmit={handleSubmit}>
<FormControl
fullWidth
error={Boolean(touched.type && errors.type)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-type-label">
{inputLabel.type}
</InputLabel>
<Select
id="channel-type-label"
label={inputLabel.type}
value={values.type}
name="type"
onBlur={handleBlur}
onChange={(e) => {
handleChange(e);
handleTypeChange(setFieldValue, e.target.value, values);
}}
MenuProps={{
PaperProps: {
style: {
maxHeight: 200,
},
},
}}
>
{Object.values(CHANNEL_OPTIONS).map((option) => {
return (
<MenuItem key={option.value} value={option.value}>
{option.text}
</MenuItem>
);
})}
</Select>
{touched.type && errors.type ? (
<FormHelperText error id="helper-tex-channel-type-label">
{errors.type}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-type-label">
{" "}
{inputPrompt.type}{" "}
</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={Boolean(touched.name && errors.name)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-name-label">
{inputLabel.name}
</InputLabel>
<OutlinedInput
id="channel-name-label"
label={inputLabel.name}
type="text"
value={values.name}
name="name"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{ autoComplete: "name" }}
aria-describedby="helper-text-channel-name-label"
/>
{touched.name && errors.name ? (
<FormHelperText error id="helper-tex-channel-name-label">
{errors.name}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-name-label">
{" "}
{inputPrompt.name}{" "}
</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={Boolean(touched.base_url && errors.base_url)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-base_url-label">
{inputLabel.base_url}
</InputLabel>
<OutlinedInput
id="channel-base_url-label"
label={inputLabel.base_url}
type="text"
value={values.base_url}
name="base_url"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
aria-describedby="helper-text-channel-base_url-label"
/>
{touched.base_url && errors.base_url ? (
<FormHelperText error id="helper-tex-channel-base_url-label">
{errors.base_url}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-base_url-label">
{" "}
{inputPrompt.base_url}{" "}
</FormHelperText>
)}
</FormControl>
{inputPrompt.other && (
<FormControl
fullWidth
error={Boolean(touched.other && errors.other)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-other-label">
{inputLabel.other}
</InputLabel>
<OutlinedInput
id="channel-other-label"
label={inputLabel.other}
type="text"
value={values.other}
name="other"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
aria-describedby="helper-text-channel-other-label"
/>
{touched.other && errors.other ? (
<FormHelperText error id="helper-tex-channel-other-label">
{errors.other}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-other-label">
{" "}
{inputPrompt.other}{" "}
</FormHelperText>
)}
</FormControl>
)}
<FormControl fullWidth sx={{ ...theme.typography.otherInput }}>
<Autocomplete
multiple
id="channel-groups-label"
options={groupOptions}
value={values.groups}
onChange={(e, value) => {
const event = {
target: {
name: "groups",
value: value,
},
};
handleChange(event);
}}
onBlur={handleBlur}
filterSelectedOptions
renderInput={(params) => (
<TextField
{...params}
name="groups"
error={Boolean(errors.groups)}
label={inputLabel.groups}
/>
)}
aria-describedby="helper-text-channel-groups-label"
/>
{errors.groups ? (
<FormHelperText error id="helper-tex-channel-groups-label">
{errors.groups}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-groups-label">
{" "}
{inputPrompt.groups}{" "}
</FormHelperText>
)}
</FormControl>
<FormControl fullWidth sx={{ ...theme.typography.otherInput }}>
<Autocomplete
multiple
freeSolo
id="channel-models-label"
options={modelOptions}
value={values.models}
onChange={(e, value) => {
const event = {
target: {
name: "models",
value: value.map((item) =>
typeof item === "string"
? { id: item, group: "自定义:点击或回车输入" }
: item
),
},
};
handleChange(event);
}}
onBlur={handleBlur}
// filterSelectedOptions
disableCloseOnSelect
renderInput={(params) => (
<TextField
{...params}
name="models"
error={Boolean(errors.models)}
label={inputLabel.models}
/>
)}
groupBy={(option) => option.group}
getOptionLabel={(option) => {
if (typeof option === "string") {
return option;
}
if (option.inputValue) {
return option.inputValue;
}
return option.id;
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
const { inputValue } = params;
const isExisting = options.some(
(option) => inputValue === option.id
);
if (inputValue !== "" && !isExisting) {
filtered.push({
id: inputValue,
group: "自定义:点击或回车输入",
});
}
return filtered;
}}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox icon={icon} checkedIcon={checkedIcon} style={{ marginRight: 8 }} checked={selected} />
{option.id}
</li>
)}
/>
{errors.models ? (
<FormHelperText error id="helper-tex-channel-models-label">
{errors.models}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-models-label">
{" "}
{inputPrompt.models}{" "}
</FormHelperText>
)}
</FormControl>
<Container
sx={{
textAlign: "right",
}}
>
<ButtonGroup
variant="outlined"
aria-label="small outlined primary button group"
>
<Button
onClick={() => {
setFieldValue("models", basicModels(values.type));
}}
>
填入渠道支持模型
</Button>
<Button
onClick={() => {
setFieldValue("models", modelOptions);
}}
>
填入所有模型
</Button>
</ButtonGroup>
</Container>
<FormControl
fullWidth
error={Boolean(touched.key && errors.key)}
sx={{ ...theme.typography.otherInput }}
>
<InputLabel htmlFor="channel-key-label">
{inputLabel.key}
</InputLabel>
<OutlinedInput
id="channel-key-label"
label={inputLabel.key}
type="text"
value={values.key}
name="key"
onBlur={handleBlur}
onChange={handleChange}
inputProps={{}}
aria-describedby="helper-text-channel-key-label"
/>
{touched.key && errors.key ? (
<FormHelperText error id="helper-tex-channel-key-label">
{errors.key}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-key-label">
{" "}
{inputPrompt.key}{" "}
</FormHelperText>
)}
</FormControl>
<FormControl
fullWidth
error={Boolean(touched.model_mapping && errors.model_mapping)}
sx={{ ...theme.typography.otherInput }}
>
{/* <InputLabel htmlFor="channel-model_mapping-label">{inputLabel.model_mapping}</InputLabel> */}
<TextField
multiline
id="channel-model_mapping-label"
label={inputLabel.model_mapping}
value={values.model_mapping}
name="model_mapping"
onBlur={handleBlur}
onChange={handleChange}
aria-describedby="helper-text-channel-model_mapping-label"
minRows={5}
placeholder={inputPrompt.model_mapping}
/>
{touched.model_mapping && errors.model_mapping ? (
<FormHelperText
error
id="helper-tex-channel-model_mapping-label"
>
{errors.model_mapping}
</FormHelperText>
) : (
<FormHelperText id="helper-tex-channel-model_mapping-label">
{" "}
{inputPrompt.model_mapping}{" "}
</FormHelperText>
)}
</FormControl>
<DialogActions>
<Button onClick={onCancel}>取消</Button>
<Button
disableElevation
disabled={isSubmitting}
type="submit"
variant="contained"
color="primary"
>
提交
</Button>
</DialogActions>
</form>
)}
</Formik>
</DialogContent>
</Dialog>
);
};
export default EditModal;
EditModal.propTypes = {
open: PropTypes.bool,
channelId: PropTypes.number,
onCancel: PropTypes.func,
onOk: PropTypes.func,
};