♻️ refactor: Refactor chat links

This commit is contained in:
Martial BE 2024-04-27 20:50:36 +08:00
parent c6a0c87ad1
commit 801b98d6fc
No known key found for this signature in database
GPG Key ID: D06C32DF0EDB9084
18 changed files with 574 additions and 41 deletions

View File

@ -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

View File

@ -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,
},
})
}

View File

@ -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")

View File

@ -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,
}

View File

@ -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

View File

@ -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)

View 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
}
];

View File

@ -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>

View File

@ -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: '日志',

View File

@ -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 />
}
]
};

View File

@ -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 />
}
]
};

View File

@ -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);
}

View 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;

View 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;

View File

@ -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>
);
};

View File

@ -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);
}

View File

@ -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);

View File

@ -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>