Merge branch 'Calcium-Ion:main' into feat/modeledit
This commit is contained in:
@@ -24,7 +24,7 @@ import { Layout } from '@douyinfe/semi-ui';
|
||||
import Midjourney from './pages/Midjourney';
|
||||
import Pricing from './pages/Pricing/index.js';
|
||||
import Task from "./pages/Task/index.js";
|
||||
import Playground from './components/Playground.js';
|
||||
import Playground from './pages/Playground/Playground.js';
|
||||
import OAuth2Callback from "./components/OAuth2Callback.js";
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,17 +9,19 @@ import '../index.css';
|
||||
import fireworks from 'react-fireworks';
|
||||
|
||||
import {
|
||||
IconClose,
|
||||
IconHelpCircle,
|
||||
IconHome,
|
||||
IconHomeStroked,
|
||||
IconKey,
|
||||
IconHomeStroked, IconIndentLeft,
|
||||
IconKey, IconMenu,
|
||||
IconNoteMoneyStroked,
|
||||
IconPriceTag,
|
||||
IconUser
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { Avatar, Button, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
|
||||
// HeaderBar Buttons
|
||||
let headerButtons = [
|
||||
@@ -31,21 +33,6 @@ let headerButtons = [
|
||||
},
|
||||
];
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
// icon: <IconHomeStroked />,
|
||||
},
|
||||
// {
|
||||
// text: 'Playground',
|
||||
// itemKey: 'playground',
|
||||
// to: '/playground',
|
||||
// // icon: <IconNoteMoneyStroked />,
|
||||
// },
|
||||
];
|
||||
|
||||
if (localStorage.getItem('chat_link')) {
|
||||
headerButtons.splice(1, 0, {
|
||||
name: '聊天',
|
||||
@@ -56,9 +43,9 @@ if (localStorage.getItem('chat_link')) {
|
||||
|
||||
const HeaderBar = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
const logo = getLogo();
|
||||
const currentDate = new Date();
|
||||
@@ -69,8 +56,25 @@ const HeaderBar = () => {
|
||||
currentDate.getDate() >= 9 &&
|
||||
currentDate.getDate() <= 24);
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
text: '首页',
|
||||
itemKey: 'home',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: '控制台',
|
||||
itemKey: 'detail',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
text: '定价',
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
},
|
||||
];
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({ type: 'logout' });
|
||||
@@ -108,36 +112,57 @@ const HeaderBar = () => {
|
||||
<div style={{ width: '100%' }}>
|
||||
<Nav
|
||||
mode={'horizontal'}
|
||||
// bodyStyle={{ height: 100 }}
|
||||
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
|
||||
const routerMap = {
|
||||
about: '/about',
|
||||
login: '/login',
|
||||
register: '/register',
|
||||
pricing: '/pricing',
|
||||
detail: '/detail',
|
||||
home: '/',
|
||||
};
|
||||
return (
|
||||
<Link
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
<div onClick={(e) => {
|
||||
if (props.itemKey === 'home') {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
styleDispatch({ type: 'SET_SIDER', payload: true });
|
||||
}
|
||||
}}>
|
||||
<Link
|
||||
className="header-bar-text"
|
||||
style={{ textDecoration: 'none' }}
|
||||
to={routerMap[props.itemKey]}
|
||||
>
|
||||
{itemElement}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selectedKeys={[]}
|
||||
// items={headerButtons}
|
||||
onSelect={(key) => {}}
|
||||
header={isMobile()?{
|
||||
header={styleState.isMobile?{
|
||||
logo: (
|
||||
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||
<>
|
||||
{
|
||||
!styleState.showSider ?
|
||||
<Button icon={<IconMenu />} theme="light" aria-label="展开侧边栏" onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: true })
|
||||
} />:
|
||||
<Button icon={<IconIndentLeft />} theme="light" aria-label="关闭侧边栏" onClick={
|
||||
() => styleDispatch({ type: 'SET_SIDER', payload: false })
|
||||
} />
|
||||
}
|
||||
</>
|
||||
),
|
||||
}:{
|
||||
logo: (
|
||||
<img src={logo} alt='logo' />
|
||||
),
|
||||
text: systemName,
|
||||
|
||||
}}
|
||||
items={buttons}
|
||||
footer={
|
||||
@@ -159,17 +184,15 @@ const HeaderBar = () => {
|
||||
)}
|
||||
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
|
||||
<>
|
||||
{!isMobile() && (
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={'large'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={styleState.isMobile?'default':'large'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
{userState.user ? (
|
||||
<>
|
||||
@@ -188,7 +211,7 @@ const HeaderBar = () => {
|
||||
>
|
||||
{userState.user.username[0]}
|
||||
</Avatar>
|
||||
<span>{userState.user.username}</span>
|
||||
{styleState.isMobile?null:<Text>{userState.user.username}</Text>}
|
||||
</Dropdown>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderAudioModelPrice,
|
||||
renderModelPrice,
|
||||
renderModelPrice, renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
stringToColor
|
||||
@@ -386,14 +386,11 @@ const LogsTable = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// let content = renderModelPrice(
|
||||
// record.prompt_tokens,
|
||||
// record.completion_tokens,
|
||||
// other.model_ratio,
|
||||
// other.model_price,
|
||||
// other.completion_ratio,
|
||||
// other.group_ratio,
|
||||
// );
|
||||
let content = renderModelPriceSimple(
|
||||
other.model_ratio,
|
||||
other.model_price,
|
||||
other.group_ratio,
|
||||
);
|
||||
return (
|
||||
<Paragraph
|
||||
ellipsis={{
|
||||
@@ -401,7 +398,7 @@ const LogsTable = () => {
|
||||
}}
|
||||
style={{ maxWidth: 240 }}
|
||||
>
|
||||
调用消费
|
||||
{content}
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
|
||||
40
web/src/components/PageLayout.js
Normal file
40
web/src/components/PageLayout.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import HeaderBar from './HeaderBar.js';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import SiderBar from './SiderBar.js';
|
||||
import App from '../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext } from 'react';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
const { Sider, Content, Header, Footer } = Layout;
|
||||
|
||||
|
||||
const PageLayout = () => {
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<Header>
|
||||
<HeaderBar />
|
||||
</Header>
|
||||
<Layout style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<Sider>
|
||||
{styleState.showSider ? <SiderBar /> : null}
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content
|
||||
style={{ overflowY: 'auto', padding: styleState.shouldInnerPadding? '24px': '0' }}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<FooterBar></FooterBar>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<ToastContainer />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageLayout;
|
||||
@@ -363,36 +363,18 @@ const PersonalSetting = () => {
|
||||
</Space>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
}
|
||||
>
|
||||
<Typography.Title heading={6}>可用模型</Typography.Title>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag
|
||||
key={model}
|
||||
color='cyan'
|
||||
onClick={() => {
|
||||
copyText(model);
|
||||
}}
|
||||
>
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
<Card
|
||||
style={{marginTop: 10}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
@@ -11,6 +11,7 @@ import LinuxDoIcon from './LinuxDoIcon.js';
|
||||
import WeChatIcon from './WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -22,6 +23,7 @@ const RegisterForm = () => {
|
||||
});
|
||||
const { username, password, password2 } = inputs;
|
||||
const [showEmailVerification, setShowEmailVerification] = useState(false);
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
@@ -133,6 +135,38 @@ const RegisterForm = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onTelegramLoginClicked = async (response) => {
|
||||
const fields = [
|
||||
'id',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'username',
|
||||
'photo_url',
|
||||
'auth_date',
|
||||
'hash',
|
||||
'lang',
|
||||
];
|
||||
const params = {};
|
||||
fields.forEach((field) => {
|
||||
if (response[field]) {
|
||||
params[field] = response[field];
|
||||
}
|
||||
});
|
||||
const res = await API.get(`/api/oauth/telegram/login`, { params });
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
userDispatch({ type: 'login', payload: data });
|
||||
localStorage.setItem('user', JSON.stringify(data));
|
||||
showSuccess('登录成功!');
|
||||
setUserData(data);
|
||||
updateAPI();
|
||||
navigate('/');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Layout>
|
||||
|
||||
@@ -31,14 +31,15 @@ import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { stringToColor } from '../helpers/render.js';
|
||||
import { useSetTheme, useTheme } from '../context/Theme/index.js';
|
||||
import { StyleContext } from '../context/Style/index.js';
|
||||
|
||||
// HeaderBar Buttons
|
||||
|
||||
const SiderBar = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
const defaultIsCollapsed =
|
||||
isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true';
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(['home']);
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
|
||||
@@ -72,12 +73,6 @@ const SiderBar = () => {
|
||||
to: '/playground',
|
||||
icon: <IconCommentStroked />,
|
||||
},
|
||||
{
|
||||
text: '模型价格',
|
||||
itemKey: 'pricing',
|
||||
to: '/pricing',
|
||||
icon: <IconPriceTag />,
|
||||
},
|
||||
{
|
||||
text: '渠道',
|
||||
itemKey: 'channel',
|
||||
@@ -101,6 +96,16 @@ const SiderBar = () => {
|
||||
to: '/token',
|
||||
icon: <IconKey />,
|
||||
},
|
||||
{
|
||||
text: '数据看板',
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? 'semi-navigation-item-normal'
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '兑换码',
|
||||
itemKey: 'redemption',
|
||||
@@ -127,16 +132,6 @@ const SiderBar = () => {
|
||||
to: '/log',
|
||||
icon: <IconHistogram />,
|
||||
},
|
||||
{
|
||||
text: '数据看板',
|
||||
itemKey: 'detail',
|
||||
to: '/detail',
|
||||
icon: <IconCalendarClock />,
|
||||
className:
|
||||
localStorage.getItem('enable_data_export') === 'true'
|
||||
? 'semi-navigation-item-normal'
|
||||
: 'tableHiddle',
|
||||
},
|
||||
{
|
||||
text: '绘图',
|
||||
itemKey: 'midjourney',
|
||||
@@ -196,7 +191,6 @@ const SiderBar = () => {
|
||||
useEffect(() => {
|
||||
loadStatus().then(() => {
|
||||
setIsCollapsed(
|
||||
isMobile() ||
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true',
|
||||
);
|
||||
});
|
||||
@@ -239,7 +233,6 @@ const SiderBar = () => {
|
||||
<Nav
|
||||
style={{ maxWidth: 220, height: '100%' }}
|
||||
defaultIsCollapsed={
|
||||
isMobile() ||
|
||||
localStorage.getItem('default_collapse_sidebar') === 'true'
|
||||
}
|
||||
isCollapsed={isCollapsed}
|
||||
@@ -280,21 +273,15 @@ const SiderBar = () => {
|
||||
}}
|
||||
items={headerButtons}
|
||||
onSelect={(key) => {
|
||||
if (key.itemKey.toString().startsWith('chat')) {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
styleDispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
setSelectedKeys([key.itemKey]);
|
||||
}}
|
||||
footer={
|
||||
<>
|
||||
{isMobile() && (
|
||||
<Switch
|
||||
checkedText='🌞'
|
||||
size={'small'}
|
||||
checked={theme === 'dark'}
|
||||
uncheckedText='🌙'
|
||||
onChange={(checked) => {
|
||||
setTheme(checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
21
web/src/components/custom/TextInput.js
Normal file
21
web/src/components/custom/TextInput.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Input, Typography } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{label}</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) => onChange(value)}
|
||||
value={value}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextInput;
|
||||
21
web/src/components/custom/TextNumberInput.js
Normal file
21
web/src/components/custom/TextNumberInput.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Input, InputNumber, Typography } from '@douyinfe/semi-ui';
|
||||
import React from 'react';
|
||||
|
||||
const TextNumberInput = ({ label, name, value, onChange, placeholder }) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>{label}</Typography.Text>
|
||||
</div>
|
||||
<InputNumber
|
||||
name={name}
|
||||
placeholder={placeholder}
|
||||
onChange={(value) => onChange(value)}
|
||||
value={value}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TextNumberInput;
|
||||
@@ -44,13 +44,6 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'teal',
|
||||
label: 'Azure OpenAI'
|
||||
},
|
||||
{
|
||||
key: 24,
|
||||
text: 'Google Gemini',
|
||||
value: 24,
|
||||
color: 'orange',
|
||||
label: 'Google Gemini'
|
||||
},
|
||||
{
|
||||
key: 34,
|
||||
text: 'Cohere',
|
||||
@@ -58,6 +51,8 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: 'Cohere'
|
||||
},
|
||||
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
|
||||
{ key: 43, text: 'DeepSeek', value: 43, color: 'blue', label: 'DeepSeek' },
|
||||
{
|
||||
key: 15,
|
||||
text: '百度文心千帆',
|
||||
@@ -93,6 +88,13 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'purple',
|
||||
label: '智谱 GLM-4V'
|
||||
},
|
||||
{
|
||||
key: 24,
|
||||
text: 'Google Gemini',
|
||||
value: 24,
|
||||
color: 'orange',
|
||||
label: 'Google Gemini'
|
||||
},
|
||||
{
|
||||
key: 11,
|
||||
text: 'Google PaLM2',
|
||||
@@ -100,7 +102,6 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'orange',
|
||||
label: 'Google PaLM2'
|
||||
},
|
||||
{ key: 39, text: 'Cloudflare', value: 39, color: 'grey', label: 'Cloudflare' },
|
||||
{ key: 25, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
|
||||
{ key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
|
||||
{ key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
|
||||
|
||||
80
web/src/context/Style/index.js
Normal file
80
web/src/context/Style/index.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// contexts/User/index.jsx
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { isMobile } from '../../helpers/index.js';
|
||||
|
||||
export const StyleContext = React.createContext({
|
||||
dispatch: () => null,
|
||||
});
|
||||
|
||||
export const StyleProvider = ({ children }) => {
|
||||
const [state, setState] = useState({
|
||||
isMobile: false,
|
||||
showSider: false,
|
||||
shouldInnerPadding: false,
|
||||
});
|
||||
|
||||
const dispatch = (action) => {
|
||||
if ('type' in action) {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_SIDER':
|
||||
setState(prev => ({ ...prev, showSider: !prev.showSider }));
|
||||
break;
|
||||
case 'SET_SIDER':
|
||||
setState(prev => ({ ...prev, showSider: action.payload }));
|
||||
break;
|
||||
case 'SET_MOBILE':
|
||||
setState(prev => ({ ...prev, isMobile: action.payload }));
|
||||
break;
|
||||
case 'SET_INNER_PADDING':
|
||||
setState(prev => ({ ...prev, shouldInnerPadding: action.payload }));
|
||||
break;
|
||||
default:
|
||||
setState(prev => ({ ...prev, ...action }));
|
||||
}
|
||||
} else {
|
||||
setState(prev => ({ ...prev, ...action }));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateIsMobile = () => {
|
||||
dispatch({ type: 'SET_MOBILE', payload: isMobile() });
|
||||
};
|
||||
|
||||
updateIsMobile();
|
||||
|
||||
const updateShowSider = () => {
|
||||
// check pathname
|
||||
const pathname = window.location.pathname;
|
||||
if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: false });
|
||||
} else {
|
||||
dispatch({ type: 'SET_SIDER', payload: true });
|
||||
dispatch({ type: 'SET_INNER_PADDING', payload: true });
|
||||
}
|
||||
|
||||
if (isMobile()) {
|
||||
dispatch({ type: 'SET_SIDER', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
updateShowSider()
|
||||
|
||||
|
||||
// Optionally, add event listeners to handle window resize
|
||||
window.addEventListener('resize', updateIsMobile);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateIsMobile);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyleContext.Provider value={[state, dispatch]}>
|
||||
{children}
|
||||
</StyleContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,8 @@ export let API = axios.create({
|
||||
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||
: '',
|
||||
headers: {
|
||||
'New-API-User': getUserIdFromLocalStorage()
|
||||
'New-API-User': getUserIdFromLocalStorage(),
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +17,8 @@ export function updateAPI() {
|
||||
? import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||
: '',
|
||||
headers: {
|
||||
'New-API-User': getUserIdFromLocalStorage()
|
||||
'New-API-User': getUserIdFromLocalStorage(),
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -67,6 +67,8 @@ export function renderQuotaNumberWithDigit(num, digits = 2) {
|
||||
}
|
||||
|
||||
export function renderNumberWithPoint(num) {
|
||||
if (num === undefined)
|
||||
return '';
|
||||
num = num.toFixed(2);
|
||||
if (num >= 100000) {
|
||||
// Convert number to string to manipulate it
|
||||
@@ -173,6 +175,19 @@ export function renderModelPrice(
|
||||
}
|
||||
}
|
||||
|
||||
export function renderModelPriceSimple(
|
||||
modelRatio,
|
||||
modelPrice = -1,
|
||||
groupRatio,
|
||||
) {
|
||||
// 1 ratio = $0.002 / 1K tokens
|
||||
if (modelPrice !== -1) {
|
||||
return '价格:$' + modelPrice + ' * 分组:' + groupRatio;
|
||||
} else {
|
||||
return '模型: ' + modelRatio + ' * 分组: ' + groupRatio;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderAudioModelPrice(
|
||||
inputTokens,
|
||||
completionTokens,
|
||||
@@ -253,6 +268,44 @@ const colors = [
|
||||
'yellow',
|
||||
];
|
||||
|
||||
// 基础10色色板 (N ≤ 10)
|
||||
const baseColors = [
|
||||
'#1664FF', // 主色
|
||||
'#1AC6FF',
|
||||
'#FF8A00',
|
||||
'#3CC780',
|
||||
'#7442D4',
|
||||
'#FFC400',
|
||||
'#304D77',
|
||||
'#B48DEB',
|
||||
'#009488',
|
||||
'#FF7DDA'
|
||||
];
|
||||
|
||||
// 扩展20色色板 (10 < N ≤ 20)
|
||||
const extendedColors = [
|
||||
'#1664FF',
|
||||
'#B2CFFF',
|
||||
'#1AC6FF',
|
||||
'#94EFFF',
|
||||
'#FF8A00',
|
||||
'#FFCE7A',
|
||||
'#3CC780',
|
||||
'#B9EDCD',
|
||||
'#7442D4',
|
||||
'#DDC5FA',
|
||||
'#FFC400',
|
||||
'#FAE878',
|
||||
'#304D77',
|
||||
'#8B959E',
|
||||
'#B48DEB',
|
||||
'#EFE3FF',
|
||||
'#009488',
|
||||
'#59BAA8',
|
||||
'#FF7DDA',
|
||||
'#FFCFEE'
|
||||
];
|
||||
|
||||
export const modelColorMap = {
|
||||
'dall-e': 'rgb(147,112,219)', // 深紫色
|
||||
// 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
|
||||
@@ -297,14 +350,33 @@ export const modelColorMap = {
|
||||
'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
|
||||
};
|
||||
|
||||
export function modelToColor(modelName) {
|
||||
// 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
|
||||
if (modelColorMap[modelName]) {
|
||||
return modelColorMap[modelName];
|
||||
}
|
||||
|
||||
// 2. 生成一个稳定的数字作为索引
|
||||
let hash = 0;
|
||||
for (let i = 0; i < modelName.length; i++) {
|
||||
hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// 3. 根据模型名称长度选择不同的色板
|
||||
const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
|
||||
|
||||
// 4. 使用hash值选择颜色
|
||||
const index = hash % colorPalette.length;
|
||||
return colorPalette[index];
|
||||
}
|
||||
|
||||
export function stringToColor(str) {
|
||||
let sum = 0;
|
||||
// 对字符串中的每个字符进行操作
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
// 将字符的ASCII值加到sum中
|
||||
sum += str.charCodeAt(i);
|
||||
}
|
||||
// 使用模运算得到个位数
|
||||
let i = sum % colors.length;
|
||||
return colors[i];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,25 @@ body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.semi-descriptions-double-small .semi-descriptions-item {
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.panel-desc-card {
|
||||
/*min-width: 320px;*/
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
#root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li {
|
||||
padding: 0 5px;
|
||||
}
|
||||
.semi-table-tbody,
|
||||
.semi-table-row,
|
||||
.semi-table-row-cell {
|
||||
@@ -39,6 +57,10 @@ body {
|
||||
row-gap: 3px;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.semi-navigation-horizontal .semi-navigation-header {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
|
||||
@@ -13,6 +13,8 @@ import { Layout } from '@douyinfe/semi-ui';
|
||||
import SiderBar from './components/SiderBar';
|
||||
import { ThemeProvider } from './context/Theme';
|
||||
import FooterBar from './components/Footer';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/PageLayout.js';
|
||||
|
||||
// initialization
|
||||
|
||||
@@ -24,27 +26,9 @@ root.render(
|
||||
<UserProvider>
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<Layout style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
<Header>
|
||||
<HeaderBar />
|
||||
</Header>
|
||||
<Layout style={{ flex: 1, overflow: 'hidden' }}>
|
||||
<Sider>
|
||||
<SiderBar />
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Content
|
||||
style={{ overflowY: 'auto', padding: '24px' }}
|
||||
>
|
||||
<App />
|
||||
</Content>
|
||||
<Layout.Footer>
|
||||
<FooterBar></FooterBar>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<ToastContainer />
|
||||
</Layout>
|
||||
<StyleProvider>
|
||||
<PageLayout/>
|
||||
</StyleProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
</UserProvider>
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
verifyJSON
|
||||
} from '../../helpers';
|
||||
import { CHANNEL_OPTIONS } from '../../constants';
|
||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||
@@ -21,28 +21,26 @@ import {
|
||||
Select,
|
||||
TextArea,
|
||||
Checkbox,
|
||||
Banner,
|
||||
Banner
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { Divider } from 'semantic-ui-react';
|
||||
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo-0301': 'gpt-3.5-turbo',
|
||||
'gpt-4-0314': 'gpt-4',
|
||||
'gpt-4-32k-0314': 'gpt-4-32k',
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||
};
|
||||
|
||||
const STATUS_CODE_MAPPING_EXAMPLE = {
|
||||
400: '500',
|
||||
400: '500'
|
||||
};
|
||||
|
||||
const REGION_EXAMPLE = {
|
||||
"default": "us-central1",
|
||||
"claude-3-5-sonnet-20240620": "europe-west1"
|
||||
}
|
||||
'default': 'us-central1',
|
||||
'claude-3-5-sonnet-20240620': 'europe-west1'
|
||||
};
|
||||
|
||||
const fetchButtonTips = "1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出"
|
||||
const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出';
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
// inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥')
|
||||
@@ -84,6 +82,9 @@ const EditChannel = (props) => {
|
||||
auto_ban: 1,
|
||||
test_model: '',
|
||||
groups: ['default'],
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
tag: ''
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
const [autoBan, setAutoBan] = useState(true);
|
||||
@@ -108,7 +109,7 @@ const EditChannel = (props) => {
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_uploads',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 5:
|
||||
@@ -128,13 +129,13 @@ const EditChannel = (props) => {
|
||||
'mj_high_variation',
|
||||
'mj_low_variation',
|
||||
'mj_pan',
|
||||
'mj_uploads',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 36:
|
||||
localModels = [
|
||||
'suno_music',
|
||||
'suno_lyrics',
|
||||
'suno_lyrics'
|
||||
];
|
||||
break;
|
||||
default:
|
||||
@@ -171,7 +172,7 @@ const EditChannel = (props) => {
|
||||
data.model_mapping = JSON.stringify(
|
||||
JSON.parse(data.model_mapping),
|
||||
null,
|
||||
2,
|
||||
2
|
||||
);
|
||||
}
|
||||
setInputs(data);
|
||||
@@ -190,70 +191,69 @@ const EditChannel = (props) => {
|
||||
|
||||
|
||||
const fetchUpstreamModelList = async (name) => {
|
||||
if (inputs["type"] !== 1) {
|
||||
showError("仅支持 OpenAI 接口格式")
|
||||
if (inputs['type'] !== 1) {
|
||||
showError('仅支持 OpenAI 接口格式');
|
||||
return;
|
||||
}
|
||||
setLoading(true)
|
||||
const models = inputs["models"] || []
|
||||
setLoading(true);
|
||||
const models = inputs['models'] || [];
|
||||
let err = false;
|
||||
if (isEdit) {
|
||||
const res = await API.get("/api/channel/fetch_models/" + channelId)
|
||||
const res = await API.get('/api/channel/fetch_models/' + channelId);
|
||||
if (res.data && res.data?.success) {
|
||||
models.push(...res.data.data)
|
||||
models.push(...res.data.data);
|
||||
} else {
|
||||
err = true
|
||||
err = true;
|
||||
}
|
||||
} else {
|
||||
if (!inputs?.["key"]) {
|
||||
showError("请填写密钥")
|
||||
err = true
|
||||
if (!inputs?.['key']) {
|
||||
showError('请填写密钥');
|
||||
err = true;
|
||||
} else {
|
||||
try {
|
||||
const host = new URL((inputs["base_url"] || "https://api.openai.com"))
|
||||
const host = new URL((inputs['base_url'] || 'https://api.openai.com'));
|
||||
|
||||
const url = `https://${host.hostname}/v1/models`;
|
||||
const key = inputs["key"];
|
||||
const key = inputs['key'];
|
||||
const res = await axios.get(url, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`
|
||||
}
|
||||
})
|
||||
});
|
||||
if (res.data && res.data?.success) {
|
||||
models.push(...res.data.data.map((model) => model.id))
|
||||
models.push(...res.data.data.map((model) => model.id));
|
||||
} else {
|
||||
err = true
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
err = true
|
||||
} catch (error) {
|
||||
err = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!err) {
|
||||
handleInputChange(name, Array.from(new Set(models)));
|
||||
showSuccess("获取模型列表成功");
|
||||
showSuccess('获取模型列表成功');
|
||||
} else {
|
||||
showError('获取模型列表失败');
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
let localModelOptions = res.data.data.map((model) => ({
|
||||
label: model.id,
|
||||
value: model.id,
|
||||
value: model.id
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
setFullModels(res.data.data.map((model) => model.id));
|
||||
setBasicModels(
|
||||
res.data.data
|
||||
.filter((model) => {
|
||||
return model.id.startsWith('gpt-3') || model.id.startsWith('text-');
|
||||
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||
})
|
||||
.map((model) => model.id),
|
||||
.map((model) => model.id)
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -269,8 +269,8 @@ const EditChannel = (props) => {
|
||||
setGroupOptions(
|
||||
res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group,
|
||||
})),
|
||||
value: group
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
@@ -280,10 +280,10 @@ const EditChannel = (props) => {
|
||||
useEffect(() => {
|
||||
let localModelOptions = [...originModelOptions];
|
||||
inputs.models.forEach((model) => {
|
||||
if (!localModelOptions.find((option) => option.key === model)) {
|
||||
if (!localModelOptions.find((option) => option.label === model)) {
|
||||
localModelOptions.push({
|
||||
label: model,
|
||||
value: model,
|
||||
value: model
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -320,7 +320,7 @@ const EditChannel = (props) => {
|
||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||
localInputs.base_url = localInputs.base_url.slice(
|
||||
0,
|
||||
localInputs.base_url.length - 1,
|
||||
localInputs.base_url.length - 1
|
||||
);
|
||||
}
|
||||
if (localInputs.type === 3 && localInputs.other === '') {
|
||||
@@ -341,7 +341,7 @@ const EditChannel = (props) => {
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/channel/`, {
|
||||
...localInputs,
|
||||
id: parseInt(channelId),
|
||||
id: parseInt(channelId)
|
||||
});
|
||||
} else {
|
||||
res = await API.post(`/api/channel/`, localInputs);
|
||||
@@ -378,7 +378,7 @@ const EditChannel = (props) => {
|
||||
// 添加到下拉选项
|
||||
key: model,
|
||||
text: model,
|
||||
value: model,
|
||||
value: model
|
||||
});
|
||||
} else if (model) {
|
||||
showError('某些模型已存在!');
|
||||
@@ -409,11 +409,11 @@ const EditChannel = (props) => {
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button theme='solid' size={'large'} onClick={submit}>
|
||||
<Button theme="solid" size={'large'} onClick={submit}>
|
||||
提交
|
||||
</Button>
|
||||
<Button
|
||||
theme='solid'
|
||||
theme="solid"
|
||||
size={'large'}
|
||||
type={'tertiary'}
|
||||
onClick={handleCancel}
|
||||
@@ -432,7 +432,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>类型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
name='type'
|
||||
name="type"
|
||||
required
|
||||
optionList={CHANNEL_OPTIONS}
|
||||
value={inputs.type}
|
||||
@@ -450,8 +450,8 @@ const EditChannel = (props) => {
|
||||
,因为 One API 会把请求体中的 model
|
||||
参数替换为你的部署名称(模型名称中的点会被剔除),
|
||||
<a
|
||||
target='_blank'
|
||||
href='https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271'
|
||||
target="_blank"
|
||||
href="https://github.com/songquanpeng/one-api/issues/133?notification_referrer_id=NT_kwDOAmJSYrM2NjIwMzI3NDgyOjM5OTk4MDUw#issuecomment-1571602271"
|
||||
>
|
||||
图片演示
|
||||
</a>
|
||||
@@ -466,8 +466,8 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='AZURE_OPENAI_ENDPOINT'
|
||||
name='azure_base_url'
|
||||
label="AZURE_OPENAI_ENDPOINT"
|
||||
name="azure_base_url"
|
||||
placeholder={
|
||||
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
|
||||
}
|
||||
@@ -475,14 +475,14 @@ const EditChannel = (props) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>默认 API 版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='默认 API 版本'
|
||||
name='azure_other'
|
||||
label="默认 API 版本"
|
||||
name="azure_other"
|
||||
placeholder={
|
||||
'请输入默认 API 版本,例如:2023-06-01-preview,该配置可以被实际的请求查询参数所覆盖'
|
||||
}
|
||||
@@ -490,7 +490,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -512,7 +512,7 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
|
||||
}
|
||||
@@ -520,49 +520,84 @@ const EditChannel = (props) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>代理:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="代理"
|
||||
name="base_url"
|
||||
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{inputs.type === 22 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>私有部署地址:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{inputs.type === 36 && (
|
||||
<>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>
|
||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
placeholder={
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name="base_url"
|
||||
placeholder={
|
||||
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com '
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div style={{marginTop: 10}}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>名称:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
required
|
||||
name='name'
|
||||
required
|
||||
name="name"
|
||||
placeholder={'请为渠道命名'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('name', value);
|
||||
}}
|
||||
value={inputs.name}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组'}
|
||||
name='groups'
|
||||
name="groups"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
@@ -572,7 +607,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
{inputs.type === 18 && (
|
||||
@@ -581,7 +616,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>模型版本:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入星火大模型版本,注意是接口地址中的版本号,例如:v2.1'
|
||||
}
|
||||
@@ -589,7 +624,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -599,7 +634,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>部署地区:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入部署地区,例如:us-central1\n支持使用模型映射格式\n' +
|
||||
'{\n' +
|
||||
@@ -612,18 +647,18 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'other',
|
||||
JSON.stringify(REGION_EXAMPLE, null, 2),
|
||||
JSON.stringify(REGION_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -637,14 +672,14 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>知识库 ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='知识库 ID'
|
||||
name='other'
|
||||
label="知识库 ID"
|
||||
name="other"
|
||||
placeholder={'请输入知识库 ID,例如:123456'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -654,7 +689,7 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>Account ID:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='other'
|
||||
name="other"
|
||||
placeholder={
|
||||
'请输入Account ID,例如:d6b5da8hk1awo8nap34ube6gh'
|
||||
}
|
||||
@@ -662,7 +697,7 @@ const EditChannel = (props) => {
|
||||
handleInputChange('other', value);
|
||||
}}
|
||||
value={inputs.other}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -671,21 +706,23 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型'}
|
||||
name='models'
|
||||
name="models"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
filter
|
||||
searchPosition='dropdown'
|
||||
onChange={(value) => {
|
||||
handleInputChange('models', value);
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
<div style={{ lineHeight: '40px', marginBottom: '12px' }}>
|
||||
<Space>
|
||||
<Button
|
||||
type='primary'
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
handleInputChange('models', basicModels);
|
||||
}}
|
||||
@@ -693,7 +730,7 @@ const EditChannel = (props) => {
|
||||
填入相关模型
|
||||
</Button>
|
||||
<Button
|
||||
type='secondary'
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
handleInputChange('models', fullModels);
|
||||
}}
|
||||
@@ -702,7 +739,7 @@ const EditChannel = (props) => {
|
||||
</Button>
|
||||
<Tooltip content={fetchButtonTips}>
|
||||
<Button
|
||||
type='tertiary'
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
fetchUpstreamModelList('models');
|
||||
}}
|
||||
@@ -711,7 +748,7 @@ const EditChannel = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type='warning'
|
||||
type="warning"
|
||||
onClick={() => {
|
||||
handleInputChange('models', []);
|
||||
}}
|
||||
@@ -721,11 +758,11 @@ const EditChannel = (props) => {
|
||||
</Space>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type='primary' onClick={addCustomModels}>
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
填入
|
||||
</Button>
|
||||
}
|
||||
placeholder='输入自定义模型名称'
|
||||
placeholder="输入自定义模型名称"
|
||||
value={customModel}
|
||||
onChange={(value) => {
|
||||
setCustomModel(value.trim());
|
||||
@@ -737,24 +774,24 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,例如:\n${JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name='model_mapping'
|
||||
name="model_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.model_mapping}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2),
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -765,8 +802,8 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
{batch ? (
|
||||
<TextArea
|
||||
label='密钥'
|
||||
name='key'
|
||||
label="密钥"
|
||||
name="key"
|
||||
required
|
||||
placeholder={'请输入密钥,一行一个'}
|
||||
onChange={(value) => {
|
||||
@@ -774,14 +811,14 @@ const EditChannel = (props) => {
|
||||
}}
|
||||
value={inputs.key}
|
||||
style={{ minHeight: 150, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 41 ? (
|
||||
<TextArea
|
||||
label='鉴权json'
|
||||
name='key'
|
||||
label="鉴权json"
|
||||
name="key"
|
||||
required
|
||||
placeholder={'{\n' +
|
||||
' "type": "service_account",\n' +
|
||||
@@ -801,23 +838,36 @@ const EditChannel = (props) => {
|
||||
}}
|
||||
autosize={{ minRows: 10 }}
|
||||
value={inputs.key}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
label='密钥'
|
||||
name='key'
|
||||
label="密钥"
|
||||
name="key"
|
||||
required
|
||||
placeholder={type2secretPrompt(inputs.type)}
|
||||
onChange={(value) => {
|
||||
handleInputChange('key', value);
|
||||
}}
|
||||
value={inputs.key}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
{!isEdit && (
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={batch}
|
||||
label="批量创建"
|
||||
name="batch"
|
||||
onChange={() => setBatch(!batch)}
|
||||
/>
|
||||
<Typography.Text strong>批量创建</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
{inputs.type === 1 && (
|
||||
<>
|
||||
@@ -825,9 +875,9 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>组织:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='组织,可选,不填则为默认组织'
|
||||
name='openai_organization'
|
||||
placeholder='请输入组织org-xxx'
|
||||
label="组织,可选,不填则为默认组织"
|
||||
name="openai_organization"
|
||||
placeholder="请输入组织org-xxx"
|
||||
onChange={(value) => {
|
||||
handleInputChange('openai_organization', value);
|
||||
}}
|
||||
@@ -839,8 +889,8 @@ const EditChannel = (props) => {
|
||||
<Typography.Text strong>默认测试模型:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='test_model'
|
||||
placeholder='不填则为模型列表第一个'
|
||||
name="test_model"
|
||||
placeholder="不填则为模型列表第一个"
|
||||
onChange={(value) => {
|
||||
handleInputChange('test_model', value);
|
||||
}}
|
||||
@@ -849,7 +899,7 @@ const EditChannel = (props) => {
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
name='auto_ban'
|
||||
name="auto_ban"
|
||||
checked={autoBan}
|
||||
onChange={() => {
|
||||
setAutoBan(!autoBan);
|
||||
@@ -861,55 +911,6 @@ const EditChannel = (props) => {
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{!isEdit && (
|
||||
<div style={{ marginTop: 10, display: 'flex' }}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={batch}
|
||||
label='批量创建'
|
||||
name='batch'
|
||||
onChange={() => setBatch(!batch)}
|
||||
/>
|
||||
<Typography.Text strong>批量创建</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
{inputs.type !== 3 && inputs.type !== 8 && inputs.type !== 22 && inputs.type !== 36 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>代理:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label='代理'
|
||||
name='base_url'
|
||||
placeholder={'此项可选,用于通过代理站来进行 API 调用'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{inputs.type === 22 && (
|
||||
<>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>私有部署地址:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
name='base_url'
|
||||
placeholder={
|
||||
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleInputChange('base_url', value);
|
||||
}}
|
||||
value={inputs.base_url}
|
||||
autoComplete='new-password'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
状态码复写(仅影响本地判断,不修改返回到上游的状态码):
|
||||
@@ -917,43 +918,84 @@ const EditChannel = (props) => {
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于复写返回的状态码,比如将claude渠道的400错误复写为500(用于重试),请勿滥用该功能,例如:\n${JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)}`}
|
||||
name='status_code_mapping'
|
||||
name="status_code_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('status_code_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.status_code_mapping}
|
||||
autoComplete='new-password'
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'status_code_mapping',
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2),
|
||||
JSON.stringify(STATUS_CODE_MAPPING_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
</Typography.Text>
|
||||
{/*<div style={{ marginTop: 10 }}>*/}
|
||||
{/* <Typography.Text strong>*/}
|
||||
{/* 最大请求token(0表示不限制):*/}
|
||||
{/* </Typography.Text>*/}
|
||||
{/*</div>*/}
|
||||
{/*<Input*/}
|
||||
{/* label='最大请求token'*/}
|
||||
{/* name='max_input_tokens'*/}
|
||||
{/* placeholder='默认为0,表示不限制'*/}
|
||||
{/* onChange={(value) => {*/}
|
||||
{/* handleInputChange('max_input_tokens', value);*/}
|
||||
{/* }}*/}
|
||||
{/* value={inputs.max_input_tokens}*/}
|
||||
{/*/>*/}
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道标签
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道标签"
|
||||
name="tag"
|
||||
placeholder={'渠道标签'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('tag', value);
|
||||
}}
|
||||
value={inputs.tag}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道优先级
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道优先级"
|
||||
name="priority"
|
||||
placeholder={'渠道优先级'}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
handleInputChange('priority', value);
|
||||
} else {
|
||||
handleInputChange('priority', number);
|
||||
}
|
||||
}}
|
||||
value={inputs.priority}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>
|
||||
渠道权重
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
label="渠道权重"
|
||||
name="weight"
|
||||
placeholder={'渠道权重'}
|
||||
onChange={(value) => {
|
||||
const number = parseInt(value);
|
||||
if (isNaN(number)) {
|
||||
handleInputChange('weight', value);
|
||||
} else {
|
||||
handleInputChange('weight', number);
|
||||
}
|
||||
}}
|
||||
value={inputs.weight}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
</>
|
||||
|
||||
366
web/src/pages/Channel/EditTagModal.js
Normal file
366
web/src/pages/Channel/EditTagModal.js
Normal file
@@ -0,0 +1,366 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { API, showError, showInfo, showSuccess, showWarning, verifyJSON } from '../../helpers';
|
||||
import { SideSheet, Space, Button, Input, Typography, Spin, Modal, Select, Banner, TextArea } from '@douyinfe/semi-ui';
|
||||
import TextInput from '../../components/custom/TextInput.js';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
|
||||
const MODEL_MAPPING_EXAMPLE = {
|
||||
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125'
|
||||
};
|
||||
|
||||
const EditTagModal = (props) => {
|
||||
const { visible, tag, handleClose, refresh } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [originModelOptions, setOriginModelOptions] = useState([]);
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [basicModels, setBasicModels] = useState([]);
|
||||
const [fullModels, setFullModels] = useState([]);
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const originInputs = {
|
||||
tag: '',
|
||||
new_tag: null,
|
||||
model_mapping: null,
|
||||
groups: [],
|
||||
models: [],
|
||||
}
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
if (name === 'type') {
|
||||
let localModels = [];
|
||||
switch (value) {
|
||||
case 2:
|
||||
localModels = [
|
||||
'mj_imagine',
|
||||
'mj_variation',
|
||||
'mj_reroll',
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 5:
|
||||
localModels = [
|
||||
'swap_face',
|
||||
'mj_imagine',
|
||||
'mj_variation',
|
||||
'mj_reroll',
|
||||
'mj_blend',
|
||||
'mj_upscale',
|
||||
'mj_describe',
|
||||
'mj_zoom',
|
||||
'mj_shorten',
|
||||
'mj_modal',
|
||||
'mj_inpaint',
|
||||
'mj_custom_zoom',
|
||||
'mj_high_variation',
|
||||
'mj_low_variation',
|
||||
'mj_pan',
|
||||
'mj_uploads'
|
||||
];
|
||||
break;
|
||||
case 36:
|
||||
localModels = [
|
||||
'suno_music',
|
||||
'suno_lyrics'
|
||||
];
|
||||
break;
|
||||
default:
|
||||
localModels = getChannelModels(value);
|
||||
break;
|
||||
}
|
||||
if (inputs.models.length === 0) {
|
||||
setInputs((inputs) => ({ ...inputs, models: localModels }));
|
||||
}
|
||||
setBasicModels(localModels);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/channel/models`);
|
||||
let localModelOptions = res.data.data.map((model) => ({
|
||||
label: model.id,
|
||||
value: model.id
|
||||
}));
|
||||
setOriginModelOptions(localModelOptions);
|
||||
setFullModels(res.data.data.map((model) => model.id));
|
||||
setBasicModels(
|
||||
res.data.data
|
||||
.filter((model) => {
|
||||
return model.id.startsWith('gpt-') || model.id.startsWith('text-');
|
||||
})
|
||||
.map((model) => model.id)
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/group/`);
|
||||
if (res === undefined) {
|
||||
return;
|
||||
}
|
||||
setGroupOptions(
|
||||
res.data.data.map((group) => ({
|
||||
label: group,
|
||||
value: group
|
||||
}))
|
||||
);
|
||||
} catch (error) {
|
||||
showError(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
setLoading(true);
|
||||
let data = {
|
||||
tag: tag,
|
||||
}
|
||||
if (inputs.model_mapping !== null && inputs.model_mapping !== '') {
|
||||
if (inputs.model_mapping !== '' && !verifyJSON(inputs.model_mapping)) {
|
||||
showInfo('模型映射必须是合法的 JSON 格式!');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
data.model_mapping = inputs.model_mapping
|
||||
}
|
||||
if (inputs.groups.length > 0) {
|
||||
data.groups = inputs.groups.join(',');
|
||||
}
|
||||
if (inputs.models.length > 0) {
|
||||
data.models = inputs.models.join(',');
|
||||
}
|
||||
data.new_tag = inputs.new_tag;
|
||||
// check have any change
|
||||
if (data.model_mapping === undefined && data.groups === undefined && data.models === undefined && data.new_tag === undefined) {
|
||||
showWarning('没有任何修改!');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await submit(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const submit = async (data) => {
|
||||
try {
|
||||
const res = await API.put('/api/channel/tag', data);
|
||||
if (res?.data?.success) {
|
||||
showSuccess('标签更新成功!');
|
||||
refresh();
|
||||
handleClose();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let localModelOptions = [...originModelOptions];
|
||||
inputs.models.forEach((model) => {
|
||||
if (!localModelOptions.find((option) => option.label === model)) {
|
||||
localModelOptions.push({
|
||||
label: model,
|
||||
value: model
|
||||
});
|
||||
}
|
||||
});
|
||||
setModelOptions(localModelOptions);
|
||||
}, [originModelOptions, inputs.models]);
|
||||
|
||||
useEffect(() => {
|
||||
setInputs({
|
||||
...originInputs,
|
||||
tag: tag,
|
||||
new_tag: tag,
|
||||
})
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
}, [visible]);
|
||||
|
||||
const addCustomModels = () => {
|
||||
if (customModel.trim() === '') return;
|
||||
// 使用逗号分隔字符串,然后去除每个模型名称前后的空格
|
||||
const modelArray = customModel.split(',').map((model) => model.trim());
|
||||
|
||||
let localModels = [...inputs.models];
|
||||
let localModelOptions = [...modelOptions];
|
||||
let hasError = false;
|
||||
|
||||
modelArray.forEach((model) => {
|
||||
// 检查模型是否已存在,且模型名称非空
|
||||
if (model && !localModels.includes(model)) {
|
||||
localModels.push(model); // 添加到模型列表
|
||||
localModelOptions.push({
|
||||
// 添加到下拉选项
|
||||
key: model,
|
||||
text: model,
|
||||
value: model
|
||||
});
|
||||
} else if (model) {
|
||||
showError('某些模型已存在!');
|
||||
hasError = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) return; // 如果有错误则终止操作
|
||||
|
||||
// 更新状态值
|
||||
setModelOptions(localModelOptions);
|
||||
setCustomModel('');
|
||||
handleInputChange('models', localModels);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<SideSheet
|
||||
title="编辑标签"
|
||||
visible={visible}
|
||||
onCancel={handleClose}
|
||||
footer={
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Space>
|
||||
<Button onClick={handleClose}>取消</Button>
|
||||
<Button type="primary" onClick={handleSave} loading={loading}>保存</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Banner
|
||||
type={'warning'}
|
||||
description={
|
||||
<>
|
||||
所有编辑均为覆盖操作,留空则不更改
|
||||
</>
|
||||
}
|
||||
></Banner>
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
<TextInput
|
||||
label="标签名,留空则解散标签"
|
||||
name="newTag"
|
||||
value={inputs.new_tag}
|
||||
onChange={(value) => setInputs({ ...inputs, new_tag: value })}
|
||||
placeholder="请输入新标签"
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型,留空则不更改:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择该渠道所支持的模型,留空则不更改'}
|
||||
name="models"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
filter
|
||||
searchPosition='dropdown'
|
||||
onChange={(value) => {
|
||||
handleInputChange('models', value);
|
||||
}}
|
||||
value={inputs.models}
|
||||
autoComplete="new-password"
|
||||
optionList={modelOptions}
|
||||
/>
|
||||
<Input
|
||||
addonAfter={
|
||||
<Button type="primary" onClick={addCustomModels}>
|
||||
填入
|
||||
</Button>
|
||||
}
|
||||
placeholder="输入自定义模型名称"
|
||||
value={customModel}
|
||||
onChange={(value) => {
|
||||
setCustomModel(value.trim());
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组,留空则不更改:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择可以使用该渠道的分组,留空则不更改'}
|
||||
name="groups"
|
||||
required
|
||||
multiple
|
||||
selection
|
||||
allowAdditions
|
||||
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
|
||||
onChange={(value) => {
|
||||
handleInputChange('groups', value);
|
||||
}}
|
||||
value={inputs.groups}
|
||||
autoComplete="new-password"
|
||||
optionList={groupOptions}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型重定向:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder={`此项可选,用于修改请求体中的模型名称,为一个 JSON 字符串,键为请求中模型名称,值为要替换的模型名称,留空则不更改`}
|
||||
name="model_mapping"
|
||||
onChange={(value) => {
|
||||
handleInputChange('model_mapping', value);
|
||||
}}
|
||||
autosize
|
||||
value={inputs.model_mapping}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<Space>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify(MODEL_MAPPING_EXAMPLE, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
填入模板
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
JSON.stringify({}, null, 2)
|
||||
);
|
||||
}}
|
||||
>
|
||||
清空重定向
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
style={{
|
||||
color: 'rgba(var(--semi-blue-5), 1)',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => {
|
||||
handleInputChange(
|
||||
'model_mapping',
|
||||
""
|
||||
);
|
||||
}}
|
||||
>
|
||||
不更改
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Spin>
|
||||
</SideSheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTagModal;
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
|
||||
|
||||
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
|
||||
import VChart from '@visactor/vchart';
|
||||
import { Button, Card, Col, Descriptions, Form, Layout, Row, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import { VChart } from "@visactor/react-vchart";
|
||||
import {
|
||||
API,
|
||||
isAdmin,
|
||||
@@ -17,11 +17,16 @@ import {
|
||||
renderQuota,
|
||||
renderQuotaNumberWithDigit,
|
||||
stringToColor,
|
||||
modelToColor,
|
||||
} from '../../helpers/render';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
const Detail = (props) => {
|
||||
const formRef = useRef();
|
||||
let now = new Date();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
@@ -40,32 +45,76 @@ const Detail = (props) => {
|
||||
inputs;
|
||||
const isAdminUser = isAdmin();
|
||||
const initialized = useRef(false);
|
||||
const [modelDataChart, setModelDataChart] = useState(null);
|
||||
const [modelDataPieChart, setModelDataPieChart] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [quotaData, setQuotaData] = useState([]);
|
||||
const [consumeQuota, setConsumeQuota] = useState(0);
|
||||
const [consumeTokens, setConsumeTokens] = useState(0);
|
||||
const [times, setTimes] = useState(0);
|
||||
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
|
||||
localStorage.getItem('data_export_default_time') || 'hour',
|
||||
);
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
if (name === 'data_export_default_time') {
|
||||
setDataExportDefaultTime(value);
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const spec_line = {
|
||||
type: 'bar',
|
||||
data: [
|
||||
{
|
||||
id: 'barData',
|
||||
values: [],
|
||||
const [pieData, setPieData] = useState([{ type: 'null', value: '0' }]);
|
||||
const [lineData, setLineData] = useState([]);
|
||||
const [spec_pie, setSpecPie] = useState({
|
||||
type: 'pie',
|
||||
data: [{
|
||||
id: 'id0',
|
||||
values: pieData
|
||||
}],
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.5,
|
||||
padAngle: 0.6,
|
||||
valueField: 'value',
|
||||
categoryField: 'type',
|
||||
pie: {
|
||||
style: {
|
||||
cornerRadius: 10,
|
||||
},
|
||||
],
|
||||
state: {
|
||||
hover: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
selected: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型调用次数占比',
|
||||
subtext: `总计:${renderNumber(times)}`,
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
orient: 'left',
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['type'],
|
||||
value: (datum) => renderNumber(datum['value']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
const [spec_line, setSpecLine] = useState({
|
||||
type: 'bar',
|
||||
data: [{
|
||||
id: 'barData',
|
||||
values: lineData
|
||||
}],
|
||||
xField: 'Time',
|
||||
yField: 'Usage',
|
||||
seriesField: 'Model',
|
||||
@@ -77,7 +126,7 @@ const Detail = (props) => {
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型消耗分布',
|
||||
subtext: '0',
|
||||
subtext: `总计:${renderQuota(consumeQuota, 2)}`,
|
||||
},
|
||||
bar: {
|
||||
// The state style of bar
|
||||
@@ -129,196 +178,197 @@ const Detail = (props) => {
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
});
|
||||
|
||||
// 添加一个新的状态来存储模型-颜色映射
|
||||
const [modelColors, setModelColors] = useState({});
|
||||
|
||||
const handleInputChange = (value, name) => {
|
||||
if (name === 'data_export_default_time') {
|
||||
setDataExportDefaultTime(value);
|
||||
return;
|
||||
}
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const spec_pie = {
|
||||
type: 'pie',
|
||||
data: [
|
||||
{
|
||||
id: 'id0',
|
||||
values: [{ type: 'null', value: '0' }],
|
||||
},
|
||||
],
|
||||
outerRadius: 0.8,
|
||||
innerRadius: 0.5,
|
||||
padAngle: 0.6,
|
||||
valueField: 'value',
|
||||
categoryField: 'type',
|
||||
pie: {
|
||||
style: {
|
||||
cornerRadius: 10,
|
||||
},
|
||||
state: {
|
||||
hover: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
selected: {
|
||||
outerRadius: 0.85,
|
||||
stroke: '#000',
|
||||
lineWidth: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
title: {
|
||||
visible: true,
|
||||
text: '模型调用次数占比',
|
||||
},
|
||||
legends: {
|
||||
visible: true,
|
||||
orient: 'left',
|
||||
},
|
||||
label: {
|
||||
visible: true,
|
||||
},
|
||||
tooltip: {
|
||||
mark: {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['type'],
|
||||
value: (datum) => renderNumber(datum['value']),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
color: {
|
||||
specified: modelColorMap,
|
||||
},
|
||||
};
|
||||
|
||||
const loadQuotaData = async (lineChart, pieChart) => {
|
||||
const loadQuotaData = async () => {
|
||||
setLoading(true);
|
||||
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
} else {
|
||||
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setQuotaData(data);
|
||||
if (data.length === 0) {
|
||||
data.push({
|
||||
count: 0,
|
||||
model_name: '无数据',
|
||||
quota: 0,
|
||||
created_at: now.getTime() / 1000,
|
||||
try {
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
} else {
|
||||
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setQuotaData(data);
|
||||
if (data.length === 0) {
|
||||
data.push({
|
||||
count: 0,
|
||||
model_name: '无数据',
|
||||
quota: 0,
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChartData(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChart(lineChart, pieChart, data);
|
||||
} else {
|
||||
showError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await loadQuotaData(modelDataChart, modelDataPieChart);
|
||||
await loadQuotaData();
|
||||
};
|
||||
|
||||
const initChart = async () => {
|
||||
let lineChart = modelDataChart;
|
||||
if (!modelDataChart) {
|
||||
lineChart = new VChart(spec_line, { dom: 'model_data' });
|
||||
setModelDataChart(lineChart);
|
||||
lineChart.renderAsync();
|
||||
}
|
||||
let pieChart = modelDataPieChart;
|
||||
if (!modelDataPieChart) {
|
||||
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
|
||||
setModelDataPieChart(pieChart);
|
||||
pieChart.renderAsync();
|
||||
}
|
||||
console.log('init vchart');
|
||||
await loadQuotaData(lineChart, pieChart);
|
||||
await loadQuotaData();
|
||||
};
|
||||
|
||||
const updateChart = (lineChart, pieChart, data) => {
|
||||
if (isAdminUser) {
|
||||
// 将所有用户合并
|
||||
}
|
||||
let pieData = [];
|
||||
let lineData = [];
|
||||
let consumeQuota = 0;
|
||||
let times = 0;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i];
|
||||
consumeQuota += item.quota;
|
||||
times += item.count;
|
||||
// 合并model_name
|
||||
let pieItem = pieData.find((it) => it.type === item.model_name);
|
||||
const updateChartData = (data) => {
|
||||
let newPieData = [];
|
||||
let newLineData = [];
|
||||
let totalQuota = 0;
|
||||
let totalTimes = 0;
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
let uniqueTimes = new Set();
|
||||
data.forEach(item => {
|
||||
uniqueModels.add(item.model_name);
|
||||
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||
totalTokens += item.token_used;
|
||||
});
|
||||
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
newModelColors[modelName] = modelColorMap[modelName] ||
|
||||
modelColors[modelName] ||
|
||||
modelToColor(modelName);
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 处理饼图数据
|
||||
for (let item of data) {
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||
if (pieItem) {
|
||||
pieItem.value += item.count;
|
||||
} else {
|
||||
pieData.push({
|
||||
newPieData.push({
|
||||
type: item.model_name,
|
||||
value: item.count,
|
||||
});
|
||||
}
|
||||
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
|
||||
// 转换日期格式
|
||||
let createTime = timestamp2string1(
|
||||
item.created_at,
|
||||
dataExportDefaultTime,
|
||||
);
|
||||
let lineItem = lineData.find(
|
||||
(it) => it.Time === createTime && it.Model === item.model_name,
|
||||
);
|
||||
if (lineItem) {
|
||||
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
|
||||
} else {
|
||||
lineData.push({
|
||||
Time: createTime,
|
||||
Model: item.model_name,
|
||||
Usage: parseFloat(getQuotaWithUnit(item.quota)),
|
||||
});
|
||||
}
|
||||
}
|
||||
setConsumeQuota(consumeQuota);
|
||||
setTimes(times);
|
||||
|
||||
// sort by count
|
||||
pieData.sort((a, b) => b.value - a.value);
|
||||
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
|
||||
spec_pie.data[0].values = pieData;
|
||||
// 处理柱状图数据
|
||||
let timePoints = Array.from(uniqueTimes);
|
||||
if (timePoints.length < 7) {
|
||||
// 根据时间粒度生成合适的时间点
|
||||
const generateTimePoints = () => {
|
||||
let lastTime = Math.max(...data.map(item => item.created_at));
|
||||
let points = [];
|
||||
let interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
|
||||
spec_line.data[0].values = lineData;
|
||||
pieChart.updateSpec(spec_pie);
|
||||
lineChart.updateSpec(spec_line);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
|
||||
}
|
||||
return points.reverse();
|
||||
};
|
||||
|
||||
// pieChart.updateData('id0', pieData);
|
||||
// lineChart.updateData('barData', lineData);
|
||||
pieChart.reLayout();
|
||||
lineChart.reLayout();
|
||||
timePoints = generateTimePoints();
|
||||
}
|
||||
|
||||
// 为每个时间点和模型生成数据
|
||||
timePoints.forEach(time => {
|
||||
Array.from(uniqueModels).forEach(model => {
|
||||
let existingData = data.find(item =>
|
||||
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
|
||||
item.model_name === model
|
||||
);
|
||||
|
||||
newLineData.push({
|
||||
Time: time,
|
||||
Model: model,
|
||||
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 排序
|
||||
newPieData.sort((a, b) => b.value - a.value);
|
||||
newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
|
||||
|
||||
// 更新图表配置和数据
|
||||
setSpecPie(prev => ({
|
||||
...prev,
|
||||
data: [{ id: 'id0', values: newPieData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderNumber(totalTimes)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
}));
|
||||
|
||||
setSpecLine(prev => ({
|
||||
...prev,
|
||||
data: [{ id: 'barData', values: newLineData }],
|
||||
title: {
|
||||
...prev.title,
|
||||
subtext: `总计:${renderQuota(totalQuota, 2)}`
|
||||
},
|
||||
color: {
|
||||
specified: newModelColors
|
||||
}
|
||||
}));
|
||||
|
||||
setPieData(newPieData);
|
||||
setLineData(newLineData);
|
||||
setConsumeQuota(totalQuota);
|
||||
setTimes(totalTimes);
|
||||
setConsumeTokens(totalTokens);
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
userDispatch({type: 'login', payload: data});
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
|
||||
// if (dataExportDefaultTime === 'day') {
|
||||
// // 设置开始时间为7天前
|
||||
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
|
||||
// inputs.start_timestamp = st;
|
||||
// formRef.current.formApi.setValue('start_timestamp', st);
|
||||
// }
|
||||
getUserData()
|
||||
if (!initialized.current) {
|
||||
initVChartSemiTheme({
|
||||
isWatchingThemeSwitch: true,
|
||||
@@ -389,33 +439,93 @@ const Detail = (props) => {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
label='查询'
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
style={{ marginTop: 24 }}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Form.Section>
|
||||
<Button
|
||||
label='查询'
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
className='btn-margin-right'
|
||||
onClick={refresh}
|
||||
loading={loading}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Form.Section>
|
||||
</>
|
||||
</Form>
|
||||
<Spin spinning={loading}>
|
||||
<div style={{ height: 500 }}>
|
||||
<div
|
||||
id='model_pie'
|
||||
style={{ width: '100%', minWidth: 100 }}
|
||||
></div>
|
||||
</div>
|
||||
<div style={{ height: 500 }}>
|
||||
<div
|
||||
id='model_data'
|
||||
style={{ width: '100%', minWidth: 100 }}
|
||||
></div>
|
||||
</div>
|
||||
<Row gutter={{ xs: 16, sm: 16, md: 16, lg: 24, xl: 24, xxl: 24 }} style={{marginTop: 20}} type="flex" justify="space-between">
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card className='panel-desc-card'>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='当前余额'>
|
||||
{renderQuota(userState?.user?.quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='历史消耗'>
|
||||
{renderQuota(userState?.user?.used_quota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='请求次数'>
|
||||
{userState.user?.request_count}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={styleState.isMobile?24:8}>
|
||||
<Card>
|
||||
<Descriptions row size="small">
|
||||
<Descriptions.Item itemKey='统计额度'>
|
||||
{renderQuota(consumeQuota)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计Tokens'>
|
||||
{consumeTokens}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='统计次数'>
|
||||
{times}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={styleState.isMobile ? 24 : 8}>
|
||||
<Card>
|
||||
<Descriptions row size='small'>
|
||||
<Descriptions.Item itemKey='平均RPM'>
|
||||
{(times /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey='平均TPM'>
|
||||
{(consumeTokens /
|
||||
((Date.parse(end_timestamp) -
|
||||
Date.parse(start_timestamp)) /
|
||||
60000)).toFixed(3)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card style={{marginTop: 20}}>
|
||||
<Tabs type="line" defaultActiveKey="1">
|
||||
<Tabs.TabPane tab="消耗分布" itemKey="1">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_line}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="调用次数分布" itemKey="2">
|
||||
<div style={{ height: 500 }}>
|
||||
<VChart
|
||||
spec={spec_pie}
|
||||
option={{ mode: "desktop-browser" }}
|
||||
/>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
|
||||
</Tabs>
|
||||
</Card>
|
||||
</Spin>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
|
||||
@@ -3,11 +3,13 @@ import { Card, Col, Row } from '@douyinfe/semi-ui';
|
||||
import { API, showError, showNotice, timestamp2string } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
const Home = () => {
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
|
||||
const [homePageContent, setHomePageContent] = useState('');
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
|
||||
const displayNotice = async () => {
|
||||
const res = await API.get('/api/notice');
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { API, getUserIdFromLocalStorage, showError } from '../helpers';
|
||||
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography } from '@douyinfe/semi-ui';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { API, getUserIdFromLocalStorage, showError } from '../../helpers/index.js';
|
||||
import { Card, Chat, Input, Layout, Select, Slider, TextArea, Typography, Button } from '@douyinfe/semi-ui';
|
||||
import { SSE } from 'sse';
|
||||
import { IconSetting } from '@douyinfe/semi-icons';
|
||||
import { StyleContext } from '../../context/Style/index.js';
|
||||
|
||||
const defaultMessage = [
|
||||
{
|
||||
@@ -20,6 +22,21 @@ const defaultMessage = [
|
||||
}
|
||||
];
|
||||
|
||||
const roleInfo = {
|
||||
user: {
|
||||
name: 'User',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/docs-icon.png'
|
||||
},
|
||||
assistant: {
|
||||
name: 'Assistant',
|
||||
avatar: 'logo.png'
|
||||
},
|
||||
system: {
|
||||
name: 'System',
|
||||
avatar: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/other/logo.png'
|
||||
}
|
||||
}
|
||||
|
||||
let id = 4;
|
||||
function getId() {
|
||||
return `${id++}`
|
||||
@@ -39,6 +56,8 @@ const Playground = () => {
|
||||
const [message, setMessage] = useState(defaultMessage);
|
||||
const [models, setModels] = useState([]);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [showSettings, setShowSettings] = useState(true);
|
||||
const [styleState, styleDispatch] = useContext(StyleContext);
|
||||
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
@@ -84,11 +103,16 @@ const Playground = () => {
|
||||
// handleInputChange('group', localGroupOptions[0].value);
|
||||
|
||||
if (localGroupOptions.length > 0) {
|
||||
// set default group at first
|
||||
localGroupOptions.unshift({
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
});
|
||||
// set user group at first
|
||||
if (userState.user && userState.user.group) {
|
||||
let userGroup = userState.user.group;
|
||||
// Find and move user's group to the front
|
||||
const userGroupIndex = localGroupOptions.findIndex(g => g.value === userGroup);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = localGroupOptions.splice(userGroupIndex, 1)[0];
|
||||
localGroupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localGroupOptions = [{
|
||||
label: '用户分组',
|
||||
@@ -242,94 +266,147 @@ const Playground = () => {
|
||||
})
|
||||
}, []);
|
||||
|
||||
const SettingsToggle = () => {
|
||||
if (!styleState.isMobile) return null;
|
||||
return (
|
||||
<Button
|
||||
icon={<IconSetting />}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: showSettings ? -10 : -20,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1000,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: '0 20px 20px 0',
|
||||
padding: 0,
|
||||
boxShadow: '2px 0 8px rgba(0, 0, 0, 0.15)',
|
||||
}}
|
||||
onClick={() => setShowSettings(!showSettings)}
|
||||
theme="solid"
|
||||
type="primary"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function CustomInputRender(props) {
|
||||
const { detailProps } = props;
|
||||
const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps;
|
||||
|
||||
return <div style={{margin: '8px 16px', display: 'flex', flexDirection:'row',
|
||||
alignItems: 'flex-end', borderRadius: 16,padding: 10, border: '1px solid var(--semi-color-border)'}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/*{uploadNode}*/}
|
||||
{inputNode}
|
||||
{sendNode}
|
||||
</div>
|
||||
}
|
||||
|
||||
const renderInputArea = useCallback((props) => {
|
||||
return (<CustomInputRender {...props} />)
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout style={{height: '100%'}}>
|
||||
<Layout.Sider>
|
||||
<Card style={commonOuterStyle}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择分组'}
|
||||
name='group'
|
||||
required
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('group', value);
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择模型'}
|
||||
name='model'
|
||||
required
|
||||
selection
|
||||
filter
|
||||
onChange={(value) => {
|
||||
handleInputChange('model', value);
|
||||
}}
|
||||
value={inputs.model}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>Temperature:</Typography.Text>
|
||||
</div>
|
||||
<Slider
|
||||
step={0.1}
|
||||
min={0.1}
|
||||
max={1}
|
||||
value={inputs.temperature}
|
||||
onChange={(value) => {
|
||||
handleInputChange('temperature', value);
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>MaxTokens:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder='MaxTokens'
|
||||
name='max_tokens'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
defaultValue={0}
|
||||
value={inputs.max_tokens}
|
||||
onChange={(value) => {
|
||||
handleInputChange('max_tokens', value);
|
||||
}}
|
||||
/>
|
||||
{(showSettings || !styleState.isMobile) && (
|
||||
<Layout.Sider style={{ display: styleState.isMobile ? 'block' : 'initial' }}>
|
||||
<Card style={commonOuterStyle}>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>分组:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择分组'}
|
||||
name='group'
|
||||
required
|
||||
selection
|
||||
onChange={(value) => {
|
||||
handleInputChange('group', value);
|
||||
}}
|
||||
value={inputs.group}
|
||||
autoComplete='new-password'
|
||||
optionList={groups.map((group) => ({
|
||||
...group,
|
||||
label: styleState.isMobile && group.label.length > 16
|
||||
? group.label.substring(0, 16) + '...'
|
||||
: group.label,
|
||||
}))}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>模型:</Typography.Text>
|
||||
</div>
|
||||
<Select
|
||||
placeholder={'请选择模型'}
|
||||
name='model'
|
||||
required
|
||||
selection
|
||||
searchPosition='dropdown'
|
||||
filter
|
||||
onChange={(value) => {
|
||||
handleInputChange('model', value);
|
||||
}}
|
||||
value={inputs.model}
|
||||
autoComplete='new-password'
|
||||
optionList={models}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>Temperature:</Typography.Text>
|
||||
</div>
|
||||
<Slider
|
||||
step={0.1}
|
||||
min={0.1}
|
||||
max={1}
|
||||
value={inputs.temperature}
|
||||
onChange={(value) => {
|
||||
handleInputChange('temperature', value);
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>MaxTokens:</Typography.Text>
|
||||
</div>
|
||||
<Input
|
||||
placeholder='MaxTokens'
|
||||
name='max_tokens'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
defaultValue={0}
|
||||
value={inputs.max_tokens}
|
||||
onChange={(value) => {
|
||||
handleInputChange('max_tokens', value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>System:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder='System Prompt'
|
||||
name='system'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
autosize
|
||||
defaultValue={systemPrompt}
|
||||
// value={systemPrompt}
|
||||
onChange={(value) => {
|
||||
setSystemPrompt(value);
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<Typography.Text strong>System:</Typography.Text>
|
||||
</div>
|
||||
<TextArea
|
||||
placeholder='System Prompt'
|
||||
name='system'
|
||||
required
|
||||
autoComplete='new-password'
|
||||
autosize
|
||||
defaultValue={systemPrompt}
|
||||
// value={systemPrompt}
|
||||
onChange={(value) => {
|
||||
setSystemPrompt(value);
|
||||
}}
|
||||
/>
|
||||
|
||||
</Card>
|
||||
</Layout.Sider>
|
||||
</Card>
|
||||
</Layout.Sider>
|
||||
)}
|
||||
<Layout.Content>
|
||||
<div style={{height: '100%'}}>
|
||||
<div style={{height: '100%', position: 'relative'}}>
|
||||
<SettingsToggle />
|
||||
<Chat
|
||||
chatBoxRenderConfig={{
|
||||
renderChatBoxAction: () => {
|
||||
return <div></div>
|
||||
}
|
||||
}}
|
||||
renderInputArea={renderInputArea}
|
||||
roleConfig={roleInfo}
|
||||
style={commonOuterStyle}
|
||||
chats={message}
|
||||
onMessageSend={onMessageSend}
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../helpers';
|
||||
import { renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
@@ -66,11 +66,16 @@ const EditRedemption = (props) => {
|
||||
}, [props.editingRedemption.id]);
|
||||
|
||||
const submit = async () => {
|
||||
if (!isEdit && inputs.name === '') return;
|
||||
let name = inputs.name;
|
||||
if (!isEdit && inputs.name === '') {
|
||||
// set default name
|
||||
name = '兑换码-' + renderQuota(quota);
|
||||
}
|
||||
setLoading(true);
|
||||
let localInputs = inputs;
|
||||
localInputs.count = parseInt(localInputs.count);
|
||||
localInputs.quota = parseInt(localInputs.quota);
|
||||
localInputs.name = name;
|
||||
let res;
|
||||
if (isEdit) {
|
||||
res = await API.put(`/api/redemption/`, {
|
||||
|
||||
Reference in New Issue
Block a user