/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config */ // The quantization page, ported from core/http/react-ui/src/pages/Quantize.jsx // with the port table: React, the translator, the shared components, the // session-bound fetch and the viewer come from ctx; the job views are the // shared ones of addons/internal/tools-ui. Class names, test ids and keys are // the core page's. import { percentOf, ACTIVE } from './lib/progress.js' const QUANT_PRESETS = [ 'q2_k', 'q3_k_s', 'q3_k_m', 'q3_k_l', 'q4_0', 'q4_k_s', 'q4_k_m', 'q5_0', 'q5_k_s', 'q5_k_m', 'q6_k', 'q8_0', 'f16', ] const DEFAULT_QUANT = 'q4_k_m' const FALLBACK_BACKEND = 'llama-cpp-quantization' const BADGE = { queued: '', downloading: 'dk-badge--warn', converting: 'dk-badge--warn', quantizing: 'dk-badge--accent', completed: 'dk-badge--ok', failed: 'dk-badge--error', stopped: '', } const startedAt = (job) => { const d = new Date(job.created_at) return Number.isNaN(d.getTime()) ? (job.created_at || '') : d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } // `shared` is the shared job views: { tools, readSse, ToolSteps, Checks, StageLine, // JobLog, ToolOff }. Returns the page and, for tests, the parts it is made of. export function createQuantize(ctx, api, shared) { const { h, Fragment, t, Icon, ui, viewer } = ctx const { useState, useEffect, useRef, useCallback, useMemo } = ctx.React const { QZ_STAGES, TERMINAL, appendLog, blocked, logLines, machineFacts, quantizeChecks, warned } = shared.tools const { ToolSteps, Checks, StageLine, JobLog, ToolOff, readSse } = shared function StatusBadge({ status }) { return {ctx.tOr(`quantize.status.${status}`, String(status || ''))} } function Field({ id, label, hint, className = '', children }) { return (
{children} {hint &&

{hint}

}
) } function Section({ done, title, children }) { return (

{title}

{children}
) } // The foot of the delete layer. The host draws the dialog around it. function DeleteDialogBody({ onConfirm, close }) { return (
) } // What to do with a finished quantization: import it as a model, then use it. function ResultPanel({ job, onRefresh, modelExports = true }) { const [modelName, setModelName] = useState('') const [importing, setImporting] = useState(false) const [error, setError] = useState('') useEffect(() => { if (job?.import_status !== 'importing') return undefined const timer = setInterval(() => { onRefresh().catch(() => {}) }, 3000) return () => clearInterval(timer) }, [job?.import_status, onRefresh]) if (!job || job.status !== 'completed') return null const handleImport = async () => { setImporting(true) setError('') try { await api.quantize.importModel(job.id, { name: modelName || undefined }) await onRefresh() } catch (e) { setError(e.message || t('quantize.importFailed')) } finally { setImporting(false) } } const imported = job.import_status === 'completed' && job.import_model_name const download = api.downloadHref(job.id) return ( <> {imported && (

{t('quantize.nextTitle', { name: job.import_model_name })}

{t('quantize.nextText')}

{t('quantize.chat', { name: job.import_model_name })} {t('quantize.models')} {t('quantize.download')}
)}

{t('quantize.outputTitle')}

{t('quantize.outputText', { type: job.quantization_type })}

{job.output_file &&

{t('quantize.file')} {job.output_file}

} {error &&

{error}

} {job.import_status === 'failed' &&

{t('quantize.importFailedWith', { message: job.import_message })}

} {!imported && (
{job.import_status === 'importing' ? (

{t('quantize.importing')} {job.import_message}

) : ( <> setModelName(e.target.value)} />
{t('quantize.download')}
{!modelExports &&

{t('quantize.importBlocked')}

} )}
)}
) } // A working, failed or finished job: progress, the log and what to do with // the result. The server streams progress events while the job is active; a // finished job is read from the job record. `seed` starts the view with an // event already read; tests use it to draw a state the stream would reach. function JobView({ job, onStop, onReuse, onTerminal, onRefresh, seed, modelExports = true }) { const [latest, setLatest] = useState(seed?.latest ?? null) const [log, setLog] = useState([]) const previousRef = useRef(null) const terminalRef = useRef(onTerminal) terminalRef.current = onTerminal useEffect(() => { previousRef.current = null setLatest(null) setLog([]) if (!job || TERMINAL.includes(job.status)) return undefined // The progress stream is a fetch read with readSse: it carries the // session and the base path of sdk.core.fetch. Unmounting aborts it. const controller = new AbortController() const onEvent = (data) => { setLatest(data) setLog(prev => appendLog(prev, logLines(data, previousRef.current))) previousRef.current = data if (TERMINAL.includes(data.status)) { controller.abort(); terminalRef.current?.() } } ;(async () => { try { const response = await api.quantize.progress(job.id, controller.signal) if (response.ok) await readSse(response, onEvent, controller.signal) } catch (_) { /* the stream ended; the job list poll shows the state */ } })() return () => controller.abort() // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the job id: the effect restarts per job, not per poll refresh }, [job?.id]) const status = latest?.status || job.status const active = ACTIVE.includes(status) const failed = status === 'failed' // A working job can read 0% before it has reported anything. That 0 is // unknown, not a measurement, so the page draws no figure for it. const progress = percentOf(latest ? { ...latest, status } : status === 'completed' ? { status } : null) const known = progress != null const message = latest?.message || job.message || '' const stageLabels = Object.fromEntries(QZ_STAGES.map(s => [s, t(`quantize.status.${s}`)])) return (
{failed && (

{t('quantize.failed')}

{message || t('fineTune.noMessage')}

)}

{job.model}

{t('quantize.jobMeta', { type: job.quantization_type, backend: job.backend, started: startedAt(job) })} ยท {job.id?.slice(0, 8)}

{active && ( )}
{(latest || status === 'completed') && (
{known ? {progress.toFixed(0)}% : {t('run.unknownPercent')}}
)} {(active || latest) && (
)} {active && } {!failed && message &&

{message}

} {status === 'completed' &&

{t('quantize.finished', { type: job.quantization_type })}

} {status === 'stopped' &&

{t('quantize.stopped')}

}
{failed && (

{t('fineTune.tryTitle')}

{t('quantize.tryText')}

)} {(active || log.length > 0) && }
) } // Whether this instance may write an imported model. Only a clear false turns // the controls off: a failed or slow answer leaves them on, as the server does. function useModelExports() { const [allowed, setAllowed] = useState(true) useEffect(() => { let off = false api.quantize.status().then(r => { if (!off && r && r.model_exports === false) setAllowed(false) }).catch(() => {}) return () => { off = true } }, []) return allowed } // The machine facts for the checks: the one request the page makes, as a // poll that waits while the tab is hidden and catches up on return. function useResources(interval) { const [resources, setResources] = useState(null) useEffect(() => { let off = false const read = async () => { if (typeof document !== 'undefined' && document.hidden) return try { const data = await api.resources.get() if (!off) setResources(data) } catch (_) { /* the next tick tries again */ } } read() const timer = setInterval(read, interval) document.addEventListener('visibilitychange', read) return () => { off = true; clearInterval(timer); document.removeEventListener('visibilitychange', read) } }, [interval]) return resources } function QuantizeBody({ path }) { const isAdmin = viewer.isAdmin() const resources = useResources(10000) const modelExports = useModelExports() const facts = useMemo(() => machineFacts(resources), [resources]) // A deep link names the model: ?model= or the first path segment. const [model, setModel] = useState(() => ctx.modelFrom(path) || '') const [quantType, setQuantType] = useState(DEFAULT_QUANT) const [customQuantType, setCustomQuantType] = useState('') const [useCustomQuant, setUseCustomQuant] = useState(false) const [backend, setBackend] = useState(FALLBACK_BACKEND) const [hfToken, setHfToken] = useState('') const [backends, setBackends] = useState(null) const [showMore, setShowMore] = useState(false) const [jobs, setJobs] = useState([]) const [jobsLoaded, setJobsLoaded] = useState(false) const [selectedJob, setSelectedJob] = useState(null) const [error, setError] = useState('') const [submitting, setSubmitting] = useState(false) const loadJobs = useCallback(async () => { try { const data = await api.quantize.listJobs() const list = Array.isArray(data) ? data : [] setJobs(list) setSelectedJob(prev => { if (!prev) return prev const fresh = list.find(j => j.id === prev.id) return fresh || prev }) } catch (_) { /* the next poll tries again */ } finally { setJobsLoaded(true) } }, []) useEffect(() => { api.quantize.listBackends().then(b => { const list = Array.isArray(b) ? b : [] setBackends(list) if (list.length) setBackend(list[0].name) }).catch(() => setBackends(null)) loadJobs() const interval = setInterval(loadJobs, 10000) return () => clearInterval(interval) }, [loadJobs]) const handleSubmit = async (e) => { e.preventDefault() setError('') setSubmitting(true) try { const req = { model, backend: backend || FALLBACK_BACKEND, quantization_type: useCustomQuant ? customQuantType : quantType, } if (hfToken) req.extra_options = { hf_token: hfToken } const resp = await api.quantize.startJob(req) setModel('') await loadJobs() setSelectedJob(await api.quantize.getJob(resp.id)) } catch (err) { setError(err.message || t('quantize.startFailed')) } finally { setSubmitting(false) } } const handleStop = async (jobId) => { try { await api.quantize.stopJob(jobId) await loadJobs() } catch (err) { setError(err.message || t('quantize.stopFailed')) } } const confirmDelete = async (jobId) => { try { await api.quantize.deleteJob(jobId) if (selectedJob?.id === jobId) setSelectedJob(null) await loadJobs() } catch (err) { setError(err.message || t('quantize.deleteFailed')) } } const askDelete = (jobId) => ctx.openDialog({ title: t('jobs.deleteDialogTitle'), description: t('quantize.deleteDialogText'), render: ({ close }) => confirmDelete(jobId)} />, }) // Put a job's setup back in the form. const reuse = (job) => { setModel(job.model || '') if (job.backend) setBackend(job.backend) const type = job.quantization_type || DEFAULT_QUANT if (QUANT_PRESETS.includes(type)) { setUseCustomQuant(false); setQuantType(type) } else { setUseCustomQuant(true); setCustomQuantType(type) } setSelectedJob(null) } const effectiveQuantType = useCustomQuant ? customQuantType : quantType const backendNames = backends && backends.length > 0 ? backends.map(b => b.name) : [FALLBACK_BACKEND] const checks = useMemo(() => quantizeChecks({ form: { model, backend, useCustom: useCustomQuant, customType: customQuantType, hfToken: hfToken.trim() }, facts, backends: { quantize: backends }, }), [model, backend, useCustomQuant, customQuantType, hfToken, facts, backends]) const stop = blocked(checks) const warnings = warned(checks) const firstMissing = checks.find(c => c.tone === 'fail') const steps = [ { key: 'setup', label: t('steps.setup') }, { key: 'check', label: t('steps.check') }, { key: 'run', label: t('steps.run') }, { key: 'result', label: t('steps.result') }, ] let current = stop ? 0 : 1 let failed = false if (selectedJob) { if (selectedJob.status === 'failed') { current = 2; failed = true } else if (TERMINAL.includes(selectedJob.status)) current = 3 else current = 2 } const refreshSelected = useCallback(async () => { if (!selectedJob) return const updated = await api.quantize.getJob(selectedJob.id) setSelectedJob(updated) await loadJobs() // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the job id: the effect restarts per job, not per poll refresh }, [selectedJob?.id, loadJobs]) return (
{t('landing.experimental')} {t('quantize.lede')}} actions={selectedJob && ( )} /> {error && (

{error}

)} {selectedJob ? ( ) : (
setModel(e.target.value)} required />
{useCustomQuant && ( setCustomQuantType(e.target.value)} required /> )}
{showMore && (
setHfToken(e.target.value)} autoComplete="off" />
)}

{model.trim() ? t('quantize.recipe', { model: model.split('/').pop(), type: effectiveQuantType || '?' }) : t('quantize.recipeEmpty')}

{t('checks.heading')}

{t('checks.live')}

{stop ? t(`checks.${firstMissing.key}`) : warnings > 0 ? ctx.plural('checks.barWarn', warnings) : t('checks.barOk')} {' '}{t('quantize.barNote')}

)}

{t('jobs.title')}

{jobs.length > 0 &&

{ctx.plural('jobs.count', jobs.length)}

}
{jobs.length === 0 ? (

{jobsLoaded ? t('quantize.emptyTitle') : t('jobs.loading')}

{jobsLoaded &&

{t('quantize.emptyText')}

}
) : (
{jobs.map(job => { const active = QZ_STAGES.includes(job.status) return ( ) })}
{t('jobs.title')}
{t('jobs.model')} {t('quantize.type')} {t('jobs.status')} {t('jobs.started')} {t('jobs.actions')}
{job.import_status === 'completed' && {t('quantize.imported', { name: job.import_model_name })}} {job.status === 'failed' && job.message && {job.message}} {job.quantization_type} {startedAt(job)}
{active ? ( ) : ( <> )}
)}
) } // The page. The off notice replaces it for an account without the feature, // so none of the page's requests run for that account. function Quantize({ path }) { return ( ) } return { Quantize, JobView, ResultPanel, DeleteDialogBody, StatusBadge } }