Merge branch 'ui/refactor' into alpha
This commit is contained in:
|
Before Width: | Height: | Size: 550 KiB After Width: | Height: | Size: 550 KiB |
@@ -1,15 +1,15 @@
|
||||
import React, { lazy, Suspense, useContext, useEffect } from 'react';
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import Loading from './components/Loading';
|
||||
import Loading from './components/common/Loading.js';
|
||||
import User from './pages/User';
|
||||
import { PrivateRoute } from './components/PrivateRoute';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import LoginForm from './components/LoginForm';
|
||||
import { AuthRedirect, PrivateRoute } from './helpers';
|
||||
import RegisterForm from './components/auth/RegisterForm.js';
|
||||
import LoginForm from './components/auth/LoginForm.js';
|
||||
import NotFound from './pages/NotFound';
|
||||
import Setting from './pages/Setting';
|
||||
import EditUser from './pages/User/EditUser';
|
||||
import PasswordResetForm from './components/PasswordResetForm';
|
||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||
import PasswordResetForm from './components/auth/PasswordResetForm.js';
|
||||
import PasswordResetConfirm from './components/auth/PasswordResetConfirm.js';
|
||||
import Channel from './pages/Channel';
|
||||
import Token from './pages/Token';
|
||||
import EditChannel from './pages/Channel/EditChannel';
|
||||
@@ -18,16 +18,14 @@ 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 Playground from './pages/Playground/index.js';
|
||||
import OAuth2Callback from './components/OAuth2Callback.js';
|
||||
import PersonalSetting from './components/PersonalSetting.js';
|
||||
import OAuth2Callback from './components/auth/OAuth2Callback.js';
|
||||
import PersonalSetting from './components/settings/PersonalSetting.js';
|
||||
import Setup from './pages/Setup/index.js';
|
||||
import SetupCheck from './components/SetupCheck';
|
||||
import AuthRedirect from './components/AuthRedirect';
|
||||
import { useSetupCheck } from './hooks/useSetupCheck.js';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const Detail = lazy(() => import('./pages/Detail'));
|
||||
@@ -37,7 +35,7 @@ function App() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<SetupCheck>
|
||||
<useSetupCheck>
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
@@ -292,7 +290,7 @@ function App() {
|
||||
/>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
</SetupCheck>
|
||||
</useSetupCheck>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
const AuthRedirect = ({ children }) => {
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/console" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AuthRedirect;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { history } from '../helpers';
|
||||
|
||||
function PrivateRoute({ children }) {
|
||||
if (!localStorage.getItem('user')) {
|
||||
return <Navigate to='/login' state={{ from: history.location }} />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PrivateRoute };
|
||||
@@ -1,18 +0,0 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { StatusContext } from '../context/Status';
|
||||
|
||||
const SetupCheck = ({ children }) => {
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (statusState?.status?.setup === false && location.pathname !== '/setup') {
|
||||
window.location.href = '/setup';
|
||||
}
|
||||
}, [statusState?.status?.setup, location.pathname]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default SetupCheck;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import {
|
||||
API,
|
||||
getLogo,
|
||||
@@ -9,12 +9,11 @@ import {
|
||||
showSuccess,
|
||||
updateAPI,
|
||||
getSystemName,
|
||||
} from '../helpers';
|
||||
import {
|
||||
setUserData,
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
} from './utils';
|
||||
onLinuxDOOAuthClicked
|
||||
} from '../../helpers/index.js';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import {
|
||||
Button,
|
||||
@@ -29,12 +28,11 @@ import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
|
||||
import { IconGithubLogo, IconMail, IconLock } from '@douyinfe/semi-icons';
|
||||
import OIDCIcon from './common/logo/OIDCIcon.js';
|
||||
import WeChatIcon from './common/logo/WeChatIcon.js';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import LinuxDoIcon from './common/logo/LinuxDoIcon.js';
|
||||
import OIDCIcon from '../common/logo/OIDCIcon.js';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [inputs, setInputs] = useState({
|
||||
@@ -505,7 +503,7 @@ const LoginForm = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div className="relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Spin, Typography, Space } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { API, showError, showSuccess, updateAPI } from '../helpers';
|
||||
import { UserContext } from '../context/User';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { API, showError, showSuccess, updateAPI, setUserData } from '../../helpers';
|
||||
import { UserContext } from '../../context/User';
|
||||
|
||||
const OAuth2Callback = (props) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../helpers';
|
||||
import { API, copy, showError, showNotice, getLogo, getSystemName } from '../../helpers';
|
||||
import { useSearchParams, Link } from 'react-router-dom';
|
||||
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconMail, IconLock } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -69,7 +69,7 @@ const PasswordResetConfirm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div className="relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../helpers';
|
||||
import { API, getLogo, showError, showInfo, showSuccess, getSystemName } from '../../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { Button, Card, Form, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconMail } from '@douyinfe/semi-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -76,7 +76,7 @@ const PasswordResetForm = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div className="relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
{/* 背景图片容器 - 放大并保持居中 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
showSuccess,
|
||||
updateAPI,
|
||||
getSystemName,
|
||||
} from '../helpers';
|
||||
setUserData
|
||||
} from '../../helpers/index.js';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import {
|
||||
Button,
|
||||
@@ -25,15 +26,14 @@ import {
|
||||
onGitHubOAuthClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
onOIDCClicked,
|
||||
} from './utils.js';
|
||||
import OIDCIcon from './common/logo/OIDCIcon.js';
|
||||
import LinuxDoIcon from './common/logo/LinuxDoIcon.js';
|
||||
import WeChatIcon from './common/logo/WeChatIcon.js';
|
||||
} from '../../helpers/index.js';
|
||||
import OIDCIcon from '../common/logo/OIDCIcon.js';
|
||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon.js';
|
||||
import WeChatIcon from '../common/logo/WeChatIcon.js';
|
||||
import TelegramLoginButton from 'react-telegram-login/src';
|
||||
import { setUserData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Background from '../images/example.png';
|
||||
import Background from '/example.png';
|
||||
|
||||
const RegisterForm = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -549,7 +549,7 @@ const RegisterForm = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div className="relative flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 z-0 bg-cover bg-center scale-125 opacity-100"
|
||||
style={{
|
||||
@@ -1,6 +1,6 @@
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import 'highlight.js/styles/default.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
import './markdown.css';
|
||||
import RemarkMath from 'remark-math';
|
||||
import RemarkBreaks from 'remark-breaks';
|
||||
@@ -13,10 +13,9 @@ import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import clsx from 'clsx';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { copy } from '../../../helpers/utils';
|
||||
import { copy, rehypeSplitWordsIntoSpans } from '../../../helpers';
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { rehypeSplitWordsIntoSpans } from '../../../utils/rehypeSplitWordsIntoSpans';
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
// 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=100');
|
||||
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(id) {
|
||||
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);
|
||||
// setChatLink(link);
|
||||
|
||||
const address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState, useMemo, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Typography } from '@douyinfe/semi-ui';
|
||||
import { getFooterHTML, getLogo, getSystemName } from '../helpers';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { getFooterHTML, getLogo, getSystemName } from '../../helpers';
|
||||
import { StatusContext } from '../../context/Status';
|
||||
|
||||
const FooterBar = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { useSetTheme, useTheme } from '../context/Theme';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showSuccess } from '../helpers';
|
||||
import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../../helpers/index.js';
|
||||
import fireworks from 'react-fireworks';
|
||||
import { CN, GB } from 'country-flag-icons/react/3x2';
|
||||
import NoticeModal from './NoticeModal';
|
||||
import NoticeModal from './NoticeModal.js';
|
||||
|
||||
import {
|
||||
IconClose,
|
||||
@@ -29,9 +29,8 @@ import {
|
||||
Typography,
|
||||
Skeleton,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { stringToColor } from '../helpers/render';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
|
||||
const HeaderBar = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Modal, Empty } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../helpers';
|
||||
import { API, showError } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import HeaderBar from './HeaderBar.js';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import SiderBar from './SiderBar.js';
|
||||
import App from '../App.js';
|
||||
import App from '../../App.js';
|
||||
import FooterBar from './Footer.js';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useStyle } from '../context/Style/index.js';
|
||||
import { useStyle } from '../../context/Style/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, getLogo, getSystemName, showError } from '../helpers/index.js';
|
||||
import { setStatusData } from '../helpers/data.js';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
const { Sider, Content, Header, Footer } = Layout;
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
import { StatusContext } from '../context/Status';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
API,
|
||||
getLogo,
|
||||
getSystemName,
|
||||
isAdmin,
|
||||
isMobile,
|
||||
showError,
|
||||
} from '../helpers';
|
||||
import '../index.css';
|
||||
showError
|
||||
} from '../../helpers/index.js';
|
||||
|
||||
import {
|
||||
IconCalendarClock,
|
||||
@@ -21,28 +15,19 @@ import {
|
||||
IconTerminal,
|
||||
IconCreditCard,
|
||||
IconGift,
|
||||
IconHelpCircle,
|
||||
IconHistogram,
|
||||
IconHome,
|
||||
IconImage,
|
||||
IconKey,
|
||||
IconLayers,
|
||||
IconPriceTag,
|
||||
IconSetting,
|
||||
IconUser,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import {
|
||||
Avatar,
|
||||
Dropdown,
|
||||
Layout,
|
||||
Nav,
|
||||
Switch,
|
||||
Divider,
|
||||
} 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 { useStyle, styleActions } from '../context/Style/index.js';
|
||||
import { useSetTheme, useTheme } from '../../context/Theme/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
// 自定义侧边栏按钮样式
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { Button, Tooltip, Toast } from '@douyinfe/semi-ui';
|
||||
import { Copy, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { copy } from '../../helpers/utils';
|
||||
import { copy } from '../../helpers';
|
||||
|
||||
const PERFORMANCE_CONFIG = {
|
||||
MAX_DISPLAY_LENGTH: 50000, // 最大显示字符数
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { renderGroupOption } from '../../helpers/render.js';
|
||||
import { renderGroupOption } from '../../helpers';
|
||||
import ParameterControl from './ParameterControl';
|
||||
import ImageUrlInput from './ImageUrlInput';
|
||||
import ConfigManager from './ConfigManager';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../utils/constants';
|
||||
import { STORAGE_KEYS, DEFAULT_CONFIG } from '../../constants/playground.constants';
|
||||
|
||||
const MESSAGES_STORAGE_KEY = 'playground_messages';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js';
|
||||
import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js';
|
||||
import SettingGlobalModel from '../pages/Setting/Model/SettingGlobalModel.js';
|
||||
import SettingGeminiModel from '../../pages/Setting/Model/SettingGeminiModel.js';
|
||||
import SettingClaudeModel from '../../pages/Setting/Model/SettingClaudeModel.js';
|
||||
import SettingGlobalModel from '../../pages/Setting/Model/SettingGlobalModel.js';
|
||||
|
||||
const ModelSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,20 +1,20 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
import SettingsLog from '../pages/Setting/Operation/SettingsLog.js';
|
||||
import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||
import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
|
||||
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
|
||||
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
|
||||
import SettingsGeneral from '../../pages/Setting/Operation/SettingsGeneral.js';
|
||||
import SettingsDrawing from '../../pages/Setting/Operation/SettingsDrawing.js';
|
||||
import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensitiveWords.js';
|
||||
import SettingsLog from '../../pages/Setting/Operation/SettingsLog.js';
|
||||
import SettingsDataDashboard from '../../pages/Setting/Operation/SettingsDataDashboard.js';
|
||||
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring.js';
|
||||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit.js';
|
||||
import ModelSettingsVisualEditor from '../../pages/Setting/Operation/ModelSettingsVisualEditor.js';
|
||||
import GroupRatioSettings from '../../pages/Setting/Operation/GroupRatioSettings.js';
|
||||
import ModelRatioSettings from '../../pages/Setting/Operation/ModelRatioSettings.js';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError, showSuccess } from '../../helpers';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ModelRatioNotSetEditor from '../pages/Setting/Operation/ModelRationNotSetEditor.js';
|
||||
import ModelRatioNotSetEditor from '../../pages/Setting/Operation/ModelRationNotSetEditor.js';
|
||||
|
||||
const OperationSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
Space,
|
||||
Card,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError, showSuccess, timestamp2string } from '../helpers';
|
||||
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from '../context/Status/index.js';
|
||||
import { StatusContext } from '../../context/Status/index.js';
|
||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||
|
||||
const OtherSetting = () => {
|
||||
@@ -8,14 +8,16 @@ import {
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
stringToColor,
|
||||
onGitHubOAuthClicked,
|
||||
onOIDCClicked,
|
||||
onLinuxDOOAuthClicked,
|
||||
} from './utils';
|
||||
onLinuxDOOAuthClicked
|
||||
} from '../../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../../context/User';
|
||||
import {
|
||||
Avatar,
|
||||
Banner,
|
||||
@@ -54,12 +56,6 @@ import {
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
||||
import { Bell, Shield, Webhook, Globe, Settings, UserPlus, ShieldCheck } from 'lucide-react';
|
||||
import {
|
||||
getQuotaPerUnit,
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
stringToColor,
|
||||
} from '../helpers/render';
|
||||
import TelegramLoginButton from 'react-telegram-login';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -410,7 +406,7 @@ const PersonalSetting = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
{/* 划转模态框 */}
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
|
||||
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
|
||||
import { API, showError, showSuccess } from '../../helpers/index.js';
|
||||
import SettingsChats from '../../pages/Setting/Operation/SettingsChats.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import RequestRateLimit from '../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||
import RequestRateLimit from '../../pages/Setting/RateLimit/SettingsRequestRateLimit.js';
|
||||
|
||||
const RateLimitSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
const { Text } = Typography;
|
||||
import {
|
||||
API,
|
||||
removeTrailingSlash,
|
||||
showError,
|
||||
showSuccess,
|
||||
verifyJSON,
|
||||
} from '../helpers/utils';
|
||||
import { API } from '../helpers/api';
|
||||
verifyJSON
|
||||
} from '../../helpers';
|
||||
import axios from 'axios';
|
||||
|
||||
const SystemSetting = () => {
|
||||
@@ -5,14 +5,12 @@ import {
|
||||
showInfo,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderGroup,
|
||||
renderNumberWithPoint,
|
||||
renderQuota,
|
||||
} from '../helpers/render';
|
||||
renderQuota
|
||||
} from '../../helpers/index.js';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
@@ -29,9 +27,9 @@ import {
|
||||
Typography,
|
||||
Checkbox,
|
||||
Card,
|
||||
Select,
|
||||
Select
|
||||
} from '@douyinfe/semi-ui';
|
||||
import EditChannel from '../pages/Channel/EditChannel';
|
||||
import EditChannel from '../../pages/Channel/EditChannel.js';
|
||||
import {
|
||||
IconList,
|
||||
IconTreeTriangleDown,
|
||||
@@ -48,8 +46,8 @@ import {
|
||||
IconCopy,
|
||||
IconSmallTriangleRight
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { loadChannelModels } from './utils.js';
|
||||
import EditTagModal from '../pages/Channel/EditTagModal.js';
|
||||
import { loadChannelModels } from '../../helpers/index.js';
|
||||
import EditTagModal from '../../pages/Channel/EditTagModal.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ChannelsTable = () => {
|
||||
@@ -8,7 +8,19 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderAudioModelPrice,
|
||||
renderClaudeLogContent,
|
||||
renderClaudeModelPrice,
|
||||
renderClaudeModelPriceSimple,
|
||||
renderGroup,
|
||||
renderLogContent,
|
||||
renderModelPrice,
|
||||
renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
getLogOther
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
@@ -29,22 +41,8 @@ import {
|
||||
Input,
|
||||
DatePicker,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import {
|
||||
renderAudioModelPrice,
|
||||
renderClaudeLogContent,
|
||||
renderClaudeModelPrice,
|
||||
renderClaudeModelPriceSimple,
|
||||
renderGroup,
|
||||
renderLogContent,
|
||||
renderModelPrice,
|
||||
renderModelPriceSimple,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
stringToColor,
|
||||
} from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
|
||||
import { getLogOther } from '../helpers/other.js';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconSetting,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useContext, useEffect, useRef, useMemo, useState } from 'react';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
|
||||
import { API, copy, showError, showInfo, showSuccess } from '../../helpers/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
IconInfoCircle,
|
||||
IconCrown,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { UserContext } from '../context/User/index.js';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { MODEL_CATEGORIES } from '../constants';
|
||||
import { MODEL_CATEGORIES } from '../../constants/index.js';
|
||||
|
||||
const ModelPricing = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -481,7 +481,7 @@ const ModelPricing = () => {
|
||||
), [filteredModels, loading, columns, rowSelection, pageSize, t]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<div className="flex justify-center p-4 sm:p-6 md:p-8">
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderQuota
|
||||
} from '../../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
IconPlay,
|
||||
IconMore,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditRedemption from '../pages/Redemption/EditRedemption';
|
||||
import EditRedemption from '../../pages/Redemption/EditRedemption';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
} from '../../helpers';
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
Tag,
|
||||
Typography,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
IconEyeOpened,
|
||||
IconSearch,
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
} from '../helpers';
|
||||
renderGroup,
|
||||
renderQuota
|
||||
} from '../../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderQuota } from '../helpers/render';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -39,9 +40,9 @@ import {
|
||||
IconHistogram,
|
||||
IconRotate,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import EditToken from '../pages/Token/EditToken';
|
||||
import EditToken from '../../pages/Token/EditToken';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UserContext } from '../context/User';
|
||||
import { UserContext } from '../../context/User';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return <>{timestamp2string(timestamp)}</>;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { API, showError, showSuccess } from '../helpers';
|
||||
import { API, showError, showSuccess, renderGroup, renderNumber, renderQuota } from '../../helpers';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -25,10 +25,9 @@ import {
|
||||
IconArrowUp,
|
||||
IconArrowDown,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
|
||||
import AddUser from '../pages/User/AddUser';
|
||||
import EditUser from '../pages/User/EditUser';
|
||||
import { ITEMS_PER_PAGE } from '../../constants';
|
||||
import AddUser from '../../pages/User/AddUser';
|
||||
import EditUser from '../../pages/User/EditUser';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -1,76 +0,0 @@
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
export async function getOAuthState() {
|
||||
let path = '/api/oauth/state';
|
||||
let affCode = localStorage.getItem('aff');
|
||||
if (affCode && affCode.length > 0) {
|
||||
path += `?aff=${affCode}`;
|
||||
}
|
||||
const res = await API.get(path);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = 'code';
|
||||
const scope = 'openid profile email';
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function onLinuxDOOAuthClicked(linuxdo_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelModels = undefined;
|
||||
export async function loadChannelModels() {
|
||||
const res = await API.get('/api/models');
|
||||
const { success, data } = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getChannelModels(type) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
if (!channelModels[type]) {
|
||||
return [];
|
||||
}
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './user.constants';
|
||||
export * from './toast.constants';
|
||||
export * from './common.constant';
|
||||
export * from './model.constants';
|
||||
export * from './playground.constants';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useReducer, useEffect, useMemo, createContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { isMobile as getIsMobile } from '../../helpers/index.js';
|
||||
import { isMobile as getIsMobile } from '../../helpers';
|
||||
|
||||
// Action Types
|
||||
const ACTION_TYPES = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getUserIdFromLocalStorage, showError } from './utils';
|
||||
import { getUserIdFromLocalStorage, showError, formatMessageForAPI, isValidMessage } from './utils';
|
||||
import axios from 'axios';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export let API = axios.create({
|
||||
baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL
|
||||
@@ -29,3 +30,185 @@ API.interceptors.response.use(
|
||||
showError(error);
|
||||
},
|
||||
);
|
||||
|
||||
// playground
|
||||
|
||||
// 构建API请求负载
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages
|
||||
.filter(isValidMessage)
|
||||
.map(formatMessageForAPI)
|
||||
.filter(Boolean);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: MESSAGE_ROLES.SYSTEM,
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
const parameterMappings = {
|
||||
temperature: 'temperature',
|
||||
top_p: 'top_p',
|
||||
max_tokens: 'max_tokens',
|
||||
frequency_penalty: 'frequency_penalty',
|
||||
presence_penalty: 'presence_penalty',
|
||||
seed: 'seed'
|
||||
};
|
||||
|
||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// 处理API错误响应
|
||||
export const handleApiError = (error, response = null) => {
|
||||
const errorInfo = {
|
||||
error: error.message || '未知错误',
|
||||
timestamp: new Date().toISOString(),
|
||||
stack: error.stack
|
||||
};
|
||||
|
||||
if (response) {
|
||||
errorInfo.status = response.status;
|
||||
errorInfo.statusText = response.statusText;
|
||||
}
|
||||
|
||||
if (error.message.includes('HTTP error')) {
|
||||
errorInfo.details = '服务器返回了错误状态码';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorInfo.details = '网络连接失败或服务器无响应';
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
};
|
||||
|
||||
// 处理模型数据
|
||||
export const processModelsData = (data, currentModel) => {
|
||||
const modelOptions = data.map(model => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}));
|
||||
|
||||
const hasCurrentModel = modelOptions.some(option => option.value === currentModel);
|
||||
const selectedModel = hasCurrentModel && modelOptions.length > 0
|
||||
? currentModel
|
||||
: modelOptions[0]?.value;
|
||||
|
||||
return { modelOptions, selectedModel };
|
||||
};
|
||||
|
||||
// 处理分组数据
|
||||
export const processGroupsData = (data, userGroup) => {
|
||||
let groupOptions = Object.entries(data).map(([group, info]) => ({
|
||||
label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc,
|
||||
value: group,
|
||||
ratio: info.ratio,
|
||||
fullLabel: info.desc,
|
||||
}));
|
||||
|
||||
if (groupOptions.length === 0) {
|
||||
groupOptions = [{
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
ratio: 1,
|
||||
}];
|
||||
} else if (userGroup) {
|
||||
const userGroupIndex = groupOptions.findIndex(g => g.value === userGroup);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = groupOptions.splice(userGroupIndex, 1)[0];
|
||||
groupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
|
||||
return groupOptions;
|
||||
};
|
||||
|
||||
// 原来components中的utils.js
|
||||
|
||||
export async function getOAuthState() {
|
||||
let path = '/api/oauth/state';
|
||||
let affCode = localStorage.getItem('aff');
|
||||
if (affCode && affCode.length > 0) {
|
||||
path += `?aff=${affCode}`;
|
||||
}
|
||||
const res = await API.get(path);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||
const response_type = 'code';
|
||||
const scope = 'openid profile email';
|
||||
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function onLinuxDOOAuthClicked(linuxdo_client_id) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
window.open(
|
||||
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
||||
);
|
||||
}
|
||||
|
||||
let channelModels = undefined;
|
||||
export async function loadChannelModels() {
|
||||
const res = await API.get('/api/models');
|
||||
const { success, data } = res.data;
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
channelModels = data;
|
||||
localStorage.setItem('channel_models', JSON.stringify(data));
|
||||
}
|
||||
|
||||
export function getChannelModels(type) {
|
||||
if (channelModels !== undefined && type in channelModels) {
|
||||
if (!channelModels[type]) {
|
||||
return [];
|
||||
}
|
||||
return channelModels[type];
|
||||
}
|
||||
let models = localStorage.getItem('channel_models');
|
||||
if (!models) {
|
||||
return [];
|
||||
}
|
||||
channelModels = JSON.parse(models);
|
||||
if (type in channelModels) {
|
||||
return channelModels[type];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
export function authHeader() {
|
||||
// return authorization header with jwt token
|
||||
let user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.token) {
|
||||
return { Authorization: 'Bearer ' + user.token };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
33
web/src/helpers/auth.js
Normal file
33
web/src/helpers/auth.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { history } from './history';
|
||||
|
||||
export function authHeader() {
|
||||
// return authorization header with jwt token
|
||||
let user = JSON.parse(localStorage.getItem('user'));
|
||||
|
||||
if (user && user.token) {
|
||||
return { Authorization: 'Bearer ' + user.token };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export const AuthRedirect = ({ children }) => {
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
if (user) {
|
||||
return <Navigate to="/console" replace />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
function PrivateRoute({ children }) {
|
||||
if (!localStorage.getItem('user')) {
|
||||
return <Navigate to='/login' state={{ from: history.location }} />;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
export { PrivateRoute };
|
||||
@@ -1,4 +1,8 @@
|
||||
export * from './history';
|
||||
export * from './auth-header';
|
||||
export * from './auth';
|
||||
export * from './utils';
|
||||
export * from './api';
|
||||
export * from './render';
|
||||
export * from './log';
|
||||
export * from './data';
|
||||
export * from './token';
|
||||
|
||||
@@ -4,4 +4,4 @@ export function getLogOther(otherStr) {
|
||||
}
|
||||
let other = JSON.parse(otherStr);
|
||||
return other;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18next from 'i18next';
|
||||
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { copy, isMobile, showSuccess } from './utils.js';
|
||||
import { copy, isMobile, showSuccess } from './utils';
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
export function renderText(text, limit) {
|
||||
if (text.length > limit) {
|
||||
@@ -419,11 +420,25 @@ export function renderModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0 && !image && !webSearch && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
'输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
nonImageInput: inputTokens - imageOutputTokens,
|
||||
imageInput: imageOutputTokens,
|
||||
imageRatio: imageRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
@@ -431,82 +446,68 @@ export function renderModelPrice(
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: image && imageOutputTokens > 0 && !webSearch && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonImageInput: inputTokens - imageOutputTokens,
|
||||
imageInput: imageOutputTokens,
|
||||
imageRatio: imageRatio,
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: webSearch && webSearchCallCount > 0 && !image && !fileSearch
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
|
||||
: fileSearch &&
|
||||
fileSearchCallCount > 0 &&
|
||||
!image &&
|
||||
!webSearch
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
fileSearchCallCount,
|
||||
fileSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: fileSearch &&
|
||||
: webSearch &&
|
||||
webSearchCallCount > 0 &&
|
||||
fileSearch &&
|
||||
fileSearchCallCount > 0 &&
|
||||
!image &&
|
||||
!webSearch
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
!image
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
fileSearchCallCount,
|
||||
fileSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: webSearch &&
|
||||
webSearchCallCount > 0 &&
|
||||
fileSearch &&
|
||||
fileSearchCallCount > 0 &&
|
||||
!image
|
||||
? i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
webSearchCallCount,
|
||||
webSearchPrice,
|
||||
fileSearchCallCount,
|
||||
fileSearchPrice,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
@@ -677,10 +678,10 @@ export function renderAudioModelPrice(
|
||||
let audioPrice =
|
||||
(audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
|
||||
(audioCompletionTokens / 1000000) *
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
inputRatioPrice *
|
||||
audioRatio *
|
||||
audioCompletionRatio *
|
||||
groupRatio;
|
||||
let price = textPrice + audioPrice;
|
||||
return (
|
||||
<>
|
||||
@@ -736,27 +737,27 @@ export function renderAudioModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0
|
||||
? i18next.t(
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
'文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: inputTokens - cacheTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cachePrice: inputRatioPrice * cacheRatio,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
total: textPrice.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{i18next.t(
|
||||
@@ -1024,33 +1025,33 @@ export function renderClaudeModelPrice(
|
||||
<p>
|
||||
{cacheTokens > 0 || cacheCreationTokens > 0
|
||||
? i18next.t(
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
'提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
nonCacheInput: nonCachedTokens,
|
||||
cacheInput: cacheTokens,
|
||||
cacheRatio: cacheRatio,
|
||||
cacheCreationInput: cacheCreationTokens,
|
||||
cacheCreationRatio: cacheCreationRatio,
|
||||
cachePrice: cacheRatioPrice,
|
||||
cacheCreationPrice: cacheCreationRatioPrice,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)
|
||||
: i18next.t(
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
'提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
|
||||
{
|
||||
input: inputTokens,
|
||||
price: inputRatioPrice,
|
||||
completion: completionTokens,
|
||||
compPrice: completionRatioPrice,
|
||||
ratio: groupRatio,
|
||||
total: price.toFixed(6),
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>{i18next.t('仅供参考,以实际扣费为准')}</p>
|
||||
</article>
|
||||
@@ -1128,3 +1129,79 @@ export function renderClaudeModelPriceSimple(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
||||
* 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
||||
*/
|
||||
export function rehypeSplitWordsIntoSpans(options = {}) {
|
||||
const { previousContentLength = 0 } = options;
|
||||
|
||||
return (tree) => {
|
||||
let currentCharCount = 0; // 当前已处理的字符数
|
||||
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(node.tagName) &&
|
||||
node.children
|
||||
) {
|
||||
const newChildren = [];
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'text') {
|
||||
try {
|
||||
// 使用 Intl.Segmenter 精准拆分中英文及标点
|
||||
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
||||
const segments = segmenter.segment(child.value);
|
||||
|
||||
Array.from(segments)
|
||||
.map((seg) => seg.segment)
|
||||
.filter(Boolean)
|
||||
.forEach((word) => {
|
||||
const wordStartPos = currentCharCount;
|
||||
const wordEndPos = currentCharCount + word.length;
|
||||
|
||||
// 判断这个词是否是新增的(在 previousContentLength 之后)
|
||||
const isNewContent = wordStartPos >= previousContentLength;
|
||||
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: isNewContent ? ['animate-fade-in'] : [],
|
||||
},
|
||||
children: [{ type: 'text', value: word }],
|
||||
});
|
||||
|
||||
currentCharCount = wordEndPos;
|
||||
});
|
||||
} catch (_) {
|
||||
// Fallback:如果浏览器不支持 Segmenter
|
||||
const textStartPos = currentCharCount;
|
||||
const isNewContent = textStartPos >= previousContentLength;
|
||||
|
||||
if (isNewContent) {
|
||||
// 新内容,添加动画
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: ['animate-fade-in'],
|
||||
},
|
||||
children: [{ type: 'text', value: child.value }],
|
||||
});
|
||||
} else {
|
||||
// 旧内容,不添加动画
|
||||
newChildren.push(child);
|
||||
}
|
||||
|
||||
currentCharCount += child.value.length;
|
||||
}
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
});
|
||||
node.children = newChildren;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
45
web/src/helpers/token.js
Normal file
45
web/src/helpers/token.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { API } from './api';
|
||||
|
||||
/**
|
||||
* 获取可用的token keys
|
||||
* @returns {Promise<string[]>} 返回active状态的token key数组
|
||||
*/
|
||||
export async function fetchTokenKeys() {
|
||||
try {
|
||||
const response = await API.get('/api/token/?p=0&size=100');
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器地址
|
||||
* @returns {string} 服务器地址
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Toast } from '@douyinfe/semi-ui';
|
||||
import { toastConstants } from '../constants';
|
||||
import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
const HTMLToastContent = ({ htmlContent }) => {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
@@ -283,3 +284,165 @@ export function compareObjects(oldObject, newObject) {
|
||||
|
||||
return changedProperties;
|
||||
}
|
||||
|
||||
// playground message
|
||||
|
||||
// 生成唯一ID
|
||||
let messageId = 4;
|
||||
export const generateMessageId = () => `${messageId++}`;
|
||||
|
||||
// 提取消息中的文本内容
|
||||
export const getTextContent = (message) => {
|
||||
if (!message || !message.content) return '';
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
return textContent?.text || '';
|
||||
}
|
||||
return typeof message.content === 'string' ? message.content : '';
|
||||
};
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
const thoughts = [];
|
||||
const replyParts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
THINK_TAG_REGEX.lastIndex = 0;
|
||||
while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
|
||||
replyParts.push(content.substring(lastIndex, match.index));
|
||||
thoughts.push(match[1]);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
replyParts.push(content.substring(lastIndex));
|
||||
|
||||
const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
reasoningContent: processedReasoningContent
|
||||
};
|
||||
};
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
}
|
||||
|
||||
const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
|
||||
return processThinkTags(content, reasoningContent);
|
||||
};
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
if (!textContent && (!imageUrls || imageUrls.length === 0)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
|
||||
|
||||
if (imageEnabled && validImageUrls.length > 0) {
|
||||
return [
|
||||
{ type: 'text', text: textContent || '' },
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url: url.trim() }
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
return textContent || '';
|
||||
};
|
||||
|
||||
// 创建新消息
|
||||
export const createMessage = (role, content, options = {}) => ({
|
||||
role,
|
||||
content,
|
||||
createAt: Date.now(),
|
||||
id: generateMessageId(),
|
||||
...options
|
||||
});
|
||||
|
||||
// 创建加载中的助手消息
|
||||
export const createLoadingAssistantMessage = () => createMessage(
|
||||
MESSAGE_ROLES.ASSISTANT,
|
||||
'',
|
||||
{
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: true,
|
||||
isThinkingComplete: false,
|
||||
hasAutoCollapsed: false,
|
||||
status: 'loading'
|
||||
}
|
||||
);
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
export const formatMessageForAPI = (message) => {
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
};
|
||||
};
|
||||
|
||||
// 验证消息是否有效
|
||||
export const isValidMessage = (message) => {
|
||||
return message &&
|
||||
message.role &&
|
||||
(message.content || message.content === '');
|
||||
};
|
||||
|
||||
// 获取最后一条用户消息
|
||||
export const getLastUserMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取最后一条助手消息
|
||||
export const getLastAssistantMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SSE } from 'sse';
|
||||
import { getUserIdFromLocalStorage } from '../helpers/index.js';
|
||||
import {
|
||||
API_ENDPOINTS,
|
||||
MESSAGE_STATUS,
|
||||
DEBUG_TABS
|
||||
} from '../utils/constants';
|
||||
import {
|
||||
buildApiPayload,
|
||||
handleApiError
|
||||
} from '../utils/apiUtils';
|
||||
} from '../constants/playground.constants';
|
||||
import {
|
||||
getUserIdFromLocalStorage,
|
||||
handleApiError,
|
||||
processThinkTags,
|
||||
processIncompleteThinkTags
|
||||
} from '../utils/messageUtils';
|
||||
} from '../helpers';
|
||||
|
||||
export const useApiRequest = (
|
||||
setMessage,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { API, showError } from '../helpers/index.js';
|
||||
import { API_ENDPOINTS } from '../utils/constants';
|
||||
import { processModelsData, processGroupsData } from '../utils/apiUtils';
|
||||
import { API, processModelsData, processGroupsData } from '../helpers';
|
||||
import { API_ENDPOINTS } from '../constants/playground.constants';
|
||||
|
||||
export const useDataLoader = (
|
||||
userState,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent } from '../utils/messageUtils';
|
||||
import { ERROR_MESSAGES } from '../utils/constants';
|
||||
import { getTextContent } from '../helpers';
|
||||
import { ERROR_MESSAGES } from '../constants/playground.constants';
|
||||
|
||||
export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
import { Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../utils/messageUtils';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../helpers';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export const useMessageEdit = (
|
||||
setMessage,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../utils/constants';
|
||||
import { DEFAULT_MESSAGES, DEFAULT_CONFIG, DEBUG_TABS, MESSAGE_STATUS } from '../constants/playground.constants';
|
||||
import { loadConfig, saveConfig, loadMessages, saveMessages } from '../components/playground/configStorage';
|
||||
import { processIncompleteThinkTags } from '../utils/messageUtils';
|
||||
import { processIncompleteThinkTags } from '../helpers';
|
||||
|
||||
export const usePlaygroundState = () => {
|
||||
// 使用惰性初始化,确保只在组件首次挂载时加载配置和消息
|
||||
|
||||
32
web/src/hooks/useSetupCheck.js
Normal file
32
web/src/hooks/useSetupCheck.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { StatusContext } from '../context/Status';
|
||||
|
||||
/**
|
||||
* 自定义Hook:检查系统setup状态并进行重定向
|
||||
* @param {Object} options - 配置选项
|
||||
* @param {boolean} options.autoRedirect - 是否自动重定向,默认true
|
||||
* @param {string} options.setupPath - setup页面路径,默认'/setup'
|
||||
* @returns {Object} 返回setup状态信息
|
||||
*/
|
||||
export function useSetupCheck(options = {}) {
|
||||
const { autoRedirect = true, setupPath = '/setup' } = options;
|
||||
const [statusState] = useContext(StatusContext);
|
||||
const location = useLocation();
|
||||
|
||||
const isSetupComplete = statusState?.status?.setup !== false;
|
||||
const needsSetup = !isSetupComplete && location.pathname !== setupPath;
|
||||
|
||||
useEffect(() => {
|
||||
if (autoRedirect && needsSetup) {
|
||||
window.location.href = setupPath;
|
||||
}
|
||||
}, [autoRedirect, needsSetup, setupPath]);
|
||||
|
||||
return {
|
||||
isSetupComplete,
|
||||
needsSetup,
|
||||
statusState,
|
||||
currentPath: location.pathname
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { MESSAGE_ROLES } from '../utils/constants';
|
||||
import { MESSAGE_ROLES } from '../constants/playground.constants';
|
||||
|
||||
export const useSyncMessageAndCustomBody = (
|
||||
customRequestMode,
|
||||
|
||||
30
web/src/hooks/useTokenKeys.js
Normal file
30
web/src/hooks/useTokenKeys.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { fetchTokenKeys, getServerAddress } from '../helpers/token';
|
||||
import { showError } from '../helpers';
|
||||
|
||||
export function useTokenKeys(id) {
|
||||
const [keys, setKeys] = 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 address = getServerAddress();
|
||||
setServerAddress(address);
|
||||
};
|
||||
|
||||
loadAllData();
|
||||
}, []);
|
||||
|
||||
return { keys, serverAddress, isLoading };
|
||||
}
|
||||
@@ -7,8 +7,9 @@ import { StatusProvider } from './context/Status';
|
||||
import { Layout } from '@douyinfe/semi-ui';
|
||||
import { ThemeProvider } from './context/Theme';
|
||||
import { StyleProvider } from './context/Style/index.js';
|
||||
import PageLayout from './components/PageLayout.js';
|
||||
import PageLayout from './components/layout/PageLayout.js';
|
||||
import './i18n/i18n.js';
|
||||
import './index.css';
|
||||
|
||||
// initialization
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Card,
|
||||
Tag,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
import { getChannelModels } from '../../helpers';
|
||||
import {
|
||||
IconSave,
|
||||
IconClose,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
IconUser,
|
||||
IconCode,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { getChannelModels } from '../../components/utils.js';
|
||||
import { getChannelModels } from '../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ChannelsTable from '../../components/ChannelsTable';
|
||||
import ChannelsTable from '../../components/table/ChannelsTable';
|
||||
|
||||
const File = () => {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { useTokenKeys } from '../../hooks/useTokenKeys';
|
||||
import { Banner, Layout } from '@douyinfe/semi-ui';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTokenKeys } from '../../components/fetchTokenKeys';
|
||||
import { useTokenKeys } from '../../hooks/useTokenKeys';
|
||||
|
||||
const chat2page = () => {
|
||||
const { keys, chatLink, serverAddress, isLoading } = useTokenKeys();
|
||||
|
||||
@@ -30,14 +30,12 @@ import {
|
||||
showError,
|
||||
timestamp2string,
|
||||
timestamp2string1,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
getQuotaWithUnit,
|
||||
modelColorMap,
|
||||
renderNumber,
|
||||
renderQuota,
|
||||
modelToColor,
|
||||
} from '../../helpers/render';
|
||||
modelToColor
|
||||
} from '../../helpers';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import { StatusContext } from '../../context/Status';
|
||||
import { marked } from 'marked';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||
import exampleImage from '../../images/example.png';
|
||||
import exampleImage from '/example.png';
|
||||
import { Link } from 'react-router-dom';
|
||||
import NoticeModal from '../../components/NoticeModal';
|
||||
import NoticeModal from '../../components/layout/NoticeModal';
|
||||
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import LogsTable from '../../components/LogsTable';
|
||||
import LogsTable from '../../components/table/LogsTable';
|
||||
|
||||
const Token = () => (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import MjLogsTable from '../../components/MjLogsTable';
|
||||
import MjLogsTable from '../../components/table/MjLogsTable';
|
||||
|
||||
const Midjourney = () => (
|
||||
<>
|
||||
|
||||
@@ -7,9 +7,7 @@ import { Layout, Toast, Modal } from '@douyinfe/semi-ui';
|
||||
import { UserContext } from '../../context/User/index.js';
|
||||
import { useStyle, styleActions } from '../../context/Style/index.js';
|
||||
|
||||
// Utils and hooks
|
||||
import { getLogo } from '../../helpers/index.js';
|
||||
import { stringToColor } from '../../helpers/render.js';
|
||||
// hooks
|
||||
import { usePlaygroundState } from '../../hooks/usePlaygroundState.js';
|
||||
import { useMessageActions } from '../../hooks/useMessageActions.js';
|
||||
import { useApiRequest } from '../../hooks/useApiRequest.js';
|
||||
@@ -19,17 +17,18 @@ import { useDataLoader } from '../../hooks/useDataLoader.js';
|
||||
|
||||
// Constants and utils
|
||||
import {
|
||||
DEFAULT_MESSAGES,
|
||||
MESSAGE_ROLES,
|
||||
ERROR_MESSAGES
|
||||
} from '../../utils/constants.js';
|
||||
} from '../../constants/playground.constants.js';
|
||||
import {
|
||||
getLogo,
|
||||
stringToColor,
|
||||
buildMessageContent,
|
||||
createMessage,
|
||||
createLoadingAssistantMessage,
|
||||
getTextContent,
|
||||
buildApiPayload
|
||||
} from '../../utils/messageUtils.js';
|
||||
} from '../../helpers';
|
||||
|
||||
// Components
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ModelPricing from '../../components/ModelPricing.js';
|
||||
import ModelPricing from '../../components/table/ModelPricing.js';
|
||||
|
||||
const Pricing = () => (
|
||||
<>
|
||||
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
isMobile,
|
||||
showError,
|
||||
showSuccess,
|
||||
} from '../../helpers';
|
||||
import {
|
||||
renderQuota,
|
||||
renderQuotaWithPrompt,
|
||||
} from '../../helpers/render';
|
||||
renderQuotaWithPrompt
|
||||
} from '../../helpers';
|
||||
import {
|
||||
AutoComplete,
|
||||
Button,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import RedemptionsTable from '../../components/RedemptionsTable';
|
||||
import RedemptionsTable from '../../components/table/RedemptionsTable';
|
||||
|
||||
const Redemption = () => {
|
||||
return (
|
||||
|
||||
@@ -17,8 +17,7 @@ import {
|
||||
IconSave,
|
||||
IconBolt,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { API, showError, showSuccess } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ModelRatioNotSetEditor(props) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ModelSettingsVisualEditor.js
|
||||
import React, { useContext, useEffect, useState, useRef } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
Form,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Radio
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IconDelete,
|
||||
@@ -19,11 +17,8 @@ import {
|
||||
IconSave,
|
||||
IconEdit,
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { showError, showSuccess } from '../../../helpers';
|
||||
import { API } from '../../../helpers';
|
||||
import { API, showError, showSuccess, getQuotaPerUnit } from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StatusContext } from '../../../context/Status/index.js';
|
||||
import { getQuotaPerUnit } from '../../../helpers/render.js';
|
||||
|
||||
export default function ModelSettingsVisualEditor(props) {
|
||||
const { t } = useTranslation();
|
||||
@@ -304,11 +299,11 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
prev.map((model, index) =>
|
||||
index === existingModelIndex
|
||||
? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
: model,
|
||||
),
|
||||
);
|
||||
@@ -456,8 +451,8 @@ export default function ModelSettingsVisualEditor(props) {
|
||||
<Modal
|
||||
title={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
? t('编辑模型')
|
||||
: t('添加模型')
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import SystemSetting from '../../components/SystemSetting';
|
||||
import SystemSetting from '../../components/settings/SystemSetting.js';
|
||||
import { isRoot } from '../../helpers';
|
||||
import OtherSetting from '../../components/OtherSetting';
|
||||
import PersonalSetting from '../../components/PersonalSetting';
|
||||
import OperationSetting from '../../components/OperationSetting';
|
||||
import RateLimitSetting from '../../components/RateLimitSetting.js';
|
||||
import ModelSetting from '../../components/ModelSetting.js';
|
||||
import OtherSetting from '../../components/settings/OtherSetting';
|
||||
import PersonalSetting from '../../components/settings/PersonalSetting.js';
|
||||
import OperationSetting from '../../components/settings/OperationSetting.js';
|
||||
import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
|
||||
import ModelSetting from '../../components/settings/ModelSetting.js';
|
||||
|
||||
const Setting = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import TaskLogsTable from '../../components/TaskLogsTable.js';
|
||||
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
|
||||
|
||||
const Task = () => (
|
||||
<>
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
showError,
|
||||
showSuccess,
|
||||
timestamp2string,
|
||||
renderGroupOption,
|
||||
renderQuotaWithPrompt
|
||||
} from '../../helpers';
|
||||
import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import {
|
||||
AutoComplete,
|
||||
Banner,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import TokensTable from '../../components/TokensTable';
|
||||
import TokensTable from '../../components/table/TokensTable';
|
||||
|
||||
const Token = () => {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import { API, showError, showInfo, showSuccess } from '../../helpers';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
showInfo,
|
||||
showSuccess,
|
||||
renderQuota,
|
||||
renderQuotaWithAmount,
|
||||
stringToColor,
|
||||
} from '../../helpers/render';
|
||||
stringToColor
|
||||
} from '../../helpers';
|
||||
import {
|
||||
Layout,
|
||||
Typography,
|
||||
@@ -262,7 +265,7 @@ const TopUp = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="bg-gray-50">
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Modal
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { API, isMobile, showError, showSuccess } from '../../helpers';
|
||||
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
|
||||
import { API, isMobile, showError, showSuccess, renderQuota, renderQuotaWithPrompt } from '../../helpers';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import UsersTable from '../../components/UsersTable';
|
||||
import UsersTable from '../../components/table/UsersTable';
|
||||
|
||||
const User = () => {
|
||||
return (
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { formatMessageForAPI } from './messageUtils';
|
||||
|
||||
// 构建API请求载荷
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages.map(formatMessageForAPI);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: 'system',
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
if (parameterEnabled.temperature && inputs.temperature !== undefined) {
|
||||
payload.temperature = inputs.temperature;
|
||||
}
|
||||
if (parameterEnabled.top_p && inputs.top_p !== undefined) {
|
||||
payload.top_p = inputs.top_p;
|
||||
}
|
||||
if (parameterEnabled.max_tokens && inputs.max_tokens !== undefined) {
|
||||
payload.max_tokens = inputs.max_tokens;
|
||||
}
|
||||
if (parameterEnabled.frequency_penalty && inputs.frequency_penalty !== undefined) {
|
||||
payload.frequency_penalty = inputs.frequency_penalty;
|
||||
}
|
||||
if (parameterEnabled.presence_penalty && inputs.presence_penalty !== undefined) {
|
||||
payload.presence_penalty = inputs.presence_penalty;
|
||||
}
|
||||
if (parameterEnabled.seed && inputs.seed !== undefined && inputs.seed !== null) {
|
||||
payload.seed = inputs.seed;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// 处理API错误响应
|
||||
export const handleApiError = (error, response = null) => {
|
||||
const errorInfo = {
|
||||
error: error.message || '未知错误',
|
||||
timestamp: new Date().toISOString(),
|
||||
stack: error.stack
|
||||
};
|
||||
|
||||
if (response) {
|
||||
errorInfo.status = response.status;
|
||||
errorInfo.statusText = response.statusText;
|
||||
}
|
||||
|
||||
if (error.message.includes('HTTP error')) {
|
||||
errorInfo.details = '服务器返回了错误状态码';
|
||||
} else if (error.message.includes('Failed to fetch')) {
|
||||
errorInfo.details = '网络连接失败或服务器无响应';
|
||||
}
|
||||
|
||||
return errorInfo;
|
||||
};
|
||||
|
||||
// 处理模型数据
|
||||
export const processModelsData = (data, currentModel) => {
|
||||
const modelOptions = data.map(model => ({
|
||||
label: model,
|
||||
value: model,
|
||||
}));
|
||||
|
||||
const hasCurrentModel = modelOptions.some(option => option.value === currentModel);
|
||||
const selectedModel = hasCurrentModel && modelOptions.length > 0
|
||||
? currentModel
|
||||
: modelOptions[0]?.value;
|
||||
|
||||
return { modelOptions, selectedModel };
|
||||
};
|
||||
|
||||
// 处理分组数据
|
||||
export const processGroupsData = (data, userGroup) => {
|
||||
let groupOptions = Object.entries(data).map(([group, info]) => ({
|
||||
label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc,
|
||||
value: group,
|
||||
ratio: info.ratio,
|
||||
fullLabel: info.desc,
|
||||
}));
|
||||
|
||||
if (groupOptions.length === 0) {
|
||||
groupOptions = [{
|
||||
label: '用户分组',
|
||||
value: '',
|
||||
ratio: 1,
|
||||
}];
|
||||
} else if (userGroup) {
|
||||
const userGroupIndex = groupOptions.findIndex(g => g.value === userGroup);
|
||||
if (userGroupIndex > -1) {
|
||||
const userGroupOption = groupOptions.splice(userGroupIndex, 1)[0];
|
||||
groupOptions.unshift(userGroupOption);
|
||||
}
|
||||
}
|
||||
|
||||
return groupOptions;
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
import { THINK_TAG_REGEX, MESSAGE_ROLES } from './constants';
|
||||
|
||||
// 生成唯一ID
|
||||
let messageId = 4;
|
||||
export const generateMessageId = () => `${messageId++}`;
|
||||
|
||||
// 提取消息中的文本内容
|
||||
export const getTextContent = (message) => {
|
||||
if (!message || !message.content) return '';
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const textContent = message.content.find(item => item.type === 'text');
|
||||
return textContent?.text || '';
|
||||
}
|
||||
return typeof message.content === 'string' ? message.content : '';
|
||||
};
|
||||
|
||||
// 处理 think 标签
|
||||
export const processThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content || !content.includes('<think>')) {
|
||||
return { content, reasoningContent };
|
||||
}
|
||||
|
||||
const thoughts = [];
|
||||
const replyParts = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
THINK_TAG_REGEX.lastIndex = 0;
|
||||
while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
|
||||
replyParts.push(content.substring(lastIndex, match.index));
|
||||
thoughts.push(match[1]);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
replyParts.push(content.substring(lastIndex));
|
||||
|
||||
const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
|
||||
const thoughtsStr = thoughts.join('\n\n---\n\n');
|
||||
const processedReasoningContent = reasoningContent && thoughtsStr
|
||||
? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
|
||||
: reasoningContent || thoughtsStr;
|
||||
|
||||
return {
|
||||
content: processedContent,
|
||||
reasoningContent: processedReasoningContent
|
||||
};
|
||||
};
|
||||
|
||||
// 处理未完成的 think 标签
|
||||
export const processIncompleteThinkTags = (content, reasoningContent = '') => {
|
||||
if (!content) return { content: '', reasoningContent };
|
||||
|
||||
const lastOpenThinkIndex = content.lastIndexOf('<think>');
|
||||
if (lastOpenThinkIndex === -1) {
|
||||
return processThinkTags(content, reasoningContent);
|
||||
}
|
||||
|
||||
const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
|
||||
if (!fragmentAfterLastOpen.includes('</think>')) {
|
||||
const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
|
||||
const cleanContent = content.substring(0, lastOpenThinkIndex);
|
||||
const processedReasoningContent = unclosedThought
|
||||
? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
|
||||
: reasoningContent;
|
||||
|
||||
return processThinkTags(cleanContent, processedReasoningContent);
|
||||
}
|
||||
|
||||
return processThinkTags(content, reasoningContent);
|
||||
};
|
||||
|
||||
// 构建消息内容(包含图片)
|
||||
export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
|
||||
if (!textContent && (!imageUrls || imageUrls.length === 0)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
|
||||
|
||||
if (imageEnabled && validImageUrls.length > 0) {
|
||||
return [
|
||||
{ type: 'text', text: textContent || '' },
|
||||
...validImageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url: url.trim() }
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
return textContent || '';
|
||||
};
|
||||
|
||||
// 创建新消息
|
||||
export const createMessage = (role, content, options = {}) => ({
|
||||
role,
|
||||
content,
|
||||
createAt: Date.now(),
|
||||
id: generateMessageId(),
|
||||
...options
|
||||
});
|
||||
|
||||
// 创建加载中的助手消息
|
||||
export const createLoadingAssistantMessage = () => createMessage(
|
||||
MESSAGE_ROLES.ASSISTANT,
|
||||
'',
|
||||
{
|
||||
reasoningContent: '',
|
||||
isReasoningExpanded: true,
|
||||
isThinkingComplete: false,
|
||||
hasAutoCollapsed: false,
|
||||
status: 'loading'
|
||||
}
|
||||
);
|
||||
|
||||
// 检查消息是否包含图片
|
||||
export const hasImageContent = (message) => {
|
||||
return message &&
|
||||
Array.isArray(message.content) &&
|
||||
message.content.some(item => item.type === 'image_url');
|
||||
};
|
||||
|
||||
// 格式化消息用于API请求
|
||||
export const formatMessageForAPI = (message) => {
|
||||
if (!message) return null;
|
||||
|
||||
return {
|
||||
role: message.role,
|
||||
content: message.content
|
||||
};
|
||||
};
|
||||
|
||||
// 验证消息是否有效
|
||||
export const isValidMessage = (message) => {
|
||||
return message &&
|
||||
message.role &&
|
||||
(message.content || message.content === '');
|
||||
};
|
||||
|
||||
// 获取最后一条用户消息
|
||||
export const getLastUserMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.USER) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 获取最后一条助手消息
|
||||
export const getLastAssistantMessage = (messages) => {
|
||||
if (!Array.isArray(messages)) return null;
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
|
||||
return messages[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 构建API请求负载(从apiUtils移动过来)
|
||||
export const buildApiPayload = (messages, systemPrompt, inputs, parameterEnabled) => {
|
||||
const processedMessages = messages
|
||||
.filter(isValidMessage)
|
||||
.map(formatMessageForAPI)
|
||||
.filter(Boolean);
|
||||
|
||||
// 如果有系统提示,插入到消息开头
|
||||
if (systemPrompt && systemPrompt.trim()) {
|
||||
processedMessages.unshift({
|
||||
role: MESSAGE_ROLES.SYSTEM,
|
||||
content: systemPrompt.trim()
|
||||
});
|
||||
}
|
||||
|
||||
const payload = {
|
||||
model: inputs.model,
|
||||
messages: processedMessages,
|
||||
stream: inputs.stream,
|
||||
};
|
||||
|
||||
// 添加启用的参数
|
||||
const parameterMappings = {
|
||||
temperature: 'temperature',
|
||||
top_p: 'top_p',
|
||||
max_tokens: 'max_tokens',
|
||||
frequency_penalty: 'frequency_penalty',
|
||||
presence_penalty: 'presence_penalty',
|
||||
seed: 'seed'
|
||||
};
|
||||
|
||||
Object.entries(parameterMappings).forEach(([key, param]) => {
|
||||
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
||||
payload[param] = inputs[param];
|
||||
}
|
||||
});
|
||||
|
||||
return payload;
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/**
|
||||
* rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
|
||||
* 仅在流式渲染阶段使用,避免已渲染文字重复动画。
|
||||
*/
|
||||
export function rehypeSplitWordsIntoSpans(options = {}) {
|
||||
const { previousContentLength = 0 } = options;
|
||||
|
||||
return (tree) => {
|
||||
let currentCharCount = 0; // 当前已处理的字符数
|
||||
|
||||
visit(tree, 'element', (node) => {
|
||||
if (
|
||||
['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(node.tagName) &&
|
||||
node.children
|
||||
) {
|
||||
const newChildren = [];
|
||||
node.children.forEach((child) => {
|
||||
if (child.type === 'text') {
|
||||
try {
|
||||
// 使用 Intl.Segmenter 精准拆分中英文及标点
|
||||
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' });
|
||||
const segments = segmenter.segment(child.value);
|
||||
|
||||
Array.from(segments)
|
||||
.map((seg) => seg.segment)
|
||||
.filter(Boolean)
|
||||
.forEach((word) => {
|
||||
const wordStartPos = currentCharCount;
|
||||
const wordEndPos = currentCharCount + word.length;
|
||||
|
||||
// 判断这个词是否是新增的(在 previousContentLength 之后)
|
||||
const isNewContent = wordStartPos >= previousContentLength;
|
||||
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: isNewContent ? ['animate-fade-in'] : [],
|
||||
},
|
||||
children: [{ type: 'text', value: word }],
|
||||
});
|
||||
|
||||
currentCharCount = wordEndPos;
|
||||
});
|
||||
} catch (_) {
|
||||
// Fallback:如果浏览器不支持 Segmenter
|
||||
const textStartPos = currentCharCount;
|
||||
const isNewContent = textStartPos >= previousContentLength;
|
||||
|
||||
if (isNewContent) {
|
||||
// 新内容,添加动画
|
||||
newChildren.push({
|
||||
type: 'element',
|
||||
tagName: 'span',
|
||||
properties: {
|
||||
className: ['animate-fade-in'],
|
||||
},
|
||||
children: [{ type: 'text', value: child.value }],
|
||||
});
|
||||
} else {
|
||||
// 旧内容,不添加动画
|
||||
newChildren.push(child);
|
||||
}
|
||||
|
||||
currentCharCount += child.value.length;
|
||||
}
|
||||
} else {
|
||||
newChildren.push(child);
|
||||
}
|
||||
});
|
||||
node.children = newChildren;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user