mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Presets : glisser-déposer, pastilles contexte et capacités, prompt système dans l'éditeur
Repris d'AJEAN et adapté à Loki : - liste réordonnable par glisser-déposer (SortableJS 1.15.6, MIT), ordre gardé côté serveur (/api/presets/order) et suivi par le sélecteur de l'en-tête ; le clic de fin de glissement ne bascule pas de preset - pastille de contexte (32K, 128K, 40K…) et pastille « capacités » : œil quand le preset lit les images (MMPROJ, API externe déclarée multimodale, Strata), ampoule + niveau quand il raisonne - éditeur : prompt système DE CE preset (rangé en base, sysprompt:<id>), projecteur vision sur le CPU (--no-mmproj-offload, visible seulement avec un mmproj) - les projecteurs mmproj ne sont plus proposés comme modèle (éditeur et sélecteur de l'en-tête) - l'aide « ? » reste survolable sur un réglage grisé Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
127013b32b
commit
ecff7de5a3
11 files changed
+412
-27
No files matched your search
@@ -113,7 +113,69 @@ func ListPresets() ([]Preset, error) {
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, nil
|
||||
return applyPresetOrder(out), nil
|
||||
}
|
||||
|
||||
// presetOrderKey : ordre d'affichage choisi par glisser-déposer dans l'UI
|
||||
// (repris d'AJEAN v0.10.9), une liste d'IDs persistée en base.
|
||||
const presetOrderKey = "preset_order"
|
||||
|
||||
func loadPresetOrder() []string {
|
||||
var ids []string
|
||||
getJSON(bkState, presetOrderKey, &ids)
|
||||
return ids
|
||||
}
|
||||
|
||||
func savePresetOrder(ids []string) error { return putJSON(bkState, presetOrderKey, ids) }
|
||||
|
||||
// applyPresetOrder réordonne `list` (déjà trié par nom) selon l'ordre stocké :
|
||||
// d'abord les presets classés dans l'ordre choisi, puis les non classés
|
||||
// (fraîchement créés) dans leur ordre alphabétique. Tri STABLE.
|
||||
func applyPresetOrder(list []Preset) []Preset {
|
||||
order := loadPresetOrder()
|
||||
if len(order) == 0 {
|
||||
return list
|
||||
}
|
||||
pos := map[string]int{}
|
||||
for i, id := range order {
|
||||
pos[id] = i
|
||||
}
|
||||
sort.SliceStable(list, func(i, j int) bool {
|
||||
pi, oki := pos[list[i].ID]
|
||||
pj, okj := pos[list[j].ID]
|
||||
if oki && okj {
|
||||
return pi < pj
|
||||
}
|
||||
if oki != okj {
|
||||
return oki // un preset classé passe avant un non classé
|
||||
}
|
||||
return false // deux non classés : l'ordre alphabétique reste
|
||||
})
|
||||
return list
|
||||
}
|
||||
|
||||
// presetCtx : la taille de contexte d'un preset, formatée pour la pastille de
|
||||
// la liste (« 32K », « 128K », « 1M »). CTX absent = 32768, le défaut de serve.
|
||||
func presetCtx(content string) string {
|
||||
raw := strings.TrimSpace(parseEnv(content)["CTX"])
|
||||
if raw == "" {
|
||||
raw = "32768"
|
||||
}
|
||||
n, err := strconv.Atoi(raw)
|
||||
if err != nil || n <= 0 {
|
||||
return ""
|
||||
}
|
||||
// Multiple de 1024 (32768, 131072…) : écrit comme on le pense, « 32K »,
|
||||
// « 128K » ; sinon décimal, comme on le tape (40000 → « 40K »).
|
||||
switch {
|
||||
case n >= 1024 && n%1024 == 0 && n < 1<<20:
|
||||
return strconv.Itoa(n/1024) + "K"
|
||||
case n >= 1000000:
|
||||
return strings.TrimSuffix(strconv.FormatFloat(float64(n)/1000000, 'f', 1, 64), ".0") + "M"
|
||||
case n >= 1000:
|
||||
return strconv.Itoa((n+500)/1000) + "K"
|
||||
}
|
||||
return strconv.Itoa(n)
|
||||
}
|
||||
|
||||
// uniquePresetID derives a unique filename id from a display name, appending
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Ordre choisi par glisser-déposer : les presets classés d'abord, dans cet
|
||||
// ordre ; les nouveaux (non classés) ensuite, par nom.
|
||||
func TestPresetOrder(t *testing.T) {
|
||||
testHome(t)
|
||||
_ = os.MkdirAll(presetsDir(), 0o755)
|
||||
for _, id := range []string{"alpha", "bravo", "charlie", "delta"} {
|
||||
if err := os.WriteFile(filepath.Join(presetsDir(), id+".env"), []byte("# NAME="+id+"\nCTX=4096\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
ids := func() []string {
|
||||
list, err := ListPresets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out []string
|
||||
for _, p := range list {
|
||||
out = append(out, p.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
if got := ids(); got[0] != "alpha" || got[3] != "delta" {
|
||||
t.Fatalf("sans ordre enregistré, tri par nom attendu : %v", got)
|
||||
}
|
||||
if err := savePresetOrder([]string{"charlie", "alpha"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{"charlie", "alpha", "bravo", "delta"}
|
||||
got := ids()
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("ordre %v, attendu %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresetCtx(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "32K", // défaut de serve : 32768
|
||||
"CTX=40000\n": "40K",
|
||||
"CTX=\"131072\"\n": "128K",
|
||||
"CTX=1000000\n": "1M",
|
||||
"CTX=1500000\n": "1.5M",
|
||||
"CTX=512\n": "512",
|
||||
"CTX=abc\n": "",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := presetCtx(in); got != want {
|
||||
t.Errorf("presetCtx(%q) = %q, attendu %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+121
-11
@@ -205,7 +205,7 @@ option,optgroup{background:var(--panel);color:var(--text)}
|
||||
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
||||
@media (min-width:721px){
|
||||
.side{width:380px}
|
||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag,.preset-meta .xtag{font-size:11px;padding:2px 9px;border-radius:4px}
|
||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag,.preset-meta .xtag,.preset-meta .ctag,.preset-meta .captag{font-size:11px;padding:2px 9px;border-radius:4px}
|
||||
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg,.preset-meta .xtag svg{width:11px;height:11px}
|
||||
}
|
||||
h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
@@ -301,6 +301,9 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
(étiquette comprise), plus sur la carte seule. */
|
||||
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
||||
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
||||
/* L'aide « ? » reste survolable sur un réglage grisé : c'est justement là qu'on
|
||||
veut savoir à quoi il sert (repris d'AJEAN, #104). */
|
||||
.gated-group.gated .help{pointer-events:auto}
|
||||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
/* Piste décochée : un gris franc (pas --border), sinon le pavé blanc ne se
|
||||
@@ -398,6 +401,17 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.preset-meta{display:flex;align-items:center;gap:6px;margin-top:3px;flex-wrap:wrap}
|
||||
.qtag{flex-shrink:0;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em;text-transform:uppercase}
|
||||
.rtag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
.ctag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
/* Pastille « capacités » (repris d'AJEAN) : vision (œil) et raisonnement
|
||||
(ampoule + niveau). Icônes en em : la bulle a la hauteur des autres tags. */
|
||||
.captag{flex-shrink:0;display:inline-flex;align-items:center;gap:4px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
.captag::before{content:"\200b";margin-right:-4px}
|
||||
.captag svg{display:block;width:1.3em;height:1.3em}
|
||||
/* Réordonnancement des presets (SortableJS) : clone plein qui suit le curseur,
|
||||
emplacement cible discret. */
|
||||
.preset-drag{cursor:grabbing;background:var(--row-bg-on);border:none;border-radius:6px;box-shadow:0 6px 18px rgba(0,0,0,.35);outline:none;opacity:1!important}
|
||||
.preset-ghost{opacity:.35}
|
||||
.preset-chosen{cursor:grabbing}
|
||||
/* Preset externe : le seul tag qui prend la couleur d'accent. Il ne décrit pas
|
||||
un réglage mais une DESTINATION — la réponse part sur internet, pas sur la
|
||||
carte graphique d'à côté, et ça mérite de se voir du premier coup d'œil.
|
||||
@@ -3446,6 +3460,11 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<span class="pe-row-l">Vision<span class="pe-sub">projecteur mmproj</span></span>
|
||||
<span class="pe-row-c"><span class="pe-selc"><select id="m-mmproj" onchange="onPickMmproj()"></select></span></span>
|
||||
</div>
|
||||
<!-- Repris d'AJEAN v0.10.3 : n'apparaît qu'avec un projecteur choisi. -->
|
||||
<div class="pe-row" id="s-mmproj-cpu-row" style="display:none">
|
||||
<span class="pe-row-l">Projecteur vision sur le CPU<span class="pe-sub">garde le mmproj hors du GPU — libère de la VRAM, images lues plus lentement (--no-mmproj-offload)</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-mmproj-cpu" onchange="eaToggleFlag('--no-mmproj-offload', this.checked)"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<!-- Recherche Hugging Face. Le champ « lien direct » juste en dessous
|
||||
reste là : c'est le mode expert (dépôt privé, fichier exotique,
|
||||
lien reçu d'ailleurs). Celui-ci évite d'avoir à quitter Loki, et
|
||||
@@ -3726,6 +3745,18 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt système DE CE preset (repris d'AJEAN v0.13.0) : il bascule avec
|
||||
le preset. Rangé en base (sysprompt:<id>), pas dans le .env qui ne
|
||||
porte qu'une ligne par clé. Vide = le prompt global s'applique. -->
|
||||
<div id="m-sys-row" class="pe-group" style="display:none">
|
||||
<div class="pe-gh">Prompt système</div>
|
||||
<div class="pe-list">
|
||||
<div class="pe-row stack">
|
||||
<textarea id="m-sysprompt" class="pe-inp" rows="4" spellcheck="false" placeholder="(optionnel) consigne propre à ce preset — vide = prompt système général"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="m-raw" class="pe-group">
|
||||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||||
<div class="pe-list">
|
||||
@@ -3842,7 +3873,8 @@ async function loadSys(){
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('loki.sys', d.text || ''); }
|
||||
}catch(e){}
|
||||
}
|
||||
// ===== Apparence =============================================================
|
||||
/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(tLine truncated
|
||||
// Un seul thème, en deux variantes : claire (par défaut) et sombre. La variante
|
||||
// est portée par l'attribut data-theme de <html> ('' ou 'dark') ; tout le reste
|
||||
// est du CSS (voir la section « Palette »). Persistée dans localStorage pour
|
||||
@@ -4587,7 +4619,9 @@ async function loadPresets(){
|
||||
const row=document.createElement('div');
|
||||
const pend = !x.active && pendingPreset===i+1;
|
||||
row.className='preset'+(x.active?' active':'')+(pend?' pending':'')+(moved?' sel-anim':'');
|
||||
row.onclick=()=>switchTo(i+1, x.name, x.id);
|
||||
row.dataset.id = x.id;
|
||||
// Fin d'un glisser-déposer : le clic qui le termine ne bascule pas le preset.
|
||||
row.onclick=()=>{ if(presetJustDragged) return; switchTo(i+1, x.name, x.id); };
|
||||
const info=document.createElement('div'); info.className='preset-info';
|
||||
const nm=document.createElement('div'); nm.className='preset-name';
|
||||
// Puce de l'actif : un ÉLÉMENT rond en CSS, pas le caractère « ● ». Le glyphe
|
||||
@@ -4615,12 +4649,27 @@ async function loadPresets(){
|
||||
q.textContent=x.quant; q.title='quantization';
|
||||
meta.appendChild(q);
|
||||
}
|
||||
if(x.reasoning){
|
||||
const rt=document.createElement('span'); rt.className='rtag';
|
||||
rt.title='raisonnement actif ('+x.reasoning+')';
|
||||
rt.innerHTML='<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1V18h6v-1.2c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2z"/></svg>';
|
||||
rt.appendChild(document.createTextNode(x.reasoning));
|
||||
meta.appendChild(rt);
|
||||
if(x.ctx && !x.external){
|
||||
const c=document.createElement('span'); c.className='ctag';
|
||||
c.title='taille de contexte'; c.textContent=x.ctx;
|
||||
meta.appendChild(c);
|
||||
}
|
||||
// Pastille « capacités » (repris d'AJEAN) : vision (œil) et raisonnement
|
||||
// (ampoule + niveau), regroupés en un seul tag.
|
||||
if(x.vision || x.reasoning){
|
||||
const cap=document.createElement('span'); cap.className='captag';
|
||||
const tip=[];
|
||||
if(x.vision){
|
||||
cap.insertAdjacentHTML('beforeend','<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>');
|
||||
tip.push('lit les images');
|
||||
}
|
||||
if(x.reasoning){
|
||||
cap.insertAdjacentHTML('beforeend','<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 21h4"/><path d="M12 3a6 6 0 0 0-4 10.5c.6.6 1 1.4 1 2.5h6c0-1.1.4-1.9 1-2.5A6 6 0 0 0 12 3z"/></svg>');
|
||||
cap.appendChild(document.createTextNode(x.reasoning));
|
||||
tip.push('raisonnement actif ('+x.reasoning+')');
|
||||
}
|
||||
cap.title=tip.join(' · ');
|
||||
meta.appendChild(cap);
|
||||
}
|
||||
if(x.bench){
|
||||
const bt=document.createElement('span'); bt.className='btag';
|
||||
@@ -4645,6 +4694,40 @@ async function loadPresets(){
|
||||
row.appendChild(info); row.appendChild(edit);
|
||||
cont.appendChild(row);
|
||||
});
|
||||
initPresetSortable(cont);
|
||||
}
|
||||
// ─── Réordonnancement des presets (SortableJS, repris d'AJEAN) ───────────────
|
||||
// La ligne entière se déplace, sans poignée. L'ordre est gardé côté serveur
|
||||
// (/api/presets/order) : il vaut aussi pour le sélecteur de l'en-tête.
|
||||
let presetJustDragged = false;
|
||||
let presetSortable = null;
|
||||
function initPresetSortable(cont){
|
||||
if(typeof Sortable==='undefined') return; // lib absente : la liste reste simple
|
||||
if(presetSortable) return; // une instance, sur le conteneur qui persiste
|
||||
presetSortable = Sortable.create(cont, {
|
||||
animation: 160,
|
||||
easing: 'cubic-bezier(.2,.7,.3,1)',
|
||||
// Clone géré par Sortable (et non l'image native, semi-transparente), posé
|
||||
// sur <body> : dans la modale (ancêtre transformé), il apparaissait loin
|
||||
// du curseur.
|
||||
forceFallback: true,
|
||||
fallbackTolerance: 3,
|
||||
fallbackOnBody: true,
|
||||
// Sur mobile, il faut MAINTENIR l'appui : un simple défilement attrapait un preset.
|
||||
delay: 220,
|
||||
delayOnTouchOnly: true,
|
||||
touchStartThreshold: 6,
|
||||
ghostClass: 'preset-ghost',
|
||||
chosenClass: 'preset-chosen',
|
||||
dragClass: 'preset-drag',
|
||||
onStart(){ presetJustDragged = true; },
|
||||
onEnd(){
|
||||
setTimeout(()=>{ presetJustDragged = false; }, 60);
|
||||
const ids=[...cont.children].map(r=>r.dataset.id).filter(Boolean);
|
||||
// Re-rendu une fois l'ordre persisté : les positions (pendingPreset) suivent.
|
||||
jpost('/api/presets/order', {ids}).then(()=>loadPresets()).catch(()=>{});
|
||||
},
|
||||
});
|
||||
}
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « loki » → « loki ».
|
||||
@@ -5313,6 +5396,10 @@ async function openItem(kind, key){
|
||||
const modelRow = document.getElementById('m-model-row');
|
||||
const settingsRow = document.getElementById('m-settings-row');
|
||||
const samplingRow = document.getElementById('m-sampling-row');
|
||||
const sysRow = document.getElementById('m-sys-row');
|
||||
if(sysRow) sysRow.style.display = kind === 'preset' ? '' : 'none';
|
||||
const sysTa = document.getElementById('m-sysprompt');
|
||||
if(sysTa) sysTa.value = '';
|
||||
const rawHead = document.getElementById('m-raw-head');
|
||||
const rawToggle = document.getElementById('m-raw-toggle');
|
||||
const rawBody = document.getElementById('m-raw-body');
|
||||
@@ -5371,6 +5458,7 @@ async function openItem(kind, key){
|
||||
document.getElementById('m-name').value = display;
|
||||
document.getElementById('m-content').value = d.content || '';
|
||||
if(kind === 'preset'){
|
||||
if(sysTa) sysTa.value = d.sysprompt || '';
|
||||
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
||||
document.getElementById('m-quant').value = currentQuantInTextarea();
|
||||
populateSettings();
|
||||
@@ -5452,6 +5540,10 @@ async function populateModelPicker(){
|
||||
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
||||
const groups = [];
|
||||
for(const m of (list||[])){
|
||||
// Projecteurs vision : ils ont leur propre sélecteur (Vision) et ne se
|
||||
// lancent pas comme modèle (repris d'AJEAN v0.16.2). Gardé seulement s'il
|
||||
// est DÉJÀ le MODEL du preset, pour ne pas masquer une erreur de saisie.
|
||||
if(isMmprojName(m.name) && m !== hit) continue;
|
||||
let g = groups.find(x => x.dir === m.dir);
|
||||
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
||||
g.items.push(m);
|
||||
@@ -5787,6 +5879,21 @@ function onPickMmproj(){
|
||||
// et on centralise le choix dans la clé MMPROJ.
|
||||
eaSetValued('--mmproj', '');
|
||||
cfgWriteKey('MMPROJ', document.getElementById('m-mmproj').value);
|
||||
syncMmprojCpuRow();
|
||||
}
|
||||
// « Projecteur sur le CPU » n'a de sens qu'avec un mmproj chargé : la ligne est
|
||||
// masquée sans vision. Source de vérité = la config, pas le select (rempli
|
||||
// APRÈS cette synchro à l'ouverture du preset). Sans projecteur, le drapeau
|
||||
// resté dans EXTRA_ARGS est retiré.
|
||||
function syncMmprojCpuRow(){
|
||||
const row = document.getElementById('s-mmproj-cpu-row');
|
||||
if(!row) return;
|
||||
const on = !!currentMmprojInTextarea();
|
||||
row.style.display = on ? '' : 'none';
|
||||
if(!on && eaHasFlag('--no-mmproj-offload')){
|
||||
eaToggleFlag('--no-mmproj-offload', false);
|
||||
const c = document.getElementById('s-mmproj-cpu'); if(c) c.checked = false;
|
||||
}
|
||||
}
|
||||
// Un projecteur multimodal se reconnaît à « mmproj » dans son nom : c'est la
|
||||
// convention de nommage universelle (llama.cpp, HF). On ne liste que ceux-là —
|
||||
@@ -6242,6 +6349,8 @@ function populateSettings(){
|
||||
}
|
||||
}
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mmproj-cpu', eaHasFlag('--no-mmproj-offload'));
|
||||
syncMmprojCpuRow();
|
||||
const lm = eaLoadMode();
|
||||
set('s-loadmode', lm);
|
||||
syncLoadModeSub(lm);
|
||||
@@ -6576,7 +6685,7 @@ async function saveItem(){
|
||||
// Presets: keyed by id (filename); duplicate display names are allowed.
|
||||
// Skills: keyed by name, rename via `old`.
|
||||
const payload = editingKind==='preset'
|
||||
? {id: editingKey, name, content}
|
||||
? {id: editingKey, name, content, sysprompt: (document.getElementById('m-sysprompt')||{}).value || ''}
|
||||
: {name, old: editingKey, content};
|
||||
const r = await jpost(K.saveUrl, payload);
|
||||
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
||||
@@ -10670,7 +10779,8 @@ function renderModelSwitch(presets, active){
|
||||
async function loadModelSwitchFiles(){
|
||||
try{
|
||||
const r = await jget('/api/models');
|
||||
MS_FILES = Array.isArray(r) ? r : [];
|
||||
// Projecteurs vision exclus : choisis comme modèle, le moteur ne démarre pas.
|
||||
MS_FILES = (Array.isArray(r) ? r : []).filter(m => !/mmproj/i.test(String(m.name||'')));
|
||||
}catch(_){ return; }
|
||||
paintModelSwitch();
|
||||
}
|
||||
|
||||
@@ -1189,6 +1189,11 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<span class="pe-row-l">Vision<span class="pe-sub">projecteur mmproj</span></span>
|
||||
<span class="pe-row-c"><span class="pe-selc"><select id="m-mmproj" onchange="onPickMmproj()"></select></span></span>
|
||||
</div>
|
||||
<!-- Repris d'AJEAN v0.10.3 : n'apparaît qu'avec un projecteur choisi. -->
|
||||
<div class="pe-row" id="s-mmproj-cpu-row" style="display:none">
|
||||
<span class="pe-row-l">Projecteur vision sur le CPU<span class="pe-sub">garde le mmproj hors du GPU — libère de la VRAM, images lues plus lentement (--no-mmproj-offload)</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-mmproj-cpu" onchange="eaToggleFlag('--no-mmproj-offload', this.checked)"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<!-- Recherche Hugging Face. Le champ « lien direct » juste en dessous
|
||||
reste là : c'est le mode expert (dépôt privé, fichier exotique,
|
||||
lien reçu d'ailleurs). Celui-ci évite d'avoir à quitter Loki, et
|
||||
@@ -1469,6 +1474,18 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt système DE CE preset (repris d'AJEAN v0.13.0) : il bascule avec
|
||||
le preset. Rangé en base (sysprompt:<id>), pas dans le .env qui ne
|
||||
porte qu'une ligne par clé. Vide = le prompt global s'applique. -->
|
||||
<div id="m-sys-row" class="pe-group" style="display:none">
|
||||
<div class="pe-gh">Prompt système</div>
|
||||
<div class="pe-list">
|
||||
<div class="pe-row stack">
|
||||
<textarea id="m-sysprompt" class="pe-inp" rows="4" spellcheck="false" placeholder="(optionnel) consigne propre à ce preset — vide = prompt système général"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="m-raw" class="pe-group">
|
||||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||||
<div class="pe-list">
|
||||
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
/*! Sortable 1.15.6 - MIT | git://github.com/SortableJS/Sortable.git */
|
||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t=t||self).Sortable=e()}(this,function(){"use strict";function e(e,t){var n,o=Object.keys(e);return Object.getOwnPropertySymbols&&(n=Object.getOwnPropertySymbols(e),t&&(n=n.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),o.push.apply(o,n)),o}function I(o){for(var t=1;t<arguments.length;t++){var i=null!=arguments[t]?arguments[t]:{};t%2?e(Object(i),!0).forEach(function(t){var e,n;e=o,t=i[n=t],n in e?Object.defineProperty(e,n,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[n]=t}):Object.getOwnPropertyDescriptors?Object.defineProperties(o,Object.getOwnPropertyDescriptors(i)):e(Object(i)).forEach(function(t){Object.defineProperty(o,t,Object.getOwnPropertyDescriptor(i,t))})}return o}function o(t){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function a(){return(a=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n,o=arguments[e];for(n in o)Object.prototype.hasOwnProperty.call(o,n)&&(t[n]=o[n])}return t}).apply(this,arguments)}function i(t,e){if(null==t)return{};var n,o=function(t,e){if(null==t)return{};for(var n,o={},i=Object.keys(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols)for(var i=Object.getOwnPropertySymbols(t),r=0;r<i.length;r++)n=i[r],0<=e.indexOf(n)||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n]);return o}function r(t){return function(t){if(Array.isArray(t))return l(t)}(t)||function(t){if("undefined"!=typeof Symbol&&null!=t[Symbol.iterator]||null!=t["@@iterator"])return Array.from(t)}(t)||function(t,e){if(t){if("string"==typeof t)return l(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);return"Map"===(n="Object"===n&&t.constructor?t.constructor.name:n)||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?l(t,e):void 0}}(t)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function l(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,o=new Array(e);n<e;n++)o[n]=t[n];return o}function t(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var y=t(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),w=t(/Edge/i),s=t(/firefox/i),u=t(/safari/i)&&!t(/chrome/i)&&!t(/android/i),c=t(/iP(ad|od|hone)/i),n=t(/chrome/i)&&t(/android/i),d={capture:!1,passive:!1};function h(t,e,n){t.addEventListener(e,n,!y&&d)}function p(t,e,n){t.removeEventListener(e,n,!y&&d)}function f(t,e){if(e&&(">"===e[0]&&(e=e.substring(1)),t))try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(t){return}}function g(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function P(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"!==e[0]||t.parentNode===n)&&f(t,e)||o&&t===n)return t}while(t!==n&&(t=g(t)))}return null}var m,v=/\s+/g;function k(t,e,n){var o;t&&e&&(t.classList?t.classList[n?"add":"remove"](e):(o=(" "+t.className+" ").replace(v," ").replace(" "+e+" "," "),t.className=(o+(n?" "+e:"")).replace(v," ")))}function R(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];o[e=!(e in o||-1!==e.indexOf("webkit"))?"-webkit-"+e:e]=n+("string"==typeof n?"":"px")}}function b(t,e){var n="";if("string"==typeof t)n=t;else do{var o=R(t,"transform")}while(o&&"none"!==o&&(n=o+" "+n),!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function D(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i<r;i++)n(o[i],i);return o}return[]}function O(){var t=document.scrollingElement;return t||document.documentElement}function X(t,e,n,o,i){if(t.getBoundingClientRect||t===window){var r,a,l,s,c,u,d=t!==window&&t.parentNode&&t!==O()?(a=(r=t.getBoundingClientRect()).top,l=r.left,s=r.bottom,c=r.right,u=r.height,r.width):(l=a=0,s=window.innerHeight,c=window.innerWidth,u=window.innerHeight,window.innerWidth);if((e||n)&&t!==window&&(i=i||t.parentNode,!y))do{if(i&&i.getBoundingClientRect&&("none"!==R(i,"transform")||n&&"static"!==R(i,"position"))){var h=i.getBoundingClientRect();a-=h.top+parseInt(R(i,"border-top-width")),l-=h.left+parseInt(R(i,"border-left-width")),s=a+r.height,c=l+r.width;break}}while(i=i.parentNode);return o&&t!==window&&(o=(e=b(i||t))&&e.a,t=e&&e.d,e&&(s=(a/=t)+(u/=t),c=(l/=o)+(d/=o))),{top:a,left:l,bottom:s,right:c,width:d,height:u}}}function Y(t,e,n){for(var o=M(tLine truncated
|
||||
@@ -60,7 +60,9 @@ async function loadPresets(){
|
||||
const row=document.createElement('div');
|
||||
const pend = !x.active && pendingPreset===i+1;
|
||||
row.className='preset'+(x.active?' active':'')+(pend?' pending':'')+(moved?' sel-anim':'');
|
||||
row.onclick=()=>switchTo(i+1, x.name, x.id);
|
||||
row.dataset.id = x.id;
|
||||
// Fin d'un glisser-déposer : le clic qui le termine ne bascule pas le preset.
|
||||
row.onclick=()=>{ if(presetJustDragged) return; switchTo(i+1, x.name, x.id); };
|
||||
const info=document.createElement('div'); info.className='preset-info';
|
||||
const nm=document.createElement('div'); nm.className='preset-name';
|
||||
// Puce de l'actif : un ÉLÉMENT rond en CSS, pas le caractère « ● ». Le glyphe
|
||||
@@ -88,12 +90,27 @@ async function loadPresets(){
|
||||
q.textContent=x.quant; q.title='quantization';
|
||||
meta.appendChild(q);
|
||||
}
|
||||
if(x.reasoning){
|
||||
const rt=document.createElement('span'); rt.className='rtag';
|
||||
rt.title='raisonnement actif ('+x.reasoning+')';
|
||||
rt.innerHTML='<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1V18h6v-1.2c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2z"/></svg>';
|
||||
rt.appendChild(document.createTextNode(x.reasoning));
|
||||
meta.appendChild(rt);
|
||||
if(x.ctx && !x.external){
|
||||
const c=document.createElement('span'); c.className='ctag';
|
||||
c.title='taille de contexte'; c.textContent=x.ctx;
|
||||
meta.appendChild(c);
|
||||
}
|
||||
// Pastille « capacités » (repris d'AJEAN) : vision (œil) et raisonnement
|
||||
// (ampoule + niveau), regroupés en un seul tag.
|
||||
if(x.vision || x.reasoning){
|
||||
const cap=document.createElement('span'); cap.className='captag';
|
||||
const tip=[];
|
||||
if(x.vision){
|
||||
cap.insertAdjacentHTML('beforeend','<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>');
|
||||
tip.push('lit les images');
|
||||
}
|
||||
if(x.reasoning){
|
||||
cap.insertAdjacentHTML('beforeend','<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 21h4"/><path d="M12 3a6 6 0 0 0-4 10.5c.6.6 1 1.4 1 2.5h6c0-1.1.4-1.9 1-2.5A6 6 0 0 0 12 3z"/></svg>');
|
||||
cap.appendChild(document.createTextNode(x.reasoning));
|
||||
tip.push('raisonnement actif ('+x.reasoning+')');
|
||||
}
|
||||
cap.title=tip.join(' · ');
|
||||
meta.appendChild(cap);
|
||||
}
|
||||
if(x.bench){
|
||||
const bt=document.createElement('span'); bt.className='btag';
|
||||
@@ -118,6 +135,40 @@ async function loadPresets(){
|
||||
row.appendChild(info); row.appendChild(edit);
|
||||
cont.appendChild(row);
|
||||
});
|
||||
initPresetSortable(cont);
|
||||
}
|
||||
// ─── Réordonnancement des presets (SortableJS, repris d'AJEAN) ───────────────
|
||||
// La ligne entière se déplace, sans poignée. L'ordre est gardé côté serveur
|
||||
// (/api/presets/order) : il vaut aussi pour le sélecteur de l'en-tête.
|
||||
let presetJustDragged = false;
|
||||
let presetSortable = null;
|
||||
function initPresetSortable(cont){
|
||||
if(typeof Sortable==='undefined') return; // lib absente : la liste reste simple
|
||||
if(presetSortable) return; // une instance, sur le conteneur qui persiste
|
||||
presetSortable = Sortable.create(cont, {
|
||||
animation: 160,
|
||||
easing: 'cubic-bezier(.2,.7,.3,1)',
|
||||
// Clone géré par Sortable (et non l'image native, semi-transparente), posé
|
||||
// sur <body> : dans la modale (ancêtre transformé), il apparaissait loin
|
||||
// du curseur.
|
||||
forceFallback: true,
|
||||
fallbackTolerance: 3,
|
||||
fallbackOnBody: true,
|
||||
// Sur mobile, il faut MAINTENIR l'appui : un simple défilement attrapait un preset.
|
||||
delay: 220,
|
||||
delayOnTouchOnly: true,
|
||||
touchStartThreshold: 6,
|
||||
ghostClass: 'preset-ghost',
|
||||
chosenClass: 'preset-chosen',
|
||||
dragClass: 'preset-drag',
|
||||
onStart(){ presetJustDragged = true; },
|
||||
onEnd(){
|
||||
setTimeout(()=>{ presetJustDragged = false; }, 60);
|
||||
const ids=[...cont.children].map(r=>r.dataset.id).filter(Boolean);
|
||||
// Re-rendu une fois l'ordre persisté : les positions (pendingPreset) suivent.
|
||||
jpost('/api/presets/order', {ids}).then(()=>loadPresets()).catch(()=>{});
|
||||
},
|
||||
});
|
||||
}
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « loki » → « loki ».
|
||||
|
||||
@@ -158,6 +158,10 @@ async function openItem(kind, key){
|
||||
const modelRow = document.getElementById('m-model-row');
|
||||
const settingsRow = document.getElementById('m-settings-row');
|
||||
const samplingRow = document.getElementById('m-sampling-row');
|
||||
const sysRow = document.getElementById('m-sys-row');
|
||||
if(sysRow) sysRow.style.display = kind === 'preset' ? '' : 'none';
|
||||
const sysTa = document.getElementById('m-sysprompt');
|
||||
if(sysTa) sysTa.value = '';
|
||||
const rawHead = document.getElementById('m-raw-head');
|
||||
const rawToggle = document.getElementById('m-raw-toggle');
|
||||
const rawBody = document.getElementById('m-raw-body');
|
||||
@@ -216,6 +220,7 @@ async function openItem(kind, key){
|
||||
document.getElementById('m-name').value = display;
|
||||
document.getElementById('m-content').value = d.content || '';
|
||||
if(kind === 'preset'){
|
||||
if(sysTa) sysTa.value = d.sysprompt || '';
|
||||
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
||||
document.getElementById('m-quant').value = currentQuantInTextarea();
|
||||
populateSettings();
|
||||
@@ -297,6 +302,10 @@ async function populateModelPicker(){
|
||||
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
||||
const groups = [];
|
||||
for(const m of (list||[])){
|
||||
// Projecteurs vision : ils ont leur propre sélecteur (Vision) et ne se
|
||||
// lancent pas comme modèle (repris d'AJEAN v0.16.2). Gardé seulement s'il
|
||||
// est DÉJÀ le MODEL du preset, pour ne pas masquer une erreur de saisie.
|
||||
if(isMmprojName(m.name) && m !== hit) continue;
|
||||
let g = groups.find(x => x.dir === m.dir);
|
||||
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
||||
g.items.push(m);
|
||||
@@ -632,6 +641,21 @@ function onPickMmproj(){
|
||||
// et on centralise le choix dans la clé MMPROJ.
|
||||
eaSetValued('--mmproj', '');
|
||||
cfgWriteKey('MMPROJ', document.getElementById('m-mmproj').value);
|
||||
syncMmprojCpuRow();
|
||||
}
|
||||
// « Projecteur sur le CPU » n'a de sens qu'avec un mmproj chargé : la ligne est
|
||||
// masquée sans vision. Source de vérité = la config, pas le select (rempli
|
||||
// APRÈS cette synchro à l'ouverture du preset). Sans projecteur, le drapeau
|
||||
// resté dans EXTRA_ARGS est retiré.
|
||||
function syncMmprojCpuRow(){
|
||||
const row = document.getElementById('s-mmproj-cpu-row');
|
||||
if(!row) return;
|
||||
const on = !!currentMmprojInTextarea();
|
||||
row.style.display = on ? '' : 'none';
|
||||
if(!on && eaHasFlag('--no-mmproj-offload')){
|
||||
eaToggleFlag('--no-mmproj-offload', false);
|
||||
const c = document.getElementById('s-mmproj-cpu'); if(c) c.checked = false;
|
||||
}
|
||||
}
|
||||
// Un projecteur multimodal se reconnaît à « mmproj » dans son nom : c'est la
|
||||
// convention de nommage universelle (llama.cpp, HF). On ne liste que ceux-là —
|
||||
@@ -1087,6 +1111,8 @@ function populateSettings(){
|
||||
}
|
||||
}
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mmproj-cpu', eaHasFlag('--no-mmproj-offload'));
|
||||
syncMmprojCpuRow();
|
||||
const lm = eaLoadMode();
|
||||
set('s-loadmode', lm);
|
||||
syncLoadModeSub(lm);
|
||||
@@ -1421,7 +1447,7 @@ async function saveItem(){
|
||||
// Presets: keyed by id (filename); duplicate display names are allowed.
|
||||
// Skills: keyed by name, rename via `old`.
|
||||
const payload = editingKind==='preset'
|
||||
? {id: editingKey, name, content}
|
||||
? {id: editingKey, name, content, sysprompt: (document.getElementById('m-sysprompt')||{}).value || ''}
|
||||
: {name, old: editingKey, content};
|
||||
const r = await jpost(K.saveUrl, payload);
|
||||
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
||||
|
||||
@@ -108,7 +108,8 @@ function renderModelSwitch(presets, active){
|
||||
async function loadModelSwitchFiles(){
|
||||
try{
|
||||
const r = await jget('/api/models');
|
||||
MS_FILES = Array.isArray(r) ? r : [];
|
||||
// Projecteurs vision exclus : choisis comme modèle, le moteur ne démarre pas.
|
||||
MS_FILES = (Array.isArray(r) ? r : []).filter(m => !/mmproj/i.test(String(m.name||'')));
|
||||
}catch(_){ return; }
|
||||
paintModelSwitch();
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ option,optgroup{background:var(--panel);color:var(--text)}
|
||||
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
||||
@media (min-width:721px){
|
||||
.side{width:380px}
|
||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag,.preset-meta .xtag{font-size:11px;padding:2px 9px;border-radius:4px}
|
||||
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag,.preset-meta .xtag,.preset-meta .ctag,.preset-meta .captag{font-size:11px;padding:2px 9px;border-radius:4px}
|
||||
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg,.preset-meta .xtag svg{width:11px;height:11px}
|
||||
}
|
||||
h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
@@ -266,6 +266,9 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
(étiquette comprise), plus sur la carte seule. */
|
||||
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
||||
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
||||
/* L'aide « ? » reste survolable sur un réglage grisé : c'est justement là qu'on
|
||||
veut savoir à quoi il sert (repris d'AJEAN, #104). */
|
||||
.gated-group.gated .help{pointer-events:auto}
|
||||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
/* Piste décochée : un gris franc (pas --border), sinon le pavé blanc ne se
|
||||
@@ -363,6 +366,17 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.preset-meta{display:flex;align-items:center;gap:6px;margin-top:3px;flex-wrap:wrap}
|
||||
.qtag{flex-shrink:0;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em;text-transform:uppercase}
|
||||
.rtag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
.ctag{flex-shrink:0;display:inline-flex;align-items:center;gap:3px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
/* Pastille « capacités » (repris d'AJEAN) : vision (œil) et raisonnement
|
||||
(ampoule + niveau). Icônes en em : la bulle a la hauteur des autres tags. */
|
||||
.captag{flex-shrink:0;display:inline-flex;align-items:center;gap:4px;font-size:9px;padding:1px 6px;border:1px solid var(--border);color:var(--dim);border-radius:4px;letter-spacing:.03em}
|
||||
.captag::before{content:"\200b";margin-right:-4px}
|
||||
.captag svg{display:block;width:1.3em;height:1.3em}
|
||||
/* Réordonnancement des presets (SortableJS) : clone plein qui suit le curseur,
|
||||
emplacement cible discret. */
|
||||
.preset-drag{cursor:grabbing;background:var(--row-bg-on);border:none;border-radius:6px;box-shadow:0 6px 18px rgba(0,0,0,.35);outline:none;opacity:1!important}
|
||||
.preset-ghost{opacity:.35}
|
||||
.preset-chosen{cursor:grabbing}
|
||||
/* Preset externe : le seul tag qui prend la couleur d'accent. Il ne décrit pas
|
||||
un réglage mais une DESTINATION — la réponse part sur internet, pas sur la
|
||||
carte graphique d'à côté, et ça mérite de se voir du premier coup d'œil.
|
||||
|
||||
@@ -441,9 +441,20 @@ func handlePresets(w http.ResponseWriter, r *http.Request) {
|
||||
if m := strings.TrimSpace(cfg[extKeyModel]); m != "" {
|
||||
item["model"] = m
|
||||
}
|
||||
if strings.TrimSpace(cfg[extKeyVision]) == "1" {
|
||||
item["vision"] = true
|
||||
}
|
||||
out = append(out, item)
|
||||
continue
|
||||
}
|
||||
// Pastilles de la liste : taille de contexte, et l'œil quand le preset
|
||||
// charge un projecteur vision (repris d'AJEAN v0.10.1 / v0.10.9).
|
||||
if c := presetCtx(content); c != "" {
|
||||
item["ctx"] = c
|
||||
}
|
||||
if strings.TrimSpace(parseEnv(content)["MMPROJ"]) != "" {
|
||||
item["vision"] = true
|
||||
}
|
||||
// Strata : réglages dans sa propre fenêtre, pas l'éditeur llama.cpp.
|
||||
if cfg := parseEnv(content); isStrataConfig(cfg) {
|
||||
item["strata"] = true
|
||||
@@ -481,6 +492,26 @@ func handlePresets(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, out)
|
||||
}
|
||||
|
||||
// handlePresetsOrder (POST {ids}) : l'ordre de la liste choisi par glisser-déposer.
|
||||
func handlePresetsOrder(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
sendJSON(w, 405, map[string]any{"ok": false, "error": "POST attendu"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
IDs []string `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := savePresetOrder(req.IDs); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
func handlePreset(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||
if id == "" {
|
||||
@@ -492,16 +523,20 @@ func handlePreset(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 404, map[string]any{"error": "not found"})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"id": id, "name": presetDisplayName(content, id), "content": content})
|
||||
sendJSON(w, 200, map[string]any{"id": id, "name": presetDisplayName(content, id), "content": content,
|
||||
"sysprompt": getStr(bkState, sysPromptKeyPrefix+id)})
|
||||
}
|
||||
|
||||
// presetSaveReq is the preset editor payload. `id` identifies an existing
|
||||
// preset to update ("" creates a new one); `name` is the display name.
|
||||
// SysPrompt : le prompt système DE CE preset, édité dans l'éditeur (repris
|
||||
// d'AJEAN v0.13.0) ; nil = champ absent, on n'y touche pas.
|
||||
type presetSaveReq struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
DeleteModel bool `json:"deleteModel"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
DeleteModel bool `json:"deleteModel"`
|
||||
SysPrompt *string `json:"sysprompt,omitempty"`
|
||||
}
|
||||
|
||||
// saveReq is the skill editor payload (skills keep name-as-identity + rename).
|
||||
@@ -604,6 +639,12 @@ func handlePresetSave(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.SysPrompt != nil {
|
||||
if err := putStr(bkState, sysPromptKeyPrefix+newID, strings.TrimSpace(*req.SysPrompt)); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if applied {
|
||||
fmt.Printf("%s config.env <- %s (preset actif modifié)\n", green("[ok]"), newID)
|
||||
fmt.Println(dim("[info] redémarrage du service..."))
|
||||
|
||||
@@ -262,6 +262,7 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/strata/install", handleStrataInstall) // job : paquet figé + installeur de Strata + preset (suivi par /api/llamacpp/job)
|
||||
api("/api/strata/settings", handleStrataSettings) // réglages d'un modèle Strata installé (relance s'il est actif)
|
||||
api("/api/presets", handlePresets)
|
||||
api("/api/presets/order", handlePresetsOrder) // ordre de la liste (glisser-déposer)
|
||||
api("/api/preset", handlePreset)
|
||||
api("/api/preset/save", handlePresetSave)
|
||||
api("/api/preset/cacheram", handlePresetCacheRAM) // aperçu de --cache-ram pour le preset édité
|
||||
|
||||
Reference in new issue
Block a user