Merge pull request #5012 from menloresearch/chore/cleanup-download-management
chore: cleanup download management
This commit is contained in:
commit
3fedc9231c
@ -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">
|
||||
{Math.round(overallProgress * 100)}%
|
||||
</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 ({Math.round(download.progress * 100)}%)
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
import { useModelSources } from '@/hooks/useModelSources'
|
||||
import { cn, fuzzySearch, toGigabytes } from '@/lib/utils'
|
||||
import { useState, useMemo, useEffect, ChangeEvent } from 'react'
|
||||
import { useState, useMemo, useEffect, ChangeEvent, useCallback } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import { Card, CardItem } from '@/containers/Card'
|
||||
import { RenderMarkdown } from '@/containers/RenderMarkdown'
|
||||
import { extractModelName, extractDescription } from '@/lib/models'
|
||||
@ -16,6 +17,17 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { downloadModel } from '@/services/models'
|
||||
import { useDownloadStore } from '@/hooks/useDownloadStore'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
|
||||
type ModelProps = {
|
||||
model: {
|
||||
id: string
|
||||
models: {
|
||||
id: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.hub as any)({
|
||||
@ -77,6 +89,74 @@ 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]
|
||||
)
|
||||
|
||||
const { getProviderByName } = useModelProvider()
|
||||
const llamaProvider = getProviderByName('llama.cpp')
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleUseModel = useCallback(
|
||||
(modelId: string) => {
|
||||
navigate({
|
||||
to: route.home,
|
||||
params: {},
|
||||
search: {
|
||||
model: {
|
||||
id: modelId,
|
||||
provider: 'llama.cpp',
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[navigate]
|
||||
)
|
||||
|
||||
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
|
||||
const isDownloaded = llamaProvider?.models.some(
|
||||
(m: { id: string }) => m.id === modelId
|
||||
)
|
||||
|
||||
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>
|
||||
) : isDownloaded ? (
|
||||
<Button size="sm" onClick={() => handleUseModel(modelId)}>
|
||||
Use
|
||||
</Button>
|
||||
) : (
|
||||
<Button size="sm" onClick={() => downloadModel(modelId)}>
|
||||
Download
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}, [downloadProcesses, llamaProvider?.models, handleUseModel])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex flex-col h-full w-full">
|
||||
@ -134,131 +214,172 @@ 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
|
||||
const isDownloaded =
|
||||
llamaProvider?.models.some(
|
||||
(m: { id: string }) =>
|
||||
m.id === variant.id
|
||||
)
|
||||
|
||||
if (isDownloading) {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (isDownloaded) {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-center rounded bg-main-view-fg/10"
|
||||
title="Use this model"
|
||||
>
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
handleUseModel(variant.id)
|
||||
}
|
||||
>
|
||||
Use
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@ -1,20 +1,32 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { route } from '@/constants/routes'
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { createFileRoute, useSearch } from '@tanstack/react-router'
|
||||
import ChatInput from '@/containers/ChatInput'
|
||||
import HeaderPage from '@/containers/HeaderPage'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DropdownModelProvider from '@/containers/DropdownModelProvider'
|
||||
import { useModelProvider } from '@/hooks/useModelProvider'
|
||||
import SetupScreen from '@/containers/SetupScreen'
|
||||
import { route } from '@/constants/routes'
|
||||
|
||||
type SearchParams = {
|
||||
model?: {
|
||||
id: string
|
||||
provider: string
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute(route.home as any)({
|
||||
component: Index,
|
||||
validateSearch: (search: Record<string, unknown>): SearchParams => ({
|
||||
model: search.model as SearchParams['model'],
|
||||
}),
|
||||
})
|
||||
|
||||
function Index() {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useModelProvider()
|
||||
const search = useSearch({ from: route.home as any })
|
||||
const selectedModel = search.model
|
||||
|
||||
// Conditional to check if there are any valid providers
|
||||
// required min 1 api_key or 1 model in llama.cpp
|
||||
@ -31,7 +43,7 @@ function Index() {
|
||||
return (
|
||||
<div className="flex h-full flex-col flex-justify-center">
|
||||
<HeaderPage>
|
||||
<DropdownModelProvider />
|
||||
<DropdownModelProvider model={selectedModel} />
|
||||
</HeaderPage>
|
||||
<div className="h-full px-8 overflow-y-auto flex flex-col gap-2 justify-center">
|
||||
<div className="w-4/6 mx-auto">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user