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
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 简化的供应商映射规则
|
// 简化的供应商映射规则
|
||||||
|
|
@ -20,6 +23,7 @@ var defaultVendorRules = map[string]string{
|
||||||
"qwen": "阿里巴巴",
|
"qwen": "阿里巴巴",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
"abab": "MiniMax",
|
"abab": "MiniMax",
|
||||||
|
"minimax": "MiniMax",
|
||||||
"ernie": "百度",
|
"ernie": "百度",
|
||||||
"spark": "讯飞",
|
"spark": "讯飞",
|
||||||
"hunyuan": "腾讯",
|
"hunyuan": "腾讯",
|
||||||
|
|
@ -32,6 +36,7 @@ var defaultVendorRules = map[string]string{
|
||||||
"grok": "xAI",
|
"grok": "xAI",
|
||||||
"llama": "Meta",
|
"llama": "Meta",
|
||||||
"doubao": "字节跳动",
|
"doubao": "字节跳动",
|
||||||
|
"seedance": "字节跳动",
|
||||||
"kling": "快手",
|
"kling": "快手",
|
||||||
"jimeng": "即梦",
|
"jimeng": "即梦",
|
||||||
"vidu": "Vidu",
|
"vidu": "Vidu",
|
||||||
|
|
@ -39,10 +44,10 @@ var defaultVendorRules = map[string]string{
|
||||||
|
|
||||||
// 供应商默认图标映射
|
// 供应商默认图标映射
|
||||||
var defaultVendorIcons = map[string]string{
|
var defaultVendorIcons = map[string]string{
|
||||||
"OpenAI": "OpenAI",
|
"OpenAI": "OpenAI.Color",
|
||||||
"Anthropic": "Claude.Color",
|
"Anthropic": "Claude.Color",
|
||||||
"Google": "Gemini.Color",
|
"Google": "Gemini.Color",
|
||||||
"Moonshot": "Moonshot",
|
"Moonshot": "Moonshot.Color",
|
||||||
"智谱": "Zhipu.Color",
|
"智谱": "Zhipu.Color",
|
||||||
"阿里巴巴": "Qwen.Color",
|
"阿里巴巴": "Qwen.Color",
|
||||||
"DeepSeek": "DeepSeek.Color",
|
"DeepSeek": "DeepSeek.Color",
|
||||||
|
|
@ -54,25 +59,30 @@ var defaultVendorIcons = map[string]string{
|
||||||
"Cloudflare": "Cloudflare.Color",
|
"Cloudflare": "Cloudflare.Color",
|
||||||
"360": "Ai360.Color",
|
"360": "Ai360.Color",
|
||||||
"零一万物": "Yi.Color",
|
"零一万物": "Yi.Color",
|
||||||
"Jina": "Jina",
|
"Jina": "Jina.Color",
|
||||||
"Mistral": "Mistral.Color",
|
"Mistral": "Mistral.Color",
|
||||||
"xAI": "XAI",
|
"xAI": "XAI.Color",
|
||||||
"Meta": "Ollama",
|
"Ollama": "Ollama.Color",
|
||||||
|
"Meta": "Meta.Color",
|
||||||
"字节跳动": "Doubao.Color",
|
"字节跳动": "Doubao.Color",
|
||||||
"快手": "Kling.Color",
|
"快手": "Kling.Color",
|
||||||
"即梦": "Jimeng.Color",
|
"即梦": "Jimeng.Color",
|
||||||
"Vidu": "Vidu",
|
"Vidu": "Vidu.Color",
|
||||||
"微软": "AzureAI",
|
"微软": "AzureAI.Color",
|
||||||
"Microsoft": "AzureAI",
|
"Microsoft": "AzureAI.Color",
|
||||||
"Azure": "AzureAI",
|
"Azure": "AzureAI.Color",
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDefaultVendorMapping 简化的默认供应商映射
|
// initDefaultVendorMapping 简化的默认供应商映射
|
||||||
func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) {
|
func initDefaultVendorMapping(metaMap map[string]*Model, vendorMap map[int]*Vendor, enableAbilities []AbilityWithChannel) {
|
||||||
for _, ability := range enableAbilities {
|
for _, ability := range enableAbilities {
|
||||||
modelName := ability.Model
|
modelName := ability.Model
|
||||||
if _, exists := metaMap[modelName]; exists {
|
if existing, exists := metaMap[modelName]; exists {
|
||||||
continue
|
// 已有记录但 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建模型元数据
|
if vendorID == 0 {
|
||||||
metaMap[modelName] = &Model{
|
// 仍未匹配到供应商 → 跳过(不给未匹配的模型写入 vendor_id=0 覆盖已有记录)
|
||||||
ModelName: modelName,
|
if strings.Contains(modelLower, "seedance") || strings.Contains(modelLower, "minimax") {
|
||||||
VendorID: vendorID,
|
common.SysLog(fmt.Sprintf("initDefaultVendorMapping: model %q matched pattern but vendorID=0 (getOrCreateVendor failed)", modelName))
|
||||||
Status: 1,
|
}
|
||||||
NameRule: NameRuleExact,
|
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 {
|
if err := newVendor.Insert(); err != nil {
|
||||||
|
common.SysLog(fmt.Sprintf("getOrCreateVendor: insert vendor %q failed: %v", vendorName, err))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
common.SysLog(fmt.Sprintf("getOrCreateVendor: created new vendor %q with id=%d", vendorName, newVendor.Id))
|
||||||
vendorMap[newVendor.Id] = newVendor
|
vendorMap[newVendor.Id] = newVendor
|
||||||
return newVendor.Id
|
return newVendor.Id
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,8 @@ var vendorKeywordAliases = []struct {
|
||||||
{"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}},
|
{"qwen", []string{"alibaba", "qwen", "tongyi", "aliyun"}},
|
||||||
{"moonshot", []string{"moonshot"}},
|
{"moonshot", []string{"moonshot"}},
|
||||||
{"kimi", []string{"moonshot"}},
|
{"kimi", []string{"moonshot"}},
|
||||||
{"doubao", []string{"bytedance", "volcengine", "volcano"}},
|
{"doubao", []string{"bytedance", "volcengine", "volcano", "字节跳动", "字节"}},
|
||||||
|
{"seedance", []string{"bytedance", "volcengine", "volcano", "doubao", "字节跳动", "字节"}},
|
||||||
{"ernie", []string{"baidu"}},
|
{"ernie", []string{"baidu"}},
|
||||||
{"wenxin", []string{"baidu"}},
|
{"wenxin", []string{"baidu"}},
|
||||||
{"hunyuan", []string{"tencent"}},
|
{"hunyuan", []string{"tencent"}},
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,7 @@ var defaultModelRatio = map[string]float64{
|
||||||
"deepseek-ai/DeepSeek-R1": 0.8,
|
"deepseek-ai/DeepSeek-R1": 0.8,
|
||||||
"deepseek-ai/DeepSeek-V3-0324": 0.8,
|
"deepseek-ai/DeepSeek-V3-0324": 0.8,
|
||||||
"deepseek-ai/DeepSeek-V3.1": 0.8,
|
"deepseek-ai/DeepSeek-V3.1": 0.8,
|
||||||
|
"DeepSeek-V3.2": 0.8,
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultModelPrice = map[string]float64{
|
var defaultModelPrice = map[string]float64{
|
||||||
|
|
@ -870,6 +871,8 @@ func GetCompletionRatioCopy() map[string]float64 {
|
||||||
|
|
||||||
// 转换模型名,减少渠道必须配置各种带参数模型
|
// 转换模型名,减少渠道必须配置各种带参数模型
|
||||||
func FormatMatchingModelName(name string) string {
|
func FormatMatchingModelName(name string) string {
|
||||||
|
// 去除空格,使 "Seedance 2.0" 与 "Seedance2.0" 等变体能匹配
|
||||||
|
name = strings.ReplaceAll(name, " ", "")
|
||||||
|
|
||||||
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
|
if strings.HasPrefix(name, "gemini-2.5-flash-lite") {
|
||||||
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")
|
name = handleThinkingBudgetModel(name, "gemini-2.5-flash-lite", "gemini-2.5-flash-lite-thinking-*")
|
||||||
|
|
|
||||||
|
|
@ -378,7 +378,6 @@ const HomeModelList = () => {
|
||||||
<PricingCardView
|
<PricingCardView
|
||||||
filteredModels={pricingData.filteredModels}
|
filteredModels={pricingData.filteredModels}
|
||||||
loading={pricingData.loading}
|
loading={pricingData.loading}
|
||||||
rowSelection={null}
|
|
||||||
pageSize={pricingData.pageSize}
|
pageSize={pricingData.pageSize}
|
||||||
setPageSize={pricingData.setPageSize}
|
setPageSize={pricingData.setPageSize}
|
||||||
currentPage={pricingData.currentPage}
|
currentPage={pricingData.currentPage}
|
||||||
|
|
@ -387,9 +386,6 @@ const HomeModelList = () => {
|
||||||
groupRatio={pricingData.groupRatio}
|
groupRatio={pricingData.groupRatio}
|
||||||
groupModelPrice={pricingData.groupModelPrice}
|
groupModelPrice={pricingData.groupModelPrice}
|
||||||
groupModelRatio={pricingData.groupModelRatio}
|
groupModelRatio={pricingData.groupModelRatio}
|
||||||
copyText={pricingData.copyText}
|
|
||||||
setModalImageUrl={pricingData.setModalImageUrl}
|
|
||||||
setIsModalOpenurl={pricingData.setIsModalOpenurl}
|
|
||||||
currency={pricingData.currency}
|
currency={pricingData.currency}
|
||||||
siteDisplayType={pricingData.siteDisplayType}
|
siteDisplayType={pricingData.siteDisplayType}
|
||||||
tokenUnit={pricingData.tokenUnit}
|
tokenUnit={pricingData.tokenUnit}
|
||||||
|
|
@ -399,10 +395,7 @@ const HomeModelList = () => {
|
||||||
pricingData.channelVideoCompletionRatio
|
pricingData.channelVideoCompletionRatio
|
||||||
}
|
}
|
||||||
channelVideoPrice={pricingData.channelVideoPrice}
|
channelVideoPrice={pricingData.channelVideoPrice}
|
||||||
showRatio={false}
|
|
||||||
t={pricingData.t}
|
t={pricingData.t}
|
||||||
selectedRowKeys={[]}
|
|
||||||
setSelectedRowKeys={() => {}}
|
|
||||||
openModelDetail={pricingData.openModelDetail}
|
openModelDetail={pricingData.openModelDetail}
|
||||||
showSizeChanger={false}
|
showSizeChanger={false}
|
||||||
blurPricing={blurPricing}
|
blurPricing={blurPricing}
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
||||||
const chatMenuItems = useMemo(() => {
|
const chatMenuItems = useMemo(() => {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
text: t('体验馆'),
|
text: t('体验'),
|
||||||
itemKey: 'playground',
|
itemKey: 'playground',
|
||||||
to: '/playground',
|
to: '/playground',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||||
const ROOT_MARGIN = '280px 0px 280px 0px';
|
const ROOT_MARGIN = '280px 0px 280px 0px';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体验馆消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
* 体验消息气泡:进入(或接近)可视区域后再挂载子树,减轻长对话下 DOM/媒体压力。
|
||||||
* variant=media:更高占位,避免图片/视频模式切换后列表高度不足导致滚不到底部。
|
* variant=media:更高占位,避免图片/视频模式切换后列表高度不足导致滚不到底部。
|
||||||
*/
|
*/
|
||||||
const LazyVisibleMessage = ({
|
const LazyVisibleMessage = ({
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ import {
|
||||||
import ParameterControl from './ParameterControl';
|
import ParameterControl from './ParameterControl';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体验馆分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
* 体验分组下拉:与全局 renderGroupOption 一致,但不展示倍率角标。
|
||||||
* @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数
|
* @param {Record<string, unknown>} item Semi Select 传入的选项渲染参数
|
||||||
*/
|
*/
|
||||||
const renderPlaygroundGroupOption = (item) =>
|
const renderPlaygroundGroupOption = (item) =>
|
||||||
|
|
@ -85,7 +85,7 @@ const SettingsPanel = ({
|
||||||
const mediaModeEnabled = isImageMode || isVideoMode;
|
const mediaModeEnabled = isImageMode || isVideoMode;
|
||||||
const videoMediaHint = isVideoMode
|
const videoMediaHint = isVideoMode
|
||||||
? t(
|
? t(
|
||||||
'体验馆视频素材提示',
|
'体验视频素材提示',
|
||||||
'图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。',
|
'图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。',
|
||||||
)
|
)
|
||||||
: '';
|
: '';
|
||||||
|
|
|
||||||
|
|
@ -83,11 +83,11 @@ const VideoUrlInput = ({
|
||||||
|
|
||||||
{!enabled ? (
|
{!enabled ? (
|
||||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
{t('体验馆视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
{t('体验视频地址停用提示', '启用后可添加视频 URL(视频生视频、视频编辑等)')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : list.length === 0 ? (
|
) : list.length === 0 ? (
|
||||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
{t('体验馆视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
{t('体验视频地址空列表提示', '点击 + 添加 .mp4 / .mov 等可访问的视频链接')}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : (
|
) : (
|
||||||
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
<Typography.Text className='text-xs text-gray-500 mb-2 block'>
|
||||||
|
|
|
||||||
|
|
@ -212,11 +212,11 @@ const NotificationSettings = ({
|
||||||
{
|
{
|
||||||
key: 'chat',
|
key: 'chat',
|
||||||
title: t('聊天区域'),
|
title: t('聊天区域'),
|
||||||
description: t('体验馆和聊天功能'),
|
description: t('体验和聊天功能'),
|
||||||
modules: [
|
modules: [
|
||||||
{
|
{
|
||||||
key: 'playground',
|
key: 'playground',
|
||||||
title: t('体验馆'),
|
title: t('体验'),
|
||||||
description: t('AI模型测试环境'),
|
description: t('AI模型测试环境'),
|
||||||
},
|
},
|
||||||
{ key: 'chat', title: t('聊天'), description: t('聊天会话管理') },
|
{ 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 React, { useContext, useMemo } from 'react';
|
||||||
import { Layout, ImagePreview } from '@douyinfe/semi-ui';
|
import { ImagePreview } from '@douyinfe/semi-ui';
|
||||||
import PricingSidebar from './PricingSidebar';
|
import ModelsSidebar from './ModelsSidebar';
|
||||||
import PricingContent from './content/PricingContent';
|
import ModelsContent from './ModelsContent';
|
||||||
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
|
import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
|
||||||
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
|
import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||||
|
|
@ -29,7 +29,6 @@ import { UserContext } from '../../../../context/User';
|
||||||
|
|
||||||
const PricingPage = () => {
|
const PricingPage = () => {
|
||||||
const pricingData = useModelPricingData();
|
const pricingData = useModelPricingData();
|
||||||
const { Sider, Content } = Layout;
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [showRatio, setShowRatio] = React.useState(false);
|
const [showRatio, setShowRatio] = React.useState(false);
|
||||||
const [viewMode, setViewMode] = React.useState('card');
|
const [viewMode, setViewMode] = React.useState('card');
|
||||||
|
|
@ -60,22 +59,22 @@ const PricingPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-white'>
|
<div className='bg-background'>
|
||||||
<Layout className='pricing-layout'>
|
<div className='models-page-layout'>
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Sider className='pricing-scroll-hide pricing-sidebar'>
|
<aside className='models-sidebar'>
|
||||||
<PricingSidebar {...allProps} />
|
<ModelsSidebar {...allProps} />
|
||||||
</Sider>
|
</aside>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Content className='pricing-scroll-hide pricing-content'>
|
<main className='models-content'>
|
||||||
<PricingContent
|
<ModelsContent
|
||||||
{...allProps}
|
{...allProps}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
sidebarProps={allProps}
|
sidebarProps={allProps}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</main>
|
||||||
</Layout>
|
</div>
|
||||||
|
|
||||||
<ImagePreview
|
<ImagePreview
|
||||||
src={pricingData.modalImageUrl}
|
src={pricingData.modalImageUrl}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { SideSheet, Typography, Button } from '@douyinfe/semi-ui';
|
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 { API } from '../../../../helpers';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||||
|
|
@ -65,6 +66,21 @@ const ModelDetailSideSheet = ({
|
||||||
channelVideoPriceMap = {},
|
channelVideoPriceMap = {},
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
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)
|
* channel_id -> 单测/运营展示 DTO(打开详情时按需拉取,不并入 /pricing)
|
||||||
*/
|
*/
|
||||||
|
|
@ -121,7 +137,18 @@ const ModelDetailSideSheet = ({
|
||||||
<SideSheet
|
<SideSheet
|
||||||
placement='right'
|
placement='right'
|
||||||
title={
|
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={{
|
bodyStyle={{
|
||||||
padding: '0',
|
padding: '0',
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,6 @@ import { Card, Skeleton } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
const PricingCardSkeleton = ({
|
const PricingCardSkeleton = ({
|
||||||
skeletonCount = 100,
|
skeletonCount = 100,
|
||||||
rowSelection = false,
|
|
||||||
showRatio = false,
|
|
||||||
}) => {
|
}) => {
|
||||||
const placeholder = (
|
const placeholder = (
|
||||||
<div className='px-2 pt-2'>
|
<div className='px-2 pt-2'>
|
||||||
|
|
@ -34,8 +32,8 @@ const PricingCardSkeleton = ({
|
||||||
className='!rounded-2xl border border-gray-200'
|
className='!rounded-2xl border border-gray-200'
|
||||||
bodyStyle={{ padding: '24px' }}
|
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='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'>
|
<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 }}
|
style={{ width: 48, height: 48, borderRadius: 16 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* 模型名称和价格区域 */}
|
{/* 模型名称 */}
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0'>
|
||||||
{/* 模型名称骨架 */}
|
|
||||||
<Skeleton.Title
|
<Skeleton.Title
|
||||||
style={{
|
style={{
|
||||||
width: `${120 + (index % 3) * 30}px`,
|
width: `${120 + (index % 3) * 30}px`,
|
||||||
height: 20,
|
height: 20,
|
||||||
marginBottom: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 价格信息骨架 */}
|
|
||||||
<Skeleton.Title
|
|
||||||
style={{
|
|
||||||
width: `${160 + (index % 4) * 20}px`,
|
|
||||||
height: 20,
|
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* 模型描述骨架 */}
|
{/* 模型描述骨架 */}
|
||||||
|
|
@ -90,43 +64,25 @@ const PricingCardSkeleton = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 标签区域骨架 */}
|
{/* 价格骨架:输入 + 输出并排 */}
|
||||||
<div className='flex flex-wrap gap-2'>
|
<div className='flex gap-3'>
|
||||||
{Array.from({ length: 2 + (index % 3) }).map((_, tagIndex) => (
|
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||||
<Skeleton.Button
|
<Skeleton.Title
|
||||||
key={tagIndex}
|
style={{ width: '80%', height: 14, marginBottom: 8 }}
|
||||||
size='small'
|
/>
|
||||||
style={{
|
<Skeleton.Title
|
||||||
width: 64,
|
style={{ width: '100%', height: 18, marginBottom: 0 }}
|
||||||
height: 18,
|
|
||||||
borderRadius: 10,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
|
||||||
</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>
|
||||||
)}
|
<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>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,15 @@ For commercial licensing, please contact support@quantumnous.com
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
Checkbox,
|
|
||||||
Empty,
|
Empty,
|
||||||
Pagination,
|
Pagination,
|
||||||
Button,
|
|
||||||
Avatar,
|
Avatar,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
|
||||||
import { Copy } from 'lucide-react';
|
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark,
|
IllustrationNoResultDark,
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import {
|
import {
|
||||||
stringToColor,
|
|
||||||
calculateModelPrice,
|
calculateModelPrice,
|
||||||
getModelPriceItems,
|
getModelPriceItems,
|
||||||
getLobeHubIcon,
|
getLobeHubIcon,
|
||||||
|
|
@ -45,7 +38,6 @@ import {
|
||||||
} from '../../../../../helpers';
|
} from '../../../../../helpers';
|
||||||
import PricingCardSkeleton from './PricingCardSkeleton';
|
import PricingCardSkeleton from './PricingCardSkeleton';
|
||||||
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
|
import { useMinimumLoadingTime } from '../../../../../hooks/common/useMinimumLoadingTime';
|
||||||
import { renderLimitedItems } from '../../../../common/ui/RenderUtils';
|
|
||||||
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../../../hooks/common/useIsMobile';
|
||||||
const CARD_STYLES = {
|
const CARD_STYLES = {
|
||||||
container:
|
container:
|
||||||
|
|
@ -60,7 +52,6 @@ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
const PricingCardView = ({
|
const PricingCardView = ({
|
||||||
filteredModels,
|
filteredModels,
|
||||||
loading,
|
loading,
|
||||||
rowSelection,
|
|
||||||
pageSize,
|
pageSize,
|
||||||
setPageSize,
|
setPageSize,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
|
@ -69,17 +60,11 @@ const PricingCardView = ({
|
||||||
groupRatio,
|
groupRatio,
|
||||||
groupModelPrice,
|
groupModelPrice,
|
||||||
groupModelRatio,
|
groupModelRatio,
|
||||||
copyText,
|
|
||||||
setModalImageUrl,
|
|
||||||
setIsModalOpenurl,
|
|
||||||
currency,
|
currency,
|
||||||
siteDisplayType,
|
siteDisplayType,
|
||||||
tokenUnit,
|
tokenUnit,
|
||||||
displayPrice,
|
displayPrice,
|
||||||
showRatio,
|
|
||||||
t,
|
t,
|
||||||
selectedRowKeys = [],
|
|
||||||
setSelectedRowKeys,
|
|
||||||
openModelDetail,
|
openModelDetail,
|
||||||
showSizeChanger = true,
|
showSizeChanger = true,
|
||||||
blurPricing = false,
|
blurPricing = false,
|
||||||
|
|
@ -87,6 +72,7 @@ const PricingCardView = ({
|
||||||
channelVideoRatio = {},
|
channelVideoRatio = {},
|
||||||
channelVideoCompletionRatio = {},
|
channelVideoCompletionRatio = {},
|
||||||
channelVideoPrice = {},
|
channelVideoPrice = {},
|
||||||
|
gridCols = 0, // 当 > 0 时使用 grid 布局(如 gridCols=2 表示 2 列)
|
||||||
}) => {
|
}) => {
|
||||||
const showSkeleton = useMinimumLoadingTime(loading);
|
const showSkeleton = useMinimumLoadingTime(loading);
|
||||||
const startIndex = (currentPage - 1) * pageSize;
|
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 calculateChannelPrices = (model, opts = {}) => {
|
||||||
const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
|
const { skipSimpleVideoFlat = false, skipSimpleFixed = false } = opts;
|
||||||
if (!model.channel_list || model.channel_list.length === 0) {
|
if (!model.channel_list || model.channel_list.length === 0) {
|
||||||
|
|
@ -765,77 +695,11 @@ const PricingCardView = ({
|
||||||
return record.description || '';
|
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) {
|
if (showSkeleton) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PricingCardSkeleton
|
<PricingCardSkeleton />
|
||||||
rowSelection={!!rowSelection}
|
|
||||||
showRatio={showRatio}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -858,11 +722,10 @@ const PricingCardView = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='px-2 pt-2'>
|
<div className={gridCols > 0 ? '' : 'px-2 pt-2'}>
|
||||||
<div className='flex flex-wrap gap-4'>
|
<div className={gridCols > 0 ? 'grid gap-4' : 'flex flex-wrap gap-4'} style={gridCols > 0 ? { gridTemplateColumns: `repeat(${gridCols}, 1fr)` } : undefined}>
|
||||||
{paginatedModels.map((model, index) => {
|
{paginatedModels.map((model, index) => {
|
||||||
const modelKey = getModelKey(model);
|
const modelKey = getModelKey(model);
|
||||||
const isSelected = selectedRowKeys.includes(modelKey);
|
|
||||||
|
|
||||||
const priceData = calculateModelPrice({
|
const priceData = calculateModelPrice({
|
||||||
record: model,
|
record: model,
|
||||||
|
|
@ -876,166 +739,31 @@ const PricingCardView = ({
|
||||||
quotaDisplayType: siteDisplayType,
|
quotaDisplayType: siteDisplayType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const supplierLogos = getSupplierLogos(model);
|
|
||||||
const hasChannelList =
|
|
||||||
Array.isArray(model.channel_list) &&
|
|
||||||
model.channel_list.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={modelKey || index}
|
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%' }}
|
bodyStyle={{ height: '100%' }}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
!blurPricing && openModelDetail && openModelDetail(model)
|
!blurPricing && openModelDetail && openModelDetail(model)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col h-full'>
|
<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'>
|
<div className='flex items-start space-x-3 flex-1 min-w-0'>
|
||||||
{getModelIcon(model)}
|
{getModelIcon(model)}
|
||||||
<div className='flex-1 min-w-0'>
|
<div className='flex-1 min-w-0'>
|
||||||
<h3 className='text-lg font-bold text-gray-900 truncate'>
|
<h3 className='text-lg font-bold text-gray-900 truncate'>
|
||||||
{renderHighlightedText(model.model_name)}
|
{renderHighlightedText(model.model_name)}
|
||||||
</h3>
|
</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>
|
</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>
|
||||||
|
|
||||||
{/* 模型描述 - 占据剩余空间 */}
|
{/* 模型描述 */}
|
||||||
<div
|
<div
|
||||||
className='flex-1 mb-4'
|
className='mb-4'
|
||||||
style={
|
style={
|
||||||
blurPricing
|
blurPricing
|
||||||
? {
|
? {
|
||||||
|
|
@ -1054,7 +782,7 @@ const PricingCardView = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 底部区域 */}
|
{/* 价格:输入 + 输出并排 */}
|
||||||
<div
|
<div
|
||||||
className='mt-auto'
|
className='mt-auto'
|
||||||
style={
|
style={
|
||||||
|
|
@ -1067,49 +795,39 @@ const PricingCardView = ({
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* 标签区域 */}
|
{(() => {
|
||||||
{renderTags(model)}
|
const items = getModelPriceItemsForCard(model, priceData);
|
||||||
|
const inputItem = items.find(i => i.key === 'input');
|
||||||
{/* 倍率信息(可选) */}
|
const outputItem = items.find(i => i.key === 'output');
|
||||||
{showRatio && (
|
return (
|
||||||
<div className='pt-3'>
|
<div className='flex gap-3'>
|
||||||
<div className='flex items-center space-x-1 mb-2'>
|
{inputItem && (
|
||||||
<span className='text-xs font-medium text-gray-700'>
|
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||||
{t('倍率信息')}
|
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{inputItem.label}</div>
|
||||||
</span>
|
<div className='text-sm font-bold text-gray-900'>
|
||||||
<Tooltip
|
{inputItem.value}{inputItem.suffix}
|
||||||
content={t('倍率是为了方便换算不同价格的模型')}
|
</div>
|
||||||
>
|
</div>
|
||||||
<IconHelpCircle
|
)}
|
||||||
className='text-blue-500 cursor-pointer'
|
{outputItem && (
|
||||||
size='small'
|
<div className='flex-1 rounded-xl px-3 py-2.5' style={{backgroundColor: 'var(--semi-color-fill-0)'}}>
|
||||||
onClick={(e) => {
|
<div className='text-[10px] mb-0.5' style={{color: 'var(--semi-color-text-2)'}}>{outputItem.label}</div>
|
||||||
e.stopPropagation();
|
<div className='text-sm font-bold text-gray-900'>
|
||||||
setModalImageUrl('/ratio.png');
|
{outputItem.value}{outputItem.suffix}
|
||||||
setIsModalOpenurl(true);
|
</div>
|
||||||
}}
|
</div>
|
||||||
/>
|
)}
|
||||||
</Tooltip>
|
{!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>
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ export const ERROR_MESSAGES = {
|
||||||
NETWORK_ERROR: '网络连接失败或服务器无响应',
|
NETWORK_ERROR: '网络连接失败或服务器无响应',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 体验馆文生视频时长(秒):3~30,默认 5 */
|
/** 体验文生视频时长(秒):3~30,默认 5 */
|
||||||
export const PLAYGROUND_VIDEO_DURATION_OPTIONS = Array.from(
|
export const PLAYGROUND_VIDEO_DURATION_OPTIONS = Array.from(
|
||||||
{ length: 28 },
|
{ length: 28 },
|
||||||
(_, i) => {
|
(_, 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 = [
|
export const PLAYGROUND_IMAGE_SIZE_OPTIONS = [
|
||||||
{ label: '480p', value: '854x480' },
|
{ label: '480p', value: '854x480' },
|
||||||
{ label: '720p', value: '1280x720' },
|
{ label: '720p', value: '1280x720' },
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export function buildImageMarkdown(sources) {
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 构造体验馆消息 patch:generatedImages + markdown 正文 */
|
/** 构造体验消息 patch:generatedImages + markdown 正文 */
|
||||||
export function buildImageMessageContentPatch(sources) {
|
export function buildImageMessageContentPatch(sources) {
|
||||||
const images = dedupeImageSources(sources);
|
const images = dedupeImageSources(sources);
|
||||||
if (!images.length) return null;
|
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) {
|
export function buildVideoNativeInput(prompt) {
|
||||||
return { prompt: String(prompt || '').trim() };
|
return { prompt: String(prompt || '').trim() };
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ const normalizeTagList = (csv) => {
|
||||||
const hasTag = (csv, tag) => normalizeTagList(csv).includes(tag);
|
const hasTag = (csv, tag) => normalizeTagList(csv).includes(tag);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 体验馆数据加载:拉取用户模型与类型(与模型广场同源元数据)、按「全部/类型」在客户端筛模型(同模型广场逻辑),分组单独加载。
|
* 体验数据加载:拉取用户模型与类型(与模型广场同源元数据)、按「全部/类型」在客户端筛模型(同模型广场逻辑),分组单独加载。
|
||||||
* @param {{ user?: object }} userState 已登录用户状态
|
* @param {{ user?: object }} userState 已登录用户状态
|
||||||
* @param {{ model?: string, model_type?: string|number, group?: string, selected_route_slug?: string }} inputs 当前表单/配置
|
* @param {{ model?: string, model_type?: string|number, group?: string, selected_route_slug?: string }} inputs 当前表单/配置
|
||||||
* @param {Array<{ label: string, value: string|number }>} modelTypes 类型下拉项(与接口同步后的状态,用于按类型重算模型列表)
|
* @param {Array<{ label: string, value: string|number }>} modelTypes 类型下拉项(与接口同步后的状态,用于按类型重算模型列表)
|
||||||
|
|
|
||||||
|
|
@ -2339,8 +2339,8 @@
|
||||||
"操作暂时被禁用": "Operation temporarily disabled",
|
"操作暂时被禁用": "Operation temporarily disabled",
|
||||||
"操作确认": "Operation confirmation",
|
"操作确认": "Operation confirmation",
|
||||||
"操作类型": "Operation Type",
|
"操作类型": "Operation Type",
|
||||||
"体验馆": "Playground",
|
"体验": "Playground",
|
||||||
"体验馆和聊天功能": "Playground and chat functions",
|
"体验和聊天功能": "Playground and chat functions",
|
||||||
"支付": "Pay",
|
"支付": "Pay",
|
||||||
"支付地址": "Payment address",
|
"支付地址": "Payment address",
|
||||||
"支付失败": "Payment failed",
|
"支付失败": "Payment failed",
|
||||||
|
|
@ -4245,7 +4245,7 @@
|
||||||
"视频本地按 token 计费": "Video billed locally by estimated tokens",
|
"视频本地按 token 计费": "Video billed locally by estimated tokens",
|
||||||
"视频本地按条/分辨率计价": "Video billed locally per clip / resolution",
|
"视频本地按条/分辨率计价": "Video billed locally per clip / resolution",
|
||||||
"视频模式支持图片或视频 URL 作为素材": "Video mode supports image or video URLs as elements",
|
"视频模式支持图片或视频 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 URL",
|
||||||
"视频生成": "Video Generation",
|
"视频生成": "Video Generation",
|
||||||
"视频生成中,请稍后": "Generating video, please wait",
|
"视频生成中,请稍后": "Generating video, please wait",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Opération temporairement désactivée",
|
"操作暂时被禁用": "Opération temporairement désactivée",
|
||||||
"操作确认": "Operation confirmation",
|
"操作确认": "Operation confirmation",
|
||||||
"操作类型": "Type d'opération",
|
"操作类型": "Type d'opération",
|
||||||
"体验馆": "Terrain de jeu",
|
"体验": "Terrain de jeu",
|
||||||
"体验馆和聊天功能": "Terrain de jeu et fonctions de discussion",
|
"体验和聊天功能": "Terrain de jeu et fonctions de discussion",
|
||||||
"支付": "Payer",
|
"支付": "Payer",
|
||||||
"支付地址": "Adresse de paiement",
|
"支付地址": "Adresse de paiement",
|
||||||
"支付失败": "Paiement échoué",
|
"支付失败": "Paiement échoué",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Operasi sementara dinonaktifkan",
|
"操作暂时被禁用": "Operasi sementara dinonaktifkan",
|
||||||
"操作确认": "Konfirmasi operasi",
|
"操作确认": "Konfirmasi operasi",
|
||||||
"操作类型": "Jenis operasi",
|
"操作类型": "Jenis operasi",
|
||||||
"体验馆": "Arena latihan",
|
"体验": "Arena latihan",
|
||||||
"体验馆和聊天功能": "Arena latihan dan fungsi obrolan",
|
"体验和聊天功能": "Arena latihan dan fungsi obrolan",
|
||||||
"支付": "Bayar",
|
"支付": "Bayar",
|
||||||
"支付地址": "Alamat pembayaran",
|
"支付地址": "Alamat pembayaran",
|
||||||
"支付失败": "Pembayaran gagal",
|
"支付失败": "Pembayaran gagal",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "この操作は一時的に無効にされています",
|
"操作暂时被禁用": "この操作は一時的に無効にされています",
|
||||||
"操作确认": "Operation confirmation",
|
"操作确认": "Operation confirmation",
|
||||||
"操作类型": "操作タイプ",
|
"操作类型": "操作タイプ",
|
||||||
"体验馆": "Playground",
|
"体验": "Playground",
|
||||||
"体验馆和聊天功能": "プレイグラウンドとチャット機能",
|
"体验和聊天功能": "プレイグラウンドとチャット機能",
|
||||||
"支付": "支払う",
|
"支付": "支払う",
|
||||||
"支付地址": "決済URL",
|
"支付地址": "決済URL",
|
||||||
"支付失败": "支払いに失敗しました",
|
"支付失败": "支払いに失敗しました",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Operasi dilumpuhkan buat sementara",
|
"操作暂时被禁用": "Operasi dilumpuhkan buat sementara",
|
||||||
"操作确认": "Pengesahan operasi",
|
"操作确认": "Pengesahan operasi",
|
||||||
"操作类型": "Jenis operasi",
|
"操作类型": "Jenis operasi",
|
||||||
"体验馆": "Padang latihan",
|
"体验": "Padang latihan",
|
||||||
"体验馆和聊天功能": "Padang latihan dan fungsi sembang",
|
"体验和聊天功能": "Padang latihan dan fungsi sembang",
|
||||||
"支付": "Bayar",
|
"支付": "Bayar",
|
||||||
"支付地址": "Alamat pembayaran",
|
"支付地址": "Alamat pembayaran",
|
||||||
"支付失败": "Pembayaran gagal",
|
"支付失败": "Pembayaran gagal",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Операция временно отключена",
|
"操作暂时被禁用": "Операция временно отключена",
|
||||||
"操作确认": "Operation confirmation",
|
"操作确认": "Operation confirmation",
|
||||||
"操作类型": "Тип операции",
|
"操作类型": "Тип операции",
|
||||||
"体验馆": "Тренировочная площадка",
|
"体验": "Тренировочная площадка",
|
||||||
"体验馆和聊天功能": "Тренировочная площадка и чат-функции",
|
"体验和聊天功能": "Тренировочная площадка и чат-функции",
|
||||||
"支付": "Оплатить",
|
"支付": "Оплатить",
|
||||||
"支付地址": "Адрес оплаты",
|
"支付地址": "Адрес оплаты",
|
||||||
"支付失败": "Оплата не удалась",
|
"支付失败": "Оплата не удалась",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Operesheni imezimwa kwa muda",
|
"操作暂时被禁用": "Operesheni imezimwa kwa muda",
|
||||||
"操作确认": "Uthibitisho wa operesheni",
|
"操作确认": "Uthibitisho wa operesheni",
|
||||||
"操作类型": "Aina ya operesheni",
|
"操作类型": "Aina ya operesheni",
|
||||||
"体验馆": "Uwanja wa mazoezi",
|
"体验": "Uwanja wa mazoezi",
|
||||||
"体验馆和聊天功能": "Uwanja wa mazoezi na mazungumzo",
|
"体验和聊天功能": "Uwanja wa mazoezi na mazungumzo",
|
||||||
"支付": "Lipa",
|
"支付": "Lipa",
|
||||||
"支付地址": "Anwani ya malipo",
|
"支付地址": "Anwani ya malipo",
|
||||||
"支付失败": "Malipo yameshindwa",
|
"支付失败": "Malipo yameshindwa",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "การดำเนินการถูกปิดชั่วคราว",
|
"操作暂时被禁用": "การดำเนินการถูกปิดชั่วคราว",
|
||||||
"操作确认": "ยืนยันการดำเนินการ",
|
"操作确认": "ยืนยันการดำเนินการ",
|
||||||
"操作类型": "ประเภทการดำเนินการ",
|
"操作类型": "ประเภทการดำเนินการ",
|
||||||
"体验馆": "สนามฝึก",
|
"体验": "สนามฝึก",
|
||||||
"体验馆和聊天功能": "สนามฝึกและแชท",
|
"体验和聊天功能": "สนามฝึกและแชท",
|
||||||
"支付": "ชำระเงิน",
|
"支付": "ชำระเงิน",
|
||||||
"支付地址": "ที่อยู่การชำระเงิน",
|
"支付地址": "ที่อยู่การชำระเงิน",
|
||||||
"支付失败": "ชำระเงินไม่สำเร็จ",
|
"支付失败": "ชำระเงินไม่สำเร็จ",
|
||||||
|
|
|
||||||
|
|
@ -2306,8 +2306,8 @@
|
||||||
"操作暂时被禁用": "Thao tác tạm thời bị vô hiệu hóa",
|
"操作暂时被禁用": "Thao tác tạm thời bị vô hiệu hóa",
|
||||||
"操作确认": "Operation confirmation",
|
"操作确认": "Operation confirmation",
|
||||||
"操作类型": "Loại thao tác",
|
"操作类型": "Loại thao tác",
|
||||||
"体验馆": "Sân chơi",
|
"体验": "Sân chơi",
|
||||||
"体验馆和聊天功能": "Chức năng sân chơi và trò chuyện",
|
"体验和聊天功能": "Chức năng sân chơi và trò chuyện",
|
||||||
"支付": "Thanh toán",
|
"支付": "Thanh toán",
|
||||||
"支付地址": "Địa chỉ thanh toán",
|
"支付地址": "Địa chỉ thanh toán",
|
||||||
"支付失败": "Thanh toán thất bại",
|
"支付失败": "Thanh toán thất bại",
|
||||||
|
|
|
||||||
|
|
@ -1268,8 +1268,8 @@
|
||||||
"操作失败,请重试": "操作失败,请重试",
|
"操作失败,请重试": "操作失败,请重试",
|
||||||
"操作成功完成!": "操作成功完成!",
|
"操作成功完成!": "操作成功完成!",
|
||||||
"操作暂时被禁用": "操作暂时被禁用",
|
"操作暂时被禁用": "操作暂时被禁用",
|
||||||
"体验馆": "体验馆",
|
"体验": "体验",
|
||||||
"体验馆和聊天功能": "体验馆和聊天功能",
|
"体验和聊天功能": "体验和聊天功能",
|
||||||
"支付地址": "支付地址",
|
"支付地址": "支付地址",
|
||||||
"支付宝": "支付宝",
|
"支付宝": "支付宝",
|
||||||
"支付方式": "支付方式",
|
"支付方式": "支付方式",
|
||||||
|
|
@ -5285,7 +5285,7 @@
|
||||||
"视频数量": "视频数量",
|
"视频数量": "视频数量",
|
||||||
"视频时长(秒)": "视频时长(秒)",
|
"视频时长(秒)": "视频时长(秒)",
|
||||||
"视频模式支持图片或视频 URL 作为素材": "视频模式支持图片或视频 URL 作为素材",
|
"视频模式支持图片或视频 URL 作为素材": "视频模式支持图片或视频 URL 作为素材",
|
||||||
"体验馆视频素材提示": "图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。",
|
"体验视频素材提示": "图片地址:第 1 张为首帧,2 张为首尾帧,更多张时最后一张为尾帧。视频地址:填写则作为源视频参与生成。未填写的字段不会加入请求。",
|
||||||
"视频地址": "视频地址",
|
"视频地址": "视频地址",
|
||||||
"视频生成": "视频生成",
|
"视频生成": "视频生成",
|
||||||
"视频生成中,请稍后": "视频生成中,请稍后",
|
"视频生成中,请稍后": "视频生成中,请稍后",
|
||||||
|
|
|
||||||
|
|
@ -2305,8 +2305,8 @@
|
||||||
"操作暂时被禁用": "操作暫時被禁用",
|
"操作暂时被禁用": "操作暫時被禁用",
|
||||||
"操作确认": "操作確認",
|
"操作确认": "操作確認",
|
||||||
"操作类型": "操作類型",
|
"操作类型": "操作類型",
|
||||||
"体验馆": "操練場",
|
"体验": "操練場",
|
||||||
"体验馆和聊天功能": "操練場和聊天功能",
|
"体验和聊天功能": "操練場和聊天功能",
|
||||||
"支付": "付款",
|
"支付": "付款",
|
||||||
"支付地址": "付款位址",
|
"支付地址": "付款位址",
|
||||||
"支付失败": "付款失敗",
|
"支付失败": "付款失敗",
|
||||||
|
|
|
||||||
|
|
@ -1432,6 +1432,131 @@ html.dark .with-pastel-balls::before {
|
||||||
min-height: 0;
|
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-ui 缁勪欢鑷畾涔夋牱寮?==================== */
|
||||||
.semi-card-header,
|
.semi-card-header,
|
||||||
.semi-card-body {
|
.semi-card-body {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
/*
|
/*
|
||||||
Copyright (C) 2025 QuantumNous
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -194,7 +194,7 @@ function HeroSection() {
|
||||||
{t("兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。")}
|
{t("兼容 OpenAI / Claude / Gemini 等协议,覆盖文本、图像、视频、语音,智能路由,统一计费。")}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 font-body">
|
<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("开始使用")}
|
{t("开始使用")}
|
||||||
</Link>
|
</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">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="p-5 text-sm font-mono leading-relaxed text-white/80 overflow-x-auto">
|
<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 "Authorization: Bearer \$YOUR_API_KEY" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d '{
|
-d '{
|
||||||
|
|
@ -259,7 +259,7 @@ function HeroSection() {
|
||||||
titleKey: '多协议兼容',
|
titleKey: '多协议兼容',
|
||||||
descKey:
|
descKey:
|
||||||
'原生支持 OpenAI、Claude、Gemini 文本协议,覆盖图像 / 视频 / 文本转语音生成。无需修改代码,切换 Base URL 即可接入。',
|
'原生支持 OpenAI、Claude、Gemini 文本协议,覆盖图像 / 视频 / 文本转语音生成。无需修改代码,切换 Base URL 即可接入。',
|
||||||
code: 'baseURL: "api.tokendance.space"',
|
code: 'baseURL: "api.turingtoken.ai"',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
titleKey: '智能路由',
|
titleKey: '智能路由',
|
||||||
|
|
@ -408,7 +408,7 @@ const LANG_LABELS = { curl: 'cURL', python: 'Python', nodejs: 'Node.js' };
|
||||||
|
|
||||||
const CODE_SAMPLES = {
|
const CODE_SAMPLES = {
|
||||||
openai: {
|
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 "Authorization: Bearer $OPENAI_API_KEY" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d '{
|
-d '{
|
||||||
|
|
@ -418,7 +418,7 @@ const CODE_SAMPLES = {
|
||||||
python: `from openai import OpenAI
|
python: `from openai import OpenAI
|
||||||
|
|
||||||
client = OpenAI(
|
client = OpenAI(
|
||||||
base_url="https://api.tokendance.space",
|
base_url="https://api.turingtoken.ai",
|
||||||
api_key="sk-your-key"
|
api_key="sk-your-key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -430,7 +430,7 @@ print(response.choices[0].message.content)`,
|
||||||
nodejs: `import OpenAI from "openai";
|
nodejs: `import OpenAI from "openai";
|
||||||
|
|
||||||
const client = new OpenAI({
|
const client = new OpenAI({
|
||||||
baseURL: "https://api.tokendance.space",
|
baseURL: "https://api.turingtoken.ai",
|
||||||
apiKey: "sk-your-key",
|
apiKey: "sk-your-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -441,7 +441,7 @@ const response = await client.chat.completions.create({
|
||||||
console.log(response.choices[0].message.content);`,
|
console.log(response.choices[0].message.content);`,
|
||||||
},
|
},
|
||||||
claude: {
|
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 "x-api-key: sk-your-key" \\
|
||||||
-H "anthropic-version: 2023-06-01" \\
|
-H "anthropic-version: 2023-06-01" \\
|
||||||
-H "Content-Type: application/json" \\
|
-H "Content-Type: application/json" \\
|
||||||
|
|
@ -453,7 +453,7 @@ console.log(response.choices[0].message.content);`,
|
||||||
python: `import anthropic
|
python: `import anthropic
|
||||||
|
|
||||||
client = anthropic.Anthropic(
|
client = anthropic.Anthropic(
|
||||||
base_url="https://api.tokendance.space",
|
base_url="https://api.turingtoken.ai",
|
||||||
api_key="sk-your-key"
|
api_key="sk-your-key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -466,7 +466,7 @@ print(response.content[0].text)`,
|
||||||
nodejs: `import Anthropic from "@anthropic-ai/sdk";
|
nodejs: `import Anthropic from "@anthropic-ai/sdk";
|
||||||
|
|
||||||
const client = new Anthropic({
|
const client = new Anthropic({
|
||||||
baseURL: "https://api.tokendance.space",
|
baseURL: "https://api.turingtoken.ai",
|
||||||
apiKey: "sk-your-key",
|
apiKey: "sk-your-key",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -478,7 +478,7 @@ const response = await client.messages.create({
|
||||||
console.log(response.content[0].text);`,
|
console.log(response.content[0].text);`,
|
||||||
},
|
},
|
||||||
gemini: {
|
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" \\
|
-H "Content-Type: application/json" \\
|
||||||
-d '{
|
-d '{
|
||||||
"contents": [{"parts":[{"text": "Hello!"}]}]
|
"contents": [{"parts":[{"text": "Hello!"}]}]
|
||||||
|
|
@ -732,12 +732,6 @@ function DarkFooter() {
|
||||||
>
|
>
|
||||||
{t('定价')}
|
{t('定价')}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
to="/about"
|
|
||||||
className="font-mono text-xs text-background/40 hover:text-background transition-all duration-200"
|
|
||||||
>
|
|
||||||
{t('关于')}
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to="/console"
|
to="/console"
|
||||||
className="font-mono text-xs text-background/40 hover:text-background transition-all duration-200"
|
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">
|
<span className="font-mono text-xs text-background/30">
|
||||||
© 2026 TokenFactory
|
© 2026 TokenFactory
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -588,7 +588,18 @@ const Playground = () => {
|
||||||
if (searchParams.get('expired')) {
|
if (searchParams.get('expired')) {
|
||||||
Toast.warning(t('登录过期,请重新登录!'));
|
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 自动更新
|
// Playground 组件无需再监听窗口变化,isMobile 由 useIsMobile Hook 自动更新
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,11 +41,11 @@ const sectionConfigs = [
|
||||||
{
|
{
|
||||||
key: 'chat',
|
key: 'chat',
|
||||||
title_key: '聊天区域',
|
title_key: '聊天区域',
|
||||||
desc_key: '体验馆和聊天功能',
|
desc_key: '体验和聊天功能',
|
||||||
modules: [
|
modules: [
|
||||||
{
|
{
|
||||||
key: 'playground',
|
key: 'playground',
|
||||||
title_key: '体验馆',
|
title_key: '体验',
|
||||||
desc_key: 'AI模型测试环境',
|
desc_key: 'AI模型测试环境',
|
||||||
},
|
},
|
||||||
{ key: 'chat', title_key: '聊天', desc_key: '聊天会话管理' },
|
{ key: 'chat', title_key: '聊天', desc_key: '聊天会话管理' },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue