diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index 5e88871d2..b1afd96f5 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -1,34 +1,42 @@ module.exports = { root: true, env: { browser: true, es2021: true, node: true }, - parserOptions: { ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true } }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, plugins: ['header', 'react-hooks'], overrides: [ { files: ['**/*.{js,jsx}'], rules: { - 'header/header': [2, 'block', [ - '', - 'Copyright (C) 2025 QuantumNous', - '', - 'This program is free software: you can redistribute it and/or modify', - 'it under the terms of the GNU Affero General Public License as', - 'published by the Free Software Foundation, either version 3 of the', - 'License, or (at your option) any later version.', - '', - 'This program is distributed in the hope that it will be useful,', - 'but WITHOUT ANY WARRANTY; without even the implied warranty of', - 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the', - 'GNU Affero General Public License for more details.', - '', - 'You should have received a copy of the GNU Affero General Public License', - 'along with this program. If not, see .', - '', - 'For commercial licensing, please contact support@quantumnous.com', - '' - ]], - 'no-multiple-empty-lines': ['error', { max: 1 }] - } - } - ] -}; \ No newline at end of file + 'header/header': [ + 2, + 'block', + [ + '', + 'Copyright (C) 2025 QuantumNous', + '', + 'This program is free software: you can redistribute it and/or modify', + 'it under the terms of the GNU Affero General Public License as', + 'published by the Free Software Foundation, either version 3 of the', + 'License, or (at your option) any later version.', + '', + 'This program is distributed in the hope that it will be useful,', + 'but WITHOUT ANY WARRANTY; without even the implied warranty of', + 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the', + 'GNU Affero General Public License for more details.', + '', + 'You should have received a copy of the GNU Affero General Public License', + 'along with this program. If not, see .', + '', + 'For commercial licensing, please contact support@quantumnous.com', + '', + ], + ], + 'no-multiple-empty-lines': ['error', { max: 1 }], + }, + }, + ], +}; diff --git a/web/index.html b/web/index.html index 8528f7fa7..09d87ae1a 100644 --- a/web/index.html +++ b/web/index.html @@ -1,19 +1,20 @@ + + + + + + + New API + - - - - - - - New API - - - - -
- - - - \ No newline at end of file + + +
+ + + diff --git a/web/postcss.config.js b/web/postcss.config.js index 590e21a49..5731ce76e 100644 --- a/web/postcss.config.js +++ b/web/postcss.config.js @@ -22,4 +22,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/web/src/App.jsx b/web/src/App.jsx index fc623309c..e3bd7db85 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -73,10 +73,7 @@ function App() { } /> - } - /> + } /> { const [emailLoginLoading, setEmailLoginLoading] = useState(false); const [loginLoading, setLoginLoading] = useState(false); const [resetPasswordLoading, setResetPasswordLoading] = useState(false); - const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = useState(false); + const [otherLoginOptionsLoading, setOtherLoginOptionsLoading] = + useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [showTwoFA, setShowTwoFA] = useState(false); @@ -247,10 +241,7 @@ const LoginForm = () => { const handleOIDCClick = () => { setOidcLoading(true); try { - onOIDCClicked( - status.oidc_authorization_endpoint, - status.oidc_client_id - ); + onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); } finally { // 由于重定向,这里不会执行到,但为了完整性添加 setTimeout(() => setOidcLoading(false), 3000); @@ -306,73 +297,87 @@ const LoginForm = () => { const renderOAuthOptions = () => { return ( -
-
-
- Logo - {systemName} +
+
+
+ Logo + + {systemName} +
- -
- {t('登 录')} + +
+ + {t('登 录')} +
-
-
+
+
{status.wechat_login && ( )} {status.github_oauth && ( )} {status.oidc_enabled && ( )} {status.linuxdo_oauth && ( )} {status.telegram_oauth && ( -
+
{
{!status.self_use_mode_enabled && ( -
+
{t('没有账户?')}{' '} {t('注册')} @@ -418,44 +423,46 @@ const LoginForm = () => { const renderEmailLoginForm = () => { return ( -
-
-
- Logo +
+
+
+ Logo {systemName}
- -
- {t('登 录')} + +
+ + {t('登 录')} +
-
-
+
+ handleChange('username', value)} prefix={} /> handleChange('password', value)} prefix={} /> -
+
- {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && ( + {(status.github_oauth || + status.oidc_enabled || + status.wechat_login || + status.linuxdo_oauth || + status.telegram_oauth) && ( <> {t('或')} -
+
-
- {t('返回登录')} +
+ + + {t('返回登录')} + +
diff --git a/web/src/components/auth/PasswordResetForm.jsx b/web/src/components/auth/PasswordResetForm.jsx index 93bedae20..92afc2afa 100644 --- a/web/src/components/auth/PasswordResetForm.jsx +++ b/web/src/components/auth/PasswordResetForm.jsx @@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com */ 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'; @@ -97,57 +104,77 @@ const PasswordResetForm = () => { } return ( -
+
{/* 背景模糊晕染球 */} -
-
-
-
-
-
- Logo - {systemName} +
+
+
+
+
+
+ Logo + + {systemName} +
- -
- {t('密码重置')} + +
+ + {t('密码重置')} +
-
-
+
+ } /> -
+
-
- {t('想起来了?')} {t('登录')} +
+ + {t('想起来了?')}{' '} + + {t('登录')} + +
{turnstileEnabled && ( -
+
{ diff --git a/web/src/components/auth/RegisterForm.jsx b/web/src/components/auth/RegisterForm.jsx index 0b95d504f..9c98bdc3a 100644 --- a/web/src/components/auth/RegisterForm.jsx +++ b/web/src/components/auth/RegisterForm.jsx @@ -27,20 +27,19 @@ import { showSuccess, updateAPI, getSystemName, - setUserData + setUserData, } from '../../helpers'; import Turnstile from 'react-turnstile'; -import { - Button, - Card, - Divider, - Form, - Icon, - Modal, -} from '@douyinfe/semi-ui'; +import { Button, Card, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; -import { IconGithubLogo, IconMail, IconUser, IconLock, IconKey } from '@douyinfe/semi-icons'; +import { + IconGithubLogo, + IconMail, + IconUser, + IconLock, + IconKey, +} from '@douyinfe/semi-icons'; import { onGitHubOAuthClicked, onLinuxDOOAuthClicked, @@ -78,7 +77,8 @@ const RegisterForm = () => { const [emailRegisterLoading, setEmailRegisterLoading] = useState(false); const [registerLoading, setRegisterLoading] = useState(false); const [verificationCodeLoading, setVerificationCodeLoading] = useState(false); - const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = useState(false); + const [otherRegisterOptionsLoading, setOtherRegisterOptionsLoading] = + useState(false); const [wechatCodeSubmitLoading, setWechatCodeSubmitLoading] = useState(false); const [disableButton, setDisableButton] = useState(false); const [countdown, setCountdown] = useState(30); @@ -236,10 +236,7 @@ const RegisterForm = () => { const handleOIDCClick = () => { setOidcLoading(true); try { - onOIDCClicked( - status.oidc_authorization_endpoint, - status.oidc_client_id - ); + onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id); } finally { setTimeout(() => setOidcLoading(false), 3000); } @@ -303,73 +300,87 @@ const RegisterForm = () => { const renderOAuthOptions = () => { return ( -
-
-
- Logo - {systemName} +
+
+
+ Logo + + {systemName} +
- -
- {t('注 册')} + +
+ + {t('注 册')} +
-
-
+
+
{status.wechat_login && ( )} {status.github_oauth && ( )} {status.oidc_enabled && ( )} {status.linuxdo_oauth && ( )} {status.telegram_oauth && ( -
+
{
-
- {t('已有账户?')} {t('登录')} +
+ + {t('已有账户?')}{' '} + + {t('登录')} + +
@@ -405,44 +424,48 @@ const RegisterForm = () => { const renderEmailRegisterForm = () => { return ( -
-
-
- Logo - {systemName} +
+
+
+ Logo + + {systemName} +
- -
- {t('注 册')} + +
+ + {t('注 册')} +
-
-
+
+ handleChange('username', value)} prefix={} /> handleChange('password', value)} prefix={} /> handleChange('password2', value)} prefix={} /> @@ -450,11 +473,11 @@ const RegisterForm = () => { {showEmailVerification && ( <> handleChange('email', value)} prefix={} suffix={ @@ -463,27 +486,31 @@ const RegisterForm = () => { loading={verificationCodeLoading} disabled={disableButton || verificationCodeLoading} > - {disableButton ? `${t('重新发送')} (${countdown})` : t('获取验证码')} + {disableButton + ? `${t('重新发送')} (${countdown})` + : t('获取验证码')} } /> handleChange('verification_code', value)} + name='verification_code' + onChange={(value) => + handleChange('verification_code', value) + } prefix={} /> )} -
+
- {(status.github_oauth || status.oidc_enabled || status.wechat_login || status.linuxdo_oauth || status.telegram_oauth) && ( + {(status.github_oauth || + status.oidc_enabled || + status.wechat_login || + status.linuxdo_oauth || + status.telegram_oauth) && ( <> {t('或')} -
+
-
- +
+ 提示:
• 验证码每30秒更新一次
• 如果无法获取验证码,请使用备用码 -
- • 每个备用码只能使用一次 +
• 每个备用码只能使用一次
@@ -145,39 +151,41 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => { } return ( -
+
两步验证 - + 请输入认证器应用显示的验证码完成登录
-
- +
+ 提示:
• 验证码每30秒更新一次
• 如果无法获取验证码,请使用备用码 -
- • 每个备用码只能使用一次 +
• 每个备用码只能使用一次
@@ -227,4 +241,4 @@ const TwoFAVerification = ({ onSuccess, onBack, isModal = false }) => { ); }; -export default TwoFAVerification; \ No newline at end of file +export default TwoFAVerification; diff --git a/web/src/components/common/markdown/MarkdownRenderer.jsx b/web/src/components/common/markdown/MarkdownRenderer.jsx index 820f2bbf6..f1283a640 100644 --- a/web/src/components/common/markdown/MarkdownRenderer.jsx +++ b/web/src/components/common/markdown/MarkdownRenderer.jsx @@ -160,7 +160,7 @@ export function PreCode(props) { }} >
@@ -367,7 +374,16 @@ function _MarkdownContent(props) { components={{ pre: PreCode, code: CustomCode, - p: (pProps) =>

, + p: (pProps) => ( +

+ ), a: (aProps) => { const href = aProps.href || ''; if (/\.(aac|mp3|opus|wav)$/.test(href)) { @@ -379,13 +395,16 @@ function _MarkdownContent(props) { } if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) { return ( -

, - h2: (props) =>

, - h3: (props) =>

, - h4: (props) =>

, - h5: (props) =>

, - h6: (props) =>
, + h1: (props) => ( +

+ ), + h2: (props) => ( +

+ ), + h3: (props) => ( +

+ ), + h4: (props) => ( +

+ ), + h5: (props) => ( +

+ ), + h6: (props) => ( +
+ ), blockquote: (props) => (
), - ul: (props) =>
    , - ol: (props) =>
      , - li: (props) =>
    1. , + ul: (props) => ( +
        + ), + ol: (props) => ( +
          + ), + li: (props) => ( +
        1. + ), table: (props) => (
          @@ -496,25 +614,29 @@ export function MarkdownRenderer(props) { color: 'var(--semi-color-text-0)', ...style, }} - dir="auto" + dir='auto' {...otherProps} > {loading ? ( -
          -
          +
          +
          正在渲染...
          ) : ( @@ -529,4 +651,4 @@ export function MarkdownRenderer(props) { ); } -export default MarkdownRenderer; \ No newline at end of file +export default MarkdownRenderer; diff --git a/web/src/components/common/markdown/markdown.css b/web/src/components/common/markdown/markdown.css index 3b5c1067d..e1e9e9cb4 100644 --- a/web/src/components/common/markdown/markdown.css +++ b/web/src/components/common/markdown/markdown.css @@ -59,12 +59,12 @@ } .user-message a { - color: #87CEEB !important; + color: #87ceeb !important; /* 浅蓝色链接 */ } .user-message a:hover { - color: #B0E0E6 !important; + color: #b0e0e6 !important; /* hover时更浅的蓝色 */ } @@ -298,7 +298,12 @@ pre:hover .copy-code-button { .markdown-body hr { border: none; height: 1px; - background: linear-gradient(to right, transparent, var(--semi-color-border), transparent); + background: linear-gradient( + to right, + transparent, + var(--semi-color-border), + transparent + ); margin: 24px 0; } @@ -332,7 +337,7 @@ pre:hover .copy-code-button { } /* 任务列表样式 */ -.markdown-body input[type="checkbox"] { +.markdown-body input[type='checkbox'] { margin-right: 8px; transform: scale(1.1); } @@ -441,4 +446,4 @@ pre:hover .copy-code-button { .animate-fade-in { animation: fade-in 0.6s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: opacity, transform; -} \ No newline at end of file +} diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx index b22719682..b0fc28e2a 100644 --- a/web/src/components/common/modals/TwoFactorAuthModal.jsx +++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx @@ -43,7 +43,7 @@ const TwoFactorAuthModal = ({ onCancel, title, description, - placeholder + placeholder, }) => { const { t } = useTranslation(); @@ -56,10 +56,18 @@ const TwoFactorAuthModal = ({ return ( -
          - - +
          +
          + +
          {title || t('安全验证')} @@ -69,11 +77,9 @@ const TwoFactorAuthModal = ({ onCancel={onCancel} footer={ <> - +
          ); @@ -214,4 +197,4 @@ CardPro.propTypes = { t: PropTypes.func, }; -export default CardPro; \ No newline at end of file +export default CardPro; diff --git a/web/src/components/common/ui/CardTable.jsx b/web/src/components/common/ui/CardTable.jsx index f7f443dbd..8a331d07e 100644 --- a/web/src/components/common/ui/CardTable.jsx +++ b/web/src/components/common/ui/CardTable.jsx @@ -19,7 +19,15 @@ For commercial licensing, please contact support@quantumnous.com import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { Table, Card, Skeleton, Pagination, Empty, Button, Collapsible } from '@douyinfe/semi-ui'; +import { + Table, + Card, + Skeleton, + Pagination, + Empty, + Button, + Collapsible, +} from '@douyinfe/semi-ui'; import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; import PropTypes from 'prop-types'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; @@ -27,7 +35,7 @@ import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTi /** * CardTable 响应式表格组件 - * + * * 在桌面端渲染 Semi-UI 的 Table 组件,在移动端则将每一行数据渲染成 Card 形式。 * 该组件与 Table 组件的大部分 API 保持一致,只需将原 Table 换成 CardTable 即可。 */ @@ -75,18 +83,22 @@ const CardTable = ({ const renderSkeletonCard = (key) => { const placeholder = ( -
          +
          {visibleCols.map((col, idx) => { if (!col.title) { return ( -
          +
          ); } return ( -
          +
          + ); }; return ( -
          +
          {[1, 2, 3].map((i) => renderSkeletonCard(i))}
          ); @@ -127,9 +139,12 @@ const CardTable = ({ (!tableProps.rowExpandable || tableProps.rowExpandable(record)); return ( - + {columns.map((col, colIdx) => { - if (tableProps?.visibleColumns && !tableProps.visibleColumns[col.key]) { + if ( + tableProps?.visibleColumns && + !tableProps.visibleColumns[col.key] + ) { return null; } @@ -140,7 +155,7 @@ const CardTable = ({ if (!title) { return ( -
          +
          {cellContent}
          ); @@ -149,14 +164,16 @@ const CardTable = ({ return (
          - + {title} -
          - {cellContent !== undefined && cellContent !== null ? cellContent : '-'} +
          + {cellContent !== undefined && cellContent !== null + ? cellContent + : '-'}
          ); @@ -177,7 +194,7 @@ const CardTable = ({ {showDetails ? t('收起') : t('详情')} -
          +
          {tableProps.expandedRowRender(record, index)}
          @@ -190,19 +207,23 @@ const CardTable = ({ if (isEmpty) { if (tableProps.empty) return tableProps.empty; return ( -
          - +
          +
          ); } return ( -
          +
          {dataSource.map((record, index) => ( - + ))} {!hidePagination && tableProps.pagination && dataSource.length > 0 && ( -
          +
          )} @@ -218,4 +239,4 @@ CardTable.propTypes = { hidePagination: PropTypes.bool, }; -export default CardTable; \ No newline at end of file +export default CardTable; diff --git a/web/src/components/common/ui/ChannelKeyDisplay.jsx b/web/src/components/common/ui/ChannelKeyDisplay.jsx index 8c6e79efb..79aa3eec7 100644 --- a/web/src/components/common/ui/ChannelKeyDisplay.jsx +++ b/web/src/components/common/ui/ChannelKeyDisplay.jsx @@ -30,9 +30,9 @@ import { copy, showSuccess } from '../../../helpers'; */ const parseChannelKeys = (keyData, t) => { if (!keyData) return []; - + const trimmed = keyData.trim(); - + // 检查是否是JSON数组格式(如Vertex AI) if (trimmed.startsWith('[')) { try { @@ -40,9 +40,10 @@ const parseChannelKeys = (keyData, t) => { if (Array.isArray(parsed)) { return parsed.map((item, index) => ({ id: index, - content: typeof item === 'string' ? item : JSON.stringify(item, null, 2), + content: + typeof item === 'string' ? item : JSON.stringify(item, null, 2), type: typeof item === 'string' ? 'text' : 'json', - label: `${t('密钥')} ${index + 1}` + label: `${t('密钥')} ${index + 1}`, })); } } catch (e) { @@ -50,25 +51,27 @@ const parseChannelKeys = (keyData, t) => { console.warn('Failed to parse JSON keys:', e); } } - + // 检查是否是多行密钥(按换行符分割) - const lines = trimmed.split('\n').filter(line => line.trim()); + const lines = trimmed.split('\n').filter((line) => line.trim()); if (lines.length > 1) { return lines.map((line, index) => ({ id: index, content: line.trim(), type: 'text', - label: `${t('密钥')} ${index + 1}` + label: `${t('密钥')} ${index + 1}`, })); } - + // 单个密钥 - return [{ - id: 0, - content: trimmed, - type: trimmed.startsWith('{') ? 'json' : 'text', - label: t('密钥') - }]; + return [ + { + id: 0, + content: trimmed, + type: trimmed.startsWith('{') ? 'json' : 'text', + label: t('密钥'), + }, + ]; }; /** @@ -85,7 +88,7 @@ const ChannelKeyDisplay = ({ showSuccessIcon = true, successText, showWarning = true, - warningText + warningText, }) => { const { t } = useTranslation(); @@ -103,34 +106,42 @@ const ChannelKeyDisplay = ({ }; return ( -
          +
          {/* 成功状态 */} {showSuccessIcon && ( -
          - - +
          + + - + {successText || t('验证成功')}
          )} {/* 密钥内容 */} -
          -
          +
          +
          {isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')} {isMultipleKeys && ( -
          - +
          + {t('共 {{count}} 个密钥', { count: parsedKeys.length })}
          )}
          - -
          + +
          {parsedKeys.map((keyItem) => ( - -
          -
          - + +
          +
          + {keyItem.label} -
          +
          {keyItem.type === 'json' && ( - {t('JSON')} + + {t('JSON')} + )}
          - -
          + +
          {keyItem.content}
          - + {keyItem.type === 'json' && ( - + {t('JSON格式密钥,请确保格式正确')} )} @@ -186,14 +214,28 @@ const ChannelKeyDisplay = ({ ))}
          - + {isMultipleKeys && ( -
          - - - +
          + + + - {t('检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。')} + {t( + '检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。', + )}
          )} @@ -201,17 +243,31 @@ const ChannelKeyDisplay = ({ {/* 安全警告 */} {showWarning && ( -
          -
          - - +
          +
          + +
          - + {t('安全提醒')} - - {warningText || t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')} + + {warningText || + t( + '请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。', + )}
          @@ -221,4 +277,4 @@ const ChannelKeyDisplay = ({ ); }; -export default ChannelKeyDisplay; \ No newline at end of file +export default ChannelKeyDisplay; diff --git a/web/src/components/common/ui/CompactModeToggle.jsx b/web/src/components/common/ui/CompactModeToggle.jsx index 631156ee1..40da0abc0 100644 --- a/web/src/components/common/ui/CompactModeToggle.jsx +++ b/web/src/components/common/ui/CompactModeToggle.jsx @@ -65,4 +65,4 @@ CompactModeToggle.propTypes = { className: PropTypes.string, }; -export default CompactModeToggle; \ No newline at end of file +export default CompactModeToggle; diff --git a/web/src/components/common/ui/JSONEditor.jsx b/web/src/components/common/ui/JSONEditor.jsx index 4acbe270f..7acdc2e37 100644 --- a/web/src/components/common/ui/JSONEditor.jsx +++ b/web/src/components/common/ui/JSONEditor.jsx @@ -36,11 +36,7 @@ import { Divider, Tooltip, } from '@douyinfe/semi-ui'; -import { - IconPlus, - IconDelete, - IconAlertTriangle, -} from '@douyinfe/semi-icons'; +import { IconPlus, IconDelete, IconAlertTriangle } from '@douyinfe/semi-icons'; const { Text } = Typography; @@ -88,7 +84,7 @@ const JSONEditor = ({ // 将键值对数组转换为对象(重复键时后面的会覆盖前面的) const keyValueArrayToObject = useCallback((arr) => { const result = {}; - arr.forEach(item => { + arr.forEach((item) => { if (item.key) { result[item.key] = item.value; } @@ -115,7 +111,8 @@ const JSONEditor = ({ // 手动模式下的本地文本缓冲 const [manualText, setManualText] = useState(() => { if (typeof value === 'string') return value; - if (value && typeof value === 'object') return JSON.stringify(value, null, 2); + if (value && typeof value === 'object') + return JSON.stringify(value, null, 2); return ''; }); @@ -140,7 +137,7 @@ const JSONEditor = ({ const keyCount = {}; const duplicates = new Set(); - keyValuePairs.forEach(pair => { + keyValuePairs.forEach((pair) => { if (pair.key) { keyCount[pair.key] = (keyCount[pair.key] || 0) + 1; if (keyCount[pair.key] > 1) { @@ -178,51 +175,65 @@ const JSONEditor = ({ useEffect(() => { if (editMode !== 'manual') { if (typeof value === 'string') setManualText(value); - else if (value && typeof value === 'object') setManualText(JSON.stringify(value, null, 2)); + else if (value && typeof value === 'object') + setManualText(JSON.stringify(value, null, 2)); else setManualText(''); } }, [value, editMode]); // 处理可视化编辑的数据变化 - const handleVisualChange = useCallback((newPairs) => { - setKeyValuePairs(newPairs); - const jsonObject = keyValueArrayToObject(newPairs); - const jsonString = Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2); + const handleVisualChange = useCallback( + (newPairs) => { + setKeyValuePairs(newPairs); + const jsonObject = keyValueArrayToObject(newPairs); + const jsonString = + Object.keys(jsonObject).length === 0 + ? '' + : JSON.stringify(jsonObject, null, 2); - setJsonError(''); + setJsonError(''); - // 通过formApi设置值 - if (formApi && field) { - formApi.setValue(field, jsonString); - } + // 通过formApi设置值 + if (formApi && field) { + formApi.setValue(field, jsonString); + } - onChange?.(jsonString); - }, [onChange, formApi, field, keyValueArrayToObject]); + onChange?.(jsonString); + }, + [onChange, formApi, field, keyValueArrayToObject], + ); // 处理手动编辑的数据变化 - const handleManualChange = useCallback((newValue) => { - setManualText(newValue); - if (newValue && newValue.trim()) { - try { - const parsed = JSON.parse(newValue); - setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); + const handleManualChange = useCallback( + (newValue) => { + setManualText(newValue); + if (newValue && newValue.trim()) { + try { + const parsed = JSON.parse(newValue); + setKeyValuePairs(objectToKeyValueArray(parsed, keyValuePairs)); + setJsonError(''); + onChange?.(newValue); + } catch (error) { + setJsonError(error.message); + } + } else { + setKeyValuePairs([]); setJsonError(''); - onChange?.(newValue); - } catch (error) { - setJsonError(error.message); + onChange?.(''); } - } else { - setKeyValuePairs([]); - setJsonError(''); - onChange?.(''); - } - }, [onChange, objectToKeyValueArray, keyValuePairs]); + }, + [onChange, objectToKeyValueArray, keyValuePairs], + ); // 切换编辑模式 const toggleEditMode = useCallback(() => { if (editMode === 'visual') { const jsonObject = keyValueArrayToObject(keyValuePairs); - setManualText(Object.keys(jsonObject).length === 0 ? '' : JSON.stringify(jsonObject, null, 2)); + setManualText( + Object.keys(jsonObject).length === 0 + ? '' + : JSON.stringify(jsonObject, null, 2), + ); setEditMode('manual'); } else { try { @@ -242,12 +253,19 @@ const JSONEditor = ({ return; } } - }, [editMode, value, manualText, keyValuePairs, keyValueArrayToObject, objectToKeyValueArray]); + }, [ + editMode, + value, + manualText, + keyValuePairs, + keyValueArrayToObject, + objectToKeyValueArray, + ]); // 添加键值对 const addKeyValue = useCallback(() => { const newPairs = [...keyValuePairs]; - const existingKeys = newPairs.map(p => p.key); + const existingKeys = newPairs.map((p) => p.key); let counter = 1; let newKey = `field_${counter}`; while (existingKeys.includes(newKey)) { @@ -257,32 +275,41 @@ const JSONEditor = ({ newPairs.push({ id: generateUniqueId(), key: newKey, - value: '' + value: '', }); handleVisualChange(newPairs); }, [keyValuePairs, handleVisualChange]); // 删除键值对 - const removeKeyValue = useCallback((id) => { - const newPairs = keyValuePairs.filter(pair => pair.id !== id); - handleVisualChange(newPairs); - }, [keyValuePairs, handleVisualChange]); + const removeKeyValue = useCallback( + (id) => { + const newPairs = keyValuePairs.filter((pair) => pair.id !== id); + handleVisualChange(newPairs); + }, + [keyValuePairs, handleVisualChange], + ); // 更新键名 - const updateKey = useCallback((id, newKey) => { - const newPairs = keyValuePairs.map(pair => - pair.id === id ? { ...pair, key: newKey } : pair - ); - handleVisualChange(newPairs); - }, [keyValuePairs, handleVisualChange]); + const updateKey = useCallback( + (id, newKey) => { + const newPairs = keyValuePairs.map((pair) => + pair.id === id ? { ...pair, key: newKey } : pair, + ); + handleVisualChange(newPairs); + }, + [keyValuePairs, handleVisualChange], + ); // 更新值 - const updateValue = useCallback((id, newValue) => { - const newPairs = keyValuePairs.map(pair => - pair.id === id ? { ...pair, value: newValue } : pair - ); - handleVisualChange(newPairs); - }, [keyValuePairs, handleVisualChange]); + const updateValue = useCallback( + (id, newValue) => { + const newPairs = keyValuePairs.map((pair) => + pair.id === id ? { ...pair, value: newValue } : pair, + ); + handleVisualChange(newPairs); + }, + [keyValuePairs, handleVisualChange], + ); // 填入模板 const fillTemplate = useCallback(() => { @@ -298,7 +325,14 @@ const JSONEditor = ({ onChange?.(templateString); setJsonError(''); } - }, [template, onChange, formApi, field, objectToKeyValueArray, keyValuePairs]); + }, [ + template, + onChange, + formApi, + field, + objectToKeyValueArray, + keyValuePairs, + ]); // 渲染值输入控件(支持嵌套) const renderValueInput = (pairId, value) => { @@ -306,12 +340,12 @@ const JSONEditor = ({ if (valueType === 'boolean') { return ( -
          +
          updateValue(pairId, newValue)} /> - + {value ? t('true') : t('false')}
          @@ -373,29 +407,29 @@ const JSONEditor = ({ // 渲染键值对编辑器 const renderKeyValueEditor = () => { return ( -
          +
          {/* 重复键警告 */} {duplicateKeys.size > 0 && ( } description={
          {t('存在重复的键名:')} {Array.from(duplicateKeys).join(', ')}
          - + {t('注意:JSON中重复的键只会保留最后一个同名键的值')}
          } - className="mb-3" + className='mb-3' /> )} {keyValuePairs.length === 0 && ( -
          - +
          + {t('暂无数据,点击下方按钮添加键值对')}
          @@ -403,13 +437,14 @@ const JSONEditor = ({ {keyValuePairs.map((pair, index) => { const isDuplicate = duplicateKeys.has(pair.key); - const isLastDuplicate = isDuplicate && - keyValuePairs.slice(index + 1).every(p => p.key !== pair.key); + const isLastDuplicate = + isDuplicate && + keyValuePairs.slice(index + 1).every((p) => p.key !== pair.key); return ( - +
          -
          +
          )}
          -
          - {renderValueInput(pair.id, pair.value)} - + {renderValueInput(pair.id, pair.value)}-
          +
          @@ -546,8 +582,8 @@ const JSONEditor = ({
          @@ -590,9 +626,9 @@ const JSONEditor = ({ +
          { if (key === 'manual' && editMode === 'visual') { @@ -602,16 +638,12 @@ const JSONEditor = ({ } }} > - - + + {template && templateLabel && ( - )} @@ -619,14 +651,14 @@ const JSONEditor = ({ } headerStyle={{ padding: '12px 16px' }} bodyStyle={{ padding: '16px' }} - className="!rounded-2xl" + className='!rounded-2xl' > {/* JSON错误提示 */} {hasJsonError && ( )} @@ -668,17 +700,15 @@ const JSONEditor = ({ {/* 额外文本显示在卡片底部 */} {extraText && ( - {extraText} + + {extraText} + )} - {extraFooter && ( -
          - {extraFooter} -
          - )} + {extraFooter &&
          {extraFooter}
          } ); }; -export default JSONEditor; \ No newline at end of file +export default JSONEditor; diff --git a/web/src/components/common/ui/Loading.jsx b/web/src/components/common/ui/Loading.jsx index 60f947486..a2fc6f8e9 100644 --- a/web/src/components/common/ui/Loading.jsx +++ b/web/src/components/common/ui/Loading.jsx @@ -21,13 +21,9 @@ import React from 'react'; import { Spin } from '@douyinfe/semi-ui'; const Loading = ({ size = 'small' }) => { - return ( -
          - +
          +
          ); }; diff --git a/web/src/components/common/ui/RenderUtils.jsx b/web/src/components/common/ui/RenderUtils.jsx index 26a72e16f..3411649ce 100644 --- a/web/src/components/common/ui/RenderUtils.jsx +++ b/web/src/components/common/ui/RenderUtils.jsx @@ -57,4 +57,4 @@ export const renderDescription = (text, maxWidth = 200) => { {text || '-'} ); -}; \ No newline at end of file +}; diff --git a/web/src/components/common/ui/ScrollableContainer.jsx b/web/src/components/common/ui/ScrollableContainer.jsx index 4ddda7d82..441c8c030 100644 --- a/web/src/components/common/ui/ScrollableContainer.jsx +++ b/web/src/components/common/ui/ScrollableContainer.jsx @@ -24,197 +24,219 @@ import React, { useCallback, useMemo, useImperativeHandle, - forwardRef + forwardRef, } from 'react'; /** * ScrollableContainer 可滚动容器组件 - * + * * 提供自动检测滚动状态和显示渐变指示器的功能 * 当内容超出容器高度且未滚动到底部时,会显示底部渐变指示器 - * + * */ -const ScrollableContainer = forwardRef(({ - children, - maxHeight = '24rem', - className = '', - contentClassName = '', - fadeIndicatorClassName = '', - checkInterval = 100, - scrollThreshold = 5, - debounceDelay = 16, // ~60fps - onScroll, - onScrollStateChange, - ...props -}, ref) => { - const scrollRef = useRef(null); - const containerRef = useRef(null); - const debounceTimerRef = useRef(null); - const resizeObserverRef = useRef(null); - const onScrollStateChangeRef = useRef(onScrollStateChange); - const onScrollRef = useRef(onScroll); - - const [showScrollHint, setShowScrollHint] = useState(false); - - useEffect(() => { - onScrollStateChangeRef.current = onScrollStateChange; - }, [onScrollStateChange]); - - useEffect(() => { - onScrollRef.current = onScroll; - }, [onScroll]); - - const debounce = useCallback((func, delay) => { - return (...args) => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - debounceTimerRef.current = setTimeout(() => func(...args), delay); - }; - }, []); - - const checkScrollable = useCallback(() => { - if (!scrollRef.current) return; - - const element = scrollRef.current; - const isScrollable = element.scrollHeight > element.clientHeight; - const isAtBottom = element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold; - const shouldShowHint = isScrollable && !isAtBottom; - - setShowScrollHint(shouldShowHint); - - if (onScrollStateChangeRef.current) { - onScrollStateChangeRef.current({ - isScrollable, - isAtBottom, - showScrollHint: shouldShowHint, - scrollTop: element.scrollTop, - scrollHeight: element.scrollHeight, - clientHeight: element.clientHeight - }); - } - }, [scrollThreshold]); - - const debouncedCheckScrollable = useMemo(() => - debounce(checkScrollable, debounceDelay), - [debounce, checkScrollable, debounceDelay] - ); - - const handleScroll = useCallback((e) => { - debouncedCheckScrollable(); - if (onScrollRef.current) { - onScrollRef.current(e); - } - }, [debouncedCheckScrollable]); - - useImperativeHandle(ref, () => ({ - checkScrollable: () => { - checkScrollable(); +const ScrollableContainer = forwardRef( + ( + { + children, + maxHeight = '24rem', + className = '', + contentClassName = '', + fadeIndicatorClassName = '', + checkInterval = 100, + scrollThreshold = 5, + debounceDelay = 16, // ~60fps + onScroll, + onScrollStateChange, + ...props }, - scrollToTop: () => { - if (scrollRef.current) { - scrollRef.current.scrollTop = 0; - } - }, - scrollToBottom: () => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, - getScrollInfo: () => { - if (!scrollRef.current) return null; - const element = scrollRef.current; - return { - scrollTop: element.scrollTop, - scrollHeight: element.scrollHeight, - clientHeight: element.clientHeight, - isScrollable: element.scrollHeight > element.clientHeight, - isAtBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - scrollThreshold + ref, + ) => { + const scrollRef = useRef(null); + const containerRef = useRef(null); + const debounceTimerRef = useRef(null); + const resizeObserverRef = useRef(null); + const onScrollStateChangeRef = useRef(onScrollStateChange); + const onScrollRef = useRef(onScroll); + + const [showScrollHint, setShowScrollHint] = useState(false); + + useEffect(() => { + onScrollStateChangeRef.current = onScrollStateChange; + }, [onScrollStateChange]); + + useEffect(() => { + onScrollRef.current = onScroll; + }, [onScroll]); + + const debounce = useCallback((func, delay) => { + return (...args) => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + debounceTimerRef.current = setTimeout(() => func(...args), delay); }; - } - }), [checkScrollable, scrollThreshold]); + }, []); - useEffect(() => { - const timer = setTimeout(() => { - checkScrollable(); - }, checkInterval); - return () => clearTimeout(timer); - }, [checkScrollable, checkInterval]); + const checkScrollable = useCallback(() => { + if (!scrollRef.current) return; - useEffect(() => { - if (!scrollRef.current) return; + const element = scrollRef.current; + const isScrollable = element.scrollHeight > element.clientHeight; + const isAtBottom = + element.scrollTop + element.clientHeight >= + element.scrollHeight - scrollThreshold; + const shouldShowHint = isScrollable && !isAtBottom; - if (typeof ResizeObserver === 'undefined') { - if (typeof MutationObserver !== 'undefined') { - const observer = new MutationObserver(() => { - debouncedCheckScrollable(); + setShowScrollHint(shouldShowHint); + + if (onScrollStateChangeRef.current) { + onScrollStateChangeRef.current({ + isScrollable, + isAtBottom, + showScrollHint: shouldShowHint, + scrollTop: element.scrollTop, + scrollHeight: element.scrollHeight, + clientHeight: element.clientHeight, }); - - observer.observe(scrollRef.current, { - childList: true, - subtree: true, - attributes: true, - characterData: true - }); - - return () => observer.disconnect(); } - return; - } + }, [scrollThreshold]); - resizeObserverRef.current = new ResizeObserver((entries) => { - for (const entry of entries) { + const debouncedCheckScrollable = useMemo( + () => debounce(checkScrollable, debounceDelay), + [debounce, checkScrollable, debounceDelay], + ); + + const handleScroll = useCallback( + (e) => { debouncedCheckScrollable(); + if (onScrollRef.current) { + onScrollRef.current(e); + } + }, + [debouncedCheckScrollable], + ); + + useImperativeHandle( + ref, + () => ({ + checkScrollable: () => { + checkScrollable(); + }, + scrollToTop: () => { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, + scrollToBottom: () => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, + getScrollInfo: () => { + if (!scrollRef.current) return null; + const element = scrollRef.current; + return { + scrollTop: element.scrollTop, + scrollHeight: element.scrollHeight, + clientHeight: element.clientHeight, + isScrollable: element.scrollHeight > element.clientHeight, + isAtBottom: + element.scrollTop + element.clientHeight >= + element.scrollHeight - scrollThreshold, + }; + }, + }), + [checkScrollable, scrollThreshold], + ); + + useEffect(() => { + const timer = setTimeout(() => { + checkScrollable(); + }, checkInterval); + return () => clearTimeout(timer); + }, [checkScrollable, checkInterval]); + + useEffect(() => { + if (!scrollRef.current) return; + + if (typeof ResizeObserver === 'undefined') { + if (typeof MutationObserver !== 'undefined') { + const observer = new MutationObserver(() => { + debouncedCheckScrollable(); + }); + + observer.observe(scrollRef.current, { + childList: true, + subtree: true, + attributes: true, + characterData: true, + }); + + return () => observer.disconnect(); + } + return; } - }); - resizeObserverRef.current.observe(scrollRef.current); + resizeObserverRef.current = new ResizeObserver((entries) => { + for (const entry of entries) { + debouncedCheckScrollable(); + } + }); - return () => { - if (resizeObserverRef.current) { - resizeObserverRef.current.disconnect(); - } - }; - }, [debouncedCheckScrollable]); + resizeObserverRef.current.observe(scrollRef.current); - useEffect(() => { - return () => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - }; - }, []); + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect(); + } + }; + }, [debouncedCheckScrollable]); - const containerStyle = useMemo(() => ({ - maxHeight - }), [maxHeight]); + useEffect(() => { + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; + }, []); - const fadeIndicatorStyle = useMemo(() => ({ - opacity: showScrollHint ? 1 : 0 - }), [showScrollHint]); + const containerStyle = useMemo( + () => ({ + maxHeight, + }), + [maxHeight], + ); - return ( -
          + const fadeIndicatorStyle = useMemo( + () => ({ + opacity: showScrollHint ? 1 : 0, + }), + [showScrollHint], + ); + + return (
          - {children} +
          + {children} +
          +
          -
          -
          - ); -}); + ); + }, +); ScrollableContainer.displayName = 'ScrollableContainer'; -export default ScrollableContainer; \ No newline at end of file +export default ScrollableContainer; diff --git a/web/src/components/common/ui/SelectableButtonGroup.jsx b/web/src/components/common/ui/SelectableButtonGroup.jsx index 2dd8f6571..3fe249084 100644 --- a/web/src/components/common/ui/SelectableButtonGroup.jsx +++ b/web/src/components/common/ui/SelectableButtonGroup.jsx @@ -20,7 +20,17 @@ For commercial licensing, please contact support@quantumnous.com import React, { useState, useRef, useEffect } from 'react'; import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; import { useContainerWidth } from '../../../hooks/common/useContainerWidth'; -import { Divider, Button, Tag, Row, Col, Collapsible, Checkbox, Skeleton, Tooltip } from '@douyinfe/semi-ui'; +import { + Divider, + Button, + Tag, + Row, + Col, + Collapsible, + Checkbox, + Skeleton, + Tooltip, +} from '@douyinfe/semi-ui'; import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; /** @@ -47,7 +57,7 @@ const SelectableButtonGroup = ({ collapsible = true, collapseHeight = 200, withCheckbox = false, - loading = false + loading = false, }) => { const [isOpen, setIsOpen] = useState(false); const [skeletonCount] = useState(12); @@ -64,15 +74,13 @@ const SelectableButtonGroup = ({ }, [text, containerWidth]); const textElement = ( - + {text} ); return isOverflowing ? ( - - {textElement} - + {textElement} ) : ( textElement ); @@ -80,10 +88,10 @@ const SelectableButtonGroup = ({ // 基于容器宽度计算响应式列数和标签显示策略 const getResponsiveConfig = () => { - if (containerWidth <= 280) return { columns: 1, showTags: true }; // 极窄:1列+标签 - if (containerWidth <= 380) return { columns: 2, showTags: true }; // 窄屏:2列+标签 - if (containerWidth <= 460) return { columns: 3, showTags: false }; // 中等:3列不加标签 - return { columns: 3, showTags: true }; // 最宽:3列+标签 + if (containerWidth <= 280) return { columns: 1, showTags: true }; // 极窄:1列+标签 + if (containerWidth <= 380) return { columns: 2, showTags: true }; // 窄屏:2列+标签 + if (containerWidth <= 460) return { columns: 3, showTags: false }; // 中等:3列不加标签 + return { columns: 3, showTags: true }; // 最宽:3列+标签 }; const { columns: perRow, showTags: shouldShowTags } = getResponsiveConfig(); @@ -102,9 +110,9 @@ const SelectableButtonGroup = ({ const maskStyle = isOpen ? {} : { - WebkitMaskImage: - 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)', - }; + WebkitMaskImage: + 'linear-gradient(to bottom, black 0%, rgba(0, 0, 0, 1) 60%, rgba(0, 0, 0, 0.2) 80%, transparent 100%)', + }; const toggle = () => { setIsOpen(!isOpen); @@ -127,25 +135,23 @@ const SelectableButtonGroup = ({ }; const renderSkeletonButtons = () => { - const placeholder = ( {Array.from({ length: skeletonCount }).map((_, index) => ( -
          -
          +
          +
          {withCheckbox && ( )} @@ -153,7 +159,7 @@ const SelectableButtonGroup = ({ active style={{ width: `${60 + (index % 3) * 20}px`, - height: 14 + height: 14, }} />
          @@ -167,26 +173,29 @@ const SelectableButtonGroup = ({ ); }; - const contentElement = showSkeleton ? renderSkeletonButtons() : ( + const contentElement = showSkeleton ? ( + renderSkeletonButtons() + ) : ( {items.map((item) => { - const isDisabled = item.disabled || (typeof item.tagCount === 'number' && item.tagCount === 0); + const isDisabled = + item.disabled || + (typeof item.tagCount === 'number' && item.tagCount === 0); const isActive = Array.isArray(activeValue) ? activeValue.includes(item.value) : activeValue === item.value; if (withCheckbox) { return ( - + @@ -210,23 +226,27 @@ const SelectableButtonGroup = ({ } return ( - + @@ -237,9 +257,12 @@ const SelectableButtonGroup = ({ ); return ( -
          +
          {title && ( - + {showSkeleton ? ( ) : ( @@ -249,23 +272,30 @@ const SelectableButtonGroup = ({ )} {needCollapse && !showSkeleton ? (
          - + {contentElement} {isOpen ? null : (
          - + {t('展开更多')}
          )} {isOpen && ( -
          - +
          + {t('收起')}
          )} @@ -277,4 +307,4 @@ const SelectableButtonGroup = ({ ); }; -export default SelectableButtonGroup; \ No newline at end of file +export default SelectableButtonGroup; diff --git a/web/src/components/dashboard/AnnouncementsPanel.jsx b/web/src/components/dashboard/AnnouncementsPanel.jsx index e24f8da2f..c62850b3b 100644 --- a/web/src/components/dashboard/AnnouncementsPanel.jsx +++ b/web/src/components/dashboard/AnnouncementsPanel.jsx @@ -21,7 +21,10 @@ import React from 'react'; import { Card, Tag, Timeline, Empty } from '@douyinfe/semi-ui'; import { Bell } from 'lucide-react'; import { marked } from 'marked'; -import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations'; +import { + IllustrationConstruction, + IllustrationConstructionDark, +} from '@douyinfe/semi-illustrations'; import ScrollableContainer from '../common/ui/ScrollableContainer'; const AnnouncementsPanel = ({ @@ -29,36 +32,43 @@ const AnnouncementsPanel = ({ announcementLegendData, CARD_PROPS, ILLUSTRATION_SIZE, - t + t, }) => { return ( -
          +
          +
          {t('系统公告')} - + {t('显示最新20条')}
          {/* 图例 */} -
          +
          {announcementLegendData.map((legend, index) => ( -
          +
          - {legend.label} + {legend.label}
          ))}
          @@ -66,9 +76,9 @@ const AnnouncementsPanel = ({ } bodyStyle={{ padding: 0 }} > - + {announcementData.length > 0 ? ( - + {announcementData.map((item, idx) => { const htmlExtra = item.extra ? marked.parse(item.extra) : ''; return ( @@ -76,16 +86,20 @@ const AnnouncementsPanel = ({ key={idx} type={item.type || 'default'} time={`${item.relative ? item.relative + ' ' : ''}${item.time}`} - extra={item.extra ? ( -
          - ) : null} + extra={ + item.extra ? ( +
          + ) : null + } >
          @@ -93,10 +107,12 @@ const AnnouncementsPanel = ({ })} ) : ( -
          +
          } - darkModeImage={} + darkModeImage={ + + } title={t('暂无系统公告')} description={t('请联系管理员在系统设置中配置公告信息')} /> @@ -107,4 +123,4 @@ const AnnouncementsPanel = ({ ); }; -export default AnnouncementsPanel; \ No newline at end of file +export default AnnouncementsPanel; diff --git a/web/src/components/dashboard/ApiInfoPanel.jsx b/web/src/components/dashboard/ApiInfoPanel.jsx index 5da250e6e..63b6def48 100644 --- a/web/src/components/dashboard/ApiInfoPanel.jsx +++ b/web/src/components/dashboard/ApiInfoPanel.jsx @@ -20,7 +20,10 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Card, Avatar, Tag, Divider, Empty } from '@douyinfe/semi-ui'; import { Server, Gauge, ExternalLink } from 'lucide-react'; -import { IllustrationConstruction, IllustrationConstructionDark } from '@douyinfe/semi-illustrations'; +import { + IllustrationConstruction, + IllustrationConstructionDark, +} from '@douyinfe/semi-illustrations'; import ScrollableContainer from '../common/ui/ScrollableContainer'; const ApiInfoPanel = ({ @@ -30,12 +33,12 @@ const ApiInfoPanel = ({ CARD_PROPS, FLEX_CENTER_GAP2, ILLUSTRATION_SIZE, - t + t, }) => { return ( @@ -44,66 +47,65 @@ const ApiInfoPanel = ({ } bodyStyle={{ padding: 0 }} > - + {apiInfoData.length > 0 ? ( apiInfoData.map((api) => ( -
          -
          - +
          +
          + {api.route.substring(0, 2)}
          -
          -
          - +
          +
          + {api.route} -
          +
          } - size="small" - color="white" + size='small' + color='white' shape='circle' onClick={() => handleSpeedTest(api.url)} - className="cursor-pointer hover:opacity-80 text-xs" + className='cursor-pointer hover:opacity-80 text-xs' > {t('测速')} } - size="small" - color="white" + size='small' + color='white' shape='circle' - onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')} - className="cursor-pointer hover:opacity-80 text-xs" + onClick={() => + window.open(api.url, '_blank', 'noopener,noreferrer') + } + className='cursor-pointer hover:opacity-80 text-xs' > {t('跳转')}
          handleCopyUrl(api.url)} > {api.url}
          -
          - {api.description} -
          +
          {api.description}
          )) ) : ( -
          +
          } - darkModeImage={} + darkModeImage={ + + } title={t('暂无API信息')} description={t('请联系管理员在系统设置中配置API信息')} /> @@ -114,4 +116,4 @@ const ApiInfoPanel = ({ ); }; -export default ApiInfoPanel; \ No newline at end of file +export default ApiInfoPanel; diff --git a/web/src/components/dashboard/ChartsPanel.jsx b/web/src/components/dashboard/ChartsPanel.jsx index 595e2e029..dc1684a20 100644 --- a/web/src/components/dashboard/ChartsPanel.jsx +++ b/web/src/components/dashboard/ChartsPanel.jsx @@ -23,7 +23,7 @@ import { PieChart } from 'lucide-react'; import { IconHistogram, IconPulse, - IconPieChart2Stroked + IconPieChart2Stroked, } from '@douyinfe/semi-icons'; import { VChart } from '@visactor/react-vchart'; @@ -38,80 +38,80 @@ const ChartsPanel = ({ CHART_CONFIG, FLEX_CENTER_GAP2, hasApiInfoPanel, - t + t, }) => { return ( +
          {t('模型数据分析')}
          - - - {t('消耗分布')} - - } itemKey="1" /> - - - {t('消耗趋势')} - - } itemKey="2" /> - - - {t('调用次数分布')} - - } itemKey="3" /> - - - {t('调用次数排行')} - - } itemKey="4" /> + + + {t('消耗分布')} + + } + itemKey='1' + /> + + + {t('消耗趋势')} + + } + itemKey='2' + /> + + + {t('调用次数分布')} + + } + itemKey='3' + /> + + + {t('调用次数排行')} + + } + itemKey='4' + />
          } bodyStyle={{ padding: 0 }} > -
          +
          {activeChartTab === '1' && ( - + )} {activeChartTab === '2' && ( - + )} {activeChartTab === '3' && ( - + )} {activeChartTab === '4' && ( - + )}
          ); }; -export default ChartsPanel; \ No newline at end of file +export default ChartsPanel; diff --git a/web/src/components/dashboard/DashboardHeader.jsx b/web/src/components/dashboard/DashboardHeader.jsx index e0be5d859..c2867e90c 100644 --- a/web/src/components/dashboard/DashboardHeader.jsx +++ b/web/src/components/dashboard/DashboardHeader.jsx @@ -27,19 +27,19 @@ const DashboardHeader = ({ showSearchModal, refresh, loading, - t + t, }) => { - const ICON_BUTTON_CLASS = "text-white hover:bg-opacity-80 !rounded-full"; + const ICON_BUTTON_CLASS = 'text-white hover:bg-opacity-80 !rounded-full'; return ( -
          +

          {getGreeting}

          -
          +
          ); } else { const showRegisterButton = !isSelfUseMode; - const commonSizingAndLayoutClass = "flex items-center justify-center !py-[10px] !px-1.5"; + const commonSizingAndLayoutClass = + 'flex items-center justify-center !py-[10px] !px-1.5'; - const loginButtonSpecificStyling = "!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors"; + const loginButtonSpecificStyling = + '!bg-semi-color-fill-0 dark:!bg-semi-color-fill-1 hover:!bg-semi-color-fill-1 dark:hover:!bg-gray-700 transition-colors'; let loginButtonClasses = `${commonSizingAndLayoutClass} ${loginButtonSpecificStyling}`; let registerButtonClasses = `${commonSizingAndLayoutClass}`; - const loginButtonTextSpanClass = "!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5"; - const registerButtonTextSpanClass = "!text-xs !text-white !p-1.5"; + const loginButtonTextSpanClass = + '!text-xs !text-semi-color-text-1 dark:!text-gray-300 !p-1.5'; + const registerButtonTextSpanClass = '!text-xs !text-white !p-1.5'; if (showRegisterButton) { if (isMobile) { - loginButtonClasses += " !rounded-full"; + loginButtonClasses += ' !rounded-full'; } else { - loginButtonClasses += " !rounded-l-full !rounded-r-none"; + loginButtonClasses += ' !rounded-l-full !rounded-r-none'; } - registerButtonClasses += " !rounded-r-full !rounded-l-none"; + registerButtonClasses += ' !rounded-r-full !rounded-l-none'; } else { - loginButtonClasses += " !rounded-full"; + loginButtonClasses += ' !rounded-full'; } return ( -
          - +
          + {showRegisterButton && ( -
          - +
          +
          diff --git a/web/src/components/layout/HeaderBar/index.jsx b/web/src/components/layout/HeaderBar/index.jsx index 0a0e89545..db104de43 100644 --- a/web/src/components/layout/HeaderBar/index.jsx +++ b/web/src/components/layout/HeaderBar/index.jsx @@ -63,7 +63,7 @@ const HeaderBar = ({ onMobileMenuToggle, drawerOpen }) => { const { mainNavLinks } = useNavigation(t, docsLink); return ( -
          +
          { unreadKeys={getUnreadKeys()} /> -
          -
          -
          +
          +
          +
          { +const NoticeModal = ({ + visible, + onClose, + isMobile, + defaultTab = 'inApp', + unreadKeys = [], +}) => { const { t } = useTranslation(); const [noticeContent, setNoticeContent] = useState(''); const [loading, setLoading] = useState(false); @@ -38,23 +54,25 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK const unreadSet = useMemo(() => new Set(unreadKeys), [unreadKeys]); - const getKeyForItem = (item) => `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`; + const getKeyForItem = (item) => + `${item?.publishDate || ''}-${(item?.content || '').slice(0, 30)}`; const processedAnnouncements = useMemo(() => { - return (announcements || []).slice(0, 20).map(item => { + return (announcements || []).slice(0, 20).map((item) => { const pubDate = item?.publishDate ? new Date(item.publishDate) : null; - const absoluteTime = pubDate && !isNaN(pubDate.getTime()) - ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}` - : (item?.publishDate || ''); - return ({ + const absoluteTime = + pubDate && !isNaN(pubDate.getTime()) + ? `${pubDate.getFullYear()}-${String(pubDate.getMonth() + 1).padStart(2, '0')}-${String(pubDate.getDate()).padStart(2, '0')} ${String(pubDate.getHours()).padStart(2, '0')}:${String(pubDate.getMinutes()).padStart(2, '0')}` + : item?.publishDate || ''; + return { key: getKeyForItem(item), type: item.type || 'default', time: absoluteTime, content: item.content, extra: item.extra, relative: getRelativeTime(item.publishDate), - isUnread: unreadSet.has(getKeyForItem(item)) - }); + isUnread: unreadSet.has(getKeyForItem(item)), + }; }); }, [announcements, unreadSet]); @@ -100,15 +118,23 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK const renderMarkdownNotice = () => { if (loading) { - return
          ; + return ( +
          + +
          + ); } if (!noticeContent) { return ( -
          +
          } - darkModeImage={} + image={ + + } + darkModeImage={ + + } description={t('暂无公告')} />
          @@ -118,7 +144,7 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK return (
          ); }; @@ -126,10 +152,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK const renderAnnouncementTimeline = () => { if (processedAnnouncements.length === 0) { return ( -
          +
          } - darkModeImage={} + image={ + + } + darkModeImage={ + + } description={t('暂无系统公告')} />
          @@ -137,8 +167,8 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK } return ( -
          - +
          + {processedAnnouncements.map((item, idx) => { const htmlContent = marked.parse(item.content || ''); const htmlExtra = item.extra ? marked.parse(item.extra) : ''; @@ -147,12 +177,14 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK key={idx} type={item.type} time={`${item.relative ? item.relative + ' ' : ''}${item.time}`} - extra={item.extra ? ( -
          - ) : null} + extra={ + item.extra ? ( +
          + ) : null + } className={item.isUnread ? '' : ''} >
          @@ -179,26 +211,40 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK return ( +
          {t('系统公告')} - - {t('通知')}} itemKey='inApp' /> - {t('系统公告')}} itemKey='system' /> + + + {t('通知')} + + } + itemKey='inApp' + /> + + {t('系统公告')} + + } + itemKey='system' + />
          } visible={visible} onCancel={onClose} - footer={( -
          - - + footer={ +
          + +
          - )} + } size={isMobile ? 'full-width' : 'large'} > {renderBody()} @@ -206,4 +252,4 @@ const NoticeModal = ({ visible, onClose, isMobile, defaultTab = 'inApp', unreadK ); }; -export default NoticeModal; \ No newline at end of file +export default NoticeModal; diff --git a/web/src/components/layout/PageLayout.jsx b/web/src/components/layout/PageLayout.jsx index dd5080687..72df89ebb 100644 --- a/web/src/components/layout/PageLayout.jsx +++ b/web/src/components/layout/PageLayout.jsx @@ -27,7 +27,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { useIsMobile } from '../../hooks/common/useIsMobile'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed'; import { useTranslation } from 'react-i18next'; -import { API, getLogo, getSystemName, showError, setStatusData } from '../../helpers'; +import { + API, + getLogo, + getSystemName, + showError, + setStatusData, +} from '../../helpers'; import { UserContext } from '../../context/User'; import { StatusContext } from '../../context/Status'; import { useLocation } from 'react-router-dom'; @@ -42,9 +48,12 @@ const PageLayout = () => { const { i18n } = useTranslation(); const location = useLocation(); - const shouldHideFooter = location.pathname.startsWith('/console') || location.pathname === '/pricing'; + const shouldHideFooter = + location.pathname.startsWith('/console') || + location.pathname === '/pricing'; - const shouldInnerPadding = location.pathname.includes('/console') && + const shouldInnerPadding = + location.pathname.includes('/console') && !location.pathname.startsWith('/console/chat') && location.pathname !== '/console/playground'; @@ -120,7 +129,10 @@ const PageLayout = () => { zIndex: 100, }} > - setDrawerOpen(prev => !prev)} drawerOpen={drawerOpen} /> + setDrawerOpen((prev) => !prev)} + drawerOpen={drawerOpen} + />
          { width: 'var(--sidebar-current-width)', }} > - { if (isMobile) setDrawerOpen(false); }} /> + { + if (isMobile) setDrawerOpen(false); + }} + /> )} { const location = useLocation(); useEffect(() => { - if (statusState?.status?.setup === false && location.pathname !== '/setup') { + if ( + statusState?.status?.setup === false && + location.pathname !== '/setup' + ) { window.location.href = '/setup'; } }, [statusState?.status?.setup, location.pathname]); @@ -34,4 +37,4 @@ const SetupCheck = ({ children }) => { return children; }; -export default SetupCheck; \ No newline at end of file +export default SetupCheck; diff --git a/web/src/components/layout/SiderBar.jsx b/web/src/components/layout/SiderBar.jsx index 86c480022..44dd0a785 100644 --- a/web/src/components/layout/SiderBar.jsx +++ b/web/src/components/layout/SiderBar.jsx @@ -23,17 +23,9 @@ import { useTranslation } from 'react-i18next'; import { getLucideIcon } from '../../helpers/render'; import { ChevronLeft } from 'lucide-react'; import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed'; -import { - isAdmin, - isRoot, - showError -} from '../../helpers'; +import { isAdmin, isRoot, showError } from '../../helpers'; -import { - Nav, - Divider, - Button, -} from '@douyinfe/semi-ui'; +import { Nav, Divider, Button } from '@douyinfe/semi-ui'; const routerMap = { home: '/', @@ -54,7 +46,7 @@ const routerMap = { personal: '/console/personal', }; -const SiderBar = ({ onNavigate = () => { } }) => { +const SiderBar = ({ onNavigate = () => {} }) => { const { t } = useTranslation(); const [collapsed, toggleCollapsed] = useSidebarCollapsed(); @@ -275,14 +267,17 @@ const SiderBar = ({ onNavigate = () => { } }) => { key={item.itemKey} itemKey={item.itemKey} text={ -
          - +
          + {item.text}
          } icon={ -
          +
          {getLucideIcon(item.itemKey, isSelected)}
          } @@ -302,14 +297,17 @@ const SiderBar = ({ onNavigate = () => { } }) => { key={item.itemKey} itemKey={item.itemKey} text={ -
          - +
          + {item.text}
          } icon={ -
          +
          {getLucideIcon(item.itemKey, isSelected)}
          } @@ -323,7 +321,10 @@ const SiderBar = ({ onNavigate = () => { } }) => { key={subItem.itemKey} itemKey={subItem.itemKey} text={ - + {subItem.text} } @@ -339,18 +340,18 @@ const SiderBar = ({ onNavigate = () => { } }) => { return (
          {/* 底部折叠按钮 */} -
          +
          diff --git a/web/src/components/playground/ChatArea.jsx b/web/src/components/playground/ChatArea.jsx index b6303112b..2c65731f5 100644 --- a/web/src/components/playground/ChatArea.jsx +++ b/web/src/components/playground/ChatArea.jsx @@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { - Card, - Chat, - Typography, - Button, -} from '@douyinfe/semi-ui'; -import { - MessageSquare, - Eye, - EyeOff, -} from 'lucide-react'; +import { Card, Chat, Typography, Button } from '@douyinfe/semi-ui'; +import { MessageSquare, Eye, EyeOff } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import CustomInputRender from './CustomInputRender'; @@ -57,37 +48,43 @@ const ChatArea = ({ return ( {/* 聊天头部 */} {styleState.isMobile ? ( -
          +
          ) : ( -
          -
          -
          -
          - +
          +
          +
          +
          +
          - + {t('AI 对话')} - + {inputs.model || t('选择模型开始对话')}
          -
          +
          @@ -97,7 +94,7 @@ const ChatArea = ({ )} {/* 聊天内容区域 */} -
          +
          @@ -129,4 +126,4 @@ const ChatArea = ({ ); }; -export default ChatArea; \ No newline at end of file +export default ChatArea; diff --git a/web/src/components/playground/CodeViewer.jsx b/web/src/components/playground/CodeViewer.jsx index 0e0d0bf54..9d8ae453a 100644 --- a/web/src/components/playground/CodeViewer.jsx +++ b/web/src/components/playground/CodeViewer.jsx @@ -102,15 +102,17 @@ const highlightJson = (str) => { color = '#569cd6'; } return `${match}`; - } + }, ); }; const isJsonLike = (content, language) => { if (language === 'json') return true; const trimmed = content.trim(); - return (trimmed.startsWith('{') && trimmed.endsWith('}')) || - (trimmed.startsWith('[') && trimmed.endsWith(']')); + return ( + (trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']')) + ); }; const formatContent = (content) => { @@ -148,7 +150,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => { const contentMetrics = useMemo(() => { const length = formattedContent.length; const isLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH; - const isVeryLarge = length > PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER; + const isVeryLarge = + length > + PERFORMANCE_CONFIG.MAX_DISPLAY_LENGTH * + PERFORMANCE_CONFIG.VERY_LARGE_MULTIPLIER; return { length, isLarge, isVeryLarge }; }, [formattedContent.length]); @@ -156,8 +161,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => { if (!contentMetrics.isLarge || isExpanded) { return formattedContent; } - return formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) + - '\n\n// ... 内容被截断以提升性能 ...'; + return ( + formattedContent.substring(0, PERFORMANCE_CONFIG.PREVIEW_LENGTH) + + '\n\n// ... 内容被截断以提升性能 ...' + ); }, [formattedContent, contentMetrics.isLarge, isExpanded]); const highlightedContent = useMemo(() => { @@ -174,9 +181,10 @@ const CodeViewer = ({ content, title, language = 'json' }) => { const handleCopy = useCallback(async () => { try { - const textToCopy = typeof content === 'object' && content !== null - ? JSON.stringify(content, null, 2) - : content; + const textToCopy = + typeof content === 'object' && content !== null + ? JSON.stringify(content, null, 2) + : content; const success = await copy(textToCopy); setCopied(true); @@ -205,11 +213,12 @@ const CodeViewer = ({ content, title, language = 'json' }) => { }, [isExpanded, contentMetrics.isVeryLarge]); if (!content) { - const placeholderText = { - preview: t('正在构造请求体预览...'), - request: t('暂无请求数据'), - response: t('暂无响应数据') - }[title] || t('暂无数据'); + const placeholderText = + { + preview: t('正在构造请求体预览...'), + request: t('暂无请求数据'), + response: t('暂无响应数据'), + }[title] || t('暂无数据'); return (
          @@ -222,7 +231,7 @@ const CodeViewer = ({ content, title, language = 'json' }) => { const contentPadding = contentMetrics.isLarge ? '52px' : '16px'; return ( -
          +
          {/* 性能警告 */} {contentMetrics.isLarge && (
          @@ -250,8 +259,8 @@ const CodeViewer = ({ content, title, language = 'json' }) => { @@ -329,4 +354,4 @@ const CodeViewer = ({ content, title, language = 'json' }) => { ); }; -export default CodeViewer; \ No newline at end of file +export default CodeViewer; diff --git a/web/src/components/playground/ConfigManager.jsx b/web/src/components/playground/ConfigManager.jsx index 753d11380..7eaa35b8a 100644 --- a/web/src/components/playground/ConfigManager.jsx +++ b/web/src/components/playground/ConfigManager.jsx @@ -18,21 +18,16 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useRef } from 'react'; -import { - Button, - Typography, - Toast, - Modal, - Dropdown, -} from '@douyinfe/semi-ui'; -import { - Download, - Upload, - RotateCcw, - Settings2, -} from 'lucide-react'; +import { Button, Typography, Toast, Modal, Dropdown } from '@douyinfe/semi-ui'; +import { Download, Upload, RotateCcw, Settings2 } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { exportConfig, importConfig, clearConfig, hasStoredConfig, getConfigTimestamp } from './configStorage'; +import { + exportConfig, + importConfig, + clearConfig, + hasStoredConfig, + getConfigTimestamp, +} from './configStorage'; const ConfigManager = ({ currentConfig, @@ -51,7 +46,10 @@ const ConfigManager = ({ ...currentConfig, timestamp: new Date().toISOString(), }; - localStorage.setItem('playground_config', JSON.stringify(configWithTimestamp)); + localStorage.setItem( + 'playground_config', + JSON.stringify(configWithTimestamp), + ); exportConfig(currentConfig, messages); Toast.success({ @@ -104,7 +102,9 @@ const ConfigManager = ({ const handleReset = () => { Modal.confirm({ title: t('重置配置'), - content: t('将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?'), + content: t( + '将清除所有保存的配置并恢复默认设置,此操作不可撤销。是否继续?', + ), okText: t('确定重置'), cancelText: t('取消'), okButtonProps: { @@ -114,7 +114,9 @@ const ConfigManager = ({ // 询问是否同时重置消息 Modal.confirm({ title: t('重置选项'), - content: t('是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。'), + content: t( + '是否同时重置对话消息?选择"是"将清空所有对话记录并恢复默认示例;选择"否"将保留当前对话记录。', + ), okText: t('同时重置消息'), cancelText: t('仅重置配置'), okButtonProps: { @@ -159,7 +161,7 @@ const ConfigManager = ({ name: 'export', onClick: handleExport, children: ( -
          +
          {t('导出配置')}
          @@ -170,7 +172,7 @@ const ConfigManager = ({ name: 'import', onClick: handleImportClick, children: ( -
          +
          {t('导入配置')}
          @@ -184,7 +186,7 @@ const ConfigManager = ({ name: 'reset', onClick: handleReset, children: ( -
          +
          {t('重置配置')}
          @@ -197,24 +199,24 @@ const ConfigManager = ({ return ( <>
          {/* 导出和导入按钮 */} -
          +
          @@ -267,8 +269,8 @@ const ConfigManager = ({ @@ -276,4 +278,4 @@ const ConfigManager = ({ ); }; -export default ConfigManager; \ No newline at end of file +export default ConfigManager; diff --git a/web/src/components/playground/CustomInputRender.jsx b/web/src/components/playground/CustomInputRender.jsx index 2191cb165..464cfa3b1 100644 --- a/web/src/components/playground/CustomInputRender.jsx +++ b/web/src/components/playground/CustomInputRender.jsx @@ -21,23 +21,24 @@ import React from 'react'; const CustomInputRender = (props) => { const { detailProps } = props; - const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = detailProps; + const { clearContextNode, uploadNode, inputNode, sendNode, onClick } = + detailProps; // 清空按钮 const styledClearNode = clearContextNode ? React.cloneElement(clearContextNode, { - className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`, - style: { - ...clearContextNode.props.style, - width: '32px', - height: '32px', - minWidth: '32px', - padding: 0, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - } - }) + className: `!rounded-full !bg-gray-100 hover:!bg-red-500 hover:!text-white flex-shrink-0 transition-all ${clearContextNode.props.className || ''}`, + style: { + ...clearContextNode.props.style, + width: '32px', + height: '32px', + minWidth: '32px', + padding: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + }) : null; // 发送按钮 @@ -52,21 +53,19 @@ const CustomInputRender = (props) => { display: 'flex', alignItems: 'center', justifyContent: 'center', - } + }, }); return ( -
          +
          {/* 清空对话按钮 - 左边 */} {styledClearNode} -
          - {inputNode} -
          +
          {inputNode}
          {/* 发送按钮 - 右边 */} {styledSendNode}
          @@ -74,4 +73,4 @@ const CustomInputRender = (props) => { ); }; -export default CustomInputRender; \ No newline at end of file +export default CustomInputRender; diff --git a/web/src/components/playground/CustomRequestEditor.jsx b/web/src/components/playground/CustomRequestEditor.jsx index e411d9e78..26b3ff504 100644 --- a/web/src/components/playground/CustomRequestEditor.jsx +++ b/web/src/components/playground/CustomRequestEditor.jsx @@ -25,13 +25,7 @@ import { Switch, Banner, } from '@douyinfe/semi-ui'; -import { - Code, - Edit, - Check, - X, - AlertTriangle, -} from 'lucide-react'; +import { Code, Edit, Check, X, AlertTriangle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; const CustomRequestEditor = ({ @@ -48,12 +42,22 @@ const CustomRequestEditor = ({ // 当切换到自定义模式时,用默认payload初始化 useEffect(() => { - if (customRequestMode && (!customRequestBody || customRequestBody.trim() === '')) { - const defaultJson = defaultPayload ? JSON.stringify(defaultPayload, null, 2) : ''; + if ( + customRequestMode && + (!customRequestBody || customRequestBody.trim() === '') + ) { + const defaultJson = defaultPayload + ? JSON.stringify(defaultPayload, null, 2) + : ''; setLocalValue(defaultJson); onCustomRequestBodyChange(defaultJson); } - }, [customRequestMode, defaultPayload, customRequestBody, onCustomRequestBodyChange]); + }, [ + customRequestMode, + defaultPayload, + customRequestBody, + onCustomRequestBodyChange, + ]); // 同步外部传入的customRequestBody到本地状态 useEffect(() => { @@ -113,21 +117,21 @@ const CustomRequestEditor = ({ }; return ( -
          +
          {/* 自定义模式开关 */} -
          -
          - - +
          +
          + + 自定义请求体模式
          @@ -135,43 +139,43 @@ const CustomRequestEditor = ({ <> {/* 提示信息 */} } - className="!rounded-lg" + className='!rounded-lg' closeIcon={null} /> {/* JSON编辑器 */}
          -
          - +
          + 请求体 JSON -
          +
          {isValid ? ( -
          +
          - + 格式正确
          ) : ( -
          +
          - + 格式错误
          )} @@ -191,12 +195,12 @@ const CustomRequestEditor = ({ /> {!isValid && errorMessage && ( - + {errorMessage} )} - + 请输入有效的JSON格式的请求体。您可以参考预览面板中的默认请求体格式。
          @@ -206,4 +210,4 @@ const CustomRequestEditor = ({ ); }; -export default CustomRequestEditor; \ No newline at end of file +export default CustomRequestEditor; diff --git a/web/src/components/playground/DebugPanel.jsx b/web/src/components/playground/DebugPanel.jsx index 24158c2b2..d931ff61c 100644 --- a/web/src/components/playground/DebugPanel.jsx +++ b/web/src/components/playground/DebugPanel.jsx @@ -26,14 +26,7 @@ import { Button, Dropdown, } from '@douyinfe/semi-ui'; -import { - Code, - Zap, - Clock, - X, - Eye, - Send, -} from 'lucide-react'; +import { Code, Zap, Clock, X, Eye, Send } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import CodeViewer from './CodeViewer'; @@ -76,7 +69,7 @@ const DebugPanel = ({ - {items.map(item => { + {items.map((item) => { return ( -
          -
          -
          - +
          +
          +
          +
          - + {t('调试信息')}
          @@ -127,75 +120,84 @@ const DebugPanel = ({
          -
          +
          - - - {t('预览请求体')} - {customRequestMode && ( - - 自定义 - - )} -
          - } itemKey="preview"> + + + {t('预览请求体')} + {customRequestMode && ( + + 自定义 + + )} +
          + } + itemKey='preview' + > - - - {t('实际请求体')} -
          - } itemKey="request"> + + + {t('实际请求体')} +
          + } + itemKey='request' + > - - - {t('响应')} -
          - } itemKey="response"> + + + {t('响应')} +
          + } + itemKey='response' + >
          -
          +
          {(debugData.timestamp || debugData.previewTimestamp) && ( -
          - - +
          + + {activeKey === 'preview' && debugData.previewTimestamp ? `${t('预览更新')}: ${new Date(debugData.previewTimestamp).toLocaleString()}` : debugData.timestamp @@ -209,4 +211,4 @@ const DebugPanel = ({ ); }; -export default DebugPanel; \ No newline at end of file +export default DebugPanel; diff --git a/web/src/components/playground/FloatingButtons.jsx b/web/src/components/playground/FloatingButtons.jsx index 87a3b0b55..3d024df4c 100644 --- a/web/src/components/playground/FloatingButtons.jsx +++ b/web/src/components/playground/FloatingButtons.jsx @@ -19,11 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { Button } from '@douyinfe/semi-ui'; -import { - Settings, - Eye, - EyeOff, -} from 'lucide-react'; +import { Settings, Eye, EyeOff } from 'lucide-react'; const FloatingButtons = ({ styleState, @@ -55,7 +51,7 @@ const FloatingButtons = ({ onClick={onToggleSettings} theme='solid' type='primary' - className="lg:hidden" + className='lg:hidden' /> )} @@ -64,8 +60,8 @@ const FloatingButtons = ({
          {!imageEnabled ? ( - - {disabled ? '图片功能在自定义请求体模式下不可用' : '启用后可添加图片URL进行多模态对话'} + + {disabled + ? '图片功能在自定义请求体模式下不可用' + : '启用后可添加图片URL进行多模态对话'} ) : imageUrls.length === 0 ? ( - - {disabled ? '图片功能在自定义请求体模式下不可用' : '点击 + 按钮添加图片URL进行多模态对话'} + + {disabled + ? '图片功能在自定义请求体模式下不可用' + : '点击 + 按钮添加图片URL进行多模态对话'} ) : ( - - 已添加 {imageUrls.length} 张图片{disabled ? ' (自定义模式下不可用)' : ''} + + 已添加 {imageUrls.length} 张图片 + {disabled ? ' (自定义模式下不可用)' : ''} )} -
          +
          {imageUrls.map((url, index) => ( -
          -
          +
          +
          handleUpdateImageUrl(index, value)} - className="!rounded-lg" - size="small" + className='!rounded-lg' + size='small' prefix={} disabled={!imageEnabled || disabled} />
          @@ -129,4 +137,4 @@ const ImageUrlInput = ({ imageUrls, imageEnabled, onImageUrlsChange, onImageEnab ); }; -export default ImageUrlInput; \ No newline at end of file +export default ImageUrlInput; diff --git a/web/src/components/playground/MessageActions.jsx b/web/src/components/playground/MessageActions.jsx index 64775ae52..093700367 100644 --- a/web/src/components/playground/MessageActions.jsx +++ b/web/src/components/playground/MessageActions.jsx @@ -18,17 +18,8 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { - Button, - Tooltip, -} from '@douyinfe/semi-ui'; -import { - RefreshCw, - Copy, - Trash2, - UserCheck, - Edit, -} from 'lucide-react'; +import { Button, Tooltip } from '@douyinfe/semi-ui'; +import { RefreshCw, Copy, Trash2, UserCheck, Edit } from 'lucide-react'; import { useTranslation } from 'react-i18next'; const MessageActions = ({ @@ -40,23 +31,32 @@ const MessageActions = ({ onRoleToggle, onMessageEdit, isAnyMessageGenerating = false, - isEditing = false + isEditing = false, }) => { const { t } = useTranslation(); - const isLoading = message.status === 'loading' || message.status === 'incomplete'; + const isLoading = + message.status === 'loading' || message.status === 'incomplete'; const shouldDisableActions = isAnyMessageGenerating || isEditing; - const canToggleRole = message.role === 'assistant' || message.role === 'system'; - const canEdit = !isLoading && message.content && typeof onMessageEdit === 'function' && !isEditing; + const canToggleRole = + message.role === 'assistant' || message.role === 'system'; + const canEdit = + !isLoading && + message.content && + typeof onMessageEdit === 'function' && + !isEditing; return ( -
          +
          {!isLoading && ( - +