147 lines
6.0 KiB
JavaScript
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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);
|