From 64b565dc1586dd6f3b307cfae4055bbfb14756f5 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 3 Jun 2025 23:56:39 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(helpers):=20refac?= =?UTF-8?q?tor=20the=20helpers=20folder=20and=20related=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/ChannelsTable.js | 10 +- web/src/components/HeaderBar.js | 3 +- web/src/components/LoginForm.js | 2 +- web/src/components/LogsTable.js | 26 +- web/src/components/OAuth2Callback.js | 3 +- web/src/components/PageLayout.js | 3 +- web/src/components/PersonalSetting.js | 10 +- web/src/components/RedemptionsTable.js | 2 +- web/src/components/RegisterForm.js | 2 +- web/src/components/SiderBar.js | 8 +- web/src/components/SystemSetting.js | 6 +- web/src/components/TokensTable.js | 3 +- web/src/components/UsersTable.js | 3 +- .../common/markdown/MarkdownRenderer.js | 3 +- web/src/components/playground/CodeViewer.js | 2 +- .../components/playground/SettingsPanel.js | 2 +- web/src/context/Style/index.js | 2 +- web/src/helpers/api.js | 106 +++++++ web/src/helpers/apiUtils.js | 105 ------- web/src/helpers/{authUtils.js => auth.js} | 0 web/src/helpers/index.js | 9 +- web/src/helpers/{logUtils.js => log.js} | 0 web/src/helpers/messageUtils.js | 201 ------------- web/src/helpers/render.js | 279 +++++++++++------- web/src/helpers/textAnimationUtils.js | 77 ----- web/src/helpers/utils.js | 163 ++++++++++ web/src/hooks/useApiRequest.js | 9 +- web/src/hooks/useDataLoader.js | 3 +- web/src/hooks/useMessageActions.js | 2 +- web/src/hooks/useMessageEdit.js | 2 +- web/src/hooks/usePlaygroundState.js | 2 +- web/src/pages/Detail/index.js | 6 +- web/src/pages/Playground/index.js | 9 +- web/src/pages/Redemption/EditRedemption.js | 6 +- .../Operation/ModelRationNotSetEditor.js | 3 +- .../Operation/ModelSettingsVisualEditor.js | 25 +- web/src/pages/Token/EditToken.js | 3 +- web/src/pages/TopUp/index.js | 9 +- web/src/pages/User/EditUser.js | 3 +- 39 files changed, 523 insertions(+), 589 deletions(-) delete mode 100644 web/src/helpers/apiUtils.js rename web/src/helpers/{authUtils.js => auth.js} (100%) rename web/src/helpers/{logUtils.js => log.js} (100%) delete mode 100644 web/src/helpers/messageUtils.js delete mode 100644 web/src/helpers/textAnimationUtils.js diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 974d8bd8..c6bdf75b 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -5,14 +5,12 @@ import { showInfo, showSuccess, timestamp2string, + renderGroup, + renderNumberWithPoint, + renderQuota } from '../helpers'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; -import { - renderGroup, - renderNumberWithPoint, - renderQuota, -} from '../helpers/render'; import { Button, Divider, @@ -29,7 +27,7 @@ import { Typography, Checkbox, Card, - Select, + Select } from '@douyinfe/semi-ui'; import EditChannel from '../pages/Channel/EditChannel'; import { diff --git a/web/src/components/HeaderBar.js b/web/src/components/HeaderBar.js index 2209d0af..d4afac2c 100644 --- a/web/src/components/HeaderBar.js +++ b/web/src/components/HeaderBar.js @@ -3,7 +3,7 @@ import { Link, useNavigate, useLocation } from 'react-router-dom'; import { UserContext } from '../context/User'; import { useSetTheme, useTheme } from '../context/Theme'; import { useTranslation } from 'react-i18next'; -import { API, getLogo, getSystemName, showSuccess } from '../helpers'; +import { API, getLogo, getSystemName, showSuccess, stringToColor } from '../helpers'; import fireworks from 'react-fireworks'; import { CN, GB } from 'country-flag-icons/react/3x2'; import NoticeModal from './NoticeModal'; @@ -29,7 +29,6 @@ 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'; diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index 3f40a472..84b293b5 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -9,6 +9,7 @@ import { showSuccess, updateAPI, getSystemName, + setUserData } from '../helpers'; import { onGitHubOAuthClicked, @@ -31,7 +32,6 @@ 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 { useTranslation } from 'react-i18next'; import Background from '../images/example.png'; diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index e28c22d4..5f2970cb 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -8,6 +8,18 @@ import { showError, showSuccess, timestamp2string, + renderAudioModelPrice, + renderClaudeLogContent, + renderClaudeModelPrice, + renderClaudeModelPriceSimple, + renderGroup, + renderLogContent, + renderModelPrice, + renderModelPriceSimple, + renderNumber, + renderQuota, + stringToColor, + getLogOther } from '../helpers'; import { @@ -30,21 +42,7 @@ import { 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 Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; -import { getLogOther } from '../helpers/logUtils.js'; import { IconRefresh, IconSetting, diff --git a/web/src/components/OAuth2Callback.js b/web/src/components/OAuth2Callback.js index 9edae986..8a36b443 100644 --- a/web/src/components/OAuth2Callback.js +++ b/web/src/components/OAuth2Callback.js @@ -1,9 +1,8 @@ import React, { useContext, useEffect, useState } from 'react'; import { Spin, Typography, Space } from '@douyinfe/semi-ui'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { API, showError, showSuccess, updateAPI } from '../helpers'; +import { API, showError, showSuccess, updateAPI, setUserData } from '../helpers'; import { UserContext } from '../context/User'; -import { setUserData } from '../helpers/data.js'; const OAuth2Callback = (props) => { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/web/src/components/PageLayout.js b/web/src/components/PageLayout.js index 6ae50cad..76d8e6b7 100644 --- a/web/src/components/PageLayout.js +++ b/web/src/components/PageLayout.js @@ -7,8 +7,7 @@ import { ToastContainer } from 'react-toastify'; import React, { useContext, useEffect } from 'react'; 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 { API, getLogo, getSystemName, showError, setStatusData } from '../helpers'; import { UserContext } from '../context/User/index.js'; import { StatusContext } from '../context/Status/index.js'; import { useLocation } from 'react-router-dom'; diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 4ecb4788..9290d3bd 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -8,6 +8,10 @@ import { showError, showInfo, showSuccess, + getQuotaPerUnit, + renderQuota, + renderQuotaWithPrompt, + stringToColor } from '../helpers'; import Turnstile from 'react-turnstile'; import { UserContext } from '../context/User'; @@ -54,12 +58,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'; diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index f9fd344a..637c7bdf 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -5,10 +5,10 @@ import { showError, showSuccess, timestamp2string, + renderQuota } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import { renderQuota } from '../helpers/render'; import { Button, Card, diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index 24732676..78db98ad 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -8,6 +8,7 @@ import { showSuccess, updateAPI, getSystemName, + setUserData } from '../helpers'; import Turnstile from 'react-turnstile'; import { @@ -30,7 +31,6 @@ import OIDCIcon from './common/logo/OIDCIcon.js'; import LinuxDoIcon from './common/logo/LinuxDoIcon.js'; import WeChatIcon from './common/logo/WeChatIcon.js'; import TelegramLoginButton from 'react-telegram-login/src'; -import { setUserData } from '../helpers/data.js'; import { UserContext } from '../context/User/index.js'; import { useTranslation } from 'react-i18next'; import Background from '../images/example.png'; diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js index d4399f92..97a4bb03 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/SiderBar.js @@ -5,12 +5,8 @@ import { StatusContext } from '../context/Status'; import { useTranslation } from 'react-i18next'; import { - API, - getLogo, - getSystemName, isAdmin, - isMobile, - showError, + showError } from '../helpers'; import '../index.css'; @@ -39,8 +35,6 @@ import { 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 Text from '@douyinfe/semi-ui/lib/es/typography/text'; diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 53ba675d..fa97df48 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/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/TokensTable.js b/web/src/components/TokensTable.js index 92b74550..a3f21f1d 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -6,10 +6,11 @@ import { showError, showSuccess, timestamp2string, + renderGroup, + renderQuota } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import { renderGroup, renderQuota } from '../helpers/render'; import { Button, Card, diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index e5085212..d4bc74a3 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/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, @@ -26,7 +26,6 @@ import { 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 { useTranslation } from 'react-i18next'; diff --git a/web/src/components/common/markdown/MarkdownRenderer.js b/web/src/components/common/markdown/MarkdownRenderer.js index 86fdf96e..a48d34d1 100644 --- a/web/src/components/common/markdown/MarkdownRenderer.js +++ b/web/src/components/common/markdown/MarkdownRenderer.js @@ -13,10 +13,9 @@ import React from 'react'; import { useDebouncedCallback } from 'use-debounce'; import clsx from 'clsx'; import { Button, Tooltip, Toast } from '@douyinfe/semi-ui'; -import { copy } from '../../../helpers/utils'; +import { copy, rehypeSplitWordsIntoSpans } from '../../../helpers'; import { IconCopy } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; -import { rehypeSplitWordsIntoSpans } from '../../../helpers/textAnimationUtils'; mermaid.initialize({ startOnLoad: false, 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/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..d58487b5 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -1,5 +1,6 @@ import { getUserIdFromLocalStorage, showError } from './utils'; import axios from 'axios'; +import { formatMessageForAPI } from './index.js'; export let API = axios.create({ baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL @@ -29,3 +30,108 @@ 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; +}; \ No newline at end of file diff --git a/web/src/helpers/apiUtils.js b/web/src/helpers/apiUtils.js deleted file mode 100644 index 77f775fd..00000000 --- a/web/src/helpers/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/helpers/authUtils.js b/web/src/helpers/auth.js similarity index 100% rename from web/src/helpers/authUtils.js rename to web/src/helpers/auth.js diff --git a/web/src/helpers/index.js b/web/src/helpers/index.js index fae2c6aa..c43be2c9 100644 --- a/web/src/helpers/index.js +++ b/web/src/helpers/index.js @@ -1,8 +1,7 @@ export * from './history'; -export * from './authUtils'; +export * from './auth'; export * from './utils'; export * from './api'; -export * from './apiUtils'; -export * from './messageUtils'; -export * from './textAnimationUtils'; -export * from './logUtils'; +export * from './render'; +export * from './log'; +export * from './data'; diff --git a/web/src/helpers/logUtils.js b/web/src/helpers/log.js similarity index 100% rename from web/src/helpers/logUtils.js rename to web/src/helpers/log.js diff --git a/web/src/helpers/messageUtils.js b/web/src/helpers/messageUtils.js deleted file mode 100644 index 76412dec..00000000 --- a/web/src/helpers/messageUtils.js +++ /dev/null @@ -1,201 +0,0 @@ -import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.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/helpers/render.js b/web/src/helpers/render.js index fa1de023..862dd8eb 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 './index.js'; +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/textAnimationUtils.js b/web/src/helpers/textAnimationUtils.js deleted file mode 100644 index a9bb6db3..00000000 --- a/web/src/helpers/textAnimationUtils.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 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 5087dc19..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 '../constants/playground.constants'; import { - buildApiPayload, - handleApiError -} from '../helpers/apiUtils'; -import { + getUserIdFromLocalStorage, + handleApiError, processThinkTags, processIncompleteThinkTags -} from '../helpers/messageUtils'; +} from '../helpers'; export const useApiRequest = ( setMessage, diff --git a/web/src/hooks/useDataLoader.js b/web/src/hooks/useDataLoader.js index 011a4f7c..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 } from '../helpers/api'; +import { API, processModelsData, processGroupsData } from '../helpers'; import { API_ENDPOINTS } from '../constants/playground.constants'; -import { processModelsData, processGroupsData } from '../helpers/apiUtils'; export const useDataLoader = ( userState, diff --git a/web/src/hooks/useMessageActions.js b/web/src/hooks/useMessageActions.js index 4a0fba69..4cfcf9f1 100644 --- a/web/src/hooks/useMessageActions.js +++ b/web/src/hooks/useMessageActions.js @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { Toast, Modal } from '@douyinfe/semi-ui'; import { useTranslation } from 'react-i18next'; -import { getTextContent } from '../helpers/messageUtils'; +import { getTextContent } from '../helpers'; import { ERROR_MESSAGES } from '../constants/playground.constants'; export const useMessageActions = (message, setMessage, onMessageSend, saveMessages) => { diff --git a/web/src/hooks/useMessageEdit.js b/web/src/hooks/useMessageEdit.js index e91ebae6..479524b6 100644 --- a/web/src/hooks/useMessageEdit.js +++ b/web/src/hooks/useMessageEdit.js @@ -1,7 +1,7 @@ import { useCallback, useState, useRef } from 'react'; import { Toast, Modal } from '@douyinfe/semi-ui'; import { useTranslation } from 'react-i18next'; -import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../helpers/messageUtils'; +import { getTextContent, buildApiPayload, createLoadingAssistantMessage } from '../helpers'; import { MESSAGE_ROLES } from '../constants/playground.constants'; export const useMessageEdit = ( diff --git a/web/src/hooks/usePlaygroundState.js b/web/src/hooks/usePlaygroundState.js index b4b4af47..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 '../constants/playground.constants'; import { loadConfig, saveConfig, loadMessages, saveMessages } from '../components/playground/configStorage'; -import { processIncompleteThinkTags } from '../helpers/messageUtils'; +import { processIncompleteThinkTags } from '../helpers'; export const usePlaygroundState = () => { // 使用惰性初始化,确保只在组件首次挂载时加载配置和消息 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/Playground/index.js b/web/src/pages/Playground/index.js index b495bc15..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 '../../constants/playground.constants.js'; import { + getLogo, + stringToColor, buildMessageContent, createMessage, createLoadingAssistantMessage, getTextContent, buildApiPayload -} from '../../helpers/messageUtils.js'; +} from '../../helpers'; // Components import { 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/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/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/TopUp/index.js b/web/src/pages/TopUp/index.js index d2e16cfc..550af5e0 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, diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index 747628bb..dceb670a 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { API, isMobile, showError, showSuccess } from '../../helpers'; -import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; +import { API, isMobile, showError, showSuccess, renderQuota, renderQuotaWithPrompt } from '../../helpers'; import { Button, Input,