mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
L'amont ne connaît qu'un fil unique (bkChat/conversation) que « clear chat »
effaçait définitivement. On garde toute la machinerie (un seul conv en
mémoire, mêmes flux SSE, même compactage) mais rangée par discussion :
bkChat/index liste des discussions (métadonnées seules)
bkChat/active discussion ouverte, partagée par tous les appareils
bkChat/conv:<id> état complet d'une discussion
Basculer réutilise le mécanisme d'epoch du reset : les abonnés SSE reçoivent
{reset:true} et rejouent le nouveau fil — aucun code de rendu à toucher. Le
fil unique existant est repris comme première discussion au premier
démarrage, et sa clé d'origine est laissée intacte.
- routes /api/conversations (liste, new, switch, rename, delete)
- barre latérale : liste (titre déduit du 1er message, date, nb d'échanges),
bouton +, renommer, supprimer ; lignes construites en DOM et non en
innerHTML, les titres venant de messages utilisateur
- suppression de la dernière discussion : convCreate et non convNew, qui
aurait réenregistré celle qu'on vient d'effacer
Le bouton « Vérifier les mises à jour » répondait « GitHub a répondu 404 » :
il interrogeait les releases du dépôt du fork, qui n'en publie aucune. En
conteneur, remplacer le binaire n'a de toute façon pas de sens — l'UI, l'API
et la CLI renvoient désormais « docker compose pull ».
Vérifié en conteneur : création, bascule, renommage (titre accentué avec
< > &), suppression de l'active puis de la dernière ; /api/update et
loki update renvoient la note Docker.
96 lines
4.2 KiB
JavaScript
96 lines
4.2 KiB
JavaScript
// Discussions — plusieurs fils au lieu de la conversation unique d'origine.
|
|
//
|
|
// Le fil actif est possédé par le SERVEUR et partagé par tous les appareils :
|
|
// basculer ici bascule partout. Le changement arrive donc par le flux SSE (un
|
|
// événement `reset`, comme pour « vider »), et c'est lui qui redessine le chat —
|
|
// on ne fait que rafraîchir la liste.
|
|
//
|
|
// Les titres viennent du premier message de l'utilisateur : ils sont construits
|
|
// en DOM (jamais en innerHTML) pour qu'un titre contenant < > & " ne casse ni le
|
|
// balisage ni les gestionnaires d'événement.
|
|
|
|
async function loadConversations(){
|
|
let r;
|
|
try{ r = await jget('/api/conversations'); }catch(_){ return; }
|
|
const cont = document.getElementById('conv-list');
|
|
if(!cont) return;
|
|
cont.innerHTML = '';
|
|
const list = r.conversations || [];
|
|
if(!list.length){ cont.innerHTML = '<span class="muted">(aucune)</span>'; return; }
|
|
for(const c of list){
|
|
const row = document.createElement('div');
|
|
row.className = 'preset' + (c.id === r.active ? ' active' : '');
|
|
row.onclick = () => convSwitch(c.id);
|
|
|
|
const info = document.createElement('div'); info.className = 'preset-info';
|
|
const nm = document.createElement('div'); nm.className = 'preset-name';
|
|
if(c.id === r.active){ const d = document.createElement('i'); d.className = 'preset-dot'; nm.appendChild(d); }
|
|
const title = c.title || 'Nouvelle discussion';
|
|
nm.appendChild(document.createTextNode(title));
|
|
nm.title = title;
|
|
const meta = document.createElement('div'); meta.className = 'preset-meta';
|
|
const when = document.createElement('span'); when.className = 'muted';
|
|
when.textContent = convWhen(c.updated) + (c.turns ? ' · ' + c.turns + ' échange' + (c.turns > 1 ? 's' : '') : '');
|
|
meta.appendChild(when);
|
|
info.appendChild(nm); info.appendChild(meta);
|
|
|
|
// Renommer / supprimer : stopPropagation, sinon le clic bascule aussi.
|
|
const acts = document.createElement('div'); acts.className = 'conv-acts';
|
|
const ren = document.createElement('button'); ren.className = 'iconbtn'; ren.textContent = '✎';
|
|
ren.title = 'renommer'; ren.onclick = e => { e.stopPropagation(); convRename(c.id, title); };
|
|
const del = document.createElement('button'); del.className = 'iconbtn'; del.textContent = '🗑';
|
|
del.title = 'supprimer'; del.onclick = e => { e.stopPropagation(); convDelete(c.id, title); };
|
|
acts.appendChild(ren); acts.appendChild(del);
|
|
|
|
row.appendChild(info); row.appendChild(acts);
|
|
cont.appendChild(row);
|
|
}
|
|
}
|
|
|
|
// Date courte : l'heure pour aujourd'hui, la date sinon — une liste de
|
|
// discussions se lit d'un coup d'œil, pas au timestamp complet.
|
|
function convWhen(unix){
|
|
if(!unix) return '';
|
|
const d = new Date(unix * 1000), now = new Date();
|
|
const sameDay = d.toDateString() === now.toDateString();
|
|
return sameDay
|
|
? d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})
|
|
: d.toLocaleDateString([], {day:'2-digit', month:'2-digit'});
|
|
}
|
|
|
|
async function convNew(){
|
|
try{
|
|
await jpost('/api/conversations/new', {});
|
|
toast('nouvelle discussion');
|
|
loadConversations();
|
|
}catch(_){ toast('erreur réseau'); }
|
|
}
|
|
|
|
async function convSwitch(id){
|
|
try{
|
|
const r = await jpost('/api/conversations/switch', {id});
|
|
if(r && r.ok === false){ toast('erreur : ' + (r.error || '')); return; }
|
|
loadConversations();
|
|
}catch(_){ toast('erreur réseau'); }
|
|
}
|
|
|
|
async function convRename(id, current){
|
|
const t = await askPrompt('Nouveau titre de la discussion', {title:'Renommer', default:current, okText:'Renommer'});
|
|
if(!t) return;
|
|
try{
|
|
const r = await jpost('/api/conversations/rename', {id, title:t});
|
|
if(r && r.ok === false){ toast('erreur : ' + (r.error || '')); return; }
|
|
loadConversations();
|
|
}catch(_){ toast('erreur réseau'); }
|
|
}
|
|
|
|
async function convDelete(id, title){
|
|
if(!await askConfirm('Supprimer « ' + title + ' » ? Cette discussion et son historique seront perdus.',
|
|
{title:'Supprimer la discussion', okText:'Supprimer', danger:true})) return;
|
|
try{
|
|
const r = await jpost('/api/conversations/delete', {id});
|
|
if(r && r.ok === false){ toast('erreur : ' + (r.error || '')); return; }
|
|
loadConversations();
|
|
}catch(_){ toast('erreur réseau'); }
|
|
}
|