Merge branch 'ui/refactor' into alpha

This commit is contained in:
Apple\Apple
2025-06-04 01:43:08 +08:00
81 changed files with 879 additions and 934 deletions

View File

Before

Width:  |  Height:  |  Size: 550 KiB

After

Width:  |  Height:  |  Size: 550 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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';
// 自定义侧边栏按钮样式

View File

@@ -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, // 最大显示字符数

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = () => {

View File

@@ -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>
{/* 划转模态框 */}

View File

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

View File

@@ -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 = () => {

View File

@@ -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 = () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,3 +3,4 @@ export * from './user.constants';
export * from './toast.constants';
export * from './common.constant';
export * from './model.constants';
export * from './playground.constants';

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,4 @@ export function getLogOther(otherStr) {
}
let other = JSON.parse(otherStr);
return other;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = () => {
// 使用惰性初始化,确保只在组件首次挂载时加载配置和消息

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

View File

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

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

View File

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

View File

@@ -26,7 +26,7 @@ import {
Card,
Tag,
} from '@douyinfe/semi-ui';
import { getChannelModels } from '../../components/utils.js';
import { getChannelModels } from '../../helpers';
import {
IconSave,
IconClose,

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ChannelsTable from '../../components/ChannelsTable';
import ChannelsTable from '../../components/table/ChannelsTable';
const File = () => {
return (

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import LogsTable from '../../components/LogsTable';
import LogsTable from '../../components/table/LogsTable';
const Token = () => (
<>

View File

@@ -1,5 +1,5 @@
import React from 'react';
import MjLogsTable from '../../components/MjLogsTable';
import MjLogsTable from '../../components/table/MjLogsTable';
const Midjourney = () => (
<>

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import ModelPricing from '../../components/ModelPricing.js';
import ModelPricing from '../../components/table/ModelPricing.js';
const Pricing = () => (
<>

View File

@@ -6,11 +6,9 @@ import {
isMobile,
showError,
showSuccess,
} from '../../helpers';
import {
renderQuota,
renderQuotaWithPrompt,
} from '../../helpers/render';
renderQuotaWithPrompt
} from '../../helpers';
import {
AutoComplete,
Button,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable';
import RedemptionsTable from '../../components/table/RedemptionsTable';
const Redemption = () => {
return (

View File

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

View File

@@ -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('添加模型')
}

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import TaskLogsTable from '../../components/TaskLogsTable.js';
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
const Task = () => (
<>

View File

@@ -6,8 +6,9 @@ import {
showError,
showSuccess,
timestamp2string,
renderGroupOption,
renderQuotaWithPrompt
} from '../../helpers';
import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render';
import {
AutoComplete,
Banner,

View File

@@ -1,5 +1,5 @@
import React from 'react';
import TokensTable from '../../components/TokensTable';
import TokensTable from '../../components/table/TokensTable';
const Token = () => {
return (

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import React from 'react';
import UsersTable from '../../components/UsersTable';
import UsersTable from '../../components/table/UsersTable';
const User = () => {
return (

View File

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

View File

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

View File

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