chore: update hub progress download, added toaster

This commit is contained in:
Faisal Amir 2025-05-19 11:15:37 +07:00
parent 2345ff172d
commit 53f5729674
2 changed files with 259 additions and 180 deletions

View File

@ -10,10 +10,12 @@ import { abortDownload } from '@/services/models'
import { getProviders } from '@/services/providers' import { getProviders } from '@/services/providers'
import { DownloadEvent, DownloadState, events } from '@janhq/core' import { DownloadEvent, DownloadState, events } from '@janhq/core'
import { IconX } from '@tabler/icons-react' import { IconX } from '@tabler/icons-react'
import { useCallback, useEffect, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
export function DownloadManagement() { export function DownloadManagement() {
const { setProviders } = useModelProvider() const { setProviders } = useModelProvider()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { downloads, updateProgress, removeDownload } = useDownloadStore() const { downloads, updateProgress, removeDownload } = useDownloadStore()
const downloadCount = useMemo( const downloadCount = useMemo(
() => Object.keys(downloads).length, () => Object.keys(downloads).length,
@ -22,7 +24,7 @@ export function DownloadManagement() {
const downloadProcesses = useMemo( const downloadProcesses = useMemo(
() => () =>
Object.values(downloads).map((download) => ({ Object.values(downloads).map((download) => ({
id: download.id, id: download.name,
name: download.name, name: download.name,
progress: download.progress, progress: download.progress,
current: download.current, current: download.current,
@ -76,6 +78,10 @@ export function DownloadManagement() {
console.debug('onFileDownloadSuccess', state) console.debug('onFileDownloadSuccess', state)
removeDownload(state.modelId) removeDownload(state.modelId)
getProviders().then(setProviders) getProviders().then(setProviders)
toast.success('Download Complete', {
id: 'download-complete',
description: `The model ${state.modelId} has been downloaded`,
})
}, },
[removeDownload, setProviders] [removeDownload, setProviders]
) )
@ -107,8 +113,9 @@ export function DownloadManagement() {
} }
return ( return (
<Popover> <>
{downloadCount > 0 && ( {downloadCount > 0 && (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger> <PopoverTrigger>
<div className="bg-left-panel-fg/10 hover:bg-left-panel-fg/12 p-2 rounded-md my-1 relative border border-left-panel-fg/10 cursor-pointer text-left"> <div className="bg-left-panel-fg/10 hover:bg-left-panel-fg/12 p-2 rounded-md my-1 relative border border-left-panel-fg/10 cursor-pointer text-left">
<div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg"> <div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
@ -123,12 +130,13 @@ export function DownloadManagement() {
</div> </div>
</div> </div>
</PopoverTrigger> </PopoverTrigger>
)}
<PopoverContent <PopoverContent
side="right" side="right"
align="end" align="end"
className="p-0 overflow-hidden text-sm select-none" className="p-0 overflow-hidden text-sm select-none"
sideOffset={6} sideOffset={6}
onFocusOutside={(e) => e.preventDefault}
> >
<div className="flex flex-col"> <div className="flex flex-col">
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6"> <div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
@ -142,20 +150,29 @@ export function DownloadManagement() {
{download.name} {download.name}
</p> </p>
<div className="shrink-0 flex items-center space-x-0.5"> <div className="shrink-0 flex items-center space-x-0.5">
{/* <IconPlayerPauseFilled
size={16}
className="text-main-view-fg/70 cursor-pointer"
title="Pause download"
/> */}
<IconX <IconX
size={16} size={16}
className="text-main-view-fg/70 cursor-pointer" className="text-main-view-fg/70 cursor-pointer"
title="Cancel download" title="Cancel download"
onClick={() => abortDownload(download.name)} onClick={() => {
abortDownload(download.name).then(() => {
toast.info('Download Cancelled', {
id: 'cancel-download',
description:
'The download process was cancelled',
})
if (downloadProcesses.length === 0) {
setIsPopoverOpen(false)
}
})
}}
/> />
</div> </div>
</div> </div>
<Progress value={download.progress * 100} className="my-2" /> <Progress
value={download.progress * 100}
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)}`}{' '} {`${renderGB(download.current)} / ${renderGB(download.total)}`}{' '}
GB ({download.progress.toFixed(2)}%) GB ({download.progress.toFixed(2)}%)
@ -166,5 +183,7 @@ export function DownloadManagement() {
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)}
</>
) )
} }

View File

@ -16,6 +16,8 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { downloadModel } from '@/services/models' import { downloadModel } from '@/services/models'
import { useDownloadStore } from '@/hooks/useDownloadStore'
import { Progress } from '@/components/ui/progress'
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Route = createFileRoute(route.hub as any)({ export const Route = createFileRoute(route.hub as any)({
@ -77,6 +79,53 @@ function Hub() {
setSearchValue(e.target.value) setSearchValue(e.target.value)
} }
const { downloads } = useDownloadStore()
const downloadProcesses = useMemo(
() =>
Object.values(downloads).map((download) => ({
id: download.name,
name: download.name,
progress: download.progress,
current: download.current,
total: download.total,
})),
[downloads]
)
interface ModelProps {
model: {
id: string
models: {
id: string
}[]
}
}
const DownloadButtonPlaceholder = useMemo(() => {
return ({ model }: ModelProps) => {
const modelId = model.models[0]?.id
const isDownloading = downloadProcesses.some((e) => e.id === modelId)
const downloadProgress =
downloadProcesses.find((e) => e.id === modelId)?.progress || 0
return (
<>
{isDownloading ? (
<div className="flex items-center gap-2 w-20">
<Progress value={downloadProgress * 100} />
<span className="text-xs text-center text-main-view-fg/70">
{Math.round(downloadProgress * 100)}%
</span>
</div>
) : (
<Button onClick={() => downloadModel(modelId)}>Download</Button>
)}
</>
)
}
}, [downloadProcesses])
return ( return (
<div className="flex h-full w-full"> <div className="flex h-full w-full">
<div className="flex flex-col h-full w-full"> <div className="flex flex-col h-full w-full">
@ -134,33 +183,24 @@ function Hub() {
</div> </div>
) : ( ) : (
<div className="flex flex-col pb-2 mb-2 gap-2"> <div className="flex flex-col pb-2 mb-2 gap-2">
{filteredModels.map((model) => { {filteredModels.map((model) => (
return (
<div key={model.id}> <div key={model.id}>
<Card <Card
header={ header={
<div className="flex items-center justify-between gap-x-2"> <div className="flex items-center justify-between gap-x-2">
<Link <Link
to={ to={`https://huggingface.co/${model.id}` as string}
`https://huggingface.co/${model.id}` as string
}
target="_blank" target="_blank"
> >
<h1 className="text-main-view-fg font-medium text-base capitalize truncate"> <h1 className="text-main-view-fg font-medium text-base capitalize truncate">
{extractModelName(model.id) || ''} {extractModelName(model.id) || ''}
</h1> </h1>
</Link> </Link>
<div className="shrink-0 space-x-3"> <div className="shrink-0 space-x-3 flex items-center">
<span className="text-main-view-fg/70 font-medium text-xs"> <span className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(model.models?.[0]?.size)} {toGigabytes(model.models?.[0]?.size)}
</span> </span>
<Button <DownloadButtonPlaceholder model={model} />
onClick={() =>
downloadModel(model.models[0]?.id)
}
>
Download
</Button>
</div> </div>
</div> </div>
} }
@ -177,8 +217,7 @@ function Hub() {
), ),
}} }}
content={ content={
extractDescription(model.metadata.description) || extractDescription(model.metadata.description) || ''
''
} }
/> />
</div> </div>
@ -222,11 +261,9 @@ function Hub() {
)} )}
</div> </div>
</div> </div>
{expandedModels[model.id] && {expandedModels[model.id] && model.models.length > 0 && (
model.models.length > 0 && (
<div className="mt-5"> <div className="mt-5">
{model.models.map((variant) => { {model.models.slice(1).map((variant) => (
return (
<CardItem <CardItem
key={variant.id} key={variant.id}
title={variant.id} title={variant.id}
@ -236,9 +273,32 @@ function Hub() {
<p className="text-main-view-fg/70 font-medium text-xs"> <p className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(variant.size)} {toGigabytes(variant.size)}
</p> </p>
{(() => {
const isDownloading =
downloadProcesses.some(
(e) => e.id === variant.id
)
const downloadProgress =
downloadProcesses.find(
(e) => e.id === variant.id
)?.progress || 0
return isDownloading ? (
<>
<div className="flex items-center gap-2 w-20">
<Progress
value={downloadProgress * 100}
/>
<span className="text-xs text-center text-main-view-fg/70">
{Math.round(downloadProgress * 100)}
%
</span>
</div>
</>
) : (
<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="Edit All Servers JSON" title="Download model"
onClick={() => onClick={() =>
downloadModel(variant.id) downloadModel(variant.id)
} }
@ -248,17 +308,17 @@ function Hub() {
className="text-main-view-fg/80" className="text-main-view-fg/80"
/> />
</div> </div>
)
})()}
</div> </div>
} }
/> />
) ))}
})}
</div> </div>
)} )}
</Card> </Card>
</div> </div>
) ))}
})}
</div> </div>
)} )}
</div> </div>