| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { Toast, Pagination } 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'; |
| import { TABLE_COMPACT_MODES_KEY } from '../constants'; |
| import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile'; |
|
|
| const HTMLToastContent = ({ htmlContent }) => { |
| return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />; |
| }; |
| export default HTMLToastContent; |
| export function isAdmin() { |
| let user = localStorage.getItem('user'); |
| if (!user) return false; |
| user = JSON.parse(user); |
| return user.role >= 10; |
| } |
|
|
| export function isRoot() { |
| let user = localStorage.getItem('user'); |
| if (!user) return false; |
| user = JSON.parse(user); |
| return user.role >= 100; |
| } |
|
|
| export function getSystemName() { |
| let system_name = localStorage.getItem('system_name'); |
| if (!system_name) return 'New API'; |
| return system_name; |
| } |
|
|
| export function getLogo() { |
| let logo = localStorage.getItem('logo'); |
| if (!logo) return '/logo.png'; |
| return logo; |
| } |
|
|
| export function getUserIdFromLocalStorage() { |
| let user = localStorage.getItem('user'); |
| if (!user) return -1; |
| user = JSON.parse(user); |
| return user.id; |
| } |
|
|
| export function getFooterHTML() { |
| return localStorage.getItem('footer_html'); |
| } |
|
|
| export async function copy(text) { |
| let okay = true; |
| try { |
| await navigator.clipboard.writeText(text); |
| } catch (e) { |
| try { |
| |
| const textarea = window.document.createElement('textarea'); |
| textarea.value = text; |
| textarea.setAttribute('readonly', ''); |
| textarea.style.position = 'fixed'; |
| textarea.style.left = '-9999px'; |
| textarea.style.top = '-9999px'; |
| window.document.body.appendChild(textarea); |
| textarea.select(); |
| window.document.execCommand('copy'); |
| window.document.body.removeChild(textarea); |
| } catch (e) { |
| okay = false; |
| console.error(e); |
| } |
| } |
| return okay; |
| } |
|
|
| |
|
|
| let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT }; |
| let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT }; |
| let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT }; |
| let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT }; |
| let showNoticeOptions = { autoClose: false }; |
|
|
| const isMobileScreen = window.matchMedia( |
| `(max-width: ${MOBILE_BREAKPOINT - 1}px)`, |
| ).matches; |
| if (isMobileScreen) { |
| showErrorOptions.position = 'top-center'; |
| |
|
|
| showSuccessOptions.position = 'top-center'; |
| |
|
|
| showInfoOptions.position = 'top-center'; |
| |
|
|
| showNoticeOptions.position = 'top-center'; |
| |
| } |
|
|
| export function showError(error) { |
| console.error(error); |
| if (error.message) { |
| if (error.name === 'AxiosError') { |
| switch (error.response.status) { |
| case 401: |
| |
| localStorage.removeItem('user'); |
| |
| window.location.href = '/login?expired=true'; |
| break; |
| case 429: |
| Toast.error('错误:请求次数过多,请稍后再试!'); |
| break; |
| case 500: |
| Toast.error('错误:服务器内部错误,请联系管理员!'); |
| break; |
| case 405: |
| Toast.info('本站仅作演示之用,无服务端!'); |
| break; |
| default: |
| Toast.error('错误:' + error.message); |
| } |
| return; |
| } |
| Toast.error('错误:' + error.message); |
| } else { |
| Toast.error('错误:' + error); |
| } |
| } |
|
|
| export function showWarning(message) { |
| Toast.warning(message); |
| } |
|
|
| export function showSuccess(message) { |
| Toast.success(message); |
| } |
|
|
| export function showInfo(message) { |
| Toast.info(message); |
| } |
|
|
| export function showNotice(message, isHTML = false) { |
| if (isHTML) { |
| toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions); |
| } else { |
| Toast.info(message); |
| } |
| } |
|
|
| export function openPage(url) { |
| window.open(url); |
| } |
|
|
| export function removeTrailingSlash(url) { |
| if (!url) return ''; |
| if (url.endsWith('/')) { |
| return url.slice(0, -1); |
| } else { |
| return url; |
| } |
| } |
|
|
| export function getTodayStartTimestamp() { |
| var now = new Date(); |
| now.setHours(0, 0, 0, 0); |
| return Math.floor(now.getTime() / 1000); |
| } |
|
|
| export function timestamp2string(timestamp) { |
| let date = new Date(timestamp * 1000); |
| let year = date.getFullYear().toString(); |
| let month = (date.getMonth() + 1).toString(); |
| let day = date.getDate().toString(); |
| let hour = date.getHours().toString(); |
| let minute = date.getMinutes().toString(); |
| let second = date.getSeconds().toString(); |
| if (month.length === 1) { |
| month = '0' + month; |
| } |
| if (day.length === 1) { |
| day = '0' + day; |
| } |
| if (hour.length === 1) { |
| hour = '0' + hour; |
| } |
| if (minute.length === 1) { |
| minute = '0' + minute; |
| } |
| if (second.length === 1) { |
| second = '0' + second; |
| } |
| return ( |
| year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second |
| ); |
| } |
|
|
| export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') { |
| let date = new Date(timestamp * 1000); |
| |
| let month = (date.getMonth() + 1).toString(); |
| let day = date.getDate().toString(); |
| let hour = date.getHours().toString(); |
| if (day === '24') { |
| console.log('timestamp', timestamp); |
| } |
| if (month.length === 1) { |
| month = '0' + month; |
| } |
| if (day.length === 1) { |
| day = '0' + day; |
| } |
| if (hour.length === 1) { |
| hour = '0' + hour; |
| } |
| let str = month + '-' + day; |
| if (dataExportDefaultTime === 'hour') { |
| str += ' ' + hour + ':00'; |
| } else if (dataExportDefaultTime === 'week') { |
| let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000); |
| let nextMonth = (nextWeek.getMonth() + 1).toString(); |
| let nextDay = nextWeek.getDate().toString(); |
| if (nextMonth.length === 1) { |
| nextMonth = '0' + nextMonth; |
| } |
| if (nextDay.length === 1) { |
| nextDay = '0' + nextDay; |
| } |
| str += ' - ' + nextMonth + '-' + nextDay; |
| } |
| return str; |
| } |
|
|
| export function downloadTextAsFile(text, filename) { |
| let blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); |
| let url = URL.createObjectURL(blob); |
| let a = document.createElement('a'); |
| a.href = url; |
| a.download = filename; |
| a.click(); |
| } |
|
|
| export const verifyJSON = (str) => { |
| try { |
| JSON.parse(str); |
| } catch (e) { |
| return false; |
| } |
| return true; |
| }; |
|
|
| export function verifyJSONPromise(value) { |
| try { |
| JSON.parse(value); |
| return Promise.resolve(); |
| } catch (e) { |
| return Promise.reject('不是合法的 JSON 字符串'); |
| } |
| } |
|
|
| export function shouldShowPrompt(id) { |
| let prompt = localStorage.getItem(`prompt-${id}`); |
| return !prompt; |
| } |
|
|
| export function setPromptShown(id) { |
| localStorage.setItem(`prompt-${id}`, 'true'); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function compareObjects(oldObject, newObject) { |
| const changedProperties = []; |
|
|
| |
| for (const key in oldObject) { |
| if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) { |
| if (oldObject[key] !== newObject[key]) { |
| changedProperties.push({ |
| key: key, |
| oldValue: oldObject[key], |
| newValue: newObject[key], |
| }); |
| } |
| } |
| } |
|
|
| return changedProperties; |
| } |
|
|
| |
|
|
| |
| 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 : ''; |
| }; |
|
|
| |
| export const processThinkTags = (content, reasoningContent = '') => { |
| if (!content || !content.includes('<think>')) { |
| return { content, reasoningContent }; |
| } |
|
|
| const thoughts = []; |
| const replyParts = []; |
| let lastIndex = 0; |
| let match; |
|
|
| THINK_TAG_REGEX.lastIndex = 0; |
| while ((match = THINK_TAG_REGEX.exec(content)) !== null) { |
| replyParts.push(content.substring(lastIndex, match.index)); |
| thoughts.push(match[1]); |
| lastIndex = match.index + match[0].length; |
| } |
| replyParts.push(content.substring(lastIndex)); |
|
|
| const processedContent = replyParts |
| .join('') |
| .replace(/<\/?think>/g, '') |
| .trim(); |
| const thoughtsStr = thoughts.join('\n\n---\n\n'); |
| const processedReasoningContent = |
| reasoningContent && thoughtsStr |
| ? `${reasoningContent}\n\n---\n\n${thoughtsStr}` |
| : reasoningContent || thoughtsStr; |
|
|
| return { |
| content: processedContent, |
| reasoningContent: processedReasoningContent, |
| }; |
| }; |
|
|
| |
| export const processIncompleteThinkTags = (content, reasoningContent = '') => { |
| if (!content) return { content: '', reasoningContent }; |
|
|
| const lastOpenThinkIndex = content.lastIndexOf('<think>'); |
| if (lastOpenThinkIndex === -1) { |
| return processThinkTags(content, reasoningContent); |
| } |
|
|
| const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex); |
| if (!fragmentAfterLastOpen.includes('</think>')) { |
| const unclosedThought = fragmentAfterLastOpen |
| .substring('<think>'.length) |
| .trim(); |
| const cleanContent = content.substring(0, lastOpenThinkIndex); |
| const processedReasoningContent = unclosedThought |
| ? reasoningContent |
| ? `${reasoningContent}\n\n---\n\n${unclosedThought}` |
| : unclosedThought |
| : reasoningContent; |
|
|
| return processThinkTags(cleanContent, processedReasoningContent); |
| } |
|
|
| return processThinkTags(content, reasoningContent); |
| }; |
|
|
| |
| export const buildMessageContent = ( |
| textContent, |
| imageUrls = [], |
| imageEnabled = false, |
| ) => { |
| if (!textContent && (!imageUrls || imageUrls.length === 0)) { |
| return ''; |
| } |
|
|
| const validImageUrls = imageUrls.filter((url) => url && url.trim() !== ''); |
|
|
| if (imageEnabled && validImageUrls.length > 0) { |
| return [ |
| { type: 'text', text: textContent || '' }, |
| ...validImageUrls.map((url) => ({ |
| type: 'image_url', |
| image_url: { url: url.trim() }, |
| })), |
| ]; |
| } |
|
|
| return textContent || ''; |
| }; |
|
|
| |
| export const createMessage = (role, content, options = {}) => ({ |
| role, |
| content, |
| createAt: Date.now(), |
| id: generateMessageId(), |
| ...options, |
| }); |
|
|
| |
| export const createLoadingAssistantMessage = () => |
| createMessage(MESSAGE_ROLES.ASSISTANT, '', { |
| reasoningContent: '', |
| isReasoningExpanded: true, |
| isThinkingComplete: false, |
| hasAutoCollapsed: false, |
| status: 'loading', |
| }); |
|
|
| |
| export const hasImageContent = (message) => { |
| return ( |
| message && |
| Array.isArray(message.content) && |
| message.content.some((item) => item.type === 'image_url') |
| ); |
| }; |
|
|
| |
| 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; |
| }; |
|
|
| |
| export const getRelativeTime = (publishDate) => { |
| if (!publishDate) return ''; |
|
|
| const now = new Date(); |
| const pubDate = new Date(publishDate); |
|
|
| |
| if (isNaN(pubDate.getTime())) return publishDate; |
|
|
| const diffMs = now.getTime() - pubDate.getTime(); |
| const diffSeconds = Math.floor(diffMs / 1000); |
| const diffMinutes = Math.floor(diffSeconds / 60); |
| const diffHours = Math.floor(diffMinutes / 60); |
| const diffDays = Math.floor(diffHours / 24); |
| const diffWeeks = Math.floor(diffDays / 7); |
| const diffMonths = Math.floor(diffDays / 30); |
| const diffYears = Math.floor(diffDays / 365); |
|
|
| |
| if (diffMs < 0) { |
| return formatDateString(pubDate); |
| } |
|
|
| |
| if (diffSeconds < 60) { |
| return '刚刚'; |
| } else if (diffMinutes < 60) { |
| return `${diffMinutes} 分钟前`; |
| } else if (diffHours < 24) { |
| return `${diffHours} 小时前`; |
| } else if (diffDays < 7) { |
| return `${diffDays} 天前`; |
| } else if (diffWeeks < 4) { |
| return `${diffWeeks} 周前`; |
| } else if (diffMonths < 12) { |
| return `${diffMonths} 个月前`; |
| } else if (diffYears < 2) { |
| return '1 年前'; |
| } else { |
| |
| return formatDateString(pubDate); |
| } |
| }; |
|
|
| |
| export const formatDateString = (date) => { |
| const year = date.getFullYear(); |
| const month = String(date.getMonth() + 1).padStart(2, '0'); |
| const day = String(date.getDate()).padStart(2, '0'); |
| return `${year}-${month}-${day}`; |
| }; |
|
|
| |
| export const formatDateTimeString = (date) => { |
| const year = date.getFullYear(); |
| const month = String(date.getMonth() + 1).padStart(2, '0'); |
| const day = String(date.getDate()).padStart(2, '0'); |
| const hours = String(date.getHours()).padStart(2, '0'); |
| const minutes = String(date.getMinutes()).padStart(2, '0'); |
| return `${year}-${month}-${day} ${hours}:${minutes}`; |
| }; |
|
|
| function readTableCompactModes() { |
| try { |
| const json = localStorage.getItem(TABLE_COMPACT_MODES_KEY); |
| return json ? JSON.parse(json) : {}; |
| } catch { |
| return {}; |
| } |
| } |
|
|
| function writeTableCompactModes(modes) { |
| try { |
| localStorage.setItem(TABLE_COMPACT_MODES_KEY, JSON.stringify(modes)); |
| } catch { |
| |
| } |
| } |
|
|
| export function getTableCompactMode(tableKey = 'global') { |
| const modes = readTableCompactModes(); |
| return !!modes[tableKey]; |
| } |
|
|
| export function setTableCompactMode(compact, tableKey = 'global') { |
| const modes = readTableCompactModes(); |
| modes[tableKey] = compact; |
| writeTableCompactModes(modes); |
| } |
|
|
| |
| |
| |
| |
| export const selectFilter = (input, option) => { |
| if (!input) return true; |
|
|
| const keyword = input.trim().toLowerCase(); |
| const valueText = (option?.value ?? '').toString().toLowerCase(); |
| const labelText = (option?.label ?? '').toString().toLowerCase(); |
|
|
| return valueText.includes(keyword) || labelText.includes(keyword); |
| }; |
|
|
| |
| |
| export const calculateModelPrice = ({ |
| record, |
| selectedGroup, |
| groupRatio, |
| tokenUnit, |
| displayPrice, |
| currency, |
| precision = 4, |
| }) => { |
| |
| let usedGroup = selectedGroup; |
| let usedGroupRatio = groupRatio[selectedGroup]; |
|
|
| if (selectedGroup === 'all' || usedGroupRatio === undefined) { |
| |
| let minRatio = Number.POSITIVE_INFINITY; |
| if ( |
| Array.isArray(record.enable_groups) && |
| record.enable_groups.length > 0 |
| ) { |
| record.enable_groups.forEach((g) => { |
| const r = groupRatio[g]; |
| if (r !== undefined && r < minRatio) { |
| minRatio = r; |
| usedGroup = g; |
| usedGroupRatio = r; |
| } |
| }); |
| } |
|
|
| |
| if (usedGroupRatio === undefined) { |
| usedGroupRatio = 1; |
| } |
| } |
|
|
| |
| if (record.quota_type === 0) { |
| |
| const inputRatioPriceUSD = record.model_ratio * 2 * usedGroupRatio; |
| const completionRatioPriceUSD = |
| record.model_ratio * record.completion_ratio * 2 * usedGroupRatio; |
|
|
| const unitDivisor = tokenUnit === 'K' ? 1000 : 1; |
| const unitLabel = tokenUnit === 'K' ? 'K' : 'M'; |
|
|
| const rawDisplayInput = displayPrice(inputRatioPriceUSD); |
| const rawDisplayCompletion = displayPrice(completionRatioPriceUSD); |
|
|
| const numInput = |
| parseFloat(rawDisplayInput.replace(/[^0-9.]/g, '')) / unitDivisor; |
| const numCompletion = |
| parseFloat(rawDisplayCompletion.replace(/[^0-9.]/g, '')) / unitDivisor; |
|
|
| let symbol = '$'; |
| if (currency === 'CNY') { |
| symbol = '¥'; |
| } else if (currency === 'CUSTOM') { |
| try { |
| const statusStr = localStorage.getItem('status'); |
| if (statusStr) { |
| const s = JSON.parse(statusStr); |
| symbol = s?.custom_currency_symbol || '¤'; |
| } else { |
| symbol = '¤'; |
| } |
| } catch (e) { |
| symbol = '¤'; |
| } |
| } |
| return { |
| inputPrice: `${symbol}${numInput.toFixed(precision)}`, |
| completionPrice: `${symbol}${numCompletion.toFixed(precision)}`, |
| unitLabel, |
| isPerToken: true, |
| usedGroup, |
| usedGroupRatio, |
| }; |
| } |
|
|
| if (record.quota_type === 1) { |
| |
| const priceUSD = parseFloat(record.model_price) * usedGroupRatio; |
| const displayVal = displayPrice(priceUSD); |
|
|
| return { |
| price: displayVal, |
| isPerToken: false, |
| usedGroup, |
| usedGroupRatio, |
| }; |
| } |
|
|
| |
| return { |
| price: '-', |
| isPerToken: false, |
| usedGroup, |
| usedGroupRatio, |
| }; |
| }; |
|
|
| |
| export const formatPriceInfo = (priceData, t) => { |
| if (priceData.isPerToken) { |
| return ( |
| <> |
| <span style={{ color: 'var(--semi-color-text-1)' }}> |
| {t('输入')} {priceData.inputPrice}/{priceData.unitLabel} |
| </span> |
| <span style={{ color: 'var(--semi-color-text-1)' }}> |
| {t('输出')} {priceData.completionPrice}/{priceData.unitLabel} |
| </span> |
| </> |
| ); |
| } |
|
|
| return ( |
| <> |
| <span style={{ color: 'var(--semi-color-text-1)' }}> |
| {t('模型价格')} {priceData.price} |
| </span> |
| </> |
| ); |
| }; |
|
|
| |
| |
| |
| export const createCardProPagination = ({ |
| currentPage, |
| pageSize, |
| total, |
| onPageChange, |
| onPageSizeChange, |
| isMobile = false, |
| pageSizeOpts = [10, 20, 50, 100], |
| showSizeChanger = true, |
| t = (key) => key, |
| }) => { |
| if (!total || total <= 0) return null; |
|
|
| const start = (currentPage - 1) * pageSize + 1; |
| const end = Math.min(currentPage * pageSize, total); |
| const totalText = `${t('显示第')} ${start} ${t('条 - 第')} ${end} ${t('条,共')} ${total} ${t('条')}`; |
|
|
| return ( |
| <> |
| {/* 桌面端左侧总数信息 */} |
| {!isMobile && ( |
| <span |
| className='text-sm select-none' |
| style={{ color: 'var(--semi-color-text-2)' }} |
| > |
| {totalText} |
| </span> |
| )} |
| |
| {/* 右侧分页控件 */} |
| <Pagination |
| currentPage={currentPage} |
| pageSize={pageSize} |
| total={total} |
| pageSizeOpts={pageSizeOpts} |
| showSizeChanger={showSizeChanger} |
| onPageSizeChange={onPageSizeChange} |
| onPageChange={onPageChange} |
| size={isMobile ? 'small' : 'default'} |
| showQuickJumper={isMobile} |
| showTotal |
| /> |
| </> |
| ); |
| }; |
|
|
| |
| const DEFAULT_PRICING_FILTERS = { |
| search: '', |
| showWithRecharge: false, |
| currency: 'USD', |
| showRatio: false, |
| viewMode: 'card', |
| tokenUnit: 'M', |
| filterGroup: 'all', |
| filterQuotaType: 'all', |
| filterEndpointType: 'all', |
| filterVendor: 'all', |
| filterTag: 'all', |
| currentPage: 1, |
| }; |
|
|
| |
| export const resetPricingFilters = ({ |
| handleChange, |
| setShowWithRecharge, |
| setCurrency, |
| setShowRatio, |
| setViewMode, |
| setFilterGroup, |
| setFilterQuotaType, |
| setFilterEndpointType, |
| setFilterVendor, |
| setFilterTag, |
| setCurrentPage, |
| setTokenUnit, |
| }) => { |
| handleChange?.(DEFAULT_PRICING_FILTERS.search); |
| setShowWithRecharge?.(DEFAULT_PRICING_FILTERS.showWithRecharge); |
| setCurrency?.(DEFAULT_PRICING_FILTERS.currency); |
| setShowRatio?.(DEFAULT_PRICING_FILTERS.showRatio); |
| setViewMode?.(DEFAULT_PRICING_FILTERS.viewMode); |
| setTokenUnit?.(DEFAULT_PRICING_FILTERS.tokenUnit); |
| setFilterGroup?.(DEFAULT_PRICING_FILTERS.filterGroup); |
| setFilterQuotaType?.(DEFAULT_PRICING_FILTERS.filterQuotaType); |
| setFilterEndpointType?.(DEFAULT_PRICING_FILTERS.filterEndpointType); |
| setFilterVendor?.(DEFAULT_PRICING_FILTERS.filterVendor); |
| setFilterTag?.(DEFAULT_PRICING_FILTERS.filterTag); |
| setCurrentPage?.(DEFAULT_PRICING_FILTERS.currentPage); |
| }; |
|
|