mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 22:18:13 +02:00
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:
1 parent
1360eb1943
commit
065264a055
11 files changed
+722
-97
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user