merge upstream

Signed-off-by: wozulong <>
This commit is contained in:
wozulong
2024-09-23 11:15:45 +08:00
32 changed files with 892 additions and 382 deletions

View File

@@ -21,11 +21,11 @@ import Redemption from './pages/Redemption';
import TopUp from './pages/TopUp';
import Log from './pages/Log';
import Chat from './pages/Chat';
import Chat2Link from './pages/Chat2Link';
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 Detail from './pages/Detail';
const Home = lazy(() => import('./pages/Home'));
const Detail = lazy(() => import('./pages/Detail'));
@@ -59,215 +59,224 @@ function App() {
}, []);
return (
<Layout>
<Layout.Content>
<Routes>
<Route
path='/'
element={
<>
<Routes>
<Route
path='/'
element={
<Suspense fallback={<Loading></Loading>}>
<Home />
</Suspense>
}
/>
<Route
path='/channel'
element={
<PrivateRoute>
<Channel />
</PrivateRoute>
}
/>
<Route
path='/channel/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
</Suspense>
}
/>
<Route
path='/channel/add'
element={
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
</Suspense>
}
/>
<Route
path='/token'
element={
<PrivateRoute>
<Token />
</PrivateRoute>
}
/>
<Route
path='/redemption'
element={
<PrivateRoute>
<Redemption />
</PrivateRoute>
}
/>
<Route
path='/user'
element={
<PrivateRoute>
<User />
</PrivateRoute>
}
/>
<Route
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
</Suspense>
}
/>
<Route
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
</Suspense>
}
/>
<Route
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm />
</Suspense>
}
/>
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<LoginForm />
</Suspense>
}
/>
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
</Suspense>
}
/>
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
</Suspense>
}
/>
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<LinuxDoOAuth />
</Suspense>
}
/>
<Route
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Home />
<Setting />
</Suspense>
}
/>
<Route
path='/channel'
element={
<PrivateRoute>
<Channel />
</PrivateRoute>
}
/>
<Route
path='/channel/edit/:id'
element={
</PrivateRoute>
}
/>
<Route
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
<TopUp />
</Suspense>
}
/>
<Route
path='/channel/add'
element={
</PrivateRoute>
}
/>
<Route
path='/log'
element={
<PrivateRoute>
<Log />
</PrivateRoute>
}
/>
<Route
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<EditChannel />
<Detail />
</Suspense>
}
/>
<Route
path='/token'
element={
<PrivateRoute>
<Token />
</PrivateRoute>
}
/>
<Route
path='/redemption'
element={
<PrivateRoute>
<Redemption />
</PrivateRoute>
}
/>
<Route
path='/user'
element={
<PrivateRoute>
<User />
</PrivateRoute>
}
/>
<Route
path='/user/edit/:id'
element={
</PrivateRoute>
}
/>
<Route
path='/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<EditUser />
<Midjourney />
</Suspense>
}
/>
<Route
path='/user/edit'
element={
</PrivateRoute>
}
/>
<Route
path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<EditUser />
<Task />
</Suspense>
}
/>
<Route
path='/user/reset'
element={
</PrivateRoute>
}
/>
<Route
path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Pricing />
</Suspense>
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat />
</Suspense>
}
/>
{/* 方便使用chat2link直接跳转聊天... */}
<Route
path='/chat2link'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm />
<Chat2Link />
</Suspense>
}
/>
<Route
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<LoginForm />
</Suspense>
}
/>
<Route
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
</Suspense>
}
/>
<Route
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
</Suspense>
}
/>
<Route
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
</Suspense>
}
/>
<Route
path='/oauth/linuxdo'
element={
<Suspense fallback={<Loading></Loading>}>
<LinuxDoOAuth />
</Suspense>
}
/>
<Route
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Setting />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/topup'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<TopUp />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/log'
element={
<PrivateRoute>
<Log />
</PrivateRoute>
}
/>
<Route
path='/detail'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Detail />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/midjourney'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Midjourney />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/task'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
<Task />
</Suspense>
</PrivateRoute>
}
/>
<Route
path='/pricing'
element={
<Suspense fallback={<Loading></Loading>}>
<Pricing />
</Suspense>
}
/>
<Route
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route
path='/chat'
element={
<Suspense fallback={<Loading></Loading>}>
<Chat />
</Suspense>
}
/>
<Route path='*' element={<NotFound />} />
</Routes>
</Layout.Content>
</Layout>
</PrivateRoute>
}
/>
<Route path='*' element={<NotFound />} />
</Routes>
</>
);
}

View File

@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import { getFooterHTML, getSystemName } from '../helpers';
import { Layout, Tooltip } from '@douyinfe/semi-ui';
const Footer = () => {
const FooterBar = () => {
const systemName = getSystemName();
const [footer, setFooter] = useState(getFooterHTML());
let remainCheckTimes = 5;
@@ -52,19 +52,17 @@ const Footer = () => {
}, []);
return (
<Layout>
<Layout.Content style={{ textAlign: 'center' }}>
{footer ? (
<div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: footer }}
></div>
) : (
defaultFooter
)}
</Layout.Content>
</Layout>
<div style={{ textAlign: 'center' }}>
{footer ? (
<div
className='custom-footer'
dangerouslySetInnerHTML={{ __html: footer }}
></div>
) : (
defaultFooter
)}
</div>
);
};
export default Footer;
export default FooterBar;

View File

@@ -3,14 +3,23 @@ import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { useSetTheme, useTheme } from '../context/Theme';
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
import { API, getLogo, getSystemName, isMobile, showSuccess } from '../helpers';
import '../index.css';
import fireworks from 'react-fireworks';
import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons';
import {
IconHelpCircle,
IconHome,
IconHomeStroked,
IconKey,
IconNoteMoneyStroked,
IconPriceTag,
IconUser,
} from '@douyinfe/semi-icons';
import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui';
import { stringToColor } from '../helpers/render';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
// HeaderBar Buttons
let headerButtons = [
@@ -22,6 +31,21 @@ let headerButtons = [
},
];
let buttons = [
{
text: '首页',
itemKey: 'home',
to: '/',
// icon: <IconHomeStroked />,
},
// {
// text: '模型价格',
// itemKey: 'pricing',
// to: '/pricing',
// icon: <IconNoteMoneyStroked />,
// },
];
if (localStorage.getItem('chat_link')) {
headerButtons.splice(1, 0, {
name: '聊天',
@@ -90,6 +114,7 @@ const HeaderBar = () => {
about: '/about',
login: '/login',
register: '/register',
home: '/',
};
return (
<Link
@@ -103,6 +128,23 @@ const HeaderBar = () => {
selectedKeys={[]}
// items={headerButtons}
onSelect={(key) => {}}
header={
isMobile()
? {
logo: (
<img
src={logo}
alt='logo'
style={{ marginRight: '0.75em' }}
/>
),
}
: {
logo: <img src={logo} alt='logo' />,
text: systemName,
}
}
items={buttons}
footer={
<>
{isNewYear && (
@@ -121,15 +163,19 @@ const HeaderBar = () => {
</Dropdown>
)}
<Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
<Switch
checkedText='🌞'
size={'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
<>
{!isMobile() && (
<Switch
checkedText='🌞'
size={'large'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</>
{userState.user ? (
<>
<Dropdown
@@ -155,7 +201,7 @@ const HeaderBar = () => {
<Nav.Item
itemKey={'login'}
text={'登录'}
icon={<IconKey />}
// icon={<IconKey />}
/>
<Nav.Item
itemKey={'register'}

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
import { API, copy, showError, showSuccess } from '../helpers';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import {
Banner,
@@ -83,6 +83,7 @@ const ModelPricing = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [modalImageUrl, setModalImageUrl] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const [selectedGroup, setSelectedGroup] = useState('default');
const rowSelection = useMemo(
() => ({
@@ -116,7 +117,8 @@ const ModelPricing = () => {
title: '可用性',
dataIndex: 'available',
render: (text, record, index) => {
return renderAvailable(text);
// if record.enable_groups contains selectedGroup, then available is true
return renderAvailable(record.enable_groups.includes(selectedGroup));
},
sorter: (a, b) => a.available - b.available,
},
@@ -162,6 +164,44 @@ const ModelPricing = () => {
},
sorter: (a, b) => a.quota_type - b.quota_type,
},
{
title: '可用分组',
dataIndex: 'enable_groups',
render: (text, record, index) => {
// enable_groups is a string array
return (
<Space>
{text.map((group) => {
if (group === selectedGroup) {
return (
<Tag color='blue' size='large' prefixIcon={<IconVerify />}>
{group}
</Tag>
);
} else {
return (
<Tag
color='blue'
size='large'
onClick={() => {
setSelectedGroup(group);
showInfo(
'当前查看的分组为:' +
group +
',倍率为:' +
groupRatio[group],
);
}}
>
{group}
</Tag>
);
}
})}
</Space>
);
},
},
{
title: () => (
<span style={{ display: 'flex', alignItems: 'center' }}>
@@ -203,6 +243,8 @@ const ModelPricing = () => {
<Text>
补全{record.quota_type === 0 ? completionRatio : '无'}
</Text>
<br />
<Text>分组{groupRatio[selectedGroup]}</Text>
</>
);
return <div>{content}</div>;
@@ -215,12 +257,13 @@ const ModelPricing = () => {
let content = text;
if (record.quota_type === 0) {
// 这里的 *2 是因为 1倍率=0.002刀,请勿删除
let inputRatioPrice = record.model_ratio * 2 * record.group_ratio;
let inputRatioPrice =
record.model_ratio * 2 * groupRatio[selectedGroup];
let completionRatioPrice =
record.model_ratio *
record.completion_ratio *
2 *
record.group_ratio;
groupRatio[selectedGroup];
content = (
<>
<Text>提示 ${inputRatioPrice} / 1M tokens</Text>
@@ -229,7 +272,7 @@ const ModelPricing = () => {
</>
);
} else {
let price = parseFloat(text) * record.group_ratio;
let price = parseFloat(text) * groupRatio[selectedGroup];
content = <>模型价格${price}</>;
}
return <div>{content}</div>;
@@ -240,12 +283,12 @@ const ModelPricing = () => {
const [models, setModels] = useState([]);
const [loading, setLoading] = useState(true);
const [userState, userDispatch] = useContext(UserContext);
const [groupRatio, setGroupRatio] = useState(1);
const [groupRatio, setGroupRatio] = useState({});
const setModelsFormat = (models, groupRatio) => {
for (let i = 0; i < models.length; i++) {
models[i].key = models[i].model_name;
models[i].group_ratio = groupRatio;
models[i].group_ratio = groupRatio[models[i].model_name];
}
// sort by quota_type
models.sort((a, b) => {
@@ -278,6 +321,7 @@ const ModelPricing = () => {
const { success, message, data, group_ratio } = res.data;
if (success) {
setGroupRatio(group_ratio);
setSelectedGroup(userState.user ? userState.user.group : 'default');
setModelsFormat(data, group_ratio);
} else {
showError(message);
@@ -310,14 +354,14 @@ const ModelPricing = () => {
type='success'
fullMode={false}
closeIcon='null'
description={`您的分组为:${userState.user.group},分组倍率为:${groupRatio}`}
description={`您的默认分组为:${userState.user.group},分组倍率为:${groupRatio[userState.user.group]}`}
/>
) : (
<Banner
type='warning'
fullMode={false}
closeIcon='null'
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio}`}
description={`您还未登陆,显示的价格为默认分组倍率: ${groupRatio['default']}`}
/>
)}
<br />

View File

@@ -23,6 +23,7 @@ const OperationSetting = () => {
CompletionRatio: '',
ModelPrice: '',
GroupRatio: '',
UserUsableGroups: '',
TopUpLink: '',
ChatLink: '',
ChatLink2: '', // 添加的新状态变量
@@ -62,6 +63,7 @@ const OperationSetting = () => {
if (
item.key === 'ModelRatio' ||
item.key === 'GroupRatio' ||
item.key === 'UserUsableGroups' ||
item.key === 'CompletionRatio' ||
item.key === 'ModelPrice'
) {

View File

@@ -19,6 +19,7 @@ import {
IconComment,
IconCreditCard,
IconGift,
IconHelpCircle,
IconHistogram,
IconHome,
IconImage,
@@ -28,8 +29,10 @@ import {
IconSetting,
IconUser,
} from '@douyinfe/semi-icons';
import { Layout, Nav } from '@douyinfe/semi-ui';
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';
// HeaderBar Buttons
@@ -44,6 +47,8 @@ const SiderBar = () => {
const systemName = getSystemName();
const logo = getLogo();
const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed);
const theme = useTheme();
const setTheme = useSetTheme();
const routerMap = {
home: '/',
@@ -64,11 +69,17 @@ const SiderBar = () => {
const headerButtons = useMemo(
() => [
// {
// text: '首页',
// itemKey: 'home',
// to: '/',
// icon: <IconHome />,
// },
{
text: '首页',
itemKey: 'home',
to: '/',
icon: <IconHome />,
text: '模型价格',
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{
text: '渠道',
@@ -105,12 +116,6 @@ const SiderBar = () => {
to: '/topup',
icon: <IconCreditCard />,
},
{
text: '模型价格',
itemKey: 'pricing',
to: '/pricing',
icon: <IconPriceTag />,
},
{
text: '用户管理',
itemKey: 'user',
@@ -206,48 +211,58 @@ const SiderBar = () => {
return (
<>
<Layout>
<div style={{ height: '100%' }}>
<Nav
// bodyStyle={{ maxWidth: 200 }}
style={{ maxWidth: 200 }}
defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed}
onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed);
}}
selectedKeys={selectedKeys}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
);
}}
items={headerButtons}
onSelect={(key) => {
setSelectedKeys([key.itemKey]);
}}
header={{
logo: (
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
),
text: systemName,
}}
// footer={{
// text: '© 2021 NekoAPI',
// }}
>
<Nav.Footer collapseButton={true}></Nav.Footer>
</Nav>
</div>
</Layout>
<Nav
style={{ maxWidth: 220, height: '100%' }}
defaultIsCollapsed={
isMobile() ||
localStorage.getItem('default_collapse_sidebar') === 'true'
}
isCollapsed={isCollapsed}
onCollapseChange={(collapsed) => {
setIsCollapsed(collapsed);
}}
selectedKeys={selectedKeys}
renderWrapper={({ itemElement, isSubNav, isInSubNav, props }) => {
return (
<Link
style={{ textDecoration: 'none' }}
to={routerMap[props.itemKey]}
>
{itemElement}
</Link>
);
}}
items={headerButtons}
onSelect={(key) => {
setSelectedKeys([key.itemKey]);
}}
// header={{
// logo: (
// <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
// ),
// text: systemName,
// }}
// footer={{
// text: '© 2021 NekoAPI',
// }}
footer={
<>
{isMobile() && (
<Switch
checkedText='🌞'
size={'small'}
checked={theme === 'dark'}
uncheckedText='🌙'
onChange={(checked) => {
setTheme(checked);
}}
/>
)}
</>
}
>
<Nav.Footer collapseButton={true}></Nav.Footer>
</Nav>
</>
);
};

View File

@@ -8,7 +8,7 @@ import {
} from '../helpers';
import { ITEMS_PER_PAGE } from '../constants';
import { renderQuota } from '../helpers/render';
import { renderGroup, renderQuota } from '../helpers/render';
import {
Button,
Dropdown,
@@ -16,6 +16,7 @@ import {
Modal,
Popconfirm,
Popover,
Space,
SplitButtonGroup,
Table,
Tag,
@@ -119,7 +120,14 @@ const TokensTable = () => {
dataIndex: 'status',
key: 'status',
render: (text, record, index) => {
return <div>{renderStatus(text, record.model_limits_enabled)}</div>;
return (
<div>
<Space>
{renderStatus(text, record.model_limits_enabled)}
{renderGroup(record.group)}
</Space>
</div>
);
},
},
{

View File

@@ -0,0 +1,70 @@
// src/hooks/useTokenKeys.js
import { useEffect, useState } from 'react';
import { API, showError } from '../helpers';
async function fetchTokenKeys() {
try {
const response = await API.get('/api/token/?p=0&size=999');
const { success, data } = response.data;
if (success) {
const activeTokens = data.filter((token) => token.status === 1);
return activeTokens.map((token) => token.key);
} else {
throw new Error('Failed to fetch token keys');
}
} catch (error) {
console.error('Error fetching token keys:', error);
return [];
}
}
function getServerAddress() {
let status = localStorage.getItem('status');
let serverAddress = '';
if (status) {
try {
status = JSON.parse(status);
serverAddress = status.server_address || '';
} catch (error) {
console.error('Failed to parse status from localStorage:', error);
}
}
if (!serverAddress) {
serverAddress = window.location.origin;
}
return serverAddress;
}
export function useTokenKeys() {
const [keys, setKeys] = useState([]);
const [chatLink, setChatLink] = useState('');
const [serverAddress, setServerAddress] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const loadAllData = async () => {
const fetchedKeys = await fetchTokenKeys();
if (fetchedKeys.length === 0) {
showError('当前没有可用的启用令牌,请确认是否有令牌处于启用状态!');
setTimeout(() => {
window.location.href = '/token';
}, 1500); // 延迟 1.5 秒后跳转
}
setKeys(fetchedKeys);
setIsLoading(false);
const link = localStorage.getItem('chat_link');
setChatLink(link);
const address = getServerAddress();
setServerAddress(address);
};
loadAllData();
}, []);
return { keys, chatLink, serverAddress, isLoading };
}

View File

@@ -15,8 +15,8 @@ export function renderText(text, limit) {
export function renderGroup(group) {
if (group === '') {
return (
<Tag size='large' key='default'>
unknown
<Tag size='large' key='default' color='orange'>
用户分组
</Tag>
);
}

View File

@@ -9,11 +9,12 @@ body {
scrollbar-width: none;
color: var(--semi-color-text-0) !important;
background-color: var(--semi-color-bg-0) !important;
height: 100%;
height: 100vh;
}
#root {
height: 100%;
height: 100vh;
flex-direction: column;
}
@media only screen and (max-width: 767px) {
@@ -50,9 +51,9 @@ body {
}
}
.semi-layout {
height: 100%;
}
/*.semi-layout {*/
/* height: 100%;*/
/*}*/
.tableShow {
display: revert;

View File

@@ -3,7 +3,6 @@ import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import HeaderBar from './components/HeaderBar';
import Footer from './components/Footer';
import 'semantic-ui-offline/semantic.min.css';
import './index.css';
import { UserProvider } from './context/User';
@@ -13,35 +12,40 @@ import { StatusProvider } from './context/Status';
import { Layout } from '@douyinfe/semi-ui';
import SiderBar from './components/SiderBar';
import { ThemeProvider } from './context/Theme';
import FooterBar from './components/Footer';
// initialization
const root = ReactDOM.createRoot(document.getElementById('root'));
const { Sider, Content, Header } = Layout;
const { Sider, Content, Header, Footer } = Layout;
root.render(
<React.StrictMode>
<StatusProvider>
<UserProvider>
<BrowserRouter>
<ThemeProvider>
<Layout>
<Sider>
<SiderBar />
</Sider>
<Layout>
<Header>
<HeaderBar />
</Header>
<Content
style={{
padding: '24px',
}}
>
<App />
</Content>
<Layout.Footer>
<Footer></Footer>
</Layout.Footer>
<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>

View File

@@ -1,14 +1,36 @@
import React from 'react';
import { useTokenKeys } from '../../components/fetchTokenKeys';
import { Layout } from '@douyinfe/semi-ui';
const Chat = () => {
const chatLink = localStorage.getItem('chat_link');
const ChatPage = () => {
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
return (
const comLink = (key) => {
if (!chatLink || !serverAddress || !key) return '';
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
};
const iframeSrc = keys.length > 0 ? comLink(keys[0]) : '';
return !isLoading && iframeSrc ? (
<iframe
src={chatLink}
src={iframeSrc}
style={{ width: '100%', height: '85vh', border: 'none' }}
title='Token Frame'
/>
) : (
<div>
<Layout>
<Layout.Header>
<h3 style={{ color: 'red' }}>
当前没有可用的已启用令牌请确认是否有令牌处于启用状态
<br />
正在跳转......
</h3>
</Layout.Header>
</Layout>
</div>
);
};
export default Chat;
export default ChatPage;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { useTokenKeys } from '../../components/fetchTokenKeys';
const chat2page = () => {
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
const comLink = (key) => {
if (!chatLink || !serverAddress || !key) return '';
return `${chatLink}/#/?settings={"key":"sk-${key}","url":"${encodeURIComponent(serverAddress)}"}`;
};
if (keys.length > 0) {
const redirectLink = comLink(keys[0]);
if (redirectLink) {
window.location.href = redirectLink;
}
}
return (
<div>
<h3>正在加载请稍候...</h3>
</div>
);
};
export default chat2page;

View File

@@ -25,6 +25,7 @@ export default function SettingsMagnification(props) {
ModelRatio: '',
CompletionRatio: '',
GroupRatio: '',
UserUsableGroups: '',
});
const refForm = useRef();
const [inputsRow, setInputsRow] = useState(inputs);
@@ -224,6 +225,33 @@ export default function SettingsMagnification(props) {
/>
</Col>
</Row>
<Row gutter={16}>
<Col span={16}>
<Form.TextArea
label={'用户可选分组'}
extraText={''}
placeholder={'为一个 JSON 文本,键为分组名称,值为倍率'}
field={'UserUsableGroups'}
autosize={{ minRows: 6, maxRows: 12 }}
trigger='blur'
stopValidateWithError
rules={[
{
validator: (rule, value) => {
return verifyJSON(value);
},
message: '不是合法的 JSON 字符串',
},
]}
onChange={(value) =>
setInputs({
...inputs,
UserUsableGroups: value,
})
}
/>
</Col>
</Row>
</Form.Section>
</Form>
<Space>

View File

@@ -19,6 +19,7 @@ import {
SideSheet,
Space,
Spin,
TextArea,
Typography,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
@@ -34,6 +35,8 @@ const EditToken = (props) => {
unlimited_quota: false,
model_limits_enabled: false,
model_limits: [],
allow_ips: '',
group: '',
};
const [inputs, setInputs] = useState(originInputs);
const {
@@ -43,9 +46,12 @@ const EditToken = (props) => {
unlimited_quota,
model_limits_enabled,
model_limits,
allow_ips,
group,
} = inputs;
// const [visible, setVisible] = useState(false);
const [models, setModels] = useState({});
const [groups, setGroups] = useState([]);
const navigate = useNavigate();
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
@@ -86,6 +92,22 @@ const EditToken = (props) => {
}
};
const loadGroups = async () => {
let res = await API.get(`/api/user/groups`);
const { success, message, data } = res.data;
if (success) {
// return data is a map, key is group name, value is group description
// label is group description, value is group name
let localGroupOptions = Object.keys(data).map((group) => ({
label: data[group],
value: group,
}));
setGroups(localGroupOptions);
} else {
showError(message);
}
};
const loadToken = async () => {
setLoading(true);
let res = await API.get(`/api/token/${props.editingToken.id}`);
@@ -118,6 +140,7 @@ const EditToken = (props) => {
});
}
loadModels();
loadGroups();
}, [isEdit]);
// 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1
@@ -374,6 +397,19 @@ const EditToken = (props) => {
</Button>
</div>
<Divider />
<div style={{ marginTop: 10 }}>
<Typography.Text>IP白名单请勿过度信任此功能</Typography.Text>
</div>
<TextArea
label='IP白名单'
name='allow_ips'
placeholder={'允许的IP一行一个'}
onChange={(value) => {
handleInputChange('allow_ips', value);
}}
value={inputs.allow_ips}
style={{ fontFamily: 'JetBrains Mono, Consolas' }}
/>
<div style={{ marginTop: 10, display: 'flex' }}>
<Space>
<Checkbox
@@ -404,6 +440,31 @@ const EditToken = (props) => {
optionList={models}
disabled={!model_limits_enabled}
/>
<div style={{ marginTop: 10 }}>
<Typography.Text>令牌分组默认为用户的分组</Typography.Text>
</div>
{groups.length > 0 ? (
<Select
style={{ marginTop: 8 }}
placeholder={'令牌分组,默认为用户的分组'}
name='gruop'
required
selection
onChange={(value) => {
handleInputChange('group', value);
}}
value={inputs.group}
autoComplete='new-password'
optionList={groups}
/>
) : (
<Select
style={{ marginTop: 8 }}
placeholder={'管理员未设置用户可选分组'}
name='gruop'
disabled={true}
/>
)}
</Spin>
</SideSheet>
</>