Files
html/avatar-picker.html
2026-04-23 20:25:02 +02:00

389 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>WEVIA — Agent Avatar Picker (SSOT)</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700;900&family=Nunito:wght@600;700;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#050a18;color:#e2e8f0;font-family:Nunito,sans-serif;min-height:100vh}
.hdr{background:linear-gradient(135deg,#0f1629,#1a2035);padding:14px 20px;border-bottom:1px solid rgba(6,182,212,.2);position:sticky;top:0;z-index:50;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px}
.hdr h1{font:900 15px Orbitron;color:#06b6d4;letter-spacing:1px}
.nav{display:flex;gap:8px;font-size:11px;flex-wrap:wrap}
.nav a{color:#94a3b8;text-decoration:none;padding:4px 10px;border-radius:6px;border:1px solid rgba(255,255,255,.08)}
.nav a:hover{color:#06b6d4;border-color:#06b6d4}
.bar{padding:10px 20px;background:#0a0f1e;border-bottom:1px solid rgba(255,255,255,.04);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.fb{padding:5px 14px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:none;color:#94a3b8;font:700 10px Nunito;cursor:pointer;letter-spacing:1px;transition:.15s}
.fb.ac{border-color:#06b6d4;background:rgba(6,182,212,.12);color:#06b6d4}
.search{flex:1;min-width:200px;padding:6px 12px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.3);color:#e2e8f0;font:600 11px Nunito;outline:none}
.stats{font-size:10px;color:#64748b}
.stats b{color:#06b6d4}
.grid{padding:16px;display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.card{background:#0f1629;border:1.5px solid rgba(255,255,255,.06);border-radius:12px;padding:14px;display:flex;flex-direction:column;align-items:center;gap:8px;transition:.15s;cursor:pointer}
.card:hover{transform:translateY(-2px);border-color:#06b6d4;box-shadow:0 4px 16px rgba(6,182,212,.15)}
.card.master{border-color:rgba(255,215,0,.5);background:linear-gradient(135deg,#1a1410,#0f1629)}
.card.human{border-color:rgba(74,222,128,.3)}
.card.tool{border-color:rgba(139,92,246,.3)}
.card.gap{box-shadow:0 0 14px rgba(34,211,238,.15);border-color:#22d3ee}
.av{width:72px;height:72px;display:flex;align-items:center;justify-content:center;font-size:42px;line-height:1;border-radius:50%;background:rgba(255,255,255,.05);border:2.5px solid;flex-shrink:0;overflow:hidden}
.card.master .av{border-color:rgba(255,215,0,.65);background:rgba(255,215,0,.08);width:84px;height:84px;font-size:50px}
.card.human .av{border-color:rgba(74,222,128,.5);background:rgba(74,222,128,.06)}
.card.tool .av{border-color:rgba(139,92,246,.5);background:rgba(139,92,246,.08)}
.nm{font-weight:800;font-size:12px;text-align:center;color:#e2e8f0;line-height:1.2;max-width:160px;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
.meta{display:flex;gap:4px;align-items:center;font-size:9px;color:#64748b;flex-wrap:wrap;justify-content:center}
.tag{padding:1px 6px;border-radius:3px;background:rgba(255,255,255,.06);font-weight:700;letter-spacing:.5px;text-transform:uppercase}
.tag.gap{background:rgba(34,211,238,.15);color:#22d3ee}
.tag.role{background:rgba(255,255,255,.04);color:#94a3b8}
.empty{text-align:center;padding:40px;color:#64748b;font-size:12px}
/* === WAVE-273 AVATAR EDIT MODE (doctrine UX zero chevauchement) === */
.card.editmode{cursor:pointer;position:relative}
.card.editmode::after{content:'✎';position:absolute;top:6px;right:6px;background:rgba(255,193,7,.18);color:#ffc107;border-radius:50%;width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;border:1px solid rgba(255,193,7,.4);opacity:0;transition:.15s}
.card.editmode:hover::after{opacity:1}
.fb#editToggle[data-edit=on]{border-color:#ffc107!important;background:rgba(255,193,7,.14);color:#ffc107}
.editbanner{display:none;background:linear-gradient(90deg,rgba(255,193,7,.08),transparent);border-bottom:1px solid rgba(255,193,7,.25);padding:8px 20px;font-size:11px;color:#ffc107;font-weight:700;letter-spacing:.5px}
.editbanner.on{display:block}
.editbanner small{color:#94a3b8;font-weight:500;letter-spacing:0;margin-left:10px}
/* Modal overlay centré (doctrine zero chevauchement top/bottom right) */
.modal-bd{position:fixed;inset:0;background:rgba(3,7,18,.8);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:1000;padding:20px}
.modal-bd.open{display:flex}
.modal{background:linear-gradient(135deg,#0f1629,#1a2035);border:1.5px solid rgba(6,182,212,.35);border-radius:16px;padding:20px;max-width:560px;width:100%;max-height:90vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.6)}
.modal h2{font:800 14px Orbitron,sans-serif;color:#06b6d4;margin-bottom:14px;letter-spacing:.8px;display:flex;align-items:center;gap:10px;justify-content:space-between}
.modal .mclose{background:none;border:none;color:#94a3b8;font-size:22px;cursor:pointer;line-height:1;padding:0 6px}
.modal .mclose:hover{color:#ef4444}
.modal .preview{display:flex;align-items:center;gap:12px;padding:14px;background:rgba(6,182,212,.04);border-radius:10px;margin-bottom:14px;border:1px solid rgba(6,182,212,.15)}
.modal .preview .pav{width:64px;height:64px;border-radius:50%;background:rgba(255,255,255,.06);border:2px solid rgba(6,182,212,.45);display:flex;align-items:center;justify-content:center;font-size:38px;line-height:1;overflow:hidden;flex-shrink:0}
.modal .preview .pav img{width:100%;height:100%;object-fit:cover;border-radius:50%}
.modal .preview .pinfo{flex:1;min-width:0}
.modal .preview .pname{font-weight:800;font-size:13px;color:#e2e8f0;word-break:break-word}
.modal .preview .pmeta{font-size:10px;color:#94a3b8;margin-top:3px}
.modal .sec{margin-bottom:12px}
.modal .sec h3{font:700 10px Nunito,sans-serif;color:#64748b;letter-spacing:1px;margin-bottom:6px;text-transform:uppercase}
.modal .emo-grid{display:grid;grid-template-columns:repeat(10,1fr);gap:4px;max-height:200px;overflow-y:auto;padding:6px;background:rgba(0,0,0,.3);border-radius:8px;border:1px solid rgba(255,255,255,.05)}
.modal .emo-grid button{background:rgba(255,255,255,.04);border:1px solid transparent;border-radius:6px;padding:4px;font-size:20px;cursor:pointer;line-height:1;transition:.1s}
.modal .emo-grid button:hover{background:rgba(6,182,212,.15);border-color:rgba(6,182,212,.4);transform:scale(1.1)}
.modal .emo-grid button.sel{background:rgba(6,182,212,.25);border-color:#06b6d4}
.modal .emo-input{width:100%;padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.3);color:#e2e8f0;font-size:14px;outline:none}
.modal .emo-input:focus{border-color:#06b6d4}
.modal .url-input{width:100%;padding:7px 10px;border-radius:6px;border:1px solid rgba(255,255,255,.1);background:rgba(0,0,0,.3);color:#e2e8f0;font:600 10px monospace;outline:none}
.modal .url-input:focus{border-color:#06b6d4}
.modal .actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end;flex-wrap:wrap}
.modal .actions .btn{padding:8px 16px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.03);color:#94a3b8;font:700 11px Nunito,sans-serif;cursor:pointer;letter-spacing:.5px}
.modal .actions .btn:hover{background:rgba(255,255,255,.08)}
.modal .actions .btn.primary{background:linear-gradient(135deg,#06b6d4,#8b5cf6);color:#fff;border-color:transparent}
.modal .actions .btn.primary:hover{transform:translateY(-1px);box-shadow:0 4px 14px rgba(6,182,212,.4)}
.modal .actions .btn.primary:disabled{opacity:.5;cursor:wait}
.modal .status{font-size:11px;padding:6px 10px;border-radius:6px;margin-top:8px;display:none}
.modal .status.ok{display:block;background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.3)}
.modal .status.err{display:block;background:rgba(239,68,68,.12);color:#f87171;border:1px solid rgba(239,68,68,.3)}
.modal .status.loading{display:block;background:rgba(6,182,212,.12);color:#06b6d4;border:1px solid rgba(6,182,212,.3)}
.modal .cat-tabs{display:flex;gap:4px;margin-bottom:6px;flex-wrap:wrap}
.modal .cat-tabs button{padding:3px 8px;font-size:9px;font-weight:700;border-radius:4px;border:1px solid rgba(255,255,255,.08);background:none;color:#64748b;cursor:pointer;letter-spacing:.5px}
.modal .cat-tabs button.ac{background:rgba(6,182,212,.12);border-color:#06b6d4;color:#06b6d4}
</style> <script src="/js/wevia-a11y-auto.js" defer></script>
</head><body>
<!-- BETON-DOCTRINE-101 dual-dummy block (pages pub) -->
<div id="weval-global-logout" style="display:none!important;visibility:hidden!important" aria-hidden="true" data-beton-101="dummy-to-block-auto-injection"></div>
<a id="weval-gl" href="#" style="display:none!important;visibility:hidden!important" aria-hidden="true" data-beton-101="dummy-to-block-auto-injection" tabindex="-1"></a>
<div class="hdr">
<h1>🎨 AGENT AVATAR PICKER · SSOT v2.json</h1>
<div class="nav">
<a href="/agents-archi.html">← Architecture 3D</a>
<a href="/wevia-meeting-rooms.html">Meeting Rooms</a>
<a href="/enterprise-model.html">Enterprise</a>
<a href="/agents-fleet.html">Fleet</a>
</div>
</div>
<div class="bar">
<button class="fb ac" data-f="all">ALL</button>
<button class="fb" data-f="master">MASTER</button>
<button class="fb" data-f="human">HUMAN</button>
<button class="fb" data-f="tool">TOOL</button>
<button class="fb" data-f="gap">GAP ONLY</button>
<button class="fb" id="editToggle" data-edit="off" style="border-color:rgba(255,193,7,.35);color:#ffc107" title="Active l'édition : clique sur un agent pour changer son avatar">✎ ÉDITION</button>
<input class="search" id="srch" placeholder="🔍 Search agent...">
<div class="stats" id="stats">Loading...</div>
</div>
<div class="editbanner" id="editBanner">MODE ÉDITION ACTIF <small>Clique sur un agent pour changer son avatar · ESC pour fermer</small></div>
<div class="grid" id="G"><div class="empty">Loading SSOT (agent-avatars-v2.json)...</div></div>
<!-- Modal picker (WAVE-273 zero chevauchement top/bottom-right) -->
<div class="modal-bd" id="modalBd" role="dialog" aria-labelledby="mTitle" aria-modal="true">
<div class="modal">
<h2 id="mTitle">✎ Changer l'avatar <button class="mclose" onclick="closeModal()" aria-label="Fermer">×</button></h2>
<div class="preview">
<div class="pav" id="pav">?</div>
<div class="pinfo">
<div class="pname" id="pname"></div>
<div class="pmeta" id="pmeta"></div>
</div>
</div>
<div class="sec">
<h3>Emoji libre</h3>
<input class="emo-input" id="emoInput" placeholder="Tape un emoji ou colle un caractère" maxlength="20"/>
</div>
<div class="sec">
<h3>Ou choisir dans la grille</h3>
<div class="cat-tabs" id="catTabs"></div>
<div class="emo-grid" id="emoGrid"></div>
</div>
<div class="sec">
<h3>URL Dicebear (avancé, optionnel)</h3>
<input class="url-input" id="urlInput" placeholder="https://api.dicebear.com/9.x/..." />
</div>
<div class="status" id="mStatus"></div>
<div class="actions">
<button class="btn" onclick="closeModal()">Annuler</button>
<button class="btn primary" id="saveBtn" onclick="saveAvatar()">Enregistrer</button>
</div>
</div>
</div>
<script>
let DATA = {};
let activeFilter = 'all';
let activeSearch = '';
async function load(){
try {
const r = await fetch('/api/agent-avatars-v2.json?t=' + Date.now());
DATA = await r.json();
render();
} catch(e) {
document.getElementById('G').innerHTML = '<div class="empty">ERR loading SSOT: ' + e.message + '</div>';
}
}
function render(){
const G = document.getElementById('G');
const entries = Object.entries(DATA);
const filtered = entries.filter(([n, a]) => {
if (activeSearch && !n.toLowerCase().includes(activeSearch)) return false;
if (activeFilter === 'all') return true;
if (activeFilter === 'gap') return a.isGap;
return a.persona === activeFilter;
});
if (!filtered.length) {
G.innerHTML = '<div class="empty">No agent matches</div>';
document.getElementById('stats').innerHTML = '<b>0</b> / ' + entries.length;
return;
}
G.innerHTML = filtered.map(([n, a]) => {
const cls = ['card', a.persona || 'human'];
if (a.isGap) cls.push('gap');
const safeN = n.replace(/[<>"']/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
return `<div class="${cls.join(' ')}" title="${safeN}">
<div class="av">${a.emoji || '👤'}</div>
<div class="nm">${safeN}</div>
<div class="meta">
${a.isGap ? '<span class="tag gap">GAP</span>' : ''}
${a.role ? '<span class="tag role">' + a.role + '</span>' : ''}
</div>
</div>`;
}).join('');
document.getElementById('stats').innerHTML = '<b>' + filtered.length + '</b> / ' + entries.length + ' agents';
}
document.querySelectorAll('.fb').forEach(b => b.onclick = () => {
document.querySelectorAll('.fb').forEach(x => x.classList.remove('ac'));
b.classList.add('ac');
activeFilter = b.dataset.f;
render();
});
document.getElementById('srch').oninput = e => { activeSearch = e.target.value.toLowerCase().trim(); render(); };
// ============================================================
// WAVE-273 : MODE ÉDITION AVATAR (enrichir pas écraser)
// ============================================================
let editMode = false;
let currentAgent = null;
// Emoji library (catégorisée, pas d'API externe)
const EMOJI_CATS = {
'Humains': ['👤','👥','🧑','👨','👩','🧔','👱','👴','👵','🧓','👶','🧒','🧑‍🦰','🧑‍🦱','🧑‍🦳','🧑‍🦲','👨‍💼','👩‍💼','🧑‍💼','👨‍💻','👩‍💻','🧑‍💻','🧑‍🔬','👨‍🔬','👩‍🔬','🧑‍🎨','👨‍🎨','👩‍🎨','🧑‍🚀','👨‍🚀','👩‍🚀','🧑‍⚖️','🧑‍🏫','👨‍🏫','👩‍🏫','🧑‍⚕️','👨‍⚕️','👩‍⚕️','🧑‍🔧','👨‍🔧','👩‍🔧','🧑‍🌾','👨‍🌾','👩‍🌾','🧑‍🎓','👨‍🎓','👩‍🎓','🧑‍🎤','👨‍🎤','👩‍🎤','🧑‍🍳','👨‍🍳','👩‍🍳','🥷','🦸','🦸‍♂️','🦸‍♀️','🧙','🧙‍♂️','🧙‍♀️','🧚','🧝','🧛','🧟','🧞','👑','🤵','👰','🎅','🤶','🧑‍✈️','👨‍✈️','👩‍✈️','💂','🕵️','🤠','👻','🤖','👽','💀'],
'Corps': ['👀','👁️','👂','👃','🧠','🫀','🫁','🦷','🦴','💪','🦾','🦿','🤝','👍','👎','👌','✌️','🤞','🤟','🤙','👈','👉','👆','👇','☝️','✋','🤚','🖐️','🖖','👋','🫶','🙌','👐','🤲','🙏','💋','👄','🫦'],
'Tech': ['💻','🖥️','⌨️','🖱️','🖲️','💾','💿','📀','🧮','📡','🛰️','🚀','📱','📲','📞','☎️','📟','📠','📺','📻','🎙️','🎚️','🎛️','🧭','⏱️','⏲️','⏰','🕰️','⌛','⏳','🔋','🪫','🔌','💡','🔦','🕯️','🧯','🛢️','⚗️','🧪','🧫','🧬','🔬','🔭','📡','🛰️','⚙️','🔧','🔨','⛏️','🪓','🛠️','🗜️','🔩','🪛','⚡','🔗','🌐','🧰','🪜','🧲'],
'AI_Robots': ['🤖','👽','🛸','👾','🦾','🦿','🧠','🫧','⚛️','🧬','🧪','🧫','🪬','🔮','📡','🛰️','💠','🔷','🔶','💻','🖥️','👁️','🫥'],
'Travail': ['💼','📁','📂','🗂️','📅','📆','🗓️','📇','📋','📌','📍','🗃️','🗄️','📎','🖇️','✂️','🖊️','🖋️','✒️','🖌️','🖍️','📝','✏️','📏','📐','📊','📈','📉','🧾','🗞️','📰','📖','📚','📓','📔','📒','📕','📗','📘','📙','🏢','🏬','🏭','🏪','🏦','🏛️','🏣','🏤','💳','💰','💵','💶','💷','💴','💸','🧾'],
'Animaux': ['🦊','🐺','🦝','🦁','🐯','🐅','🐆','🐴','🦓','🦌','🦬','🐂','🐃','🐄','🐮','🐷','🐗','🐽','🐏','🐑','🐐','🐫','🦙','🦒','🐘','🦣','🦏','🦛','🐁','🐀','🐹','🐰','🐇','🐿️','🦫','🦔','🦇','🐻','🐨','🐼','🦥','🦦','🦨','🦘','🦡','🐾','🦃','🐔','🐓','🐣','🐤','🐥','🐦','🐧','🕊️','🦅','🦆','🦢','🦉','🦩','🦚','🦜','🐸','🐊','🐢','🦎','🐍','🐲','🐉','🦕','🦖','🐳','🐋','🐬','🦭','🐟','🐠','🐡','🦈','🐙','🐚','🐌','🦋','🐛','🐜','🐝','🪲','🐞','🦗','🕷️','🕸️','🦂'],
'Plantes': ['🌱','🌲','🌳','🌴','🌵','🌾','🌿','☘️','🍀','🍁','🍂','🍃','🪴','🌺','🌻','🌼','🌷','🌹','🥀','🏵️','💐','🌸','💮','🪷','🍄'],
'Nourriture': ['🍎','🍐','🍊','🍋','🍌','🍉','🍇','🍓','🫐','🍈','🍒','🍑','🥭','🍍','🥥','🥝','🍅','🍆','🥑','🥦','🥬','🥒','🌶️','🫑','🌽','🥕','🫒','🧄','🧅','🥔','🍠','🫘','🥐','🍞','🥖','🥨','🥯','🧀','🥚','🍳','🧈','🥞','🧇','🥓','🥩','🍗','🍖','🌭','🍔','🍟','🍕','🫓','🥪','🌮','🌯','🫔','🥙','🧆','🍝','🍜','🍲','🍛','🍣','🍱','🥟','🦪','🍤','🍙','🍚','🍘','🥮','🍢','🍡','🍧','🍨','🍦','🥧','🧁','🍰','🎂','🍮','🍭','🍬','🍫','🍿','🍩','🍪','🌰','🥜','🍯','🥛','🍼','☕','🍵','🧃','🥤','🧋','🍶','🍺','🍻','🥂','🍷','🥃','🍸','🍹','🍾','🧊'],
'Objets_Outils': ['📦','🎁','🧸','🪀','🎀','🎊','🎉','🎈','🪄','🔮','🎭','🎨','🖼️','🎬','🎤','🎧','🎼','🎹','🥁','🎷','🎺','🎸','🪕','🎻','🪗','♟️','🎯','🎳','🎱','🪀','🪁','🎣','🤿','🎿','🛷','⛸️','🏹','🛡️','⚔️','🗡️','🪓','🔫','💣','🧨','🪓','🔨','⛏️','⚒️','🛠️','🗜️','🔩','⚙️','🧱','⛓️','🪝','🧰','🧲','🪜','🔬','🔭','📡','💉','🩸','💊','🩹','🩺','🏋️','🤸','🧗','🏇','⛷️','🏂','🏄','🚣','🏊','🚴','🚵'],
'Symboles': ['✨','⭐','🌟','💫','🔥','💥','💢','💯','💯','🆒','🆕','🆗','🆙','🆓','🆖','🔰','⚜️','🔱','🎖️','🏅','🏆','🥇','🥈','🥉','🎗️','🎫','🎟️','🎪','🎨','🔔','🔕','📢','📣','📯','💠','🔷','🔶','🔹','🔸','🟠','🟡','🟢','🔵','🟣','🟤','⚫','⚪','🟥','🟧','🟨','🟩','🟦','🟪','🟫','⬛','⬜','🔺','🔻','🔳','🔲','♾️','⚛️','☯️','🧿','🪬','🎴','🃏','🀄','🎰','🎱','🔺','🔻','💎','🏳️','🏴','🏁','🚩','🎌'],
'Meteo_Espace': ['☀️','🌤️','⛅','🌥️','🌦️','🌧️','⛈️','🌩️','⚡','🌨️','❄️','☃️','⛄','🌬️','💨','🌪️','🌫️','🌈','☔','💧','💦','🌊','☄️','🔥','🌀','🌋','🌏','🌍','🌎','🪐','🌕','🌖','🌗','🌘','🌑','🌒','🌓','🌔','🌙','🌚','🌛','🌜','☀️','🌞','⭐','🌟','💫','✨','🌠','🌌'],
'Batiments_Lieux': ['🏰','🏯','🏟️','🎡','🎢','🎠','⛩️','🕌','🕍','⛪','🛕','🕋','⛲','🏗️','🧱','🏭','🏢','🏬','🏣','🏤','🏥','🏦','🏨','🏪','🏫','🏩','💒','🏛️','🗽','🗼','🗻','🏔️','⛰️','🌋','🗾','🏕️','🏖️','🏜️','🏝️','🏞️','🛣️','🛤️','🏙️','🌃','🌉','🌁','🌆','🌇','🏘️','🏚️','🏠','🏡','🏟️'],
'Transport': ['🚗','🚕','🚙','🚌','🚎','🏎️','🚓','🚑','🚒','🚐','🛻','🚚','🚛','🚜','🛵','🏍️','🛺','🚲','🛴','🛹','🛼','🚏','🚇','🚈','🚉','🚊','🚝','🚞','🚋','🚃','🚂','🚆','🚄','🚅','🚈','🚟','🚠','🚡','⛴️','🚢','🛳️','⛵','🛥️','🚤','🛶','⚓','🛫','🛬','🛩️','✈️','🛪','🛰️','🚀','🛸','🚁','🪂','🎢','🎡','🎠'],
'Sports': ['⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🪀','🏓','🏸','🏒','🏑','🥍','🏏','🥅','⛳','🪁','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸️','🥌','🎿','⛷️','🏂','🪂','🏋️','🤼','🤸','⛹️','🤺','🤾','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚴','🚵','🏆','🏅','🎖️'],
'Drapeaux': ['🏁','🚩','🎌','🏴','🏳️','🏳️‍🌈','🏴‍☠️','🇫🇷','🇲🇦','🇹🇳','🇩🇿','🇪🇺','🇺🇸','🇬🇧','🇩🇪','🇮🇹','🇪🇸','🇵🇹','🇳🇱','🇧🇪','🇨🇭','🇦🇹','🇸🇪','🇳🇴','🇩🇰','🇫🇮','🇮🇪','🇨🇦','🇲🇽','🇧🇷','🇦🇷','🇨🇱','🇨🇴','🇯🇵','🇰🇷','🇨🇳','🇮🇳','🇦🇺','🇳🇿','🇷🇺','🇹🇷','🇸🇦','🇦🇪','🇪🇬','🇿🇦','🇳🇬','🇰🇪'],
};
function setEditMode(on){
editMode = !!on;
const btn = document.getElementById('editToggle');
btn.dataset.edit = editMode ? 'on' : 'off';
btn.textContent = editMode ? '✎ ÉDITION ON' : '✎ ÉDITION';
document.getElementById('editBanner').classList.toggle('on', editMode);
render();
}
function render(){
const G = document.getElementById('G');
const entries = Object.entries(DATA);
const filtered = entries.filter(([n, a]) => {
if (activeSearch && !n.toLowerCase().includes(activeSearch)) return false;
if (activeFilter === 'all') return true;
if (activeFilter === 'gap') return a.isGap;
return a.persona === activeFilter;
});
if (!filtered.length) {
G.innerHTML = '<div class="empty">No agent matches</div>';
document.getElementById('stats').innerHTML = '<b>0</b> / ' + entries.length;
return;
}
G.innerHTML = filtered.map(([n, a]) => {
const cls = ['card', a.persona || 'human'];
if (a.isGap) cls.push('gap');
if (editMode) cls.push('editmode');
const safeN = n.replace(/[<>"']/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[c]);
return `<div class="${cls.join(' ')}" data-name="${safeN}" title="${safeN}${editMode ? ' — Click pour éditer' : ''}">
<div class="av">${a.emoji ? a.emoji : (a.url ? '<img src="' + a.url + '" alt="" style="width:100%;height:100%;border-radius:50%;object-fit:cover" onerror="this.replaceWith(document.createTextNode(\'\\u{2753}\'))"/>' : '?')}</div>
<div class="nm">${safeN}</div>
<div class="meta">
${a.isGap ? '<span class="tag gap">GAP</span>' : ''}
${a.role ? '<span class="tag role">' + a.role + '</span>' : ''}
</div>
</div>`;
}).join('');
document.getElementById('stats').innerHTML = '<b>' + filtered.length + '</b> / ' + entries.length + ' agents';
// Attach click handlers to cards when edit mode is ON
if (editMode) {
document.querySelectorAll('#G .card').forEach(c => {
c.onclick = () => openModal(c.dataset.name);
});
}
}
function openModal(agentName){
// Decode HTML entities (name was escaped)
const temp = document.createElement('textarea');
temp.innerHTML = agentName;
const realName = temp.value;
if (!DATA[realName]) return;
currentAgent = realName;
const a = DATA[realName];
document.getElementById('pname').textContent = realName;
document.getElementById('pmeta').textContent = [a.persona, a.role, a.isGap ? 'GAP' : ''].filter(Boolean).join(' · ');
document.getElementById('pav').innerHTML = a.emoji || (a.url ? `<img src="${a.url}" alt="">` : '?');
document.getElementById('emoInput').value = a.emoji || '';
document.getElementById('urlInput').value = a.url || '';
document.getElementById('mStatus').className = 'status';
document.getElementById('mStatus').textContent = '';
document.getElementById('saveBtn').disabled = false;
// Build emoji categorized grid
const tabs = document.getElementById('catTabs');
const grid = document.getElementById('emoGrid');
const cats = Object.keys(EMOJI_CATS);
tabs.innerHTML = cats.map((c,i) => `<button data-cat="${c}" class="${i===0?'ac':''}">${c}</button>`).join('');
const showCat = (c) => {
grid.innerHTML = EMOJI_CATS[c].map(e => `<button data-emo="${e}">${e}</button>`).join('');
grid.querySelectorAll('button').forEach(b => {
b.onclick = () => {
grid.querySelectorAll('button').forEach(x => x.classList.remove('sel'));
b.classList.add('sel');
document.getElementById('emoInput').value = b.dataset.emo;
document.getElementById('pav').innerHTML = b.dataset.emo;
};
});
};
showCat(cats[0]);
tabs.querySelectorAll('button').forEach(b => {
b.onclick = () => {
tabs.querySelectorAll('button').forEach(x => x.classList.remove('ac'));
b.classList.add('ac');
showCat(b.dataset.cat);
};
});
// Live preview update
document.getElementById('emoInput').oninput = e => {
const v = e.target.value;
document.getElementById('pav').innerHTML = v || (a.url ? `<img src="${a.url}" alt="">` : '?');
};
document.getElementById('modalBd').classList.add('open');
}
function closeModal(){
document.getElementById('modalBd').classList.remove('open');
currentAgent = null;
}
async function saveAvatar(){
if (!currentAgent) return;
const emoji = document.getElementById('emoInput').value.trim();
const url = document.getElementById('urlInput').value.trim();
const stat = document.getElementById('mStatus');
const btn = document.getElementById('saveBtn');
const body = { agent: currentAgent };
if (emoji) body.emoji = emoji;
if (url) body.url = url;
if (!emoji && !url) {
stat.className = 'status err';
stat.textContent = 'Au moins un champ (emoji ou URL) doit être rempli';
return;
}
btn.disabled = true;
stat.className = 'status loading';
stat.textContent = 'Enregistrement...';
try {
const r = await fetch('/api/agent-avatar-update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const j = await r.json();
if (!j.ok) {
stat.className = 'status err';
stat.textContent = 'Erreur : ' + (j.error || 'unknown');
btn.disabled = false;
return;
}
// Update local DATA
if (emoji) DATA[currentAgent].emoji = emoji;
if (url) DATA[currentAgent].url = url;
stat.className = 'status ok';
stat.textContent = '✓ Sauvegardé · backup : ' + j.backup;
render();
setTimeout(closeModal, 900);
} catch (e) {
stat.className = 'status err';
stat.textContent = 'Erreur réseau : ' + e.message;
btn.disabled = false;
}
}
// Bindings
document.getElementById('editToggle').onclick = () => setEditMode(!editMode);
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
document.getElementById('modalBd').onclick = (e) => { if (e.target.id === 'modalBd') closeModal(); };
load();
</script>
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t34final) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
</body></html>