Upload 3 files
Browse files- LICENSE +21 -0
- README.md +82 -5
- index.html +56 -18
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Hugdrigo
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,89 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description: AI fairness
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FairnessScope
|
| 3 |
+
emoji: ⚖️
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
short_description: AI fairness and threshold simulator.
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# FairnessScope
|
| 14 |
+
|
| 15 |
+
### Model Evaluation & Fairness Diagnostics Studio | Hugdrigo Project #112
|
| 16 |
+
|
| 17 |
+
**Short description:** AI fairness and threshold simulator.
|
| 18 |
+
|
| 19 |
+
FairnessScope is an interactive, entirely client-side laboratory for exploring **binary classification diagnostics across two fictional, non-demographic groups**. It computes confusion matrices, selection rates, error-rate gaps, comparative thresholds, and illustrative false-positive/false-negative costs. This project is a transparent educational simulator, **not a fairness certification, legal assessment, or trained AI model**.
|
| 20 |
+
|
| 21 |
+
## What it does
|
| 22 |
+
|
| 23 |
+
- Executive dashboard for accuracy, selection gap, true-positive-rate (TPR) gap, and confusion matrices.
|
| 24 |
+
- Interactive threshold laboratory with editable false-positive and false-negative costs.
|
| 25 |
+
- Configurable policy presets: inclusive (35%), balanced (50%), conservative (70%), and custom (5%–95%).
|
| 26 |
+
- Threshold curves for group selection rates, normalized error cost, and selection-rate gap.
|
| 27 |
+
- Group explorer: sample count, positive-label prevalence, selection rate, TPR, FPR, precision, accuracy, and outcome composition.
|
| 28 |
+
- Editable 160-record synthetic dataset: add, edit, archive and restore without deleting individual records.
|
| 29 |
+
- Search and filter the record library, export CSV, export and validate JSON backup, and manually restore data.
|
| 30 |
+
- Dark theme, responsive desktop/mobile layout, browser-local persistence and no external libraries, APIs, model downloads, authentication or paid services.
|
| 31 |
+
|
| 32 |
+
## Hugging Face publication
|
| 33 |
+
|
| 34 |
+
1. Open https://huggingface.co/new-space under `Hugdrigo`.
|
| 35 |
+
2. Space name: **`fairnessscope-model-evaluation-studio`**.
|
| 36 |
+
3. Select **Static** SDK, blank template, **Public** visibility, and **MIT** license.
|
| 37 |
+
4. Upload `README.md`, `index.html`, and `LICENSE` to the repository **root**.
|
| 38 |
+
5. Commit changes. Hugging Face serves the application from `index.html`.
|
| 39 |
+
|
| 40 |
+
The YAML uses valid, explicitly allowed Hugging Face color values: `colorFrom: blue` and `colorTo: purple`. No API keys or environment secrets are required.
|
| 41 |
+
|
| 42 |
+
## Local use
|
| 43 |
+
|
| 44 |
+
Open `index.html` in a modern browser. If local-file permissions limit browser storage, launch a local server (`python3 -m http.server 8000` in the project folder), then visit `http://localhost:8000`.
|
| 45 |
+
|
| 46 |
+
## Data provenance
|
| 47 |
+
|
| 48 |
+
The app generates exactly 160 deterministic **fictional** observations, 80 in each abstract cohort (`A` and `B`), from a fixed pseudorandom seed. Each record stores `id`, `group`, an integer `score` from 0–100, binary `label` (0/1), and `archived` (boolean). Outcomes are synthetic and provide no evidence about real groups, institutions, demographic categories, model fairness, discrimination, or legal compliance. The two group names deliberately do not denote protected or sensitive traits.
|
| 49 |
+
|
| 50 |
+
**Decision rule:** predict positive if `score >= threshold`.
|
| 51 |
+
|
| 52 |
+
At each threshold the confusion matrix tracks **TP, TN, FP, FN**. Undefined rates where a denominator is zero display as `—`.
|
| 53 |
+
|
| 54 |
+
**Group metrics:**
|
| 55 |
+
|
| 56 |
+
- Selection rate = `(TP + FP) / N`
|
| 57 |
+
- Base rate = `(TP + FN) / N`
|
| 58 |
+
- TPR / recall = `TP / (TP + FN)`
|
| 59 |
+
- FPR = `FP / (FP + TN)`
|
| 60 |
+
- Precision = `TP / (TP + FP)`
|
| 61 |
+
- Accuracy = `(TP + TN) / N`
|
| 62 |
+
- Demographic parity gap = absolute difference between the two groups' selection rates.
|
| 63 |
+
- Equal opportunity gap = absolute difference between their TPRs.
|
| 64 |
+
- Equalized odds diagnostic = greater of absolute TPR gap and absolute FPR gap.
|
| 65 |
+
- Weighted error cost = `FP * configured FP cost + FN * configured FN cost`, with cost units purely hypothetical.
|
| 66 |
+
|
| 67 |
+
The line chart's error cost is **normalized to its own highest plotted value**, while its second line shows the selection gap in percentage points. Therefore the lines are visualization aids, not common-unit financial measures. The scenario comparison table displays raw costs.
|
| 68 |
+
|
| 69 |
+
## Limits and warnings
|
| 70 |
+
|
| 71 |
+
- These metrics are **descriptive**, not prescriptive, and are not a guarantee of fairness or an automated approval process. No normative pass/fail threshold is assigned.
|
| 72 |
+
- Fairness definitions may be incompatible in populations with different base rates. Measurement quality, group selection, uncertainty, and downstream impacts require contextual analysis.
|
| 73 |
+
- Synthetic labels and classifier scores are intentionally simplified. Results cannot be generalized to real operational decision systems.
|
| 74 |
+
- No confidence intervals, causal claims, policy recommendations, or statistical power estimates are offered.
|
| 75 |
+
- The app does not use a neural network, machine-learning inference API, automated demographic inference, or real personal data.
|
| 76 |
+
|
| 77 |
+
## Privacy and validation
|
| 78 |
+
|
| 79 |
+
- Processing and storage take place solely in the user's browser. No network requests, analytics, trackers, fonts or CDN libraries are used.
|
| 80 |
+
- Local browser storage can be disabled in restricted environments. Export JSON to retain work independently.
|
| 81 |
+
- JSON imports are restricted to version 1, 1–500 records, unique safe ASCII identifiers (2–32 characters), valid groups, 0–100 integer scores, exact binary labels, boolean archive values, bounded thresholds and costs.
|
| 82 |
+
- Stored and imported text is HTML escaped before rendering. CSV export quotes values and neutralizes common spreadsheet-formula prefixes.
|
| 83 |
+
- Archive status is reversible; the Reset control can restore demonstration data after confirmation.
|
| 84 |
+
|
| 85 |
+
## Repository contents
|
| 86 |
+
|
| 87 |
+
- `README.md`: Hugging Face configuration, overview, methodology, and limitations.
|
| 88 |
+
- `index.html`: complete responsive application with all CSS and JavaScript bundled locally.
|
| 89 |
+
- `LICENSE`: MIT license.
|
index.html
CHANGED
|
@@ -1,19 +1,57 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#101d35"><title>FairnessScope | Model Evaluation Studio</title>
|
| 3 |
+
<style>
|
| 4 |
+
:root{--bg:#f3f6fb;--paper:#fff;--side:#101d35;--side2:#172a49;--ink:#1b2945;--muted:#71819b;--line:#e3eaf3;--soft:#eff3fc;--brand:#5149bd;--brand2:#8e83e7;--good:#1e9c81;--orange:#e7a24e;--red:#c95470;--radius:16px;--shadow:0 18px 40px rgba(27,47,77,.045);color-scheme:light}
|
| 5 |
+
*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;font:13px/1.5 Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;color:var(--ink);background:var(--bg)}button,input,select{font:inherit}button{cursor:pointer}input,select{min-width:0}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #ecb750;outline-offset:2px}a{color:var(--brand)}.shell{min-height:100vh;display:flex}.side{width:235px;background:var(--side);position:fixed;inset:0 auto 0 0;color:#b2c4da;padding:27px 15px;display:flex;flex-direction:column;z-index:12}.logo{display:flex;gap:10px;align-items:center;margin:4px 6px 35px}.logoMark{height:44px;width:44px;display:grid;place-items:center;border-radius:13px;color:#131c44;background:linear-gradient(140deg,#c5c0ff,#76d8dc);font-weight:900;font-size:25px}.logo strong{font-size:19px;color:white;letter-spacing:-.7px}.logo small{display:block;color:#92a9c3;font-size:8px;letter-spacing:1.8px;font-weight:800}.sideHead{color:#7589ad;font-size:9px;font-weight:900;letter-spacing:1.9px;text-transform:uppercase;margin:0 10px 12px}.nav{display:grid;gap:7px}.nav button{color:#acbfd9;display:flex;align-items:center;gap:11px;width:100%;text-align:left;background:transparent;border:0;padding:13px;border-radius:9px;font-weight:700}.nav button:hover{background:#273b60;color:#fff}.nav button.active{background:#2c386c;color:#fff;box-shadow:inset 3px 0 #b8aeff}.nav span{font-size:17px;width:22px;text-align:center}.sidefoot{margin-top:auto;background:#1b2c4b;border:1px solid #304364;border-radius:13px;padding:14px;font-size:11px;line-height:1.7}.sidefoot b{color:#c3c0ff}.main{margin-left:235px;width:calc(100% - 235px);max-width:1800px;padding:25px 35px 45px}.top{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:27px}.crumb{color:var(--muted);font-size:11px;font-weight:700}.crumb b{color:var(--ink)}.actions{display:flex;gap:8px;flex-wrap:wrap}.btn{border:1px solid var(--line);border-radius:10px;background:var(--paper);padding:10px 13px;font-size:11px;font-weight:800;color:var(--ink);white-space:nowrap}.btn:hover{background:var(--soft);border-color:#c2c8ec}.btn.primary{background:var(--brand);border-color:var(--brand);color:#fff}.btn.primary:hover{background:#4035a3}.btn.small{padding:7px 10px;font-size:10px}.btn.danger{color:var(--red)}.header{display:flex;gap:15px;justify-content:space-between;align-items:start;margin-bottom:24px}.eyebrow{color:var(--brand);text-transform:uppercase;letter-spacing:1.9px;font-weight:900;font-size:10px;margin-bottom:8px}.header h1{font-size:clamp(27px,3.3vw,40px);letter-spacing:-1.6px;line-height:1.1;margin:0 0 7px}.header p{margin:0;color:var(--muted);font-size:13px;max-width:780px}.tag{background:#e9f9f5;color:#178b76;border-radius:30px;font-size:10px;font-weight:900;padding:8px 12px;white-space:nowrap}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin-bottom:16px}.kpi,.panel{background:var(--paper);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:var(--radius)}.kpi{padding:18px;min-height:137px}.kTop{display:flex;align-items:center;justify-content:space-between;color:var(--muted);font-size:11px;font-weight:800;gap:8px}.ico{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;background:#f0efff;color:var(--brand);font-size:17px}.kpi strong{display:block;font-size:30px;font-weight:850;letter-spacing:-1px;margin:11px 0 2px}.kpi .note{font-size:10px;color:var(--muted)}.grid{display:grid;grid-template-columns:1.55fr 1fr;gap:15px;margin-bottom:16px}.grid.equal{grid-template-columns:1fr 1fr}.panel{padding:20px;min-width:0}.panelHeader{display:flex;justify-content:space-between;align-items:start;gap:10px;margin-bottom:18px}.panelHeader h2{font-size:16px;margin:0;letter-spacing:-.4px}.panelHeader p{margin:4px 0 0;color:var(--muted);font-size:11px}.chip{border-radius:30px;background:#f0effd;color:var(--brand);font-weight:850;font-size:10px;padding:6px 9px;white-space:nowrap}.plot{width:100%;display:block;overflow:visible}.legend{display:flex;gap:16px;flex-wrap:wrap;font-size:11px;color:var(--muted);margin-top:8px}.dot{height:8px;width:8px;border-radius:50%;display:inline-block;margin-right:6px}.rowBars{display:grid;gap:15px}.barrow{display:grid;grid-template-columns:105px 1fr 55px;gap:9px;align-items:center;font-size:11px}.track{background:#eef0f7;height:10px;border-radius:7px;overflow:hidden}.track i{height:100%;border-radius:7px;display:block}.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}.stat{padding:15px;border:1px solid var(--line);background:var(--soft);border-radius:12px}.stat b{display:block;font-size:21px;letter-spacing:-.5px}.stat small{font-size:10px;color:var(--muted)}.hint{background:#f3f2ff;color:#464082;border-left:3px solid var(--brand2);font-size:11px;padding:13px;border-radius:7px;line-height:1.65}.tabs{display:flex;gap:7px;flex-wrap:wrap}.tab{border-radius:25px;border:1px solid var(--line);background:var(--paper);padding:9px 13px;color:var(--muted);font-weight:800;font-size:11px}.tab.active{color:#fff;border-color:var(--brand);background:var(--brand)}.control{display:grid;gap:7px;color:var(--muted);font-weight:750;font-size:11px}.control input:not([type=range]),.control select,.control textarea{border:1px solid #d6e0ed;background:var(--paper);color:var(--ink);padding:11px;border-radius:9px;width:100%}.control input[type=range]{accent-color:var(--brand);width:100%;min-height:20px}.controls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.hr{border:0;border-top:1px solid var(--line);margin:18px 0}.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px}.miniStat{padding:14px;border:1px solid var(--line);border-radius:12px}.miniStat b{display:block;font-size:21px}.miniStat small{color:var(--muted)}.tableWrap{width:100%;overflow-x:auto;border:1px solid var(--line);border-radius:12px}table{width:100%;border-collapse:collapse;text-align:left;min-width:600px}th{font-size:10px;letter-spacing:.7px;text-transform:uppercase;color:#8491a5;background:#f7f9fc}th,td{padding:12px 13px;border-bottom:1px solid var(--line)}td{font-size:12px}tr:last-child td{border-bottom:0}tr:hover td{background:#fafbff}.status{padding:4px 8px;border-radius:20px;background:#e6f8f2;color:#1b9577;display:inline-block;font-size:10px;font-weight:800}.status.off{background:#fff0f0;color:#ba6374}.metric{font-variant-numeric:tabular-nums}.empty{color:var(--muted);padding:26px;text-align:center}.labelText{font-size:11px;color:var(--muted)}.footer{color:var(--muted);font-size:10px;margin-top:20px}.formula{font-size:12px;line-height:1.8;color:var(--ink)}.formula code{background:var(--soft);padding:3px 5px;border-radius:4px}.stack{display:grid;gap:12px}.test{display:flex;align-items:start;gap:12px;border:1px solid var(--line);padding:14px;border-radius:12px}.test strong{font-size:12px}.test small{display:block;color:var(--muted);margin-top:4px}.toast{position:fixed;right:22px;bottom:22px;z-index:30;background:#111d35;color:#fff;border-radius:12px;padding:13px 16px;box-shadow:0 18px 55px #11203450;max-width:min(90vw,420px);display:none}.toast.show{display:block}.view{display:none}.view.active{display:block}.hide{display:none!important}.opt{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted)}.opt input{accent-color:var(--brand)}.dangerText{color:var(--red)}.goodText{color:var(--good)}body.dark{--bg:#0e1628;--paper:#18243a;--ink:#e6ecfc;--muted:#9aabc3;--line:#2b3a53;--soft:#202e49;--brand:#a89ffa;--brand2:#a89ffa;--shadow:none;color-scheme:dark}body.dark .chip,body.dark .ico{background:#302f59}body.dark th{background:#202b43;color:#aabbd2}body.dark tr:hover td{background:#202b43}body.dark .track{background:#34415a}body.dark .hint{background:#242a54;color:#ddd8ff}body.dark .tag{background:#16453e;color:#a3e9d0}body.dark .status{background:#18443a;color:#a3e6d2}body.dark .status.off{background:#4b2938;color:#ffc0ca}
|
| 6 |
+
@media(max-width:1050px){.side{width:78px;padding:20px 10px}.logo{justify-content:center;margin:5px 0 30px}.logo strong,.logo small,.sideHead,.nav button .navName,.sidefoot{display:none}.nav button{justify-content:center}.nav span{width:auto}.main{margin-left:78px;width:calc(100% - 78px);padding:22px}.grid{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:600px){.shell{display:block}.side{position:sticky;inset:0 0 auto;width:100%;height:auto;padding:9px 12px;display:flex;flex-direction:row;align-items:center;gap:11px;overflow-x:auto}.logo{margin:0;flex:none}.logoMark{width:34px;height:34px;border-radius:9px;font-size:18px}.nav{display:flex;gap:3px}.nav button{width:auto;padding:9px;border-radius:8px;flex:none}.nav span{font-size:16px}.main{margin:0;width:100%;padding:16px 13px 40px}.top{align-items:start;flex-wrap:wrap;margin-bottom:20px}.actions{width:100%}.header{flex-wrap:wrap;gap:10px}.kpis{gap:9px}.kpi{padding:13px;min-height:117px}.kpi strong{font-size:24px}.grid,.grid.equal{grid-template-columns:1fr;gap:11px}.panel{padding:15px}.controls,.three{grid-template-columns:1fr}.stats{grid-template-columns:1fr 1fr}.barrow{grid-template-columns:83px 1fr 47px}.tag{margin-bottom:0}.header h1{font-size:30px}.btn{padding:9px 10px}.kpi .ico{height:28px;width:28px}.panelHeader{flex-wrap:wrap}}
|
| 7 |
+
</style></head>
|
| 8 |
+
<body><div class="shell"><aside class="side"><div class="logo"><div class="logoMark">◈</div><div><strong>FairnessScope</strong><small>HUGDRIGO · #112</small></div></div><div class="sideHead">Intelligence studio</div><nav class="nav" id="nav"><button class="active" data-view="overview"><span>◫</span><span class="navName">Executive overview</span></button><button data-view="lab"><span>◈</span><span class="navName">Threshold lab</span></button><button data-view="groups"><span>☷</span><span class="navName">Group explorer</span></button><button data-view="records"><span>▦</span><span class="navName">Record library</span></button><button data-view="method"><span>◎</span><span class="navName">Methodology</span></button></nav><div class="sidefoot"><b>EDUCATIONAL SIMULATOR</b><div>Entirely synthetic records. No protected attributes, personal information, APIs or external tracking.</div></div></aside><main class="main"><div class="top"><div class="crumb">Hugdrigo Portfolio / Model Governance / <b>FairnessScope</b></div><div class="actions"><button class="btn" id="themeBtn">◐ Theme</button><button class="btn" id="csvBtn">↓ CSV</button><button class="btn" id="jsonBtn">↓ Backup</button><button class="btn" id="importBtn">↑ Restore JSON</button><input id="fileInput" type="file" accept=".json,application/json" hidden></div></div>
|
| 9 |
+
<section class="view active" id="overview"><div class="header"><div><div class="eyebrow">Decision systems · fairness diagnostics</div><h1>Measure the disparity.<br>Understand the trade-off.</h1><p>Explore transparent classification metrics across two synthetic groups. Inspect threshold choices, errors and group outcomes without claiming that any single metric proves fairness.</p></div><div class="tag">● SIMULATED DATA</div></div><div class="kpis" id="kpis"></div><div class="grid"><article class="panel"><div class="panelHeader"><div><h2>Outcome balance</h2><p>Selection rates and true-positive rates at the active threshold</p></div><span class="chip" id="thresholdChip">Threshold 50%</span></div><div class="rowBars" id="mainBars"></div><hr class="hr"><div class="legend"><span><i class="dot" style="background:#6557d7"></i>Group A</span><span><i class="dot" style="background:#29a3aa"></i>Group B</span><span>Equal bars do not imply equal error rates</span></div></article><article class="panel"><div class="panelHeader"><div><h2>Diagnostic summary</h2><p>Absolute gaps between groups</p></div><span class="chip">Percentage points</span></div><div class="stats" id="gaps"></div><div class="hint" style="margin-top:13px">A disparity is a prompt to investigate, not proof of unlawful discrimination. Base rates, labels and measurement quality matter.</div></article></div><div class="grid equal"><article class="panel"><div class="panelHeader"><div><h2>Group confusion matrices</h2><p>Errors and correct predictions by cohort</p></div></div><div class="stats" id="confusions"></div></article><article class="panel"><div class="panelHeader"><div><h2>Threshold sensitivity</h2><p>Compare selection disparity as the decision cutoff moves</p></div></div><svg class="plot" id="overviewChart" viewBox="0 0 560 232" aria-label="Line chart showing group selection rates across thresholds" role="img"></svg><div class="legend"><span><i class="dot" style="background:#6557d7"></i>Group A selection</span><span><i class="dot" style="background:#29a3aa"></i>Group B selection</span></div></article></div></section>
|
| 10 |
+
<section class="view" id="lab"><div class="header"><div><div class="eyebrow">Scenario testing</div><h1>Threshold laboratory</h1><p>Set a decision cutoff, assign relative error costs and compare policy choices on the same synthetic population.</p></div><span class="tag">REPRODUCIBLE</span></div><div class="grid"><article class="panel"><div class="panelHeader"><div><h2>Decision controls</h2><p>Changes update every metric immediately</p></div></div><div class="controls"><label class="control">Scenario<select id="scenario"><option value="balanced">Balanced · 50%</option><option value="conservative">Conservative · 70%</option><option value="inclusive">Inclusive · 35%</option><option value="custom">Custom</option></select></label><label class="control">False positive cost (units)<input id="fpCost" type="number" min="1" max="100" step="1" value="2"></label><label class="control">False negative cost (units)<input id="fnCost" type="number" min="1" max="100" step="1" value="4"></label></div><div style="margin-top:20px"><label class="control" for="threshold">Decision threshold: <b id="thresholdRead">50%</b><input type="range" id="threshold" min="5" max="95" step="1" value="50"></label></div><hr class="hr"><div class="three" id="labStats"></div><div class="hint" style="margin-top:17px">This tool compares threshold-based decisions. It does not establish legal compliance, recommend deployment, or evaluate causes of group disparities.</div></article><article class="panel"><div class="panelHeader"><div><h2>Cost vs disparity</h2><p>Changing the threshold changes both error costs and selection gap</p></div></div><svg class="plot" id="labChart" viewBox="0 0 540 302" role="img" aria-label="Cost and group disparity chart"></svg><div class="legend"><span><i class="dot" style="background:#6557d7"></i>Normalized error cost</span><span><i class="dot" style="background:#e6a04e"></i>Selection gap, pp</span></div></article></div><div class="panel"><div class="panelHeader"><div><h2>Threshold comparison</h2><p>Compare preconfigured options and current custom setting</p></div></div><div class="tableWrap"><table><thead><tr><th>Policy</th><th>Threshold</th><th>Selection gap</th><th>TPR gap</th><th>Overall accuracy</th><th>Weighted error cost</th></tr></thead><tbody id="scenarioTable"></tbody></table></div></div></section>
|
| 11 |
+
<section class="view" id="groups"><div class="header"><div><div class="eyebrow">Cohort diagnostics</div><h1>Group explorer</h1><p>Compare observed performance and error distributions. The fictional groups are generic operational cohorts, not demographics.</p></div></div><div class="panel" style="margin-bottom:15px"><div class="panelHeader"><div><h2>Classification outcomes</h2><p>Counts and rates at the active threshold</p></div></div><div class="tableWrap"><table><thead><tr><th>Group</th><th>N</th><th>Base rate</th><th>Selection</th><th>TPR</th><th>FPR</th><th>Precision</th><th>Accuracy</th></tr></thead><tbody id="groupTable"></tbody></table></div></div><div class="grid equal"><article class="panel"><div class="panelHeader"><div><h2>Observed outcome composition</h2><p>TP, FP, FN and TN counts</p></div></div><div id="outcomeBars" class="stack"></div></article><article class="panel"><div class="panelHeader"><div><h2>Notes for interpretation</h2><p>Questions worth asking before acting</p></div></div><div class="stack"><div class="test"><div class="ico">1</div><div><strong>How were labels collected?</strong><small>Historical outcomes can contain missingness, noise and pre-existing bias.</small></div></div><div class="test"><div class="ico">2</div><div><strong>Do base rates differ?</strong><small>Conflicting group-fairness criteria may not all be jointly achievable.</small></div></div><div class="test"><div class="ico">3</div><div><strong>Is the sample representative?</strong><small>Small groups and sampling bias can distort apparent gaps.</small></div></div><div class="test"><div class="ico">4</div><div><strong>Are effects measured after deployment?</strong><small>Real governance requires stakeholders, monitoring and impact review.</small></div></div></div></article></div></section>
|
| 12 |
+
<section class="view" id="records"><div class="header"><div><div class="eyebrow">Data stewardship</div><h1>Synthetic record library</h1><p>Create, edit, archive or restore fictional classifier observations. Your modifications remain in this browser unless you export a JSON backup.</p></div><button class="btn primary" id="newBtn">+ Add record</button></div><div class="panel" style="margin-bottom:15px"><div class="controls"><label class="control">Search by record ID<input id="search" type="search" placeholder="e.g. FS-001"></label><label class="control">Group<select id="filterGroup"><option value="all">All groups</option><option value="A">Group A</option><option value="B">Group B</option></select></label><label class="control">Status<select id="filterStatus"><option value="active">Active records</option><option value="archived">Archived records</option><option value="all">All records</option></select></label></div></div><div class="panel"><div class="panelHeader"><div><h2>Record registry</h2><p id="recordCount">0 entries</p></div><span class="chip">Max 500 records</span></div><div class="tableWrap"><table><thead><tr><th>ID</th><th>Group</th><th>Model score</th><th>Observed label</th><th>Decision</th><th>Status</th><th>Actions</th></tr></thead><tbody id="recordTable"></tbody></table></div></div><div class="panel hide" id="editor" style="margin-top:15px"><div class="panelHeader"><h2 id="editTitle">Add synthetic record</h2><button class="btn small" id="cancelEdit">Close</button></div><form id="editForm"><div class="controls"><label class="control">Record ID<input id="recId" maxlength="32" required placeholder="FS-201" pattern="[A-Za-z0-9_-]{2,32}"></label><label class="control">Group<select id="recGroup"><option value="A">Group A</option><option value="B">Group B</option></select></label><label class="control">Score (0 to 100)<input id="recScore" type="number" min="0" max="100" step="1" required></label></div><div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap"><label class="opt">Observed positive label <input id="recLabel" type="checkbox"></label><button type="submit" class="btn primary">Save record</button></div></form></div></section>
|
| 13 |
+
<section class="view" id="method"><div class="header"><div><div class="eyebrow">Transparency & limitations</div><h1>Methodology</h1><p>Audit the assumptions behind this educational classifier-comparison application.</p></div><span class="tag">NO LIVE MODEL</span></div><div class="grid equal"><article class="panel"><div class="panelHeader"><h2>Metric definitions</h2></div><div class="formula"><p><b>Prediction:</b> positive when <code>score ≥ threshold</code>.</p><p><b>Selection rate:</b> predicted positives / group total.</p><p><b>True positive rate (TPR):</b> TP / (TP + FN).</p><p><b>False positive rate (FPR):</b> FP / (FP + TN).</p><p><b>Precision:</b> TP / (TP + FP).</p><p><b>Accuracy:</b> (TP + TN) / group total.</p><p><b>Demographic parity gap:</b> absolute difference in group selection rates.</p><p><b>Equal opportunity gap:</b> absolute difference in group TPR.</p><p><b>Equalized odds diagnostic:</b> max of absolute TPR and FPR differences.</p><p><b>Error cost:</b> FP × FP cost + FN × FN cost (illustrative units).</p></div></article><article class="panel"><div class="panelHeader"><h2>Safeguards and limitations</h2></div><div class="stack"><div class="test"><span class="ico">✓</span><div><strong>Synthetic data only</strong><small>Generated with a deterministic pseudorandom function. No sensitive attributes or real persons.</small></div></div><div class="test"><span class="ico">✓</span><div><strong>No fairness certification</strong><small>Different definitions can conflict. Metrics depend on label quality, setting and social context.</small></div></div><div class="test"><span class="ico">✓</span><div><strong>Not a trained classifier</strong><small>Only a simulated score and binary label dataset is analyzed. There is no online inference.</small></div></div><div class="test"><span class="ico">✓</span><div><strong>Local-first</strong><small>No network calls, cookies, ads, external fonts, telemetry or servers. JSON backup is user-controlled.</small></div></div><div class="test"><span class="ico">✓</span><div><strong>Defensive inputs</strong><small>Validates bounded JSON datasets; escapes HTML and neutralizes common spreadsheet formulas in CSV exports.</small></div></div></div></article></div><div class="panel"><div class="panelHeader"><h2>Portfolio project #112</h2></div><p style="color:var(--muted)">Built as a reproducible demonstration of transparent AI evaluation for the Hugdrigo portfolio. Threshold experiments are descriptive, not policy advice. The datasets are fictional and should never be used as evidence about real groups or individuals.</p><button class="btn" id="resetBtn">Reset all synthetic data</button></div></section><div class="footer">FairnessScope · Hugdrigo #112 · Static educational application · MIT License · No external services</div></main></div><div class="toast" id="toast" role="status" aria-live="polite"></div>
|
| 14 |
+
<script>
|
| 15 |
+
'use strict';
|
| 16 |
+
(()=>{
|
| 17 |
+
const KEY='fairnessscope112.v1',VALID=['A','B'];let editKey=null,view='overview';
|
| 18 |
+
const $=id=>document.getElementById(id),round=(n,d=1)=>Number.isFinite(n)?n.toFixed(d):'—',pct=n=>Number.isFinite(n)?round(n*100,1)+'%':'—',safe=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 19 |
+
function seedData(){let z=1122026;const rnd=()=>((z^=z<<13,z^=z>>>17,z^=z<<5)>>>0)/4294967296;const out=[];for(let i=0;i<160;i++){const group=i%2===0?'A':'B';const base=group==='A'?0.53:0.43;const pos=rnd()<base?1:0;const score=Math.max(0,Math.min(100,Math.round((pos?67:35)+(rnd()+rnd()+rnd()-1.5)*33+(group==='B'?-5:3))));out.push({id:'FS-'+String(i+1).padStart(3,'0'),group,score,label:pos,archived:false});}return out;}
|
| 20 |
+
const DEFAULT=()=>({version:1,records:seedData(),threshold:50,scenario:'balanced',fpCost:2,fnCost:4,dark:false});
|
| 21 |
+
function validate(v){if(!v||v.version!==1||!Array.isArray(v.records)||v.records.length>500||v.records.length<1)throw Error('Invalid backup format or record count (1–500).');const ids=new Set();const records=v.records.map(x=>{if(!x||typeof x.id!=='string'||!/^[A-Za-z0-9_-]{2,32}$/.test(x.id)||ids.has(x.id)||!VALID.includes(x.group)||!Number.isInteger(x.score)||x.score<0||x.score>100||![0,1].includes(x.label)||typeof x.archived!=='boolean')throw Error('Invalid, duplicated, or out-of-range record.');ids.add(x.id);return {id:x.id,group:x.group,score:x.score,label:x.label,archived:x.archived};});const threshold=v.threshold,fp=v.fpCost,fn=v.fnCost;if(!Number.isInteger(threshold)||threshold<5||threshold>95||!Number.isInteger(fp)||fp<1||fp>100||!Number.isInteger(fn)||fn<1||fn>100||typeof v.dark!=='boolean'||!['balanced','inclusive','conservative','custom'].includes(v.scenario))throw Error('Invalid settings.');return {version:1,records,threshold,scenario:v.scenario,fpCost:fp,fnCost:fn,dark:v.dark};}
|
| 22 |
+
let state=DEFAULT();try{const raw=localStorage.getItem(KEY);if(raw)state=validate(JSON.parse(raw));}catch(e){try{localStorage.removeItem(KEY);}catch(_){} }
|
| 23 |
+
function save(){try{localStorage.setItem(KEY,JSON.stringify(state));}catch(e){toast('Local storage unavailable. Export a JSON backup to keep your work.');}}
|
| 24 |
+
function toast(message){$('toast').textContent=message;$('toast').classList.add('show');clearTimeout(toast.timer);toast.timer=setTimeout(()=>$('toast').classList.remove('show'),3500);}
|
| 25 |
+
function active(){return state.records.filter(r=>!r.archived)}
|
| 26 |
+
function cm(recs,t){const m={tp:0,tn:0,fp:0,fn:0,n:recs.length,positive:0,selected:0};for(const r of recs){const y=r.label===1,hat=r.score>=t;if(y)m.positive++;if(hat)m.selected++;if(y&&hat)m.tp++;else if(!y&&hat)m.fp++;else if(y)m.fn++;else m.tn++;}const div=(a,b)=>b?a/b:null;m.selection=div(m.selected,m.n);m.base=div(m.positive,m.n);m.tpr=div(m.tp,m.tp+m.fn);m.fpr=div(m.fp,m.fp+m.tn);m.ppv=div(m.tp,m.tp+m.fp);m.acc=div(m.tp+m.tn,m.n);return m;}
|
| 27 |
+
const abs=(a,b)=>Number.isFinite(a)&&Number.isFinite(b)?Math.abs(a-b):null;
|
| 28 |
+
function analyze(t=state.threshold){const rs=active(),a=cm(rs.filter(x=>x.group==='A'),t),b=cm(rs.filter(x=>x.group==='B'),t),all=cm(rs,t);return {a,b,all,dp:abs(a.selection,b.selection),eo:abs(a.tpr,b.tpr),fprGap:abs(a.fpr,b.fpr),odds:Math.max(abs(a.tpr,b.tpr)??0,abs(a.fpr,b.fpr)??0),cost:all.fp*state.fpCost+all.fn*state.fnCost};}
|
| 29 |
+
function barRow(label,n,color){return `<div class="barrow"><b>${safe(label)}</b><div class="track"><i style="width:${Math.max(0,Math.min(100,(n??0)*100))}%;background:${color}"></i></div><b class="metric" style="text-align:right">${pct(n)}</b></div>`;}
|
| 30 |
+
function kpi(label,val,note,icon){return `<div class="kpi"><div class="kTop">${label}<span class="ico">${icon}</span></div><strong class="metric">${val}</strong><div class="note">${note}</div></div>`;}
|
| 31 |
+
function chart(id,series,maxY=100){const svg=$(id),w=id==='labChart'?540:560,h=id==='labChart'?302:232,L=43,R=12,T=17,B=32,W=w-L-R,H=h-T-B,pts=series.map(s=>({color:s.color,values:s.values.map((p,i)=>({x:L+(i/(s.values.length-1))*W,y:T+H-(Math.min(maxY,Math.max(0,p))/maxY)*H}))}));let html='';for(let k=0;k<=4;k++){const y=T+H-H*k/4;html+=`<line x1="${L}" x2="${w-R}" y1="${y}" y2="${y}" stroke="var(--line)"/><text x="${L-9}" y="${y+4}" fill="var(--muted)" text-anchor="end" font-size="10">${Math.round(maxY*k/4)}</text>`;}for(let k=0;k<=4;k++){const x=L+W*k/4;html+=`<text x="${x}" y="${h-8}" fill="var(--muted)" text-anchor="middle" font-size="10">${Math.round(5+90*k/4)}%</text>`;}for(const s of pts){const d=s.values.map((p,i)=>(i?'L':'M')+p.x.toFixed(2)+' '+p.y.toFixed(2)).join(' ');html+=`<path d="${d}" stroke="${s.color}" stroke-width="3" fill="none" stroke-linecap="round"/>`; }const x=L+W*(state.threshold-5)/90;html+=`<line x1="${x}" x2="${x}" y1="${T}" y2="${T+H}" stroke="var(--orange)" stroke-width="1.3" stroke-dasharray="5 4"/>`;svg.innerHTML=html;}
|
| 32 |
+
function renderOverview(){const o=analyze();$('kpis').innerHTML=kpi('Active observations',String(o.all.n),'Synthetic binary-score records','▦')+kpi('Overall accuracy',pct(o.all.acc),'Correct decisions at current cutoff','◎')+kpi('Selection rate gap',pct(o.dp),'Absolute between-group difference','◈')+kpi('Opportunity gap',pct(o.eo),'Absolute TPR difference','⇌');$('thresholdChip').textContent='Threshold '+state.threshold+'%';$('mainBars').innerHTML='<h3 style="font-size:11px;margin:0 0 2px">Selection rate</h3>'+barRow('Group A',o.a.selection,'#6557d7')+barRow('Group B',o.b.selection,'#29a3aa')+'<h3 style="font-size:11px;margin:7px 0 2px">True-positive rate</h3>'+barRow('Group A',o.a.tpr,'#6557d7')+barRow('Group B',o.b.tpr,'#29a3aa');$('gaps').innerHTML=[['Selection gap',o.dp],['TPR gap',o.eo],['FPR gap',o.fprGap],['Odds diagnostic',o.odds]].map(([l,v])=>`<div class="stat"><small>${l}</small><b class="metric">${v===null?'—':round(v*100,1)+' pp'}</b></div>`).join('');$('confusions').innerHTML=[['A',o.a],['B',o.b]].map(([k,g])=>`<div class="stat"><b style="font-size:14px">Group ${k}</b><small>TP ${g.tp} · FP ${g.fp}</small><br><small>FN ${g.fn} · TN ${g.tn}</small><br><small>N = ${g.n}</small></div>`).join('');const th=Array.from({length:19},(_,i)=>5+i*5);chart('overviewChart',[{color:'#6557d7',values:th.map(t=>(analyze(t).a.selection??0)*100)},{color:'#29a3aa',values:th.map(t=>(analyze(t).b.selection??0)*100)}]);}
|
| 33 |
+
function renderLab(){if(document.activeElement!==$('threshold'))$('threshold').value=state.threshold;$('thresholdRead').textContent=state.threshold+'%';$('fpCost').value=state.fpCost;$('fnCost').value=state.fnCost;$('scenario').value=state.scenario;const d=analyze();$('labStats').innerHTML=`<div class="miniStat"><small>Error cost</small><b>${d.cost.toLocaleString('en-US')}</b></div><div class="miniStat"><small>Correct predictions</small><b>${pct(d.all.acc)}</b></div><div class="miniStat"><small>Selection gap</small><b>${pct(d.dp)}</b></div>`;const ts=Array.from({length:91},(_,i)=>5+i),a=ts.map(t=>analyze(t)),costs=a.map(x=>x.cost),mx=Math.max(1,...costs),costLine=ts.filter((_,i)=>i%5===0).map((_,j)=>costs[j*5]/mx*100),gapLine=ts.filter((_,i)=>i%5===0).map((_,j)=>(a[j*5].dp??0)*100);chart('labChart',[{color:'#6557d7',values:costLine},{color:'#e6a04e',values:gapLine}],100);const rows=[['Inclusive',35],['Balanced',50],['Conservative',70],['Current',state.threshold]];$('scenarioTable').innerHTML=rows.map(([name,t])=>{const v=analyze(t);return `<tr><td><b>${name}</b></td><td>${t}%</td><td>${pct(v.dp)}</td><td>${pct(v.eo)}</td><td>${pct(v.all.acc)}</td><td>${v.cost.toLocaleString('en-US')}</td></tr>`}).join('');}
|
| 34 |
+
function renderGroups(){const o=analyze();$('groupTable').innerHTML=[['Group A',o.a],['Group B',o.b],['Overall',o.all]].map(([s,m])=>`<tr><td><b>${s}</b></td><td>${m.n}</td><td>${pct(m.base)}</td><td>${pct(m.selection)}</td><td>${pct(m.tpr)}</td><td>${pct(m.fpr)}</td><td>${pct(m.ppv)}</td><td>${pct(m.acc)}</td></tr>`).join('');$('outcomeBars').innerHTML=[['A',o.a],['B',o.b]].map(([g,m])=>{const parts=[['TP',m.tp,'#269c82'],['FP',m.fp,'#df9b4c'],['FN',m.fn,'#c95673'],['TN',m.tn,'#5f57c5']];return `<div><b>Group ${g} · ${m.n} records</b><div style="display:flex;height:20px;overflow:hidden;border-radius:6px;margin-top:8px">${parts.map(([lab,v,c])=>`<div title="${lab}: ${v}" style="width:${m.n?100*v/m.n:0}%;background:${c}"></div>`).join('')}</div><div class="legend">${parts.map(([lab,v,c])=>`<span><i class="dot" style="background:${c}"></i>${lab} ${v}</span>`).join('')}</div></div>`}).join('');}
|
| 35 |
+
function renderRecords(){const q=$('search').value.trim().toLowerCase(),grp=$('filterGroup').value,st=$('filterStatus').value,rr=state.records.filter(r=>(!q||r.id.toLowerCase().includes(q))&&(grp==='all'||r.group===grp)&&(st==='all'||(st==='archived')===r.archived));$('recordCount').textContent=rr.length+' of '+state.records.length+' records';$('recordTable').innerHTML=rr.slice(0,500).map(r=>`<tr><td><b>${safe(r.id)}</b></td><td>Group ${r.group}</td><td>${r.score}%</td><td>${r.label?'Positive':'Negative'}</td><td>${r.score>=state.threshold?'Positive':'Negative'}</td><td><span class="status ${r.archived?'off':''}">${r.archived?'Archived':'Active'}</span></td><td><div style="display:flex;gap:6px"><button class="btn small" data-edit="${safe(r.id)}">Edit</button><button class="btn small" data-archive="${safe(r.id)}">${r.archived?'Restore':'Archive'}</button></div></td></tr>`).join('')||`<tr><td colspan="7" class="empty">No matching records</td></tr>`;}
|
| 36 |
+
function render(){document.body.classList.toggle('dark',state.dark);renderOverview();renderLab();renderGroups();renderRecords();}
|
| 37 |
+
function switchView(v){if(!['overview','lab','groups','records','method'].includes(v))return;view=v;document.querySelectorAll('.view').forEach(el=>el.classList.toggle('active',el.id===v));document.querySelectorAll('#nav button').forEach(b=>b.classList.toggle('active',b.dataset.view===v));window.scrollTo({top:0,behavior:'instant'});}
|
| 38 |
+
function openEditor(id){switchView('records');const r=state.records.find(r=>r.id===id);editKey=id||null;$('editTitle').textContent=r?'Edit synthetic record':'Add synthetic record';$('recId').value=r?.id||'';$('recId').disabled=!!r;$('recGroup').value=r?.group||'A';$('recScore').value=r?.score??50;$('recLabel').checked=!!r?.label;$('editor').classList.remove('hide');$('editor').scrollIntoView({behavior:'smooth',block:'nearest'});}
|
| 39 |
+
function download(name,type,text){const blob=new Blob([text],{type}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);}
|
| 40 |
+
function csvEscape(v){let s=String(v);if(/^[\s\uFEFF]*[=+\-@\t\r]/.test(s))s="'"+s;return '"'+s.replace(/"/g,'""')+'"';}
|
| 41 |
+
$('nav').addEventListener('click',e=>{const b=e.target.closest('[data-view]');if(b)switchView(b.dataset.view)});
|
| 42 |
+
$('themeBtn').addEventListener('click',()=>{state.dark=!state.dark;save();render();});
|
| 43 |
+
$('threshold').addEventListener('input',e=>{state.threshold=Number(e.target.value);state.scenario='custom';save();render();});
|
| 44 |
+
$('scenario').addEventListener('change',e=>{state.scenario=e.target.value;const t={balanced:50,conservative:70,inclusive:35}[state.scenario];if(t!==undefined)state.threshold=t;save();render();});
|
| 45 |
+
for(const [id,field] of [['fpCost','fpCost'],['fnCost','fnCost']]){$(id).addEventListener('change',e=>{const n=Number(e.target.value);if(!Number.isInteger(n)||n<1||n>100){toast('Enter an integer cost from 1 to 100.');renderLab();return;}state[field]=n;save();render();});}
|
| 46 |
+
for(const id of ['search','filterGroup','filterStatus'])$(id).addEventListener('input',renderRecords);
|
| 47 |
+
$('newBtn').addEventListener('click',()=>openEditor(null));$('cancelEdit').addEventListener('click',()=>$('editor').classList.add('hide'));
|
| 48 |
+
$('recordTable').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.edit){openEditor(b.dataset.edit);return;}if(b.dataset.archive){const r=state.records.find(x=>x.id===b.dataset.archive);if(!r)return;r.archived=!r.archived;save();render();toast(r.archived?'Record archived; can be restored.':'Record restored.');}});
|
| 49 |
+
$('editForm').addEventListener('submit',e=>{e.preventDefault();const id=$('recId').value.trim(),group=$('recGroup').value,score=Number($('recScore').value),label=$('recLabel').checked?1:0;if(!/^[A-Za-z0-9_-]{2,32}$/.test(id)||!VALID.includes(group)||!Number.isInteger(score)||score<0||score>100){toast('Invalid record values.');return;}if(editKey){const r=state.records.find(x=>x.id===editKey);if(!r)return;r.group=group;r.score=score;r.label=label;}else{if(state.records.length>=500){toast('Maximum 500 records reached.');return;}if(state.records.some(x=>x.id===id)){toast('Record ID already exists.');return;}state.records.push({id,group,score,label,archived:false});}$('editor').classList.add('hide');editKey=null;save();render();toast('Record saved.');});
|
| 50 |
+
$('csvBtn').addEventListener('click',()=>{const r=[['id','group','score','label','archived'],...state.records.map(x=>[x.id,x.group,x.score,x.label,x.archived])];download('fairnessscope112-records.csv','text/csv;charset=utf-8',r.map(row=>row.map(csvEscape).join(',')).join('\r\n'));toast('CSV export downloaded.');});
|
| 51 |
+
$('jsonBtn').addEventListener('click',()=>{download('fairnessscope112-backup.json','application/json',JSON.stringify(state,null,2));toast('JSON backup downloaded.');});
|
| 52 |
+
$('importBtn').addEventListener('click',()=>$('fileInput').click());
|
| 53 |
+
$('fileInput').addEventListener('change',async e=>{const f=e.target.files?.[0];e.target.value='';if(!f)return;if(f.size>250000){toast('Backup exceeds 250 KB.');return;}try{const raw=await f.text(),next=validate(JSON.parse(raw));if(!confirm(`Replace current dataset with ${next.records.length} records from this backup?`))return;state=next;save();render();toast('Backup successfully restored.');}catch(err){toast('Import rejected: '+String(err.message||err).slice(0,160));}});
|
| 54 |
+
$('resetBtn').addEventListener('click',()=>{if(!confirm('Reset all synthetic records and settings? Export a backup first if needed.'))return;state=DEFAULT();save();render();switchView('overview');toast('Synthetic dataset restored.');});
|
| 55 |
+
render();window.FairnessScopeQA={getState:()=>structuredClone(state),analyze,cm,validate,seedData};
|
| 56 |
+
})();
|
| 57 |
+
</script></body></html>
|