♻️ refactor: Refactor chat links
This commit is contained in:
parent
c6a0c87ad1
commit
801b98d6fc
@ -15,6 +15,7 @@ var Footer = ""
|
||||
var Logo = ""
|
||||
var TopUpLink = ""
|
||||
var ChatLink = ""
|
||||
var ChatLinks = ""
|
||||
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
||||
var DisplayInCurrencyEnabled = true
|
||||
var DisplayTokenStatEnabled = true
|
||||
|
@ -44,6 +44,7 @@ func GetStatus(c *gin.Context) {
|
||||
"telegram_bot": telegram_bot,
|
||||
"mj_notify_enabled": common.MjNotifyEnabled,
|
||||
"chat_cache_enabled": common.ChatCacheEnabled,
|
||||
"chat_links": common.ChatLinks,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -54,6 +54,39 @@ func GetToken(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func GetPlaygroundToken(c *gin.Context) {
|
||||
tokenName := "sys_playground"
|
||||
token, err := model.GetTokenByName(tokenName)
|
||||
if err != nil {
|
||||
cleanToken := model.Token{
|
||||
UserId: c.GetInt("id"),
|
||||
Name: tokenName,
|
||||
Key: common.GenerateKey(),
|
||||
CreatedTime: common.GetTimestamp(),
|
||||
AccessedTime: common.GetTimestamp(),
|
||||
ExpiredTime: 0,
|
||||
RemainQuota: 0,
|
||||
UnlimitedQuota: true,
|
||||
ChatCache: false,
|
||||
}
|
||||
err = cleanToken.Insert()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "创建令牌失败,请去系统手动配置一个名称为:sys_playground 的令牌",
|
||||
})
|
||||
return
|
||||
}
|
||||
token = &cleanToken
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": token.Key,
|
||||
})
|
||||
}
|
||||
|
||||
func GetTokenStatus(c *gin.Context) {
|
||||
tokenId := c.GetInt("token_id")
|
||||
userId := c.GetInt("id")
|
||||
|
@ -70,6 +70,7 @@ func InitOptionMap() {
|
||||
common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
|
||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||
common.OptionMap["ChatLink"] = common.ChatLink
|
||||
common.OptionMap["ChatLinks"] = common.ChatLinks
|
||||
common.OptionMap["QuotaPerUnit"] = strconv.FormatFloat(common.QuotaPerUnit, 'f', -1, 64)
|
||||
common.OptionMap["RetryTimes"] = strconv.Itoa(common.RetryTimes)
|
||||
common.OptionMap["RetryCooldownSeconds"] = strconv.Itoa(common.RetryCooldownSeconds)
|
||||
@ -166,6 +167,7 @@ var optionStringMap = map[string]*string{
|
||||
"TurnstileSecretKey": &common.TurnstileSecretKey,
|
||||
"TopUpLink": &common.TopUpLink,
|
||||
"ChatLink": &common.ChatLink,
|
||||
"ChatLinks": &common.ChatLinks,
|
||||
"LarkClientId": &common.LarkClientId,
|
||||
"LarkClientSecret": &common.LarkClientSecret,
|
||||
}
|
||||
|
@ -115,6 +115,16 @@ func GetTokenById(id int) (*Token, error) {
|
||||
return &token, err
|
||||
}
|
||||
|
||||
func GetTokenByName(name string) (*Token, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("name 为空!")
|
||||
}
|
||||
token := Token{Name: name}
|
||||
var err error = nil
|
||||
err = DB.First(&token, "name = ?", name).Error
|
||||
return &token, err
|
||||
}
|
||||
|
||||
func (token *Token) Insert() error {
|
||||
if token.ChatCache && !common.ChatCacheEnabled {
|
||||
token.ChatCache = false
|
||||
|
@ -93,6 +93,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
tokenRoute := apiRouter.Group("/token")
|
||||
tokenRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
tokenRoute.GET("/playground", controller.GetPlaygroundToken)
|
||||
tokenRoute.GET("/", controller.GetUserTokensList)
|
||||
tokenRoute.GET("/:id", controller.GetToken)
|
||||
tokenRoute.POST("/", controller.AddToken)
|
||||
|
22
web/src/constants/chatLinks.js
Normal file
22
web/src/constants/chatLinks.js
Normal file
@ -0,0 +1,22 @@
|
||||
export const CHAT_LINKS = [
|
||||
{
|
||||
name: 'ChatGPT Next',
|
||||
url: 'https://app.nextchat.dev/#/?settings={"key":"{key}","url":"{server}"}',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
name: 'chatgpt-web-midjourney-proxy',
|
||||
url: 'https://vercel.ddaiai.com/#/?settings={"key":"{key}","url":"{server}"}',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
name: 'AMA 问天',
|
||||
url: 'ama://set-api-key?server={server}&key={key}',
|
||||
show: false
|
||||
},
|
||||
{
|
||||
name: 'OpenCat',
|
||||
url: 'opencat://team/join?domain={server}&token={key}',
|
||||
show: false
|
||||
}
|
||||
];
|
@ -74,6 +74,11 @@ const Header = () => {
|
||||
<Button component={Link} variant="text" to="/" color={pathname === '/' ? 'primary' : 'inherit'}>
|
||||
首页
|
||||
</Button>
|
||||
{account.user && (
|
||||
<Button component={Link} variant="text" to="/playground" color={pathname === '/playground' ? 'primary' : 'inherit'}>
|
||||
Playground
|
||||
</Button>
|
||||
)}
|
||||
<Button component={Link} variant="text" to="/about" color={pathname === '/about' ? 'primary' : 'inherit'}>
|
||||
关于
|
||||
</Button>
|
||||
@ -134,6 +139,12 @@ const Header = () => {
|
||||
<ListItemText primary={<Typography variant="body2">首页</Typography>} />
|
||||
</ListItemButton>
|
||||
|
||||
{account.user && (
|
||||
<ListItemButton component={Link} variant="text" to="/playground">
|
||||
<ListItemText primary={<Typography variant="body2">Playground</Typography>} />
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
<ListItemButton component={Link} variant="text" to="/about">
|
||||
<ListItemText primary={<Typography variant="body2">关于</Typography>} />
|
||||
</ListItemButton>
|
||||
|
@ -13,7 +13,8 @@ import {
|
||||
IconBrandTelegram,
|
||||
IconReceipt2,
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot
|
||||
IconBrandGithubCopilot,
|
||||
IconBallFootball
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
// constant
|
||||
@ -31,7 +32,8 @@ const icons = {
|
||||
IconBrandTelegram,
|
||||
IconReceipt2,
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot
|
||||
IconBrandGithubCopilot,
|
||||
IconBallFootball
|
||||
};
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
@ -75,6 +77,14 @@ const panel = {
|
||||
icon: icons.IconKey,
|
||||
breadcrumbs: false
|
||||
},
|
||||
{
|
||||
id: 'playground',
|
||||
title: 'Playground',
|
||||
type: 'item',
|
||||
url: '/panel/playground',
|
||||
icon: icons.IconBallFootball,
|
||||
breadcrumbs: false
|
||||
},
|
||||
{
|
||||
id: 'log',
|
||||
title: '日志',
|
||||
|
@ -18,6 +18,7 @@ const Telegram = Loadable(lazy(() => import('views/Telegram')));
|
||||
const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
||||
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
||||
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
||||
const Playground = Loadable(lazy(() => import('views/Playground')));
|
||||
|
||||
// dashboard routing
|
||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||
@ -91,6 +92,10 @@ const MainRoutes = {
|
||||
{
|
||||
path: 'model_price',
|
||||
element: <ModelPrice />
|
||||
},
|
||||
{
|
||||
path: 'playground',
|
||||
element: <Playground />
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ const Home = Loadable(lazy(() => import('views/Home')));
|
||||
const About = Loadable(lazy(() => import('views/About')));
|
||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
||||
const Jump = Loadable(lazy(() => import('views/Jump')));
|
||||
const Playground = Loadable(lazy(() => import('views/Playground')));
|
||||
|
||||
// ==============================|| AUTHENTICATION ROUTING ||============================== //
|
||||
|
||||
@ -61,6 +62,10 @@ const OtherRoutes = {
|
||||
{
|
||||
path: '/jump',
|
||||
element: <Jump />
|
||||
},
|
||||
{
|
||||
path: '/playground',
|
||||
element: <Playground />
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { snackbarConstants } from 'constants/SnackbarConstants';
|
||||
import { API } from './api';
|
||||
import { CHAT_LINKS } from 'constants/chatLinks';
|
||||
|
||||
export function getSystemName() {
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
@ -228,3 +229,35 @@ export function trims(values) {
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function getChatLinks(filterShow = false) {
|
||||
let links = [];
|
||||
let siteInfo = JSON.parse(localStorage.getItem('siteInfo'));
|
||||
let chatLinks = JSON.parse(siteInfo?.chat_links || '[]');
|
||||
|
||||
if (chatLinks.length === 0) {
|
||||
links = CHAT_LINKS;
|
||||
if (siteInfo?.chat_link) {
|
||||
// 循环找到name为ChatGPT Next的链接
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
if (links[i].name === 'ChatGPT Next') {
|
||||
links[i].url = siteInfo.chat_link + `/#/?settings={"key":"sk-{key}","url":"{server}"}`;
|
||||
links[i].show = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
links = chatLinks;
|
||||
}
|
||||
|
||||
if (filterShow) {
|
||||
links = links.filter((link) => link.show);
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
export function replaceChatPlaceholders(text, key, server) {
|
||||
return text.replace('{key}', key).replace('{server}', server);
|
||||
}
|
||||
|
109
web/src/views/Playground/index.js
Normal file
109
web/src/views/Playground/index.js
Normal file
@ -0,0 +1,109 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { API } from 'utils/api';
|
||||
import { getChatLinks, showError, replaceChatPlaceholders } from 'utils/common';
|
||||
import { Typography, Tabs, Tab, Box, Card } from '@mui/material';
|
||||
import SubCard from 'ui-component/cards/SubCard';
|
||||
// import { Link } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
function TabPanel(props) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`playground-tabpanel-${index}`}
|
||||
aria-labelledby={`playground-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography>{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
id: `playground-tab-${index}`,
|
||||
'aria-controls': `playground-tabpanel-${index}`
|
||||
};
|
||||
}
|
||||
|
||||
const Playground = () => {
|
||||
const [value, setValue] = useState('');
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
const chatLinks = getChatLinks(true);
|
||||
const [iframeSrc, setIframeSrc] = useState(null);
|
||||
|
||||
const loadTokens = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
const res = await API.get(`/api/token/playground`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setValue(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(event, newIndex) => {
|
||||
setTabIndex(newIndex);
|
||||
let server = '';
|
||||
if (siteInfo?.server_address) {
|
||||
server = siteInfo.server_address;
|
||||
} else {
|
||||
server = window.location.host;
|
||||
}
|
||||
server = encodeURIComponent(server);
|
||||
const key = 'sk-' + value;
|
||||
|
||||
setIframeSrc(replaceChatPlaceholders(chatLinks[newIndex].url, key, server));
|
||||
},
|
||||
[siteInfo, value, chatLinks]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadTokens().then(() => {
|
||||
if (value !== '') {
|
||||
handleTabChange(null, 0);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loadTokens, value]);
|
||||
|
||||
if (chatLinks.length === 0 || isLoading || value === '') {
|
||||
return (
|
||||
<SubCard title="Playground">
|
||||
<Typography align="center">{isLoading ? 'Loading...' : 'No playground available'}</Typography>
|
||||
</SubCard>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Card>
|
||||
<Tabs variant="scrollable" value={tabIndex} onChange={handleTabChange} sx={{ borderRight: 1, borderColor: 'divider' }}>
|
||||
{chatLinks.map((link, index) => link.show && <Tab label={link.name} {...a11yProps(index)} key={index} />)}
|
||||
</Tabs>
|
||||
<Box>
|
||||
<iframe title="playground" src={iframeSrc} style={{ width: '100%', height: '85vh', border: 'none' }} />
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Playground;
|
255
web/src/views/Setting/component/ChatLinksDataGrid.js
Normal file
255
web/src/views/Setting/component/ChatLinksDataGrid.js
Normal file
@ -0,0 +1,255 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { GridRowModes, DataGrid, GridToolbarContainer, GridActionsCellItem } from '@mui/x-data-grid';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import SaveIcon from '@mui/icons-material/Save';
|
||||
import CancelIcon from '@mui/icons-material/Close';
|
||||
import { showError } from 'utils/common';
|
||||
|
||||
function validation(row) {
|
||||
if (row.name === '') {
|
||||
return '名称不能为空';
|
||||
}
|
||||
|
||||
if (row.url === '') {
|
||||
return 'URL不能为空';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function randomId() {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
function EditToolbar({ setRows, setRowModesModel }) {
|
||||
const handleClick = () => {
|
||||
const id = randomId();
|
||||
setRows((oldRows) => [{ id, name: '', url: '', show: true, isNew: true }, ...oldRows]);
|
||||
setRowModesModel((oldModel) => ({
|
||||
[id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' },
|
||||
...oldModel
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<GridToolbarContainer>
|
||||
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick}>
|
||||
新增
|
||||
</Button>
|
||||
</GridToolbarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
EditToolbar.propTypes = {
|
||||
setRows: PropTypes.func.isRequired,
|
||||
setRowModesModel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const ChatLinksDataGrid = ({ links, onChange }) => {
|
||||
const [rows, setRows] = useState([]);
|
||||
const [rowModesModel, setRowModesModel] = useState({});
|
||||
|
||||
const setLinks = useCallback(
|
||||
(linksRow) => {
|
||||
let linksJson = [];
|
||||
// 删除 linksrow 中的 isNew 属性
|
||||
linksRow.forEach((row) => {
|
||||
let { isNew, ...rest } = row; // eslint-disable-line no-unused-vars
|
||||
linksJson.push(rest);
|
||||
});
|
||||
onChange({ target: { name: 'ChatLinks', value: JSON.stringify(linksJson, null, 2) } });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleEditClick = useCallback(
|
||||
(id) => () => {
|
||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
|
||||
},
|
||||
[rowModesModel]
|
||||
);
|
||||
|
||||
const handleSaveClick = useCallback(
|
||||
(id) => () => {
|
||||
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
|
||||
},
|
||||
[rowModesModel]
|
||||
);
|
||||
|
||||
const handleDeleteClick = useCallback(
|
||||
(id) => () => {
|
||||
setLinks(rows.filter((row) => row.id !== id));
|
||||
},
|
||||
[rows, setLinks]
|
||||
);
|
||||
|
||||
const handleCancelClick = useCallback(
|
||||
(id) => () => {
|
||||
setRowModesModel({
|
||||
...rowModesModel,
|
||||
[id]: { mode: GridRowModes.View, ignoreModifications: true }
|
||||
});
|
||||
|
||||
const editedRow = rows.find((row) => row.id === id);
|
||||
if (editedRow.isNew) {
|
||||
setRows(rows.filter((row) => row.id !== id));
|
||||
}
|
||||
},
|
||||
[rowModesModel, rows]
|
||||
);
|
||||
|
||||
const processRowUpdate = (newRow, oldRows) => {
|
||||
if (!newRow.isNew && newRow.name === oldRows.name && newRow.url === oldRows.url && newRow.show === oldRows.show) {
|
||||
return oldRows;
|
||||
}
|
||||
const updatedRow = { ...newRow, isNew: false };
|
||||
const error = validation(updatedRow);
|
||||
if (error) {
|
||||
return Promise.reject(new Error(error));
|
||||
}
|
||||
setLinks(rows.map((row) => (row.id === newRow.id ? updatedRow : row)));
|
||||
return updatedRow;
|
||||
};
|
||||
|
||||
const handleProcessRowUpdateError = useCallback((error) => {
|
||||
showError(error.message);
|
||||
}, []);
|
||||
|
||||
const handleRowModesModelChange = (newRowModesModel) => {
|
||||
setRowModesModel(newRowModesModel);
|
||||
};
|
||||
|
||||
const modelRatioColumns = useMemo(
|
||||
() => [
|
||||
{
|
||||
field: 'name',
|
||||
sortable: true,
|
||||
headerName: '名称',
|
||||
flex: 1,
|
||||
minWidth: 220,
|
||||
editable: true,
|
||||
hideable: false
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
sortable: false,
|
||||
headerName: '链接',
|
||||
flex: 1,
|
||||
minWidth: 300,
|
||||
editable: true,
|
||||
hideable: false
|
||||
},
|
||||
{
|
||||
field: 'show',
|
||||
sortable: false,
|
||||
headerName: '是否显示在playground',
|
||||
flex: 1,
|
||||
minWidth: 200,
|
||||
type: 'boolean',
|
||||
editable: true,
|
||||
hideable: false
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
headerName: '操作',
|
||||
width: 100,
|
||||
cellClassName: 'actions',
|
||||
hideable: false,
|
||||
getActions: ({ id }) => {
|
||||
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
|
||||
|
||||
if (isInEditMode) {
|
||||
return [
|
||||
<GridActionsCellItem
|
||||
icon={<SaveIcon />}
|
||||
key={'Save-' + id}
|
||||
label="Save"
|
||||
sx={{
|
||||
color: 'primary.main'
|
||||
}}
|
||||
onClick={handleSaveClick(id)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
icon={<CancelIcon />}
|
||||
key={'Cancel-' + id}
|
||||
label="Cancel"
|
||||
className="textPrimary"
|
||||
onClick={handleCancelClick(id)}
|
||||
color="inherit"
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<GridActionsCellItem
|
||||
key={'Edit-' + id}
|
||||
icon={<EditIcon />}
|
||||
label="Edit"
|
||||
className="textPrimary"
|
||||
onClick={handleEditClick(id)}
|
||||
color="inherit"
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
key={'Delete-' + id}
|
||||
icon={<DeleteIcon />}
|
||||
label="Delete"
|
||||
onClick={handleDeleteClick(id)}
|
||||
color="inherit"
|
||||
/>
|
||||
];
|
||||
}
|
||||
}
|
||||
],
|
||||
[handleEditClick, handleSaveClick, handleDeleteClick, handleCancelClick, rowModesModel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let itemJson = JSON.parse(links);
|
||||
setRows(itemJson);
|
||||
}, [links]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
'& .actions': {
|
||||
color: 'text.secondary'
|
||||
},
|
||||
'& .textPrimary': {
|
||||
color: 'text.primary'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DataGrid
|
||||
autoHeight
|
||||
rows={rows}
|
||||
columns={modelRatioColumns}
|
||||
editMode="row"
|
||||
hideFooter
|
||||
disableRowSelectionOnClick
|
||||
rowModesModel={rowModesModel}
|
||||
onRowModesModelChange={handleRowModesModelChange}
|
||||
processRowUpdate={processRowUpdate}
|
||||
onProcessRowUpdateError={handleProcessRowUpdateError}
|
||||
slots={{
|
||||
toolbar: EditToolbar
|
||||
}}
|
||||
slotProps={{
|
||||
toolbar: { setRows, setRowModesModel }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
ChatLinksDataGrid.propTypes = {
|
||||
links: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ChatLinksDataGrid;
|
@ -1,12 +1,14 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import SubCard from 'ui-component/cards/SubCard';
|
||||
import { Stack, FormControl, InputLabel, OutlinedInput, Checkbox, Button, FormControlLabel, TextField } from '@mui/material';
|
||||
import { Stack, FormControl, InputLabel, OutlinedInput, Checkbox, Button, FormControlLabel, TextField, Alert } from '@mui/material';
|
||||
import { showSuccess, showError, verifyJSON } from 'utils/common';
|
||||
import { API } from 'utils/api';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||
import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker';
|
||||
import ChatLinksDataGrid from './ChatLinksDataGrid';
|
||||
import dayjs from 'dayjs';
|
||||
import { LoadStatusContext } from 'contexts/StatusContext';
|
||||
require('dayjs/locale/zh-cn');
|
||||
|
||||
const OperationSetting = () => {
|
||||
@ -20,6 +22,7 @@ const OperationSetting = () => {
|
||||
GroupRatio: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
ChatLinks: '',
|
||||
QuotaPerUnit: 0,
|
||||
AutomaticDisableChannelEnabled: '',
|
||||
AutomaticEnableChannelEnabled: '',
|
||||
@ -37,6 +40,7 @@ const OperationSetting = () => {
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
let [historyTimestamp, setHistoryTimestamp] = useState(now.getTime() / 1000 - 30 * 24 * 3600); // a month ago new Date().getTime() / 1000 + 3600
|
||||
const loadStatus = useContext(LoadStatusContext);
|
||||
|
||||
const getOptions = async () => {
|
||||
try {
|
||||
@ -78,6 +82,8 @@ const OperationSetting = () => {
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
||||
getOptions();
|
||||
await loadStatus();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
@ -118,6 +124,15 @@ const OperationSetting = () => {
|
||||
await updateOption('GroupRatio', inputs.GroupRatio);
|
||||
}
|
||||
break;
|
||||
case 'chatlinks':
|
||||
if (originInputs['ChatLinks'] !== inputs.ChatLinks) {
|
||||
if (!verifyJSON(inputs.ChatLinks)) {
|
||||
showError('links不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('ChatLinks', inputs.ChatLinks);
|
||||
}
|
||||
break;
|
||||
case 'quota':
|
||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||
@ -520,6 +535,39 @@ const OperationSetting = () => {
|
||||
</Button>
|
||||
</Stack>
|
||||
</SubCard>
|
||||
|
||||
<SubCard title="聊天链接设置">
|
||||
<Stack spacing={2}>
|
||||
<Alert severity="info">
|
||||
配置聊天链接,该配置在令牌中的聊天生效以及首页的Playground中的聊天生效. <br />
|
||||
链接中可以使{'{key}'}替换用户的令牌,{'{server}'}替换服务器地址。例如:
|
||||
{'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{server}"}'}
|
||||
<br />
|
||||
如果未配置,会默认配置以下4个链接:
|
||||
<br />
|
||||
ChatGPT Next : {'https://chat.oneapi.pro/#/?settings={"key":"{key}","url":"{server}"}'}
|
||||
<br />
|
||||
chatgpt-web-midjourney-proxy : {'https://vercel.ddaiai.com/#/?settings={"key":"{key}","url":"{server}"}'}
|
||||
<br />
|
||||
AMA 问天 : {'ama://set-api-key?server={server}&key={key}'}
|
||||
<br />
|
||||
opencat : {'opencat://team/join?domain={server}&token={key}'}
|
||||
<br />
|
||||
</Alert>
|
||||
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
||||
<ChatLinksDataGrid links={inputs.ChatLinks || '[]'} onChange={handleInputChange} />
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
submitConfig('chatlinks').then();
|
||||
}}
|
||||
>
|
||||
保存聊天链接设置
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</SubCard>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import SubCard from 'ui-component/cards/SubCard';
|
||||
import {
|
||||
Stack,
|
||||
@ -19,6 +19,7 @@ import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { showError, showSuccess } from 'utils/common'; //,
|
||||
import { API } from 'utils/api';
|
||||
import { marked } from 'marked';
|
||||
import { LoadStatusContext } from 'contexts/StatusContext';
|
||||
|
||||
const OtherSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
@ -35,6 +36,7 @@ const OtherSetting = () => {
|
||||
tag_name: '',
|
||||
content: ''
|
||||
});
|
||||
const loadStatus = useContext(LoadStatusContext);
|
||||
|
||||
const getOptions = async () => {
|
||||
try {
|
||||
@ -70,8 +72,9 @@ const OtherSetting = () => {
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
||||
showSuccess('保存成功');
|
||||
getOptions();
|
||||
await loadStatus();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useContext } from 'react';
|
||||
import SubCard from 'ui-component/cards/SubCard';
|
||||
import {
|
||||
Stack,
|
||||
@ -21,6 +21,7 @@ import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { showError, showSuccess, removeTrailingSlash } from 'utils/common'; //,
|
||||
import { API } from 'utils/api';
|
||||
import { createFilterOptions } from '@mui/material/Autocomplete';
|
||||
import { LoadStatusContext } from 'contexts/StatusContext';
|
||||
|
||||
const filter = createFilterOptions();
|
||||
const SystemSetting = () => {
|
||||
@ -56,6 +57,7 @@ const SystemSetting = () => {
|
||||
let [loading, setLoading] = useState(false);
|
||||
const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
|
||||
const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
|
||||
const loadStatus = useContext(LoadStatusContext);
|
||||
|
||||
const getOptions = async () => {
|
||||
try {
|
||||
@ -116,6 +118,8 @@ const SystemSetting = () => {
|
||||
...inputs,
|
||||
[key]: value
|
||||
}));
|
||||
getOptions();
|
||||
await loadStatus();
|
||||
showSuccess('设置成功!');
|
||||
} else {
|
||||
showError(message);
|
||||
|
@ -20,25 +20,10 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import TableSwitch from 'ui-component/Switch';
|
||||
import { renderQuota, timestamp2string, copy } from 'utils/common';
|
||||
import { renderQuota, timestamp2string, copy, getChatLinks, replaceChatPlaceholders } from 'utils/common';
|
||||
|
||||
import { IconDotsVertical, IconEdit, IconTrash, IconCaretDownFilled } from '@tabler/icons-react';
|
||||
|
||||
const COPY_OPTIONS = [
|
||||
{
|
||||
key: 'next',
|
||||
text: 'ChatGPT Next',
|
||||
url: 'https://chat.oneapi.pro/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}',
|
||||
encode: false
|
||||
},
|
||||
{ key: 'ama', text: 'AMA 问天', url: 'ama://set-api-key?server={serverAddress}&key=sk-{key}', encode: true },
|
||||
{ key: 'opencat', text: 'OpenCat', url: 'opencat://team/join?domain={serverAddress}&token=sk-{key}', encode: true }
|
||||
];
|
||||
|
||||
function replacePlaceholders(text, key, serverAddress) {
|
||||
return text.replace('{key}', key).replace('{serverAddress}', serverAddress);
|
||||
}
|
||||
|
||||
function createMenu(menuItems) {
|
||||
return (
|
||||
<>
|
||||
@ -73,6 +58,7 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [statusSwitch, setStatusSwitch] = useState(item.status);
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
const chatLinks = getChatLinks();
|
||||
|
||||
const handleDeleteOpen = () => {
|
||||
handleCloseMenu();
|
||||
@ -134,25 +120,19 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
|
||||
]);
|
||||
|
||||
const handleCopy = (option, type) => {
|
||||
let serverAddress = '';
|
||||
let server = '';
|
||||
if (siteInfo?.server_address) {
|
||||
serverAddress = siteInfo.server_address;
|
||||
server = siteInfo.server_address;
|
||||
} else {
|
||||
serverAddress = window.location.host;
|
||||
server = window.location.host;
|
||||
}
|
||||
|
||||
if (option.encode) {
|
||||
serverAddress = encodeURIComponent(serverAddress);
|
||||
}
|
||||
server = encodeURIComponent(server);
|
||||
|
||||
let url = option.url;
|
||||
|
||||
if (option.key === 'next' && siteInfo?.chat_link) {
|
||||
url = siteInfo.chat_link + `/#/?settings={"key":"sk-{key}","url":"{serverAddress}"}`;
|
||||
}
|
||||
|
||||
const key = item.key;
|
||||
const text = replacePlaceholders(url, key, serverAddress);
|
||||
const key = 'sk-' + item.key;
|
||||
const text = replaceChatPlaceholders(url, key, server);
|
||||
if (type === 'link') {
|
||||
window.open(text);
|
||||
} else {
|
||||
@ -162,8 +142,8 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
|
||||
};
|
||||
|
||||
const copyItems = createMenu(
|
||||
COPY_OPTIONS.map((option) => ({
|
||||
text: option.text,
|
||||
chatLinks.map((option) => ({
|
||||
text: option.name,
|
||||
icon: undefined,
|
||||
onClick: () => handleCopy(option, 'copy'),
|
||||
color: undefined
|
||||
@ -171,8 +151,8 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
|
||||
);
|
||||
|
||||
const linkItems = createMenu(
|
||||
COPY_OPTIONS.map((option) => ({
|
||||
text: option.text,
|
||||
chatLinks.map((option) => ({
|
||||
text: option.name,
|
||||
icon: undefined,
|
||||
onClick: () => handleCopy(option, 'link'),
|
||||
color: undefined
|
||||
@ -227,9 +207,9 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
|
||||
<IconCaretDownFilled size={'16px'} />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup size="small" aria-label="split button">
|
||||
<ButtonGroup size="small" onClick={(e) => handleOpenMenu(e, 'link')} aria-label="split button">
|
||||
<Button color="primary">聊天</Button>
|
||||
<Button size="small" onClick={(e) => handleOpenMenu(e, 'link')}>
|
||||
<Button size="small">
|
||||
<IconCaretDownFilled size={'16px'} />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
Loading…
Reference in New Issue
Block a user