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 })
|
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
|
// 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. */
|
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
||||||
@media (min-width:721px){
|
@media (min-width:721px){
|
||||||
.side{width:380px}
|
.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}
|
.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)}
|
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. */
|
(étiquette comprise), plus sur la carte seule. */
|
||||||
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
||||||
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
.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{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
.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
|
/* 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}
|
.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}
|
.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}
|
.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
|
/* 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
|
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.
|
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-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>
|
<span class="pe-row-c"><span class="pe-selc"><select id="m-mmproj" onchange="onPickMmproj()"></select></span></span>
|
||||||
</div>
|
</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
|
<!-- Recherche Hugging Face. Le champ « lien direct » juste en dessous
|
||||||
reste là : c'est le mode expert (dépôt privé, fichier exotique,
|
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
|
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>
|
||||||
</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 id="m-raw" class="pe-group">
|
||||||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||||||
<div class="pe-list">
|
<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 || ''); }
|
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('loki.sys', d.text || ''); }
|
||||||
}catch(e){}
|
}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
|
// 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 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
|
// 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 row=document.createElement('div');
|
||||||
const pend = !x.active && pendingPreset===i+1;
|
const pend = !x.active && pendingPreset===i+1;
|
||||||
row.className='preset'+(x.active?' active':'')+(pend?' pending':'')+(moved?' sel-anim':'');
|
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 info=document.createElement('div'); info.className='preset-info';
|
||||||
const nm=document.createElement('div'); nm.className='preset-name';
|
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
|
// 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';
|
q.textContent=x.quant; q.title='quantization';
|
||||||
meta.appendChild(q);
|
meta.appendChild(q);
|
||||||
}
|
}
|
||||||
if(x.reasoning){
|
if(x.ctx && !x.external){
|
||||||
const rt=document.createElement('span'); rt.className='rtag';
|
const c=document.createElement('span'); c.className='ctag';
|
||||||
rt.title='raisonnement actif ('+x.reasoning+')';
|
c.title='taille de contexte'; c.textContent=x.ctx;
|
||||||
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>';
|
meta.appendChild(c);
|
||||||
rt.appendChild(document.createTextNode(x.reasoning));
|
}
|
||||||
meta.appendChild(rt);
|
// 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){
|
if(x.bench){
|
||||||
const bt=document.createElement('span'); bt.className='btag';
|
const bt=document.createElement('span'); bt.className='btag';
|
||||||
@@ -4645,6 +4694,40 @@ async function loadPresets(){
|
|||||||
row.appendChild(info); row.appendChild(edit);
|
row.appendChild(info); row.appendChild(edit);
|
||||||
cont.appendChild(row);
|
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.
|
// « 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 ».
|
// 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 modelRow = document.getElementById('m-model-row');
|
||||||
const settingsRow = document.getElementById('m-settings-row');
|
const settingsRow = document.getElementById('m-settings-row');
|
||||||
const samplingRow = document.getElementById('m-sampling-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 rawHead = document.getElementById('m-raw-head');
|
||||||
const rawToggle = document.getElementById('m-raw-toggle');
|
const rawToggle = document.getElementById('m-raw-toggle');
|
||||||
const rawBody = document.getElementById('m-raw-body');
|
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-name').value = display;
|
||||||
document.getElementById('m-content').value = d.content || '';
|
document.getElementById('m-content').value = d.content || '';
|
||||||
if(kind === 'preset'){
|
if(kind === 'preset'){
|
||||||
|
if(sysTa) sysTa.value = d.sysprompt || '';
|
||||||
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
||||||
document.getElementById('m-quant').value = currentQuantInTextarea();
|
document.getElementById('m-quant').value = currentQuantInTextarea();
|
||||||
populateSettings();
|
populateSettings();
|
||||||
@@ -5452,6 +5540,10 @@ async function populateModelPicker(){
|
|||||||
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
||||||
const groups = [];
|
const groups = [];
|
||||||
for(const m of (list||[])){
|
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);
|
let g = groups.find(x => x.dir === m.dir);
|
||||||
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
||||||
g.items.push(m);
|
g.items.push(m);
|
||||||
@@ -5787,6 +5879,21 @@ function onPickMmproj(){
|
|||||||
// et on centralise le choix dans la clé MMPROJ.
|
// et on centralise le choix dans la clé MMPROJ.
|
||||||
eaSetValued('--mmproj', '');
|
eaSetValued('--mmproj', '');
|
||||||
cfgWriteKey('MMPROJ', document.getElementById('m-mmproj').value);
|
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
|
// 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à —
|
// 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-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||||
|
chk('s-mmproj-cpu', eaHasFlag('--no-mmproj-offload'));
|
||||||
|
syncMmprojCpuRow();
|
||||||
const lm = eaLoadMode();
|
const lm = eaLoadMode();
|
||||||
set('s-loadmode', lm);
|
set('s-loadmode', lm);
|
||||||
syncLoadModeSub(lm);
|
syncLoadModeSub(lm);
|
||||||
@@ -6576,7 +6685,7 @@ async function saveItem(){
|
|||||||
// Presets: keyed by id (filename); duplicate display names are allowed.
|
// Presets: keyed by id (filename); duplicate display names are allowed.
|
||||||
// Skills: keyed by name, rename via `old`.
|
// Skills: keyed by name, rename via `old`.
|
||||||
const payload = editingKind==='preset'
|
const payload = editingKind==='preset'
|
||||||
? {id: editingKey, name, content}
|
? {id: editingKey, name, content, sysprompt: (document.getElementById('m-sysprompt')||{}).value || ''}
|
||||||
: {name, old: editingKey, content};
|
: {name, old: editingKey, content};
|
||||||
const r = await jpost(K.saveUrl, payload);
|
const r = await jpost(K.saveUrl, payload);
|
||||||
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
||||||
@@ -10670,7 +10779,8 @@ function renderModelSwitch(presets, active){
|
|||||||
async function loadModelSwitchFiles(){
|
async function loadModelSwitchFiles(){
|
||||||
try{
|
try{
|
||||||
const r = await jget('/api/models');
|
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; }
|
}catch(_){ return; }
|
||||||
paintModelSwitch();
|
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-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>
|
<span class="pe-row-c"><span class="pe-selc"><select id="m-mmproj" onchange="onPickMmproj()"></select></span></span>
|
||||||
</div>
|
</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
|
<!-- Recherche Hugging Face. Le champ « lien direct » juste en dessous
|
||||||
reste là : c'est le mode expert (dépôt privé, fichier exotique,
|
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
|
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>
|
||||||
</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 id="m-raw" class="pe-group">
|
||||||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||||||
<div class="pe-list">
|
<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 row=document.createElement('div');
|
||||||
const pend = !x.active && pendingPreset===i+1;
|
const pend = !x.active && pendingPreset===i+1;
|
||||||
row.className='preset'+(x.active?' active':'')+(pend?' pending':'')+(moved?' sel-anim':'');
|
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 info=document.createElement('div'); info.className='preset-info';
|
||||||
const nm=document.createElement('div'); nm.className='preset-name';
|
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
|
// 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';
|
q.textContent=x.quant; q.title='quantization';
|
||||||
meta.appendChild(q);
|
meta.appendChild(q);
|
||||||
}
|
}
|
||||||
if(x.reasoning){
|
if(x.ctx && !x.external){
|
||||||
const rt=document.createElement('span'); rt.className='rtag';
|
const c=document.createElement('span'); c.className='ctag';
|
||||||
rt.title='raisonnement actif ('+x.reasoning+')';
|
c.title='taille de contexte'; c.textContent=x.ctx;
|
||||||
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>';
|
meta.appendChild(c);
|
||||||
rt.appendChild(document.createTextNode(x.reasoning));
|
}
|
||||||
meta.appendChild(rt);
|
// 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){
|
if(x.bench){
|
||||||
const bt=document.createElement('span'); bt.className='btag';
|
const bt=document.createElement('span'); bt.className='btag';
|
||||||
@@ -118,6 +135,40 @@ async function loadPresets(){
|
|||||||
row.appendChild(info); row.appendChild(edit);
|
row.appendChild(info); row.appendChild(edit);
|
||||||
cont.appendChild(row);
|
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.
|
// « 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 ».
|
// 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 modelRow = document.getElementById('m-model-row');
|
||||||
const settingsRow = document.getElementById('m-settings-row');
|
const settingsRow = document.getElementById('m-settings-row');
|
||||||
const samplingRow = document.getElementById('m-sampling-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 rawHead = document.getElementById('m-raw-head');
|
||||||
const rawToggle = document.getElementById('m-raw-toggle');
|
const rawToggle = document.getElementById('m-raw-toggle');
|
||||||
const rawBody = document.getElementById('m-raw-body');
|
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-name').value = display;
|
||||||
document.getElementById('m-content').value = d.content || '';
|
document.getElementById('m-content').value = d.content || '';
|
||||||
if(kind === 'preset'){
|
if(kind === 'preset'){
|
||||||
|
if(sysTa) sysTa.value = d.sysprompt || '';
|
||||||
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
// Ces deux-là LISENT le contenu : elles doivent passer après son arrivée.
|
||||||
document.getElementById('m-quant').value = currentQuantInTextarea();
|
document.getElementById('m-quant').value = currentQuantInTextarea();
|
||||||
populateSettings();
|
populateSettings();
|
||||||
@@ -297,6 +302,10 @@ async function populateModelPicker(){
|
|||||||
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
// regroupe par dossier, celui de loki d'abord (l'API les renvoie dans cet ordre).
|
||||||
const groups = [];
|
const groups = [];
|
||||||
for(const m of (list||[])){
|
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);
|
let g = groups.find(x => x.dir === m.dir);
|
||||||
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
if(!g){ g = {dir: m.dir, home: m.home, items: []}; groups.push(g); }
|
||||||
g.items.push(m);
|
g.items.push(m);
|
||||||
@@ -632,6 +641,21 @@ function onPickMmproj(){
|
|||||||
// et on centralise le choix dans la clé MMPROJ.
|
// et on centralise le choix dans la clé MMPROJ.
|
||||||
eaSetValued('--mmproj', '');
|
eaSetValued('--mmproj', '');
|
||||||
cfgWriteKey('MMPROJ', document.getElementById('m-mmproj').value);
|
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
|
// 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à —
|
// 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-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||||
|
chk('s-mmproj-cpu', eaHasFlag('--no-mmproj-offload'));
|
||||||
|
syncMmprojCpuRow();
|
||||||
const lm = eaLoadMode();
|
const lm = eaLoadMode();
|
||||||
set('s-loadmode', lm);
|
set('s-loadmode', lm);
|
||||||
syncLoadModeSub(lm);
|
syncLoadModeSub(lm);
|
||||||
@@ -1421,7 +1447,7 @@ async function saveItem(){
|
|||||||
// Presets: keyed by id (filename); duplicate display names are allowed.
|
// Presets: keyed by id (filename); duplicate display names are allowed.
|
||||||
// Skills: keyed by name, rename via `old`.
|
// Skills: keyed by name, rename via `old`.
|
||||||
const payload = editingKind==='preset'
|
const payload = editingKind==='preset'
|
||||||
? {id: editingKey, name, content}
|
? {id: editingKey, name, content, sysprompt: (document.getElementById('m-sysprompt')||{}).value || ''}
|
||||||
: {name, old: editingKey, content};
|
: {name, old: editingKey, content};
|
||||||
const r = await jpost(K.saveUrl, payload);
|
const r = await jpost(K.saveUrl, payload);
|
||||||
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
if(!r.ok){ toast('erreur : ' + (r.error||'')); return; }
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ function renderModelSwitch(presets, active){
|
|||||||
async function loadModelSwitchFiles(){
|
async function loadModelSwitchFiles(){
|
||||||
try{
|
try{
|
||||||
const r = await jget('/api/models');
|
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; }
|
}catch(_){ return; }
|
||||||
paintModelSwitch();
|
paintModelSwitch();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ option,optgroup{background:var(--panel);color:var(--text)}
|
|||||||
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
/* Desktop : menu plus large et bulles (badges) un peu plus grosses. */
|
||||||
@media (min-width:721px){
|
@media (min-width:721px){
|
||||||
.side{width:380px}
|
.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}
|
.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)}
|
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. */
|
(étiquette comprise), plus sur la carte seule. */
|
||||||
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
.gated-group{margin:0 0 10px;transition:opacity .15s}
|
||||||
.gated-group.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
.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{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||||
.switch input{position:absolute;opacity:0;width:0;height:0}
|
.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
|
/* 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}
|
.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}
|
.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}
|
.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
|
/* 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
|
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.
|
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 != "" {
|
if m := strings.TrimSpace(cfg[extKeyModel]); m != "" {
|
||||||
item["model"] = m
|
item["model"] = m
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(cfg[extKeyVision]) == "1" {
|
||||||
|
item["vision"] = true
|
||||||
|
}
|
||||||
out = append(out, item)
|
out = append(out, item)
|
||||||
continue
|
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.
|
// Strata : réglages dans sa propre fenêtre, pas l'éditeur llama.cpp.
|
||||||
if cfg := parseEnv(content); isStrataConfig(cfg) {
|
if cfg := parseEnv(content); isStrataConfig(cfg) {
|
||||||
item["strata"] = true
|
item["strata"] = true
|
||||||
@@ -481,6 +492,26 @@ func handlePresets(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 200, out)
|
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) {
|
func handlePreset(w http.ResponseWriter, r *http.Request) {
|
||||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
||||||
if id == "" {
|
if id == "" {
|
||||||
@@ -492,16 +523,20 @@ func handlePreset(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 404, map[string]any{"error": "not found"})
|
sendJSON(w, 404, map[string]any{"error": "not found"})
|
||||||
return
|
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
|
// presetSaveReq is the preset editor payload. `id` identifies an existing
|
||||||
// preset to update ("" creates a new one); `name` is the display name.
|
// 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 {
|
type presetSaveReq struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
DeleteModel bool `json:"deleteModel"`
|
DeleteModel bool `json:"deleteModel"`
|
||||||
|
SysPrompt *string `json:"sysprompt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveReq is the skill editor payload (skills keep name-as-identity + rename).
|
// 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()})
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||||
return
|
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 {
|
if applied {
|
||||||
fmt.Printf("%s config.env <- %s (preset actif modifié)\n", green("[ok]"), newID)
|
fmt.Printf("%s config.env <- %s (preset actif modifié)\n", green("[ok]"), newID)
|
||||||
fmt.Println(dim("[info] redémarrage du service..."))
|
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/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/strata/settings", handleStrataSettings) // réglages d'un modèle Strata installé (relance s'il est actif)
|
||||||
api("/api/presets", handlePresets)
|
api("/api/presets", handlePresets)
|
||||||
|
api("/api/presets/order", handlePresetsOrder) // ordre de la liste (glisser-déposer)
|
||||||
api("/api/preset", handlePreset)
|
api("/api/preset", handlePreset)
|
||||||
api("/api/preset/save", handlePresetSave)
|
api("/api/preset/save", handlePresetSave)
|
||||||
api("/api/preset/cacheram", handlePresetCacheRAM) // aperçu de --cache-ram pour le preset édité
|
api("/api/preset/cacheram", handlePresetCacheRAM) // aperçu de --cache-ram pour le preset édité
|
||||||
|
|||||||
Reference in new issue
Block a user