389 lines
27 KiB
HTML
389 lines
27 KiB
HTML
<!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 => ({'<':'<','>':'>','"':'"',"'":'''})[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 => ({'<':'<','>':'>','"':'"',"'":'''})[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>
|