diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html index 0554a51..4b2dba1 100644 --- a/internal/loki/ui/index.html +++ b/internal/loki/ui/index.html @@ -1344,6 +1344,35 @@ button:hover{border-color:var(--dim);color:var(--text)} #chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px} #composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))} } + +/* --- Terre : rangée de bulles techniques ------------------------------ + Raisonnement, outils et mémoire ne portent qu'une étiquette tant qu'ils sont + repliés : les empiler gaspillait une ligne entière par appel et repoussait la + réponse hors de l'écran. Ils défilent donc en rangée, et reprennent la pleine + largeur dès qu'on en déplie un. */ +.toolrow{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start} +.toolrow>.msg.collapsible{margin:0;max-width:100%} +.toolrow>.msg.collapsible.collapsed{width:auto} +/* Dépliée : la bulle passe seule sur sa ligne, sinon son contenu serait + comprimé dans une colonne étroite à côté d'une pastille. */ +.toolrow>.msg.collapsible:not(.collapsed){flex:1 0 100%} + +/* --- Terre : sections groupées en cartes ------------------------------ + Une carte par section empilait onze arrondis et autant de gouttières : la + barre latérale débordait de l'écran alors que la plupart de ces lignes sont + des titres repliés d'une seule ligne. Les sections d'IA d'un côté, celles de + l'application de l'autre, tiennent maintenant chacune dans UNE carte, avec un + filet discret entre les lignes. */ +@media (min-width:721px){ + .sidegroup{background:var(--panel);border-radius:20px;box-shadow:var(--shadow); + padding:2px 16px;display:flex;flex-direction:column} + /* Dans un groupe, les sections ne sont plus des cartes : elles redeviennent + des lignes, séparées par un filet — sauf la première. */ + .sidegroup>details{background:transparent;box-shadow:none;border-radius:0;padding:0; + border-top:1px solid var(--border)} + .sidegroup>details:first-child{border-top:0} + .sidegroup>details[open]{padding-bottom:12px} +} + +
Journal du moteur
@@ -1542,11 +1575,16 @@ button:hover{border-color:var(--dim);color:var(--text)}
+
Paramètres
+ +
Identité
@@ -1625,35 +1663,6 @@ button:hover{border-color:var(--dim);color:var(--text)}
-
Accès distant -
ajean.link?Accédez à ce serveur Loki depuis n'importe où, sans ouvrir de port, à travers le relais ajean.link. Le trafic est chiffré de bout en bout : le relais ne voit rien. Abonnement 4,80 €/mois.
- -
-

Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.

- -
- - -
Actions
@@ -3147,7 +3157,7 @@ async function loadAll(){ // allSettled et pas all : un seul chargement en échec (accès distant coupé, // clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les // hauteurs réservées en place pour toujours. - await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp(),loadRemote()]); + await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp()]); releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } @@ -4209,7 +4219,22 @@ function addMsg(role, text){ } } el.querySelector('.body').textContent=text; - chatEl().appendChild(el); + // Bulles techniques (raisonnement, outils, mémoire) : elles se posent CÔTE À + // CÔTE dans une rangée qui déborde à la ligne. Un tour d'agent en enchaîne + // parfois dix ; empilées, elles repoussaient la réponse hors de l'écran alors + // qu'elles ne portent qu'une étiquette tant qu'elles sont repliées. Dépliée, + // une bulle reprend toute la largeur de la rangée (CSS .toolrow). + if(collapsible){ + let row = chatEl().lastElementChild; + if(!row || !row.classList.contains('toolrow')){ + row = document.createElement('div'); + row.className = 'toolrow'; + chatEl().appendChild(row); + } + row.appendChild(el); + } else { + chatEl().appendChild(el); + } scrollMaybe(); return el; } @@ -5353,102 +5378,6 @@ async function deleteMcp(){ loadMCP(); toast('serveur supprimé'); } -// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal. -// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte + -// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à -// l'agent local (/api/link/connect) qui fait le `loki link` (token + service). - -const LOKI_APP_ORIGIN = 'https://app.ajean.link'; - -function renderRemote(d){ - const off = document.getElementById('remote-off'), on = document.getElementById('remote-on'); - const badge = document.getElementById('remote-badge'); - if(!d || !d.linked){ - // Serveur jamais lié : on l'affiche explicitement plutôt que de laisser la - // pastille vide, qui se lisait comme « je ne sais pas ». - off.style.display=''; on.style.display='none'; - setBadge(badge, false, 'non connecté'); - return; - } - off.style.display='none'; on.style.display=''; - document.getElementById('remote-url').value = d.machineURL || ''; - const st = document.getElementById('remote-status'); - if(d.active){ st.textContent='● en ligne — accessible à distance'; st.style.color='var(--accent)'; } - else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; } - const sb = document.getElementById('remote-start'); - if(sb){ sb.style.display = d.active ? 'none' : ''; } - setBadge(badge, d.active ? true : 'warn', d.active ? 'connecté' : 'arrêté'); -} - -async function loadRemote(){ - // Le panneau ne vaut qu'en LOCAL : il pilote /api/link/* que le tunnel bloque - // (boîte noire). Sur le portail distant (app.ajean.link/server.html), on le cache. - const det = document.getElementById('remote-details'); - if(location.hostname === 'app.ajean.link'){ if(det) det.style.display='none'; return; } - // Statut injoignable : on retombe sur « non connecté » plutôt que de garder - // l'affichage précédent, qui pourrait annoncer « connecté » à tort. - try{ renderRemote(await jget('/api/link/status')); }catch(e){ renderRemote(null); } -} - -// Ouvre la popup de connexion et attend la clé renvoyée par postMessage. -function remoteConnect(){ - const params = new URLSearchParams({ origin: window.location.origin, host: window.location.hostname || 'ce serveur' }); - const url = LOKI_APP_ORIGIN + '/connect.html?' + params.toString(); - const pop = window.open(url, 'loki-connect', 'width=440,height=640'); - if(!pop){ toast('autorisez les popups pour connecter ajean.link'); return; } - - async function onMsg(ev){ - // Anti-usurpation : n'accepte QUE des messages du portail ajean.link. - if(ev.origin !== LOKI_APP_ORIGIN) return; - const d = ev.data || {}; - if(d.type !== 'loki-link' || !d.token) return; - window.removeEventListener('message', onMsg); - try{ - const r = await jpost('/api/link/connect', { token: d.token }); - if(r && r.linked){ - toast('✓ accès distant connecté'); - if(r.serviceErr){ toast('token enregistré (service : '+r.serviceErr+')'); } - loadRemote(); - } else { - toast((r && r.error) || 'échec de la connexion'); - } - }catch(e){ toast('erreur : '+e); } - } - window.addEventListener('message', onMsg); -} - -// Relance le tunnel avec la clé déjà enregistrée (sans repasser par la popup). -async function remoteStart(){ - const b = document.getElementById('remote-start'); - if(b){ b.disabled = true; b.textContent = 'démarrage…'; } - try{ - const r = await jpost('/api/link/start', {}); - if(r && r.active){ toast('✓ tunnel démarré'); } - else { toast((r && r.error) || 'le tunnel n\'a pas démarré'); } - }catch(e){ toast('erreur : '+e); } - if(b){ b.disabled = false; b.textContent = 'démarrer le tunnel'; } - loadRemote(); -} - -async function remoteDisconnect(){ - const ok = await askConfirm('Couper l\'accès distant et oublier la clé de liaison de ce serveur ?', {title:'Accès distant', okLabel:'Déconnecter'}); - if(!ok) return; - try{ await jpost('/api/link/disconnect', {}); toast('accès distant coupé'); loadRemote(); } - catch(e){ toast('erreur : '+e); } -} - -async function remotePairCode(){ - const box = document.getElementById('remote-pair'); - box.style.display=''; box.textContent='génération du code…'; - try{ - const r = await jpost('/api/link/paircode', {}); - if(r && r.code){ - box.innerHTML = 'Empreinte : '+(r.fingerprint||'—')+'
Code d\'appairage (valable 10 min, usage unique) : '+r.code+''; - } else { - box.textContent = (r && r.error) || 'code indisponible'; - } - }catch(e){ box.textContent='erreur : '+e; } -} // ===== Pièces jointes du chat ============================================== // Un fichier joint n'est PAS lu par le navigateur ni injecté dans le contexte : // il est déposé tel quel dans le dossier de travail de l'IA (uploads/, côté diff --git a/internal/loki/ui/src/index.tmpl.html b/internal/loki/ui/src/index.tmpl.html index 1e8ea46..f0ab32e 100644 --- a/internal/loki/ui/src/index.tmpl.html +++ b/internal/loki/ui/src/index.tmpl.html @@ -55,6 +55,10 @@ OU par son propre chevron — c'est une section comme les autres, sinon on ne trouvait pas comment la refermer. C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. --> + +
Journal du moteur
@@ -223,11 +227,16 @@
+
Paramètres
+ +
Identité
@@ -306,35 +315,6 @@
-
Accès distant -
ajean.link?Accédez à ce serveur Loki depuis n'importe où, sans ouvrir de port, à travers le relais ajean.link. Le trafic est chiffré de bout en bout : le relais ne voit rien. Abonnement 4,80 €/mois.
- -
-

Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.

- -
- - -
Actions
diff --git a/internal/loki/ui/src/js/06-settings.js b/internal/loki/ui/src/js/06-settings.js index e98fbf7..561ca7b 100644 --- a/internal/loki/ui/src/js/06-settings.js +++ b/internal/loki/ui/src/js/06-settings.js @@ -491,7 +491,7 @@ async function loadAll(){ // allSettled et pas all : un seul chargement en échec (accès distant coupé, // clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les // hauteurs réservées en place pour toujours. - await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp(),loadRemote()]); + await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp()]); releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois } async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); } diff --git a/internal/loki/ui/src/js/08-chat-render.js b/internal/loki/ui/src/js/08-chat-render.js index 1f46756..16a91d9 100644 --- a/internal/loki/ui/src/js/08-chat-render.js +++ b/internal/loki/ui/src/js/08-chat-render.js @@ -56,7 +56,22 @@ function addMsg(role, text){ } } el.querySelector('.body').textContent=text; - chatEl().appendChild(el); + // Bulles techniques (raisonnement, outils, mémoire) : elles se posent CÔTE À + // CÔTE dans une rangée qui déborde à la ligne. Un tour d'agent en enchaîne + // parfois dix ; empilées, elles repoussaient la réponse hors de l'écran alors + // qu'elles ne portent qu'une étiquette tant qu'elles sont repliées. Dépliée, + // une bulle reprend toute la largeur de la rangée (CSS .toolrow). + if(collapsible){ + let row = chatEl().lastElementChild; + if(!row || !row.classList.contains('toolrow')){ + row = document.createElement('div'); + row.className = 'toolrow'; + chatEl().appendChild(row); + } + row.appendChild(el); + } else { + chatEl().appendChild(el); + } scrollMaybe(); return el; } diff --git a/internal/loki/ui/src/js/13-remote.js b/internal/loki/ui/src/js/13-remote.js deleted file mode 100644 index 890fa6d..0000000 --- a/internal/loki/ui/src/js/13-remote.js +++ /dev/null @@ -1,96 +0,0 @@ -// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal. -// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte + -// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à -// l'agent local (/api/link/connect) qui fait le `loki link` (token + service). - -const LOKI_APP_ORIGIN = 'https://app.ajean.link'; - -function renderRemote(d){ - const off = document.getElementById('remote-off'), on = document.getElementById('remote-on'); - const badge = document.getElementById('remote-badge'); - if(!d || !d.linked){ - // Serveur jamais lié : on l'affiche explicitement plutôt que de laisser la - // pastille vide, qui se lisait comme « je ne sais pas ». - off.style.display=''; on.style.display='none'; - setBadge(badge, false, 'non connecté'); - return; - } - off.style.display='none'; on.style.display=''; - document.getElementById('remote-url').value = d.machineURL || ''; - const st = document.getElementById('remote-status'); - if(d.active){ st.textContent='● en ligne — accessible à distance'; st.style.color='var(--accent)'; } - else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; } - const sb = document.getElementById('remote-start'); - if(sb){ sb.style.display = d.active ? 'none' : ''; } - setBadge(badge, d.active ? true : 'warn', d.active ? 'connecté' : 'arrêté'); -} - -async function loadRemote(){ - // Le panneau ne vaut qu'en LOCAL : il pilote /api/link/* que le tunnel bloque - // (boîte noire). Sur le portail distant (app.ajean.link/server.html), on le cache. - const det = document.getElementById('remote-details'); - if(location.hostname === 'app.ajean.link'){ if(det) det.style.display='none'; return; } - // Statut injoignable : on retombe sur « non connecté » plutôt que de garder - // l'affichage précédent, qui pourrait annoncer « connecté » à tort. - try{ renderRemote(await jget('/api/link/status')); }catch(e){ renderRemote(null); } -} - -// Ouvre la popup de connexion et attend la clé renvoyée par postMessage. -function remoteConnect(){ - const params = new URLSearchParams({ origin: window.location.origin, host: window.location.hostname || 'ce serveur' }); - const url = LOKI_APP_ORIGIN + '/connect.html?' + params.toString(); - const pop = window.open(url, 'loki-connect', 'width=440,height=640'); - if(!pop){ toast('autorisez les popups pour connecter ajean.link'); return; } - - async function onMsg(ev){ - // Anti-usurpation : n'accepte QUE des messages du portail ajean.link. - if(ev.origin !== LOKI_APP_ORIGIN) return; - const d = ev.data || {}; - if(d.type !== 'loki-link' || !d.token) return; - window.removeEventListener('message', onMsg); - try{ - const r = await jpost('/api/link/connect', { token: d.token }); - if(r && r.linked){ - toast('✓ accès distant connecté'); - if(r.serviceErr){ toast('token enregistré (service : '+r.serviceErr+')'); } - loadRemote(); - } else { - toast((r && r.error) || 'échec de la connexion'); - } - }catch(e){ toast('erreur : '+e); } - } - window.addEventListener('message', onMsg); -} - -// Relance le tunnel avec la clé déjà enregistrée (sans repasser par la popup). -async function remoteStart(){ - const b = document.getElementById('remote-start'); - if(b){ b.disabled = true; b.textContent = 'démarrage…'; } - try{ - const r = await jpost('/api/link/start', {}); - if(r && r.active){ toast('✓ tunnel démarré'); } - else { toast((r && r.error) || 'le tunnel n\'a pas démarré'); } - }catch(e){ toast('erreur : '+e); } - if(b){ b.disabled = false; b.textContent = 'démarrer le tunnel'; } - loadRemote(); -} - -async function remoteDisconnect(){ - const ok = await askConfirm('Couper l\'accès distant et oublier la clé de liaison de ce serveur ?', {title:'Accès distant', okLabel:'Déconnecter'}); - if(!ok) return; - try{ await jpost('/api/link/disconnect', {}); toast('accès distant coupé'); loadRemote(); } - catch(e){ toast('erreur : '+e); } -} - -async function remotePairCode(){ - const box = document.getElementById('remote-pair'); - box.style.display=''; box.textContent='génération du code…'; - try{ - const r = await jpost('/api/link/paircode', {}); - if(r && r.code){ - box.innerHTML = 'Empreinte : '+(r.fingerprint||'—')+'
Code d\'appairage (valable 10 min, usage unique) : '+r.code+''; - } else { - box.textContent = (r && r.error) || 'code indisponible'; - } - }catch(e){ box.textContent='erreur : '+e; } -} diff --git a/internal/loki/ui/src/styles.css b/internal/loki/ui/src/styles.css index 44e98dd..400a67e 100644 --- a/internal/loki/ui/src/styles.css +++ b/internal/loki/ui/src/styles.css @@ -1318,3 +1318,32 @@ button:hover{border-color:var(--dim);color:var(--text)} #chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px} #composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))} } + +/* --- Terre : rangée de bulles techniques ------------------------------ + Raisonnement, outils et mémoire ne portent qu'une étiquette tant qu'ils sont + repliés : les empiler gaspillait une ligne entière par appel et repoussait la + réponse hors de l'écran. Ils défilent donc en rangée, et reprennent la pleine + largeur dès qu'on en déplie un. */ +.toolrow{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start} +.toolrow>.msg.collapsible{margin:0;max-width:100%} +.toolrow>.msg.collapsible.collapsed{width:auto} +/* Dépliée : la bulle passe seule sur sa ligne, sinon son contenu serait + comprimé dans une colonne étroite à côté d'une pastille. */ +.toolrow>.msg.collapsible:not(.collapsed){flex:1 0 100%} + +/* --- Terre : sections groupées en cartes ------------------------------ + Une carte par section empilait onze arrondis et autant de gouttières : la + barre latérale débordait de l'écran alors que la plupart de ces lignes sont + des titres repliés d'une seule ligne. Les sections d'IA d'un côté, celles de + l'application de l'autre, tiennent maintenant chacune dans UNE carte, avec un + filet discret entre les lignes. */ +@media (min-width:721px){ + .sidegroup{background:var(--panel);border-radius:20px;box-shadow:var(--shadow); + padding:2px 16px;display:flex;flex-direction:column} + /* Dans un groupe, les sections ne sont plus des cartes : elles redeviennent + des lignes, séparées par un filet — sauf la première. */ + .sidegroup>details{background:transparent;box-shadow:none;border-radius:0;padding:0; + border-top:1px solid var(--border)} + .sidegroup>details:first-child{border-top:0} + .sidegroup>details[open]{padding-bottom:12px} +}