mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 02:38:20 +02:00
Cartes raisonnement bornées et fluides ; version 0.11.0
Un long raisonnement (des milliers de tokens) faisait grandir la page de plusieurs écrans et finissait par figer l'affichage : le bloc entier était re-parsé en Markdown à chaque tick, en O(n²). Deux mesures : - Les cartes raisonnement/outils ont une hauteur bornée (280 px) avec défilement interne, collé en bas pendant la génération (un défilement manuel vers le haut est respecté). - En direct, un bloc de raisonnement géant n'est re-parsé que sur sa fin (REASON_TAIL) ; le texte complet est posé au rendu de fin de bloc. La finalité voyage avec le rendu en attente (renderPending.final) : le timer déjà armé passait final=false et consommait le rendu final en ne posant que la queue — le début du raisonnement n'était jamais rendu. Version 0.11.0 (const, versioninfo, .syso régénérés) ; README et RELEASE_NOTES à jour sur les nouveautés du fork. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a333603ca6
commit
43489769f4
9 files changed
+122
-29
No files matched your search
@@ -320,8 +320,18 @@ Ajoutées par ce fork :
|
||||
`AGENT_BUDGET=off` le désactive complètement.
|
||||
- **Identité** : ton prénom et un avatar emoji pour toi et pour Loki, affichés
|
||||
dans le fil.
|
||||
- **Paramètres** : les réglages d'application (identité, apparence, accès
|
||||
OpenAI, actions) sont regroupés à part des réglages d'IA.
|
||||
- **Réglages en modale** : tous les réglages vivent dans une fenêtre à deux
|
||||
volets — la nav des sections à gauche (IA, moteur, application), le panneau
|
||||
choisi à droite. La barre latérale ne garde que les discussions (les plus
|
||||
récentes en tête) et le moniteur machine.
|
||||
- **Nom du modèle sur chaque réponse** : une pastille à côté de « Loki » dit
|
||||
quel modèle a produit la réponse. Elle est journalisée avec le tour : elle
|
||||
survit au rechargement, et un vieux tour garde le modèle de l'époque.
|
||||
- **Cartes raisonnement/outils à hauteur bornée** : un long raisonnement ne
|
||||
fait plus grandir la page de plusieurs écrans — la carte reste à taille fixe
|
||||
et défile toute seule pendant la génération. Sur un raisonnement géant, seul
|
||||
le bas du bloc est re-rendu en direct (le texte complet est posé à la fin) :
|
||||
l'affichage ne se fige plus.
|
||||
|
||||
Retirées par ce fork :
|
||||
|
||||
|
||||
+36
-9
@@ -1,19 +1,46 @@
|
||||
La grande nouveauté de cette version : AJEAN sait enfin voir les images.
|
||||
# Loki 0.11.0
|
||||
|
||||
## La vision, configurable par preset
|
||||
Première version numérotée du fork depuis la synchronisation avec l'amont
|
||||
AJEAN (v0.10.7). L'essentiel de cette version : une interface qui respire.
|
||||
|
||||
Jusqu'ici rien dans l'interface ne permettait de donner des yeux à un modèle. Il fallait glisser l'option `--mmproj` à la main dans la configuration brute, et son chemin n'était même pas résolu comme celui du modèle. L'éditeur de preset gagne un champ **Vision** : tu y choisis le projecteur multimodal (fichier `mmproj`) qui accompagne le modèle, et c'est tout. Au démarrage du moteur, AJEAN le charge tout seul.
|
||||
## Réglages en modale
|
||||
|
||||
Le champ ne liste que les projecteurs (les fichiers dont le nom contient `mmproj`), pas les modèles de plusieurs Go, pour que le choix reste lisible. Et s'il te manque un projecteur, le champ « télécharger un modèle » accepte aussi un lien vers un `mmproj` : une fois récupéré, il se place directement dans le champ Vision.
|
||||
Tous les réglages quittent la barre latérale pour une fenêtre à deux volets :
|
||||
la nav des sections à gauche (IA, moteur, application), le panneau choisi à
|
||||
droite — chaque section a enfin toute la largeur. La barre latérale ne garde
|
||||
que les discussions et le moniteur machine. Les chemins historiques (pastille
|
||||
d'état du moteur, pastille d'installation llama.cpp) rouvrent la modale au bon
|
||||
endroit.
|
||||
|
||||
## Les images arrivent vraiment au modèle
|
||||
## Discussions : la plus récente en tête
|
||||
|
||||
Avant, une image collée dans le chat était simplement déposée comme un fichier dans le dossier de travail, à charge pour le modèle de l'ouvrir avec ses outils (ce qui ne donnait qu'un tas d'octets illisibles). Désormais, quand un projecteur est configuré, l'image part au modèle en contenu multimodal : il la voit, et peut en parler.
|
||||
La liste se lit en ordre d'activité, la discussion du moment en haut — y
|
||||
compris une discussion toute neuve, qui apparaissait sous celle qu'on venait de
|
||||
quitter.
|
||||
|
||||
## Nom du modèle sur chaque réponse
|
||||
|
||||
Une pastille à côté de « Loki » dit quel modèle a produit la réponse. Le nom
|
||||
est journalisé avec le tour : il survit au rechargement de page, et un vieux
|
||||
tour garde le modèle de l'époque, pas celui chargé aujourd'hui.
|
||||
|
||||
## Cartes de raisonnement à hauteur bornée
|
||||
|
||||
Un long raisonnement (plusieurs milliers de tokens) faisait grandir la page de
|
||||
plusieurs écrans et finissait par figer l'affichage (re-parse Markdown du bloc
|
||||
entier à chaque token, en O(n²)). La carte reste maintenant à taille fixe et
|
||||
suit la génération en défilant toute seule ; en direct, seul le bas d'un bloc
|
||||
géant est re-rendu, et le texte complet est posé en fin de bloc.
|
||||
|
||||
## Divers
|
||||
|
||||
- Jeton Hugging Face réglable dans l'interface (dépôts verrouillés : le
|
||||
message dit lesquels, et pourquoi le 401).
|
||||
- Tâches planifiées reprises de l'amont : l'IA travaille toute seule sur une
|
||||
fréquence réglable, isolée des discussions.
|
||||
|
||||
## Mise à jour
|
||||
|
||||
```
|
||||
ajean update
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
Non vérifié sur cette version : le résultat final dépend d'un modèle vision et de son projecteur compatibles (Qwen2.5-VL, Gemma 3, etc.). À noter, l'image reste dans l'historique de la conversation et repart au moteur à chaque tour tant que la conversation dure.
|
||||
Binary file not shown.
Binary file not shown.
@@ -2,14 +2,14 @@
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 9,
|
||||
"Patch": 4,
|
||||
"Minor": 11,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 9,
|
||||
"Patch": 4,
|
||||
"Minor": 11,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 Loki contributors. MIT License.",
|
||||
"OriginalFilename": "loki.exe",
|
||||
"ProductName": "Loki",
|
||||
"ProductVersion": "0.9.4",
|
||||
"ProductVersion": "0.11.0",
|
||||
"Comments": "https://github.com/R0m1k3/Loki — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.9.4"
|
||||
const Version = "0.11.0"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/loki ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -687,6 +687,12 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
/* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth)
|
||||
→ repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */
|
||||
.msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px}
|
||||
/* Raisonnement / outils : hauteur BORNÉE avec défilement interne. Un long
|
||||
raisonnement (des milliers de tokens) faisait grandir la page de plusieurs
|
||||
écrans ; la carte reste maintenant à taille fixe et suit la génération en
|
||||
défilant toute seule (voir flushRender, 09-stream.js). L'animation de repli
|
||||
garde son overflow:hidden via la hauteur inline — les deux cohabitent. */
|
||||
.msg.reasoning .bodywrap,.msg.tool .bodywrap{max-height:280px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}
|
||||
.msg.collapsible .bodywrap>.body{min-width:0}
|
||||
.msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0}
|
||||
/* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine
|
||||
@@ -6379,11 +6385,15 @@ function syncSendBtn(){
|
||||
// garanti par flushRender(), appelé à chaque frontière de bloc (outil, fin de
|
||||
// tour, erreur, caught_up).
|
||||
let renderTimer=null, renderPending=null, lastRenderMs=0; // {el, text}
|
||||
function scheduleRender(el, text){
|
||||
function scheduleRender(el, text, final){
|
||||
// Changement de bloc en cours de route : on rend d'abord l'ancien à sa dernière
|
||||
// valeur, sinon son ultime bout de texte serait perdu.
|
||||
if(renderPending && renderPending.el!==el) flushRender();
|
||||
renderPending={el, text};
|
||||
// `final` voyage AVEC le rendu en attente : le timer déjà armé (qui passe
|
||||
// final=false) peut être celui qui consommera ce rendu — sans le drapeau sur
|
||||
// le pending, le rendu de FIN de bloc repassait par le chemin « queue seule »
|
||||
// et le début du raisonnement n'était jamais posé.
|
||||
renderPending={el, text, final:!!final};
|
||||
if(renderTimer) return;
|
||||
// Cadence ADAPTATIVE : on vise à ne pas passer plus d'~1/6 du temps à re-parser
|
||||
// le Markdown. Tant que le bloc est petit, un rendu coûte 1-2 ms → plancher
|
||||
@@ -6391,15 +6401,33 @@ function scheduleRender(el, text){
|
||||
// devient énorme, le rendu coûte cher et on espace tout seul jusqu'à 500 ms :
|
||||
// le O(n²) est cassé sans jamais figer l'interface.
|
||||
const delay=Math.min(500, Math.max(16, lastRenderMs*6));
|
||||
renderTimer=setTimeout(flushRender, delay);
|
||||
renderTimer=setTimeout(()=>flushRender(false), delay);
|
||||
}
|
||||
function flushRender(){
|
||||
// Au-delà de cette taille, un raisonnement EN COURS n'est re-parsé que sur sa
|
||||
// fin : re-parser le bloc entier à chaque tick est en O(n²) et finissait par
|
||||
// figer l'affichage (le texte semblait s'arrêter alors que le moteur débitait).
|
||||
// La carte étant bornée en hauteur et collée en bas, on ne perd rien à l'écran ;
|
||||
// le texte COMPLET est posé au rendu final (frontière de bloc → final=true).
|
||||
const REASON_TAIL=6000;
|
||||
function flushRender(final){
|
||||
if(renderTimer){ clearTimeout(renderTimer); renderTimer=null; }
|
||||
const p=renderPending; renderPending=null;
|
||||
if(!p) return;
|
||||
let text=p.text;
|
||||
if(final===false && !p.final && text.length>REASON_TAIL+800 && p.el.closest('.msg.reasoning')){
|
||||
let cut=text.length-REASON_TAIL;
|
||||
const nl=text.indexOf('\n', cut); if(nl>-1 && nl<cut+400) cut=nl+1;
|
||||
text='*… (début replié pendant la génération — le texte complet est posé à la fin)*\n\n'+text.slice(cut);
|
||||
}
|
||||
// Carte bornée (raisonnement/outil) : si on était collé en bas, on y reste —
|
||||
// la carte suit la génération. Un défilement manuel vers le haut est respecté.
|
||||
const bw=p.el.parentElement;
|
||||
const scrollable=bw && bw.classList.contains('bodywrap');
|
||||
const stick=scrollable && bw.scrollTop+bw.clientHeight>=bw.scrollHeight-60;
|
||||
const t0=performance.now();
|
||||
renderBody(p.el, p.text);
|
||||
renderBody(p.el, text);
|
||||
lastRenderMs=performance.now()-t0;
|
||||
if(scrollable && stick) bw.scrollTop=bw.scrollHeight;
|
||||
}
|
||||
// Annule le rendu en attente SANS le poser : le bloc visé disparaît (fil vidé,
|
||||
// raisonnement jeté), le rendre ensuite écrirait dans un élément détaché.
|
||||
@@ -6418,7 +6446,7 @@ function smoothReset(){ if(smooth&&smooth.raf) cancelAnimationFrame(smooth.raf);
|
||||
// Solde le bloc courant : on affiche TOUT immédiatement. Appelé à chaque
|
||||
// frontière (fin de tour, outil, changement de bloc) — sinon l'ultime bout de
|
||||
// texte resterait en retard derrière le curseur de révélation.
|
||||
function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target); }
|
||||
function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target, true); }
|
||||
// Débit BASÉ SUR LE TEMPS (indépendant du taux de rafraîchissement). La vitesse
|
||||
// de révélation est proportionnelle au RETARD accumulé, avec une constante de
|
||||
// temps TAU : le curseur traîne volontairement ~TAU derrière l'arrivée, ce qui
|
||||
|
||||
@@ -278,11 +278,15 @@ function syncSendBtn(){
|
||||
// garanti par flushRender(), appelé à chaque frontière de bloc (outil, fin de
|
||||
// tour, erreur, caught_up).
|
||||
let renderTimer=null, renderPending=null, lastRenderMs=0; // {el, text}
|
||||
function scheduleRender(el, text){
|
||||
function scheduleRender(el, text, final){
|
||||
// Changement de bloc en cours de route : on rend d'abord l'ancien à sa dernière
|
||||
// valeur, sinon son ultime bout de texte serait perdu.
|
||||
if(renderPending && renderPending.el!==el) flushRender();
|
||||
renderPending={el, text};
|
||||
// `final` voyage AVEC le rendu en attente : le timer déjà armé (qui passe
|
||||
// final=false) peut être celui qui consommera ce rendu — sans le drapeau sur
|
||||
// le pending, le rendu de FIN de bloc repassait par le chemin « queue seule »
|
||||
// et le début du raisonnement n'était jamais posé.
|
||||
renderPending={el, text, final:!!final};
|
||||
if(renderTimer) return;
|
||||
// Cadence ADAPTATIVE : on vise à ne pas passer plus d'~1/6 du temps à re-parser
|
||||
// le Markdown. Tant que le bloc est petit, un rendu coûte 1-2 ms → plancher
|
||||
@@ -290,15 +294,33 @@ function scheduleRender(el, text){
|
||||
// devient énorme, le rendu coûte cher et on espace tout seul jusqu'à 500 ms :
|
||||
// le O(n²) est cassé sans jamais figer l'interface.
|
||||
const delay=Math.min(500, Math.max(16, lastRenderMs*6));
|
||||
renderTimer=setTimeout(flushRender, delay);
|
||||
renderTimer=setTimeout(()=>flushRender(false), delay);
|
||||
}
|
||||
function flushRender(){
|
||||
// Au-delà de cette taille, un raisonnement EN COURS n'est re-parsé que sur sa
|
||||
// fin : re-parser le bloc entier à chaque tick est en O(n²) et finissait par
|
||||
// figer l'affichage (le texte semblait s'arrêter alors que le moteur débitait).
|
||||
// La carte étant bornée en hauteur et collée en bas, on ne perd rien à l'écran ;
|
||||
// le texte COMPLET est posé au rendu final (frontière de bloc → final=true).
|
||||
const REASON_TAIL=6000;
|
||||
function flushRender(final){
|
||||
if(renderTimer){ clearTimeout(renderTimer); renderTimer=null; }
|
||||
const p=renderPending; renderPending=null;
|
||||
if(!p) return;
|
||||
let text=p.text;
|
||||
if(final===false && !p.final && text.length>REASON_TAIL+800 && p.el.closest('.msg.reasoning')){
|
||||
let cut=text.length-REASON_TAIL;
|
||||
const nl=text.indexOf('\n', cut); if(nl>-1 && nl<cut+400) cut=nl+1;
|
||||
text='*… (début replié pendant la génération — le texte complet est posé à la fin)*\n\n'+text.slice(cut);
|
||||
}
|
||||
// Carte bornée (raisonnement/outil) : si on était collé en bas, on y reste —
|
||||
// la carte suit la génération. Un défilement manuel vers le haut est respecté.
|
||||
const bw=p.el.parentElement;
|
||||
const scrollable=bw && bw.classList.contains('bodywrap');
|
||||
const stick=scrollable && bw.scrollTop+bw.clientHeight>=bw.scrollHeight-60;
|
||||
const t0=performance.now();
|
||||
renderBody(p.el, p.text);
|
||||
renderBody(p.el, text);
|
||||
lastRenderMs=performance.now()-t0;
|
||||
if(scrollable && stick) bw.scrollTop=bw.scrollHeight;
|
||||
}
|
||||
// Annule le rendu en attente SANS le poser : le bloc visé disparaît (fil vidé,
|
||||
// raisonnement jeté), le rendre ensuite écrirait dans un élément détaché.
|
||||
@@ -317,7 +339,7 @@ function smoothReset(){ if(smooth&&smooth.raf) cancelAnimationFrame(smooth.raf);
|
||||
// Solde le bloc courant : on affiche TOUT immédiatement. Appelé à chaque
|
||||
// frontière (fin de tour, outil, changement de bloc) — sinon l'ultime bout de
|
||||
// texte resterait en retard derrière le curseur de révélation.
|
||||
function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target); }
|
||||
function smoothSnap(){ if(!smooth) return; const el=smooth.el, target=smooth.target; smoothReset(); scheduleRender(el, target, true); }
|
||||
// Débit BASÉ SUR LE TEMPS (indépendant du taux de rafraîchissement). La vitesse
|
||||
// de révélation est proportionnelle au RETARD accumulé, avec une constante de
|
||||
// temps TAU : le curseur traîne volontairement ~TAU derrière l'arrivée, ce qui
|
||||
|
||||
@@ -656,6 +656,12 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
/* height ET width sont animées (en px, pilotées en JS via scrollHeight/scrollWidth)
|
||||
→ repli/dépli fluide en diagonale. overflow:hidden clippe pendant l'animation. */
|
||||
.msg.collapsible .bodywrap{overflow:hidden;transition:height .42s cubic-bezier(.4,0,.2,1),width .42s cubic-bezier(.4,0,.2,1),opacity .3s ease,margin-top .42s cubic-bezier(.4,0,.2,1);opacity:1;margin-top:4px}
|
||||
/* Raisonnement / outils : hauteur BORNÉE avec défilement interne. Un long
|
||||
raisonnement (des milliers de tokens) faisait grandir la page de plusieurs
|
||||
écrans ; la carte reste maintenant à taille fixe et suit la génération en
|
||||
défilant toute seule (voir flushRender, 09-stream.js). L'animation de repli
|
||||
garde son overflow:hidden via la hauteur inline — les deux cohabitent. */
|
||||
.msg.reasoning .bodywrap,.msg.tool .bodywrap{max-height:280px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}
|
||||
.msg.collapsible .bodywrap>.body{min-width:0}
|
||||
.msg.collapsible.collapsed .bodywrap{opacity:0;margin-top:0}
|
||||
/* Composer = un seul bloc cohérent : barre de contexte intégrée en tête (fine
|
||||
|
||||
Reference in new issue
Block a user