🐛fix: immediately show download progress (#5308)

This commit is contained in:
Faisal Amir 2025-06-17 12:46:23 +07:00 committed by GitHub
parent a745d24fbe
commit f0ec3e03d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 90 additions and 27 deletions

View File

@ -19,7 +19,13 @@ export function DownloadManagement() {
const { setProviders } = useModelProvider() const { setProviders } = useModelProvider()
const { open: isLeftPanelOpen } = useLeftPanel() const { open: isLeftPanelOpen } = useLeftPanel()
const [isPopoverOpen, setIsPopoverOpen] = useState(false) const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { downloads, updateProgress, removeDownload } = useDownloadStore() const {
downloads,
updateProgress,
localDownloadingModels,
removeDownload,
removeLocalDownloadingModel,
} = useDownloadStore()
const { updateState } = useAppUpdater() const { updateState } = useAppUpdater()
const [appUpdateState, setAppUpdateState] = useState({ const [appUpdateState, setAppUpdateState] = useState({
@ -76,23 +82,36 @@ export function DownloadManagement() {
}) })
}, []) }, [])
const downloadCount = useMemo(() => { const downloadProcesses = useMemo(() => {
const modelDownloads = Object.keys(downloads).length // Get downloads with progress data
const appUpdateDownload = appUpdateState.isDownloading ? 1 : 0 const downloadsWithProgress = Object.values(downloads).map((download) => ({
const total = modelDownloads + appUpdateDownload
return total
}, [downloads, appUpdateState.isDownloading])
const downloadProcesses = useMemo(
() =>
Object.values(downloads).map((download) => ({
id: download.name, id: download.name,
name: download.name, name: download.name,
progress: download.progress, progress: download.progress,
current: download.current, current: download.current,
total: download.total, total: download.total,
})), }))
[downloads]
) // Add local downloading models that don't have progress data yet
const localDownloadsWithoutProgress = Array.from(localDownloadingModels)
.filter((modelId) => !downloads[modelId]) // Only include models not in downloads
.map((modelId) => ({
id: modelId,
name: modelId,
progress: 0,
current: 0,
total: 0,
}))
return [...downloadsWithProgress, ...localDownloadsWithoutProgress]
}, [downloads, localDownloadingModels])
const downloadCount = useMemo(() => {
const modelDownloads = downloadProcesses.length
const appUpdateDownload = appUpdateState.isDownloading ? 1 : 0
const total = modelDownloads + appUpdateDownload
return total
}, [downloadProcesses, appUpdateState.isDownloading])
const overallProgress = useMemo(() => { const overallProgress = useMemo(() => {
const modelTotal = downloadProcesses.reduce((acc, download) => { const modelTotal = downloadProcesses.reduce((acc, download) => {
@ -139,29 +158,32 @@ export function DownloadManagement() {
(state: DownloadState) => { (state: DownloadState) => {
console.debug('onFileDownloadError', state) console.debug('onFileDownloadError', state)
removeDownload(state.modelId) removeDownload(state.modelId)
removeLocalDownloadingModel(state.modelId)
}, },
[removeDownload] [removeDownload, removeLocalDownloadingModel]
) )
const onFileDownloadStopped = useCallback( const onFileDownloadStopped = useCallback(
(state: DownloadState) => { (state: DownloadState) => {
console.debug('onFileDownloadError', state) console.debug('onFileDownloadError', state)
removeDownload(state.modelId) removeDownload(state.modelId)
removeLocalDownloadingModel(state.modelId)
}, },
[removeDownload] [removeDownload, removeLocalDownloadingModel]
) )
const onFileDownloadSuccess = useCallback( const onFileDownloadSuccess = useCallback(
async (state: DownloadState) => { async (state: DownloadState) => {
console.debug('onFileDownloadSuccess', state) console.debug('onFileDownloadSuccess', state)
removeDownload(state.modelId) removeDownload(state.modelId)
removeLocalDownloadingModel(state.modelId)
getProviders().then(setProviders) getProviders().then(setProviders)
toast.success('Download Complete', { toast.success('Download Complete', {
id: 'download-complete', id: 'download-complete',
description: `The model ${state.modelId} has been downloaded`, description: `The model ${state.modelId} has been downloaded`,
}) })
}, },
[removeDownload, setProviders] [removeDownload, removeLocalDownloadingModel, setProviders]
) )
useEffect(() => { useEffect(() => {
@ -264,12 +286,16 @@ export function DownloadManagement() {
/> />
<p className="text-main-view-fg/60 text-xs"> <p className="text-main-view-fg/60 text-xs">
{`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '} {`${renderGB(appUpdateState.downloadedBytes)} / ${renderGB(appUpdateState.totalBytes)}`}{' '}
GB ({Math.round(appUpdateState.downloadProgress * 100)}%) GB ({Math.round(appUpdateState.downloadProgress * 100)}
%)
</p> </p>
</div> </div>
)} )}
{downloadProcesses.map((download) => ( {downloadProcesses.map((download) => (
<div className="bg-main-view-fg/4 rounded-md p-2"> <div
key={download.id}
className="bg-main-view-fg/4 rounded-md p-2"
>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80"> <p className="truncate text-main-view-fg/80">
{download.name} {download.name}
@ -299,8 +325,9 @@ export function DownloadManagement() {
className="my-2" className="my-2"
/> />
<p className="text-main-view-fg/60 text-xs"> <p className="text-main-view-fg/60 text-xs">
{`${renderGB(download.current)} / ${renderGB(download.total)}`}{' '} {download.total > 0
GB ({Math.round(download.progress * 100)}%) ? `${renderGB(download.current)} / ${renderGB(download.total)} GB (${Math.round(download.progress * 100)}%)`
: 'Initializing download...'}
</p> </p>
</div> </div>
))} ))}

View File

@ -11,6 +11,7 @@ export interface DownloadProgressProps {
// Zustand store for thinking block state // Zustand store for thinking block state
export type DownloadState = { export type DownloadState = {
downloads: { [id: string]: DownloadProgressProps } downloads: { [id: string]: DownloadProgressProps }
localDownloadingModels: Set<string>
removeDownload: (id: string) => void removeDownload: (id: string) => void
updateProgress: ( updateProgress: (
id: string, id: string,
@ -19,6 +20,8 @@ export type DownloadState = {
current?: number, current?: number,
total?: number total?: number
) => void ) => void
addLocalDownloadingModel: (modelId: string) => void
removeLocalDownloadingModel: (modelId: string) => void
} }
/** /**
@ -26,6 +29,7 @@ export type DownloadState = {
*/ */
export const useDownloadStore = create<DownloadState>((set) => ({ export const useDownloadStore = create<DownloadState>((set) => ({
downloads: {}, downloads: {},
localDownloadingModels: new Set(),
removeDownload: (id: string) => removeDownload: (id: string) =>
set((state) => { set((state) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -46,4 +50,18 @@ export const useDownloadStore = create<DownloadState>((set) => ({
}, },
}, },
})), })),
addLocalDownloadingModel: (modelId: string) =>
set((state) => ({
localDownloadingModels: new Set(state.localDownloadingModels).add(
modelId
),
})),
removeLocalDownloadingModel: (modelId: string) =>
set((state) => {
const newSet = new Set(state.localDownloadingModels)
newSet.delete(modelId)
return { localDownloadingModels: newSet }
}),
})) }))

View File

@ -182,7 +182,8 @@ function Hub() {
} }
} }
const { downloads } = useDownloadStore() const { downloads, localDownloadingModels, addLocalDownloadingModel } =
useDownloadStore()
const downloadProcesses = useMemo( const downloadProcesses = useMemo(
() => () =>
@ -225,7 +226,9 @@ function Hub() {
model.models.find((e) => model.models.find((e) =>
defaultModelQuantizations.some((m) => e.id.toLowerCase().includes(m)) defaultModelQuantizations.some((m) => e.id.toLowerCase().includes(m))
)?.id ?? model.models[0]?.id )?.id ?? model.models[0]?.id
const isDownloading = downloadProcesses.some((e) => e.id === modelId) const isDownloading =
localDownloadingModels.has(modelId) ||
downloadProcesses.some((e) => e.id === modelId)
const downloadProgress = const downloadProgress =
downloadProcesses.find((e) => e.id === modelId)?.progress || 0 downloadProcesses.find((e) => e.id === modelId)?.progress || 0
const isDownloaded = llamaProvider?.models.some( const isDownloaded = llamaProvider?.models.some(
@ -233,6 +236,12 @@ function Hub() {
) )
const isRecommended = isRecommendedModel(model.metadata?.id) const isRecommended = isRecommendedModel(model.metadata?.id)
const handleDownload = () => {
// Immediately set local downloading state
addLocalDownloadingModel(modelId)
downloadModel(modelId)
}
return ( return (
<div <div
className={cn( className={cn(
@ -255,7 +264,7 @@ function Hub() {
) : ( ) : (
<Button <Button
size="sm" size="sm"
onClick={() => downloadModel(modelId)} onClick={handleDownload}
className={cn(isDownloading && 'hidden')} className={cn(isDownloading && 'hidden')}
ref={isRecommended ? downloadButtonRef : undefined} ref={isRecommended ? downloadButtonRef : undefined}
> >
@ -271,6 +280,8 @@ function Hub() {
handleUseModel, handleUseModel,
isRecommendedModel, isRecommendedModel,
downloadButtonRef, downloadButtonRef,
localDownloadingModels,
addLocalDownloadingModel,
]) ])
const { step } = useSearch({ from: Route.id }) const { step } = useSearch({ from: Route.id })
@ -320,7 +331,8 @@ function Hub() {
} }
// Check if any model is currently downloading // Check if any model is currently downloading
const isDownloading = downloadProcesses.length > 0 const isDownloading =
localDownloadingModels.size > 0 || downloadProcesses.length > 0
const steps = [ const steps = [
{ {
@ -553,6 +565,9 @@ function Hub() {
</p> </p>
{(() => { {(() => {
const isDownloading = const isDownloading =
localDownloadingModels.has(
variant.id
) ||
downloadProcesses.some( downloadProcesses.some(
(e) => e.id === variant.id (e) => e.id === variant.id
) )
@ -607,9 +622,12 @@ function Hub() {
<div <div
className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out" className="size-6 cursor-pointer flex items-center justify-center rounded hover:bg-main-view-fg/10 transition-all duration-200 ease-in-out"
title="Download model" title="Download model"
onClick={() => onClick={() => {
addLocalDownloadingModel(
variant.id
)
downloadModel(variant.id) downloadModel(variant.id)
} }}
> >
<IconDownload <IconDownload
size={16} size={16}