Compare commits
20 Commits
5fa5f87e1e
...
59cc502e6e
| Author | SHA1 | Date |
|---|---|---|
|
|
59cc502e6e | |
|
|
f2da0cdb24 | |
|
|
86bb4ee0cf | |
|
|
eb045c9a46 | |
|
|
4a5fa8aa3b | |
|
|
20de5cb147 | |
|
|
07a6e29c1e | |
|
|
4f8cde37d8 | |
|
|
6eec903060 | |
|
|
0d798c2fbc | |
|
|
2165430d87 | |
|
|
977e9e8bf5 | |
|
|
189f314734 | |
|
|
4e8d2b8057 | |
|
|
06aa707141 | |
|
|
46819578de | |
|
|
77b885b190 | |
|
|
4fbdafee1c | |
|
|
825954e43f | |
|
|
61b0d902b6 |
|
|
@ -1,7 +1,10 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
)
|
||||
|
||||
// 简化的供应商映射规则
|
||||
|
|
@ -20,6 +23,7 @@ var defaultVendorRules = map[string]string{
|
|||
"qwen": "阿里巴巴",
|
||||
"deepseek": "DeepSeek",
|
||||
"abab": "MiniMax",
|
||||
"minimax": "MiniMax",
|
||||
"ernie": "百度",
|
||||
"spark": "讯飞",
|
||||
"hunyuan": "腾讯",
|
||||
|
|
@ -32,6 +36,7 @@ var defaultVendorRules = map[string]string{
|
|||
"grok": "xAI",
|
||||
"llama": "Meta",
|
||||
"doubao": "字节跳动",
|
||||
"seedance": "字节跳动",
|
||||
"kling": "快手",
|
||||
"jimeng": "即梦",
|
||||
"vidu": "Vidu",
|
||||
|
|
@ -39,10 +44,10 @@ var defaultVendorRules = map[string]string{
|
|||
|
||||
// 供应商默认图标映射
|
||||
var defaultVendorIcons = map[string]string{
|
||||
"OpenAI": "OpenAI",
|
||||
"OpenAI": "OpenAI.Color",
|
||||
"Anthropic": "Claude.Color",
|
||||
"Google": "Gemini.Color",
|
||||
"Moonshot": "Moonshot",
|
||||
"Moonshot": "Moonshot.Color",
|
||||
"智谱": "Zhipu.Color",
|
||||
"阿里巴巴": "Qwen.Color",
|
||||
"DeepSeek": "DeepSeek.Color",
|
||||
|
|
@ -54,25 +59,30 @@ var defaultVendorIcons = map[string]string{
|
|||
"Cloudflare": "Cloudflare.Color",
|
||||
"360": "Ai360.Color",
|
||||
"零一万物": "Yi.Color",
|
||||
"Jina": "Jina",
|
||||
"Jina": "Jina.Color",
|
||||
"Mistral": "Mistral.Color",
|
||||
"xAI": "XAI",
|
||||
"Meta": "Ollama",
|
||||
"xAI": "XAI.Color",
|
||||
"Ollama": "Ollama.Color",
|
||||
"Meta": "Meta.Color",
|
||||
"字节跳动": "Doubao.Color",
|
||||
"快手": "Kling.Color",
|
||||
"即梦": "Jimeng.Color",
|
||||
"Vidu": "Vidu",
|
||||
"微软": "AzureAI",
|
||||
"Microsoft": "AzureAI",
|
||||
"Azure": "AzureAI",
|
||||
"Vidu": "Vidu.Color",
|
||||
"微软": "AzureAI.Color",
|
||||
"Microsoft": "AzureAI.Color",
|
||||
"Azure": "AzureAI.Color",
|
||||
}
|
||||
|
||||
// initDefaultVendorMapping 简化的默认供应商映射
|
||||
func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) {
|
||||
for _, ability := range enableAbilities {
|
||||
modelName := ability.Model
|
||||
if _, exists := metaMap[modelName]; exists {
|
||||
continue
|
||||
if existing, exists := metaMap[modelName]; exists {
|
||||
// 已有记录但 vendor_id 已赋值 → 跳过
|
||||
if existing.VendorID != 0 {
|
||||
continue
|
||||
}
|
||||
// vendor_id=0 → 继续尝试匹配(修复已有 DB 记录但缺供应商的场景)
|
||||
}
|
||||
|
||||
// 匹配供应商
|
||||
|
|
@ -85,12 +95,24 @@ func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vend
|
|||
}
|
||||
}
|
||||
|
||||
// 创建模型元数据
|
||||
metaMap[modelName] = &Model{
|
||||
ModelName: modelName,
|
||||
VendorID: vendorID,
|
||||
Status: 1,
|
||||
NameRule: NameRuleExact,
|
||||
if vendorID == 0 {
|
||||
// 仍未匹配到供应商 → 跳过(不给未匹配的模型写入 vendor_id=0 覆盖已有记录)
|
||||
if strings.Contains(modelLower, "seedance") || strings.Contains(modelLower, "minimax") {
|
||||
common.SysLog(fmt.Sprintf("initDefaultVendorMapping: model %q matched pattern but vendorID=0 (getOrCreateVendor failed)", modelName))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 写入或更新供应商 ID
|
||||
if existing, exists := metaMap[modelName]; exists {
|
||||
existing.VendorID = vendorID
|
||||
} else {
|
||||
metaMap[modelName] = &Model{
|
||||
ModelName: modelName,
|
||||
VendorID: vendorID,
|
||||
Status: 1,
|
||||
NameRule: NameRuleExact,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -112,9 +134,11 @@ func getOrCreateVendor(vendorName string, vendorMap map[int]*Vendor) int {
|
|||
}
|
||||
|
||||
if err := newVendor.Insert(); err != nil {
|
||||
common.SysLog(fmt.Sprintf("getOrCreateVendor: insert vendor %q failed: %v", vendorName, err))
|
||||
return 0
|
||||
}
|
||||
|
||||
common.SysLog(fmt.Sprintf("getOrCreateVendor: created new vendor %q with id=%d", vendorName, newVendor.Id))
|
||||
vendorMap[newVendor.Id] = newVendor
|
||||
return newVendor.Id
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,7 +323,8 @@ var vendorKeywordAliases = []struct {
|
|||
{"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}},
|
||||
{"moonshot", []string{"moonshot"}},
|
||||
{"kimi", []string{"moonshot"}},
|
||||
{"doubao", []string{"bytedance", "volcengine", "volcano"}},
|
||||
{"doubao", []string{"bytedance", "volcengine", "volcano", "字节跳动", "字节"}},
|
||||
{"seedance", []string{"bytedance", "volcengine", "volcano", "doubao", "字节跳动", "字节"}},
|
||||
{"ernie", []string{"baidu"}},
|
||||
{"wenxin", []string{"baidu"}},
|
||||
{"hunyuan", []string{"tencent"}},
|
||||
|
|
|
|||
|
|
@ -272,6 +272,7 @@ var defaultModelRatio = map[string]float64{
|
|||
"deepseek-ai/DeepSeek-R1": 0.8,
|
||||
"deepseek-ai/DeepSeek-V3-0324": 0.8,
|
||||
"deepseek-ai/DeepSeek-V3.1": 0.8,
|
||||
"DeepSeek-V3.2": 0.8,
|
||||
}
|
||||
|
||||
var defaultModelPrice = map[string]float64{
|
||||
|
|
@ -870,6 +871,8 @@ func GetCompletionRatioCopy() map[string]float64 {
|
|||
|
||||
// 转换模型名,减少渠道必须配置各种带参数模型
|
||||
func FormatMatchingModelName(name string) string {
|
||||
// 去除空格,使 "Seedance 2.0" 与 "Seedance2.0" 等变体能匹配
|
||||
name = strings.ReplaceAll(name, " ", "")
|
||||
|
||||
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
|
||||
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")
|
||||
|
|
|
|||
|
|
@ -378,7 +378,6 @@ const HomeModelList = () => {
|
|||
<PricingCardView
|
||||
filteredModels={pricingData.filteredModels}
|
||||
loading={pricingData.loading}
|
||||
rowSelection={null}
|
||||
pageSize={pricingData.pageSize}
|
||||
setPageSize={pricingData.setPageSize}
|
||||
currentPage={pricingData.currentPage}
|
||||
|
|
@ -387,9 +386,6 @@ const HomeModelList = () => {
|
|||
groupRatio={pricingData.groupRatio}
|
||||
groupModelPrice={pricingData.groupModelPrice}
|
||||
groupModelRatio={pricingData.groupModelRatio}
|
||||
copyText={pricingData.copyText}
|
||||
setModalImageUrl={pricingData.setModalImageUrl}
|
||||
setIsModalOpenurl={pricingData.setIsModalOpenurl}
|
||||
currency={pricingData.currency}
|
||||
siteDisplayType={pricingData.siteDisplayType}
|
||||
tokenUnit={pricingData.tokenUnit}
|
||||
|
|
@ -399,10 +395,7 @@ const HomeModelList = () => {
|
|||
pricingData.channelVideoCompletionRatio
|
||||
}
|
||||
channelVideoPrice={pricingData.channelVideoPrice}
|
||||
showRatio={false}
|
||||
t={pricingData.t}
|
||||
selectedRowKeys={[]}
|
||||
setSelectedRowKeys={() => {}}
|
||||
openModelDetail={pricingData.openModelDetail}
|
||||
showSizeChanger={false}
|
||||
blurPricing={blurPricing}
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||
const chatMenuItems = useMemo(() => {
|
||||
const items = [
|
||||
{
|
||||
text: t('体验馆'),
|
||||
text: t('体验'),
|
||||
itemKey: 'playground',
|
||||
to: '/playground',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
const ROOT_MARGIN = '280px 0px 280px 0px';
|
||||
|
||||
/**
|
||||
* 体验馆消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
||||
* 体验消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
||||
* variant=media:更高占位,避免图片/视频模式切换后列表高度不足导致滚不到底部。
|
||||
*/
|
||||
const LazyVisibleMessage = ({
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ import {
|
|||
import ParameterControl from './ParameterControl';
|
||||
|
||||
/**
|
||||
* 体验馆分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
||||
* 体验分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
||||
* @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数
|
||||
*/
|
||||
const renderPlaygroundGroupOption = (item) =>
|
||||
|
|
@ -85,7 +85,7 @@ const SettingsPanel = ({
|
|||
const mediaModeEnabled = isImageMode || isVideoMode;
|
||||
const videoMediaHint = isVideoMode
|
||||
? t(
|
||||
'体验馆视频素材提示',
|
||||
'体验视频素材提示',
|
||||
'图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。',
|
||||
)
|
||||
: '';
|
||||
|
|
|
|||
|
|
@ -83,11 +83,11 @@ const VideoUrlInput = ({
|
|||
|
||||
{!enabled ? (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{t('体验馆视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
||||
{t('体验视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
||||
</Typography.Text>
|
||||
) : list.length === 0 ? (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
{t('体验馆视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
||||
{t('体验视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||
|
|
|
|||
|
|
@ -212,11 +212,11 @@ const NotificationSettings = ({
|
|||
{
|
||||
key: 'chat',
|
||||
title: t('聊天区域'),
|
||||
description: t('体验馆和聊天功能'),
|
||||
description: t('体验和聊天功能'),
|
||||
modules: [
|
||||
{
|
||||
key: 'playground',
|
||||
title: t('体验馆'),
|
||||
title: t('体验'),
|
||||
description: t('AI模型测试环境'),
|
||||
},
|
||||
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Search } from 'lucide-react';
|
||||
import PricingCardView from '../view/card/PricingCardView';
|
||||
|
||||
// 顶部 Tab 到 tag 值的映射
|
||||
const TAB_TO_TAG = {
|
||||
all: 'all',
|
||||
text: '文本',
|
||||
image: '图片',
|
||||
audio: '音频',
|
||||
video: '视频',
|
||||
};
|
||||
|
||||
const TYPE_TABS = [
|
||||
{ key: 'all', labelKey: '全部' },
|
||||
{ key: 'text', labelKey: '文本' },
|
||||
{ key: 'image', labelKey: '图片' },
|
||||
{ key: 'audio', labelKey: '音频' },
|
||||
{ key: 'video', labelKey: '视频' },
|
||||
];
|
||||
|
||||
const ModelsContent = ({
|
||||
filteredModels,
|
||||
filterTag, setFilterTag,
|
||||
searchValue, setSearchValue,
|
||||
loading,
|
||||
isMobile,
|
||||
blurPricing,
|
||||
t,
|
||||
...cardProps
|
||||
}) => {
|
||||
// 根据当前 filterTag 反推激活的 tab key
|
||||
const activeTabKey = filterTag === 'all' ? 'all'
|
||||
: (Object.entries(TAB_TO_TAG).find(([, v]) => v === filterTag)?.[0] || 'all');
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Type Tabs */}
|
||||
<div className='models-tabs-bar'>
|
||||
{TYPE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
className={`models-tab-btn${activeTabKey === tab.key ? ' active' : ''}`}
|
||||
onClick={() => setFilterTag(TAB_TO_TAG[tab.key])}
|
||||
>
|
||||
{t(tab.labelKey)}
|
||||
</button>
|
||||
))}
|
||||
<div className='flex-1' />
|
||||
<div className='relative'>
|
||||
<Search size={15} className='absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/60' />
|
||||
<input
|
||||
type='text'
|
||||
placeholder={t('搜索模型名称...')}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className='w-48 bg-muted/30 border border-input rounded-lg pl-9 pr-3 py-1.5 text-sm text-foreground placeholder-muted-foreground/50 focus:outline-none focus:border-ring focus:bg-muted/50 transition-all'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Cards Grid */}
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
<PricingCardView
|
||||
filteredModels={filteredModels}
|
||||
loading={loading}
|
||||
blurPricing={blurPricing}
|
||||
t={t}
|
||||
gridCols={2}
|
||||
{...cardProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsContent;
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
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.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { usePricingFilterCounts } from '../../../../hooks/model-pricing/usePricingFilterCounts';
|
||||
import { getLobeHubIcon } from '../../../../helpers';
|
||||
|
||||
const ModelsSidebar = ({
|
||||
models,
|
||||
filterVendor, setFilterVendor,
|
||||
filterSupplierType, setFilterSupplierType,
|
||||
filterTag, setFilterTag,
|
||||
filterGroup, filterQuotaType, filterEndpointType, searchValue,
|
||||
loading, t,
|
||||
}) => {
|
||||
const { vendorModels, tagModels, supplierTypeModels } = usePricingFilterCounts({
|
||||
models,
|
||||
filterGroup,
|
||||
filterQuotaType,
|
||||
filterEndpointType,
|
||||
filterVendor,
|
||||
filterTag,
|
||||
filterSupplierType,
|
||||
searchValue,
|
||||
});
|
||||
|
||||
// ─── 作者 (Vendors) ───
|
||||
const vendors = React.useMemo(() => {
|
||||
const names = [...new Set(models.map((m) => m.vendor_name).filter(Boolean))].sort();
|
||||
return names.map((name) => {
|
||||
const icon = models.find((m) => m.vendor_name === name)?.vendor_icon;
|
||||
return {
|
||||
name,
|
||||
icon,
|
||||
count: vendorModels.filter((m) => m.vendor_name === name).length,
|
||||
};
|
||||
});
|
||||
}, [models, vendorModels]);
|
||||
|
||||
// ─── 服务商 (Supplier Types) ───
|
||||
const supplierTypes = React.useMemo(() => {
|
||||
const types = new Set();
|
||||
models.forEach((m) => {
|
||||
(m.channel_list || []).forEach((ch) => {
|
||||
if (ch.supplier_type) types.add(ch.supplier_type);
|
||||
});
|
||||
});
|
||||
return [...types].sort();
|
||||
}, [models]);
|
||||
|
||||
// ─── 输入模态 (Tags) ───
|
||||
const TAG_ORDER = ['文本', '图片', '音频', '视频'];
|
||||
const tags = React.useMemo(() => {
|
||||
const allTags = new Set();
|
||||
models.forEach((m) => {
|
||||
if (m.tags) {
|
||||
String(m.tags).split(/[,;|]/).map(s => s.trim()).filter(Boolean).forEach(t => allTags.add(t));
|
||||
}
|
||||
});
|
||||
// 使用预定义顺序,不在预定义列表中的标签追加到末尾
|
||||
const ordered = TAG_ORDER.filter(t => allTags.has(t));
|
||||
const rest = [...allTags].filter(t => !TAG_ORDER.includes(t));
|
||||
return [...ordered, ...rest];
|
||||
}, [models]);
|
||||
|
||||
const FilterItem = ({ label, count, active, onClick }) => (
|
||||
<div
|
||||
className={`models-filter-item${active ? ' active' : ''}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span className='count'>{count}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 作者 */}
|
||||
<div className='models-filter-section'>
|
||||
<div className='models-filter-title'>{t('作者')}</div>
|
||||
<FilterItem label={t('全部')} count={models.length} active={filterVendor === 'all'} onClick={() => setFilterVendor('all')} />
|
||||
{vendors.map((v) => (
|
||||
<FilterItem
|
||||
key={v.name}
|
||||
label={
|
||||
<span className='flex items-center gap-2'>
|
||||
{v.icon && (
|
||||
<img
|
||||
src={getLobeHubIcon(v.icon)}
|
||||
alt=''
|
||||
className='w-4 h-4 rounded-full'
|
||||
onError={(e) => { e.target.style.display = 'none'; }}
|
||||
/>
|
||||
)}
|
||||
<span>{v.name}</span>
|
||||
</span>
|
||||
}
|
||||
count={v.count}
|
||||
active={filterVendor === v.name}
|
||||
onClick={() => setFilterVendor(filterVendor === v.name ? 'all' : v.name)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 服务商 */}
|
||||
<div className='models-filter-section'>
|
||||
<div className='models-filter-title'>{t('服务商')}</div>
|
||||
<FilterItem label={t('全部')} count={models.length} active={filterSupplierType === 'all'} onClick={() => setFilterSupplierType('all')} />
|
||||
{supplierTypes.map((type) => (
|
||||
<FilterItem
|
||||
key={type}
|
||||
label={type}
|
||||
count={supplierTypeModels.filter((m) =>
|
||||
(m.channel_list || []).some((ch) => ch.supplier_type === type)
|
||||
).length}
|
||||
active={filterSupplierType === type}
|
||||
onClick={() => setFilterSupplierType(filterSupplierType === type ? 'all' : type)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 输入模态 */}
|
||||
<div className='models-filter-section'>
|
||||
<div className='models-filter-title'>{t('输入模态')}</div>
|
||||
<FilterItem label={t('全部')} count={models.length} active={filterTag === 'all'} onClick={() => setFilterTag('all')} />
|
||||
{tags.map((tag) => (
|
||||
<FilterItem
|
||||
key={tag}
|
||||
label={tag}
|
||||
count={tagModels.filter((m) =>
|
||||
m.tags && String(m.tags).split(/[,;|]/).map(s => s.trim()).includes(tag)
|
||||
).length}
|
||||
active={filterTag === tag}
|
||||
onClick={() => setFilterTag(filterTag === tag ? 'all' : tag)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelsSidebar;
|
||||
|
|
@ -18,9 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import { Layout, ImagePreview } from '@douyinfe/semi-ui';
|
||||
import PricingSidebar from './PricingSidebar';
|
||||
import PricingContent from './content/PricingContent';
|
||||
import { ImagePreview } from '@douyinfe/semi-ui';
|
||||
import ModelsSidebar from './ModelsSidebar';
|
||||
import ModelsContent from './ModelsContent';
|
||||
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
|
||||
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
|
@ -29,7 +29,6 @@ import { UserContext } from '../../../../context/User';
|
|||
|
||||
const PricingPage = () => {
|
||||
const pricingData = useModelPricingData();
|
||||
const { Sider, Content } = Layout;
|
||||
const isMobile = useIsMobile();
|
||||
const [showRatio, setShowRatio] = React.useState(false);
|
||||
const [viewMode, setViewMode] = React.useState('card');
|
||||
|
|
@ -60,22 +59,22 @@ const PricingPage = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='bg-white'>
|
||||
<Layout className='pricing-layout'>
|
||||
<div className='bg-background'>
|
||||
<div className='models-page-layout'>
|
||||
{!isMobile && (
|
||||
<Sider className='pricing-scroll-hide pricing-sidebar'>
|
||||
<PricingSidebar {...allProps} />
|
||||
</Sider>
|
||||
<aside className='models-sidebar'>
|
||||
<ModelsSidebar {...allProps} />
|
||||
</aside>
|
||||
)}
|
||||
|
||||
<Content className='pricing-scroll-hide pricing-content'>
|
||||
<PricingContent
|
||||
<main className='models-content'>
|
||||
<ModelsContent
|
||||
{...allProps}
|
||||
isMobile={isMobile}
|
||||
sidebarProps={allProps}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<ImagePreview
|
||||
src={pricingData.modalImageUrl}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SideSheet, Typography, Button } from '@douyinfe/semi-ui';
|
||||
import { IconClose } from '@douyinfe/semi-icons';
|
||||
import { IconClose, IconPlayCircle } from '@douyinfe/semi-icons';
|
||||
|
||||
import { API } from '../../../../helpers';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
|
|
@ -65,6 +66,21 @@ const ModelDetailSideSheet = ({
|
|||
channelVideoPriceMap = {},
|
||||
}) => {
|
||||
const isMobile = useIsMobile();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleExperience = () => {
|
||||
const modelName = modelData?.model_name;
|
||||
const vendorId = modelData?.vendor_id;
|
||||
if (modelName) {
|
||||
const params = new URLSearchParams();
|
||||
params.set('model', modelName);
|
||||
if (vendorId != null) {
|
||||
params.set('model_type', vendorId);
|
||||
}
|
||||
navigate(`/console/playground?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* channel_id -> 单测/运营展示 DTO(打开详情时按需拉取,不并入 /pricing)
|
||||
*/
|
||||
|
|
@ -121,7 +137,18 @@ const ModelDetailSideSheet = ({
|
|||
<SideSheet
|
||||
placement='right'
|
||||
title={
|
||||
<ModelHeader modelData={modelData} vendorsMap={vendorsMap} t={t} />
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<ModelHeader modelData={modelData} vendorsMap={vendorsMap} t={t} />
|
||||
<Button
|
||||
icon={<IconPlayCircle />}
|
||||
theme='solid'
|
||||
type='primary'
|
||||
size='small'
|
||||
onClick={handleExperience}
|
||||
>
|
||||
{t('体验')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
bodyStyle={{
|
||||
padding: '0',
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
|
|||
|
||||
const PricingCardSkeleton = ({
|
||||
skeletonCount = 100,
|
||||
rowSelection = false,
|
||||
showRatio = false,
|
||||
}) => {
|
||||
const placeholder = (
|
||||
<div className='px-2 pt-2'>
|
||||
|
|
@ -34,8 +32,8 @@ const PricingCardSkeleton = ({
|
|||
className='!rounded-2xl border border-gray-200'
|
||||
bodyStyle={{ padding: '24px' }}
|
||||
>
|
||||
{/* 头部:图标 + 模型名称 + 操作按钮 */}
|
||||
<div className='flex items-start justify-between mb-3'>
|
||||
{/* 头部:图标 + 模型名称 */}
|
||||
<div className='flex items-start mb-3'>
|
||||
<div className='flex items-start space-x-3 flex-1 min-w-0'>
|
||||
{/* 模型图标骨架 */}
|
||||
<div className='w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-sm'>
|
||||
|
|
@ -44,41 +42,17 @@ const PricingCardSkeleton = ({
|
|||
style={{ width: 48, height: 48, borderRadius: 16 }}
|
||||
/>
|
||||
</div>
|
||||
{/* 模型名称和价格区域 */}
|
||||
{/* 模型名称 */}
|
||||
<div className='flex-1 min-w-0'>
|
||||
{/* 模型名称骨架 */}
|
||||
<Skeleton.Title
|
||||
style={{
|
||||
width: `${120 + (index % 3) * 30}px`,
|
||||
height: 20,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
/>
|
||||
{/* 价格信息骨架 */}
|
||||
<Skeleton.Title
|
||||
style={{
|
||||
width: `${160 + (index % 4) * 20}px`,
|
||||
height: 20,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-2 ml-3'>
|
||||
{/* 复制按钮骨架 */}
|
||||
<Skeleton.Button
|
||||
size='small'
|
||||
style={{ width: 16, height: 16, borderRadius: 4 }}
|
||||
/>
|
||||
{/* 勾选框骨架 */}
|
||||
{rowSelection && (
|
||||
<Skeleton.Button
|
||||
size='small'
|
||||
style={{ width: 16, height: 16, borderRadius: 2 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模型描述骨架 */}
|
||||
|
|
@ -90,43 +64,25 @@ const PricingCardSkeleton = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 标签区域骨架 */}
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{Array.from({ length: 2 + (index % 3) }).map((_, tagIndex) => (
|
||||
<Skeleton.Button
|
||||
key={tagIndex}
|
||||
size='small'
|
||||
style={{
|
||||
width: 64,
|
||||
height: 18,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
{/* 价格骨架:输入 + 输出并排 */}
|
||||
<div className='flex gap-3'>
|
||||
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||
<Skeleton.Title
|
||||
style={{ width: '80%', height: 14, marginBottom: 8 }}
|
||||
/>
|
||||
<Skeleton.Title
|
||||
style={{ width: '100%', height: 18, marginBottom: 0 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 倍率信息骨架(可选) */}
|
||||
{showRatio && (
|
||||
<div className='mt-4 pt-3 border-t border-gray-100'>
|
||||
<div className='flex items-center space-x-1 mb-2'>
|
||||
<Skeleton.Title
|
||||
style={{ width: 60, height: 12, marginBottom: 0 }}
|
||||
/>
|
||||
<Skeleton.Button
|
||||
size='small'
|
||||
style={{ width: 14, height: 14, borderRadius: 7 }}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
{Array.from({ length: 3 }).map((_, ratioIndex) => (
|
||||
<Skeleton.Title
|
||||
key={ratioIndex}
|
||||
style={{ width: '100%', height: 12, marginBottom: 0 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||
<Skeleton.Title
|
||||
style={{ width: '80%', height: 14, marginBottom: 8 }}
|
||||
/>
|
||||
<Skeleton.Title
|
||||
style={{ width: '100%', height: 18, marginBottom: 0 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,22 +20,15 @@ For commercial licensing, please contact support@quantumnous.com
|
|||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
Empty,
|
||||
Pagination,
|
||||
Button,
|
||||
Avatar,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||
import { Copy } from 'lucide-react';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import {
|
||||
stringToColor,
|
||||
calculateModelPrice,
|
||||
getModelPriceItems,
|
||||
getLobeHubIcon,
|
||||
|
|
@ -45,7 +38,6 @@ import {
|
|||
} from '../../../../../helpers';
|
||||
import PricingCardSkeleton from './PricingCardSkeleton';
|
||||
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
|
||||
import { renderLimitedItems } from '../../../../common/ui/RenderUtils';
|
||||
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
|
||||
const CARD_STYLES = {
|
||||
container:
|
||||
|
|
@ -60,7 +52,6 @@ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|||
const PricingCardView = ({
|
||||
filteredModels,
|
||||
loading,
|
||||
rowSelection,
|
||||
pageSize,
|
||||
setPageSize,
|
||||
currentPage,
|
||||
|
|
@ -69,17 +60,11 @@ const PricingCardView = ({
|
|||
groupRatio,
|
||||
groupModelPrice,
|
||||
groupModelRatio,
|
||||
copyText,
|
||||
setModalImageUrl,
|
||||
setIsModalOpenurl,
|
||||
currency,
|
||||
siteDisplayType,
|
||||
tokenUnit,
|
||||
displayPrice,
|
||||
showRatio,
|
||||
t,
|
||||
selectedRowKeys = [],
|
||||
setSelectedRowKeys,
|
||||
openModelDetail,
|
||||
showSizeChanger = true,
|
||||
blurPricing = false,
|
||||
|
|
@ -87,6 +72,7 @@ const PricingCardView = ({
|
|||
channelVideoRatio = {},
|
||||
channelVideoCompletionRatio = {},
|
||||
channelVideoPrice = {},
|
||||
gridCols = 0, // 当 > 0 时使用 grid 布局(如 gridCols=2 表示 2 列)
|
||||
}) => {
|
||||
const showSkeleton = useMinimumLoadingTime(loading);
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
|
|
@ -121,62 +107,6 @@ const PricingCardView = ({
|
|||
);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (model, checked) => {
|
||||
if (!setSelectedRowKeys) return;
|
||||
const modelKey = getModelKey(model);
|
||||
const newKeys = checked
|
||||
? Array.from(new Set([...selectedRowKeys, modelKey]))
|
||||
: selectedRowKeys.filter((key) => key !== modelKey);
|
||||
setSelectedRowKeys(newKeys);
|
||||
rowSelection?.onChange?.(newKeys, null);
|
||||
};
|
||||
|
||||
// 根据 supplier_type 返回对应的 Tag 颜色
|
||||
const getSupplierTypeColor = (supplierType) => {
|
||||
switch (supplierType) {
|
||||
case '公有云':
|
||||
return 'green';
|
||||
case 'AIDC':
|
||||
return 'light-green';
|
||||
case '企业中转站':
|
||||
return 'lime';
|
||||
case '个人中转站':
|
||||
return 'yellow';
|
||||
default:
|
||||
return stringToColor(supplierType);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据模型的 channel_list 推导可展示的供应商项。
|
||||
// 无 logo 时不展示 supplier_alias,仅保留供应商类型标签。
|
||||
const getSupplierLogos = (model) => {
|
||||
if (!model?.channel_list || model.channel_list.length === 0) return [];
|
||||
const seen = new Set();
|
||||
const items = [];
|
||||
model.channel_list.forEach((ch, idx) => {
|
||||
const logo =
|
||||
(ch?.company_logo_url && String(ch.company_logo_url).trim()) || '';
|
||||
const supplierType =
|
||||
(ch?.supplier_type && String(ch.supplier_type).trim()) || '';
|
||||
const alias =
|
||||
(ch?.supplier_alias && String(ch.supplier_alias).trim()) || '';
|
||||
const name = ch?.channel_name || '';
|
||||
if (!logo && !supplierType) return;
|
||||
const displayAlias = logo ? alias : '';
|
||||
const dedupKey = `${logo}|${supplierType}|${displayAlias}`;
|
||||
if (seen.has(dedupKey)) return;
|
||||
seen.add(dedupKey);
|
||||
items.push({
|
||||
key: ch?.channel_id ?? `${dedupKey}-${idx}`,
|
||||
logo,
|
||||
supplierType,
|
||||
alias: displayAlias,
|
||||
name,
|
||||
});
|
||||
});
|
||||
return items;
|
||||
};
|
||||
|
||||
const calculateChannelPrices = (model, opts = {}) => {
|
||||
const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
|
||||
if (!model.channel_list || model.channel_list.length === 0) {
|
||||
|
|
@ -765,77 +695,11 @@ const PricingCardView = ({
|
|||
return record.description || '';
|
||||
};
|
||||
|
||||
// 渲染标签
|
||||
const renderTags = (record) => {
|
||||
// 计费类型标签(左边)- 使用 channel_list[0].quota_type
|
||||
const channelQuotaType =
|
||||
record.channel_list && record.channel_list.length > 0
|
||||
? record.channel_list[0].quota_type
|
||||
: record.quota_type;
|
||||
|
||||
let billingTag = (
|
||||
<Tag key='billing' shape='circle' color='white' size='small'>
|
||||
-
|
||||
</Tag>
|
||||
);
|
||||
if (channelQuotaType === 1) {
|
||||
billingTag = (
|
||||
<Tag key='billing' shape='circle' color='teal' size='small'>
|
||||
{t('按次计费')}
|
||||
</Tag>
|
||||
);
|
||||
} else if (channelQuotaType === 0) {
|
||||
billingTag = (
|
||||
<Tag key='billing' shape='circle' color='violet' size='small'>
|
||||
{t('按量计费')}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
// 自定义标签(右边)
|
||||
const customTags = [];
|
||||
if (record.tags) {
|
||||
const tagArr = record.tags.split(',').filter(Boolean);
|
||||
tagArr.forEach((tg, idx) => {
|
||||
customTags.push(
|
||||
<Tag
|
||||
key={`custom-${idx}`}
|
||||
shape='circle'
|
||||
color={stringToColor(tg)}
|
||||
size='small'
|
||||
>
|
||||
{renderHighlightedText(tg)}
|
||||
</Tag>,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>{billingTag}</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
{customTags.length > 0 &&
|
||||
renderLimitedItems({
|
||||
items: customTags.map((tag, idx) => ({
|
||||
key: `custom-${idx}`,
|
||||
element: tag,
|
||||
})),
|
||||
renderItem: (item, idx) => item.element,
|
||||
maxDisplay: 3,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 显示骨架屏
|
||||
if (showSkeleton) {
|
||||
return (
|
||||
<>
|
||||
<PricingCardSkeleton
|
||||
rowSelection={!!rowSelection}
|
||||
showRatio={showRatio}
|
||||
/>
|
||||
<PricingCardSkeleton />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -858,11 +722,10 @@ const PricingCardView = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className='px-2 pt-2'>
|
||||
<div className='flex flex-wrap gap-4'>
|
||||
<div className={gridCols > 0 ? '' : 'px-2 pt-2'}>
|
||||
<div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}>
|
||||
{paginatedModels.map((model, index) => {
|
||||
const modelKey = getModelKey(model);
|
||||
const isSelected = selectedRowKeys.includes(modelKey);
|
||||
|
||||
const priceData = calculateModelPrice({
|
||||
record: model,
|
||||
|
|
@ -876,166 +739,31 @@ const PricingCardView = ({
|
|||
quotaDisplayType: siteDisplayType,
|
||||
});
|
||||
|
||||
const supplierLogos = getSupplierLogos(model);
|
||||
const hasChannelList =
|
||||
Array.isArray(model.channel_list) &&
|
||||
model.channel_list.length > 0;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={modelKey || index}
|
||||
className={`flex-1 min-w-[350px] max-w-[600px] !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default}`}
|
||||
className={`${gridCols > 0 ? '!w-full' : 'flex-1 min-w-[350px] max-w-[600px]'} !rounded-2xl transition-all duration-200 hover:shadow-lg border ${blurPricing ? '' : 'cursor-pointer'} ${CARD_STYLES.default}`}
|
||||
bodyStyle={{ height: '100%' }}
|
||||
onClick={() =>
|
||||
!blurPricing && openModelDetail && openModelDetail(model)
|
||||
}
|
||||
>
|
||||
<div className='flex flex-col h-full'>
|
||||
{/* 头部:图标 + 模型名称 + 操作按钮 */}
|
||||
<div className='flex items-start justify-between mb-3'>
|
||||
{/* 头部:图标 + 模型名称 */}
|
||||
<div className='flex items-start mb-3'>
|
||||
<div className='flex items-start space-x-3 flex-1 min-w-0'>
|
||||
{getModelIcon(model)}
|
||||
<div className='flex-1 min-w-0'>
|
||||
<h3 className='text-lg font-bold text-gray-900 truncate'>
|
||||
{renderHighlightedText(model.model_name)}
|
||||
</h3>
|
||||
<div
|
||||
className='flex flex-col gap-1 text-xs mt-1'
|
||||
style={
|
||||
blurPricing
|
||||
? {
|
||||
filter: 'blur(6px)',
|
||||
userSelect: 'none',
|
||||
pointerEvents: 'none',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{getModelPriceItemsForCard(model, priceData).map(
|
||||
(item) => (
|
||||
<div key={item.key} className='flex items-center'>
|
||||
<span className='w-20 flex-shrink-0'>
|
||||
{item.label}
|
||||
</span>
|
||||
<span className='flex-1 font-bold text-black inline-flex items-center flex-wrap gap-1'>
|
||||
{item.valueNode ? (
|
||||
item.valueNode
|
||||
) : item.original ? (
|
||||
<>
|
||||
<span className='line-through text-gray-400 font-normal text-[10px]'>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--semi-color-primary)',
|
||||
}}
|
||||
>
|
||||
官方
|
||||
</span>{' '}
|
||||
{item.original.text}
|
||||
</span>
|
||||
<Tag
|
||||
color='red'
|
||||
size='small'
|
||||
shape='circle'
|
||||
>
|
||||
-{item.original.discount}%
|
||||
</Tag>
|
||||
<span>
|
||||
<span
|
||||
style={{
|
||||
color: 'var(--semi-color-warning)',
|
||||
}}
|
||||
>
|
||||
我们
|
||||
</span>{' '}
|
||||
{item.value}
|
||||
{item.suffix}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
{item.value}
|
||||
{item.suffix}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
<div className='flex items-center'>
|
||||
<span className='w-20 flex-shrink-0'>
|
||||
{t('供应商')}
|
||||
</span>
|
||||
<div className='flex-1 flex items-center flex-wrap gap-1'>
|
||||
{supplierLogos.length === 0 ? (
|
||||
hasChannelList ? null : (
|
||||
<span className='font-bold text-black'>
|
||||
{t('官方')}
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
supplierLogos.map((s) => (
|
||||
<div
|
||||
key={s.key}
|
||||
className='h-7 rounded-md flex items-center overflow-hidden'
|
||||
style={{backgroundColor: 'var(--semi-color-fill-0)'}}
|
||||
>
|
||||
{s.logo ? (
|
||||
<img
|
||||
src={s.logo}
|
||||
alt={s.alias || s.name || ''}
|
||||
className='w-7 h-7 object-contain rounded-md'
|
||||
/>
|
||||
) : null}
|
||||
{s.supplierType && (
|
||||
<Tag
|
||||
size='small'
|
||||
shape='circle'
|
||||
color={getSupplierTypeColor(
|
||||
s.supplierType,
|
||||
)}
|
||||
className='mx-1'
|
||||
>
|
||||
{s.supplierType}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center space-x-2 ml-3'>
|
||||
{/* 复制按钮 */}
|
||||
<Button
|
||||
size='small'
|
||||
theme='outline'
|
||||
type='tertiary'
|
||||
icon={<Copy size={12} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyText(model.model_name);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 选择框 */}
|
||||
{rowSelection && (
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCheckboxChange(model, e.target.checked);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 模型描述 - 占据剩余空间 */}
|
||||
{/* 模型描述 */}
|
||||
<div
|
||||
className='flex-1 mb-4'
|
||||
className='mb-4'
|
||||
style={
|
||||
blurPricing
|
||||
? {
|
||||
|
|
@ -1054,7 +782,7 @@ const PricingCardView = ({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* 底部区域 */}
|
||||
{/* 价格:输入 + 输出并排 */}
|
||||
<div
|
||||
className='mt-auto'
|
||||
style={
|
||||
|
|
@ -1067,49 +795,39 @@ const PricingCardView = ({
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
{/* 标签区域 */}
|
||||
{renderTags(model)}
|
||||
|
||||
{/* 倍率信息(可选) */}
|
||||
{showRatio && (
|
||||
<div className='pt-3'>
|
||||
<div className='flex items-center space-x-1 mb-2'>
|
||||
<span className='text-xs font-medium text-gray-700'>
|
||||
{t('倍率信息')}
|
||||
</span>
|
||||
<Tooltip
|
||||
content={t('倍率是为了方便换算不同价格的模型')}
|
||||
>
|
||||
<IconHelpCircle
|
||||
className='text-blue-500 cursor-pointer'
|
||||
size='small'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setModalImageUrl('/ratio.png');
|
||||
setIsModalOpenurl(true);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
{(() => {
|
||||
const items = getModelPriceItemsForCard(model, priceData);
|
||||
const inputItem = items.find(i => i.key === 'input');
|
||||
const outputItem = items.find(i => i.key === 'output');
|
||||
return (
|
||||
<div className='flex gap-3'>
|
||||
{inputItem && (
|
||||
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{inputItem.label}</div>
|
||||
<div className='text-sm font-bold text-gray-900'>
|
||||
{inputItem.value}{inputItem.suffix}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{outputItem && (
|
||||
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{outputItem.label}</div>
|
||||
<div className='text-sm font-bold text-gray-900'>
|
||||
{outputItem.value}{outputItem.suffix}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!inputItem && !outputItem && items.map(item => (
|
||||
<div key={item.key} className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{item.label}</div>
|
||||
<div className='text-sm font-bold text-gray-900'>
|
||||
{item.valueNode || (item.value + (item.suffix || ''))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='grid grid-cols-3 gap-2 text-xs text-gray-600'>
|
||||
<div>
|
||||
{t('模型')}:{' '}
|
||||
{model.quota_type === 0
|
||||
? (priceData?.inputRatio ?? model.model_ratio)
|
||||
: t('无')}
|
||||
</div>
|
||||
<div>
|
||||
{t('输出')}:{' '}
|
||||
{model.quota_type === 0
|
||||
? parseFloat(model.completion_ratio.toFixed(2))
|
||||
: t('无')}
|
||||
</div>
|
||||
<div>
|
||||
{t('分组')}: {priceData?.usedGroupRatio ?? '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export const ERROR_MESSAGES = {
|
|||
NETWORK_ERROR: '网络连接失败或服务器无响应',
|
||||
};
|
||||
|
||||
/** 体验馆文生视频时长(秒):3~30,默认 5 */
|
||||
/** 体验文生视频时长(秒):3~30,默认 5 */
|
||||
export const PLAYGROUND_VIDEO_DURATION_OPTIONS = Array.from(
|
||||
{ length: 28 },
|
||||
(_, i) => {
|
||||
|
|
@ -153,7 +153,7 @@ export const PLAYGROUND_VIDEO_DURATION_OPTIONS = Array.from(
|
|||
},
|
||||
);
|
||||
|
||||
// 体验馆图片分辨率:value 为上游 size,label 仅展示 480p / 720p 等
|
||||
// 体验图片分辨率:value 为上游 size,label 仅展示 480p / 720p 等
|
||||
export const PLAYGROUND_IMAGE_SIZE_OPTIONS = [
|
||||
{ label: '480p', value: '854x480' },
|
||||
{ label: '720p', value: '1280x720' },
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export function buildImageMarkdown(sources) {
|
|||
.join('\n\n');
|
||||
}
|
||||
|
||||
/** 构造体验馆消息 patch:generatedImages + markdown 正文 */
|
||||
/** 构造体验消息 patch:generatedImages + markdown 正文 */
|
||||
export function buildImageMessageContentPatch(sources) {
|
||||
const images = dedupeImageSources(sources);
|
||||
if (!images.length) return null;
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export function applyVideoFrameMetadata(metadata, imageUrls) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 体验馆 metadata.input:只传 prompt,media.type 由后端 alivideo adaptor 按模型规范化。
|
||||
* 体验 metadata.input:只传 prompt,media.type 由后端 alivideo adaptor 按模型规范化。
|
||||
*/
|
||||
export function buildVideoNativeInput(prompt) {
|
||||
return { prompt: String(prompt || '').trim() };
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const normalizeTagList = (csv) => {
|
|||
const hasTag = (csv, tag) => normalizeTagList(csv).includes(tag);
|
||||
|
||||
/**
|
||||
* 体验馆数据加载:拉取用户模型与类型(与模型广场同源元数据)、按「全部/类型」在客户端筛模型(同模型广场逻辑),分组单独加载。
|
||||
* 体验数据加载:拉取用户模型与类型(与模型广场同源元数据)、按「全部/类型」在客户端筛模型(同模型广场逻辑),分组单独加载。
|
||||
* @param {{ user?: object }} userState 已登录用户状态
|
||||
* @param {{ model?: string, model_type?: string|number, group?: string, selected_route_slug?: string }} inputs 当前表单/配置
|
||||
* @param {Array<{ label: string, value: string|number }>} modelTypes 类型下拉项(与接口同步后的状态,用于按类型重算模型列表)
|
||||
|
|
|
|||
|
|
@ -2339,8 +2339,8 @@
|
|||
"操作暂时被禁用": "Operation temporarily disabled",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作类型": "Operation Type",
|
||||
"体验馆": "Playground",
|
||||
"体验馆和聊天功能": "Playground and chat functions",
|
||||
"体验": "Playground",
|
||||
"体验和聊天功能": "Playground and chat functions",
|
||||
"支付": "Pay",
|
||||
"支付地址": "Payment address",
|
||||
"支付失败": "Payment failed",
|
||||
|
|
@ -4245,7 +4245,7 @@
|
|||
"视频本地按 token 计费": "Video billed locally by estimated tokens",
|
||||
"视频本地按条/分辨率计价": "Video billed locally per clip / resolution",
|
||||
"视频模式支持图片或视频 URL 作为素材": "Video mode supports image or video URLs as elements",
|
||||
"体验馆视频素材提示": "Image URLs: 1st = first frame, 2 = first+last frame, 3+ = first, references in between, last = final frame. Video URLs: included when filled. Empty fields are omitted from the request.",
|
||||
"体验视频素材提示": "Image URLs: 1st = first frame, 2 = first+last frame, 3+ = first, references in between, last = final frame. Video URLs: included when filled. Empty fields are omitted from the request.",
|
||||
"视频地址": "Video URL",
|
||||
"视频生成": "Video Generation",
|
||||
"视频生成中,请稍后": "Generating video, please wait",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Opération temporairement désactivée",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作类型": "Type d'opération",
|
||||
"体验馆": "Terrain de jeu",
|
||||
"体验馆和聊天功能": "Terrain de jeu et fonctions de discussion",
|
||||
"体验": "Terrain de jeu",
|
||||
"体验和聊天功能": "Terrain de jeu et fonctions de discussion",
|
||||
"支付": "Payer",
|
||||
"支付地址": "Adresse de paiement",
|
||||
"支付失败": "Paiement échoué",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Operasi sementara dinonaktifkan",
|
||||
"操作确认": "Konfirmasi operasi",
|
||||
"操作类型": "Jenis operasi",
|
||||
"体验馆": "Arena latihan",
|
||||
"体验馆和聊天功能": "Arena latihan dan fungsi obrolan",
|
||||
"体验": "Arena latihan",
|
||||
"体验和聊天功能": "Arena latihan dan fungsi obrolan",
|
||||
"支付": "Bayar",
|
||||
"支付地址": "Alamat pembayaran",
|
||||
"支付失败": "Pembayaran gagal",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "この操作は一時的に無効にされています",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作类型": "操作タイプ",
|
||||
"体验馆": "Playground",
|
||||
"体验馆和聊天功能": "プレイグラウンドとチャット機能",
|
||||
"体验": "Playground",
|
||||
"体验和聊天功能": "プレイグラウンドとチャット機能",
|
||||
"支付": "支払う",
|
||||
"支付地址": "決済URL",
|
||||
"支付失败": "支払いに失敗しました",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Operasi dilumpuhkan buat sementara",
|
||||
"操作确认": "Pengesahan operasi",
|
||||
"操作类型": "Jenis operasi",
|
||||
"体验馆": "Padang latihan",
|
||||
"体验馆和聊天功能": "Padang latihan dan fungsi sembang",
|
||||
"体验": "Padang latihan",
|
||||
"体验和聊天功能": "Padang latihan dan fungsi sembang",
|
||||
"支付": "Bayar",
|
||||
"支付地址": "Alamat pembayaran",
|
||||
"支付失败": "Pembayaran gagal",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Операция временно отключена",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作类型": "Тип операции",
|
||||
"体验馆": "Тренировочная площадка",
|
||||
"体验馆和聊天功能": "Тренировочная площадка и чат-функции",
|
||||
"体验": "Тренировочная площадка",
|
||||
"体验和聊天功能": "Тренировочная площадка и чат-функции",
|
||||
"支付": "Оплатить",
|
||||
"支付地址": "Адрес оплаты",
|
||||
"支付失败": "Оплата не удалась",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Operesheni imezimwa kwa muda",
|
||||
"操作确认": "Uthibitisho wa operesheni",
|
||||
"操作类型": "Aina ya operesheni",
|
||||
"体验馆": "Uwanja wa mazoezi",
|
||||
"体验馆和聊天功能": "Uwanja wa mazoezi na mazungumzo",
|
||||
"体验": "Uwanja wa mazoezi",
|
||||
"体验和聊天功能": "Uwanja wa mazoezi na mazungumzo",
|
||||
"支付": "Lipa",
|
||||
"支付地址": "Anwani ya malipo",
|
||||
"支付失败": "Malipo yameshindwa",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "การดำเนินการถูกปิดชั่วคราว",
|
||||
"操作确认": "ยืนยันการดำเนินการ",
|
||||
"操作类型": "ประเภทการดำเนินการ",
|
||||
"体验馆": "สนามฝึก",
|
||||
"体验馆和聊天功能": "สนามฝึกและแชท",
|
||||
"体验": "สนามฝึก",
|
||||
"体验和聊天功能": "สนามฝึกและแชท",
|
||||
"支付": "ชำระเงิน",
|
||||
"支付地址": "ที่อยู่การชำระเงิน",
|
||||
"支付失败": "ชำระเงินไม่สำเร็จ",
|
||||
|
|
|
|||
|
|
@ -2306,8 +2306,8 @@
|
|||
"操作暂时被禁用": "Thao tác tạm thời bị vô hiệu hóa",
|
||||
"操作确认": "Operation confirmation",
|
||||
"操作类型": "Loại thao tác",
|
||||
"体验馆": "Sân chơi",
|
||||
"体验馆和聊天功能": "Chức năng sân chơi và trò chuyện",
|
||||
"体验": "Sân chơi",
|
||||
"体验和聊天功能": "Chức năng sân chơi và trò chuyện",
|
||||
"支付": "Thanh toán",
|
||||
"支付地址": "Địa chỉ thanh toán",
|
||||
"支付失败": "Thanh toán thất bại",
|
||||
|
|
|
|||
|
|
@ -1268,8 +1268,8 @@
|
|||
"操作失败,请重试": "操作失败,请重试",
|
||||
"操作成功完成!": "操作成功完成!",
|
||||
"操作暂时被禁用": "操作暂时被禁用",
|
||||
"体验馆": "体验馆",
|
||||
"体验馆和聊天功能": "体验馆和聊天功能",
|
||||
"体验": "体验",
|
||||
"体验和聊天功能": "体验和聊天功能",
|
||||
"支付地址": "支付地址",
|
||||
"支付宝": "支付宝",
|
||||
"支付方式": "支付方式",
|
||||
|
|
@ -5285,7 +5285,7 @@
|
|||
"视频数量": "视频数量",
|
||||
"视频时长(秒)": "视频时长(秒)",
|
||||
"视频模式支持图片或视频 URL 作为素材": "视频模式支持图片或视频 URL 作为素材",
|
||||
"体验馆视频素材提示": "图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。",
|
||||
"体验视频素材提示": "图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。",
|
||||
"视频地址": "视频地址",
|
||||
"视频生成": "视频生成",
|
||||
"视频生成中,请稍后": "视频生成中,请稍后",
|
||||
|
|
|
|||
|
|
@ -2305,8 +2305,8 @@
|
|||
"操作暂时被禁用": "操作暫時被禁用",
|
||||
"操作确认": "操作確認",
|
||||
"操作类型": "操作類型",
|
||||
"体验馆": "操練場",
|
||||
"体验馆和聊天功能": "操練場和聊天功能",
|
||||
"体验": "操練場",
|
||||
"体验和聊天功能": "操練場和聊天功能",
|
||||
"支付": "付款",
|
||||
"支付地址": "付款位址",
|
||||
"支付失败": "付款失敗",
|
||||
|
|
|
|||
|
|
@ -1432,6 +1432,131 @@ html.dark .with-pastel-balls::before {
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ─── Models 新布局 ─── */
|
||||
.models-page-layout {
|
||||
display: flex;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
padding: 56px 24px 0;
|
||||
gap: 32px;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.models-sidebar {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
overflow-y: auto;
|
||||
padding: 0 16px 24px 0;
|
||||
border-right: 1px solid var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
.models-sidebar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.models-sidebar::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
.models-sidebar:hover::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.15);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.models-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.models-tabs-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 16px 0;
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
.models-tab-btn {
|
||||
padding: 6px 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border-color, #d1d5db);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted, #6b7280);
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.models-tab-btn:hover {
|
||||
border-color: var(--primary-color, #3b82f6);
|
||||
color: var(--primary-color, #3b82f6);
|
||||
}
|
||||
.models-tab-btn.active {
|
||||
background: var(--primary-color, #3b82f6);
|
||||
color: white;
|
||||
border-color: var(--primary-color, #3b82f6);
|
||||
}
|
||||
|
||||
.models-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.models-card-grid::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.models-card-grid::-webkit-scrollbar-thumb {
|
||||
background: rgba(0,0,0,0.12);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* 筛选面板标题 */
|
||||
.models-filter-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.models-filter-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted, #6b7280);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
.models-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
font-size: 14px;
|
||||
color: var(--text-color, #374151);
|
||||
gap: 8px;
|
||||
}
|
||||
.models-filter-item:hover {
|
||||
background: var(--hover-bg, #f3f4f6);
|
||||
}
|
||||
.models-filter-item.active {
|
||||
background: var(--primary-light, #eff6ff);
|
||||
color: var(--primary-color, #3b82f6);
|
||||
font-weight: 500;
|
||||
}
|
||||
.models-filter-item .count {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #9ca3af);
|
||||
}
|
||||
|
||||
/* ==================== semi-ui 缁勪欢鑷畾涔夋牱寮?==================== */
|
||||
.semi-card-header,
|
||||
.semi-card-body {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/*
|
||||
Copyright (C) 2025 QuantumNous
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -194,7 +194,7 @@ function HeroSection() {
|
|||
{t("兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。")}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 font-body">
|
||||
<Link to="/register" className="inline-flex items-center bg-white text-black px-6 py-3 text-sm font-mono font-medium uppercase tracking-widest hover:bg-transparent hover:text-white border border-white transition-colors duration-200">
|
||||
<Link to="/console/token" className="inline-flex items-center bg-white text-black px-6 py-3 text-sm font-mono font-medium uppercase tracking-widest hover:bg-transparent hover:text-white border border-white transition-colors duration-200">
|
||||
{t("开始使用")}
|
||||
</Link>
|
||||
<Link to="/pricing" className="inline-flex items-center text-white/60 px-6 py-3 text-sm font-mono font-medium uppercase tracking-widest hover:text-white transition-colors duration-200">
|
||||
|
|
@ -216,7 +216,7 @@ function HeroSection() {
|
|||
</button>
|
||||
</div>
|
||||
<pre className="p-5 text-sm font-mono leading-relaxed text-white/80 overflow-x-auto">
|
||||
<code>{`curl https://api.tokendance.space/v1/chat/completions \\
|
||||
<code>{`curl https://api.turingtoken.ai/v1/chat/completions \\
|
||||
-H "Authorization: Bearer \$YOUR_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
|
|
@ -259,7 +259,7 @@ function HeroSection() {
|
|||
titleKey: '多协议兼容',
|
||||
descKey:
|
||||
'原生支持 OpenAI、Claude、Gemini 文本协议,覆盖图像 / 视频 / 文本转语音生成。无需修改代码,切换 Base URL 即可接入。',
|
||||
code: 'baseURL: "api.tokendance.space"',
|
||||
code: 'baseURL: "api.turingtoken.ai"',
|
||||
},
|
||||
{
|
||||
titleKey: '智能路由',
|
||||
|
|
@ -408,7 +408,7 @@ const LANG_LABELS = { curl: 'cURL', python: 'Python', nodejs: 'Node.js' };
|
|||
|
||||
const CODE_SAMPLES = {
|
||||
openai: {
|
||||
curl: `curl https://api.tokendance.space/v1/chat/completions \\
|
||||
curl: `curl https://api.turingtoken.ai/v1/chat/completions \\
|
||||
-H "Authorization: Bearer $OPENAI_API_KEY" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
|
|
@ -418,7 +418,7 @@ const CODE_SAMPLES = {
|
|||
python: `from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="https://api.tokendance.space",
|
||||
base_url="https://api.turingtoken.ai",
|
||||
api_key="sk-your-key"
|
||||
)
|
||||
|
||||
|
|
@ -430,7 +430,7 @@ print(response.choices[0].message.content)`,
|
|||
nodejs: `import OpenAI from "openai";
|
||||
|
||||
const client = new OpenAI({
|
||||
baseURL: "https://api.tokendance.space",
|
||||
baseURL: "https://api.turingtoken.ai",
|
||||
apiKey: "sk-your-key",
|
||||
});
|
||||
|
||||
|
|
@ -441,7 +441,7 @@ const response = await client.chat.completions.create({
|
|||
console.log(response.choices[0].message.content);`,
|
||||
},
|
||||
claude: {
|
||||
curl: `curl https://api.tokendance.space/v1/messages \\
|
||||
curl: `curl https://api.turingtoken.ai/v1/messages \\
|
||||
-H "x-api-key: sk-your-key" \\
|
||||
-H "anthropic-version: 2023-06-01" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
|
|
@ -453,7 +453,7 @@ console.log(response.choices[0].message.content);`,
|
|||
python: `import anthropic
|
||||
|
||||
client = anthropic.Anthropic(
|
||||
base_url="https://api.tokendance.space",
|
||||
base_url="https://api.turingtoken.ai",
|
||||
api_key="sk-your-key"
|
||||
)
|
||||
|
||||
|
|
@ -466,7 +466,7 @@ print(response.content[0].text)`,
|
|||
nodejs: `import Anthropic from "@anthropic-ai/sdk";
|
||||
|
||||
const client = new Anthropic({
|
||||
baseURL: "https://api.tokendance.space",
|
||||
baseURL: "https://api.turingtoken.ai",
|
||||
apiKey: "sk-your-key",
|
||||
});
|
||||
|
||||
|
|
@ -478,7 +478,7 @@ const response = await client.messages.create({
|
|||
console.log(response.content[0].text);`,
|
||||
},
|
||||
gemini: {
|
||||
curl: `curl "https://api.tokendance.space/v1beta/models/gemini-2.0-flash:generateContent?key=sk-your-key" \\
|
||||
curl: `curl "https://api.turingtoken.ai/v1beta/models/gemini-2.0-flash:generateContent?key=sk-your-key" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{
|
||||
"contents": [{"parts":[{"text": "Hello!"}]}]
|
||||
|
|
@ -732,12 +732,6 @@ function DarkFooter() {
|
|||
>
|
||||
{t('定价')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/about"
|
||||
className="font-mono text-xs text-background/40 hover:text-background transition-all duration-200"
|
||||
>
|
||||
{t('关于')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/console"
|
||||
className="font-mono text-xs text-background/40 hover:text-background transition-all duration-200"
|
||||
|
|
@ -750,16 +744,6 @@ function DarkFooter() {
|
|||
<span className="font-mono text-xs text-background/30">
|
||||
© 2026 TokenFactory
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 md:gap-x-6">
|
||||
<a
|
||||
href="https://github.com/fyinfor/token-factory"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-mono text-xs text-background/30 hover:text-background transition-all duration-200"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -588,7 +588,18 @@ const Playground = () => {
|
|||
if (searchParams.get('expired')) {
|
||||
Toast.warning(t('登录过期,请重新登录!'));
|
||||
}
|
||||
}, [searchParams, t]);
|
||||
const modelParam = searchParams.get('model');
|
||||
const modelTypeParam = searchParams.get('model_type');
|
||||
if (modelTypeParam) {
|
||||
const v = Number(modelTypeParam);
|
||||
if (!Number.isNaN(v)) {
|
||||
handleInputChange('model_type', v);
|
||||
}
|
||||
}
|
||||
if (modelParam) {
|
||||
handleInputChange('model', modelParam);
|
||||
}
|
||||
}, [searchParams, t, handleInputChange]);
|
||||
|
||||
// Playground 组件无需再监听窗口变化,isMobile 由 useIsMobile Hook 自动更新
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ const sectionConfigs = [
|
|||
{
|
||||
key: 'chat',
|
||||
title_key: '聊天区域',
|
||||
desc_key: '体验馆和聊天功能',
|
||||
desc_key: '体验和聊天功能',
|
||||
modules: [
|
||||
{
|
||||
key: 'playground',
|
||||
title_key: '体验馆',
|
||||
title_key: '体验',
|
||||
desc_key: 'AI模型测试环境',
|
||||
},
|
||||
{ key: 'chat', title_key: '聊天', desc_key: '聊天会话管理' },
|
||||
|
|
|
|||
Loading…
Reference in New Issue