From 07b2fd58d646817e597bd6edceabb1c12e57ed38 Mon Sep 17 00:00:00 2001 From: Buer <42402987+MartialBE@users.noreply.github.com> Date: Tue, 28 May 2024 01:22:40 +0800 Subject: [PATCH] feat: berry theme update & bug fix (#1471) * feat: load channel models from server * chore: support AWS Claude/Cloudflare/Coze * fix: Popup message when copying fails * chore: Optimize tips --- web/berry/src/constants/ChannelConstants.js | 30 +- web/berry/src/constants/SnackbarConstants.js | 42 +- web/berry/src/utils/common.js | 37 ++ .../AuthForms/ResetPasswordForm.js | 43 +- .../src/views/Channel/component/EditModal.js | 502 +++++++----------- .../src/views/Channel/component/NameLabel.js | 27 +- web/berry/src/views/Channel/index.js | 8 +- web/berry/src/views/Channel/type/Config.js | 196 ++++--- web/berry/src/views/Profile/index.js | 5 +- .../views/Redemption/component/TableRow.js | 5 +- .../src/views/Token/component/TableRow.js | 14 +- .../src/views/Topup/component/InviteCard.js | 8 +- 12 files changed, 448 insertions(+), 469 deletions(-) diff --git a/web/berry/src/constants/ChannelConstants.js b/web/berry/src/constants/ChannelConstants.js index e6b0aed5..589ef1fb 100644 --- a/web/berry/src/constants/ChannelConstants.js +++ b/web/berry/src/constants/ChannelConstants.js @@ -11,12 +11,18 @@ export const CHANNEL_OPTIONS = { value: 14, color: 'primary' }, - // 33: { - // key: 33, - // text: 'AWS Claude', - // value: 33, - // color: 'primary' - // }, + 33: { + key: 33, + text: 'AWS Claude', + value: 33, + color: 'primary' + }, + 37: { + key: 37, + text: 'Cloudflare', + value: 37, + color: 'success' + }, 3: { key: 3, text: 'Azure OpenAI', @@ -119,12 +125,12 @@ export const CHANNEL_OPTIONS = { value: 32, color: 'primary' }, - // 34: { - // key: 34, - // text: 'Coze', - // value: 34, - // color: 'primary' - // }, + 34: { + key: 34, + text: 'Coze', + value: 34, + color: 'primary' + }, 35: { key: 35, text: 'Cohere', diff --git a/web/berry/src/constants/SnackbarConstants.js b/web/berry/src/constants/SnackbarConstants.js index 19523da1..05f79231 100644 --- a/web/berry/src/constants/SnackbarConstants.js +++ b/web/berry/src/constants/SnackbarConstants.js @@ -1,24 +1,56 @@ +import { closeSnackbar } from 'notistack'; +import { IconX } from '@tabler/icons-react'; +import { IconButton } from '@mui/material'; +const action = (snackbarId) => ( + <> + { + closeSnackbar(snackbarId); + }} + > + + + +); + export const snackbarConstants = { Common: { ERROR: { variant: 'error', - autoHideDuration: 5000 + autoHideDuration: 5000, + preventDuplicate: true, + action }, WARNING: { variant: 'warning', - autoHideDuration: 10000 + autoHideDuration: 10000, + preventDuplicate: true, + action }, SUCCESS: { variant: 'success', - autoHideDuration: 1500 + autoHideDuration: 1500, + preventDuplicate: true, + action }, INFO: { variant: 'info', - autoHideDuration: 3000 + autoHideDuration: 3000, + preventDuplicate: true, + action }, NOTICE: { variant: 'info', - autoHideDuration: 7000 + autoHideDuration: 20000, + preventDuplicate: true, + action + }, + COPY: { + variant: 'copy', + persist: true, + preventDuplicate: true, + allowDownload: true, + action } }, Mobile: { diff --git a/web/berry/src/utils/common.js b/web/berry/src/utils/common.js index 947df3bf..d74d032e 100644 --- a/web/berry/src/utils/common.js +++ b/web/berry/src/utils/common.js @@ -193,3 +193,40 @@ export function removeTrailingSlash(url) { return url; } } + +let channelModels = undefined; +export async function loadChannelModels() { + const res = await API.get('/api/models'); + const { success, data } = res.data; + if (!success) { + return; + } + channelModels = data; + localStorage.setItem('channel_models', JSON.stringify(data)); +} + +export function getChannelModels(type) { + if (channelModels !== undefined && type in channelModels) { + return channelModels[type]; + } + let models = localStorage.getItem('channel_models'); + if (!models) { + return []; + } + channelModels = JSON.parse(models); + if (type in channelModels) { + return channelModels[type]; + } + return []; +} + +export function copy(text, name = '') { + try { + navigator.clipboard.writeText(text); + } catch (error) { + text = `复制${name}失败,请手动复制:

${text}`; + enqueueSnackbar(, getSnackbarOptions('COPY')); + return; + } + showSuccess(`复制${name}成功!`); +} diff --git a/web/berry/src/views/Authentication/AuthForms/ResetPasswordForm.js b/web/berry/src/views/Authentication/AuthForms/ResetPasswordForm.js index eaa8dc95..a9f0f9e3 100644 --- a/web/berry/src/views/Authentication/AuthForms/ResetPasswordForm.js +++ b/web/berry/src/views/Authentication/AuthForms/ResetPasswordForm.js @@ -1,22 +1,22 @@ -import { useState, useEffect } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useState, useEffect } from 'react'; +import { useSearchParams } from 'react-router-dom'; // material-ui -import { Button, Stack, Typography, Alert } from "@mui/material"; +import { Button, Stack, Typography, Alert } from '@mui/material'; // assets -import { showError, showInfo } from "utils/common"; -import { API } from "utils/api"; +import { showError, copy } from 'utils/common'; +import { API } from 'utils/api'; // ===========================|| FIREBASE - REGISTER ||=========================== // const ResetPasswordForm = () => { const [searchParams] = useSearchParams(); const [inputs, setInputs] = useState({ - email: "", - token: "", + email: '', + token: '' }); - const [newPassword, setNewPassword] = useState(""); + const [newPassword, setNewPassword] = useState(''); const submit = async () => { const res = await API.post(`/api/user/reset`, inputs); @@ -24,31 +24,25 @@ const ResetPasswordForm = () => { if (success) { let password = res.data.data; setNewPassword(password); - navigator.clipboard.writeText(password); - showInfo(`新密码已复制到剪贴板:${password}`); + copy(password, '新密码'); } else { showError(message); } }; useEffect(() => { - let email = searchParams.get("email"); - let token = searchParams.get("token"); + let email = searchParams.get('email'); + let token = searchParams.get('token'); setInputs({ token, - email, + email }); }, []); return ( - + {!inputs.email || !inputs.token ? ( - + 无效的链接 ) : newPassword ? ( @@ -57,14 +51,7 @@ const ResetPasswordForm = () => { 请登录后及时修改密码 ) : ( - )} diff --git a/web/berry/src/views/Channel/component/EditModal.js b/web/berry/src/views/Channel/component/EditModal.js index 03b4df57..4f7f216d 100644 --- a/web/berry/src/views/Channel/component/EditModal.js +++ b/web/berry/src/views/Channel/component/EditModal.js @@ -1,9 +1,9 @@ -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 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, getChannelModels } from 'utils/common'; import { Dialog, DialogTitle, @@ -22,15 +22,15 @@ import { Autocomplete, FormHelperText, Switch, - Checkbox, -} from "@mui/material"; + 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"; +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 = ; const checkedIcon = ; @@ -38,38 +38,34 @@ const checkedIcon = ; 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("密钥 不能为空"), + name: Yup.string().required('名称 不能为空'), + type: Yup.number().required('渠道 不能为空'), + key: Yup.string().when(['is_edit', 'type'], { + is: (is_edit, type) => !is_edit && type !== 33, + then: Yup.string().required('密钥 不能为空') }), other: Yup.string(), - models: Yup.array().min(1, "模型 不能为空"), - groups: Yup.array().min(1, "用户组 不能为空"), - base_url: Yup.string().when("type", { + 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 可以是任意字符串 + 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; + 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 }) => { @@ -81,12 +77,13 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { const [groupOptions, setGroupOptions] = useState([]); const [modelOptions, setModelOptions] = useState([]); const [batchAdd, setBatchAdd] = useState(false); + const [basicModels, setBasicModels] = useState([]); const initChannel = (typeValue) => { if (typeConfig[typeValue]?.inputLabel) { setInputLabel({ ...defaultConfig.inputLabel, - ...typeConfig[typeValue].inputLabel, + ...typeConfig[typeValue].inputLabel }); } else { setInputLabel(defaultConfig.inputLabel); @@ -95,7 +92,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { if (typeConfig[typeValue]?.prompt) { setInputPrompt({ ...defaultConfig.prompt, - ...typeConfig[typeValue].prompt, + ...typeConfig[typeValue].prompt }); } else { setInputPrompt(defaultConfig.prompt); @@ -104,40 +101,14 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { 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]); - }); + initChannel(typeValue); + let localModels = getChannelModels(typeValue); + setBasicModels(localModels); + if (localModels.length > 0 && Array.isArray(values['models']) && values['models'].length == 0) { + setFieldValue('models', initialModel(localModels)); } - }; - 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; + setFieldValue('config', {}); }; const fetchGroups = async () => { @@ -155,7 +126,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { const { data } = res.data; data.forEach((item) => { if (!item.owned_by) { - item.owned_by = "未知"; + item.owned_by = '未知'; } }); // 先对data排序 @@ -171,7 +142,7 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { data.map((model) => { return { id: model.id, - group: model.owned_by, + group: model.owned_by }; }) ); @@ -182,33 +153,41 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { const submit = async (values, { setErrors, setStatus, setSubmitting }) => { setSubmitting(true); - if (values.base_url && values.base_url.endsWith("/")) { + 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 === 3 && values.other === '') { + values.other = '2023-09-01-preview'; } - if (values.type === 18 && values.other === "") { - values.other = "v2.1"; + if (values.type === 18 && values.other === '') { + values.other = 'v2.1'; } + if (values.key === '') { + if (values.config.ak !== '' && values.config.sk !== '' && values.config.region !== '') { + values.key = `${values.config.ak}|${values.config.sk}|${values.config.region}`; + } + } + let res; - const modelsStr = values.models.map((model) => model.id).join(","); - values.group = values.groups.join(","); + const modelsStr = values.models.map((model) => model.id).join(','); + const configStr = JSON.stringify(values.config); + values.group = values.groups.join(','); if (channelId) { res = await API.put(`/api/channel/`, { ...values, id: parseInt(channelId), models: modelsStr, + config: configStr }); } else { - res = await API.post(`/api/channel/`, { ...values, models: modelsStr }); + res = await API.post(`/api/channel/`, { ...values, models: modelsStr, config: configStr }); } const { success, message } = res.data; if (success) { if (channelId) { - showSuccess("渠道更新成功!"); + showSuccess('渠道更新成功!'); } else { - showSuccess("渠道创建成功!"); + showSuccess('渠道创建成功!'); } setSubmitting(false); setStatus({ success: true }); @@ -226,15 +205,15 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { } // 如果 channelModel 是一个字符串 - if (typeof channelModel === "string") { - channelModel = channelModel.split(","); + 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 { id: model, group: '自定义:点击或回车输入' }; }); return modelList; } @@ -243,24 +222,24 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { let res = await API.get(`/api/channel/${channelId}`); const { success, message, data } = res.data; if (success) { - if (data.models === "") { + if (data.models === '') { data.models = []; } else { data.models = initialModel(data.models); } - if (data.group === "") { + if (data.group === '') { data.groups = []; } else { - data.groups = data.group.split(","); + data.groups = data.group.split(','); } - if (data.model_mapping !== "") { - data.model_mapping = JSON.stringify( - JSON.parse(data.model_mapping), - null, - 2 - ); + if (data.model_mapping !== '') { + data.model_mapping = JSON.stringify(JSON.parse(data.model_mapping), null, 2); } - data.base_url = data.base_url ?? ""; + if (data.config !== '') { + data.config = JSON.parse(data.config); + } + + data.base_url = data.base_url ?? ''; data.is_edit = true; initChannel(data.type); setInitialInput(data); @@ -286,45 +265,25 @@ const EditModal = ({ open, channelId, onCancel, onOk }) => { }, [channelId]); return ( - + - {channelId ? "编辑渠道" : "新建渠道"} + {channelId ? '编辑渠道' : '新建渠道'} - - {({ - errors, - handleBlur, - handleChange, - handleSubmit, - isSubmitting, - touched, - values, - setFieldValue, - }) => ( + + {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values, setFieldValue }) => (
- - - {inputLabel.type} - + + {inputLabel.type}