Download index.html from Hugdrigo/decisionforge-strategic-intelligence: direct link, hf CLI and curl.
- Browser
- Download file 59 kB
-
https://huggingface.co/spaces/Hugdrigo/decisionforge-strategic-intelligence/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hugdrigo/decisionforge-strategic-intelligence/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hugdrigo/decisionforge-strategic-intelligence/resolve/main/index.html
59 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <meta name="theme-color" content="#0c1120"> | |
| <meta name="description" content="DecisionForge: a transparent multi-criteria decision intelligence studio for strategic portfolio comparisons, sensitivity analysis and scenario simulation."> | |
| <title>DecisionForge | Strategic Decision Intelligence</title> | |
| <style> | |
| :root{color-scheme:dark;--bg:#090e19;--side:#0d1425;--surface:#121b2c;--surface2:#182339;--card:#121c2e;--border:#29364d;--borderSoft:#202d43;--text:#f1f4ff;--muted:#9baac4;--faint:#647795;--cyan:#60e1db;--indigo:#9b9cfa;--amber:#ffd38c;--rose:#ff9caa;--green:#8ce5b4;--purple:#bba6fa;--shadow:0 22px 70px rgba(0,0,0,.18)} | |
| *,*:before,*:after{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:14px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:var(--bg);color:var(--text)}button,input,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--cyan);outline-offset:3px}a{color:inherit}input,select{color:var(--text);background:#0c1626;border:1px solid #344661;border-radius:9px;padding:10px 11px;max-width:100%;min-height:38px}input[type=number]{font-variant-numeric:tabular-nums}input[type=range]{accent-color:var(--cyan);min-height:initial;cursor:pointer}input:focus,select:focus{border-color:var(--cyan)}h1,h2,h3,h4,p{margin-top:0}h1{font-size:clamp(28px,3.3vw,44px);line-height:1.16;letter-spacing:-1.6px;margin-bottom:12px}h2{font-size:22px;letter-spacing:-.6px;margin-bottom:4px}h3{font-size:15px;letter-spacing:-.1px;margin-bottom:3px}p{color:var(--muted)}.app{display:flex;min-height:100vh}.sidebar{width:238px;min-width:238px;position:sticky;top:0;height:100vh;background:linear-gradient(180deg,#0e172a,#0b1220);border-right:1px solid var(--borderSoft);padding:27px 16px 20px;display:flex;flex-direction:column}.logo{display:flex;gap:11px;align-items:center;padding:0 10px 33px;text-decoration:none}.logo-mark{height:39px;width:39px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(140deg,#66e2e3 0%,#9da3ff 60%,#bd98ee 100%);box-shadow:0 8px 25px #6796ff26;color:#091524;font-size:22px;font-weight:950}.logo-name{font-weight:850;font-size:18px;letter-spacing:-.7px}.logo-name small{display:block;color:#8794b7;font-size:9px;letter-spacing:2.2px;margin-top:-4px}.side-label{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:1.7px;color:#7182a2;padding:0 12px;margin:6px 0 9px}.side-nav{display:grid;gap:5px}.side-nav a{border-radius:10px;padding:12px 12px;display:flex;align-items:center;gap:11px;color:#a2b1cb;text-decoration:none;font-weight:660;border:1px solid transparent}.side-nav a:hover,.side-nav a.active{background:#1b2a42;color:white}.side-nav a.active{border-color:#304766;box-shadow:inset 3px 0 0 var(--cyan)}.nav-icon{width:20px;text-align:center;font-size:16px;color:var(--cyan)}.side-bottom{margin-top:auto}.side-box{border:1px solid #30425d;border-radius:16px;padding:16px;background:#17243b}.side-box .side-dot{display:inline-block;height:8px;width:8px;border-radius:50%;background:var(--green);margin-right:6px}.side-box b{display:block;font-size:13px;margin:5px 0}.side-box p{font-size:11px;margin:0}.side-footer{font-size:10px;color:#6e7f99;padding:14px 9px 0}.content{width:100%;min-width:0;max-width:1700px;margin:0 auto;padding:27px clamp(19px,3vw,53px) 70px}.topbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:27px}.breadcrumbs{color:var(--muted);font-size:12px;font-weight:650}.breadcrumbs strong{color:var(--cyan)}.top-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;background:#1b2a42;border:1px solid #3c4f6a;border-radius:10px;color:var(--text);padding:10px 14px;min-height:39px;text-decoration:none;font-weight:700;white-space:nowrap;transition:background .15s,border .15s}.btn:hover{background:#273952;border-color:#667b9e}.btn.primary{background:var(--cyan);border-color:var(--cyan);color:#0b222a}.btn.primary:hover{background:#a2f5ec}.btn.danger{color:#ffd0d5;border-color:#805166}.btn.tiny{min-height:31px;padding:6px 9px;font-size:11px}.btn.ghost{background:transparent;border-color:transparent;color:var(--muted)}.badge{display:inline-flex;gap:7px;align-items:center;font-size:10px;font-weight:750;letter-spacing:.6px;padding:6px 10px;border:1px solid #3c506d;color:#b3c7e7;border-radius:50px;text-transform:uppercase}.badge .dot{width:6px;height:6px;background:var(--green);border-radius:50%}.hero{background:radial-gradient(ellipse at 95% 10%,#28335d 0%,transparent 55%),linear-gradient(125deg,#1b3751,#18233b 55%,#21254b);border:1px solid #334561;border-radius:24px;padding:32px clamp(24px,3vw,39px);display:flex;align-items:center;justify-content:space-between;gap:25px;position:relative;overflow:hidden;margin-bottom:23px;min-height:241px}.hero:before{content:"";position:absolute;height:310px;width:310px;border:1px solid #7cbaf522;border-radius:100%;right:-110px;top:-150px;box-shadow:0 0 0 53px #7cbaf508,0 0 0 106px #7cbaf508;pointer-events:none}.hero-copy{position:relative;max-width:665px}.eyebrow{color:#75ece1;letter-spacing:2px;font-size:10px;font-weight:850;text-transform:uppercase;margin-bottom:14px}.hero p{max-width:610px;color:#b9c7df;line-height:1.8;margin-bottom:18px}.hero-tags{display:flex;gap:7px;flex-wrap:wrap}.hero-tag{padding:7px 11px;border-radius:9px;background:#ffffff0d;border:1px solid #7893b63a;color:#d0d9eb;font-size:11px;font-weight:670}.hero-graphic{width:230px;flex-shrink:0;position:relative;z-index:1;opacity:.97}.stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:30px}.stat{border:1px solid var(--border);border-radius:16px;padding:17px 18px;background:var(--surface);min-width:0}.stat .label{font-size:11px;color:#a5b5cd;display:flex;align-items:center;justify-content:space-between;gap:5px}.stat .label span{font-size:15px;color:var(--cyan)}.stat strong{display:block;font-size:clamp(20px,2.4vw,32px);letter-spacing:-1px;font-weight:800;margin:11px 0 1px;font-variant-numeric:tabular-nums}.stat small{color:#8194b4;font-size:11px}.section{scroll-margin-top:20px;margin:0 0 35px}.section-heading{display:flex;align-items:end;justify-content:space-between;gap:16px;margin-bottom:16px;flex-wrap:wrap}.section-heading p{font-size:12px;margin-bottom:0;max-width:650px}.section-counter{color:var(--cyan);letter-spacing:1.8px;font-size:10px;font-weight:850;text-transform:uppercase}.grid-main{display:grid;grid-template-columns:minmax(0,1.12fr) minmax(320px,.88fr);gap:16px}.grid-two{display:grid;grid-template-columns:1fr 1fr;gap:16px}.panel{background:var(--surface);border:1px solid var(--border);border-radius:18px;min-width:0;overflow:hidden}.panel.pad{padding:20px}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:17px}.panel-head p{font-size:11px;margin:2px 0 0}.panel-body{padding:20px}.rec{display:flex;align-items:start;gap:14px;border:1px solid #326960;background:linear-gradient(120deg,#123a39,#172a3e);border-radius:13px;padding:16px;margin-bottom:17px}.rec .icon{font-size:22px;border:1px solid #4eaba0;background:#28605b;border-radius:10px;height:42px;width:42px;display:grid;place-items:center}.rec .kicker{color:#71ded1;font-size:10px;font-weight:850;letter-spacing:1.2px;text-transform:uppercase}.rec b{display:block;font-size:18px;letter-spacing:-.3px}.rec p{font-size:11px;margin:3px 0 0;color:#bed3df}.rankings{display:grid;gap:17px}.rank-row{display:grid;grid-template-columns:25px minmax(0,1fr) 66px;align-items:center;gap:11px}.rank-no{display:grid;place-items:center;border-radius:9px;height:26px;background:#25354d;font-weight:850;font-size:11px;color:#d1ddf3}.rank-name{display:flex;gap:10px;align-items:center;justify-content:space-between;font-size:12px;font-weight:700;margin-bottom:7px;min-width:0}.rank-name .name{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.rank-sub{color:var(--faint);font-weight:550;font-size:10px}.track{height:8px;border-radius:10px;background:#29384f;overflow:hidden}.fill{height:100%;border-radius:10px;background:linear-gradient(90deg,#6cdadb,#9e9cf5)}.rank-score{text-align:right;font-weight:850;font-size:17px;font-variant-numeric:tabular-nums}.rank-score small{display:block;color:var(--faint);font-size:9px;font-weight:600}.insight{margin-top:18px;border-radius:11px;background:#1c2940;border:1px solid #30415c;padding:12px 14px;font-size:11px;line-height:1.6;color:#bdcbe0}.insight strong{color:#e7edf8}.insight.warning{border-color:#856f41;background:#3c321f}.stack-row{display:grid;grid-template-columns:110px minmax(0,1fr) 52px;gap:10px;align-items:center;margin-bottom:14px}.stack-row>span{font-size:11px;color:#c4d1e5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stack-row>b{font-size:11px;text-align:right;font-variant-numeric:tabular-nums}.stacks{display:flex;align-items:stretch;height:14px;border-radius:5px;overflow:hidden;background:#26344b}.stacks i{height:100%;display:block}.legend{display:flex;gap:12px;flex-wrap:wrap;margin:14px 0 0}.legend span{display:inline-flex;align-items:center;gap:6px;color:#adbbd2;font-size:10px}.legend i{width:9px;height:9px;display:inline-block;border-radius:2px}.scroll-table{overflow-x:auto;scrollbar-color:#4a607d transparent}.data-table{border-collapse:collapse;width:100%;text-align:left;font-size:11px}.data-table th{color:#9dacc7;letter-spacing:.25px;font-size:10px;text-transform:uppercase;background:#18243a;padding:13px 12px;white-space:nowrap}.data-table td{padding:11px 12px;border-top:1px solid #263550;white-space:nowrap}.data-table .muted{color:#91a0bb}.data-table .right{text-align:right;font-variant-numeric:tabular-nums}.heat{display:inline-grid;place-items:center;min-width:64px;padding:8px 7px;border-radius:7px;font-weight:850;font-size:11px}.matrix-table{border-collapse:separate;border-spacing:0;width:100%;min-width:850px;font-size:12px}.matrix-table th{position:relative;background:#18253b;padding:13px 10px;border-bottom:1px solid #31425c;font-size:10px;color:#b6c6dd;font-weight:760;white-space:nowrap}.matrix-table th:first-child{text-align:left;width:190px;position:sticky;left:0;z-index:2;box-shadow:2px 0 0 var(--border)}.matrix-table td:first-child{position:sticky;left:0;background:#151f32;z-index:1;box-shadow:2px 0 0 var(--border)}.matrix-table td{border-bottom:1px solid #2a3750;padding:11px 10px;text-align:center}.matrix-table tr:last-child td{border-bottom:0}.matrix-table th .small{color:#7e95b5;font-weight:550;display:block;line-height:1.7}.matrix-table input[type=number]{padding:8px;width:96px;text-align:right;font-size:12px;background:#0e192b}.matrix-table .alt-input{width:160px;font-weight:690;font-size:12px;background:#101e32;padding:9px}.matrix-table tr:hover td{background:#1a283d}.matrix-table tr:hover td:first-child{background:#1a283d}.chip{display:inline-block;border:1px solid #47607b;background:#23364e;border-radius:9px;padding:5px 8px;font-weight:700;color:#c5d7e7;font-size:10px}.chip.top{border-color:#358076;color:#7df1dc;background:#174039}.tiny-note{color:#899bb9;font-size:11px;margin:10px 0 0}.criteria-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.criterion{border:1px solid #2d3c55;background:#152136;border-radius:13px;padding:15px;min-width:0}.criterion-top{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}.criterion-top .cname{font-weight:730;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.criterion-top .weight-pill{color:#71e4dc;border:1px solid #376860;background:#1a423d;border-radius:30px;padding:3px 8px;font-size:10px;font-weight:850;white-space:nowrap}.criterion .meter{display:flex;align-items:center;gap:8px}.criterion input[type=range]{flex:1;min-width:0}.criterion input[type=number]{width:70px;padding:7px;text-align:center;font-size:11px}.criterion small{color:#8092b0;font-size:10px;display:block;margin-top:7px}.preset-set{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:20px}.preset{background:#1d2b43;border:1px solid #374967;border-radius:9px;color:#b8c8e0;min-height:38px;padding:8px 12px;font-size:11px;font-weight:760}.preset.active,.preset:hover{background:#23514e;color:#a9fff1;border-color:#54c0b2}.field-row{display:grid;gap:6px}.field-row label{font-size:11px;font-weight:740;color:#bdcce3}.slider-wrap{padding:11px 0}.slider-top{display:flex;justify-content:space-between;font-size:12px;align-items:center;margin-bottom:6px}.slider-top b{color:var(--cyan)}.slider-wrap input{width:100%}.dist-row{display:grid;grid-template-columns:110px 1fr 76px;gap:11px;align-items:center;padding:9px 0;border-bottom:1px solid #253550}.dist-row:last-child{border-bottom:0}.dist-row>span{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:11px;font-weight:700}.dist-row b{text-align:right;font-size:12px}.barcell{position:relative;border-radius:7px;height:15px;background:#26364d;overflow:hidden}.barcell span{height:100%;position:absolute;left:0;top:0;background:var(--cyan);border-radius:7px}.scatter{max-width:100%;width:100%;height:auto;display:block}.chart-label{fill:#94a8c5;font:11px Inter,ui-sans-serif,system-ui}.chart-grid{stroke:#334258;stroke-width:1;stroke-dasharray:3 5}.methods{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.method{background:#152137;border:1px solid var(--border);padding:18px;border-radius:14px}.method b{font-size:12px}.method small{color:var(--cyan);font-size:10px;letter-spacing:1.7px;font-weight:800;display:block;margin-bottom:8px}.method p{font-size:11px;line-height:1.75;margin:9px 0 0}.note{margin-top:13px;background:#1d2a3d;border:1px solid #334660;border-radius:12px;padding:12px 15px;color:#b2c3da;font-size:11px}.accent{color:var(--cyan)}.footer{border-top:1px solid #2b3951;margin-top:45px;padding-top:20px;color:#8090ab;font-size:11px;display:flex;justify-content:space-between;gap:15px;flex-wrap:wrap}.footer strong{color:#c8d6ed}.toast{position:fixed;right:22px;bottom:22px;z-index:50;background:#21473f;border:1px solid #5bbcad;color:white;padding:12px 16px;border-radius:12px;box-shadow:var(--shadow);max-width:min(420px,calc(100vw - 40px));font-size:12px;font-weight:700;opacity:0;transform:translateY(8px);transition:.2s;pointer-events:none}.toast.show{opacity:1;transform:translateY(0)}.toast.error{background:#5c2e3c;border-color:#e89aa3}.overlay{position:fixed;inset:0;z-index:20;background:#040916c7;display:none;align-items:center;justify-content:center;padding:20px}.overlay.open{display:flex}.modal{border:1px solid #405571;background:#152237;border-radius:18px;width:min(690px,100%);max-height:90vh;overflow:auto;box-shadow:0 28px 80px #0008}.modal-head{display:flex;align-items:start;justify-content:space-between;padding:20px 23px 12px;gap:10px}.modal-head h2{font-size:19px}.modal-head p{font-size:11px;margin-bottom:0}.modal-body{padding:10px 23px 24px}.modal-footer{padding:16px 23px;border-top:1px solid #2b3953;display:flex;gap:9px;justify-content:end;flex-wrap:wrap}.form-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.form-grid .wide{grid-column:span 2}.form-grid input,.form-grid select{width:100%}.delete-row{display:flex;align-items:center;justify-content:space-between;gap:10px;border:1px solid #34445c;padding:12px;border-radius:10px;margin:8px 0}.delete-row b{font-size:12px}.delete-row p{font-size:10px;margin:2px 0 0}.empty{border:1px dashed #435775;border-radius:11px;padding:20px;color:#9dadc6;text-align:center;font-size:12px}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)} | |
| @media(max-width:1200px){.hero-graphic{width:160px}.grid-main{grid-template-columns:1fr}.stats{gap:9px}.criteria-grid{grid-template-columns:repeat(2,minmax(0,1fr))}} | |
| @media(max-width:900px){.sidebar{position:relative;height:auto;min-width:0;width:auto;padding:15px 17px;display:block;border-bottom:1px solid var(--border)}.app{display:block}.logo{padding:0 0 13px}.side-label,.side-bottom{display:none}.side-nav{display:flex;gap:7px;overflow:auto}.side-nav a{white-space:nowrap;padding:8px 10px;font-size:11px}.content{padding:20px 17px 60px}.stats{grid-template-columns:repeat(2,minmax(0,1fr))}.methods{grid-template-columns:repeat(2,minmax(0,1fr))}.topbar{margin-bottom:20px}} | |
| @media(max-width:620px){.hero{padding:26px 20px}.hero-graphic{display:none}.topbar{align-items:start}.top-actions{justify-content:flex-end}.top-actions .hide-mobile{display:none}.stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}.stat{padding:13px}.stat strong{font-size:22px}.grid-two,.criteria-grid,.methods{grid-template-columns:1fr}.content{padding:16px 12px 48px}.panel.pad{padding:16px}.section{margin-bottom:30px}.rank-row{grid-template-columns:20px minmax(0,1fr) 51px;gap:8px}.rank-score{font-size:14px}.rank-name{font-size:11px}.stack-row{grid-template-columns:74px 1fr 44px}.form-grid{grid-template-columns:1fr}.form-grid .wide{grid-column:span 1}.section-heading h2{font-size:19px}.top-actions .btn{padding:8px}.breadcrumbs{font-size:10px}} | |
| @media print{html,body{background:white!important;color:#1b293c!important}.sidebar,.topbar,.hero-graphic,.btn,.top-actions,.preset-set,.field-row,.section#matrix,.section#scenarios,.footer,.toast,.overlay,.section#methodology{display:none!important}.app{display:block}.content{padding:0!important}.hero,.panel,.stat{background:#fff!important;color:#1b293c!important;box-shadow:none;border:1px solid #cbd5e1!important}.hero{min-height:0}.hero *,.stat *,.panel *{color:#233047!important}.section{break-inside:avoid}h1{font-size:30px}.stats{grid-template-columns:repeat(4,1fr)}.grid-main,.grid-two{grid-template-columns:1fr 1fr}.rank-row{break-inside:avoid}p{color:#53647b!important}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <aside class="sidebar" aria-label="Primary navigation"> | |
| <a class="logo" href="#overview"><span class="logo-mark">◇</span><span class="logo-name">DecisionForge<small>INTELLIGENCE STUDIO</small></span></a> | |
| <div class="side-label">Workspace</div> | |
| <nav class="side-nav" id="nav"><a class="active" href="#overview"><span class="nav-icon">◈</span>Overview</a><a href="#matrix"><span class="nav-icon">▦</span>Decision matrix</a><a href="#scenarios"><span class="nav-icon">◎</span>Scenarios</a><a href="#uncertainty"><span class="nav-icon">◌</span>Uncertainty</a><a href="#methodology"><span class="nav-icon">≡</span>Methodology</a></nav> | |
| <div class="side-bottom"><div class="side-box"><span class="badge"><span class="side-dot"></span>Offline ready</span><b>Private by design</b><p>Decision data remains in this browser. No accounts, APIs, or telemetry.</p></div><div class="side-footer">HUGDRIGO LABS · PROJECT 078<br>MIT · 2026</div></div> | |
| </aside> | |
| <main class="content"> | |
| <div class="topbar"><div class="breadcrumbs">Hugdrigo / Decision Intelligence / <strong>Project 078</strong></div><div class="top-actions"><span class="badge hide-mobile"><span class="dot"></span>CLIENT-SIDE</span><button class="btn tiny" id="importBtn" type="button">↥ Import</button><button class="btn tiny" id="exportBtn" type="button">↧ Export</button><button class="btn tiny hide-mobile" id="printBtn" type="button">⎙ Report</button></div></div> | |
| <section id="overview" class="section" aria-labelledby="heroTitle"> | |
| <div class="hero"><div class="hero-copy"><div class="eyebrow">Strategic portfolio analytics · Edition 01</div><h1 id="heroTitle">Turn complex choices<br>into <span style="color:#7fece3">clear decisions.</span></h1><p>Compare strategic options with transparent weighted scoring, editable trade-offs, sensitivity analysis and reproducible uncertainty simulations.</p><div class="hero-tags"><span class="hero-tag">Multi-criteria scoring</span><span class="hero-tag">Scenario analysis</span><span class="hero-tag">Monte Carlo lab</span></div></div><svg class="hero-graphic" viewBox="0 0 250 220" role="img" aria-label="Illustration of a weighted decision network"><defs><linearGradient id="orb" x1="0" x2="1"><stop stop-color="#62e3da"/><stop offset="1" stop-color="#a7a4ff"/></linearGradient></defs><circle cx="125" cy="110" r="93" stroke="#789cc8" stroke-opacity=".3" fill="none" stroke-dasharray="4 7"/><circle cx="125" cy="110" r="63" stroke="#789cc8" stroke-opacity=".5" fill="none"/><path d="M125 25L210 154 86 194 36 79Z" fill="url(#orb)" fill-opacity=".12" stroke="url(#orb)" stroke-width="2"/><path d="M125 25L125 110 210 154M36 79L125 110 86 194" fill="none" stroke="#91cff3" stroke-opacity=".5"/><g fill="#6ce5df"><circle cx="125" cy="25" r="7"/><circle cx="210" cy="154" r="7"/></g><g fill="#b1a8ff"><circle cx="86" cy="194" r="7"/><circle cx="36" cy="79" r="7"/></g><circle cx="125" cy="110" r="18" fill="#1c344e" stroke="#a3e8e6" stroke-width="2"/><path d="m116 110 6 6 13-14" stroke="#7beade" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></div> | |
| <div class="stats"><div class="stat"><div class="label">Recommended option <span>◈</span></div><strong id="statWinner">—</strong><small id="statWinnerSub">Highest weighted utility</small></div><div class="stat"><div class="label">Simulation win rate <span>◌</span></div><strong id="statProbability">—</strong><small>Simulation win frequency</small></div><div class="stat"><div class="label">Evaluated options <span>▦</span></div><strong id="statOptions">—</strong><small id="statCriteriaSub">Across 0 criteria</small></div><div class="stat"><div class="label">Score advantage <span>↗</span></div><strong id="statLead">—</strong><small>Points ahead of runner-up</small></div></div> | |
| <div class="section-heading"><div><span class="section-counter">01 / Decision overview</span><h2>Decision command center</h2><p>A transparent ranking based on your current options, anchors and normalized criterion weights.</p></div><button id="sampleResetBtn" class="btn tiny" type="button">↺ Restore sample</button></div> | |
| <div class="grid-main"><div class="panel pad"><div class="panel-head"><div><h3>Strategic option ranking</h3><p>Higher weighted scores are better · 0 to 100</p></div><span class="badge">LIVE MODEL</span></div><div id="recommendation" class="rec" aria-live="polite"></div><div id="rankingBars" class="rankings"></div><div id="overviewInsight" class="insight"></div></div><div class="panel pad"><div class="panel-head"><div><h3>Criteria contribution</h3><p>Point composition of each weighted score</p></div><span class="badge">EXPLAINABLE</span></div><div id="contributionBars"></div><div id="criteriaLegend" class="legend"></div><div class="note" style="margin-top:19px">Every criterion uses fixed user-editable low/high reference anchors. Adding an option does not silently change the normalization of existing options.</div></div></div> | |
| </section> | |
| <section class="section" id="matrix" aria-labelledby="matrixTitle"><div class="section-heading"><div><span class="section-counter">02 / Decision inputs</span><h2 id="matrixTitle">Performance matrix</h2><p>Edit each option's raw values. Higher/lower preference and fixed reference ranges determine its normalized score.</p></div><div class="top-actions"><button id="addOptionBtn" class="btn tiny primary">+ Add option</button><button id="manageOptionsBtn" class="btn tiny">Manage options</button><button id="addCriterionBtn" class="btn tiny">+ Criterion</button><button id="manageCriteriaBtn" class="btn tiny">Edit criteria</button></div></div><div class="panel"><div class="scroll-table" id="matrixTableWrap"><table class="matrix-table" id="matrixTable" aria-label="Editable decision performance matrix"></table></div><div style="padding:12px 15px;color:#8ea1be;font-size:11px;border-top:1px solid var(--border)">Example values are synthetic and illustrative. Changes save locally. Fixed anchors ensure comparable scores, but their selection remains a judgment call.</div></div></section> | |
| <section class="section" id="scenarios" aria-labelledby="scenarioTitle"><div class="section-heading"><div><span class="section-counter">03 / Priorities and trade-offs</span><h2 id="scenarioTitle">Scenario laboratory</h2><p>Move strategic priorities and see immediately how the preferred option changes.</p></div><button id="resetWeightsBtn" class="btn tiny">↺ Baseline weights</button></div><div class="grid-two"><div class="panel pad"><div class="panel-head"><div><h3>Strategic weighting</h3><p>Weights are normalized to 100% for every calculation</p></div><span class="badge">INTERACTIVE</span></div><div id="presets" class="preset-set"><button class="preset active" data-preset="base">Balanced</button><button class="preset" data-preset="value">Value first</button><button class="preset" data-preset="speed">Faster delivery</button><button class="preset" data-preset="resilience">Risk aware</button></div><div id="weightControls" class="criteria-grid"></div><div class="note" id="weightNote"></div></div><div class="panel pad"><div class="panel-head"><div><h3>One-way sensitivity</h3><p>Sweep one criterion from 0% to 100%, rebalancing all others proportionally</p></div><span class="badge">ROBUSTNESS</span></div><div class="field-row" style="margin-bottom:8px"><label for="sensitivitySelect">Criterion to stress-test</label><select id="sensitivitySelect"></select></div><div id="sensitivityChart" role="img" aria-label="Sensitivity analysis chart showing option scores as criterion importance changes"></div><div id="sensitivityLegend" class="legend"></div><div id="sensitivityInsight" class="insight"></div></div></div></section> | |
| <section class="section" id="uncertainty" aria-labelledby="uncertaintyTitle"><div class="section-heading"><div><span class="section-counter">04 / Uncertainty and stability</span><h2 id="uncertaintyTitle">Uncertainty intelligence</h2><p>Stress-test uncertain performance inputs through a seeded, reproducible simulation, not a predictive AI model.</p></div><button id="exportRankingBtn" class="btn tiny">↧ Rankings CSV</button></div><div class="grid-two"><div class="panel pad"><div class="panel-head"><div><h3>Monte Carlo experiment</h3><p>1,200 scenario draws · identical inputs yield identical results</p></div><span class="badge">SEEDED</span></div><div class="slider-wrap"><div class="slider-top"><label for="uncertaintyRange">Assumed uncertainty</label><b id="uncertaintyValue">12% of anchor range</b></div><input id="uncertaintyRange" type="range" min="0" max="25" step="1" value="12"></div><p class="tiny-note" style="margin-bottom:18px">Each raw input varies independently using a centered triangular perturbation of ± the selected fraction of its anchor range. This is an assumption stress test, not a probability calibrated to observed errors.</p><div id="uncertaintyBars"></div><div id="uncertaintyInsight" class="insight"></div></div><div class="panel pad"><div class="panel-head"><div><h3>Score distributions</h3><p>Simulated 10th to 90th percentile utility intervals</p></div><span class="badge">P10 · P90</span></div><div class="scroll-table"><table class="data-table" aria-label="Monte Carlo score uncertainty results" id="uncertaintyTable"><thead><tr><th>Option</th><th class="right">Base</th><th class="right">P10</th><th class="right">Median</th><th class="right">P90</th><th class="right">Win %</th></tr></thead><tbody id="uncertaintyRows"></tbody></table></div><div class="note">Win frequency measures how often an option ranks first under the chosen illustrative random assumptions. It does not establish the real-world probability of success. Ties count fractionally.</div></div></div></section> | |
| <section class="section" id="methodology" aria-labelledby="methodTitle"><div class="section-heading"><div><span class="section-counter">05 / Trust and reproducibility</span><h2 id="methodTitle">Methods and operating boundaries</h2><p>Every calculation is local, inspectable, and explicitly assumption-dependent.</p></div><button id="downloadTemplateBtn" class="btn tiny">↧ Example JSON</button></div><div class="methods"><div class="method"><small>01 / SCALE</small><b>Fixed-anchor normalization</b><p>Each criterion has a low and high reference. For higher-is-better, utility is (value − low) ÷ (high − low) × 100. Lower-is-better reverses that result; values outside anchors are capped at 0 or 100.</p></div><div class="method"><small>02 / RANK</small><b>Weighted additive model</b><p>Raw weights are normalized to total 100%, then utility is the weighted average of normalized criterion scores. Equal scores are ordered alphabetically for display. Weights and scales encode judgment, not objective truth.</p></div><div class="method"><small>03 / STABILITY</small><b>One-way priority sweeps</b><p>The selected criterion varies from 0% to 100% in increments of 5%; remaining criteria divide the balance in proportion to their raw weights, or evenly if all other weights are zero.</p></div><div class="method"><small>04 / UNCERTAINTY</small><b>Seeded triangular sampling</b><p>A deterministic seeded generator draws two uniforms whose average yields a centered triangular value. Each input is perturbed independently by ± the chosen fraction of the anchor range and clipped to its anchors.</p></div><div class="method"><small>05 / GOVERNANCE</small><b>Explainability and fairness</b><p>The model reveals all factors and score contributions. Check independence of criteria, double-counting, anchoring bias, and whether evidence really supports each input. No evidence verification is automated.</p></div><div class="method"><small>06 / PRIVACY</small><b>Zero-cloud by default</b><p>No tracking, data uploads, paid services, model downloads, or external code dependencies. Changes persist only in your local browser when storage is enabled. Export JSON to preserve or transfer your work.</p></div></div></section> | |
| <div class="footer"><span><strong>◇ DecisionForge</strong> · A Hugdrigo portfolio application · Project #78</span><span>MIT License · 100% client-side · Research and decision support</span></div> | |
| </main></div> | |
| <div id="toast" class="toast" role="status" aria-live="polite"></div> | |
| <div id="dialog" class="overlay" role="dialog" aria-modal="true" aria-labelledby="dialogTitle"><div class="modal"><div class="modal-head"><div><h2 id="dialogTitle"></h2><p id="dialogSub"></p></div><button class="btn tiny ghost" id="closeDialog" aria-label="Close dialog">✕</button></div><form id="dialogForm"><div class="modal-body" id="dialogBody"></div><div class="modal-footer"><button class="btn" type="button" id="cancelDialog">Cancel</button><button class="btn primary" id="dialogSubmit" type="submit">Save</button></div></form></div></div> | |
| <input id="importFile" type="file" accept=".json,application/json" hidden> | |
| <script> | |
| ; | |
| (function(){ | |
| const STORAGE_KEY='hugdrigo-decisionforge-v1'; | |
| const COLORS=['#60e1db','#9f9cfb','#ffd18b','#ff96ab','#88e6b0','#a8c7ff','#dfa7ec','#f7b889','#86cbd4','#d7b3ac','#c4e583','#9cb3eb']; | |
| const SAMPLE={version:1,criteria:[ | |
| {id:'fit',name:'Strategic fit',unit:'pts',direction:'higher',low:0,high:100,weight:25}, | |
| {id:'value',name:'Annual net value',unit:'$k',direction:'higher',low:0,high:1000,weight:30}, | |
| {id:'speed',name:'Time to launch',unit:'months',direction:'lower',low:1,high:24,weight:15}, | |
| {id:'risk',name:'Delivery risk',unit:'pts',direction:'lower',low:0,high:100,weight:20}, | |
| {id:'impact',name:'Sustainability',unit:'pts',direction:'higher',low:0,high:100,weight:10} | |
| ],options:[ | |
| {id:'saas',name:'SaaS Expansion',values:{fit:92,value:760,speed:8,risk:36,impact:78}}, | |
| {id:'partner',name:'Regional Partnership',values:{fit:83,value:640,speed:5,risk:24,impact:85}}, | |
| {id:'automation',name:'Process Automation',values:{fit:78,value:540,speed:4,risk:17,impact:94}}, | |
| {id:'market',name:'New Market Entry',values:{fit:88,value:920,speed:17,risk:67,impact:70}} | |
| ],uncertainty:12}; | |
| const $=id=>document.getElementById(id); const fmt=(n,dp=1)=>Number(n).toLocaleString('en-US',{minimumFractionDigits:dp,maximumFractionDigits:dp}); const clamp=(v,a,b)=>Math.min(b,Math.max(a,v)); | |
| const html=(s)=>String(s).replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); | |
| const clone=s=>JSON.parse(JSON.stringify(s)); let state=readSaved();let dialogMode='',editId=null;let lastFocus=null;let latestScores=[],latestMC=null;let storedSimulationSignature='';let toastTimer=null; | |
| function validData(v){if(!v||v.version!==1||!Array.isArray(v.criteria)||!Array.isArray(v.options)||v.criteria.length<2||v.criteria.length>8||v.options.length<2||v.options.length>12)return false;let ids=new Set(),oids=new Set();for(const c of v.criteria){if(!c||!/^[-\w]{1,30}$/.test(c.id)||ids.has(c.id)||typeof c.name!=='string'||!c.name.trim()||c.name.length>55||typeof c.unit!=='string'||c.unit.length>20||!['higher','lower'].includes(c.direction)||![c.low,c.high,c.weight].every(x=>typeof x==='number'&&Number.isFinite(x))||c.low>=c.high||c.high-c.low>1e9||Math.abs(c.low)>1e9||Math.abs(c.high)>1e9||c.weight<0||c.weight>100)return false;ids.add(c.id)}if(!v.criteria.some(c=>c.weight>0))return false;for(const o of v.options){if(!o||!/^[-\w]{1,30}$/.test(o.id)||oids.has(o.id)||typeof o.name!=='string'||!o.name.trim()||o.name.length>60||!o.values||typeof o.values!=='object')return false;oids.add(o.id);for(const c of v.criteria){const x=o.values[c.id];if(typeof x!=='number'||!Number.isFinite(x)||Math.abs(x)>1e9)return false}}return Number.isInteger(v.uncertainty)&&v.uncertainty>=0&&v.uncertainty<=25} | |
| function readSaved(){try{const raw=localStorage.getItem(STORAGE_KEY);if(!raw)return clone(SAMPLE);const v=JSON.parse(raw);return validData(v)?v:clone(SAMPLE)}catch{return clone(SAMPLE)}} | |
| function save(){try{localStorage.setItem(STORAGE_KEY,JSON.stringify(state))}catch{/*private browsing or quota: app remains usable in memory*/}} | |
| function toast(message,error=false){const el=$('toast');el.textContent=message;el.className='toast show'+(error?' error':'');clearTimeout(toastTimer);toastTimer=setTimeout(()=>el.className='toast',3600)} | |
| function normalize(value,c){const t=clamp((value-c.low)/(c.high-c.low),0,1);return(c.direction==='higher'?t:1-t)*100} | |
| function weights(criteria=state.criteria){let total=criteria.reduce((s,c)=>s+c.weight,0);return criteria.map(c=>total>0?c.weight/total:1/criteria.length)} | |
| function scoreOption(option,criteria=state.criteria,weightsArr=weights(criteria)){const subs=criteria.map(c=>normalize(option.values[c.id],c));const contrib=subs.map((x,i)=>x*weightsArr[i]);return {id:option.id,name:option.name,score:contrib.reduce((a,b)=>a+b,0),subs,contrib}} | |
| function rank(criteria=state.criteria,weightsArr=weights(criteria)){return state.options.map(o=>scoreOption(o,criteria,weightsArr)).sort((a,b)=>b.score-a.score||a.name.localeCompare(b.name))} | |
| function dominates(a,b){let strict=false;for(let i=0;i<a.subs.length;i++){if(a.subs[i]<b.subs[i]-1e-8)return false;if(a.subs[i]>b.subs[i]+1e-8)strict=true}return strict} | |
| function renderOverview(){latestScores=rank();const first=latestScores[0],second=latestScores[1];const mc=simulation();$('statWinner').textContent=first.name;$('statWinner').title=first.name;$('statWinner').style.fontSize=first.name.length>18?'18px':'';$('statProbability').textContent=fmt(mc.find(x=>x.id===first.id).winPct,0)+'%';$('statOptions').textContent=String(state.options.length);$('statCriteriaSub').textContent='Across '+state.criteria.length+' criteria';$('statLead').textContent='+'+fmt(Math.max(0,first.score-second.score),1); | |
| const dominated=latestScores.filter(x=>latestScores.some(y=>y.id!==x.id&&dominates(y,x))).map(x=>x.id); | |
| $('recommendation').innerHTML='<span class="icon">✦</span><div><span class="kicker">Current preferred option</span><b>'+html(first.name)+'</b><p>'+fmt(first.score,1)+'/100 weighted utility · '+fmt(first.score-second.score,1)+' point lead over '+html(second.name)+'</p></div>'; | |
| $('rankingBars').innerHTML=latestScores.map((a,i)=>'<div class="rank-row"><span class="rank-no">'+(i+1)+'</span><div><div class="rank-name"><span class="name" title="'+html(a.name)+'">'+html(a.name)+'</span><span class="rank-sub">'+(a.id===first.id?'LEADER':dominated.includes(a.id)?'DOMINATED':'ALTERNATIVE')+'</span></div><div class="track"><div class="fill" style="width:'+clamp(a.score,0,100).toFixed(3)+'%;background:'+COLORS[i%COLORS.length]+'"></div></div></div><div class="rank-score">'+fmt(a.score,1)+'<small>/ 100</small></div></div>').join(''); | |
| $('overviewInsight').innerHTML='<strong>Decision insight:</strong> The leader wins under the current priority weights. '+(dominated.length?dominated.length+' option(s) are Pareto-dominated across all normalized criteria. ':'No option is Pareto-dominated across every criterion. ')+'<br>Compare scenario shifts and uncertainty before committing resources.'; | |
| $('contributionBars').innerHTML=latestScores.map(a=>'<div class="stack-row"><span title="'+html(a.name)+'">'+html(a.name)+'</span><div class="stacks">'+a.contrib.map((p,i)=>'<i title="'+html(state.criteria[i].name)+': '+fmt(p,1)+' points" style="width:'+p.toFixed(3)+'%;background:'+COLORS[i]+'"></i>').join('')+'</div><b>'+fmt(a.score,1)+'</b></div>').join('');$('criteriaLegend').innerHTML=state.criteria.map((c,i)=>'<span><i style="background:'+COLORS[i]+'"></i>'+html(c.name)+'</span>').join('')} | |
| function renderMatrix(){const cols=state.criteria.map(c=>'<th scope="col" title="'+html(c.direction==='higher'?'Higher is better':'Lower is better')+'">'+html(c.name)+'<span class="small">'+html(c.unit)+' · '+(c.direction==='higher'?'↑ higher':'↓ lower')+'</span></th>').join('');$('matrixTable').innerHTML='<thead><tr><th scope="col">Strategic option</th>'+cols+'<th scope="col">Score</th></tr></thead><tbody>'+state.options.map(o=>{const s=scoreOption(o);return'<tr><td><input class="alt-input" aria-label="Option name" type="text" data-option="'+o.id+'" data-field="name" maxlength="60" value="'+html(o.name)+'"></td>'+state.criteria.map(c=>'<td><input type="number" aria-label="'+html(o.name+' / '+c.name)+'" data-option="'+o.id+'" data-criterion="'+c.id+'" step="any" min="-1000000000" max="1000000000" value="'+o.values[c.id]+'"></td>').join('')+'<td><span class="chip '+(s.id===latestScores[0].id?'top':'')+'">'+fmt(s.score,1)+'</span></td></tr>'}).join('')+'</tbody>'} | |
| function setWeights(preset){const defs={base:[25,30,15,20,10],value:[16,45,9,20,10],speed:[20,20,40,12,8],resilience:[20,17,13,40,10]};const ids=['fit','value','speed','risk','impact'];const mapping=Object.fromEntries(ids.map((k,i)=>[k,defs[preset][i]]));let matched=state.criteria.filter(c=>Object.hasOwn(mapping,c.id));if(!matched.length){state.criteria.forEach(c=>c.weight=20);save();render();toast('Equal priorities restored for custom criteria.');return}state.criteria.forEach(c=>{if(Object.hasOwn(mapping,c.id))c.weight=mapping[c.id]});save();render();} | |
| function renderWeights(){let norm=weights();$('weightControls').innerHTML=state.criteria.map((c,i)=>'<div class="criterion"><div class="criterion-top"><span class="cname" title="'+html(c.name)+'">'+html(c.name)+'</span><span class="weight-pill" id="norm-'+c.id+'">'+fmt(norm[i]*100,1)+'%</span></div><div class="meter"><input type="range" aria-label="'+html(c.name)+' priority" data-weight="'+c.id+'" min="0" max="100" step="1" value="'+c.weight+'"><input type="number" data-weight-number="'+c.id+'" aria-label="'+html(c.name)+' raw weight" min="0" max="100" step="1" value="'+c.weight+'"></div><small>'+html(c.unit)+' · '+(c.direction==='higher'?'higher':'lower')+' is better</small></div>').join('');$('weightNote').innerHTML='Current normalized weight total: <b>100%</b>. Raw input total: <b>'+state.criteria.reduce((s,c)=>s+c.weight,0)+'</b>. We normalize automatically; no hidden recalibration of performance anchors.';let sampleSame=state.criteria.length===5&&['fit','value','speed','risk','impact'].every(x=>state.criteria.some(c=>c.id===x));document.querySelectorAll('[data-preset]').forEach(b=>{b.disabled=!sampleSame;let v=b.dataset.preset;const sets={base:[25,30,15,20,10],value:[16,45,9,20,10],speed:[20,20,40,12,8],resilience:[20,17,13,40,10]};b.classList.toggle('active',sampleSame&&state.criteria.every(c=>c.weight===sets[v][['fit','value','speed','risk','impact'].indexOf(c.id)]))})} | |
| function sensitivityWeights(selected,percentage){const selectedIndex=state.criteria.findIndex(c=>c.id===selected);const restTotal=state.criteria.reduce((s,c,i)=>s+(i===selectedIndex?0:c.weight),0);const restCount=state.criteria.length-1;return state.criteria.map((c,i)=>i===selectedIndex?percentage/100:(1-percentage/100)*(restTotal>0?c.weight/restTotal:1/restCount))} | |
| function renderSensitivity(){const select=$('sensitivitySelect');const cur=select.value;select.innerHTML=state.criteria.map(c=>'<option value="'+c.id+'">'+html(c.name)+'</option>').join('');select.value=state.criteria.some(c=>c.id===cur)?cur:state.criteria[0].id;const chosen=select.value;let dataset=[];for(let pct=0;pct<=100;pct+=5){const w=sensitivityWeights(chosen,pct);dataset.push({pct,rank:rank(state.criteria,w)})}const W=650,H=278,L=39,R=13,T=13,B=37;let svg='<svg class="scatter" viewBox="0 0 '+W+' '+H+'" role="img" aria-label="Score sensitivity as '+html(state.criteria.find(c=>c.id===chosen).name)+' weight changes" xmlns="http://www.w3.org/2000/svg">';for(let tick=0;tick<=100;tick+=20){const y=T+(H-T-B)*(1-tick/100),x=L+(W-L-R)*(tick/100);svg+='<line x1="'+L+'" y1="'+y+'" x2="'+(W-R)+'" y2="'+y+'" class="chart-grid"/><text class="chart-label" x="'+(L-7)+'" y="'+(y+4)+'" text-anchor="end">'+tick+'</text><line x1="'+x+'" y1="'+T+'" x2="'+x+'" y2="'+(H-B)+'" class="chart-grid"/><text class="chart-label" x="'+x+'" y="'+(H-12)+'" text-anchor="middle">'+tick+'%</text>'}dataset[0].rank.forEach((option,i)=>{let path=dataset.map((d,j)=>{const item=d.rank.find(x=>x.id===option.id),x=L+(W-L-R)*d.pct/100,y=T+(H-T-B)*(1-item.score/100);return(j?'L':'M')+x.toFixed(2)+' '+y.toFixed(2)}).join(' ');svg+='<path d="'+path+'" fill="none" stroke="'+COLORS[i]+'" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round"/>'});svg+='</svg>';$('sensitivityChart').innerHTML=svg; $('sensitivityLegend').innerHTML=dataset[0].rank.map((o,i)=>'<span><i style="background:'+COLORS[i]+'"></i>'+html(o.name)+'</span>').join('');const winners=dataset.map(d=>d.rank[0].id);const unique=[...new Set(winners)];const baseWinner=latestScores[0];const stepWins=winners.filter(x=>x===baseWinner.id).length;$('sensitivityInsight').innerHTML='<strong>Sensitivity readout:</strong> '+(unique.length===1?html(dataset[0].rank[0].name)+' leads at every tested weight.':unique.length+' different options lead across the 21 tested weight settings.')+' The current baseline leader, '+html(baseWinner.name)+', leads in '+stepWins+'/21 stress settings. Scores shift because importance changes, not because performance data was edited.'} | |
| function stringSeed(str){let h=2166136261;for(let i=0;i<str.length;i++){h^=str.charCodeAt(i);h=Math.imul(h,16777619)}return h>>>0} | |
| function seeded(seed){let a=seed>>>0;return()=>{a+=0x6D2B79F5;let t=a;t=Math.imul(t^t>>>15,t|1);t^=t+Math.imul(t^t>>>7,t|61);return((t^t>>>14)>>>0)/4294967296}} | |
| function quantile(sorted,q){const x=(sorted.length-1)*q,lo=Math.floor(x),hi=Math.ceil(x);return sorted[lo]+(sorted[hi]-sorted[lo])*(x-lo)} | |
| function simulation(){let signature=JSON.stringify({criteria:state.criteria,options:state.options,u:state.uncertainty});if(signature===storedSimulationSignature&&latestMC)return latestMC;const r=seeded(stringSeed(signature));const weightsArr=weights();const N=1200,n=state.options.length,spread=state.uncertainty/100;const wins=Array(n).fill(0),scores=Array.from({length:n},()=>[]);for(let t=0;t<N;t++){let best=-1e9,leaders=[];for(let i=0;i<n;i++){const o=state.options[i];let score=0;for(let j=0;j<state.criteria.length;j++){const c=state.criteria[j];let v=o.values[c.id];if(spread>0){const shift=(r()+r()-1)*spread*(c.high-c.low);v=clamp(v+shift,c.low,c.high)}score+=weightsArr[j]*normalize(v,c)}scores[i].push(score);if(score>best+1e-10){best=score;leaders=[i]}else if(Math.abs(score-best)<1e-10){leaders.push(i)}}for(const i of leaders)wins[i]+=1/leaders.length}latestMC=state.options.map((o,i)=>{scores[i].sort((a,b)=>a-b);return{id:o.id,name:o.name,winPct:100*wins[i]/N,p10:quantile(scores[i],.1),median:quantile(scores[i],.5),p90:quantile(scores[i],.9)}}).sort((a,b)=>b.winPct-a.winPct||a.name.localeCompare(b.name));storedSimulationSignature=signature;return latestMC} | |
| function renderUncertainty(){const vals=simulation();$('uncertaintyRange').value=String(state.uncertainty);$('uncertaintyValue').textContent=state.uncertainty+'% of anchor range';$('uncertaintyBars').innerHTML=vals.map((x,i)=>'<div class="dist-row"><span title="'+html(x.name)+'">'+html(x.name)+'</span><div class="barcell"><span style="width:'+x.winPct.toFixed(3)+'%;background:'+COLORS[i]+'"></span></div><b>'+fmt(x.winPct,1)+'%</b></div>').join('');const base=rank(),winMap=new Map(vals.map(x=>[x.id,x]));$('uncertaintyRows').innerHTML=base.map((x)=>{const a=winMap.get(x.id);return'<tr><td><b>'+html(x.name)+'</b></td><td class="right">'+fmt(x.score,1)+'</td><td class="right">'+fmt(a.p10,1)+'</td><td class="right">'+fmt(a.median,1)+'</td><td class="right">'+fmt(a.p90,1)+'</td><td class="right" style="color:var(--cyan);font-weight:760">'+fmt(a.winPct,1)+'%</td></tr>'}).join('');const top=vals[0];$('uncertaintyInsight').innerHTML='<strong>Uncertainty readout:</strong> '+html(top.name)+' places first in '+fmt(top.winPct,1)+'% of synthetic draws.'+(top.winPct<60?' Ranking stability is limited under these assumptions.':' It appears comparatively stable under these assumptions.')+' These percentages are model sensitivity measures, not empirical success probabilities.'} | |
| function render(){renderOverview();renderMatrix();renderWeights();renderSensitivity();renderUncertainty()} | |
| function updateWeight(id,value){const c=state.criteria.find(x=>x.id===id),n=Number(value);if(!c||!Number.isInteger(n)||n<0||n>100)return;if(n===0&&state.criteria.every(x=>x.id===id||x.weight===0)){toast('At least one criterion must retain a positive weight.',true);renderWeights();return}c.weight=n;save();render()} | |
| function safeFile(filename,text,mime='text/plain;charset=utf-8'){const blob=new Blob([text],{type:mime}),u=URL.createObjectURL(blob);const a=document.createElement('a');a.href=u;a.download=filename;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(u),1200)} | |
| function exportJSON(){safeFile('decisionforge-workspace.json',JSON.stringify(state,null,2)+'\n','application/json');toast('Workspace JSON exported.')} | |
| function exportCSV(){const head=['rank','option','weighted_score_0_100','illustrative_win_pct',...state.criteria.map(c=>'raw_'+c.id),...state.criteria.map(c=>'normalized_'+c.id)];const mc=new Map(simulation().map(x=>[x.id,x]));const cell=v=>'"'+String(v).replace(/"/g,'""')+'"';const lines=[head.map(cell).join(',')];rank().forEach((o,i)=>{const record=state.options.find(x=>x.id===o.id),line=[i+1,o.name,o.score.toFixed(4),mc.get(o.id).winPct.toFixed(4),...state.criteria.map(c=>record.values[c.id]),...o.subs.map(x=>x.toFixed(4))];lines.push(line.map(cell).join(','))});safeFile('decisionforge-ranked-options.csv',lines.join('\r\n')+'\r\n','text/csv;charset=utf-8');toast('Ranking CSV exported.')} | |
| function openDialog(mode,id=null){dialogMode=mode;editId=id;lastFocus=document.activeElement;const titles={addOption:['Add strategic option','Add a new alternative to your decision matrix.'],manageOptions:['Manage strategic options','Rename or remove options without replacing your full workspace.'],addCriterion:['Add decision criterion','Define a measurable factor and fixed reference anchors.'],manageCriteria:['Manage decision criteria','Review, edit or remove criteria and their reference scales.']};$('dialogTitle').textContent=titles[mode][0];$('dialogSub').textContent=titles[mode][1];$('dialogSubmit').hidden=mode.startsWith('manage');$('dialogSubmit').textContent=mode==='addOption'?'Add option':'Add criterion';if(mode==='addOption'){$('dialogBody').innerHTML='<div class="form-grid"><div class="field-row wide"><label for="newName">Option name</label><input id="newName" name="name" maxlength="60" required placeholder="e.g., Product localization"></div>'+state.criteria.map(c=>'<div class="field-row"><label for="newv_'+c.id+'">'+html(c.name)+' ('+html(c.unit)+')</label><input id="newv_'+c.id+'" type="number" step="any" min="-1000000000" max="1000000000" required value="'+fmt((c.low+c.high)/2,1).replaceAll(',','')+'"></div>').join('')+'</div>'}else if(mode==='addCriterion'){$('dialogBody').innerHTML='<div class="form-grid"><div class="field-row wide"><label for="newName">Criterion name</label><input id="newName" maxlength="55" required placeholder="e.g., Customer impact"></div><div class="field-row"><label for="newUnit">Display unit</label><input id="newUnit" maxlength="20" value="pts" required></div><div class="field-row"><label for="newDirection">Preferred direction</label><select id="newDirection"><option value="higher">Higher is better</option><option value="lower">Lower is better</option></select></div><div class="field-row"><label for="newLow">Low reference anchor</label><input id="newLow" type="number" step="any" min="-1000000000" max="1000000000" value="0" required></div><div class="field-row"><label for="newHigh">High reference anchor</label><input id="newHigh" type="number" step="any" min="-1000000000" max="1000000000" value="100" required></div><div class="field-row wide"><label for="newWeight">Raw priority weight (0–100)</label><input id="newWeight" type="number" min="0" max="100" step="1" value="10" required></div></div><div class="note">New criteria are initialized at the midpoint of their reference range for every option; edit the matrix to provide evidence-based values.</div>'}else if(mode==='manageOptions'){$('dialogBody').innerHTML=state.options.map(o=>'<div class="delete-row"><div><b>'+html(o.name)+'</b><p>'+html(o.id)+'</p></div><div class="top-actions"><button class="btn tiny" type="button" data-rename-option="'+o.id+'">Rename</button><button class="btn tiny danger" type="button" data-remove-option="'+o.id+'" '+(state.options.length<=2?'disabled':'')+'>Remove</button></div></div>').join('')+'<p class="tiny-note">Minimum 2 options. Removals require confirmation.</p>'}else{$('dialogBody').innerHTML=state.criteria.map(c=>'<div class="delete-row"><div><b>'+html(c.name)+'</b><p>'+html(c.unit)+' · '+html(c.direction)+' · ['+c.low+', '+c.high+']</p></div><div class="top-actions"><button class="btn tiny" type="button" data-edit-criterion="'+c.id+'">Edit</button><button class="btn tiny danger" type="button" data-remove-criterion="'+c.id+'" '+(state.criteria.length<=2||state.criteria.filter(x=>x.weight>0).length<=1&&c.weight>0?'disabled':'')+'>Remove</button></div></div>').join('')+'<p class="tiny-note">Minimum 2 criteria and at least one positive priority weight. Removals require confirmation.</p>'}$('dialog').classList.add('open');document.body.style.overflow='hidden';const first=$('dialogBody').querySelector('input,button');(first||$('closeDialog')).focus()} | |
| function closeDialog(){$('dialog').classList.remove('open');document.body.style.overflow='';if(lastFocus&&lastFocus.isConnected)lastFocus.focus()} | |
| function nextId(prefix){let id;do{id=prefix+'-'+Math.random().toString(36).slice(2,9)}while(state.criteria.some(c=>c.id===id)||state.options.some(o=>o.id===id));return id} | |
| function submitDialog(e){e.preventDefault();if(dialogMode==='addOption'){if(state.options.length>=12){toast('Maximum 12 options.',true);return}const name=$('newName').value.trim();if(!name){toast('Enter a name.',true);return}const values={};for(const c of state.criteria){const n=Number($('newv_'+c.id).value);if(!Number.isFinite(n)||$('newv_'+c.id).value.trim()===''||Math.abs(n)>1e9){toast('Enter a valid finite value for '+c.name+'.',true);return}values[c.id]=n}state.options.push({id:nextId('opt'),name,values})}else if(dialogMode==='addCriterion'){if(state.criteria.length>=8){toast('Maximum 8 criteria.',true);return}const name=$('newName').value.trim(),unit=$('newUnit').value.trim(),low=Number($('newLow').value),high=Number($('newHigh').value),weight=Number($('newWeight').value);if(!name||!unit||!Number.isFinite(low)||!Number.isFinite(high)||low>=high||high-low>1e9||Math.abs(low)>1e9||Math.abs(high)>1e9||!Number.isInteger(weight)||weight<0||weight>100){toast('Check criterion name, anchors, and weight.',true);return}const id=nextId('crt');state.criteria.push({id,name,unit,direction:$('newDirection').value,low,high,weight});for(const o of state.options)o.values[id]=(low+high)/2}else return;save();closeDialog();render();toast('Decision workspace updated.')} | |
| function handleManageClick(e){const b=e.target.closest('button');if(!b)return;const d=b.dataset;if(d.renameOption){const o=state.options.find(x=>x.id===d.renameOption);const name=prompt('Rename this strategic option:',o.name);if(name===null)return;const t=name.trim();if(!t||t.length>60){toast('Name must have 1–60 characters.',true);return}o.name=t;save();openDialog('manageOptions');render();toast('Option renamed.')}else if(d.removeOption){if(state.options.length<=2)return;const o=state.options.find(x=>x.id===d.removeOption);if(!confirm('Remove option "'+o.name+'"? This deletes its values from this workspace.'))return;state.options=state.options.filter(x=>x.id!==o.id);save();openDialog('manageOptions');render();toast('Option removed.')}else if(d.editCriterion){const c=state.criteria.find(x=>x.id===d.editCriterion);const name=prompt('Criterion name:',c.name);if(name===null)return;if(!name.trim()||name.trim().length>55){toast('Enter a valid criterion name.',true);return}const low=prompt('Low anchor (numeric):',String(c.low));if(low===null)return;const high=prompt('High anchor (numeric; must exceed low):',String(c.high));if(high===null)return;const unit=prompt('Display unit:',c.unit);if(unit===null)return;const direction=prompt('Preferred direction (higher or lower):',c.direction);if(direction===null)return;const lo=Number(low),hi=Number(high);if(!low.trim()||!high.trim()||!Number.isFinite(lo)||!Number.isFinite(hi)||Math.abs(lo)>1e9||Math.abs(hi)>1e9||hi<=lo||hi-lo>1e9||!unit.trim()||unit.trim().length>20||!['higher','lower'].includes(direction.trim())){toast('Invalid reference anchors, unit or direction.',true);return}Object.assign(c,{name:name.trim(),low:lo,high:hi,unit:unit.trim(),direction:direction.trim()});save();openDialog('manageCriteria');render();toast('Criterion settings updated.')}else if(d.removeCriterion){if(state.criteria.length<=2)return;const c=state.criteria.find(x=>x.id===d.removeCriterion);if(c.weight>0&&state.criteria.filter(x=>x.weight>0).length<=1)return;if(!confirm('Remove criterion "'+c.name+'" and all its values?'))return;state.criteria=state.criteria.filter(x=>x.id!==c.id);state.options.forEach(o=>delete o.values[c.id]);save();openDialog('manageCriteria');render();toast('Criterion removed.')}} | |
| $('matrixTable').addEventListener('change',e=>{const t=e.target;const oid=t.dataset.option;if(!oid)return;const o=state.options.find(x=>x.id===oid);if(!o)return;if(t.dataset.field==='name'){const v=t.value.trim();if(v.length<1||v.length>60){toast('Option names must contain 1–60 characters.',true);renderMatrix();return}o.name=v}else if(t.dataset.criterion){const v=Number(t.value);if(t.value.trim()===''||!Number.isFinite(v)||Math.abs(v)>1e9){toast('Enter a valid number.',true);renderMatrix();return}o.values[t.dataset.criterion]=v}else return;save();render()}); | |
| $('weightControls').addEventListener('change',e=>{const t=e.target;if(t.dataset.weight||t.dataset.weightNumber)updateWeight(t.dataset.weight||t.dataset.weightNumber,t.value)}); | |
| $('weightControls').addEventListener('input',e=>{const t=e.target;if(t.dataset.weight){const match=$('weightControls').querySelector('[data-weight-number="'+t.dataset.weight+'"]');if(match)match.value=t.value}}); | |
| $('presets').addEventListener('click',e=>{const b=e.target.closest('[data-preset]');if(b&&!b.disabled)setWeights(b.dataset.preset)}); | |
| $('resetWeightsBtn').addEventListener('click',()=>setWeights('base')); | |
| $('sensitivitySelect').addEventListener('change',renderSensitivity); | |
| $('uncertaintyRange').addEventListener('input',e=>{$('uncertaintyValue').textContent=e.target.value+'% of anchor range'}); | |
| $('uncertaintyRange').addEventListener('change',e=>{state.uncertainty=Number(e.target.value);save();render()}); | |
| $('addOptionBtn').onclick=()=>state.options.length<12?openDialog('addOption'):toast('Maximum 12 strategic options.',true); | |
| $('addCriterionBtn').onclick=()=>state.criteria.length<8?openDialog('addCriterion'):toast('Maximum 8 criteria.',true); | |
| $('manageOptionsBtn').onclick=()=>openDialog('manageOptions');$('manageCriteriaBtn').onclick=()=>openDialog('manageCriteria'); | |
| $('closeDialog').onclick=$('cancelDialog').onclick=closeDialog;$('dialogForm').addEventListener('submit',submitDialog);$('dialogBody').addEventListener('click',handleManageClick);$('dialog').addEventListener('click',e=>{if(e.target===$('dialog'))closeDialog()});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('dialog').classList.contains('open'))closeDialog()}); | |
| $('exportBtn').onclick=exportJSON;$('printBtn').onclick=()=>window.print();$('exportRankingBtn').onclick=exportCSV;$('downloadTemplateBtn').onclick=()=>{safeFile('decisionforge-example-data.json',JSON.stringify(SAMPLE,null,2)+'\n','application/json');toast('Example JSON downloaded.')}; | |
| $('importBtn').onclick=()=>{$('importFile').value='';$('importFile').click()};$('importFile').addEventListener('change',async e=>{const f=e.target.files?.[0];if(!f)return;if(f.size>300000){toast('JSON exceeds the 300 KB import limit.',true);return}try{const v=JSON.parse(await f.text());if(!validData(v))throw Error('Invalid or incompatible workspace schema.');if(!confirm('Replace the current local workspace with the imported data? Export a backup first if needed.'))return;state=v;storedSimulationSignature='';save();render();toast('Workspace imported successfully.')}catch(err){toast('Import failed: '+err.message,true)}}); | |
| $('sampleResetBtn').onclick=()=>{if(!confirm('Restore fictional sample data? This replaces current local edits. Export a backup first if needed.'))return;state=clone(SAMPLE);save();render();toast('Sample workspace restored.')}; | |
| document.querySelectorAll('#nav a').forEach(a=>a.addEventListener('click',()=>{document.querySelectorAll('#nav a').forEach(x=>x.classList.remove('active'));a.classList.add('active')})); | |
| window.DecisionForgeTest={normalize,weights,scoreOption,rank,simulation,quantile,dominates,sensitivityWeights,validData,getState:()=>clone(state),getSample:()=>clone(SAMPLE),importState:(v)=>{if(!validData(v))throw Error('invalid');state=clone(v);save();render()}}; | |
| render(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |