diff --git a/web/src/images/example.png b/web/public/example.png similarity index 100% rename from web/src/images/example.png rename to web/public/example.png diff --git a/web/src/App.js b/web/src/App.js index 571d0745..93b4e496 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -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 ( - + } /> - + ); } diff --git a/web/src/components/AuthRedirect.js b/web/src/components/AuthRedirect.js deleted file mode 100644 index d22c675d..00000000 --- a/web/src/components/AuthRedirect.js +++ /dev/null @@ -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 ; - } - - return children; -}; - -export default AuthRedirect; \ No newline at end of file diff --git a/web/src/components/PrivateRoute.js b/web/src/components/PrivateRoute.js deleted file mode 100644 index ca938c41..00000000 --- a/web/src/components/PrivateRoute.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Navigate } from 'react-router-dom'; - -import { history } from '../helpers'; - -function PrivateRoute({ children }) { - if (!localStorage.getItem('user')) { - return ; - } - return children; -} - -export { PrivateRoute }; diff --git a/web/src/components/SetupCheck.js b/web/src/components/SetupCheck.js deleted file mode 100644 index 99364b00..00000000 --- a/web/src/components/SetupCheck.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/web/src/components/LoginForm.js b/web/src/components/auth/LoginForm.js similarity index 97% rename from web/src/components/LoginForm.js rename to web/src/components/auth/LoginForm.js index 3f40a472..ec28dba8 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/auth/LoginForm.js @@ -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 ( -
+
{/* 背景图片容器 - 放大并保持居中 */}
{ const [searchParams, setSearchParams] = useSearchParams(); diff --git a/web/src/components/PasswordResetConfirm.js b/web/src/components/auth/PasswordResetConfirm.js similarity index 96% rename from web/src/components/PasswordResetConfirm.js rename to web/src/components/auth/PasswordResetConfirm.js index 7ed089bf..6e9086a9 100644 --- a/web/src/components/PasswordResetConfirm.js +++ b/web/src/components/auth/PasswordResetConfirm.js @@ -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 ( -
+
{/* 背景图片容器 - 放大并保持居中 */}
{ } return ( -
+
{/* 背景图片容器 - 放大并保持居中 */}
{ const { t } = useTranslation(); @@ -549,7 +549,7 @@ const RegisterForm = () => { }; return ( -
+
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 }; -} diff --git a/web/src/components/Footer.js b/web/src/components/layout/Footer.js similarity index 97% rename from web/src/components/Footer.js rename to web/src/components/layout/Footer.js index fbc33158..d549a4e0 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/layout/Footer.js @@ -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(); diff --git a/web/src/components/HeaderBar.js b/web/src/components/layout/HeaderBar.js similarity index 98% rename from web/src/components/HeaderBar.js rename to web/src/components/layout/HeaderBar.js index 2209d0af..c78ba299 100644 --- a/web/src/components/HeaderBar.js +++ b/web/src/components/layout/HeaderBar.js @@ -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(); diff --git a/web/src/components/NoticeModal.js b/web/src/components/layout/NoticeModal.js similarity index 98% rename from web/src/components/NoticeModal.js rename to web/src/components/layout/NoticeModal.js index df45a581..626d8d17 100644 --- a/web/src/components/NoticeModal.js +++ b/web/src/components/layout/NoticeModal.js @@ -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'; diff --git a/web/src/components/PageLayout.js b/web/src/components/layout/PageLayout.js similarity index 93% rename from web/src/components/PageLayout.js rename to web/src/components/layout/PageLayout.js index 6ae50cad..6b0c4c6d 100644 --- a/web/src/components/PageLayout.js +++ b/web/src/components/layout/PageLayout.js @@ -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; diff --git a/web/src/components/SiderBar.js b/web/src/components/layout/SiderBar.js similarity index 95% rename from web/src/components/SiderBar.js rename to web/src/components/layout/SiderBar.js index d4399f92..30117eee 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/layout/SiderBar.js @@ -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'; // 自定义侧边栏按钮样式 diff --git a/web/src/components/playground/CodeViewer.js b/web/src/components/playground/CodeViewer.js index e02b72e2..1ce723ce 100644 --- a/web/src/components/playground/CodeViewer.js +++ b/web/src/components/playground/CodeViewer.js @@ -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, // 最大显示字符数 diff --git a/web/src/components/playground/SettingsPanel.js b/web/src/components/playground/SettingsPanel.js index 4f3ea276..b2e8310a 100644 --- a/web/src/components/playground/SettingsPanel.js +++ b/web/src/components/playground/SettingsPanel.js @@ -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'; diff --git a/web/src/components/playground/configStorage.js b/web/src/components/playground/configStorage.js index a4e068cb..91fda88a 100644 --- a/web/src/components/playground/configStorage.js +++ b/web/src/components/playground/configStorage.js @@ -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'; diff --git a/web/src/components/ModelSetting.js b/web/src/components/settings/ModelSetting.js similarity index 89% rename from web/src/components/ModelSetting.js rename to web/src/components/settings/ModelSetting.js index 9c60a390..1d583634 100644 --- a/web/src/components/ModelSetting.js +++ b/web/src/components/settings/ModelSetting.js @@ -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(); diff --git a/web/src/components/OperationSetting.js b/web/src/components/settings/OperationSetting.js similarity index 82% rename from web/src/components/OperationSetting.js rename to web/src/components/settings/OperationSetting.js index 28fb3a8f..2dc0b88e 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/settings/OperationSetting.js @@ -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(); diff --git a/web/src/components/OtherSetting.js b/web/src/components/settings/OtherSetting.js similarity index 99% rename from web/src/components/OtherSetting.js rename to web/src/components/settings/OtherSetting.js index 570a86f8..a054e0da 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/settings/OtherSetting.js @@ -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 = () => { diff --git a/web/src/components/PersonalSetting.js b/web/src/components/settings/PersonalSetting.js similarity index 99% rename from web/src/components/PersonalSetting.js rename to web/src/components/settings/PersonalSetting.js index 4ecb4788..f8cbc022 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/settings/PersonalSetting.js @@ -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 ( -
+
{/* 划转模态框 */} diff --git a/web/src/components/RateLimitSetting.js b/web/src/components/settings/RateLimitSetting.js similarity index 87% rename from web/src/components/RateLimitSetting.js rename to web/src/components/settings/RateLimitSetting.js index 5f0200e1..4d061363 100644 --- a/web/src/components/RateLimitSetting.js +++ b/web/src/components/settings/RateLimitSetting.js @@ -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(); diff --git a/web/src/components/SystemSetting.js b/web/src/components/settings/SystemSetting.js similarity index 99% rename from web/src/components/SystemSetting.js rename to web/src/components/settings/SystemSetting.js index 53ba675d..8219159b 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/settings/SystemSetting.js @@ -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 = () => { diff --git a/web/src/components/ChannelsTable.js b/web/src/components/table/ChannelsTable.js similarity index 99% rename from web/src/components/ChannelsTable.js rename to web/src/components/table/ChannelsTable.js index 974d8bd8..9081aa63 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -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 = () => { diff --git a/web/src/components/LogsTable.js b/web/src/components/table/LogsTable.js similarity index 99% rename from web/src/components/LogsTable.js rename to web/src/components/table/LogsTable.js index 912b198b..88992452 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/table/LogsTable.js @@ -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, diff --git a/web/src/components/MjLogsTable.js b/web/src/components/table/MjLogsTable.js similarity index 99% rename from web/src/components/MjLogsTable.js rename to web/src/components/table/MjLogsTable.js index 08c47e7d..077512dc 100644 --- a/web/src/components/MjLogsTable.js +++ b/web/src/components/table/MjLogsTable.js @@ -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, diff --git a/web/src/components/ModelPricing.js b/web/src/components/table/ModelPricing.js similarity index 99% rename from web/src/components/ModelPricing.js rename to web/src/components/table/ModelPricing.js index 39daa168..22738127 100644 --- a/web/src/components/ModelPricing.js +++ b/web/src/components/table/ModelPricing.js @@ -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 ( -
+
diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/table/RedemptionsTable.js similarity index 98% rename from web/src/components/RedemptionsTable.js rename to web/src/components/table/RedemptionsTable.js index f9fd344a..23813bbb 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/table/RedemptionsTable.js @@ -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; diff --git a/web/src/components/TaskLogsTable.js b/web/src/components/table/TaskLogsTable.js similarity index 99% rename from web/src/components/TaskLogsTable.js rename to web/src/components/table/TaskLogsTable.js index c4a8414b..791ae9ce 100644 --- a/web/src/components/TaskLogsTable.js +++ b/web/src/components/table/TaskLogsTable.js @@ -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, diff --git a/web/src/components/TokensTable.js b/web/src/components/table/TokensTable.js similarity index 98% rename from web/src/components/TokensTable.js rename to web/src/components/table/TokensTable.js index 92b74550..d4637953 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -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)}; diff --git a/web/src/components/UsersTable.js b/web/src/components/table/UsersTable.js similarity index 98% rename from web/src/components/UsersTable.js rename to web/src/components/table/UsersTable.js index e5085212..b5e5f7da 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/table/UsersTable.js @@ -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; diff --git a/web/src/components/utils.js b/web/src/components/utils.js deleted file mode 100644 index 93a5fb85..00000000 --- a/web/src/components/utils.js +++ /dev/null @@ -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 []; -} diff --git a/web/src/constants/index.js b/web/src/constants/index.js index 3f8c0232..9538a7dc 100644 --- a/web/src/constants/index.js +++ b/web/src/constants/index.js @@ -3,3 +3,4 @@ export * from './user.constants'; export * from './toast.constants'; export * from './common.constant'; export * from './model.constants'; +export * from './playground.constants'; diff --git a/web/src/utils/constants.js b/web/src/constants/playground.constants.js similarity index 100% rename from web/src/utils/constants.js rename to web/src/constants/playground.constants.js diff --git a/web/src/context/Style/index.js b/web/src/context/Style/index.js index 41d4633a..7bfe0ef7 100644 --- a/web/src/context/Style/index.js +++ b/web/src/context/Style/index.js @@ -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 = { diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index 84d2df1f..eb70cd27 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -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 []; +} diff --git a/web/src/helpers/auth-header.js b/web/src/helpers/auth-header.js deleted file mode 100644 index f094dd1b..00000000 --- a/web/src/helpers/auth-header.js +++ /dev/null @@ -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 {}; - } -} diff --git a/web/src/helpers/auth.js b/web/src/helpers/auth.js new file mode 100644 index 00000000..cb694ccf --- /dev/null +++ b/web/src/helpers/auth.js @@ -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 ; + } + + return children; +}; + +function PrivateRoute({ children }) { + if (!localStorage.getItem('user')) { + return ; + } + return children; +} + +export { PrivateRoute }; diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js index ac164960..1524afbe 100644 --- a/web/src/helpers/index.js +++ b/web/src/helpers/index.js @@ -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'; diff --git a/web/src/helpers/other.js b/web/src/helpers/log.js similarity index 98% rename from web/src/helpers/other.js rename to web/src/helpers/log.js index c5d8c269..ffbe0d74 100644 --- a/web/src/helpers/other.js +++ b/web/src/helpers/log.js @@ -4,4 +4,4 @@ export function getLogOther(otherStr) { } let other = JSON.parse(otherStr); return other; -} +} \ No newline at end of file diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index fa1de023..c8302feb 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -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(

{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), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

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

{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), + }, + )}

{i18next.t( @@ -1024,33 +1025,33 @@ export function renderClaudeModelPrice(

{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), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -1128,3 +1129,79 @@ export function renderClaudeModelPriceSimple( } } } + +/** + * rehype 插件:将段落等文本节点拆分为逐词 ,并添加淡入动画 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; + } + }); + }; +} \ No newline at end of file diff --git a/web/src/helpers/token.js b/web/src/helpers/token.js new file mode 100644 index 00000000..ecdeaa3a --- /dev/null +++ b/web/src/helpers/token.js @@ -0,0 +1,45 @@ +import { API } from './api'; + +/** + * 获取可用的token keys + * @returns {Promise} 返回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; +} \ No newline at end of file diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index 35f20c89..cd05653e 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -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
; @@ -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('')) { + 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(''); + if (lastOpenThinkIndex === -1) { + return processThinkTags(content, reasoningContent); + } + + const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex); + if (!fragmentAfterLastOpen.includes('')) { + const unclosedThought = fragmentAfterLastOpen.substring(''.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; +}; diff --git a/web/src/hooks/useApiRequest.js b/web/src/hooks/useApiRequest.js index 55688726..4a3b7c3a 100644 --- a/web/src/hooks/useApiRequest.js +++ b/web/src/hooks/useApiRequest.js @@ -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, diff --git a/web/src/hooks/useDataLoader.js b/web/src/hooks/useDataLoader.js index 2454b007..83d53199 100644 --- a/web/src/hooks/useDataLoader.js +++ b/web/src/hooks/useDataLoader.js @@ -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, diff --git a/web/src/hooks/useMessageActions.js b/web/src/hooks/useMessageActions.js index 84d94d4d..4cfcf9f1 100644 --- a/web/src/hooks/useMessageActions.js +++ b/web/src/hooks/useMessageActions.js @@ -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(); diff --git a/web/src/hooks/useMessageEdit.js b/web/src/hooks/useMessageEdit.js index 513f2b32..479524b6 100644 --- a/web/src/hooks/useMessageEdit.js +++ b/web/src/hooks/useMessageEdit.js @@ -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, diff --git a/web/src/hooks/usePlaygroundState.js b/web/src/hooks/usePlaygroundState.js index 9e76aa2e..e8c4727d 100644 --- a/web/src/hooks/usePlaygroundState.js +++ b/web/src/hooks/usePlaygroundState.js @@ -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 = () => { // 使用惰性初始化,确保只在组件首次挂载时加载配置和消息 diff --git a/web/src/hooks/useSetupCheck.js b/web/src/hooks/useSetupCheck.js new file mode 100644 index 00000000..d2233de8 --- /dev/null +++ b/web/src/hooks/useSetupCheck.js @@ -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 + }; +} \ No newline at end of file diff --git a/web/src/hooks/useSyncMessageAndCustomBody.js b/web/src/hooks/useSyncMessageAndCustomBody.js index 36e5fc67..6f0c19ad 100644 --- a/web/src/hooks/useSyncMessageAndCustomBody.js +++ b/web/src/hooks/useSyncMessageAndCustomBody.js @@ -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, diff --git a/web/src/hooks/useTokenKeys.js b/web/src/hooks/useTokenKeys.js new file mode 100644 index 00000000..a6583591 --- /dev/null +++ b/web/src/hooks/useTokenKeys.js @@ -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 }; +} \ No newline at end of file diff --git a/web/src/index.js b/web/src/index.js index 1f180bbd..0f57f5a1 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -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 diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index 810df9dc..feed8beb 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -26,7 +26,7 @@ import { Card, Tag, } from '@douyinfe/semi-ui'; -import { getChannelModels } from '../../components/utils.js'; +import { getChannelModels } from '../../helpers'; import { IconSave, IconClose, diff --git a/web/src/pages/Channel/EditTagModal.js b/web/src/pages/Channel/EditTagModal.js index 5ce05bec..52dd4bbb 100644 --- a/web/src/pages/Channel/EditTagModal.js +++ b/web/src/pages/Channel/EditTagModal.js @@ -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; diff --git a/web/src/pages/Channel/index.js b/web/src/pages/Channel/index.js index 523ea27c..dc9f8c48 100644 --- a/web/src/pages/Channel/index.js +++ b/web/src/pages/Channel/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import ChannelsTable from '../../components/ChannelsTable'; +import ChannelsTable from '../../components/table/ChannelsTable'; const File = () => { return ( diff --git a/web/src/pages/Chat/index.js b/web/src/pages/Chat/index.js index 77ece906..bd4e19a1 100644 --- a/web/src/pages/Chat/index.js +++ b/web/src/pages/Chat/index.js @@ -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'; diff --git a/web/src/pages/Chat2Link/index.js b/web/src/pages/Chat2Link/index.js index a7b5e6ae..553dab3f 100644 --- a/web/src/pages/Chat2Link/index.js +++ b/web/src/pages/Chat2Link/index.js @@ -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(); diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index 44cbd857..8dfdbe97 100644 --- a/web/src/pages/Detail/index.js +++ b/web/src/pages/Detail/index.js @@ -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'; diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index f676fe69..4e8a61d6 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -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; diff --git a/web/src/pages/Log/index.js b/web/src/pages/Log/index.js index 4ee109ca..4b6f8ba3 100644 --- a/web/src/pages/Log/index.js +++ b/web/src/pages/Log/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import LogsTable from '../../components/LogsTable'; +import LogsTable from '../../components/table/LogsTable'; const Token = () => ( <> diff --git a/web/src/pages/Midjourney/index.js b/web/src/pages/Midjourney/index.js index ed22ecd0..7c605bd4 100644 --- a/web/src/pages/Midjourney/index.js +++ b/web/src/pages/Midjourney/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import MjLogsTable from '../../components/MjLogsTable'; +import MjLogsTable from '../../components/table/MjLogsTable'; const Midjourney = () => ( <> diff --git a/web/src/pages/Playground/index.js b/web/src/pages/Playground/index.js index 3b890259..8940dee3 100644 --- a/web/src/pages/Playground/index.js +++ b/web/src/pages/Playground/index.js @@ -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 { diff --git a/web/src/pages/Pricing/index.js b/web/src/pages/Pricing/index.js index cb56a477..21fd7256 100644 --- a/web/src/pages/Pricing/index.js +++ b/web/src/pages/Pricing/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import ModelPricing from '../../components/ModelPricing.js'; +import ModelPricing from '../../components/table/ModelPricing.js'; const Pricing = () => ( <> diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js index 1529a1cd..e720f90c 100644 --- a/web/src/pages/Redemption/EditRedemption.js +++ b/web/src/pages/Redemption/EditRedemption.js @@ -6,11 +6,9 @@ import { isMobile, showError, showSuccess, -} from '../../helpers'; -import { renderQuota, - renderQuotaWithPrompt, -} from '../../helpers/render'; + renderQuotaWithPrompt +} from '../../helpers'; import { AutoComplete, Button, diff --git a/web/src/pages/Redemption/index.js b/web/src/pages/Redemption/index.js index f877b601..0154d979 100644 --- a/web/src/pages/Redemption/index.js +++ b/web/src/pages/Redemption/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import RedemptionsTable from '../../components/RedemptionsTable'; +import RedemptionsTable from '../../components/table/RedemptionsTable'; const Redemption = () => { return ( diff --git a/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js b/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js index 496208f4..d5d8d832 100644 --- a/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js +++ b/web/src/pages/Setting/Operation/ModelRationNotSetEditor.js @@ -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) { diff --git a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js b/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js index 409aa13f..85426a5e 100644 --- a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js +++ b/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js @@ -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) { model.name === currentModel.name) + currentModel.name && + models.some((model) => model.name === currentModel.name) ? t('编辑模型') : t('添加模型') } diff --git a/web/src/pages/Setting/index.js b/web/src/pages/Setting/index.js index 0619ab9a..056fc207 100644 --- a/web/src/pages/Setting/index.js +++ b/web/src/pages/Setting/index.js @@ -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(); diff --git a/web/src/pages/Task/index.js b/web/src/pages/Task/index.js index 47b541b6..05326719 100644 --- a/web/src/pages/Task/index.js +++ b/web/src/pages/Task/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import TaskLogsTable from '../../components/TaskLogsTable.js'; +import TaskLogsTable from '../../components/table/TaskLogsTable.js'; const Task = () => ( <> diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index ad8c11c8..946164da 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -6,8 +6,9 @@ import { showError, showSuccess, timestamp2string, + renderGroupOption, + renderQuotaWithPrompt } from '../../helpers'; -import { renderGroupOption, renderQuotaWithPrompt } from '../../helpers/render'; import { AutoComplete, Banner, diff --git a/web/src/pages/Token/index.js b/web/src/pages/Token/index.js index eea68941..d63247a4 100644 --- a/web/src/pages/Token/index.js +++ b/web/src/pages/Token/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import TokensTable from '../../components/TokensTable'; +import TokensTable from '../../components/table/TokensTable'; const Token = () => { return ( diff --git a/web/src/pages/TopUp/index.js b/web/src/pages/TopUp/index.js index d2e16cfc..2f920647 100644 --- a/web/src/pages/TopUp/index.js +++ b/web/src/pages/TopUp/index.js @@ -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 ( -
+
{ return ( diff --git a/web/src/utils/apiUtils.js b/web/src/utils/apiUtils.js deleted file mode 100644 index 77f775fd..00000000 --- a/web/src/utils/apiUtils.js +++ /dev/null @@ -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; -}; \ No newline at end of file diff --git a/web/src/utils/messageUtils.js b/web/src/utils/messageUtils.js deleted file mode 100644 index 0ab23315..00000000 --- a/web/src/utils/messageUtils.js +++ /dev/null @@ -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('')) { - 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(''); - if (lastOpenThinkIndex === -1) { - return processThinkTags(content, reasoningContent); - } - - const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex); - if (!fragmentAfterLastOpen.includes('')) { - const unclosedThought = fragmentAfterLastOpen.substring(''.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; -}; \ No newline at end of file diff --git a/web/src/utils/rehypeSplitWordsIntoSpans.js b/web/src/utils/rehypeSplitWordsIntoSpans.js deleted file mode 100644 index a9bb6db3..00000000 --- a/web/src/utils/rehypeSplitWordsIntoSpans.js +++ /dev/null @@ -1,77 +0,0 @@ -import { visit } from 'unist-util-visit'; - -/** - * rehype 插件:将段落等文本节点拆分为逐词 ,并添加淡入动画 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; - } - }); - }; -} \ No newline at end of file