696 lines
36 KiB
HTML
696 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<meta name="description" content="Simulez le ROI par agent WEVAL : gains quantitatifs & qualitatifs, calculs en temps réel selon taille, maturité IA et secteur.">
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WEVAL · ROI Simulator — Gains quantitatifs & qualitatifs par agent</title>
|
||
<style>
|
||
:root {
|
||
--bg-0:#05060a; --bg-1:#0b0d15; --bg-2:#11141f; --bg-3:#171b2a; --bg-4:#1e2336;
|
||
--border:rgba(99,102,241,0.15); --border-h:rgba(99,102,241,0.35);
|
||
--text:#e2e8f0; --dim:#94a3b8; --mute:#64748b;
|
||
--accent:#14b8a6; --accent2:#6366f1; --purple:#a855f7; --cyan:#06b6d4;
|
||
--ok:#22c55e; --warn:#f59e0b; --err:#ef4444; --rose:#f43f5e; --gold:#eab308;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: 'Inter', system-ui, sans-serif;
|
||
background: radial-gradient(ellipse at top, #0f1420, #05060a 65%);
|
||
color: var(--text); min-height: 100vh; font-size: 13.5px; line-height: 1.55;
|
||
}
|
||
.container { max-width: 1680px; margin: 0 auto; padding: 28px 32px 80px; }
|
||
|
||
/* HEADER */
|
||
header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
||
header h1 { font-size: 26px; font-weight: 800; background: linear-gradient(90deg, #22d3ee, #a855f7, #eab308); -webkit-background-clip: text; background-clip: text; color: transparent; letter-spacing: -0.4px; }
|
||
header .sub { color: var(--dim); font-size: 13px; margin-top: 6px; max-width: 820px; }
|
||
.actions { display: flex; gap: 9px; }
|
||
.btn { padding: 8px 15px; background: var(--bg-2); border: 1px solid var(--border); color: var(--text); border-radius: 8px; font-size: 12.5px; cursor: pointer; text-decoration: none; font-family: inherit; transition: all .2s; }
|
||
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
||
.btn-pri { background: linear-gradient(135deg, var(--gold), var(--warn)); color: #0b0d15; font-weight: 700; border: none; }
|
||
.pulse { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 0 rgba(34,197,94,.7); animation: pulse 2s infinite; margin-right: 4px; }
|
||
@keyframes pulse { 0%{box-shadow:0 0 0 0 rgba(34,197,94,.7)} 70%{box-shadow:0 0 0 8px rgba(34,197,94,0)} 100%{box-shadow:0 0 0 0 rgba(34,197,94,0)} }
|
||
|
||
/* MAIN LAYOUT */
|
||
.main-grid { display: grid; grid-template-columns: 320px 1fr 340px; gap: 18px; }
|
||
@media(max-width: 1400px) { .main-grid { grid-template-columns: 1fr; } }
|
||
|
||
/* PARAMETER PANEL (left) */
|
||
.panel { background: var(--bg-1); border: 1px solid var(--border); border-radius: 14px; padding: 20px; position: sticky; top: 18px; }
|
||
.panel h3 { font-size: 15px; font-weight: 700; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
||
.param-group { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--bg-3); }
|
||
.param-group:last-child { border: none; padding-bottom: 0; margin-bottom: 0; }
|
||
.param-label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.6px; font-weight: 600; margin-bottom: 8px; display: block; }
|
||
.param-pills { display: flex; flex-wrap: wrap; gap: 5px; }
|
||
.param-pill { padding: 6px 10px; background: var(--bg-3); border: 1px solid var(--border); border-radius: 16px; font-size: 11px; cursor: pointer; color: var(--dim); transition: all .2s; flex: 1; text-align: center; min-width: 60px; }
|
||
.param-pill:hover { color: var(--text); border-color: var(--accent); }
|
||
.param-pill.active { background: linear-gradient(135deg, var(--accent2), var(--purple)); color: white; border: none; font-weight: 600; }
|
||
.param-info { font-size: 10.5px; color: var(--mute); margin-top: 6px; padding: 6px 8px; background: var(--bg-2); border-radius: 5px; line-height: 1.4; }
|
||
select { width: 100%; padding: 8px 10px; background: var(--bg-3); border: 1px solid var(--border); color: var(--text); border-radius: 6px; font-family: inherit; font-size: 12px; }
|
||
|
||
/* CENTER: AGENT LIST */
|
||
.center-col { display: flex; flex-direction: column; gap: 16px; }
|
||
.quick-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||
.qs { background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; padding: 14px; position: relative; overflow: hidden; }
|
||
.qs::before { content: ''; position: absolute; left: 0; top: 0; width: 3px; height: 100%; background: var(--accent); }
|
||
.qs.gold::before { background: linear-gradient(180deg, var(--gold), var(--warn)); }
|
||
.qs.cy::before { background: var(--cyan); }
|
||
.qs.pu::before { background: var(--purple); }
|
||
.qs .lbl { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
||
.qs .val { font-size: 22px; font-weight: 800; color: var(--text); line-height: 1; margin-top: 4px; }
|
||
.qs .val.gold { background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||
.qs .sub { font-size: 10px; color: var(--mute); margin-top: 3px; }
|
||
|
||
.filter-bar { display: flex; gap: 5px; flex-wrap: wrap; padding: 12px; background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; align-items: center; }
|
||
.filter-bar .lbl { font-size: 11px; color: var(--dim); font-weight: 600; margin-right: 6px; }
|
||
.filter-pill { padding: 5px 10px; background: var(--bg-3); border: 1px solid var(--border); color: var(--dim); border-radius: 14px; font-size: 11px; cursor: pointer; font-family: inherit; transition: all .2s; }
|
||
.filter-pill:hover { color: var(--text); border-color: var(--accent); }
|
||
.filter-pill.active { background: linear-gradient(135deg, var(--accent), var(--cyan)); color: white; border: none; font-weight: 600; }
|
||
.filter-bar button.btn-sml { margin-left: auto; padding: 5px 11px; font-size: 10.5px; }
|
||
|
||
.agent-list { display: flex; flex-direction: column; gap: 8px; max-height: none; }
|
||
.agent-card { background: var(--bg-1); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; display: grid; grid-template-columns: 26px 1fr 130px 100px 100px; gap: 14px; align-items: center; transition: all .15s; cursor: pointer; }
|
||
.agent-card:hover { background: var(--bg-2); border-color: var(--border-h); }
|
||
.agent-card.selected { background: linear-gradient(135deg, rgba(20,184,166,0.08), rgba(99,102,241,0.06)); border-color: var(--accent); }
|
||
.agent-card input[type=checkbox] { width: 18px; height: 18px; accent-color: var(--accent); cursor: pointer; }
|
||
.ag-main .ag-name { font-size: 13px; font-weight: 600; color: var(--text); display: flex; align-items: center; gap: 6px; }
|
||
.ag-main .ag-name::before { content: '🤖'; }
|
||
.ag-main .ag-pain { font-size: 11px; color: var(--dim); margin-top: 3px; line-height: 1.3; }
|
||
.ag-main .ag-meta { display: flex; gap: 6px; margin-top: 5px; flex-wrap: wrap; }
|
||
.ag-main .ag-meta span { font-size: 9.5px; padding: 1px 6px; border-radius: 4px; background: var(--bg-3); color: var(--dim); }
|
||
.ag-main .ag-meta .dept { background: rgba(168,85,247,0.15); color: #d4a7fa; }
|
||
.ag-sav { text-align: right; }
|
||
.ag-sav .v { font-size: 16px; font-weight: 800; background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; font-family: 'JetBrains Mono', monospace; }
|
||
.ag-sav .l { font-size: 9.5px; color: var(--dim); }
|
||
.ag-quali { text-align: center; }
|
||
.ag-quali .v { font-size: 16px; font-weight: 800; color: var(--accent); font-family: 'JetBrains Mono', monospace; }
|
||
.ag-quali .l { font-size: 9.5px; color: var(--dim); }
|
||
.ag-payback { text-align: center; }
|
||
.ag-payback .v { font-size: 14px; font-weight: 700; color: var(--purple); font-family: 'JetBrains Mono', monospace; }
|
||
.ag-payback .l { font-size: 9.5px; color: var(--dim); }
|
||
|
||
/* RIGHT COL: SELECTED PACK + RADAR + CURVE */
|
||
.right-col { display: flex; flex-direction: column; gap: 14px; position: sticky; top: 18px; }
|
||
.selection-box { background: var(--bg-1); border: 1px solid var(--border); border-radius: 12px; padding: 18px; }
|
||
.selection-box h3 { font-size: 14px; font-weight: 700; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
|
||
.selection-box h3 .count-badge { font-size: 11px; background: var(--accent); color: white; padding: 2px 8px; border-radius: 10px; font-weight: 700; }
|
||
.pack-kpis { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 14px; }
|
||
.pack-kpi { padding: 10px; background: var(--bg-2); border-radius: 8px; border-left: 2px solid var(--accent); }
|
||
.pack-kpi.gold { border-left-color: var(--gold); }
|
||
.pack-kpi.rose { border-left-color: var(--rose); }
|
||
.pack-kpi .l { font-size: 9.5px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
||
.pack-kpi .v { font-size: 17px; font-weight: 800; color: var(--text); margin-top: 3px; line-height: 1; font-family: 'JetBrains Mono', monospace; }
|
||
.pack-kpi.gold .v { background: linear-gradient(135deg, var(--gold), var(--warn)); -webkit-background-clip: text; background-clip: text; color: transparent; }
|
||
|
||
/* Radar chart quali */
|
||
.radar-wrap { position: relative; text-align: center; }
|
||
.radar-wrap svg { width: 100%; max-width: 280px; }
|
||
|
||
/* 12m curve */
|
||
.curve-wrap { background: var(--bg-2); border-radius: 8px; padding: 12px; }
|
||
|
||
/* Empty state */
|
||
.empty { text-align: center; padding: 24px 12px; color: var(--mute); font-size: 12px; }
|
||
|
||
.loading { text-align: center; padding: 60px; color: var(--dim); }
|
||
.spinner { width: 40px; height: 40px; border: 3px solid var(--bg-3); border-top-color: var(--accent); border-radius: 50%; margin: 0 auto 16px; animation: spin 1s linear infinite; }
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
@media(max-width: 1400px) {
|
||
.panel, .right-col { position: static; }
|
||
.quick-stats { grid-template-columns: repeat(2, 1fr); }
|
||
.agent-card { grid-template-columns: 24px 1fr; }
|
||
.ag-sav, .ag-quali, .ag-payback { display: none; }
|
||
}
|
||
|
||
/* === OPUS RESPONSIVE FIX v2 19avr — append-only, doctrine #14 === */
|
||
@media(max-width: 480px) {
|
||
html, body { overflow-x: hidden !important; max-width: 100vw; }
|
||
body, main, section, article { word-break: break-word; overflow-wrap: anywhere; }
|
||
img, video, iframe, canvas, svg, table, pre, code { max-width: 100% !important; }
|
||
pre, code { white-space: pre-wrap; word-break: break-all; }
|
||
table { display: block; overflow-x: auto; }
|
||
.container, [class*="container"], [class*="wrapper"] { max-width: 100vw !important; padding-left: 12px !important; padding-right: 12px !important; }
|
||
[class*="grid"], [class*="-grid"] { grid-template-columns: 1fr !important; gap: 10px !important; }
|
||
[class*="kpi"], [class*="stats"], [class*="-cards"] { grid-template-columns: 1fr !important; }
|
||
header, nav, footer { flex-wrap: wrap !important; }
|
||
header > *, nav > *, footer > * { max-width: 100%; }
|
||
h1 { font-size: 22px !important; word-break: break-word; }
|
||
h2 { font-size: 18px !important; }
|
||
.pitch, [class*="pitch"], [class*="hero"] { word-break: break-word; overflow-wrap: anywhere; }
|
||
}
|
||
/* === OPUS RESPONSIVE FIX v2 END === */
|
||
|
||
/* === WEVIA Gemini Rolling v2 VISIBLE Enrichment (wave 306 batch) === */
|
||
.kpi,[class*="card"],[class*="panel"],[class*="room"],.stat-card,.metric-card,.hub-card,.widget,.stat,.box{position:relative!important}
|
||
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card{animation:geV2Entrance .8s cubic-bezier(.34,1.56,.64,1) backwards}
|
||
.kpi:nth-child(1),[class*="card"]:nth-child(1){animation-delay:0s}
|
||
.kpi:nth-child(2),[class*="card"]:nth-child(2){animation-delay:.09s}
|
||
.kpi:nth-child(3),[class*="card"]:nth-child(3){animation-delay:.18s}
|
||
.kpi:nth-child(4),[class*="card"]:nth-child(4){animation-delay:.27s}
|
||
.kpi:nth-child(5),[class*="card"]:nth-child(5){animation-delay:.36s}
|
||
.kpi:nth-child(6),[class*="card"]:nth-child(6){animation-delay:.45s}
|
||
@keyframes geV2Entrance{from{opacity:0;transform:translateY(24px) scale(.94)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||
.kpi,[class*="card"],.stat-card,.metric-card,.hub-card,.widget{border:1px solid transparent!important;box-shadow:0 0 0 1px rgba(236,72,153,.15),0 4px 16px rgba(0,0,0,.25)!important;transition:box-shadow .4s,transform .3s cubic-bezier(.34,1.56,.64,1),filter .3s!important}
|
||
.kpi:hover,[class*="card"]:hover,.stat-card:hover,.metric-card:hover,.hub-card:hover{transform:translateY(-6px) scale(1.03)!important;filter:brightness(1.2)!important;box-shadow:0 0 0 2px rgba(236,72,153,.6),0 12px 32px rgba(236,72,153,.25),0 0 24px rgba(78,205,196,.2)!important}
|
||
.kpi::before,[class*="card"]::before,.stat-card::before,.metric-card::before,.hub-card::before{content:"";position:absolute;top:12px;right:12px;width:10px;height:10px;border-radius:50%;background:radial-gradient(circle,#2ed573,#1a9a4e);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5);animation:geV2Pulse 1.6s ease-out infinite;z-index:100;pointer-events:none}
|
||
@keyframes geV2Pulse{0%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}50%{transform:scale(1.4);box-shadow:0 0 20px #2ed573,0 0 40px rgba(46,213,115,.8)}100%{transform:scale(1);box-shadow:0 0 12px #2ed573,0 0 24px rgba(46,213,115,.5)}}
|
||
body::after{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 70% 30%,transparent 40%,rgba(236,72,153,.06) 100%),radial-gradient(ellipse at 30% 70%,transparent 40%,rgba(78,205,196,.04) 100%);animation:geV2Ambient 10s ease-in-out infinite;z-index:0}
|
||
@keyframes geV2Ambient{0%,100%{opacity:.5}50%{opacity:1}}
|
||
h1,.header-title,.main-title,.hub-title,.page-title{background-image:linear-gradient(90deg,currentColor 0%,currentColor 40%,rgba(236,72,153,1) 50%,currentColor 60%,currentColor 100%)!important;background-size:200% auto!important;-webkit-background-clip:text!important;background-clip:text!important;-webkit-text-fill-color:transparent!important;animation:geV2Shimmer 5s linear infinite!important}
|
||
@keyframes geV2Shimmer{0%{background-position:200% center}100%{background-position:-200% center}}
|
||
/* Doctrine zero chevauchement - hide common offenders */
|
||
.opus-x-btn,.toggle-top-right-btn,.fab-corner{display:none!important}
|
||
/* === end WEVIA Gemini Rolling v2 batch === */
|
||
</style>
|
||
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-142645 -->
|
||
<style id="doctrine60-ux-direct">
|
||
|
||
/* DOCTRINE-60-UX-ENRICH injected-direct */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: 0; left: 0; width: 100vw; height: 100vh;
|
||
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
|
||
pointer-events: none;
|
||
z-index: -1;
|
||
}
|
||
.card, .kpi, .panel, .btn {
|
||
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
|
||
}
|
||
.card:hover, .kpi:hover, .panel:hover {
|
||
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
|
||
border-color: rgba(100,180,255,0.5);
|
||
}
|
||
@keyframes pulseD60 {
|
||
0%,100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.7; transform: scale(1.05); }
|
||
}
|
||
.pulse, .live-indicator, .active, .online {
|
||
animation: pulseD60 3s ease-in-out infinite;
|
||
}
|
||
.modal, .chat, .speech, .overlay {
|
||
backdrop-filter: blur(12px);
|
||
-webkit-backdrop-filter: blur(12px);
|
||
}
|
||
.enter-stagger {
|
||
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
|
||
}
|
||
@keyframes enterStagD60 {
|
||
from { opacity: 0; transform: translateY(20px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
|
||
<header>
|
||
<div>
|
||
<h1>🧮 ROI Simulator · Agent-by-Agent <span class="pulse"></span></h1>
|
||
<div class="sub">Simulez les gains quantitatifs & qualitatifs pour chaque agent WEVAL. Paramètres contextuels client (taille/maturité/vertical). Calculs temps réel.</div>
|
||
</div>
|
||
<div class="actions">
|
||
<a href="/pain-points-atlas.html" class="btn">← Atlas</a>
|
||
<a href="/weval-technology-platform.html" class="btn">🏠 WTP</a>
|
||
<button class="btn btn-pri" onclick="exportJSON()">📦 Export JSON</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-grid">
|
||
|
||
<!-- LEFT: PARAMS -->
|
||
<div class="panel">
|
||
<h3>⚙️ Contexte client</h3>
|
||
|
||
<div class="param-group">
|
||
<span class="param-label">🏢 Taille entreprise</span>
|
||
<div class="param-pills" id="size-pills"></div>
|
||
<div class="param-info" id="size-info">—</div>
|
||
</div>
|
||
|
||
<div class="param-group">
|
||
<span class="param-label">🧠 Maturité IA</span>
|
||
<div class="param-pills" id="maturity-pills"></div>
|
||
<div class="param-info" id="maturity-info">—</div>
|
||
</div>
|
||
|
||
<div class="param-group">
|
||
<span class="param-label">🏭 Vertical</span>
|
||
<select aria-label="form-field" id="vertical-select">
|
||
<option value="">— sélectionner —</option>
|
||
</select>
|
||
<div class="param-info" id="vert-info">—</div>
|
||
</div>
|
||
|
||
<div class="param-group">
|
||
<span class="param-label">💰 Multiplicateur global</span>
|
||
<div class="param-info" style="font-size:14px;text-align:center;color:var(--text);font-weight:700;font-family:'JetBrains Mono',monospace" id="mult-display">1.00×</div>
|
||
<div class="param-info" style="margin-top:4px;font-size:10px">= size × maturity × vertical (× 1.25 si département aligné vertical)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CENTER: AGENTS LIST -->
|
||
<div class="center-col">
|
||
<div class="quick-stats">
|
||
<div class="qs gold"><div class="lbl">Savings pack sélectionné</div><div class="val gold" id="qs-sav">0€</div><div class="sub" id="qs-sav-sub">— par an</div></div>
|
||
<div class="qs cy"><div class="lbl">Implementation cost</div><div class="val" id="qs-impl">0€</div><div class="sub" id="qs-impl-sub">one-shot</div></div>
|
||
<div class="qs pu"><div class="lbl">Payback pack</div><div class="val" id="qs-pay">— mois</div><div class="sub">moyenne pondérée</div></div>
|
||
<div class="qs"><div class="lbl">NPV 3 ans</div><div class="val" id="qs-npv">0€</div><div class="sub">savings - cost - 20% maint</div></div>
|
||
</div>
|
||
|
||
<div class="filter-bar">
|
||
<span class="lbl">Département :</span>
|
||
<div id="dept-filters" style="display:contents"></div>
|
||
<button class="btn btn-sml" onclick="selectAll()">✓ Tous</button>
|
||
<button class="btn btn-sml" onclick="selectNone()">✗ Aucun</button>
|
||
</div>
|
||
|
||
<div class="agent-list" id="agent-list"><div class="loading"><div class="spinner"></div>Chargement…</div></div>
|
||
</div>
|
||
|
||
<!-- RIGHT: SELECTION DETAILS -->
|
||
<div class="right-col">
|
||
<div class="selection-box">
|
||
<h3>🎯 Pack sélectionné <span class="count-badge" id="sel-count">0</span></h3>
|
||
|
||
<div class="pack-kpis">
|
||
<div class="pack-kpi gold"><div class="l">Savings/an</div><div class="v" id="pk-sav">0€</div></div>
|
||
<div class="pack-kpi rose"><div class="l">Impl cost</div><div class="v" id="pk-impl">0€</div></div>
|
||
<div class="pack-kpi"><div class="l">Quali avg</div><div class="v" id="pk-quali">—/100</div></div>
|
||
<div class="pack-kpi"><div class="l">Effort</div><div class="v" id="pk-effort">— MD</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="selection-box">
|
||
<h3>📐 Radar qualitatif (moyenne pack)</h3>
|
||
<div class="radar-wrap">
|
||
<svg viewBox="0 0 260 240" id="radar-svg">
|
||
<defs>
|
||
<radialGradient id="rgrad"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.5"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0.2"/></radialGradient>
|
||
</defs>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="selection-box">
|
||
<h3>📈 ROI cumulé 12 mois</h3>
|
||
<div class="curve-wrap">
|
||
<svg id="curve-svg" viewBox="0 0 280 130" style="width:100%" preserveAspectRatio="none"></svg>
|
||
<div style="display:flex;justify-content:space-between;font-size:9.5px;color:var(--mute);margin-top:6px">
|
||
<span>M1</span><span>M6</span><span>M12</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<script>
|
||
const API = '/api/wevia-v67-roi-simulator.php';
|
||
let DATA = null;
|
||
let sel = new Set();
|
||
let ctx = { size: 'mid', maturity: 'medium', vertical: '' };
|
||
let deptFilter = 'all';
|
||
|
||
async function load(){
|
||
const r = await fetch(API + '?t=' + Date.now());
|
||
DATA = await r.json();
|
||
|
||
// Normalize quali scores (backend bug: max was 355 instead of 100, divide by 3.55)
|
||
const maxObserved = Math.max(...DATA.agents.map(a => a.qualitative_composite_score || 0));
|
||
const normFactor = maxObserved > 100 ? 100 / maxObserved : 1;
|
||
DATA.agents.forEach(a => {
|
||
a.quali_normalized = Math.round(a.qualitative_composite_score * normFactor);
|
||
});
|
||
|
||
renderParams();
|
||
renderAgents();
|
||
recalc();
|
||
}
|
||
|
||
function fmtEur(n){
|
||
if (!n) return '0€';
|
||
if (Math.abs(n) >= 1000000) return (n/1000000).toFixed(2)+'M€';
|
||
if (Math.abs(n) >= 1000) return (n/1000).toFixed(0)+'k€';
|
||
return Math.round(n)+'€';
|
||
}
|
||
|
||
function renderParams(){
|
||
const sf = DATA.scaling_factors;
|
||
|
||
// Size pills
|
||
document.getElementById('size-pills').innerHTML = Object.entries(sf.company_size).map(([k,v]) =>
|
||
`<button class="param-pill ${ctx.size===k?'active':''}" data-g="size" data-v="${k}">${v.label.split(' ')[0]}</button>`
|
||
).join('');
|
||
updateSizeInfo();
|
||
|
||
// Maturity pills
|
||
document.getElementById('maturity-pills').innerHTML = Object.entries(sf.maturity_ai).map(([k,v]) =>
|
||
`<button class="param-pill ${ctx.maturity===k?'active':''}" data-g="maturity" data-v="${k}">${v.label.split(' ')[0]}</button>`
|
||
).join('');
|
||
updateMaturityInfo();
|
||
|
||
// Vertical dropdown
|
||
document.getElementById('vertical-select').innerHTML = '<option value="">— aucun (baseline) —</option>' +
|
||
Object.entries(sf.verticals).map(([k,v]) => `<option value="${k}">${v.label} (×${v.multiplier})</option>`).join('');
|
||
|
||
// Dept filters
|
||
const depts = [...new Set(DATA.agents.map(a => a.dept))].sort();
|
||
const deptLabels = { finance:'💰 Fin', supply:'📦 Sup', manufacturing:'🏭 Mfg', sales:'💼 Sales', hr:'👥 HR', marketing:'📈 Mkt', security:'🔐 Sec', operations:'⚙️ Ops', direction:'👔 Dir' };
|
||
document.getElementById('dept-filters').innerHTML =
|
||
`<button class="filter-pill ${deptFilter==='all'?'active':''}" data-d="all">Tous (${DATA.agents.length})</button>` +
|
||
depts.map(d => {
|
||
const n = DATA.agents.filter(a => a.dept === d).length;
|
||
return `<button class="filter-pill ${deptFilter===d?'active':''}" data-d="${d}">${deptLabels[d]||d} (${n})</button>`;
|
||
}).join('');
|
||
|
||
// Event listeners
|
||
document.querySelectorAll('.param-pill').forEach(b => b.onclick = (e) => {
|
||
const g = e.target.dataset.g, v = e.target.dataset.v;
|
||
ctx[g] = v;
|
||
document.querySelectorAll(`.param-pill[data-g=${g}]`).forEach(x => x.classList.toggle('active', x.dataset.v===v));
|
||
if (g === 'size') updateSizeInfo();
|
||
if (g === 'maturity') updateMaturityInfo();
|
||
recalc();
|
||
});
|
||
document.getElementById('vertical-select').onchange = (e) => {
|
||
ctx.vertical = e.target.value;
|
||
updateVertInfo();
|
||
recalc();
|
||
};
|
||
document.querySelectorAll('[data-d]').forEach(b => b.onclick = (e) => {
|
||
deptFilter = e.target.dataset.d;
|
||
document.querySelectorAll('[data-d]').forEach(x => x.classList.toggle('active', x.dataset.d===deptFilter));
|
||
renderAgents();
|
||
});
|
||
}
|
||
|
||
function updateSizeInfo(){
|
||
const s = DATA.scaling_factors.company_size[ctx.size];
|
||
document.getElementById('size-info').textContent = s.label + ' · ' + s.employees + ' employés · mult ×' + s.multiplier;
|
||
}
|
||
function updateMaturityInfo(){
|
||
const m = DATA.scaling_factors.maturity_ai[ctx.maturity];
|
||
document.getElementById('maturity-info').textContent = m.label + ' · mult ×' + m.multiplier + ' · ' + m.note;
|
||
}
|
||
function updateVertInfo(){
|
||
if (!ctx.vertical) { document.getElementById('vert-info').textContent = 'Aucun vertical sélectionné (baseline ×1.0)'; return; }
|
||
const v = DATA.scaling_factors.verticals[ctx.vertical];
|
||
document.getElementById('vert-info').textContent = v.label + ' · mult ×' + v.multiplier + ' · depts amplifiés: ' + v.amplified_depts.join(', ');
|
||
}
|
||
|
||
function scaledSavings(agent){
|
||
const sf = DATA.scaling_factors;
|
||
let m = sf.company_size[ctx.size].multiplier * sf.maturity_ai[ctx.maturity].multiplier;
|
||
if (ctx.vertical){
|
||
const v = sf.verticals[ctx.vertical];
|
||
m *= v.multiplier;
|
||
if (v.amplified_depts.includes(agent.dept)) m *= 1.25;
|
||
}
|
||
return Math.round(agent.savings_eur_year * m);
|
||
}
|
||
|
||
function renderAgents(){
|
||
const wrap = document.getElementById('agent-list');
|
||
let list = DATA.agents;
|
||
if (deptFilter !== 'all') list = list.filter(a => a.dept === deptFilter);
|
||
|
||
wrap.innerHTML = list.map(a => {
|
||
const scaled = scaledSavings(a);
|
||
const isSel = sel.has(a.id);
|
||
return `<div class="agent-card ${isSel?'selected':''}" data-id="${a.id}">
|
||
<input aria-label="form-field" type="checkbox" ${isSel?'checked':''} data-id="${a.id}">
|
||
<div class="ag-main">
|
||
<div class="ag-name">${a.agent}</div>
|
||
<div class="ag-pain">${a.pain}</div>
|
||
<div class="ag-meta">
|
||
<span class="dept">${a.dept}</span>
|
||
<span>${a.id}</span>
|
||
<span>⚡ ${a.complexity}/5</span>
|
||
<span>⚠️ ${a.risk_of_failure}/5</span>
|
||
<span>⏱ ${a.effort_md} MD</span>
|
||
</div>
|
||
</div>
|
||
<div class="ag-sav"><div class="v">${fmtEur(scaled)}</div><div class="l">/an (scaled)</div></div>
|
||
<div class="ag-quali"><div class="v">${a.quali_normalized||'—'}</div><div class="l">/100 quali</div></div>
|
||
<div class="ag-payback"><div class="v">${a.payback_months}mo</div><div class="l">payback</div></div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
wrap.querySelectorAll('input[type=checkbox]').forEach(cb => cb.onchange = (e) => {
|
||
const id = e.target.dataset.id;
|
||
if (e.target.checked) sel.add(id); else sel.delete(id);
|
||
const card = e.target.closest('.agent-card');
|
||
card.classList.toggle('selected', e.target.checked);
|
||
recalc();
|
||
});
|
||
// Click card = toggle checkbox
|
||
wrap.querySelectorAll('.agent-card').forEach(c => {
|
||
c.onclick = (e) => {
|
||
if (e.target.tagName === 'INPUT') return;
|
||
const cb = c.querySelector('input[type=checkbox]');
|
||
cb.checked = !cb.checked;
|
||
cb.dispatchEvent(new Event('change'));
|
||
};
|
||
});
|
||
}
|
||
|
||
function selectAll(){
|
||
DATA.agents.forEach(a => sel.add(a.id));
|
||
renderAgents(); recalc();
|
||
}
|
||
function selectNone(){
|
||
sel.clear(); renderAgents(); recalc();
|
||
}
|
||
|
||
function recalc(){
|
||
const sf = DATA.scaling_factors;
|
||
let mult = sf.company_size[ctx.size].multiplier * sf.maturity_ai[ctx.maturity].multiplier;
|
||
if (ctx.vertical) mult *= sf.verticals[ctx.vertical].multiplier;
|
||
document.getElementById('mult-display').textContent = mult.toFixed(2) + '×';
|
||
|
||
// Selected agents
|
||
const selAgents = DATA.agents.filter(a => sel.has(a.id));
|
||
const nSel = selAgents.length;
|
||
document.getElementById('sel-count').textContent = nSel;
|
||
|
||
if (nSel === 0){
|
||
['qs-sav','qs-impl','qs-pay','qs-npv','pk-sav','pk-impl','pk-quali','pk-effort'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.textContent = id.includes('quali') ? '—/100' : id.includes('pay') ? '— mois' : id.includes('effort') ? '— MD' : '0€';
|
||
});
|
||
document.getElementById('qs-sav-sub').textContent = '— par an';
|
||
renderRadar(null);
|
||
renderCurve(0);
|
||
// Still show all agents list (not filtered by selection)
|
||
return;
|
||
}
|
||
|
||
const totalSav = selAgents.reduce((s,a) => s + scaledSavings(a), 0);
|
||
const totalImpl = selAgents.reduce((s,a) => s + a.implementation_cost_eur, 0);
|
||
const avgPayback = selAgents.reduce((s,a) => s + a.payback_months, 0) / nSel;
|
||
const totalEffort = selAgents.reduce((s,a) => s + a.effort_md, 0);
|
||
const avgQuali = selAgents.reduce((s,a) => s + (a.quali_normalized||0), 0) / nSel;
|
||
const npv3y = totalSav * 3 - totalImpl - totalSav * 0.2 * 3;
|
||
|
||
document.getElementById('qs-sav').textContent = fmtEur(totalSav);
|
||
document.getElementById('qs-sav-sub').textContent = totalSav.toLocaleString('fr-FR') + ' € / an';
|
||
document.getElementById('qs-impl').textContent = fmtEur(totalImpl);
|
||
document.getElementById('qs-impl-sub').textContent = totalEffort + ' MD × 1200€';
|
||
document.getElementById('qs-pay').textContent = avgPayback.toFixed(1) + ' mois';
|
||
document.getElementById('qs-npv').textContent = fmtEur(npv3y);
|
||
|
||
document.getElementById('pk-sav').textContent = fmtEur(totalSav);
|
||
document.getElementById('pk-impl').textContent = fmtEur(totalImpl);
|
||
document.getElementById('pk-quali').textContent = avgQuali.toFixed(0) + '/100';
|
||
document.getElementById('pk-effort').textContent = totalEffort + ' MD';
|
||
|
||
// Avg qualitative per axis
|
||
const axes = ['time_fte_saved','risk_reduction','compliance_uplift','cx_impact','brand_score','strategic_value'];
|
||
const avgAxes = {};
|
||
axes.forEach(ax => {
|
||
avgAxes[ax] = selAgents.reduce((s,a) => s + (a.qualitative?.[ax] || 0), 0) / nSel;
|
||
});
|
||
renderRadar(avgAxes);
|
||
renderCurve(totalSav);
|
||
}
|
||
|
||
function renderRadar(axes){
|
||
const svg = document.getElementById('radar-svg');
|
||
const cx = 130, cy = 120, r = 80;
|
||
const axesNames = ['⏱ Time saved','🛡 Risk↓','📋 Compliance','🙂 CX/NPS','✨ Brand','🎯 Strategic'];
|
||
const axesKeys = ['time_fte_saved','risk_reduction','compliance_uplift','cx_impact','brand_score','strategic_value'];
|
||
|
||
let html = `<defs><radialGradient id="rgrad"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.6"/><stop offset="100%" stop-color="#6366f1" stop-opacity="0.15"/></radialGradient></defs>`;
|
||
// Concentric grid (5 levels)
|
||
for (let lvl=1; lvl<=5; lvl++){
|
||
const rr = (r*lvl)/5;
|
||
html += `<circle cx="${cx}" cy="${cy}" r="${rr}" fill="none" stroke="#1f2436" stroke-width="1"/>`;
|
||
}
|
||
// Axis lines + labels
|
||
axesNames.forEach((name, i) => {
|
||
const angle = (Math.PI*2*i)/6 - Math.PI/2;
|
||
const x = cx + r * Math.cos(angle), y = cy + r * Math.sin(angle);
|
||
html += `<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="#1f2436" stroke-width="1"/>`;
|
||
const lx = cx + (r+18) * Math.cos(angle), ly = cy + (r+18) * Math.sin(angle);
|
||
html += `<text x="${lx}" y="${ly}" text-anchor="middle" alignment-baseline="middle" font-size="9.5" fill="#94a3b8">${name}</text>`;
|
||
});
|
||
|
||
// Data polygon
|
||
if (axes){
|
||
const pts = axesKeys.map((k, i) => {
|
||
const val = axes[k] || 0;
|
||
const angle = (Math.PI*2*i)/6 - Math.PI/2;
|
||
const rr = (r*val)/5;
|
||
return [cx + rr * Math.cos(angle), cy + rr * Math.sin(angle)];
|
||
});
|
||
const pathD = 'M' + pts.map(p => p.map(n=>n.toFixed(1)).join(',')).join(' L') + ' Z';
|
||
html += `<path d="${pathD}" fill="url(#rgrad)" stroke="#14b8a6" stroke-width="2"/>`;
|
||
pts.forEach(p => { html += `<circle cx="${p[0]}" cy="${p[1]}" r="3" fill="#14b8a6"/>`; });
|
||
} else {
|
||
html += `<text x="${cx}" y="${cy+3}" text-anchor="middle" font-size="10" fill="#64748b">Sélectionner des agents</text>`;
|
||
}
|
||
svg.innerHTML = html;
|
||
}
|
||
|
||
function renderCurve(maxSav){
|
||
const svg = document.getElementById('curve-svg');
|
||
const W = 280, H = 130, PAD = 10;
|
||
const ramp = [0, 0.05, 0.15, 0.30, 0.45, 0.60, 0.72, 0.82, 0.90, 0.95, 0.98, 1.0];
|
||
const pts = ramp.map((p, i) => [PAD + (W-2*PAD)*i/11, H-PAD - (H-2*PAD)*p]);
|
||
const pathLine = 'M' + pts.map(p => p.map(n=>n.toFixed(1)).join(',')).join(' L');
|
||
const pathArea = pathLine + ` L ${(W-PAD).toFixed(1)} ${(H-PAD)} L ${PAD} ${(H-PAD)} Z`;
|
||
|
||
let html = `<defs><linearGradient id="lgrad" x1="0" x2="0" y1="0" y2="1"><stop offset="0%" stop-color="#14b8a6" stop-opacity="0.5"/><stop offset="100%" stop-color="#14b8a6" stop-opacity="0"/></linearGradient></defs>`;
|
||
// Grid Y
|
||
for (let i=0; i<=4; i++){
|
||
const y = PAD + (H-2*PAD)*i/4;
|
||
html += `<line x1="${PAD}" y1="${y}" x2="${W-PAD}" y2="${y}" stroke="#1f2436" stroke-width="0.5"/>`;
|
||
}
|
||
html += `<path d="${pathArea}" fill="url(#lgrad)"/>`;
|
||
html += `<path d="${pathLine}" fill="none" stroke="#14b8a6" stroke-width="2" stroke-linejoin="round"/>`;
|
||
// Labels end
|
||
if (maxSav > 0){
|
||
html += `<text x="${W-PAD-5}" y="${PAD+12}" text-anchor="end" font-size="10" font-weight="700" fill="#eab308">${fmtEur(maxSav)}/an</text>`;
|
||
html += `<text x="${W-PAD-5}" y="${PAD+24}" text-anchor="end" font-size="9" fill="#94a3b8">à M12 (run rate)</text>`;
|
||
}
|
||
svg.innerHTML = html;
|
||
}
|
||
|
||
function exportJSON(){
|
||
const selAgents = DATA.agents.filter(a => sel.has(a.id)).map(a => ({
|
||
id: a.id, agent: a.agent, dept: a.dept,
|
||
baseline_savings: a.savings_eur_year,
|
||
scaled_savings: scaledSavings(a),
|
||
impl_cost: a.implementation_cost_eur,
|
||
payback_months: a.payback_months,
|
||
quali_score: a.quali_normalized
|
||
}));
|
||
const payload = {
|
||
generated: new Date().toISOString(),
|
||
context: ctx,
|
||
selected_count: selAgents.length,
|
||
totals: {
|
||
savings: selAgents.reduce((s,a) => s+a.scaled_savings, 0),
|
||
impl_cost: selAgents.reduce((s,a) => s+a.impl_cost, 0)
|
||
},
|
||
agents: selAgents
|
||
};
|
||
const blob = new Blob([JSON.stringify(payload, null, 2)], {type:'application/json'});
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url; a.download = 'weval-roi-simulation-' + Date.now() + '.json';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
load();
|
||
</script>
|
||
|
||
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
|
||
<script>
|
||
(function(){
|
||
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
|
||
var d = document;
|
||
var m = d.createElement('div');
|
||
m.id = 'opus-udrill';
|
||
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
|
||
var inner = d.createElement('div');
|
||
inner.id = 'opus-udrill-in';
|
||
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
|
||
inner.addEventListener('click', function(e){ e.stopPropagation(); });
|
||
m.appendChild(inner);
|
||
m.addEventListener('click', function(){ m.style.display='none'; });
|
||
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
|
||
(d.body || d.documentElement).appendChild(m);
|
||
function openCard(card) {
|
||
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
|
||
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
|
||
inner.innerHTML = html;
|
||
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
|
||
m.style.display = 'flex';
|
||
}
|
||
function wire(root) {
|
||
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
|
||
var cards = root.querySelectorAll(sels);
|
||
for (var i = 0; i < cards.length; i++) {
|
||
var c = cards[i];
|
||
if (c.__opusWired) continue;
|
||
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
|
||
var r = c.getBoundingClientRect();
|
||
if (r.width < 60 || r.height < 40) continue;
|
||
c.__opusWired = true;
|
||
c.style.cursor = 'pointer';
|
||
c.setAttribute('role','button');
|
||
c.setAttribute('tabindex','0');
|
||
c.addEventListener('click', function(ev){
|
||
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
|
||
if (ev.target.closest('a,button,input,select')) return;
|
||
ev.preventDefault(); ev.stopPropagation();
|
||
openCard(this);
|
||
});
|
||
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
|
||
}
|
||
}
|
||
var initRun = function(){ wire(d.body || d.documentElement); };
|
||
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
|
||
else initRun();
|
||
var mo = new MutationObserver(function(muts){
|
||
var newCard = false;
|
||
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
|
||
if (newCard) initRun();
|
||
});
|
||
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
|
||
})();
|
||
</script>
|
||
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
|
||
|
||
|
||
<script src="/api/a11y-auto-enhancer.js" defer></script>
|
||
<!-- WTP_UDOCK_V1 (Opus 21-avr t32b4) --><script src="/wtp-unified-dock.js" defer></script>
|
||
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
|
||
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
|
||
|
||
// DOCTRINE-60-UX-JS staggered entrance
|
||
(function(){
|
||
if (!('IntersectionObserver' in window)) return;
|
||
const obs = new IntersectionObserver((entries) => {
|
||
entries.forEach((e, i) => {
|
||
if (e.isIntersecting) {
|
||
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
|
||
obs.unobserve(e.target);
|
||
}
|
||
});
|
||
});
|
||
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
|
||
})();
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|