This commit is contained in:
pporcheret
2026-10-31 11:04:45 +01:00
parent 90ffa635a0
commit 2c5537aff1
53 changed files with 7950 additions and 22 deletions
+61
View File
@@ -0,0 +1,61 @@
/* DONA_FILES_V91 — documents et images, contenu affiché exclusivement avec textContent. */
(()=>{
'use strict';
if(window.DonaFilesV91)return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!==undefined)n.textContent=String(text);return n;};
const button=(text,fn,cls='df-button')=>{const b=el('button',cls,text);b.type='button';b.addEventListener('click',fn);return b;};
let active=null, busy=false, previewURL=null;
function status(text){if($('dfStatus'))$('dfStatus').textContent=String(text||'');}
function setBusy(val){busy=val;for(const id of ['dfUpload','dfAnalyze','dfDrop','dfDelete']){const n=$(id);if(n)n.disabled=val;}}
async function postJson(path,data){const response=await fetch(path,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(data)});const obj=await response.json().catch(()=>({}));if(!response.ok||obj.ok===false)throw Error(obj.error||'Erreur HTTP '+response.status);return obj;}
function open(){if(!$('donaFilesV91'))init();$('donaFilesV91').hidden=false;document.body.classList.add('df-open');}
function close(){if(busy)return;$('donaFilesV91').hidden=true;document.body.classList.remove('df-open');}
function showAttachment(info,file){active=info;$('dfName').textContent=info.filename+' · '+(Math.round(info.size/1024))+' Ko';$('dfCurrent').hidden=false;$('dfEmpty').hidden=true;$('dfAnswer').textContent='';$('dfQuestion').value=info.kind==='image'?'Que vois-tu sur cette image ?':'Fais un résumé clair de ce document.';
if(previewURL){URL.revokeObjectURL(previewURL);previewURL=null;}const picture=$('dfPreview');picture.hidden=true;picture.removeAttribute('src');
if(info.kind==='image'&&file){previewURL=URL.createObjectURL(file);picture.src=previewURL;picture.hidden=false;}status('Fichier prêt. Tu peux demander une analyse.');}
async function upload(file){if(!file||busy)return;if(file.size>10*1024*1024){status('Taille maximale : 10 Mo.');return;}setBusy(true);status('Téléversement…');try{const form=new FormData();form.append('file',file,file.name);const response=await fetch('/api/dona/files/upload',{method:'POST',credentials:'same-origin',headers:{'X-Alfred-Request':'1'},body:form});const obj=await response.json().catch(()=>({}));if(!response.ok||obj.ok===false)throw Error(obj.error||'Téléversement impossible.');const previous=active;showAttachment(obj,file);if(previous&&previous.id!==obj.id)postJson('/api/dona/files/delete',{id:previous.id}).catch(()=>{});}catch(e){status('Erreur : '+e.message);}finally{setBusy(false);}}
async function analyze(){if(!active||busy){status('Ajoute d’abord un fichier.');return;}const question=String($('dfQuestion').value||'').trim();if(!question){status('Saisis une question.');return;}setBusy(true);status('Hermes analyse le fichier…');$('dfAnswer').textContent='';try{const result=await postJson('/api/dona/files/analyze',{id:active.id,question});$('dfAnswer').textContent=result.answer||'Aucune réponse.';status('Analyse terminée.');}catch(e){status('Erreur : '+e.message);}finally{setBusy(false);}}
async function remove(){if(!active||busy)return;setBusy(true);try{await postJson('/api/dona/files/delete',{id:active.id});active=null;$('dfCurrent').hidden=true;$('dfEmpty').hidden=false;$('dfAnswer').textContent='';$('dfName').textContent='';$('dfInput').value='';status('Fichier effacé.');if(previewURL){URL.revokeObjectURL(previewURL);previewURL=null;}}catch(e){status(e.message);}finally{setBusy(false);}}
function init(){if($('donaFilesV91'))return;const overlay=el('div','df-overlay');overlay.id='donaFilesV91';overlay.hidden=true;const panel=el('section','df-panel');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Analyser un fichier avec Dona');
const head=el('div','df-head');const heading=el('div');heading.append(el('span','df-eyebrow','DONA · PIÈCES JOINTES'),el('h2','','Analyser un document ou une photo'));head.append(heading,button('×',close,'df-close'));panel.append(head);
const help=el('p','df-hint','PDF, Word (.docx), texte, Markdown, JPEG, PNG et WebP · 10 Mo max · Conservation temporaire 30 minutes · Pas d’indexation automatique.');panel.append(help);
const zone=el('div','df-drop');zone.id='dfDrop';zone.setAttribute('role','group');zone.append(el('span','df-upload-icon','📎'),el('strong','','Dépose ton fichier ici ou sélectionne-le'));
const fileInput=el('input','df-file');fileInput.type='file';fileInput.id='dfInput';fileInput.accept='.pdf,.docx,.txt,.md,.jpg,.jpeg,.png,.webp';fileInput.addEventListener('change',()=>{if(fileInput.files?.[0])upload(fileInput.files[0]);});zone.append(fileInput);
const choose=button('Choisir un fichier',()=>fileInput.click(),'df-button df-primary');choose.id='dfUpload';zone.append(choose);zone.addEventListener('dragover',e=>{e.preventDefault();zone.classList.add('df-over');});zone.addEventListener('dragleave',()=>zone.classList.remove('df-over'));zone.addEventListener('drop',e=>{e.preventDefault();zone.classList.remove('df-over');if(e.dataTransfer.files?.[0])upload(e.dataTransfer.files[0]);});panel.append(zone);
const empty=el('p','df-hint','Aucun fichier sélectionné.');empty.id='dfEmpty';panel.append(empty);
const current=el('div','df-current');current.id='dfCurrent';current.hidden=true;const filename=el('div','df-filename');filename.id='dfName';const preview=el('img','df-preview');preview.id='dfPreview';preview.alt='Aperçu du fichier image sélectionné';preview.hidden=true;current.append(filename,preview);const del=button('Supprimer cette pièce jointe',remove,'df-button df-del');del.id='dfDelete';current.append(del);panel.append(current);
const label=el('label','df-label','Que veux-tu savoir ?');label.setAttribute('for','dfQuestion');panel.append(label);const question=el('textarea','df-question');question.id='dfQuestion';question.rows=3;question.maxLength=1200;question.placeholder='Résume ce document, explique les points importants, analyse cette photo…';panel.append(question);
const examples=el('div','df-examples');[['Résumé','Fais un résumé clair de ce document.'],['Points clés','Quels sont les points importants à retenir ?'],['Analyser','Analyse ce fichier et explique-moi ce qu’il contient.'],['Explication','Explique le contenu simplement.']].forEach(([name,q])=>examples.append(button(name,()=>{question.value=q;},'df-chip')));panel.append(examples);
const run=button('Analyser avec Hermes',analyze,'df-button df-primary df-run');run.id='dfAnalyze';panel.append(run);
const msg=el('p','df-status');msg.id='dfStatus';msg.setAttribute('role','status');panel.append(msg);
const result=el('div','df-answer');result.id='dfAnswer';result.setAttribute('aria-live','polite');panel.append(result);
panel.append(el('p','df-hint','Dona garde les fichiers en RAM 30 min ; Hermes peut conserver des traces de session. Le fournisseur IA utilisé dépend de la configuration Hermes. Vérifie-la avant d’envoyer des documents confidentiels. Les PDF scannés sans texte ne sont pas encore pris en charge.'));
overlay.append(panel);document.body.append(overlay);overlay.addEventListener('click',e=>{if(e.target===overlay)close();});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!overlay.hidden)close();});
// DONA_FILES_V911_COMPOSE : 📎 à droite du bouton d'envoi de « Écrire à Dona ».
// Bouton de type button pour ne jamais soumettre le formulaire lors de son activation.
function mountComposeAttachment(){
const send=$('donaComposeSend');
if(!send || !send.closest('.dona-compose-row'))return false;
let launch=$('dfOpen');
if(!launch){launch=button('📎',open,'df-compose-attach');launch.id='dfOpen';}
launch.type='button';
launch.classList.remove('av-round','df-launch');
launch.classList.add('df-compose-attach');
launch.title='Joindre un fichier à Dona';
launch.setAttribute('aria-label','Joindre un document ou une photo');
send.insertAdjacentElement('afterend',launch);
return true;
}
if(!mountComposeAttachment()){
let count=0;
const timer=setInterval(()=>{
if(mountComposeAttachment() || ++count>100)clearInterval(timer);
},150);
}
const v9quick=document.querySelector('#donaSuiteV9 .dv9-quick');if(v9quick)v9quick.append(button('📎 Analyser un fichier',()=>{if(window.DonaSuiteV9?.close)window.DonaSuiteV9.close();open();},'dv9-chip'));
window.alfredSocket?.on('dona_files_v91_result',result=>{if(result?.answer&&active?.id===result.id){$('dfAnswer').textContent=result.answer;status('Analyse vocale terminée.');}});
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
window.DonaFilesV91={open,close};
})();
@@ -0,0 +1,82 @@
/* DONA_LEARNING_V93 : feedback explicite, corrections modérées, aucun entraînement implicite. */
(()=>{
'use strict';
if(window.DonaLearningV93)return;
const el=(tag,cls,txt)=>{const e=document.createElement(tag);if(cls)e.className=cls;if(txt!==undefined)e.textContent=String(txt);return e;};
const btn=(label,click,cls='dl93-button')=>{const b=el('button',cls,label);b.type='button';b.addEventListener('click',click);return b;};
const field=(label,input)=>{const x=el('label','dl93-field');x.append(el('span','',label),input);return x;};
const txt=(text,n=120)=>{const s=String(text||'');return s.length>n?s.slice(0,n)+'…':s;};
const api=async(url,body)=>{const o=body===undefined?{cache:'no-store',credentials:'same-origin'}:{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},credentials:'same-origin',body:JSON.stringify(body)};const r=await fetch(url,o);const data=await r.json().catch(()=>({}));if(!r.ok||!data.ok)throw Error(data.error||'Erreur HTTP '+r.status);return data;};
let last=null,dock,panel,dialog,menuTick=0,socketBound=false,submitting=false;
function notify(message,bad=false){const n=document.getElementById('dl93Status');if(n){n.textContent=message||'';n.classList.toggle('dl93-error',bad);}}
function showDock(){if(!last||!last.token)return;dock.hidden=false;dock.querySelector('.dl93-context').textContent=txt(last.heard||'Dernière demande',90);}
function setTurn(d){if(!d||!d.token)return;last={token:d.token,heard:String(d.heard||''),answer:String(d.answer||''),source:d.source||'voice'};showDock();notify('');}
function labelBtn(name,fn){return btn(name,fn,'dl93-tool');}
function createDock(){dock=el('div','dl93-dock');dock.id='dl93Dock';dock.hidden=true;dock.setAttribute('role','region');dock.setAttribute('aria-label','Évaluer la dernière réponse de Dona');
dock.append(el('span','dl93-context','Dernière réponse de Dona'),labelBtn('👍',()=>rateUp()),labelBtn('👎',()=>openCorrection()),labelBtn('Historique',()=>openPanel()));
const close=labelBtn('×',()=>{dock.hidden=true;});close.title='Masquer les évaluations';close.setAttribute('aria-label','Masquer les évaluations');dock.append(close);document.body.append(dock);
}
async function rateUp(){if(!last||submitting)return;submitting=true;try{await api('/api/dona/v93/learning/feedback',{token:last.token,rating:'up'});dock.hidden=true;last=null;openToast('👍 Réponse enregistrée.');}catch(e){openToast(e.message,true);}finally{submitting=false;}}
function openToast(message,bad=false){let box=document.getElementById('dl93Toast');if(!box){box=el('div','dl93-toast');box.id='dl93Toast';document.body.append(box);}box.textContent=message;box.classList.toggle('dl93-error',bad);box.hidden=false;setTimeout(()=>{box.hidden=true;},5500);}
function makeDialog(){dialog=el('div','dl93-overlay');dialog.id='dl93Correction';dialog.hidden=true;dialog.setAttribute('role','dialog');dialog.setAttribute('aria-modal','true');dialog.setAttribute('aria-label','Corriger une réponse Dona');
const sheet=el('div','dl93-sheet');
const h=el('div','dl93-top');h.append(el('h3','', 'Corriger Dona'),btn('×',()=>closeCorrection(),'dl93-close'));sheet.append(h);
const summary=el('div','dl93-summary');summary.id='dl93Summary';sheet.append(summary);
const kind=document.createElement('select');kind.id='dl93Kind';[['transcription','Vosk a mal transcrit'],['comprehension','Hermes a mal compris'],['execution','Mauvaise action / erreur d’outil']].forEach(([v,l])=>{const opt=el('option','',l);opt.value=v;kind.append(opt);});
sheet.append(field('Type d’erreur',kind));
const correction=document.createElement('textarea');correction.id='dl93CorrectionText';correction.maxLength=1800;correction.rows=4;
sheet.append(field('Ce que j’ai dit / ce qui était attendu',correction));
const explanation=document.createElement('textarea');explanation.id='dl93Note';explanation.rows=2;explanation.maxLength=800;explanation.placeholder='Précision facultative';sheet.append(field('Commentaire',explanation));
const note=el('p','dl93-hint','La correction restera en attente dans Apprentissage. Après validation : Vosk utilisera la correction pour la même phrase exacte ; Hermes recevra une indication pour cette demande précise. Aucune commande ne sera relancée automatiquement.');sheet.append(note);
kind.addEventListener('change',()=>{correction.placeholder=kind.value==='transcription'?'Écris exactement les mots que tu avais prononcés…':kind.value==='comprehension'?'Explique ce que Dona devait comprendre / faire…':'Indique la bonne action ou l’équipement visé…';note.textContent=kind.value==='execution'?'Ce signalement sera conservé dans l’historique. Aucune règle d’exécution ne sera appliquée automatiquement.':'La correction restera en attente jusqu’à sa validation dans Apprentissage. Seules les formulations identiques seront corrigées.';});
const actions=el('div','dl93-actions');actions.append(btn('Annuler',closeCorrection,'dl93-button dl93-neutral'),btn('Enregistrer 👎',submitBad));sheet.append(actions);
const state=el('p','dl93-state');state.id='dl93CorrectionStatus';state.setAttribute('role','status');sheet.append(state);
dialog.append(sheet);document.body.append(dialog);dialog.addEventListener('pointerdown',e=>{if(e.target===dialog)closeCorrection();});
}
function openCorrection(){if(!last){openToast('Aucune réponse récente.');return;}dialog.hidden=false;const raw=last.heard||'Transcription indisponible';document.getElementById('dl93Summary').replaceChildren(el('p','','Dona a entendu : '+txt(raw,500)),el('p','','Réponse : '+txt(last.answer,500)));const kind=document.getElementById('dl93Kind');kind.value='transcription';const c=document.getElementById('dl93CorrectionText');c.value='';c.placeholder='Écris exactement les mots que tu avais prononcés…';document.getElementById('dl93Note').value='';document.getElementById('dl93CorrectionStatus').textContent='';kind.dispatchEvent(new Event('change'));c.focus();}
function closeCorrection(){if(dialog)dialog.hidden=true;}
async function submitBad(){if(!last||submitting)return;const kind=document.getElementById('dl93Kind').value;const correction=document.getElementById('dl93CorrectionText').value;const note=document.getElementById('dl93Note').value;const error=document.getElementById('dl93CorrectionStatus');submitting=true;
try{await api('/api/dona/v93/learning/feedback',{token:last.token,rating:'down',kind,correction,note});closeCorrection();last=null;dock.hidden=true;openToast('👎 Correction enregistrée. Valide-la dans Apprentissage pour l’activer.');}
catch(e){error.textContent=e.message;error.classList.add('dl93-error');}
finally{submitting=false;}}
function makePanel(){panel=el('div','dl93-overlay');panel.id='dl93Panel';panel.hidden=true;panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Historique des corrections Dona');
const sheet=el('div','dl93-sheet dl93-history');const top=el('div','dl93-top');top.append(el('h3','','Apprentissage de Dona'),btn('×',closePanel,'dl93-close'));sheet.append(top);
const intro=el('p','dl93-hint','Historique local des réponses évaluées. Aucun son enregistré. Les règles doivent être approuvées manuellement et peuvent être retirées à tout moment.');sheet.append(intro);
const content=el('div','dl93-panel-body');content.id='dl93PanelContent';sheet.append(content);
const foot=el('div','dl93-actions');foot.append(btn('Actualiser',loadPanel,'dl93-button dl93-neutral'),btn('Fermer',closePanel));sheet.append(foot);
const state=el('p','dl93-state');state.id='dl93Status';state.setAttribute('role','status');sheet.append(state);panel.append(sheet);document.body.append(panel);panel.addEventListener('pointerdown',e=>{if(e.target===panel)closePanel();});
}
async function openPanel(){panel.hidden=false;await loadPanel();}
function closePanel(){if(panel)panel.hidden=true;}
function prettyKind(k){return({transcription:'Vosk / transcription',comprehension:'Compréhension Hermes',execution:'Exécution'})[k]||'Avis';}
function chip(s){const v={approved:'Règle active',pending:'À valider',recorded:'Enregistré',rejected:'Rejeté',revoked:'Règle retirée'};return el('span','dl93-chip',v[s]||s);}
function itemRow(item,content){const card=el('article','dl93-record');const top=el('div','dl93-record-head');top.append(el('strong','',item.rating==='up'?'👍 Correct':'👎 '+prettyKind(item.kind)),chip(item.status));card.append(top);
card.append(el('p','dl93-small','Entendu : '+txt(item.heard,260)));if(item.answer)card.append(el('p','dl93-small','Réponse : '+txt(item.answer,180)));
if(item.correction)card.append(el('p','dl93-small','Correction : '+txt(item.correction,260)));
if(item.note)card.append(el('p','dl93-small','Note : '+txt(item.note,180)));
if(item.status==='pending'){
const actions=el('div','dl93-actions');
actions.append(btn('✓ Valider la règle',async()=>{if(!confirm('Appliquer cette correction aux prochaines demandes identiques ?'))return;await change('/api/dona/v93/learning/'+item.id+'/approve');},'dl93-button'));
actions.append(btn('Rejeter',async()=>{await change('/api/dona/v93/learning/'+item.id+'/reject');},'dl93-button dl93-neutral'));card.append(actions);
}
const remove=btn('Supprimer cette évaluation',async()=>{if(confirm('Supprimer cette évaluation et sa règle éventuelle ?'))await change('/api/dona/v93/learning/'+item.id+'/delete');},'dl93-button dl93-neutral');card.append(remove);
content.append(card);}
async function change(url){try{await api(url,{});await loadPanel();}catch(e){notify(e.message,true);}}
async function loadPanel(){const content=document.getElementById('dl93PanelContent');if(!content)return;content.replaceChildren(el('p','','Chargement…'));notify('');try{
const data=await api('/api/dona/v93/learning');content.replaceChildren();content.append(el('h4','','Évaluations : '+data.stats.positive+' 👍 · '+data.stats.negative+' 👎'));
if(data.rules.length){const rules=el('section','dl93-rules');rules.append(el('h4','','Règles actives ('+data.rules.length+')'));
for(const r of data.rules){const row=el('div','dl93-rule');row.append(el('span','',prettyKind(r.kind)+' : '+txt(r.original,90)+' → '+txt(r.replacement,90)),btn('Retirer',async()=>{if(confirm('Supprimer cette règle ?'))await change('/api/dona/v93/learning/rules/'+r.id+'/remove');},'dl93-button dl93-neutral'));rules.append(row);}content.append(rules);}
if(!data.history.length)content.append(el('p','dl93-small','Aucune réponse évaluée pour le moment.'));
data.history.forEach(f=>itemRow(f,content));
}catch(e){content.replaceChildren(el('p','dl93-error',e.message));}}
function attachMenu(){const m=document.getElementById('dv92ToolsMenu');if(!m||m.hidden||m.querySelector('#dl93MenuEntry'))return;const b=btn('🎓 Apprentissage et corrections',()=>{m.hidden=true;document.getElementById('dv92Hamburger')?.setAttribute('aria-expanded','false');openPanel();},'dv92-menu-entry');b.id='dl93MenuEntry';m.append(b);}
function init(){if(document.getElementById('dl93Dock'))return;createDock();makeDialog();makePanel();document.addEventListener('click',e=>{if(e.target.closest('#dv92Hamburger'))window.setTimeout(attachMenu,10);});document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeCorrection();closePanel();}});
const waitForSocket=()=>{const sock=window.alfredSocket;if(!sock?.on){if(++menuTick<80)setTimeout(waitForSocket,150);return;}if(socketBound)return;socketBound=true;
sock.on('dona_learning_turn',setTurn);
sock.on('dona_learning_stt',data=>{const n=document.getElementById('dl93Transcript');if(n){n.textContent='Vosk : '+data.raw+' → Correction : '+data.used;n.hidden=false;}});
sock.on('alfred_state',state=>{if(state?.mode==='thinking'){if(dock)dock.hidden=true;if(document.getElementById('dl93Transcript'))document.getElementById('dl93Transcript').hidden=true;}});
};waitForSocket();
const t=el('div','dl93-transcript');t.id='dl93Transcript';t.hidden=true;document.body.append(t);
window.DonaLearningV93={openPanel,openCorrection};}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
})();
@@ -0,0 +1,59 @@
/* DONA_UI_V921_CLOSE — Répare le panneau Raccourcis V9.2. */
(() => {
'use strict';
if (window.DonaUIV921Close) return;
let attempts = 0;
function init() {
const root = document.getElementById('alfredMainV1');
const tray = root?.querySelector('.dona-tools-tray');
const grid = tray?.querySelector('.av-shortcuts');
const toolbar = tray?.querySelector('.dv92-shortcut-toolbar');
if (!grid || !toolbar || !root?.querySelector('#dv92Hamburger')) {
if (++attempts < 80) window.setTimeout(init, 150);
return;
}
if (tray.dataset.dv921Close === 'ready') return;
tray.dataset.dv921Close = 'ready';
const closeTray = () => { tray.open = false; };
const heading = document.createElement('div');
heading.className = 'dv921-tray-heading';
const label = document.createElement('span');
label.textContent = 'Raccourcis';
const close = document.createElement('button');
close.type = 'button';
close.className = 'dv921-tray-close';
close.textContent = '×';
close.title = 'Fermer les raccourcis';
close.setAttribute('aria-label', 'Fermer les raccourcis');
close.addEventListener('click', (event) => {
event.stopPropagation();
closeTray();
});
heading.append(label, close);
// V9.2 régénère la grille après une modification : réinstaller l'entête.
const ensureHeading = () => {
if (grid.firstElementChild !== heading) grid.prepend(heading);
};
ensureHeading();
new MutationObserver(ensureHeading).observe(grid, { childList: true });
// Fermer sur clic à l'extérieur, sans bloquer les autres commandes.
document.addEventListener('pointerdown', (event) => {
if (tray.open && !tray.contains(event.target)) closeTray();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && tray.open) closeTray();
});
// Fermer aussi lorsqu'un raccourci est lancé.
grid.addEventListener('click', (event) => {
if (event.target.closest('button.dv92-shortcut')) closeTray();
});
// L'éditeur ne nécessite pas que le panneau reste ouvert derrière lui.
toolbar.addEventListener('click', (event) => {
if (event.target.closest('.dv92-edit-open')) closeTray();
});
window.DonaUIV921Close = { close: closeTray };
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else init();
})();
+143
View File
@@ -0,0 +1,143 @@
/* DONA_UI_V92 — Hamburger + raccourcis éditables, sans dupliquer les contrôleurs. */
(()=>{
'use strict';
if(window.DonaUIV92)return;
const $=id=>document.getElementById(id);
const create=(tag,cls,text)=>{const el=document.createElement(tag);if(cls)el.className=cls;if(text!==undefined)el.textContent=String(text);return el;};
const button=(text,fn,cls='dv92-btn')=>{const b=create('button',cls,text);b.type='button';b.addEventListener('click',fn);return b;};
const icons={clock:'◷','cloud-sun':'☀','house':'⌂',server:'▤','shield-check':'⬡',image:'▧','chef-hat':'♨','calendar-days':'▦',sun:'☼',lightbulb:'♧','list-checks':'☷',music:'♫',youtube:'▶',map:'⌖',camera:'▣','book-open':'▤',sparkles:'✦',brain:'✧',settings:'⚙',bell:'♧',zap:'ϟ',star:'☆','file-text':'▤',terminal:'⌘'};
const labels={clock:'Horloge','cloud-sun':'Météo',house:'Maison',server:'Serveur','shield-check':'Sécurité',image:'Image','chef-hat':'Cuisine','calendar-days':'Agenda',sun:'Soleil',lightbulb:'Lumière','list-checks':'Liste',music:'Musique',youtube:'YouTube',map:'Carte',camera:'Caméra','book-open':'Livre',sparkles:'Étoile',brain:'IA',settings:'Réglages',bell:'Notification',zap:'Éclair',star:'Favori','file-text':'Document',terminal:'Terminal'};
const builtins=new Set(['time','weather','house','proxmox','wazuh','comfyui']);
const builtinTitles={time:'Quelle heure est-il ?',weather:'Météo',house:'État de la maison',proxmox:'Vérifier Proxmox',wazuh:'Alertes Wazuh',comfyui:'Créer une image'};
const fallback=['time','weather','house','proxmox','wazuh','comfyui'].map((id,i)=>({id,title:builtinTitles[id],icon:['clock','cloud-sun','house','server','shield-check','image'][i],prompt:builtinTitles[id],visible:true,mode:'native',action:id}));
let items=fallback.map(x=>({...x}));let editing=false,draft=[];let sending=false;
let header,bar,menu,burger,editDialog,tray,shortcutHost,status;
function msg(text){if(status)status.textContent=String(text||'');}
function request(url,body){return fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(body)}).then(async r=>{let data=await r.json().catch(()=>({}));if(!r.ok||data.ok===false)throw Error(data.error||'Erreur '+r.status);return data;});}
function icon(key){return create('span','dv92-symbol',icons[key]||'✦');}
function closeMenu(){if(!menu)return;menu.hidden=true;burger?.setAttribute('aria-expanded','false');}
function positionMenu(){if(!menu||menu.hidden)return;const rect=burger.getBoundingClientRect();let left=Math.min(rect.right-menu.offsetWidth,window.innerWidth-menu.offsetWidth-8);left=Math.max(8,left);let top=rect.bottom+9;if(top+menu.offsetHeight>window.innerHeight-8)top=Math.max(8,rect.top-menu.offsetHeight-9);menu.style.left=left+'px';menu.style.top=top+'px';}
function showMenu(){menu.hidden=!menu.hidden;burger.setAttribute('aria-expanded',String(!menu.hidden));if(!menu.hidden){renderMenu();positionMenu();}}
function renderMenu(){
menu.replaceChildren(create('div','dv92-menu-heading','Outils Dona'));
const controls=Array.from(bar.children).filter(x=>x!==burger&&x.id!=='avTopMic');
for(const original of controls){
const title=(original.getAttribute('aria-label')||original.title||original.textContent||'Outil').trim();
const proxy=button('',()=>{closeMenu();original.click();},'dv92-menu-entry');
const ico=original.querySelector('svg');
if(ico){const svg=ico.cloneNode(true);svg.removeAttribute('id');svg.setAttribute('aria-hidden','true');proxy.append(svg);}
else proxy.append(create('span','dv92-menu-symbol',(original.textContent||'•').trim().slice(0,2)));
proxy.append(create('span','',title));menu.append(proxy);
}
menu.append(create('div','dv92-menu-rule'));
menu.append(button('✎ Modifier les raccourcis',()=>{closeMenu();openEditor();},'dv92-menu-entry dv92-menu-edit'));
}
function initHeader(root){
bar=root.querySelector('.av-head-actions');if(!bar)return;
burger=button('☰',showMenu,'av-round dv92-hamburger');burger.id='dv92Hamburger';burger.setAttribute('aria-label','Ouvrir le menu des outils');burger.setAttribute('aria-expanded','false');burger.setAttribute('aria-controls','dv92ToolsMenu');burger.title='Menu des outils';
bar.append(burger);
menu=create('div','dv92-menu');menu.id='dv92ToolsMenu';menu.hidden=true;menu.setAttribute('role','group');menu.setAttribute('aria-label','Outils Dona');document.body.append(menu);
renderMenu();
const observer=new MutationObserver(()=>{if(!menu.hidden){renderMenu();positionMenu();}});observer.observe(bar,{childList:true});
window.addEventListener('resize',positionMenu,{passive:true});window.addEventListener('scroll',positionMenu,{passive:true});
document.addEventListener('pointerdown',e=>{if(!menu.hidden&&!menu.contains(e.target)&&!burger.contains(e.target))closeMenu();});
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeMenu();closeEditor();}});
}
async function submitPrompt(prompt){
if(sending)return;sending=true;msg('Transmission à Dona…');
try{const x=await request('/api/dona/texte',{message:prompt});msg(x.message||'Demande transmise à Dona.');}
catch(e){msg('Erreur : '+e.message);}
finally{sending=false;}
}
function drawShortcuts(){
if(!shortcutHost)return;
shortcutHost.replaceChildren();
for(const item of items.filter(x=>x.visible)){
const b=button('',()=>{},'dv92-shortcut');b.replaceChildren(icon(item.icon),create('span','',item.title));b.title=item.mode==='prompt'?item.prompt:item.title;
if(item.mode==='native'&&builtins.has(item.id)){
b.dataset.avAction=item.action;
// The original delegated handler on #alfredMainV1 handles native actions.
}else{
b.addEventListener('click',e=>{e.stopPropagation();submitPrompt(item.prompt);});
}
shortcutHost.append(b);
}
if(!shortcutHost.children.length)shortcutHost.append(create('p','dv92-shortcut-empty','Aucun raccourci visible. Clique sur Modifier pour en ajouter.'));
}
function editorButton(){
tray=shortcutHost.closest('.dona-tools-tray');if(!tray)return;
const tools=create('div','dv92-shortcut-toolbar');tools.append(button('✎ Modifier les raccourcis',openEditor,'dv92-edit-open'));status=create('span','dv92-shortcut-status');status.setAttribute('role','status');status.setAttribute('aria-live','polite');tools.append(status);
shortcutHost.before(tools);
}
function openEditor(){
if(!editDialog)return;
draft=items.map(x=>({...x}));editing=true;drawEditor();editDialog.hidden=false;document.body.classList.add('dv92-editing');
}
function closeEditor(){if(!editDialog||!editing)return;editing=false;editDialog.hidden=true;document.body.classList.remove('dv92-editing');}
function editorFields(item,index,host){
const summary=create('summary','dv92-item-summary');summary.append(icon(item.icon),create('strong','dv92-summary-name',item.title),create('span','dv92-summary-mode',item.mode==='native'?'Action intégrée':'Texte personnalisé'));
const detail=create('details','dv92-edit-item');detail.append(summary);
const controls=create('div','dv92-inline-controls');
const up=button('↑',()=>move(index,-1),'dv92-small');up.title='Monter';up.disabled=index===0;
const down=button('↓',()=>move(index,1),'dv92-small');down.title='Descendre';down.disabled=index===draft.length-1;
const visible=create('label','dv92-check');const check=create('input');check.type='checkbox';check.checked=item.visible;check.addEventListener('change',()=>{item.visible=check.checked;});visible.append(check,create('span','', 'Afficher'));
const del=button('Supprimer',()=>{if(confirm('Supprimer ce raccourci ?')){draft.splice(index,1);drawEditor();}},'dv92-small dv92-danger');
controls.append(visible,up,down,del);detail.append(controls);
function field(labelText,control){const box=create('label','dv92-field');box.append(create('span','',labelText),control);return box;}
const title=create('input','dv92-input');title.type='text';title.maxLength=70;title.value=item.title;title.required=true;title.addEventListener('input',()=>{item.title=title.value;summary.querySelector('.dv92-summary-name').textContent=title.value;});
detail.append(field('Nom affiché',title));
const pick=create('select','dv92-input');for(const [key,label] of Object.entries(labels)){const o=create('option','',`${icons[key]} ${label}`);o.value=key;o.selected=key===item.icon;pick.append(o);}pick.addEventListener('change',()=>{item.icon=pick.value;summary.querySelector('.dv92-symbol').textContent=icons[pick.value];});
detail.append(field('Icône',pick));
const how=create('select','dv92-input');const nativeOption=create('option','', 'Action intégrée (fonctionnement d’origine)');nativeOption.value='native';const promptOption=create('option','', 'Envoyer mon texte à Dona / Hermes');promptOption.value='prompt';if(builtins.has(item.id))how.append(nativeOption);how.append(promptOption);how.value=item.mode;
how.addEventListener('change',()=>{item.mode=how.value;summary.querySelector('.dv92-summary-mode').textContent=how.value==='native'?'Action intégrée':'Texte personnalisé';});
detail.append(field('Quand je touche ce raccourci',how));
const prompt=create('textarea','dv92-textarea');prompt.rows=3;prompt.maxLength=1600;prompt.value=item.prompt;prompt.addEventListener('input',()=>{item.prompt=prompt.value;if(item.mode==='native'){item.mode='prompt';how.value='prompt';summary.querySelector('.dv92-summary-mode').textContent='Texte personnalisé';}});
detail.append(field('Texte exact envoyé à Dona',prompt));
detail.append(create('p','dv92-help', 'Modifier ce texte active automatiquement le mode « Texte personnalisé ». Les commandes passent par le formulaire écrit de Dona, sans synthèse vocale.'));
host.append(detail);
}
function move(i,d){const j=i+d;if(j<0||j>=draft.length)return;[draft[i],draft[j]]=[draft[j],draft[i]];drawEditor();}
function drawEditor(){const target=$('dv92EditorRows');if(!target)return;target.replaceChildren();draft.forEach((item,i)=>editorFields(item,i,target));$('dv92EditorCount').textContent=draft.length+' / 35';}
async function saveEditor(){
const save=$('dv92Save');save.disabled=true;
try{const data=await request('/api/dona/v92/shortcuts',{items:draft});items=data.items;drawShortcuts();closeEditor();msg('Raccourcis enregistrés.');}
catch(e){$('dv92EditorStatus').textContent='Erreur : '+e.message;}
finally{save.disabled=false;}
}
async function resetEditor(){
if(!confirm('Restaurer les raccourcis d’origine ? Tes modifications seront perdues.'))return;
try{const data=await request('/api/dona/v92/shortcuts/reset',{});items=data.items;draft=items.map(x=>({...x}));drawEditor();drawShortcuts();$('dv92EditorStatus').textContent='Raccourcis réinitialisés.';}
catch(e){$('dv92EditorStatus').textContent='Erreur : '+e.message;}
}
function addShortcut(){
if(draft.length>=35){$('dv92EditorStatus').textContent='Maximum de 35 raccourcis.';return;}
const uniq=()=>('user_'+Math.random().toString(36).slice(2,12)+Date.now().toString(36));
draft.push({id:uniq(),title:'Nouveau raccourci',icon:'sparkles',prompt:'Dona, ',visible:true,mode:'prompt',action:''});drawEditor();
const last=$('dv92EditorRows').lastElementChild;last.open=true;last.querySelector('.dv92-input')?.focus();
}
function initEditor(){
editDialog=create('div','dv92-editor-overlay');editDialog.id='dv92Editor';editDialog.hidden=true;
const panel=create('section','dv92-editor');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Modifier les raccourcis Dona');
const heading=create('div','dv92-editor-header');const title=create('div');title.append(create('small','', 'DONA · PERSONNALISATION'),create('h2','', 'Mes raccourcis'));
heading.append(title,button('×',closeEditor,'dv92-close'));panel.append(heading);
panel.append(create('p','dv92-help','Renomme, classe ou masque tes raccourcis. Modifie « Texte exact envoyé à Dona » pour personnaliser leur commande.'));
const counter=create('div','dv92-count');counter.id='dv92EditorCount';panel.append(counter);
const list=create('div','dv92-editor-list');list.id='dv92EditorRows';panel.append(list);
const bottom=create('div','dv92-editor-actions');bottom.append(button('+ Ajouter un raccourci',addShortcut,'dv92-btn dv92-secondary'),button('↺ Réinitialiser',resetEditor,'dv92-btn dv92-secondary'),button('Annuler',closeEditor,'dv92-btn dv92-secondary'));
const save=button('Enregistrer',saveEditor,'dv92-btn dv92-primary');save.id='dv92Save';bottom.append(save);panel.append(bottom);
const message=create('div','dv92-feedback');message.id='dv92EditorStatus';message.setAttribute('role','status');panel.append(message);editDialog.append(panel);document.body.append(editDialog);
editDialog.addEventListener('pointerdown',e=>{if(e.target===editDialog)closeEditor();});
}
async function loadShortcuts(){
try{const response=await fetch('/api/dona/v92/shortcuts',{credentials:'same-origin',cache:'no-store'});const data=await response.json();if(!response.ok||!data.ok)throw Error(data.error||'Erreur de lecture');items=data.items;drawShortcuts();}
catch(e){drawShortcuts();msg('Raccourcis par défaut. Configuration indisponible : '+e.message);}
}
function init(){
const root=$('alfredMainV1');if(!root||!root.querySelector('.av-head-actions')||!root.querySelector('.av-shortcuts'))return false;
shortcutHost=root.querySelector('.av-shortcuts');
initHeader(root);editorButton();initEditor();loadShortcuts();
window.DonaUIV92={openEditor};return true;
}
let tries=0;function ready(){if(!init()&&tries++<80)setTimeout(ready,150);}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',ready,{once:true});else ready();
})();
+67
View File
@@ -0,0 +1,67 @@
/* DONA_VISION_V95 : YOLOv8 local opt-in et image Hermes sur demande. */
(()=>{'use strict';if(window.DonaVisionV95)return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!==undefined)n.textContent=String(text);return n};
const btn=(text,action,cls)=>{const n=el('button',cls||'dv95-btn',text);n.type='button';n.addEventListener('click',action);return n};
let opened=false,busy=false,monitoring=false,toastTimer;
async function request(url,data){const r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(data)});const out=await r.json().catch(()=>({}));if(!r.ok||out.ok===false)throw Error(out.error||'Erreur '+r.status);return out}
async function getStatus(){const r=await fetch('/api/dona/vision/v95/status',{credentials:'same-origin'});if(!r.ok)throw Error('État indisponible');return r.json()}
function message(value){const n=$('dv95Status');if(n)n.textContent=String(value||'')}
function render(data){monitoring=!!data.running;$('dv95Start').hidden=monitoring;$('dv95Stop').hidden=!monitoring;$('dv95People').disabled=monitoring;$('dv95Objects').disabled=monitoring;
message((data.message||'')+(data.ultralytics_installed?'':' · YOLOv8 à installer')+(data.model_ready?'':' · modèle absent'));
$('dv95Model').textContent='Modèle : '+(data.model||'yolov8n.pt')+' · balayage '+(data.interval||2.5)+' s';
const objects=$('dv95Detections');objects.replaceChildren();(data.latest||[]).slice(0,10).forEach(x=>objects.append(el('span','dv95-pill',x.label+' · '+Math.round(x.confidence*100)+' %')));
if(!objects.children.length)objects.append(el('span','dv95-muted','Aucune détection récente.'));
const history=$('dv95History');history.replaceChildren();(data.events||[]).forEach(x=>{const n=el('li');n.append(el('strong','',x.time+' · '+x.title),el('p','',x.detail));history.append(n)});
if(!history.children.length)history.append(el('li','dv95-muted','Aucun événement pour cette session.'));
}
async function refresh(){try{render(await getStatus())}catch(e){message(e.message)}}
async function setMonitor(enabled){if(busy)return;busy=true;message(enabled?'Démarrage YOLOv8…':'Arrêt en cours…');
try{const x=await request('/api/dona/vision/v95/monitor',{enabled,person:$('dv95People').checked,objects:$('dv95Objects').checked});render(x)}catch(e){message(e.message)}finally{busy=false}}
async function analyze(){if(busy)return;busy=true;$('dv95Analyze').disabled=true;const question=$('dv95Question').value.trim()||'Décris ce que tu vois devant toi.';
$('dv95Answer').textContent='Analyse de la capture par Hermes en cours…';
try{const x=await request('/api/dona/vision/v95/analyze',{question});$('dv95Answer').textContent=x.answer||'Pas de description.'}
catch(e){$('dv95Answer').textContent=e.message}finally{busy=false;$('dv95Analyze').disabled=false}}
function startPreview(){const img=$('dv95Feed');if(img.src)return;img.src='/api/camera/stream?v=95&t='+Date.now();img.hidden=false;$('dv95PreviewHint').textContent='L’aperçu est local et non enregistré. Si la caméra est déjà utilisée ailleurs, ferme l’autre aperçu.';}
function stopPreview(){const img=$('dv95Feed');img.removeAttribute('src');img.hidden=true;$('dv95PreviewHint').textContent='Aperçu coupé.'}
function open(){opened=true;$('dv95Modal').hidden=false;refresh()}
function close(){opened=false;$('dv95Modal').hidden=true;stopPreview()}
function toast(evt){let box=$('dv95Toast');if(!box)return;box.replaceChildren(el('strong','',evt.title||'Vision Dona'),el('span','',evt.detail||''));box.hidden=false;clearTimeout(toastTimer);toastTimer=setTimeout(()=>{box.hidden=true},8500)}
function init(){const modal=el('div','dv95-overlay');modal.id='dv95Modal';modal.hidden=true;modal.setAttribute('role','dialog');modal.setAttribute('aria-modal','true');modal.setAttribute('aria-label','Vision intelligente');
const frame=el('section','dv95-frame');const head=el('header','dv95-head');const h=el('div');h.append(el('h2','','◉ Vision intelligente'),el('p','dv95-muted','Détection locale YOLOv8 · analyse ponctuelle Hermes'));
head.append(h,btn('×',close,'dv95-close'));frame.append(head);
const content=el('div','dv95-body');const left=el('div','dv95-column');const right=el('div','dv95-column');
const video=el('div','dv95-preview');const img=el('img');img.id='dv95Feed';img.alt='Aperçu caméra locale';img.hidden=true;const info=el('p','dv95-muted','Aperçu caméra arrêté.');info.id='dv95PreviewHint';video.append(img,info);left.append(video);
const previewBtns=el('div','dv95-controls');previewBtns.append(btn('Afficher la caméra',startPreview),btn('Couper l’aperçu',stopPreview));left.append(previewBtns);
left.append(el('h3','','Surveillance des événements'));
const c1=el('label','dv95-choice');const p=el('input');p.type='checkbox';p.id='dv95People';p.checked=true;c1.append(p,el('span','','Apparition d’une personne'));
const c2=el('label','dv95-choice');const o=el('input');o.type='checkbox';o.id='dv95Objects';o.checked=true;c2.append(o,el('span','','Objet visible au centre de la caméra'));
left.append(c1,c2);
const run=el('div','dv95-controls');const start=btn('▶ Activer YOLOv8',()=>setMonitor(true),'dv95-btn dv95-primary');start.id='dv95Start';const stop=btn('■ Arrêter YOLOv8',()=>setMonitor(false),'dv95-btn dv95-stop');stop.id='dv95Stop';stop.hidden=true;run.append(start,stop);left.append(run);
const status=el('p','dv95-status','Surveillance désactivée.');status.id='dv95Status';left.append(status);const model=el('p','dv95-muted','');model.id='dv95Model';left.append(model);
const detections=el('div','dv95-pills');detections.id='dv95Detections';left.append(el('h3','','Dernières détections'),detections);
right.append(el('h3','','Demander à Hermes ce que voit Dona'));
const question=el('textarea','dv95-question');question.id='dv95Question';question.rows=3;question.maxLength=600;question.value='Décris les objets et les personnes visibles. Ne devine pas leur identité.';right.append(question);
const analyzeBtn=btn('◉ Analyser une capture',analyze,'dv95-btn dv95-primary');analyzeBtn.id='dv95Analyze';right.append(analyzeBtn);
const answer=el('div','dv95-answer','L’analyse est déclenchée uniquement sur demande.');answer.id='dv95Answer';right.append(answer);
right.append(btn('◎ Identifier une personne (MCP facial)',()=>{close();const face=$('donaFaceButton');if(face)face.click();else message('Le MCP facial V7 est indisponible.');}));
right.append(el('h3','','Événements récents'));const history=el('ul','dv95-history');history.id='dv95History';right.append(history);
right.append(el('p','dv95-muted','Aucune image n’est conservée par ce module. Les événements ne déclenchent ni reconnaissance faciale ni action domotique. Les images demandées à Hermes suivent la configuration de son fournisseur IA.'));
content.append(left,right);frame.append(content);modal.append(frame);document.body.append(modal);
modal.addEventListener('pointerdown',e=>{if(e.target===modal)close()});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&opened)close()});
const toastNode=el('div','dv95-toast');toastNode.id='dv95Toast';toastNode.hidden=true;toastNode.setAttribute('role','status');document.body.append(toastNode);
// V9.2 génère le menu hamburger à partir des boutons originaux de l'en-tête.
const mount=()=>{const root=$('alfredMainV1');const bar=root?.querySelector('.av-head-actions');if(!bar)return false;if($('dv95Launch'))return true;
const action=btn('◉',open,'av-round dv95-launch');action.id='dv95Launch';action.title='Vision intelligente · YOLOv8';action.setAttribute('aria-label','Vision intelligente · YOLOv8');bar.append(action);return true};
if(!mount()){let attempts=0;const timer=setInterval(()=>{if(mount()||++attempts>100)clearInterval(timer)},150)}
const socket=window.socket||window.alfredSocket||null;
// Si la Socket.IO de l'application n'est pas exposée globalement, client dédié.
try{const listener=(socket&&typeof socket.on==='function')?socket:(typeof window.io==='function'?window.io({transports:['websocket','polling']}):null);
listener?.on('dona_vision_event_v95',event=>{toast(event);if(opened)refresh()});
listener?.on('dona_vision_status_v95',()=>{if(opened)refresh()});
}catch(e){console.debug('Vision : événements non disponibles',e)}
setInterval(()=>{if(opened)refresh()},6000);
window.DonaVisionV95={open,close,refresh};
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
})();