Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
'use client';

import { CheckCircle2, Download, LoaderCircle } from 'lucide-react';

import { CollapsiblePanel } from '@/components/ui/collapsible';
import { Progress } from '@/components/ui/progress';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useI18n } from '@/contexts/i18n-context';
import { formatFileSize } from '@/lib/utils';

export interface DownloadProgressFile {
name: string;
downloaded_bytes: number;
total_bytes: number | null;
progress: number | null;
speed_bytes_per_second: number | null;
elapsed_seconds: number;
eta_seconds: number | null;
status: string;
replica_id?: number;
replica_model_uid?: string;
}

interface DownloadProgressDetailsProps {
files: DownloadProgressFile[];
}

function getProgressPercent(file: DownloadProgressFile): number {
const progress = Number(file.progress);

if (Number.isFinite(progress)) {
const percent = progress <= 1 ? progress * 100 : progress;
return Math.max(0, Math.min(100, percent));
}

if (file.total_bytes && file.total_bytes > 0) {
return Math.max(0, Math.min(100, (file.downloaded_bytes / file.total_bytes) * 100));
}

return 0;
}
Comment thread
qinxuye marked this conversation as resolved.

function formatSpeed(bytesPerSecond: number | null, completed: boolean): string {
if (completed || bytesPerSecond === null || !Number.isFinite(bytesPerSecond)) {
return '—';
}

return `${formatFileSize(Math.max(0, bytesPerSecond))}/s`;
}

function formatDuration(seconds: number | null): string {
if (seconds === null || !Number.isFinite(seconds)) {
return '—';
}

const totalSeconds = Math.max(0, Math.ceil(seconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const remainingSeconds = totalSeconds % 60;

return [hours, minutes, remainingSeconds]
.map((value) => String(value).padStart(2, '0'))
.join(':');
}

export default function DownloadProgressDetails({ files }: DownloadProgressDetailsProps) {
const { t } = useI18n();
const hasMultipleReplicas =
new Set(files.map((file) => file.replica_model_uid).filter(Boolean)).size > 1;

return (
<CollapsiblePanel
title={
<span className="flex items-center gap-2">
{t('launchModel.downloadDetails')}
<span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-semibold text-primary">
{files.length}
</span>
</span>
}
icon={<Download className="size-4" />}
className="rounded-lg"
contentClassName="p-0"
>
{files.length === 0 ? (
<div className="flex min-h-24 items-center justify-center gap-2 px-4 py-6 text-sm text-muted-foreground">
<LoaderCircle className="size-4 animate-spin text-primary" />
{t('launchModel.waitingDownloadDetails')}
</div>
) : (
<div className="max-h-64 overflow-y-auto" aria-live="polite">
<Table size="small" className="min-w-[760px] table-fixed">
<TableHeader className="sticky top-0 z-10">
<TableRow className="hover:bg-muted">
<TableHead className="w-[34%]">{t('launchModel.downloadFileName')}</TableHead>
<TableHead className="w-[25%]">{t('launchModel.downloadProgress')}</TableHead>
<TableHead className="w-[15%]">{t('launchModel.downloadStatus')}</TableHead>
<TableHead className="w-[13%]">{t('launchModel.downloadSpeed')}</TableHead>
<TableHead className="w-[13%]">{t('launchModel.downloadEta')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{files.map((file, index) => {
const progress = getProgressPercent(file);
const completed = file.status === 'completed' || progress >= 100;

return (
<TableRow key={`${file.replica_model_uid || 'model'}:${file.name}:${index}`}>
<TableCell className="min-w-0">
<div className="truncate font-medium" title={file.name || '-'}>
{file.name || '-'}
</div>
{hasMultipleReplicas && (
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
{t('launchModel.replica')} {file.replica_id}
</div>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={progress} className="h-1.5 min-w-24 flex-1" />
<span className="w-9 shrink-0 text-right tabular-nums text-muted-foreground">
{Math.round(progress)}%
</span>
</div>
</TableCell>
<TableCell>
<span className="flex items-center gap-1.5 whitespace-nowrap">
{completed ? (
<CheckCircle2 className="size-3.5 text-emerald-500" />
) : (
<LoaderCircle className="size-3.5 animate-spin text-primary" />
)}
{t(
completed
? 'launchModel.downloadCompleted'
: 'launchModel.downloadInProgress'
)}
</span>
</TableCell>
<TableCell className="whitespace-nowrap tabular-nums text-muted-foreground">
{formatSpeed(file.speed_bytes_per_second, completed)}
</TableCell>
<TableCell className="whitespace-nowrap tabular-nums text-muted-foreground">
{formatDuration(completed ? 0 : file.eta_seconds)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</CollapsiblePanel>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
GPU_IDX_PATTERN,
} from '../utils';
import CommandLine from './command-line';
import DownloadProgressDetails, { type DownloadProgressFile } from './download-progress-details';
import ReplicaPlacementConfig from './replica-placement-config';
import { FormField } from '@/components/ui/form-field';

Expand All @@ -66,6 +67,23 @@ interface LaunchDialogProps {
onOpenChange: (open: boolean) => void;
}

interface LaunchProgressReplica {
replica_id: number;
replica_model_uid: string;
progress: number;
stage: string;
info: string | null;
updated_at: number | null;
download_files: DownloadProgressFile[];
}

interface LaunchProgressResponse {
progress?: number | string;
stage?: string;
download_files?: DownloadProgressFile[];
replicas?: LaunchProgressReplica[];
}

export default function LaunchDialog({
model,
modelType,
Expand All @@ -83,6 +101,7 @@ export default function LaunchDialog({
const [canceling, setCanceling] = useState(false);
const [saveAutostart, setSaveAutostart] = useState(false);
const [progress, setProgress] = useState(0);
const [progressDetails, setProgressDetails] = useState<LaunchProgressResponse | null>(null);
const [replicaStatuses, setReplicaStatuses] = useState<ReplicaItem[]>([]);
const [configCacheRefreshKey, setConfigCacheRefreshKey] = useState(0);
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
Expand Down Expand Up @@ -1315,9 +1334,7 @@ export default function LaunchDialog({
const modelUid = form.getFieldValue('model_uid') || model?.model_name;
try {
const [progressRes, replicaRes] = await Promise.all([
request.get<number | string | { progress?: number | string }>(
`/v1/models/${modelUid}/progress`
),
request.get<number | string | LaunchProgressResponse>(`/v1/models/${modelUid}/progress`),
request.get<unknown>(`/v1/models/${modelUid}/replicas`),
]);

Expand All @@ -1326,6 +1343,7 @@ export default function LaunchDialog({
const nextProgress = normalizeProgress(progressValue);

setProgress(nextProgress);
setProgressDetails(progressRes && typeof progressRes === 'object' ? progressRes : null);
setReplicaStatuses(normalizeReplicaStatuses(replicaRes));

if (nextProgress >= 100) {
Expand Down Expand Up @@ -1416,6 +1434,7 @@ export default function LaunchDialog({
stopPolling();
setLoading(false);
setProgress(0);
setProgressDetails(null);
setReplicaStatuses([]);
toast.success(t('launchModel.launchCanceled'));
} finally {
Expand Down Expand Up @@ -1450,6 +1469,7 @@ export default function LaunchDialog({
isCanceledLaunchRef.current = false;
setLoading(true);
setProgress(0);
setProgressDetails(null);
setReplicaStatuses([]);

request
Expand Down Expand Up @@ -1504,6 +1524,7 @@ export default function LaunchDialog({
setLoading(false);
setCanceling(false);
setProgress(0);
setProgressDetails(null);
setReplicaStatuses([]);
setSaveAutostart(false);
stopPolling();
Expand Down Expand Up @@ -1564,7 +1585,10 @@ export default function LaunchDialog({
onOpenChange(open);
}}
>
<DialogContent className="!max-w-3xl" maskClosable={false}>
<DialogContent
className={cn(loading ? '!max-h-[calc(100%-2rem)] !max-w-6xl' : '!max-w-3xl')}
maskClosable={false}
>
<DialogHeader>
<div className="flex min-w-0 items-center justify-between gap-3 pr-10">
<DialogTitle className="min-w-0 truncate">{model?.model_name}</DialogTitle>
Expand All @@ -1590,7 +1614,20 @@ export default function LaunchDialog({
{renderLaunchFields(currentLaunchFields)}
</Form>
<DialogFooter className={cn(loading ? '!flex-col' : '')}>
{loading && <Progress value={progress} />}
{loading && (
<div className="w-full space-y-2 pr-3">
<div className="flex items-center gap-3">
<Progress value={progress} className="flex-1" />
<span className="w-10 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
{Math.round(progress)}%
</span>
</div>
{(progressDetails?.stage === 'downloading' ||
Boolean(progressDetails?.download_files?.length)) && (
<DownloadProgressDetails files={progressDetails?.download_files ?? []} />
)}
</div>
)}
<div className="flex w-full flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<Switch checked={saveAutostart} disabled={loading} onChange={setSaveAutostart} />
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ const en = {
launchCanceled: 'Deployment stopped',
initializing: 'Initializing...',
moreDetails: 'More Details',
downloadDetails: 'Download Details',
waitingDownloadDetails: 'Waiting for file download details...',
downloadFileName: 'File name',
downloadProgress: 'Progress',
downloadStatus: 'Status',
downloadSpeed: 'Speed',
downloadEta: 'Estimated time remaining',
downloadInProgress: 'Downloading',
downloadCompleted: 'Completed',
},
runningModels: {
searchPlaceholder: 'Search model name or model UID...',
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ const ja = {
launchCanceled: 'デプロイを停止しました',
initializing: '初期化中...',
moreDetails: '詳細を見る',
downloadDetails: 'ダウンロード詳細',
waitingDownloadDetails: 'ファイルのダウンロード情報を待っています...',
downloadFileName: 'ファイル名',
downloadProgress: '進捗',
downloadStatus: '状態',
downloadSpeed: '速度',
downloadEta: '推定残り時間',
downloadInProgress: 'ダウンロード中',
downloadCompleted: '完了',
},
runningModels: {
searchPlaceholder: 'モデル名またはモデルUIDを検索...',
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ const ko = {
launchCanceled: '배포가 중지되었습니다',
initializing: '초기화 중...',
moreDetails: '자세히 보기',
downloadDetails: '다운로드 상세 정보',
waitingDownloadDetails: '파일 다운로드 정보를 기다리는 중...',
downloadFileName: '파일 이름',
downloadProgress: '진행률',
downloadStatus: '상태',
downloadSpeed: '속도',
downloadEta: '예상 남은 시간',
downloadInProgress: '다운로드 중',
downloadCompleted: '완료',
},
runningModels: {
searchPlaceholder: '모델 이름 또는 모델 UID 검색...',
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ const zh = {
launchCanceled: '已停止部署',
initializing: '初始化中...',
moreDetails: '更多详情',
downloadDetails: '下载详情',
waitingDownloadDetails: '正在等待文件下载信息...',
downloadFileName: '文件名',
downloadProgress: '进度',
downloadStatus: '状态',
downloadSpeed: '速度',
downloadEta: '预计剩余时间',
downloadInProgress: '下载中',
downloadCompleted: '已完成',
},
runningModels: {
searchPlaceholder: '搜索模型名称或模型UID...',
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/lib/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ requestInstance.interceptors.response.use(
return response.data;
},
async (error) => {
console.log(error, error.message, error.status, 'error');
const response = error.response;
if (!response) {
eventBus.emit(RequestEvents.SERVER_ERROR, error.message || 'Network Error');
Expand Down Expand Up @@ -134,7 +133,6 @@ requestInstance.interceptors.response.use(
response.data?.msg ||
error.message ||
'Unknown error';
console.log(status, response, 'response');

switch (status) {
case 401: {
Expand Down
6 changes: 3 additions & 3 deletions xinference/api/restful_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,13 +1018,13 @@ async def get_model_replicas(self, model_uid: str) -> JSONResponse:

async def get_launch_model_progress(self, model_uid: str) -> JSONResponse:
try:
progress = await (
progress_details = await (
await self._get_supervisor_ref()
).get_launch_builtin_model_progress(model_uid)
).get_launch_builtin_model_progress_details(model_uid)
except Exception as e:
logger.error(str(e), exc_info=True)
raise HTTPException(status_code=500, detail=str(e))
return JSONResponse(content={"progress": progress})
return JSONResponse(content=progress_details)

async def cancel_launch_model(self, model_uid: str) -> JSONResponse:
try:
Expand Down
Loading
Loading