v0.6.5 : telechargement de modeles bien plus rapide, barre de progression et annulation

- Telechargement d'un .gguf en 8 connexions paralleles (requetes Range,
  ecriture en place avec WriteAt) au lieu d'un seul flux TCP : sur le CDN
  Hugging Face le gain est typiquement de 3 a 6x. Repli automatique sur le
  flux unique si la source ne gere pas les ranges.
- Client HTTP dedie (pool de connexions reutilisees, HTTP/2 desactive) et
  reprise par tranche en cas de coupure. Reglable via JEAN_DL_CONNS.
- Barre de progression integree dans la modale de preset, avec vitesse et
  temps restant.
- Bouton d'annulation : coupe vraiment le telechargement et supprime le
  fichier partiel. Fermer la modale ne l'interrompt plus, l'UI se rebranche
  dessus a la reouverture.
- Les .gguf.part orphelins (crash, restart du service) sont nettoyes au
  demarrage.
- La liste de selection du modele s'ouvre alignee a gauche et sur toute la
  largeur de la ligne.
This commit is contained in:
nathaninline committed 2026-07-30 15:06:01 +02:00
1 parent 1360eb1943
commit 065264a055
11 files changed
+722 -97

No files matched your search

+4 -2
View File
@@ -395,9 +395,9 @@
<div class="pe-group">
<div class="pe-gh">Modèle</div>
<div class="pe-list">
<div class="pe-row">
<div class="pe-row stack">
<span class="pe-row-l">Fichier</span>
<span class="pe-row-c"><span class="pe-selc"><select id="m-model" onchange="onPickModel()"></select></span></span>
<span class="pe-selc wide"><select id="m-model" onchange="onPickModel()"></select></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Quantization<span class="pe-sub">auto si vide</span></span>
@@ -409,7 +409,9 @@
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">↓</button>
</div>
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
<button id="m-hf-cancel" class="pe-link" onclick="cancelDownload()" style="display:none">Annuler le téléchargement</button>
</div>
</div>
</div>
+98 -22
View File
@@ -78,7 +78,8 @@ async function openItem(kind, key){
modelRow.style.display = 'flex';
settingsRow.style.display = 'flex';
document.getElementById('m-hf-url').value = '';
document.getElementById('m-hf-progress').style.display = 'none';
resetDlUI();
attachDownload(); // téléchargement encore en cours côté serveur ?
document.getElementById('m-del-model').checked = false;
document.getElementById('m-quant').value = currentQuantInTextarea();
populateSettings();
@@ -298,7 +299,9 @@ function toggleRaw(){
}
const openPreset = (id)=>openItem('preset', id);
const openMem = (n)=>openItem('mem', n);
function closeModal(){ document.getElementById('modal').style.display = 'none'; }
// Fermer la modale n'annule PAS un téléchargement en cours (il vit côté
// serveur) : on arrête juste de l'interroger.
function closeModal(){ document.getElementById('modal').style.display = 'none'; stopDlPoll(); }
async function saveItem(){
const K = KINDS[editingKind];
const name = document.getElementById('m-name').value.trim();
@@ -333,39 +336,112 @@ async function delItem(){
} else { toast('supprimé'); }
closeModal(); K.reload();
}
// Download a .gguf from Hugging Face, polling progress until done.
let dlPoll = null;
// Download a .gguf from Hugging Face. Le téléchargement vit côté serveur : fermer
// la modale ne l'interrompt pas, on se rebranche dessus à la réouverture
// (attachDownload) et seul « Annuler » l'arrête vraiment.
let dlPoll = null, dlName = '';
function dlEls(){
return {
btn: document.getElementById('m-hf-btn'),
prog: document.getElementById('m-hf-progress'),
bar: document.getElementById('m-hf-bar'),
cancel: document.getElementById('m-hf-cancel'),
};
}
// Arrête le poll local sans toucher au téléchargement serveur.
function stopDlPoll(){ if(dlPoll){ clearInterval(dlPoll); dlPoll=null; } }
// Remet la zone de téléchargement à zéro (réouverture de la modale).
function resetDlUI(){
const e = dlEls();
stopDlPoll(); dlName='';
e.prog.style.display='none'; e.bar.style.display='none';
e.cancel.style.display='none'; e.btn.disabled=false;
}
async function startDownload(){
const url = document.getElementById('m-hf-url').value.trim();
if(!url){ toast('colle un lien .gguf'); return; }
const btn = document.getElementById('m-hf-btn');
const prog = document.getElementById('m-hf-progress');
btn.disabled = true;
prog.style.display = 'block';
prog.textContent = 'démarrage…';
const e = dlEls();
e.btn.disabled = true;
e.prog.style.display = 'block';
e.prog.textContent = 'démarrage…';
e.bar.style.display = 'block';
e.bar.className = 'pe-bar indet';
e.bar.firstElementChild.style.width = '';
const r = await jpost('/api/models/download', {url});
if(!r.ok){ prog.innerHTML = '<span style="color:var(--err)">erreur : '+(r.error||'')+'</span>'; btn.disabled=false; return; }
const fname = r.filename;
if(dlPoll) clearInterval(dlPoll);
dlPoll = setInterval(async ()=>{
if(!r.ok){
e.prog.innerHTML = '<span style="color:var(--err)">erreur : '+(r.error||'')+'</span>';
e.bar.style.display = 'none'; e.btn.disabled=false; return;
}
watchDownload(r.filename);
}
// Annule le téléchargement en cours : le serveur coupe les connexions et
// supprime le fichier partiel.
async function cancelDownload(){
if(!dlName) return;
const e = dlEls();
e.cancel.disabled = true;
await jpost('/api/models/download/cancel', {filename: dlName});
// L'état « annulé » est confirmé par le prochain tick de poll.
}
// Suit un téléchargement (nouveau ou déjà en cours) et pilote barre + texte.
function watchDownload(fname){
const e = dlEls();
dlName = fname;
stopDlPoll();
e.btn.disabled = true;
e.prog.style.display = 'block';
e.bar.style.display = 'block';
e.cancel.style.display = 'inline';
e.cancel.disabled = false;
const stop = ()=>{ stopDlPoll(); dlName=''; e.btn.disabled=false; e.cancel.style.display='none'; };
const tick = async ()=>{
const list = await jget('/api/models/download/status');
const st = (list||[]).find(d=>d.filename===fname);
if(!st){ return; }
if(!st) return;
if(st.canceled){
e.prog.textContent = 'téléchargement annulé — '+fname;
e.bar.style.display = 'none'; stop(); return;
}
if(st.error){
prog.innerHTML = '<span style="color:var(--err)">erreur : '+st.error+'</span>';
clearInterval(dlPoll); dlPoll=null; btn.disabled=false; return;
e.prog.innerHTML = '<span style="color:var(--err)">erreur : '+st.error+'</span>';
e.bar.style.display = 'none'; stop(); return;
}
if(st.finished){
prog.innerHTML = '<span style="color:var(--ok)">✓ '+fname+' téléchargé ('+fmtSize(st.done)+')</span>';
clearInterval(dlPoll); dlPoll=null; btn.disabled=false;
e.prog.innerHTML = '<span style="color:var(--ok)">✓ '+fname+' téléchargé ('+fmtSize(st.done)+')</span>';
e.bar.className = 'pe-bar done'; e.bar.firstElementChild.style.width = '100%';
stop();
await populateModelPicker();
document.getElementById('m-model').value = fname; onPickModel();
return;
}
const pct = st.total>0 ? Math.round(st.done*100/st.total) : 0;
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+pct+'%)' : '';
prog.textContent = '↓ '+fmtSize(st.done)+tot+' — '+fname;
}, 800);
const pct = st.total>0 ? st.done*100/st.total : 0;
if(st.total>0){
e.bar.className = 'pe-bar';
e.bar.firstElementChild.style.width = Math.min(100, pct).toFixed(1)+'%';
} else {
e.bar.className = 'pe-bar indet';
}
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+Math.round(pct)+'%)' : '';
let extra = '';
if(st.speed>0){
extra += ' — '+fmtSize(st.speed)+'/s';
if(st.total>0){
const eta = Math.max(0, Math.round((st.total-st.done)/st.speed));
const m = Math.floor(eta/60), s = eta%60;
extra += ' — reste '+(m>0 ? m+' min '+s+' s' : s+' s');
}
}
e.prog.textContent = '↓ '+fmtSize(st.done)+tot+extra+' — '+fname;
};
dlPoll = setInterval(tick, 800);
tick();
}
// À l'ouverture de la modale : se rebrancher sur un téléchargement encore en
// cours côté serveur (modale fermée entre-temps, page rechargée, autre appareil).
async function attachDownload(){
const list = await jget('/api/models/download/status');
const st = (list||[]).find(d=>!d.finished);
if(st) watchDownload(st.filename);
}
// Smart autoscroll: follow the bottom while the user hasn't manually scrolled
// up. Re-stick when they scroll back near bottom themselves.
+17
View File
@@ -534,6 +534,12 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
.pe-selc select{appearance:none;-webkit-appearance:none;border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;text-align-last:right;padding:4px 16px 4px 0;cursor:pointer;max-width:100%;text-overflow:ellipsis}
.pe-selc select:focus{outline:none}
.pe-selc::after{content:"›";position:absolute;right:0;transform:rotate(90deg);color:var(--dim);font-size:14px;pointer-events:none}
/* Variante pleine largeur : la liste native s'ouvre alignée à gauche de la
ligne (et non collée au bord droit) et le nom de fichier a la place de
s'afficher en entier. */
.pe-selc.wide{width:100%}
.pe-selc.wide select{flex:1;text-align:left;text-align-last:left;padding-right:18px}
.pe-selc.wide::after{right:2px}
/* Ligne « champ pleine largeur » (Nom, chemin, config brute…). */
.pe-row.stack{flex-direction:column;align-items:stretch;gap:8px}
.pe-inp{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:9px 11px;font:inherit;transition:border-color .12s,box-shadow .12s}
@@ -559,6 +565,17 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
.pe-seg .be-note{display:none}
.pe-seg-note{font-size:11px;color:var(--dim);padding:0 2px;min-height:14px}
.pe-note{font-size:11px;color:var(--dim);line-height:1.45}
/* Barre de progression (téléchargement de modèle). Sans total connu, elle
défile en boucle via .indet. */
.pe-bar{height:4px;border-radius:999px;background:color-mix(in srgb,var(--dim) 26%,transparent);overflow:hidden}
.pe-bar>i{display:block;height:100%;width:0;border-radius:999px;background:var(--accent);transition:width .35s linear}
.pe-bar.indet>i{width:35%;animation:pe-bar-slide 1.2s ease-in-out infinite;transition:none}
.pe-bar.done>i{width:100%}
@keyframes pe-bar-slide{0%{margin-left:-35%}100%{margin-left:100%}}
/* Action discrète en texte (annuler un téléchargement…). */
.pe-link{align-self:flex-start;border:none;background:none;padding:0;font:inherit;font-size:11px;color:var(--dim);text-decoration:underline;cursor:pointer}
.pe-link:hover{color:var(--text)}
.pe-link:disabled{opacity:.5;cursor:default;text-decoration:none}
/* Interrupteur à glissière. */
.pe-switch{position:relative;flex:none;width:40px;height:24px}
.pe-switch input{position:absolute;opacity:0;width:0;height:0}