From e1f10fec9511f7f530703939d9f17cb5efff91fb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 3 Mar 2026 15:12:13 +0800 Subject: [PATCH 1/3] add localiazed --- model/ability.go | 3 +- model/model_meta.go | 12 ++ model/pricing.go | 4 +- .../common/ui/HorizontalFilterRow.jsx | 140 +++++++++++++ .../view/card/PricingCardView.jsx | 13 +- .../model-pricing/useModelPricingData.jsx | 11 + .../model-pricing/usePricingFilterCounts.js | 26 +++ web/src/pages/Home/HomePricingFilters.jsx | 197 ++++++++++++++++++ web/src/pages/Home/index.jsx | 87 +------- web/vite.config.js | 5 + 10 files changed, 410 insertions(+), 88 deletions(-) create mode 100644 web/src/components/common/ui/HorizontalFilterRow.jsx create mode 100644 web/src/pages/Home/HomePricingFilters.jsx diff --git a/model/ability.go b/model/ability.go index 1d7c53f..3f26aa5 100644 --- a/model/ability.go +++ b/model/ability.go @@ -33,7 +33,8 @@ func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) { err := DB.Table("abilities"). Select("abilities.*, channels.type as channel_type"). Joins("left join channels on abilities.channel_id = channels.id"). - Where("abilities.enabled = ?", true). + Joins("left join models on abilities.model = models.model_name"). + Where("abilities.enabled = ? and models.model_name is NOT null", true). Scan(&abilities).Error return abilities, err } diff --git a/model/model_meta.go b/model/model_meta.go index 860b960..4f76898 100644 --- a/model/model_meta.go +++ b/model/model_meta.go @@ -15,6 +15,17 @@ const ( NameRuleSuffix ) +const ( + ModelTypeChat = 1 + iota + ModelTypeImage + ModelTypeAudio + ModelTypeVideo + ModelTypeEmbedding + ModelTypeRerank + ModelTypeVision + ModelTypeOther +) + type BoundChannel struct { Name string `json:"name"` Type int `json:"type"` @@ -26,6 +37,7 @@ type Model struct { Description string `json:"description,omitempty" gorm:"type:text"` Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` + Type int `json:"type" gorm:"default:0"` VendorID int `json:"vendor_id,omitempty" gorm:"index"` Endpoints string `json:"endpoints,omitempty" gorm:"type:text"` Status int `json:"status" gorm:"default:1"` diff --git a/model/pricing.go b/model/pricing.go index cb687d0..6dfe69b 100644 --- a/model/pricing.go +++ b/model/pricing.go @@ -28,6 +28,7 @@ type Pricing struct { EnableGroup []string `json:"enable_groups"` SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` PricingVersion string `json:"pricing_version,omitempty"` + Type int `json:"type"` } type PricingVendor struct { @@ -279,13 +280,14 @@ func updatePricing() { // 补充模型元数据(描述、标签、供应商、状态) if meta, ok := metaMap[model]; ok { // 若模型被禁用(status!=1),则直接跳过,不返回给前端 - if meta.Status != 1 { + if meta.Status != 1 || meta.Type == 0 { continue } pricing.Description = meta.Description pricing.Icon = meta.Icon pricing.Tags = meta.Tags pricing.VendorID = meta.VendorID + pricing.Type = meta.Type } modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) if findPrice { diff --git a/web/src/components/common/ui/HorizontalFilterRow.jsx b/web/src/components/common/ui/HorizontalFilterRow.jsx new file mode 100644 index 0000000..b98cce6 --- /dev/null +++ b/web/src/components/common/ui/HorizontalFilterRow.jsx @@ -0,0 +1,140 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useRef } from 'react'; +import { Button, Tag, Skeleton } from '@douyinfe/semi-ui'; +import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons'; +import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; + +/** + * 横向筛选项行,通过左右箭头翻页 + * @param {string} title 标题 + * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 选项列表 + * @param {*} activeValue 当前选中值 + * @param {(value:any)=>void} onChange 选择回调 + * @param {boolean} loading 加载中 + * @param {Function} t i18n + */ +const HorizontalFilterRow = ({ + title, + items = [], + activeValue, + onChange, + loading = false, + t = (v) => v, +}) => { + const scrollRef = useRef(null); + const showSkeleton = useMinimumLoadingTime(loading); + + const handleScroll = (dir) => { + const el = scrollRef.current; + if (!el) return; + const step = el.clientWidth * 0.8; + el.scrollBy({ + left: dir === 'left' ? -step : step, + behavior: 'smooth', + }); + }; + + if (showSkeleton) { + return ( +
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+ ); + } + + return ( +
+
+ {title} +
+
+ + ); + })} +
+
+ + ); +}; + +export default HorizontalFilterRow; diff --git a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx index 6d53286..849ea97 100644 --- a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx +++ b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx @@ -74,13 +74,14 @@ const PricingCardView = ({ selectedRowKeys = [], setSelectedRowKeys, openModelDetail, + gridColsClass = 'grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3', + showPagination = true, }) => { const showSkeleton = useMinimumLoadingTime(loading); const startIndex = (currentPage - 1) * pageSize; - const paginatedModels = filteredModels.slice( - startIndex, - startIndex + pageSize, - ); + const paginatedModels = showPagination + ? filteredModels.slice(startIndex, startIndex + pageSize) + : filteredModels; const getModelKey = (model) => model.key ?? model.model_name ?? model.id; const isMobile = useIsMobile(); @@ -234,7 +235,7 @@ const PricingCardView = ({ return (
-
+
{paginatedModels.map((model, index) => { const modelKey = getModelKey(model); const isSelected = selectedRowKeys.includes(modelKey); @@ -357,7 +358,7 @@ const PricingCardView = ({
{/* 分页 */} - {filteredModels.length > 0 && ( + {showPagination && filteredModels.length > 0 && (
{ const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string + const [filterType, setFilterType] = useState('all'); // 模型类型筛选: 'all' | number (model_meta.ModelType) const [pageSize, setPageSize] = useState(20); const [currentPage, setCurrentPage] = useState(1); const [currency, setCurrency] = useState('USD'); @@ -135,6 +136,12 @@ export const useModelPricingData = () => { }); } + // 模型类型筛选(pricing.Type / model.Type) + if (filterType !== 'all') { + const typeNum = Number(filterType); + result = result.filter((model) => model.type === typeNum); + } + // 搜索筛选 if (searchValue.length > 0) { const searchTerm = searchValue.toLowerCase(); @@ -159,6 +166,7 @@ export const useModelPricingData = () => { filterEndpointType, filterVendor, filterTag, + filterType, ]); const rowSelection = useMemo( @@ -321,6 +329,7 @@ export const useModelPricingData = () => { filterEndpointType, filterVendor, filterTag, + filterType, searchValue, ]); @@ -350,6 +359,8 @@ export const useModelPricingData = () => { setFilterVendor, filterTag, setFilterTag, + filterType, + setFilterType, pageSize, setPageSize, currentPage, diff --git a/web/src/hooks/model-pricing/usePricingFilterCounts.js b/web/src/hooks/model-pricing/usePricingFilterCounts.js index 3bb3d85..dd422d2 100644 --- a/web/src/hooks/model-pricing/usePricingFilterCounts.js +++ b/web/src/hooks/model-pricing/usePricingFilterCounts.js @@ -37,6 +37,7 @@ export const usePricingFilterCounts = ({ filterEndpointType = 'all', filterVendor = 'all', filterTag = 'all', + filterType = 'all', searchValue = '', }) => { // 均使用同一份模型列表,避免创建新引用 @@ -84,6 +85,12 @@ export const usePricingFilterCounts = ({ if (!tagsArr.includes(filterTag.toLowerCase())) return false; } + // 模型类型(pricing.Type / model.Type) + if (!ignore.includes('type') && filterType !== 'all') { + const typeNum = Number(filterType); + if (model.type !== typeNum) return false; + } + // 搜索 if (!ignore.includes('search') && searchValue) { const term = searchValue.toLowerCase(); @@ -112,6 +119,7 @@ export const usePricingFilterCounts = ({ filterEndpointType, filterVendor, filterTag, + filterType, searchValue, ], ); @@ -124,6 +132,7 @@ export const usePricingFilterCounts = ({ filterQuotaType, filterVendor, filterTag, + filterType, searchValue, ], ); @@ -136,6 +145,7 @@ export const usePricingFilterCounts = ({ filterQuotaType, filterEndpointType, filterTag, + filterType, searchValue, ], ); @@ -148,6 +158,20 @@ export const usePricingFilterCounts = ({ filterQuotaType, filterEndpointType, filterVendor, + filterType, + searchValue, + ], + ); + + const typeModels = useMemo( + () => allModels.filter((m) => matchesFilters(m, ['type'])), + [ + allModels, + filterGroup, + filterQuotaType, + filterEndpointType, + filterVendor, + filterTag, searchValue, ], ); @@ -160,6 +184,7 @@ export const usePricingFilterCounts = ({ filterEndpointType, filterVendor, filterTag, + filterType, searchValue, ], ); @@ -170,5 +195,6 @@ export const usePricingFilterCounts = ({ vendorModels, groupCountModels, tagModels, + typeModels, }; }; diff --git a/web/src/pages/Home/HomePricingFilters.jsx b/web/src/pages/Home/HomePricingFilters.jsx new file mode 100644 index 0000000..7726a0d --- /dev/null +++ b/web/src/pages/Home/HomePricingFilters.jsx @@ -0,0 +1,197 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useMemo, useState } from 'react'; +import { ImagePreview } from '@douyinfe/semi-ui'; +import HorizontalFilterRow from '../../components/common/ui/HorizontalFilterRow'; +import PricingCardView from '../../components/table/model-pricing/view/card/PricingCardView'; +import ModelDetailSideSheet from '../../components/table/model-pricing/modal/ModelDetailSideSheet'; +import { useModelPricingData } from '../../hooks/model-pricing/useModelPricingData'; +import { usePricingFilterCounts } from '../../hooks/model-pricing/usePricingFilterCounts'; +import { getLobeHubIcon } from '../../helpers'; + +// 与 model_meta.go ModelType 常量一致:1=Chat, 2=Image, 3=Audio, 4=Video, 5=Embedding, 6=Rerank, 7=Vision, 8=Other +const MODEL_TYPE_LIST = [ + { value: 1, labelKey: '对话' }, + { value: 2, labelKey: '图像' }, + { value: 3, labelKey: '音频' }, + { value: 4, labelKey: '视频' }, + { value: 5, labelKey: '嵌入' }, + { value: 6, labelKey: '重排序' }, + { value: 7, labelKey: '视觉' }, + { value: 8, labelKey: '其他' }, +]; + +const HomePricingFilters = ({ t }) => { + const [showRatio, setShowRatio] = useState(false); + const pricingData = useModelPricingData(); + const { + vendorModels, + typeModels, + } = usePricingFilterCounts({ + models: pricingData.models, + filterGroup: pricingData.filterGroup, + filterQuotaType: pricingData.filterQuotaType, + filterEndpointType: pricingData.filterEndpointType, + filterVendor: pricingData.filterVendor, + filterTag: pricingData.filterTag, + filterType: pricingData.filterType, + searchValue: pricingData.searchValue, + }); + + const vendorItems = useMemo(() => { + const models = pricingData.models; + const vendorSet = new Set(); + const vendorIcons = new Map(); + let hasUnknown = false; + (models || []).forEach((m) => { + if (m.vendor_name) { + vendorSet.add(m.vendor_name); + if (m.vendor_icon && !vendorIcons.has(m.vendor_name)) { + vendorIcons.set(m.vendor_name, m.vendor_icon); + } + } else { + hasUnknown = true; + } + }); + const vendors = Array.from(vendorSet).sort(); + const getCount = (v) => { + if (v === 'all') return vendorModels.length; + if (v === 'unknown') return vendorModels.filter((m) => !m.vendor_name).length; + return vendorModels.filter((m) => m.vendor_name === v).length; + }; + const items = [ + { value: 'all', label: t('全部供应商'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, + ]; + vendors.forEach((v) => { + const count = getCount(v); + items.push({ + value: v, + label: v, + icon: vendorIcons.get(v) ? getLobeHubIcon(vendorIcons.get(v), 16) : null, + tagCount: count, + disabled: count === 0, + }); + }); + if (hasUnknown) { + items.push({ + value: 'unknown', + label: t('未知供应商'), + tagCount: getCount('unknown'), + disabled: getCount('unknown') === 0, + }); + } + return items; + }, [pricingData.models, vendorModels, t]); + + const typeItems = useMemo(() => { + const models = pricingData.models; + const getCount = (typeVal) => { + if (typeVal === 'all') return typeModels.length; + const n = Number(typeVal); + return typeModels.filter((m) => m.type === n).length; + }; + const items = [ + { value: 'all', label: t('全部类型'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, + ]; + MODEL_TYPE_LIST.forEach(({ value, labelKey }) => { + const count = getCount(value); + items.push({ + value: String(value), + label: t(labelKey), + tagCount: count, + disabled: count === 0, + }); + }); + return items; + }, [pricingData.models, typeModels, t]); + + return ( +
+
+ + +
+ +
+ +
+ + pricingData.setIsModalOpenurl(visible)} + /> + + +
+ ); +}; + +export default HomePricingFilters; diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx index c153c1b..e1a238c 100644 --- a/web/src/pages/Home/index.jsx +++ b/web/src/pages/Home/index.jsx @@ -40,6 +40,7 @@ import { } from '@douyinfe/semi-icons'; import { Link } from 'react-router-dom'; import NoticeModal from '../../components/layout/NoticeModal'; +import HomePricingFilters from './HomePricingFilters'; import { Moonshot, OpenAI, @@ -162,9 +163,9 @@ const Home = () => { {/* 背景模糊晕染球 */}
-
+
{/* 居中内容区 */} -
+

{ )}

- {/* 框架兼容性图标 */} -
-
- - {t('支持众多的大模型供应商')} - -
-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- - 30+ - -
-
-
+
+ {/* 模型广场筛选项与列表 */} +
+
diff --git a/web/vite.config.js b/web/vite.config.js index 73e4621..65828e0 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -29,6 +29,11 @@ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, './src'), + // 绕过 semi-ui exports 限制,dist/css/semi.css 不在 package.json exports 中 + '@douyinfe/semi-ui/dist/css/semi.css': path.resolve( + __dirname, + 'node_modules/@douyinfe/semi-ui/dist/css/semi.css', + ), }, }, plugins: [ From 1b08d03a2fc05fe151a6dcbd988e0f3676796895 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 3 Mar 2026 16:11:19 +0800 Subject: [PATCH 2/3] update icons --- web/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/package.json b/web/package.json index 97c7c82..145448d 100644 --- a/web/package.json +++ b/web/package.json @@ -6,13 +6,15 @@ "dependencies": { "@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1", - "@lobehub/icons": "^2.0.0", + "@lobehub/icons": "^5.0.1", + "@lobehub/ui": "^4.38.0", "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", "axios": "1.13.5", "clsx": "^2.1.1", "dayjs": "^1.11.11", + "es-toolkit": "^1.21.0", "history": "^5.3.0", "i18next": "^23.16.8", "i18next-browser-languagedetector": "^7.2.0", From bf779f306827a7da136d261db6f26a53f6c7647e Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 5 Mar 2026 10:29:29 +0800 Subject: [PATCH 3/3] 'upgrade' --- .../modal/ModelDetailSideSheet.jsx | 2 + .../modal/components/ModelCodeSnippet.jsx | 232 ++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx diff --git a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx index 6542137..c23e9f8 100644 --- a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx +++ b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx @@ -26,6 +26,7 @@ import ModelHeader from './components/ModelHeader'; import ModelBasicInfo from './components/ModelBasicInfo'; import ModelEndpoints from './components/ModelEndpoints'; import ModelPricingTable from './components/ModelPricingTable'; +import ModelCodeSnippet from './components/ModelCodeSnippet'; const { Text } = Typography; @@ -99,6 +100,7 @@ const ModelDetailSideSheet = ({ autoGroups={autoGroups} t={t} /> + )}
diff --git a/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx new file mode 100644 index 0000000..cad1549 --- /dev/null +++ b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx @@ -0,0 +1,232 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useMemo, useContext } from 'react'; +import { + Card, + Avatar, + Typography, + RadioGroup, + Radio, + TextArea, + Button, +} from '@douyinfe/semi-ui'; +import { IconCode } from '@douyinfe/semi-icons'; +import { copy, showSuccess, showInfo } from '../../../../../helpers'; +import { StatusContext } from '../../../../../context/Status'; + +const { Text } = Typography; + +const LANG_OPTIONS = [ + { value: 'python', label: 'Python' }, + { value: 'java', label: 'Java' }, + { value: 'go', label: 'Go' }, + { value: 'shell', label: 'Shell' }, +]; + +// 四种语言的调用示例代码模板,使用 __MODEL_NAME__ 与 __BASE_URL__ 作为占位符 +const getDefaultCodeByLang = () => ({ + python: ` +from openai import OpenAI + +client = OpenAI( + api_key="", + base_url="__BASE_URL__" +) + +response = client.chat.completions.create( + model="__MODEL_NAME__", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello, how are you?"} + ], + max_tokens=128000, + temperature=0.7 +) + +print(response.choices[0].message.content) + `, + java: ` +import com.openai.OpenAI; +import com.openai.models.*; + +OpenAI client = new OpenAI("", "__BASE_URL__"); + +ChatCompletionRequest request = ChatCompletionRequest.builder() + .model("__MODEL_NAME__") + .messages(Arrays.asList( + new ChatMessage("system", "You are a helpful assistant."), + new ChatMessage("user", "Hello, how are you?") + )) + .maxTokens(1000) + .temperature(0.7) + .build(); + +ChatCompletion response = client.chatCompletions().create(request); +System.out.println(response.getChoices().get(0).getMessage().getContent()); + `, + go: ` +package main + +import ( + "context" + "fmt" + "github.com/openai/openai-go" +) + +func main() { + client := openai.NewClient("", "__BASE_URL__") + + messages := []openai.ChatMessage{ + {Role: "system", Content: "You are a helpful assistant."}, + {Role: "user", Content: "Hello, how are you?"}, + } + + response, err := client.ChatCompletions.Create(context.Background(), openai.ChatCompletionRequest{ + Model: "__MODEL_NAME__", + Messages: messages, + MaxTokens: 128000, + Temperature: 0.7, + }) + + if err != nil { + panic(err) + } + + fmt.Println(response.Choices[0].Message.Content) +} + `, + shell: ` +#!/bin/bash + +API_KEY="" +MODEL_ID="__MODEL_NAME__" +BASE_URL="__BASE_URL__" + +curl -X POST "$BASE_URL/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $API_KEY" \ + -d '{ + "model": "'$MODEL_ID'", + "messages": [ + { + "role": "system", + "content": "You are a helpful assistant." + }, + { + "role": "user", + "content": "Hello, how are you?" + } + ], + "max_tokens": 128000, + "temperature": 0.7 + }' + `, +}); + +const PLACEHOLDER_MODEL = '__MODEL_NAME__'; +const PLACEHOLDER_BASE_URL = '__BASE_URL__'; + +const ModelCodeSnippet = ({ modelData, t }) => { + const [statusState] = useContext(StatusContext); + const serverAddress = + statusState?.status?.server_address || (typeof window !== 'undefined' ? window.location.origin : ''); + const [lang, setLang] = useState('python'); + const codeByLang = useMemo(() => { + const templates = getDefaultCodeByLang(); + const modelName = modelData?.model_name || ''; + const baseUrl = serverAddress || ''; + return Object.fromEntries( + Object.entries(templates).map(([key, code]) => { + let result = code.replaceAll(PLACEHOLDER_BASE_URL, baseUrl); + result = result.replaceAll(PLACEHOLDER_MODEL, modelName); + return [key, result]; + }), + ); + }, [modelData?.model_name, serverAddress]); + + const currentCode = codeByLang[lang] || ''; + + const handleCopy = async () => { + if (!currentCode || currentCode.trim() === '') { + showInfo(t('当前语言暂无示例代码')); + return; + } + const ok = await copy(currentCode); + if (ok) { + showSuccess(t('已复制到剪切板')); + } + }; + + return ( + +
+ + + +
+ {t('调用示例')} +
+ {t('使用 OpenAI 兼容接口调用该模型的示例代码')} +
+
+
+ +
+ + {t('选择语言')} + + setLang(e.target.value)} + direction='horizontal' + > + {LANG_OPTIONS.map((opt) => ( + + {opt.label} + + ))} + +
+ +
+