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 { DownloadEvent, DownloadState, events } from '@janhq/core'
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() {
const { setProviders } = useModelProvider()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const { downloads, updateProgress, removeDownload } = useDownloadStore()
const downloadCount = useMemo(
() => Object.keys(downloads).length,
@ -22,7 +24,7 @@ export function DownloadManagement() {
const downloadProcesses = useMemo(
() =>
Object.values(downloads).map((download) => ({
id: download.id,
id: download.name,
name: download.name,
progress: download.progress,
current: download.current,
@ -76,6 +78,10 @@ export function DownloadManagement() {
console.debug('onFileDownloadSuccess', state)
removeDownload(state.modelId)
getProviders().then(setProviders)
toast.success('Download Complete', {
id: 'download-complete',
description: `The model ${state.modelId} has been downloaded`,
})
},
[removeDownload, setProviders]
)
@ -107,64 +113,77 @@ export function DownloadManagement() {
}
return (
<Popover>
<>
{downloadCount > 0 && (
<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-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
{downloadCount}
</div>
<p className="text-left-panel-fg/80 font-medium">Downloads</p>
<div className="mt-2 flex items-center justify-between space-x-2">
<Progress value={overallProgress * 100} />
<span className="text-xs font-medium text-main-view-fg/80 shrink-0">
{overallProgress.toFixed(2)}%
</span>
</div>
</div>
</PopoverTrigger>
)}
<PopoverContent
side="right"
align="end"
className="p-0 overflow-hidden text-sm select-none"
sideOffset={6}
>
<div className="flex flex-col">
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
<p className="text-xs text-main-view-fg/70">Downloading</p>
</div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{downloadProcesses.map((download) => (
<div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80">
{download.name}
</p>
<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
size={16}
className="text-main-view-fg/70 cursor-pointer"
title="Cancel download"
onClick={() => abortDownload(download.name)}
/>
</div>
</div>
<Progress value={download.progress * 100} className="my-2" />
<p className="text-main-view-fg/60 text-xs">
{`${renderGB(download.current)} / ${renderGB(download.total)}`}{' '}
GB ({download.progress.toFixed(2)}%)
</p>
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<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-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
{downloadCount}
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
<p className="text-left-panel-fg/80 font-medium">Downloads</p>
<div className="mt-2 flex items-center justify-between space-x-2">
<Progress value={overallProgress * 100} />
<span className="text-xs font-medium text-main-view-fg/80 shrink-0">
{overallProgress.toFixed(2)}%
</span>
</div>
</div>
</PopoverTrigger>
<PopoverContent
side="right"
align="end"
className="p-0 overflow-hidden text-sm select-none"
sideOffset={6}
onFocusOutside={(e) => e.preventDefault}
>
<div className="flex flex-col">
<div className="p-2 py-1.5 bg-main-view-fg/5 border-b border-main-view-fg/6">
<p className="text-xs text-main-view-fg/70">Downloading</p>
</div>
<div className="p-2 max-h-[300px] overflow-y-auto space-y-2">
{downloadProcesses.map((download) => (
<div className="bg-main-view-fg/4 rounded-md p-2">
<div className="flex items-center justify-between">
<p className="truncate text-main-view-fg/80">
{download.name}
</p>
<div className="shrink-0 flex items-center space-x-0.5">
<IconX
size={16}
className="text-main-view-fg/70 cursor-pointer"
title="Cancel download"
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>
<Progress
value={download.progress * 100}
className="my-2"
/>
<p className="text-main-view-fg/60 text-xs">
{`${renderGB(download.current)} / ${renderGB(download.total)}`}{' '}
GB ({download.progress.toFixed(2)}%)
</p>
</div>
))}
</div>
</div>
</PopoverContent>
</Popover>
)}
</>
)
}

View File

@ -16,6 +16,8 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
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
export const Route = createFileRoute(route.hub as any)({
@ -77,6 +79,53 @@ function Hub() {
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 (
<div className="flex h-full w-full">
<div className="flex flex-col h-full w-full">
@ -134,131 +183,142 @@ function Hub() {
</div>
) : (
<div className="flex flex-col pb-2 mb-2 gap-2">
{filteredModels.map((model) => {
return (
<div key={model.id}>
<Card
header={
<div className="flex items-center justify-between gap-x-2">
<Link
to={
`https://huggingface.co/${model.id}` as string
}
target="_blank"
>
<h1 className="text-main-view-fg font-medium text-base capitalize truncate">
{extractModelName(model.id) || ''}
</h1>
</Link>
<div className="shrink-0 space-x-3">
<span className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(model.models?.[0]?.size)}
</span>
<Button
onClick={() =>
downloadModel(model.models[0]?.id)
{filteredModels.map((model) => (
<div key={model.id}>
<Card
header={
<div className="flex items-center justify-between gap-x-2">
<Link
to={`https://huggingface.co/${model.id}` as string}
target="_blank"
>
<h1 className="text-main-view-fg font-medium text-base capitalize truncate">
{extractModelName(model.id) || ''}
</h1>
</Link>
<div className="shrink-0 space-x-3 flex items-center">
<span className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(model.models?.[0]?.size)}
</span>
<DownloadButtonPlaceholder model={model} />
</div>
</div>
}
>
<div className="line-clamp-2 mt-3 text-main-view-fg/60">
<RenderMarkdown
components={{
a: ({ ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
content={
extractDescription(model.metadata.description) || ''
}
/>
</div>
<div className="flex items-center gap-2 mt-2">
<span className="capitalize text-main-view-fg/80">
By {model?.author}
</span>
<div className="flex items-center gap-4 ml-2">
<div className="flex items-center gap-1">
<IconDownload
size={18}
className="text-main-view-fg/50"
title="Downloads"
/>
<span className="text-main-view-fg/80">
{model.metadata?.downloads || 0}
</span>
</div>
<div className="flex items-center gap-1">
<IconFileCode
size={20}
className="text-main-view-fg/50"
title="Variants"
/>
<span className="text-main-view-fg/80">
{model.models?.length || 0}
</span>
</div>
{model.models.length > 1 && (
<div className="flex items-center gap-2">
<Switch
checked={!!expandedModels[model.id]}
onCheckedChange={() =>
toggleModelExpansion(model.id)
}
>
Download
</Button>
</div>
</div>
}
>
<div className="line-clamp-2 mt-3 text-main-view-fg/60">
<RenderMarkdown
components={{
a: ({ ...props }) => (
<a
{...props}
target="_blank"
rel="noopener noreferrer"
/>
),
}}
content={
extractDescription(model.metadata.description) ||
''
}
/>
</div>
<div className="flex items-center gap-2 mt-2">
<span className="capitalize text-main-view-fg/80">
By {model?.author}
</span>
<div className="flex items-center gap-4 ml-2">
<div className="flex items-center gap-1">
<IconDownload
size={18}
className="text-main-view-fg/50"
title="Downloads"
/>
<span className="text-main-view-fg/80">
{model.metadata?.downloads || 0}
</span>
</div>
<div className="flex items-center gap-1">
<IconFileCode
size={20}
className="text-main-view-fg/50"
title="Variants"
/>
<span className="text-main-view-fg/80">
{model.models?.length || 0}
</span>
</div>
{model.models.length > 1 && (
<div className="flex items-center gap-2">
<Switch
checked={!!expandedModels[model.id]}
onCheckedChange={() =>
toggleModelExpansion(model.id)
}
/>
<p className="text-main-view-fg/70">
Show variants
</p>
</div>
)}
</div>
</div>
{expandedModels[model.id] &&
model.models.length > 0 && (
<div className="mt-5">
{model.models.map((variant) => {
return (
<CardItem
key={variant.id}
title={variant.id}
actions={
<div className="flex items-center gap-2">
{/* {defaultVariant && <>test</>} */}
<p className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(variant.size)}
</p>
<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"
title="Edit All Servers JSON"
onClick={() =>
downloadModel(variant.id)
}
>
<IconDownload
size={16}
className="text-main-view-fg/80"
/>
</div>
</div>
}
/>
)
})}
<p className="text-main-view-fg/70">
Show variants
</p>
</div>
)}
</Card>
</div>
)
})}
</div>
</div>
{expandedModels[model.id] && model.models.length > 0 && (
<div className="mt-5">
{model.models.slice(1).map((variant) => (
<CardItem
key={variant.id}
title={variant.id}
actions={
<div className="flex items-center gap-2">
{/* {defaultVariant && <>test</>} */}
<p className="text-main-view-fg/70 font-medium text-xs">
{toGigabytes(variant.size)}
</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
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"
onClick={() =>
downloadModel(variant.id)
}
>
<IconDownload
size={16}
className="text-main-view-fg/80"
/>
</div>
)
})()}
</div>
}
/>
))}
</div>
)}
</Card>
</div>
))}
</div>
)}
</div>