mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 19:49:25 +01:00
Model downloads that fail (e.g., when Ollama is too old for a model) were silently retrying 40 times with no UI feedback. Now errors are broadcast via SSE and shown in the Active Model Downloads section. Version mismatch errors use UnrecoverableError to fail immediately instead of retrying. Stale failed jobs are cleared on retry so users aren't permanently blocked. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { useTransmit } from 'react-adonis-transmit'
|
|
|
|
export type OllamaModelDownload = {
|
|
model: string
|
|
percent: number
|
|
timestamp: string
|
|
error?: string
|
|
}
|
|
|
|
export default function useOllamaModelDownloads() {
|
|
const { subscribe } = useTransmit()
|
|
const [downloads, setDownloads] = useState<Map<string, OllamaModelDownload>>(new Map())
|
|
const timeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set())
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = subscribe('ollama-model-download', (data: OllamaModelDownload) => {
|
|
setDownloads((prev) => {
|
|
const updated = new Map(prev)
|
|
|
|
if (data.percent === -1) {
|
|
// Download failed — show error state, auto-remove after 15 seconds
|
|
updated.set(data.model, data)
|
|
const errorTimeout = setTimeout(() => {
|
|
timeoutsRef.current.delete(errorTimeout)
|
|
setDownloads((current) => {
|
|
const next = new Map(current)
|
|
next.delete(data.model)
|
|
return next
|
|
})
|
|
}, 15000)
|
|
timeoutsRef.current.add(errorTimeout)
|
|
} else if (data.percent >= 100) {
|
|
// If download is complete, keep it for a short time before removing to allow UI to show 100% progress
|
|
updated.set(data.model, data)
|
|
const timeout = setTimeout(() => {
|
|
timeoutsRef.current.delete(timeout)
|
|
setDownloads((current) => {
|
|
const next = new Map(current)
|
|
next.delete(data.model)
|
|
return next
|
|
})
|
|
}, 2000)
|
|
timeoutsRef.current.add(timeout)
|
|
} else {
|
|
updated.set(data.model, data)
|
|
}
|
|
|
|
return updated
|
|
})
|
|
})
|
|
|
|
return () => {
|
|
unsubscribe()
|
|
timeoutsRef.current.forEach(clearTimeout)
|
|
timeoutsRef.current.clear()
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [subscribe])
|
|
|
|
const downloadsArray = Array.from(downloads.values())
|
|
|
|
return { downloads: downloadsArray, activeCount: downloads.size }
|
|
}
|