/* 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, useEffect, useMemo, useRef } from 'react'; import { Modal, Form, Input, Select, InputNumber, Switch, Collapse, Card, Divider, Button, Typography, Space, Spin, Tag, Row, Col, Tooltip, Radio, } from '@douyinfe/semi-ui'; import { IconPlus, IconMinus, IconHelpCircle, IconCopy, } from '@douyinfe/semi-icons'; import { API } from '../../../../helpers'; import { showError, showSuccess, copy } from '../../../../helpers'; const { Text, Title } = Typography; const { Option } = Select; const RadioGroup = Radio.Group; const BUILTIN_IMAGE = 'ollama/ollama:latest'; const DEFAULT_TRAFFIC_PORT = 11434; const generateRandomKey = () => { try { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return `ionet-${crypto.randomUUID().replace(/-/g, '')}`; } } catch (error) { // ignore } return `ionet-${Math.random().toString(36).slice(2)}${Math.random() .toString(36) .slice(2)}`; }; const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => { const [formApi, setFormApi] = useState(null); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); // Resource data states const [hardwareTypes, setHardwareTypes] = useState([]); const [hardwareTotalAvailable, setHardwareTotalAvailable] = useState(null); const [locations, setLocations] = useState([]); const [locationTotalAvailable, setLocationTotalAvailable] = useState(null); const [priceEstimation, setPriceEstimation] = useState(null); // UI states const [loadingHardware, setLoadingHardware] = useState(false); const [loadingReplicas, setLoadingReplicas] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false); const [envVariables, setEnvVariables] = useState([{ key: '', value: '' }]); const [secretEnvVariables, setSecretEnvVariables] = useState([ { key: '', value: '' }, ]); const [entrypoint, setEntrypoint] = useState(['']); const [args, setArgs] = useState(['']); const [imageMode, setImageMode] = useState('builtin'); const [autoOllamaKey, setAutoOllamaKey] = useState(''); const customSecretEnvRef = useRef(null); const customEnvRef = useRef(null); const customImageRef = useRef(''); const customTrafficPortRef = useRef(null); const prevImageModeRef = useRef('builtin'); const basicSectionRef = useRef(null); const priceSectionRef = useRef(null); const advancedSectionRef = useRef(null); const replicaRequestIdRef = useRef(0); const [formDefaults, setFormDefaults] = useState({ resource_private_name: '', image_url: BUILTIN_IMAGE, gpus_per_container: 1, replica_count: 1, duration_hours: 1, traffic_port: DEFAULT_TRAFFIC_PORT, location_ids: [], }); const [formKey, setFormKey] = useState(0); const [priceCurrency, setPriceCurrency] = useState('usdc'); const normalizeCurrencyValue = (value) => { if (typeof value === 'string') return value.toLowerCase(); if (value && typeof value === 'object') { if (typeof value.value === 'string') return value.value.toLowerCase(); if (typeof value.target?.value === 'string') { return value.target.value.toLowerCase(); } } return 'usdc'; }; const handleCurrencyChange = (value) => { const normalized = normalizeCurrencyValue(value); setPriceCurrency(normalized); }; const hardwareLabelMap = useMemo(() => { const map = {}; hardwareTypes.forEach((hardware) => { const displayName = hardware.brand_name ? `${hardware.brand_name} ${hardware.name}`.trim() : hardware.name; map[hardware.id] = displayName; }); return map; }, [hardwareTypes]); const locationLabelMap = useMemo(() => { const map = {}; locations.forEach((location) => { map[location.id] = location.name; }); return map; }, [locations]); const getHardwareMaxGpus = (hardwareId) => { if (!hardwareId) return 1; const hardware = hardwareTypes.find((h) => h.id === hardwareId); const maxGpus = Number(hardware?.max_gpus); return Number.isFinite(maxGpus) && maxGpus > 0 ? maxGpus : 1; }; // Form values for price calculation const [selectedHardwareId, setSelectedHardwareId] = useState(null); const [selectedLocationIds, setSelectedLocationIds] = useState([]); const [gpusPerContainer, setGpusPerContainer] = useState(1); const [durationHours, setDurationHours] = useState(1); const [replicaCount, setReplicaCount] = useState(1); useEffect(() => { if (!selectedHardwareId) { return; } const nextMaxGpus = getHardwareMaxGpus(selectedHardwareId); if (gpusPerContainer !== nextMaxGpus) { setGpusPerContainer(nextMaxGpus); } if (formApi) { formApi.setValue('gpus_per_container', nextMaxGpus); } }, [selectedHardwareId, hardwareTypes, formApi, gpusPerContainer]); // Load initial data when modal opens useEffect(() => { if (visible) { loadHardwareTypes(); resetFormState(); } }, [visible]); // Load available replicas when hardware or locations change useEffect(() => { if (!visible) { return; } if (selectedHardwareId && gpusPerContainer > 0) { loadAvailableReplicas(selectedHardwareId, gpusPerContainer); } }, [selectedHardwareId, gpusPerContainer, visible]); // Calculate price when relevant parameters change useEffect(() => { if (!visible) { return; } if ( selectedHardwareId && selectedLocationIds.length > 0 && gpusPerContainer > 0 && durationHours > 0 && replicaCount > 0 ) { calculatePrice(); } else { setPriceEstimation(null); } }, [ selectedHardwareId, selectedLocationIds, gpusPerContainer, durationHours, replicaCount, priceCurrency, visible, ]); useEffect(() => { if (!visible) { return; } const prevMode = prevImageModeRef.current; if (prevMode === imageMode) { return; } if (imageMode === 'builtin') { if (prevMode === 'custom') { if (formApi) { customImageRef.current = formApi.getValue('image_url') || customImageRef.current; customTrafficPortRef.current = formApi.getValue('traffic_port') ?? customTrafficPortRef.current; } customSecretEnvRef.current = secretEnvVariables.map((item) => ({ ...item, })); customEnvRef.current = envVariables.map((item) => ({ ...item })); } const newKey = generateRandomKey(); setAutoOllamaKey(newKey); setSecretEnvVariables([{ key: 'OLLAMA_API_KEY', value: newKey }]); setEnvVariables([{ key: '', value: '' }]); if (formApi) { formApi.setValue('image_url', BUILTIN_IMAGE); formApi.setValue('traffic_port', DEFAULT_TRAFFIC_PORT); } } else { const restoredSecrets = customSecretEnvRef.current && customSecretEnvRef.current.length > 0 ? customSecretEnvRef.current.map((item) => ({ ...item })) : [{ key: '', value: '' }]; const restoredEnv = customEnvRef.current && customEnvRef.current.length > 0 ? customEnvRef.current.map((item) => ({ ...item })) : [{ key: '', value: '' }]; setSecretEnvVariables(restoredSecrets); setEnvVariables(restoredEnv); if (formApi) { const restoredImage = customImageRef.current || ''; formApi.setValue('image_url', restoredImage); if (customTrafficPortRef.current) { formApi.setValue('traffic_port', customTrafficPortRef.current); } } } prevImageModeRef.current = imageMode; }, [imageMode, visible, secretEnvVariables, envVariables, formApi]); useEffect(() => { if (!visible || !formApi) { return; } if (imageMode === 'builtin') { formApi.setValue('image_url', BUILTIN_IMAGE); } }, [formApi, imageMode, visible]); useEffect(() => { if (!formApi) { return; } if (selectedHardwareId !== null && selectedHardwareId !== undefined) { formApi.setValue('hardware_id', selectedHardwareId); } }, [formApi, selectedHardwareId]); useEffect(() => { if (!formApi) { return; } formApi.setValue('location_ids', selectedLocationIds); }, [formApi, selectedLocationIds]); useEffect(() => { if (!visible) { return; } if (selectedHardwareId) { return; } else { setLocations([]); setSelectedLocationIds([]); setLocationTotalAvailable(null); setLoadingReplicas(false); replicaRequestIdRef.current = 0; if (formApi) { formApi.setValue('location_ids', []); } } }, [selectedHardwareId, visible, formApi]); const resetFormState = () => { const randomName = `deployment-${Math.random().toString(36).slice(2, 8)}`; const generatedKey = generateRandomKey(); setSelectedHardwareId(null); setSelectedLocationIds([]); setGpusPerContainer(1); setDurationHours(1); setReplicaCount(1); setPriceEstimation(null); setLocations([]); setLocationTotalAvailable(null); setHardwareTotalAvailable(null); setEnvVariables([{ key: '', value: '' }]); setSecretEnvVariables([{ key: 'OLLAMA_API_KEY', value: generatedKey }]); setEntrypoint(['']); setArgs(['']); setShowAdvanced(false); setImageMode('builtin'); setAutoOllamaKey(generatedKey); customSecretEnvRef.current = null; customEnvRef.current = null; customImageRef.current = ''; customTrafficPortRef.current = DEFAULT_TRAFFIC_PORT; prevImageModeRef.current = 'builtin'; setFormDefaults({ resource_private_name: randomName, image_url: BUILTIN_IMAGE, gpus_per_container: 1, replica_count: 1, duration_hours: 1, traffic_port: DEFAULT_TRAFFIC_PORT, location_ids: [], }); setFormKey((prev) => prev + 1); setPriceCurrency('usdc'); }; const arraysEqual = (a = [], b = []) => a.length === b.length && a.every((value, index) => value === b[index]); const loadHardwareTypes = async () => { try { setLoadingHardware(true); const response = await API.get('/api/deployments/hardware-types'); if (response.data.success) { const { hardware_types: hardwareList = [], total_available } = response.data.data || {}; const normalizedHardware = hardwareList.map((hardware) => { const availableCountValue = Number(hardware.available_count); const availableCount = Number.isNaN(availableCountValue) ? 0 : availableCountValue; const availableBool = typeof hardware.available === 'boolean' ? hardware.available : availableCount > 0; return { ...hardware, available: availableBool, available_count: availableCount, }; }); const providedTotal = Number(total_available); const fallbackTotal = normalizedHardware.reduce( (acc, item) => acc + (Number.isNaN(item.available_count) ? 0 : item.available_count), 0, ); const hasProvidedTotal = total_available !== undefined && total_available !== null && total_available !== '' && !Number.isNaN(providedTotal); setHardwareTypes(normalizedHardware); setHardwareTotalAvailable( hasProvidedTotal ? providedTotal : fallbackTotal, ); } else { showError(t('获取硬件类型失败: ') + response.data.message); } } catch (error) { showError(t('获取硬件类型失败: ') + error.message); } finally { setLoadingHardware(false); } }; const loadAvailableReplicas = async (hardwareId, gpuCount) => { if (!hardwareId || !gpuCount) { setLocations([]); setLocationTotalAvailable(null); setLoadingReplicas(false); return; } const requestId = Date.now(); replicaRequestIdRef.current = requestId; setLoadingReplicas(true); setLocations([]); setLocationTotalAvailable(null); try { const response = await API.get( `/api/deployments/available-replicas?hardware_id=${hardwareId}&gpu_count=${gpuCount}`, ); if (replicaRequestIdRef.current !== requestId) { return; } if (response.data.success) { const replicasList = response.data.data?.replicas || []; const nextLocationsMap = new Map(); replicasList.forEach((replica) => { const rawId = replica?.location_id ?? replica?.location?.id; if (rawId === null || rawId === undefined) { return; } const id = rawId; const mapKey = String(rawId); const existing = nextLocationsMap.get(mapKey) || null; const rawIso2 = replica?.iso2 ?? replica?.location_iso2 ?? replica?.location?.iso2; const iso2 = rawIso2 ? String(rawIso2).toUpperCase() : ''; const name = replica?.location_name ?? replica?.location?.name ?? replica?.name ?? id; const available = Number(replica?.available_count) || 0; if (existing) { existing.available += available; return; } nextLocationsMap.set(mapKey, { id, name: String(name), iso2, region: replica?.region ?? replica?.location_region ?? replica?.location?.region, country: replica?.country ?? replica?.location_country ?? replica?.location?.country, code: replica?.code ?? replica?.location_code ?? replica?.location?.code, available, }); }); setLocations(Array.from(nextLocationsMap.values())); setLocationTotalAvailable( Array.from(nextLocationsMap.values()).reduce( (total, location) => total + (location.available || 0), 0, ), ); } else { showError(t('获取可用资源失败: ') + response.data.message); setLocationTotalAvailable(null); } } catch (error) { if (replicaRequestIdRef.current === requestId) { console.error('Load available replicas error:', error); setLocationTotalAvailable(null); } } finally { if (replicaRequestIdRef.current === requestId) { setLoadingReplicas(false); } } }; const calculatePrice = async () => { try { setLoadingPrice(true); const requestData = { location_ids: selectedLocationIds, hardware_id: selectedHardwareId, gpus_per_container: gpusPerContainer, duration_hours: durationHours, replica_count: replicaCount, currency: priceCurrency?.toLowerCase?.() || priceCurrency, duration_type: 'hour', duration_qty: durationHours, hardware_qty: gpusPerContainer, }; const response = await API.post( '/api/deployments/price-estimation', requestData, ); if (response.data.success) { setPriceEstimation(response.data.data); } else { showError(t('价格计算失败: ') + response.data.message); setPriceEstimation(null); } } catch (error) { console.error('Price calculation error:', error); setPriceEstimation(null); } finally { setLoadingPrice(false); } }; const handleSubmit = async (values) => { try { setSubmitting(true); // Prepare environment variables const envVars = {}; envVariables.forEach((env) => { if (env.key && env.value) { envVars[env.key] = env.value; } }); const secretEnvVars = {}; secretEnvVariables.forEach((env) => { if (env.key && env.value) { secretEnvVars[env.key] = env.value; } }); if (imageMode === 'builtin') { if (!secretEnvVars.OLLAMA_API_KEY) { const ensuredKey = autoOllamaKey || generateRandomKey(); secretEnvVars.OLLAMA_API_KEY = ensuredKey; setAutoOllamaKey(ensuredKey); } } // Prepare entrypoint and args const cleanEntrypoint = entrypoint.filter((item) => item.trim() !== ''); const cleanArgs = args.filter((item) => item.trim() !== ''); const resolvedImage = imageMode === 'builtin' ? BUILTIN_IMAGE : values.image_url; const resolvedTrafficPort = values.traffic_port || (imageMode === 'builtin' ? DEFAULT_TRAFFIC_PORT : undefined); const requestData = { resource_private_name: values.resource_private_name, duration_hours: values.duration_hours, gpus_per_container: gpusPerContainer, hardware_id: values.hardware_id, location_ids: values.location_ids, container_config: { replica_count: values.replica_count, env_variables: envVars, secret_env_variables: secretEnvVars, entrypoint: cleanEntrypoint.length > 0 ? cleanEntrypoint : undefined, args: cleanArgs.length > 0 ? cleanArgs : undefined, traffic_port: resolvedTrafficPort, }, registry_config: { image_url: resolvedImage, registry_username: values.registry_username || undefined, registry_secret: values.registry_secret || undefined, }, }; const response = await API.post('/api/deployments', requestData); if (response.data.success) { showSuccess(t('容器创建成功')); onSuccess?.(response.data.data); onCancel(); } else { showError(t('容器创建失败: ') + response.data.message); } } catch (error) { showError(t('容器创建失败: ') + error.message); } finally { setSubmitting(false); } }; const handleAddEnvVariable = (type) => { if (type === 'env') { setEnvVariables([...envVariables, { key: '', value: '' }]); } else { setSecretEnvVariables([...secretEnvVariables, { key: '', value: '' }]); } }; const handleRemoveEnvVariable = (index, type) => { if (type === 'env') { const newEnvVars = envVariables.filter((_, i) => i !== index); setEnvVariables( newEnvVars.length > 0 ? newEnvVars : [{ key: '', value: '' }], ); } else { const newSecretEnvVars = secretEnvVariables.filter((_, i) => i !== index); setSecretEnvVariables( newSecretEnvVars.length > 0 ? newSecretEnvVars : [{ key: '', value: '' }], ); } }; const handleEnvVariableChange = (index, field, value, type) => { if (type === 'env') { const newEnvVars = [...envVariables]; newEnvVars[index][field] = value; setEnvVariables(newEnvVars); } else { const newSecretEnvVars = [...secretEnvVariables]; newSecretEnvVars[index][field] = value; setSecretEnvVariables(newSecretEnvVars); } }; const handleArrayFieldChange = (index, value, type) => { if (type === 'entrypoint') { const newEntrypoint = [...entrypoint]; newEntrypoint[index] = value; setEntrypoint(newEntrypoint); } else { const newArgs = [...args]; newArgs[index] = value; setArgs(newArgs); } }; const handleAddArrayField = (type) => { if (type === 'entrypoint') { setEntrypoint([...entrypoint, '']); } else { setArgs([...args, '']); } }; const handleRemoveArrayField = (index, type) => { if (type === 'entrypoint') { const newEntrypoint = entrypoint.filter((_, i) => i !== index); setEntrypoint(newEntrypoint.length > 0 ? newEntrypoint : ['']); } else { const newArgs = args.filter((_, i) => i !== index); setArgs(newArgs.length > 0 ? newArgs : ['']); } }; useEffect(() => { if (!visible) { return; } if (!selectedHardwareId) { if (selectedLocationIds.length > 0) { setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); } } return; } const validLocationIds = locations .filter((location) => (Number(location.available) || 0) > 0) .map((location) => location.id); if (validLocationIds.length === 0) { if (selectedLocationIds.length > 0) { setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); } } return; } if (selectedLocationIds.length === 0) { return; } const filteredSelection = selectedLocationIds.filter((id) => validLocationIds.includes(id), ); if (!arraysEqual(selectedLocationIds, filteredSelection)) { setSelectedLocationIds(filteredSelection); if (formApi) { formApi.setValue('location_ids', filteredSelection); } } }, [locations, selectedHardwareId, selectedLocationIds, visible, formApi]); const maxAvailableReplicas = useMemo(() => { if (!selectedLocationIds.length) return 0; return locations .filter((location) => selectedLocationIds.includes(location.id)) .reduce((total, location) => { const availableValue = Number(location.available); return total + (Number.isNaN(availableValue) ? 0 : availableValue); }, 0); }, [selectedLocationIds, locations]); const isPriceReady = useMemo( () => selectedHardwareId && selectedLocationIds.length > 0 && gpusPerContainer > 0 && durationHours > 0 && replicaCount > 0, [ selectedHardwareId, selectedLocationIds, gpusPerContainer, durationHours, replicaCount, ], ); const currencyLabel = ( priceEstimation?.currency || priceCurrency || '' ).toUpperCase(); const selectedHardwareLabel = selectedHardwareId ? hardwareLabelMap[selectedHardwareId] : ''; const selectedLocationNames = selectedLocationIds .map((id) => locationLabelMap[id]) .filter(Boolean); const totalGpuHours = Number(gpusPerContainer || 0) * Number(replicaCount || 0) * Number(durationHours || 0); const priceSummaryItems = [ { key: 'hardware', label: t('硬件类型'), value: selectedHardwareLabel || '--', }, { key: 'locations', label: t('部署位置'), value: selectedLocationNames.length ? selectedLocationNames.join('、') : '--', }, { key: 'replicas', label: t('副本数量'), value: (replicaCount ?? 0).toString(), }, { key: 'gpus', label: t('最大GPU数量'), value: (gpusPerContainer ?? 0).toString(), }, { key: 'duration', label: t('运行时长(小时)'), value: durationHours ? durationHours.toString() : '0', }, { key: 'gpu-hours', label: t('总 GPU 小时'), value: totalGpuHours > 0 ? totalGpuHours.toLocaleString() : '0', }, ]; const scrollToSection = (ref) => { if (ref?.current && typeof ref.current.scrollIntoView === 'function') { ref.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; const priceUnavailableContent = (
{loadingPrice ? ( {t('价格计算中...')} ) : ( {isPriceReady ? t('价格暂时不可用,请稍后重试') : t('完成硬件类型、部署位置、副本数量等配置后,将自动计算价格')} )}
); useEffect(() => { if (!visible || !formApi) { return; } if (maxAvailableReplicas > 0 && replicaCount > maxAvailableReplicas) { setReplicaCount(maxAvailableReplicas); formApi.setValue('replica_count', maxAvailableReplicas); } }, [maxAvailableReplicas, replicaCount, visible, formApi]); return ( formApi?.submitForm()} okText={t('创建')} cancelText={t('取消')} width={800} confirmLoading={submitting} style={{ top: 20 }} >
{t('部署配置')}
{t('镜像选择')}
setImageMode(value?.target?.value ?? value) } > {t('内置 Ollama 镜像')} {t('自定义镜像')}
{ if (imageMode === 'custom') { customImageRef.current = value; } }} /> {imageMode === 'builtin' && ( {t('系统已为该部署准备 Ollama 镜像与随机 API Key')} )} { const nextMaxGpus = getHardwareMaxGpus(value); setSelectedHardwareId(value); setGpusPerContainer(nextMaxGpus); setSelectedLocationIds([]); if (formApi) { formApi.setValue('location_ids', []); formApi.setValue('gpus_per_container', nextMaxGpus); } }} style={{ width: '100%' }} dropdownStyle={{ maxHeight: 360, overflowY: 'auto' }} renderSelectedItem={(optionNode) => optionNode ? hardwareLabelMap[optionNode?.value] || optionNode?.label || optionNode?.value || '' : '' } > {hardwareTypes.map((hardware) => { const displayName = hardware.brand_name ? `${hardware.brand_name} ${hardware.name}`.trim() : hardware.name; const availableCount = typeof hardware.available_count === 'number' ? hardware.available_count : 0; const hasAvailability = availableCount > 0; return ( ); })} {typeof hardwareTotalAvailable === 'number' && ( {t('全部硬件总可用资源')}: {hardwareTotalAvailable} )} {t('部署位置')} {loadingReplicas && } } placeholder={ !selectedHardwareId ? t('请先选择硬件类型') : loadingReplicas ? t('正在加载可用部署位置...') : t('选择部署位置(可多选)') } multiple loading={loadingReplicas} disabled={!selectedHardwareId || loadingReplicas} rules={[{ required: true, message: t('请选择至少一个部署位置') }]} onChange={(value) => setSelectedLocationIds(value)} style={{ width: '100%' }} dropdownStyle={{ maxHeight: 360, overflowY: 'auto' }} renderSelectedItem={(optionNode) => ({ isRenderInTag: true, content: !optionNode ? '' : loadingReplicas ? t('部署位置加载中...') : locationLabelMap[optionNode?.value] || optionNode?.label || optionNode?.value || '', })} > {locations.map((location) => { const numeric = Number(location.available); const availableCount = Number.isNaN(numeric) ? 0 : numeric; const locationLabel = location.region || location.country || (location.iso2 ? location.iso2.toUpperCase() : '') || location.code || ''; const disableOption = availableCount === 0; return ( ); })} {typeof locationTotalAvailable === 'number' && ( {t('全部地区总可用资源')}: {locationTotalAvailable} )} setReplicaCount(value)} style={{ width: '100%' }} /> {maxAvailableReplicas > 0 && ( {t('最大可用')}: {maxAvailableReplicas} )} setDurationHours(value)} style={{ width: '100%' }} /> {t('流量端口')} } placeholder={DEFAULT_TRAFFIC_PORT} min={1} max={65535} style={{ width: '100%' }} disabled={imageMode === 'builtin'} />
{t('镜像仓库配置')} {t('容器启动配置')}
{t('启动命令 (Entrypoint)')} {entrypoint.map((cmd, index) => (
handleArrayFieldChange(index, value, 'entrypoint') } style={{ flex: 1, marginRight: 8 }} />
))}
{t('启动参数 (Args)')} {args.map((arg, index) => (
handleArrayFieldChange(index, value, 'args') } style={{ flex: 1, marginRight: 8 }} />
))}
{t('环境变量')}
{t('普通环境变量')} {envVariables.map((env, index) => ( handleEnvVariableChange( index, 'key', value, 'env', ) } /> handleEnvVariableChange( index, 'value', value, 'env', ) } />
{t('密钥环境变量')} {secretEnvVariables.map((env, index) => { const isAutoSecret = imageMode === 'builtin' && env.key === 'OLLAMA_API_KEY'; return ( handleEnvVariableChange( index, 'key', value, 'secret', ) } disabled={isAutoSecret} /> handleEnvVariableChange( index, 'value', value, 'secret', ) } disabled={isAutoSecret} />
{t('价格预估')} {t('计价币种')} USDC IOCOIN {currencyLabel}
{priceEstimation ? (
{t('预估总费用')}
{typeof priceEstimation.estimated_cost === 'number' ? `${priceEstimation.estimated_cost.toFixed(4)} ${currencyLabel}` : '--'}
{t('小时费率')} {typeof priceEstimation.price_breakdown?.hourly_rate === 'number' ? `${priceEstimation.price_breakdown.hourly_rate.toFixed(4)} ${currencyLabel}/h` : '--'}
{t('计算成本')} {typeof priceEstimation.price_breakdown?.compute_cost === 'number' ? `${priceEstimation.price_breakdown.compute_cost.toFixed(4)} ${currencyLabel}` : '--'}
{priceSummaryItems.map((item) => (
{item.label} {item.value}
))}
) : ( priceUnavailableContent )} {priceEstimation && loadingPrice && ( {t('价格重新计算中...')} )}
); }; export default CreateDeploymentModal;