Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/routers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def _dl(f=filename):

# Reserve 1-95 for file downloads, leave 95-100 for finalisation
pct = 1 + round((i + 1) / total * 94)
yield _fmt({"percent": pct, "file": filename})
yield _fmt({"percent": pct, "file": filename, "fileIndex": i + 1, "totalFiles": total})

yield _fmt({"percent": 100, "status": "done"})

Expand Down
4 changes: 2 additions & 2 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe

ipcMain.handle('model:download', async (event, { repoId, modelId }: { repoId: string; modelId: string }) => {
try {
await downloadModelFromHF(repoId, modelId, (pct) => {
event.sender.send('model:downloadProgress', { modelId, percent: pct })
await downloadModelFromHF(repoId, modelId, (progress) => {
event.sender.send('model:downloadProgress', { modelId, ...progress })
})
return { success: true }
} catch (err) {
Expand Down
17 changes: 15 additions & 2 deletions electron/main/model-downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
import { join } from 'path'

export type ProgressCallback = (percent: number) => void
export interface DownloadProgress {
percent: number
file?: string
fileIndex?: number
totalFiles?: number
status?: string
}
export type ProgressCallback = (progress: DownloadProgress) => void

const PYTHON_API_URL = process.env['PYTHON_API_URL'] ?? 'http://127.0.0.1:8765'

Expand Down Expand Up @@ -126,7 +133,13 @@ export async function downloadModelFromHF(
if (!line.startsWith('data: ')) continue
try {
const data = JSON.parse(line.slice(6))
if (typeof data.percent === 'number') onProgress(data.percent)
if (typeof data.percent === 'number') onProgress({
percent: data.percent,
file: data.file,
fileIndex: data.fileIndex,
totalFiles: data.totalFiles,
status: data.status,
})
if (data.error) throw new Error(`HF download error: ${data.error}`)
} catch (e) {
if (e instanceof Error && e.message.startsWith('HF download error:')) throw e
Expand Down
2 changes: 1 addition & 1 deletion electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ contextBridge.exposeInMainWorld('electron', {
isDownloaded: (modelId: string) => ipcRenderer.invoke('model:isDownloaded', modelId),
download: (repoId: string, modelId: string) => ipcRenderer.invoke('model:download', { repoId, modelId }),
delete: (modelId: string) => ipcRenderer.invoke('model:delete', modelId),
onProgress: (cb: (data: { modelId: string; percent: number }) => void) => {
onProgress: (cb: (data: { modelId: string; percent: number; file?: string; fileIndex?: number; totalFiles?: number; status?: string }) => void) => {
ipcRenderer.on('model:downloadProgress', (_event, data) => cb(data))
},
offProgress: () => ipcRenderer.removeAllListeners('model:downloadProgress')
Expand Down
18 changes: 4 additions & 14 deletions src/areas/models/ModelsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { ConfirmModal } from '@shared/components/ui'
import { LocalModel } from './models'
import { formatModelName } from './utils'
import { ModelCard } from './components/ModelCard'
import { DownloadingCard } from './components/DownloadingCard'
import { ExtensionCard, ExtensionVariant } from './components/ExtensionCard'

// ─── Page ─────────────────────────────────────────────────────────────────────
Expand All @@ -29,7 +28,7 @@ export default function ModelsPage(): JSX.Element {

// HF models state
const [models, setModels] = useState<LocalModel[]>([])
const [downloading, setDownloading] = useState<Record<string, number>>({})
const [downloading, setDownloading] = useState<Record<string, { percent: number; file?: string; fileIndex?: number; totalFiles?: number }>>({})
const [deleteTarget, setDeleteTarget] = useState<LocalModel | null>(null)
const [deleteError, setDeleteError] = useState<string | null>(null)
const [uninstallTarget, setUninstallTarget] = useState<string | null>(null)
Expand All @@ -49,8 +48,8 @@ export default function ModelsPage(): JSX.Element {
useEffect(() => {
refresh()
loadExtensions()
window.electron.model.onProgress(({ modelId: id, percent }) => {
setDownloading((prev) => ({ ...prev, [id]: percent }))
window.electron.model.onProgress(({ modelId: id, percent, file, fileIndex, totalFiles }) => {
setDownloading((prev) => ({ ...prev, [id]: { percent, file, fileIndex, totalFiles } }))
if (percent === 100) {
setDownloading((prev) => { const n = { ...prev }; delete n[id]; return n })
refresh()
Expand Down Expand Up @@ -270,7 +269,7 @@ export default function ModelsPage(): JSX.Element {
ext.models.map((v) => loadErrors[v.id]).find(Boolean)
}
onInstall={(variant: ExtensionVariant) => {
setDownloading((prev) => ({ ...prev, [variant.id]: 0 }))
setDownloading((prev) => ({ ...prev, [variant.id]: { percent: 0 } }))
window.electron.model.download(variant.repoId, variant.id).then((result) => {
if (!result.success) {
setDownloading((prev) => { const n = { ...prev }; delete n[variant.id]; return n })
Expand All @@ -284,15 +283,6 @@ export default function ModelsPage(): JSX.Element {
)}
</div>

{/* In-progress downloads */}
{inProgressIds.length > 0 && (
<div className="mb-5 flex flex-col gap-2">
<p className="text-[11px] font-semibold uppercase tracking-widest text-zinc-500 mb-1">Downloading</p>
{inProgressIds.map((id) => (
<DownloadingCard key={id} modelId={id} percent={downloading[id]} />
))}
</div>
)}

{/* Empty state */}
{models.length === 0 && inProgressIds.length === 0 && (
Expand Down
20 changes: 14 additions & 6 deletions src/areas/models/components/ExtensionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type { Extension, ExtensionVariant }
interface Props {
ext: Extension
installedIds: string[]
downloading: Record<string, number>
downloading: Record<string, { percent: number; file?: string; fileIndex?: number; totalFiles?: number }>
loadError?: string
disabled?: boolean
onInstall: (variant: ExtensionVariant) => void
Expand Down Expand Up @@ -104,8 +104,12 @@ export function ExtensionCard({ ext, installedIds, downloading, loadError, disab
<div className="flex flex-col gap-1.5 pt-1 border-t border-zinc-800/60">
{ext.models.map((variant) => {
const installed = installedIds.includes(variant.id)
const dlPercent = downloading[variant.id]
const isDownloading = dlPercent !== undefined
const dlInfo = downloading[variant.id]
const isDownloading = dlInfo !== undefined
const dlPercent = dlInfo?.percent ?? 0
const dlFile = dlInfo?.file?.split('/').pop()
const dlFileIndex = dlInfo?.fileIndex
const dlTotalFiles = dlInfo?.totalFiles

return (
<div key={variant.id} className="flex items-center gap-2">
Expand All @@ -124,10 +128,14 @@ export function ExtensionCard({ ext, installedIds, downloading, loadError, disab
<span className="text-[10px] font-semibold text-emerald-400">Ready</span>
</div>
) : isDownloading ? (
<div className="flex flex-col gap-0.5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<span className="text-[10px] text-zinc-500">Downloading…</span>
<span className="text-[10px] font-mono text-zinc-400">{dlPercent}%</span>
<span className="text-[10px] text-zinc-500 truncate max-w-[120px]" title={dlFile}>
Downloading… {dlFile ?? ''}
</span>
<span className="text-[10px] font-mono text-zinc-400 shrink-0 ml-1">
{dlFileIndex && dlTotalFiles ? `${dlFileIndex}/${dlTotalFiles} · ${dlPercent}%` : `${dlPercent}%`}
</span>
</div>
<div className="h-1 rounded-full bg-zinc-800 overflow-hidden">
<div
Expand Down
2 changes: 1 addition & 1 deletion src/shared/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ declare global {
isDownloaded: (modelId: string) => Promise<boolean>
download: (repoId: string, modelId: string) => Promise<{ success: boolean; error?: string }>
delete: (modelId: string) => Promise<{ success: boolean; error?: string }>
onProgress: (cb: (data: { modelId: string; percent: number }) => void) => void
onProgress: (cb: (data: { modelId: string; percent: number; file?: string; fileIndex?: number; totalFiles?: number; status?: string }) => void) => void
offProgress: () => void
}
app: {
Expand Down