/* 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, { useEffect, useRef, useState } from 'react'; import { Modal, Form, InputNumber, Typography, Card, Space, Divider, Button, Tag, Banner, Spin, } from '@douyinfe/semi-ui'; import { FaClock, FaCalculator, FaInfoCircle, FaExclamationTriangle, } from 'react-icons/fa'; import { API, showError, showSuccess } from '../../../../helpers'; const { Text } = Typography; const ExtendDurationModal = ({ visible, onCancel, deployment, onSuccess, t, }) => { const formRef = useRef(null); const [loading, setLoading] = useState(false); const [durationHours, setDurationHours] = useState(1); const [costLoading, setCostLoading] = useState(false); const [priceEstimation, setPriceEstimation] = useState(null); const [priceError, setPriceError] = useState(null); const [detailsLoading, setDetailsLoading] = useState(false); const [deploymentDetails, setDeploymentDetails] = useState(null); const costRequestIdRef = useRef(0); const resetState = () => { costRequestIdRef.current += 1; setDurationHours(1); setPriceEstimation(null); setPriceError(null); setDeploymentDetails(null); setCostLoading(false); }; const fetchDeploymentDetails = async (deploymentId) => { setDetailsLoading(true); try { const response = await API.get(`/api/deployments/${deploymentId}`); if (response.data.success) { const details = response.data.data; setDeploymentDetails(details); setPriceError(null); return details; } const message = response.data.message || ''; const errorMessage = t('获取详情失败') + (message ? `: ${message}` : ''); showError(errorMessage); setDeploymentDetails(null); setPriceEstimation(null); setPriceError(errorMessage); return null; } catch (error) { const message = error?.response?.data?.message || error.message || ''; const errorMessage = t('获取详情失败') + (message ? `: ${message}` : ''); showError(errorMessage); setDeploymentDetails(null); setPriceEstimation(null); setPriceError(errorMessage); return null; } finally { setDetailsLoading(false); } }; const calculatePrice = async (hours, details) => { if (!visible || !details) { return; } const sanitizedHours = Number.isFinite(hours) ? Math.round(hours) : 0; if (sanitizedHours <= 0) { setPriceEstimation(null); setPriceError(null); return; } const hardwareId = Number(details?.hardware_id) || 0; const totalGPUs = Number(details?.total_gpus) || 0; const totalContainers = Number(details?.total_containers) || 0; const baseGpusPerContainer = Number(details?.gpus_per_container) || 0; const resolvedGpusPerContainer = baseGpusPerContainer > 0 ? baseGpusPerContainer : totalContainers > 0 && totalGPUs > 0 ? Math.max(1, Math.round(totalGPUs / totalContainers)) : 0; const resolvedReplicaCount = totalContainers > 0 ? totalContainers : resolvedGpusPerContainer > 0 && totalGPUs > 0 ? Math.max(1, Math.round(totalGPUs / resolvedGpusPerContainer)) : 0; const locationIds = Array.isArray(details?.locations) ? details.locations .map((location) => Number( location?.id ?? location?.location_id ?? location?.locationId, ), ) .filter((id) => Number.isInteger(id) && id > 0) : []; if ( hardwareId <= 0 || resolvedGpusPerContainer <= 0 || resolvedReplicaCount <= 0 || locationIds.length === 0 ) { setPriceEstimation(null); setPriceError(t('价格计算失败')); return; } const requestId = Date.now(); costRequestIdRef.current = requestId; setCostLoading(true); setPriceError(null); const payload = { location_ids: locationIds, hardware_id: hardwareId, gpus_per_container: resolvedGpusPerContainer, duration_hours: sanitizedHours, replica_count: resolvedReplicaCount, currency: 'usdc', duration_type: 'hour', duration_qty: sanitizedHours, hardware_qty: resolvedGpusPerContainer, }; try { const response = await API.post( '/api/deployments/price-estimation', payload, ); if (costRequestIdRef.current !== requestId) { return; } if (response.data.success) { setPriceEstimation(response.data.data); } else { const message = response.data.message || ''; setPriceEstimation(null); setPriceError( t('价格计算失败') + (message ? `: ${message}` : ''), ); } } catch (error) { if (costRequestIdRef.current !== requestId) { return; } const message = error?.response?.data?.message || error.message || ''; setPriceEstimation(null); setPriceError( t('价格计算失败') + (message ? `: ${message}` : ''), ); } finally { if (costRequestIdRef.current === requestId) { setCostLoading(false); } } }; useEffect(() => { if (visible && deployment?.id) { resetState(); if (formRef.current) { formRef.current.setValue('duration_hours', 1); } fetchDeploymentDetails(deployment.id); } if (!visible) { resetState(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, deployment?.id]); useEffect(() => { if (!visible) { return; } if (!deploymentDetails) { return; } calculatePrice(durationHours, deploymentDetails); // eslint-disable-next-line react-hooks/exhaustive-deps }, [durationHours, deploymentDetails, visible]); const handleExtend = async () => { try { if (formRef.current) { await formRef.current.validate(); } setLoading(true); const response = await API.post( `/api/deployments/${deployment.id}/extend`, { duration_hours: Math.round(durationHours), }, ); if (response.data.success) { showSuccess(t('容器时长延长成功')); onSuccess?.(response.data.data); handleCancel(); } } catch (error) { showError( t('延长时长失败') + ': ' + (error?.response?.data?.message || error.message), ); } finally { setLoading(false); } }; const handleCancel = () => { if (formRef.current) { formRef.current.reset(); } resetState(); onCancel(); }; const currentRemainingTime = deployment?.time_remaining || '0分钟'; const newTotalTime = `${currentRemainingTime} + ${durationHours}${t('小时')}`; const priceData = priceEstimation || {}; const breakdown = priceData.price_breakdown || priceData.PriceBreakdown || {}; const currencyLabel = ( priceData.currency || priceData.Currency || 'USDC' ) .toString() .toUpperCase(); const estimatedTotalCost = typeof priceData.estimated_cost === 'number' ? priceData.estimated_cost : typeof priceData.EstimatedCost === 'number' ? priceData.EstimatedCost : typeof breakdown.total_cost === 'number' ? breakdown.total_cost : breakdown.TotalCost; const hourlyRate = typeof breakdown.hourly_rate === 'number' ? breakdown.hourly_rate : breakdown.HourlyRate; const computeCost = typeof breakdown.compute_cost === 'number' ? breakdown.compute_cost : breakdown.ComputeCost; const resolvedHardwareName = deploymentDetails?.hardware_name || deployment?.hardware_name || '--'; const gpuCount = deploymentDetails?.total_gpus || deployment?.hardware_quantity || 0; const containers = deploymentDetails?.total_containers || 0; return ( {t('延长容器时长')} } visible={visible} onCancel={handleCancel} onOk={handleExtend} okText={t('确认延长')} cancelText={t('取消')} confirmLoading={loading} okButtonProps={{ disabled: !deployment?.id || detailsLoading || !durationHours || durationHours < 1, }} width={600} className='extend-duration-modal' > {deployment?.container_name || deployment?.deployment_name} ID: {deployment?.id} {resolvedHardwareName} {gpuCount ? ` x${gpuCount}` : ''} {t('当前剩余')}: {currentRemainingTime} } title={t('重要提醒')} description={ {t('延长容器时长将会产生额外费用,请确认您有足够的账户余额。')} {t('延长操作一旦确认无法撤销,费用将立即扣除。')} } /> (formRef.current = api)} layout='vertical' onValueChange={(values) => { if (values.duration_hours !== undefined) { const numericValue = Number(values.duration_hours); setDurationHours(Number.isFinite(numericValue) ? numericValue : 0); } }} > {t('快速选择')}: {[1, 2, 6, 12, 24, 48, 72, 168].map((hours) => ( { setDurationHours(hours); if (formRef.current) { formRef.current.setValue('duration_hours', hours); } }} > {hours < 24 ? `${hours}${t('小时')}` : `${hours / 24}${t('天')}`} ))} {t('费用预估')} } className='border border-green-200' > {priceEstimation ? ( {t('延长时长')}: {Math.round(durationHours)} {t('小时')} {t('硬件配置')}: {resolvedHardwareName} {gpuCount ? ` x${gpuCount}` : ''} {containers ? ( {t('容器数量')}: {containers} ) : null} {t('单GPU小时费率')}: {typeof hourlyRate === 'number' ? `${hourlyRate.toFixed(4)} ${currencyLabel}` : '--'} {typeof computeCost === 'number' && ( {t('计算成本')}: {computeCost.toFixed(4)} {currencyLabel} )} {t('预估总费用')}: {typeof estimatedTotalCost === 'number' ? `${estimatedTotalCost.toFixed(4)} ${currencyLabel}` : '--'} {t('延长后总时长')}: {newTotalTime} {t('预估费用仅供参考,实际费用可能略有差异')} ) : ( {costLoading ? ( {t('计算费用中...')} ) : priceError ? ( {priceError} ) : deploymentDetails ? ( {t('请输入延长时长')} ) : ( {t('加载详情中...')} )} )} {t('确认延长容器时长')} {t('点击"确认延长"后将立即扣除费用并延长容器运行时间')} ); }; export default ExtendDurationModal;
{t('延长容器时长将会产生额外费用,请确认您有足够的账户余额。')}
{t('延长操作一旦确认无法撤销,费用将立即扣除。')}