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,64 +113,77 @@ export function DownloadManagement() {
} }
return ( return (
<Popover> <>
{downloadCount > 0 && ( {downloadCount > 0 && (
<PopoverTrigger> <Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<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"> <PopoverTrigger>
<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-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">
{downloadCount} <div className="bg-primary font-bold size-5 rounded-full absolute -top-2 -right-1 flex items-center justify-center text-primary-fg">
</div> {downloadCount}
<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>
</div> </div>
))} <p className="text-left-panel-fg/80 font-medium">Downloads</p>
</div> <div className="mt-2 flex items-center justify-between space-x-2">
</div> <Progress value={overallProgress * 100} />
</PopoverContent> <span className="text-xs font-medium text-main-view-fg/80 shrink-0">
</Popover> {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, 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,131 +183,142 @@ 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={`https://huggingface.co/${model.id}` as string}
to={ target="_blank"
`https://huggingface.co/${model.id}` as string >
} <h1 className="text-main-view-fg font-medium text-base capitalize truncate">
target="_blank" {extractModelName(model.id) || ''}
> </h1>
<h1 className="text-main-view-fg font-medium text-base capitalize truncate"> </Link>
{extractModelName(model.id) || ''} <div className="shrink-0 space-x-3 flex items-center">
</h1> <span className="text-main-view-fg/70 font-medium text-xs">
</Link> {toGigabytes(model.models?.[0]?.size)}
<div className="shrink-0 space-x-3"> </span>
<span className="text-main-view-fg/70 font-medium text-xs"> <DownloadButtonPlaceholder model={model} />
{toGigabytes(model.models?.[0]?.size)} </div>
</span> </div>
<Button }
onClick={() => >
downloadModel(model.models[0]?.id) <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"> <p className="text-main-view-fg/70">
{model.metadata?.downloads || 0} Show variants
</span> </p>
</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>
}
/>
)
})}
</div> </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>
)} )}
</div> </div>