mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Hauteur : bulles techniques en rangée, sections groupées, accès distant retiré
Trois gains de place, mesurés au navigateur en 1500×900. - Raisonnement, outils et mémoire se posent CÔTE À CÔTE dans une rangée qui déborde à la ligne : un tour d'agent en enchaîne parfois dix, et 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. Mesuré : 8 bulles occupent 2 lignes au lieu de 8. Dépliée, une bulle reprend toute la largeur — son contenu ne doit pas être comprimé dans une colonne étroite. - Les sections de la barre latérale sont réunies en DEUX cartes (réglages d'IA / réglages d'application) au lieu d'une carte chacune : onze arrondis et autant de gouttières faisaient déborder la colonne. Mesuré : 876 px de contenu pour 876 px visibles — plus d'ascenseur. - Section « Accès distant » supprimée, ainsi que son module JS (13-remote.js) et son chargement : elle pilotait le tunnel ajean.link, sans objet ici. Le code serveur du relais reste en place mais inerte (aucun jeton) : le retirer créerait des conflits à chaque reprise de l'amont.
This commit is contained in:
6 files changed
+110
-252
No files matched your search
+55
-126
@@ -1344,6 +1344,35 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
#chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px}
|
#chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px}
|
||||||
#composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))}
|
#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}
|
||||||
|
}
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<button id="menubtn" onclick="toggleSide()">☰</button>
|
<button id="menubtn" onclick="toggleSide()">☰</button>
|
||||||
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
|
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
|
||||||
@@ -1374,6 +1403,10 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
||||||
trouvait pas comment la refermer.
|
trouvait pas comment la refermer.
|
||||||
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
||||||
|
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||||
|
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||||
|
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||||
|
<div class="sidegroup">
|
||||||
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
||||||
<div class="row" style="justify-content:flex-end;align-items:center">
|
<div class="row" style="justify-content:flex-end;align-items:center">
|
||||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||||
@@ -1542,11 +1575,16 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
|
</div><!-- /sidegroup -->
|
||||||
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||||
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||||
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||||
l'application elle-même. -->
|
l'application elle-même. -->
|
||||||
<div class="side-sep">Paramètres</div>
|
<div class="side-sep">Paramètres</div>
|
||||||
|
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||||
|
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||||
|
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||||
|
<div class="sidegroup">
|
||||||
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<div class="pe-row">
|
<div class="pe-row">
|
||||||
@@ -1625,35 +1663,6 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
<details id="remote-details"><summary><span class="stitle">Accès distant</span><span id="remote-badge" class="sbadge"></span></summary>
|
|
||||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Loki depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
|
||||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
|
||||||
<div id="remote-off" class="slist"><div class="rowbody">
|
|
||||||
<p class="muted" style="margin:0 0 8px">Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.</p>
|
|
||||||
<button onclick="remoteConnect()" style="border-color:var(--accent);color:var(--accent)">Connecter à ajean.link</button>
|
|
||||||
</div></div>
|
|
||||||
<!-- Connecté : URL publique + appairage + déconnexion. Le bouton « démarrer
|
|
||||||
le tunnel » vit DANS la carte de l'adresse : c'est l'action de cette
|
|
||||||
ligne, il flottait auparavant entre deux cartes. -->
|
|
||||||
<div id="remote-on" style="display:none">
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="muted" style="margin:0 0 6px">Votre adresse d'accès distant</div>
|
|
||||||
<div class="row" style="margin:0">
|
|
||||||
<input id="remote-url" readonly class="io-input">
|
|
||||||
<button class="iconbtn" onclick="copyText(document.getElementById('remote-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
|
|
||||||
<button class="iconbtn" onclick="window.open(document.getElementById('remote-url').value,'_blank')" title="ouvrir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><path d="M15 3h6v6"/><path d="M10 14L21 3"/></svg></button>
|
|
||||||
</div>
|
|
||||||
<div id="remote-status" class="muted" style="margin-top:6px"></div>
|
|
||||||
<div class="row" style="margin-bottom:0"><button id="remote-start" onclick="remoteStart()" style="display:none;border-color:var(--accent);color:var(--accent)">démarrer le tunnel</button></div>
|
|
||||||
</div></div>
|
|
||||||
<div class="subhead">Première connexion<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">À votre première ouverture du portail distant, confirmez l'<b>empreinte</b> et saisissez le <b>code d'appairage</b> : cela verrouille la boîte noire sur CE serveur (anti-interception). Une seule fois par appareil.</span></span></div>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="row" style="margin:0"><button onclick="remotePairCode()">Afficher l'empreinte et le code d'appairage</button></div>
|
|
||||||
<div id="remote-pair" class="muted" style="display:none;margin-top:6px"></div>
|
|
||||||
<div class="row" style="margin-bottom:0"><button onclick="remoteDisconnect()" class="btn-danger-o">déconnecter l'accès distant</button></div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||||
@@ -1671,6 +1680,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
|
</div><!-- /sidegroup -->
|
||||||
</div>
|
</div>
|
||||||
<div class="main" style="position:relative">
|
<div class="main" style="position:relative">
|
||||||
<div id="chat"></div>
|
<div id="chat"></div>
|
||||||
@@ -3147,7 +3157,7 @@ async function loadAll(){
|
|||||||
// allSettled et pas all : un seul chargement en échec (accès distant coupé,
|
// 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
|
// clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les
|
||||||
// hauteurs réservées en place pour toujours.
|
// 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
|
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); }
|
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;
|
el.querySelector('.body').textContent=text;
|
||||||
|
// 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);
|
chatEl().appendChild(el);
|
||||||
|
}
|
||||||
scrollMaybe();
|
scrollMaybe();
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
@@ -5353,102 +5378,6 @@ async function deleteMcp(){
|
|||||||
loadMCP();
|
loadMCP();
|
||||||
toast('serveur supprimé');
|
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 : <b>'+(r.fingerprint||'—')+'</b><br>Code d\'appairage (valable 10 min, usage unique) : <b>'+r.code+'</b>';
|
|
||||||
} else {
|
|
||||||
box.textContent = (r && r.error) || 'code indisponible';
|
|
||||||
}
|
|
||||||
}catch(e){ box.textContent='erreur : '+e; }
|
|
||||||
}
|
|
||||||
// ===== Pièces jointes du chat ==============================================
|
// ===== Pièces jointes du chat ==============================================
|
||||||
// Un fichier joint n'est PAS lu par le navigateur ni injecté dans le contexte :
|
// 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é
|
// il est déposé tel quel dans le dossier de travail de l'IA (uploads/, côté
|
||||||
|
|||||||
@@ -55,6 +55,10 @@
|
|||||||
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
OU par son propre chevron — c'est une section comme les autres, sinon on ne
|
||||||
trouvait pas comment la refermer.
|
trouvait pas comment la refermer.
|
||||||
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
||||||
|
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||||
|
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||||
|
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||||
|
<div class="sidegroup">
|
||||||
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
||||||
<div class="row" style="justify-content:flex-end;align-items:center">
|
<div class="row" style="justify-content:flex-end;align-items:center">
|
||||||
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
<button onclick="loadSvcLog()" title="rafraîchir">↻</button>
|
||||||
@@ -223,11 +227,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
|
</div><!-- /sidegroup -->
|
||||||
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||||
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||||
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||||
l'application elle-même. -->
|
l'application elle-même. -->
|
||||||
<div class="side-sep">Paramètres</div>
|
<div class="side-sep">Paramètres</div>
|
||||||
|
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||||
|
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||||
|
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||||
|
<div class="sidegroup">
|
||||||
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<div class="pe-row">
|
<div class="pe-row">
|
||||||
@@ -306,35 +315,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
<details id="remote-details"><summary><span class="stitle">Accès distant</span><span id="remote-badge" class="sbadge"></span></summary>
|
|
||||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Loki depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
|
||||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
|
||||||
<div id="remote-off" class="slist"><div class="rowbody">
|
|
||||||
<p class="muted" style="margin:0 0 8px">Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.</p>
|
|
||||||
<button onclick="remoteConnect()" style="border-color:var(--accent);color:var(--accent)">Connecter à ajean.link</button>
|
|
||||||
</div></div>
|
|
||||||
<!-- Connecté : URL publique + appairage + déconnexion. Le bouton « démarrer
|
|
||||||
le tunnel » vit DANS la carte de l'adresse : c'est l'action de cette
|
|
||||||
ligne, il flottait auparavant entre deux cartes. -->
|
|
||||||
<div id="remote-on" style="display:none">
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="muted" style="margin:0 0 6px">Votre adresse d'accès distant</div>
|
|
||||||
<div class="row" style="margin:0">
|
|
||||||
<input id="remote-url" readonly class="io-input">
|
|
||||||
<button class="iconbtn" onclick="copyText(document.getElementById('remote-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
|
|
||||||
<button class="iconbtn" onclick="window.open(document.getElementById('remote-url').value,'_blank')" title="ouvrir"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><path d="M15 3h6v6"/><path d="M10 14L21 3"/></svg></button>
|
|
||||||
</div>
|
|
||||||
<div id="remote-status" class="muted" style="margin-top:6px"></div>
|
|
||||||
<div class="row" style="margin-bottom:0"><button id="remote-start" onclick="remoteStart()" style="display:none;border-color:var(--accent);color:var(--accent)">démarrer le tunnel</button></div>
|
|
||||||
</div></div>
|
|
||||||
<div class="subhead">Première connexion<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">À votre première ouverture du portail distant, confirmez l'<b>empreinte</b> et saisissez le <b>code d'appairage</b> : cela verrouille la boîte noire sur CE serveur (anti-interception). Une seule fois par appareil.</span></span></div>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="row" style="margin:0"><button onclick="remotePairCode()">Afficher l'empreinte et le code d'appairage</button></div>
|
|
||||||
<div id="remote-pair" class="muted" style="display:none;margin-top:6px"></div>
|
|
||||||
<div class="row" style="margin-bottom:0"><button onclick="remoteDisconnect()" class="btn-danger-o">déconnecter l'accès distant</button></div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||||
@@ -352,6 +332,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div></div>
|
</div></div>
|
||||||
</details>
|
</details>
|
||||||
|
</div><!-- /sidegroup -->
|
||||||
</div>
|
</div>
|
||||||
<div class="main" style="position:relative">
|
<div class="main" style="position:relative">
|
||||||
<div id="chat"></div>
|
<div id="chat"></div>
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ async function loadAll(){
|
|||||||
// allSettled et pas all : un seul chargement en échec (accès distant coupé,
|
// 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
|
// clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les
|
||||||
// hauteurs réservées en place pour toujours.
|
// 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
|
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); }
|
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||||
@@ -56,7 +56,22 @@ function addMsg(role, text){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
el.querySelector('.body').textContent=text;
|
el.querySelector('.body').textContent=text;
|
||||||
|
// 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);
|
chatEl().appendChild(el);
|
||||||
|
}
|
||||||
scrollMaybe();
|
scrollMaybe();
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 : <b>'+(r.fingerprint||'—')+'</b><br>Code d\'appairage (valable 10 min, usage unique) : <b>'+r.code+'</b>';
|
|
||||||
} else {
|
|
||||||
box.textContent = (r && r.error) || 'code indisponible';
|
|
||||||
}
|
|
||||||
}catch(e){ box.textContent='erreur : '+e; }
|
|
||||||
}
|
|
||||||
@@ -1318,3 +1318,32 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
#chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px}
|
#chat{padding:26px max(20px,calc(50% - var(--measure)/2)) 8px}
|
||||||
#composer{--col:calc(max(20px,calc(50% - var(--measure)/2)) + var(--sbw,0px))}
|
#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}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user