Files
html/api/weval-avatar-helper.js
2026-04-18 03:24:41 +02:00

147 lines
6.0 KiB
JavaScript

/**
* /api/weval-avatar-helper.js — V74 Doctrine 91
*
* SOURCE UNIQUE DE VÉRITÉ pour le rendu des avatars dans tout l'écosystème WEVAL.
* Respecte le pattern meeting-rooms :
* - persona "human" / "master" → emoji humain (👩🏽‍💼, 👨🏿‍💻…)
* - persona "tool" → emoji métier/outil (📧, 🦙, 📐…)
*
* Backward compat: si agent introuvable ou JSON v2 indisponible,
* fallback sur l'ancienne image dicebear/robohash de /api/agent-avatars.json.
*
* Usage:
* await WevalAvatar.init();
* WevalAvatar.html('WEVIA Master') → '<span class="wev-av" ...>👩🏽‍💼</span>'
* WevalAvatar.emoji('Ollama') → '🦙'
* WevalAvatar.hydrate(document) → remplace tous les <img data-agent="…"> par emoji DOM
* WevalAvatar.canvas(ctx,'WEVIA Master',x,y,s)→ dessine un emoji sur un Canvas 2D
*/
(function(root){
'use strict';
const REG_V2 = '/api/agent-avatars-v2.json';
const REG_V1 = '/api/agent-avatars.json';
const DEFAULT_EMOJI = '👤';
const DEFAULT_TOOL = '🤖';
const DEFAULT_COLOR = '#64748b';
let _data = {};
let _v1 = {};
let _ready = false;
let _pending = null;
function _normalize(name){ return (name||'').trim().toLowerCase(); }
function _lookup(name){
if (!name) return null;
if (_data[name]) return _data[name];
const low = _normalize(name);
for (const k in _data) if (_normalize(k) === low) return _data[k];
// fuzzy word-match
for (const k in _data) if (_normalize(k).includes(low) || low.includes(_normalize(k))) return _data[k];
return null;
}
const api = {
isReady(){ return _ready; },
async init(){
if (_ready) return;
if (_pending) return _pending;
_pending = (async () => {
try {
const r = await fetch(REG_V2 + '?t=' + Date.now());
_data = await r.json();
} catch(e){ console.warn('[WevalAvatar] v2 load failed', e); }
try {
const r = await fetch(REG_V1 + '?t=' + Date.now());
_v1 = await r.json();
} catch(e){}
_ready = true;
})();
return _pending;
},
get(name){
const e = _lookup(name);
if (e) return e;
const url = _v1[name];
if (url){
const isTool = /robohash/.test(url);
return { persona: isTool?'tool':'human', emoji: isTool?DEFAULT_TOOL:DEFAULT_EMOJI, color: DEFAULT_COLOR, url };
}
return { persona:'human', emoji: DEFAULT_EMOJI, color: DEFAULT_COLOR, url: 'https://api.dicebear.com/9.x/adventurer/svg?seed='+encodeURIComponent(name||'default') };
},
emoji(name){ return this.get(name).emoji; },
url(name){ return this.get(name).url; },
color(name){ return this.get(name).color; },
persona(name){ return this.get(name).persona; },
/** Safe HTML pill */
html(name, opts){
opts = opts || {};
const e = this.get(name);
const size = opts.size || 34;
const fontSz = Math.round(size * 0.62);
const bg = opts.bg || (e.color + '22');
const border = opts.border || e.color;
const label = (opts.showName === false) ? '' :
`<span style="font:600 11px Nunito,system-ui;margin-left:6px;color:${opts.nameColor||'#e2e8f0'}">${this.esc(name)}</span>`;
return `<span class="wev-av" title="${this.esc(name)}" data-agent="${this.esc(name)}" style="display:inline-flex;align-items:center;gap:0;vertical-align:middle"><span style="width:${size}px;height:${size}px;border-radius:50%;background:${bg};border:1.5px solid ${border};display:inline-flex;align-items:center;justify-content:center;font-size:${fontSz}px;line-height:1;box-shadow:0 2px 6px rgba(0,0,0,.15)">${e.emoji}</span>${label}</span>`;
},
/** Replace any <img data-agent="X"> with an emoji span */
hydrate(scope){
const root = scope || document;
const imgs = root.querySelectorAll('img[data-agent], img[alt]:not([data-weval-hydrated])');
imgs.forEach(img => {
const name = img.dataset.agent || img.getAttribute('alt') || img.getAttribute('title');
if (!name) return;
// skip if name looks like a file path (real picture, not an agent)
if (/\.(png|jpg|svg|webp|gif)$/i.test(name)) return;
const e = this.get(name);
if (!e || !e.emoji) return;
const size = img.width || img.height || 34;
const bg = e.color + '22';
const span = document.createElement('span');
span.className = 'wev-av';
span.dataset.agent = name;
span.title = name;
span.setAttribute('data-weval-hydrated','1');
span.style.cssText = `display:inline-flex;width:${size}px;height:${size}px;border-radius:50%;background:${bg};border:1.5px solid ${e.color};align-items:center;justify-content:center;font-size:${Math.round(size*0.62)}px;line-height:1;vertical-align:middle;flex-shrink:0`;
span.textContent = e.emoji;
// preserve any classes added to the image
img.classList.forEach(c => span.classList.add(c));
img.parentNode && img.parentNode.replaceChild(span, img);
});
},
/** Draw on a Canvas2D */
canvas(ctx, name, x, y, size){
const e = this.get(name);
ctx.save();
ctx.beginPath(); ctx.arc(x, y, size/2, 0, Math.PI*2);
ctx.fillStyle = e.color + '22'; ctx.fill();
ctx.lineWidth = 1.5; ctx.strokeStyle = e.color; ctx.stroke();
ctx.font = `${Math.round(size*0.65)}px Apple Color Emoji, Segoe UI Emoji, sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(e.emoji, x, y);
ctx.restore();
},
esc(s){ return String(s||'').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); },
/** Stats helper */
stats(){
const by = { human:0, master:0, tool:0 };
Object.values(_data).forEach(a => { if (by[a.persona] !== undefined) by[a.persona]++; });
return { total: Object.keys(_data).length, ...by };
}
};
root.WevalAvatar = api;
// auto-init
api.init();
})(typeof window !== 'undefined' ? window : globalThis);