From 89b835d6814d20cc5773ed05c5d1e764d95c76af Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Fri, 7 Aug 2026 14:49:39 -0400 Subject: [PATCH] Mercury Genesys Token Calculator --- .gitignore | 2 + CLAUDE.md | 20 +- Makefile | 9 +- README.md | 8 +- .../notebooks/cx_discovery.ipynb | 5788 +++++++------ .../Genesys_Token_Calculator/README.md | 172 + .../Genesys_Token_Calculator/config.toml | 81 + .../Genesys_Token_Calculator/exports/.gitkeep | 0 .../genesyscalc/__init__.py | 142 + .../genesyscalc/appendix.py | 126 + .../genesyscalc/billing.py | 151 + .../genesyscalc/meters.py | 165 + .../genesyscalc/model.py | 270 + .../genesyscalc/ratecard.py | 358 + .../genesyscalc/sensitivity.py | 133 + .../genesyscalc/staging.py | 61 + .../genesyscalc/tts.py | 147 + .../genesyscalc/usage.py | 262 + .../notebooks/genesys_token_calculator.ipynb | 7300 +++++++++++++++++ .../Genesys_Token_Calculator/pyproject.toml | 36 + .../scripts/export_report.py | 140 + .../tests/conftest.py | 70 + .../tests/test_billing.py | 154 + .../tests/test_catalogue.py | 94 + .../tests/test_engagement.py | 115 + .../tests/test_model.py | 263 + .../tests/test_rate_card.py | 174 + .../tests/test_sensitivity.py | 110 + .../tests/test_staging.py | 61 + .../tests/test_tts.py | 114 + .../tests/test_usage.py | 206 + docs/Calculator_Pattern_V1-00.md | 264 + pyproject.toml | 9 + requirements.txt | 9 - tests/nbcheck.py | 3 +- 35 files changed, 14105 insertions(+), 2912 deletions(-) create mode 100644 calculators/Genesys_Token_Calculator/README.md create mode 100644 calculators/Genesys_Token_Calculator/config.toml create mode 100644 calculators/Genesys_Token_Calculator/exports/.gitkeep create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/__init__.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/appendix.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/billing.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/meters.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/model.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/ratecard.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/sensitivity.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/staging.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/tts.py create mode 100644 calculators/Genesys_Token_Calculator/genesyscalc/usage.py create mode 100644 calculators/Genesys_Token_Calculator/notebooks/genesys_token_calculator.ipynb create mode 100644 calculators/Genesys_Token_Calculator/pyproject.toml create mode 100644 calculators/Genesys_Token_Calculator/scripts/export_report.py create mode 100644 calculators/Genesys_Token_Calculator/tests/conftest.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_billing.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_catalogue.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_engagement.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_model.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_rate_card.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_sensitivity.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_staging.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_tts.py create mode 100644 calculators/Genesys_Token_Calculator/tests/test_usage.py create mode 100644 docs/Calculator_Pattern_V1-00.md delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index b2f8a21..63d5c42 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ studies/*/exports/* !studies/*/exports/.gitkeep assessments/*/exports/* !assessments/*/exports/.gitkeep +calculators/*/exports/* +!calculators/*/exports/.gitkeep # Client / engagement documents must NEVER be committed — masters stay # client-clean (see CLAUDE.md, Confidentiality). Text/image source material diff --git a/CLAUDE.md b/CLAUDE.md index 7cd95a4..587f740 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,12 @@ and [docs/Study_Pattern_V1-00.md](docs/Study_Pattern_V1-00.md). - **Assessment** (`assessments/Instrument_Name`, undated) — reusable workshop instrument; a living master. Reference implementation: `assessments/CX_Discovery_Workshop/`. +- **Calculator** (`calculators/Vendor_Subject_Calculator`, undated) — prices a + vendor's product from that vendor's **published rate card**: verbatim + rate-card anchor + client overlay. Undated because the anchor is immutable + only *until the vendor republishes* — the publication date lives in the + anchor, not the directory name. Reference implementation: + `calculators/Genesys_Token_Calculator/`. - **Engagement copy** (`YYYYMM_Client_Instrument`, stamped at copy-time) — a master copied OUT of this repo for a client; confidential; never merges back. - **`template/MercuryNotebook/`** — copy-me scaffold for new studies (py-engine model @@ -96,7 +102,10 @@ structural suite. Running a master's pytest from the wrong venv is the classic **Show first** — produce the diff/output, present it, wait for a human "go": content edits to `topic-bank`/`engagement-data` cells or anchor-adjacent wording; re-pinning a gate or test after a content/engine change (show the pin diff and the -KPI moves honestly); moving or renaming a master; `.gitignore` changes; `git commit`. +KPI moves honestly); **re-anchoring a Calculator to a newer vendor publication** +(show the rate diff, the source-date bump, and the cost moves — the protocol is in +the Calculator Pattern); moving or renaming a master; `.gitignore` changes; +`git commit`. **Forbidden without explicit go-ahead:** `git push`; editing `*_VERBATIM` anchors; committing any client document (SOW, quote, @@ -230,6 +239,12 @@ quietly leaving it. Live ones worth knowing: - **`template/MercuryNotebook/` encodes only the py-engine model** (and its `staging.py` lacks the mypy-strict `backstage_md` variant); rework is a recorded follow-up. Its `exports/*.{html,md}` are tracked — predates the exports rule. +- **`make lint` fails repo-wide — 143 pre-existing ruff errors** (94 in the CTM + study, 16 in AI Diagnostic, the rest spread across the TEI twins, the template + and `00_setup.ipynb`); mostly import sorting and E402 in notebook cells. Newer + masters are clean (`ruff check calculators/` passes), so lint per-directory + when you touch one; a repo-wide sweep is its own commit, not a rider on + feature work. - **`docs/brand.md` references a `brand_dark.md` that does not exist.** - **`core.bootstrap.init(study=…)` imports `studies..config`** — vestigial (no study ships a `config.py`; `studies/__init__.py` exists to serve it). Don't @@ -258,6 +273,9 @@ this file summarises:** notebook-first content, engagement-data cell, copy-out checklist - [docs/Study_Pattern_V1-00.md](docs/Study_Pattern_V1-00.md) — studies: verbatim anchor + overlay, reproduction gate, published-PDF exception +- [docs/Calculator_Pattern_V1-00.md](docs/Calculator_Pattern_V1-00.md) — + calculators: the rate-card anchor (and why it lives in `.py`), the + re-anchoring protocol, the proportionality rule (price it; don't build a case) Everything else: diff --git a/Makefile b/Makefile index ebfdf81..d55eb21 100644 --- a/Makefile +++ b/Makefile @@ -6,12 +6,15 @@ PIP := $(VENV)/bin/pip .PHONY: setup lab test check-notebooks lint format clean -## One-time: create venv, install deps + palladium (editable) +## One-time: create venv, install deps + palladium (editable). +## `pyproject.toml` carries the whole runtime toolchain as core deps and +## pytest/ruff in the `dev` extra — `pip install -e ".[dev]"` is enough to +## run, test, and lint. (There is deliberately no requirements.txt: a +## second dependency list is how `make setup` silently rots.) setup: python3 -m venv $(VENV) $(PIP) install --upgrade pip - $(PIP) install -r requirements.txt - $(PIP) install -e . + $(PIP) install -e ".[dev]" @echo "" @echo "✅ Done. Next: make lab → open 00_setup.ipynb" diff --git a/README.md b/README.md index 6769648..8625c2e 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,14 @@ Every master obeys one **three-layer contract** (the always-on rules live in |---|---|---|---| | **Study** | Reproduction of a dated base document (Forrester TEI or similar), personalized as an overlay | `YYYYMM_TEI_Vendor_Product` / `YYYYMM_Client_Engagement` (dated) | `studies/` | | **Assessment** | Reusable workshop instrument (discovery workshop, diagnostic) | `Instrument_Name` (undated, living) | `assessments/` | +| **Calculator** | Prices a vendor's product from that vendor's published rate card — verbatim anchor + client overlay | `Vendor_Subject_Calculator` (undated, living) | `calculators/` | | **Engagement copy** | A master copied out for a client engagement — acquires client data, becomes **confidential** | `YYYYMM_Client_Instrument`, stamped at copy-time | **outside this repo** | Masters in this repo stay **client-clean**: placeholder engagement data, published or synthetic numbers, nothing a client said. Patterns: [Assessment](docs/Assessment_Pattern_V1-00.md) · [Study](docs/Study_Pattern_V1-00.md) · +[Calculator](docs/Calculator_Pattern_V1-00.md) · [shared Mercury mechanics](docs/Mercury_Notebook_Pattern_V1-00.md). ## Repository layout @@ -39,6 +41,8 @@ palladium/ ├── assessments/ │ ├── CX_Discovery_Workshop/ # ★ reference implementation (notebook-first model) │ └── CX_AI_Diagnostic/ # capability diagnostic (pre-redesign: generated notebook) +├── calculators/ +│ └── Genesys_Token_Calculator/ # ★ Genesys Cloud AI token cost — published rate card, 2026-07-12 ├── studies/ │ ├── 202512_TEI_Genesys_CX_Cloud/ # Forrester TEI reproduction — NPV $10.8M · ROI 266% │ ├── 202602_TEI_Amazon_Connect/ # Forrester TEI reproduction — NPV $78.7M · ROI 342% @@ -109,7 +113,9 @@ For an **Assessment**, start from the reference implementation and its pattern d `assessments/CX_Discovery_Workshop/` + [Assessment Pattern](docs/Assessment_Pattern_V1-00.md). For a **Study**, copy `template/MercuryNotebook/` and follow the [Study Pattern](docs/Study_Pattern_V1-00.md) (note: the template still encodes the -py-engine model; its notebook-first rework is a recorded follow-up). Either way: +py-engine model; its notebook-first rework is a recorded follow-up). For a +**Calculator**, start from `calculators/Genesys_Token_Calculator/` + +[Calculator Pattern](docs/Calculator_Pattern_V1-00.md). Any of them: underscores in names (never dashes — directories are Python packages), and register the new notebook in [tests/nbcheck.py](tests/nbcheck.py) — the completeness test fails until you classify it. diff --git a/assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb b/assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb index ed44dc6..3c86b65 100644 --- a/assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb +++ b/assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb @@ -16,23 +16,17 @@ "execution_count": 1, "id": "0e66bf69", "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:39:27.437335Z", + "iopub.status.busy": "2026-08-02T20:39:27.437043Z", + "iopub.status.idle": "2026-08-02T20:39:27.568740Z", + "shell.execute_reply": "2026-08-02T20:39:27.567019Z" + }, "tags": [ "presentation" ] }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'mercury'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 10\u001b[39m\n\u001b[32m 6\u001b[39m sys.path.insert(\u001b[32m0\u001b[39m, str(_ROOT))\n\u001b[32m 7\u001b[39m \n\u001b[32m 8\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m html \u001b[38;5;28;01mas\u001b[39;00m _html\n\u001b[32m 9\u001b[39m \n\u001b[32m---> \u001b[39m\u001b[32m10\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m mercury \u001b[38;5;28;01mas\u001b[39;00m mr\n\u001b[32m 11\u001b[39m \n\u001b[32m 12\u001b[39m \u001b[38;5;66;03m# Schema + session logic come from the library; the CONTENT (the topic\u001b[39;00m\n\u001b[32m 13\u001b[39m \u001b[38;5;66;03m# bank) lives in the tagged topic-bank cell below — edit it there.\u001b[39;00m\n", - "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'mercury'" - ] - } - ], + "outputs": [], "source": [ "# ── Setup ──────────────────────────────────────────────────────────\n", "import sys, pathlib\n", @@ -71,10 +65,10 @@ "id": "13ec330d", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.349972Z", - "iopub.status.busy": "2026-07-31T16:30:46.349909Z", - "iopub.status.idle": "2026-07-31T16:30:46.352247Z", - "shell.execute_reply": "2026-07-31T16:30:46.352009Z" + "iopub.execute_input": "2026-08-02T20:39:27.572765Z", + "iopub.status.busy": "2026-08-02T20:39:27.572225Z", + "iopub.status.idle": "2026-08-02T20:39:27.589115Z", + "shell.execute_reply": "2026-08-02T20:39:27.584209Z" }, "tags": [ "presentation" @@ -139,10 +133,10 @@ "id": "4798dd67", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.353000Z", - "iopub.status.busy": "2026-07-31T16:30:46.352946Z", - "iopub.status.idle": "2026-07-31T16:30:46.358813Z", - "shell.execute_reply": "2026-07-31T16:30:46.358476Z" + "iopub.execute_input": "2026-08-02T20:39:27.592383Z", + "iopub.status.busy": "2026-08-02T20:39:27.592086Z", + "iopub.status.idle": "2026-08-02T20:39:27.607801Z", + "shell.execute_reply": "2026-08-02T20:39:27.606861Z" }, "tags": [ "topic-bank" @@ -546,10 +540,10 @@ "id": "6140ecae", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.359510Z", - "iopub.status.busy": "2026-07-31T16:30:46.359448Z", - "iopub.status.idle": "2026-07-31T16:30:46.360874Z", - "shell.execute_reply": "2026-07-31T16:30:46.360657Z" + "iopub.execute_input": "2026-08-02T20:39:27.610164Z", + "iopub.status.busy": "2026-08-02T20:39:27.609883Z", + "iopub.status.idle": "2026-08-02T20:39:27.614481Z", + "shell.execute_reply": "2026-08-02T20:39:27.613565Z" }, "tags": [ "engagement-data" @@ -578,10 +572,10 @@ "id": "e84c36b2", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.361416Z", - "iopub.status.busy": "2026-07-31T16:30:46.361358Z", - "iopub.status.idle": "2026-07-31T16:30:46.389634Z", - "shell.execute_reply": "2026-07-31T16:30:46.389421Z" + "iopub.execute_input": "2026-08-02T20:39:27.616480Z", + "iopub.status.busy": "2026-08-02T20:39:27.616280Z", + "iopub.status.idle": "2026-08-02T20:39:27.710552Z", + "shell.execute_reply": "2026-08-02T20:39:27.709836Z" }, "tags": [ "presentation" @@ -591,17 +585,17 @@ { "data": { "application/mercury+json": { - "model_id": "089de65ce2dd47789e7ca894f1b6def0", + "model_id": "5315b040652049c09ec602b63536aebb", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "089de65ce2dd47789e7ca894f1b6def0", + "model_id": "5315b040652049c09ec602b63536aebb", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -610,17 +604,17 @@ { "data": { "application/mercury+json": { - "model_id": "8e45d2735d56446c93405512d56406e8", + "model_id": "24bb8dec2f8f4c78bf88b3f52373297f", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "8e45d2735d56446c93405512d56406e8", + "model_id": "24bb8dec2f8f4c78bf88b3f52373297f", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -629,17 +623,17 @@ { "data": { "application/mercury+json": { - "model_id": "73617de24aaf41d0bc328a0d7a1f7886", + "model_id": "06614b7e2c61448988fda27cc797c8fb", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "73617de24aaf41d0bc328a0d7a1f7886", + "model_id": "06614b7e2c61448988fda27cc797c8fb", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -648,17 +642,17 @@ { "data": { "application/mercury+json": { - "model_id": "ca5b6dc1228045c6a8226f2c6fb62751", + "model_id": "751b7349c889461cb1966e1046bda562", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "ca5b6dc1228045c6a8226f2c6fb62751", + "model_id": "751b7349c889461cb1966e1046bda562", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -667,17 +661,17 @@ { "data": { "application/mercury+json": { - "model_id": "0f170e571f2e41f8bf1f257cf31039af", + "model_id": "13e7c5f467664ac194d261fadba4959a", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "0f170e571f2e41f8bf1f257cf31039af", + "model_id": "13e7c5f467664ac194d261fadba4959a", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -686,17 +680,17 @@ { "data": { "application/mercury+json": { - "model_id": "23b5ed7e898a4c35abdb51fad036b7e2", + "model_id": "79e0a92fa043419a8e7647b280648806", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "23b5ed7e898a4c35abdb51fad036b7e2", + "model_id": "79e0a92fa043419a8e7647b280648806", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -705,17 +699,17 @@ { "data": { "application/mercury+json": { - "model_id": "81bdb505a7ed45a0ae595d2d27c666a4", + "model_id": "6a9ae131cdb74b89bbb08ab7e641e9a5", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "81bdb505a7ed45a0ae595d2d27c666a4", + "model_id": "6a9ae131cdb74b89bbb08ab7e641e9a5", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -724,17 +718,17 @@ { "data": { "application/mercury+json": { - "model_id": "bc99cd23ba4848a4b81af0d99bf57828", + "model_id": "270238fd5160455eb05c27504d2b5837", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "bc99cd23ba4848a4b81af0d99bf57828", + "model_id": "270238fd5160455eb05c27504d2b5837", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -743,17 +737,17 @@ { "data": { "application/mercury+json": { - "model_id": "4fad84b423d7460892d020e5311aa2e3", + "model_id": "761eb4eb9fc44ed9aac2abe03647a963", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "4fad84b423d7460892d020e5311aa2e3", + "model_id": "761eb4eb9fc44ed9aac2abe03647a963", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -762,17 +756,17 @@ { "data": { "application/mercury+json": { - "model_id": "d249edd2c0694c5aa6d41391b4843d32", + "model_id": "fbe8fdd901784c6f9c048000a06f97b0", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "d249edd2c0694c5aa6d41391b4843d32", + "model_id": "fbe8fdd901784c6f9c048000a06f97b0", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -781,17 +775,17 @@ { "data": { "application/mercury+json": { - "model_id": "0d2239669d004a0c85af805d73b5e9cd", + "model_id": "dbeb682621e44f94a7d9a1f9c62702a0", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "0d2239669d004a0c85af805d73b5e9cd", + "model_id": "dbeb682621e44f94a7d9a1f9c62702a0", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -800,17 +794,17 @@ { "data": { "application/mercury+json": { - "model_id": "baaac2ab4371417aa66465413053f6e0", + "model_id": "d1fa2807b324467db7b3e394440fab4a", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "baaac2ab4371417aa66465413053f6e0", + "model_id": "d1fa2807b324467db7b3e394440fab4a", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -819,17 +813,17 @@ { "data": { "application/mercury+json": { - "model_id": "2e8ac309308c4e5c9741ae4b38e4994e", + "model_id": "1f9d02f3b8bb46d687a1e679f3ff2fc8", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "2e8ac309308c4e5c9741ae4b38e4994e", + "model_id": "1f9d02f3b8bb46d687a1e679f3ff2fc8", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -838,17 +832,17 @@ { "data": { "application/mercury+json": { - "model_id": "3bb4c1106f7d4a3ca51a53268b3c6faa", + "model_id": "ecf6ea40937e4cc3ac0f7369be197134", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "3bb4c1106f7d4a3ca51a53268b3c6faa", + "model_id": "ecf6ea40937e4cc3ac0f7369be197134", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -857,17 +851,17 @@ { "data": { "application/mercury+json": { - "model_id": "d5fc113c037a45fcb33cc2ee210547ce", + "model_id": "57a5e081f6024b97af20e9f2884679ff", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "d5fc113c037a45fcb33cc2ee210547ce", + "model_id": "57a5e081f6024b97af20e9f2884679ff", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -876,17 +870,17 @@ { "data": { "application/mercury+json": { - "model_id": "adcce9bc4b7f45c3bccef29bbe7f4729", + "model_id": "1106845f53b9482785e0f05fdb4963d0", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "adcce9bc4b7f45c3bccef29bbe7f4729", + "model_id": "1106845f53b9482785e0f05fdb4963d0", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -895,17 +889,17 @@ { "data": { "application/mercury+json": { - "model_id": "1b94e1065db748daadd2cc9974094a72", + "model_id": "f02bf16913fa4c44ac665e96bd9d6f47", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "1b94e1065db748daadd2cc9974094a72", + "model_id": "f02bf16913fa4c44ac665e96bd9d6f47", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -914,17 +908,17 @@ { "data": { "application/mercury+json": { - "model_id": "c0ee439c71714f0ca6a1876a68f3f376", + "model_id": "e1f98c09c21140b59bc95ff244dfefec", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "c0ee439c71714f0ca6a1876a68f3f376", + "model_id": "e1f98c09c21140b59bc95ff244dfefec", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -933,17 +927,17 @@ { "data": { "application/mercury+json": { - "model_id": "a3acd4a93864411cae3f58d185cabc59", + "model_id": "ac45ef5a635c45948e7114eeb7d2f1db", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "a3acd4a93864411cae3f58d185cabc59", + "model_id": "ac45ef5a635c45948e7114eeb7d2f1db", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -952,17 +946,17 @@ { "data": { "application/mercury+json": { - "model_id": "a0a0a90b79f44db08819b52ffe94d365", + "model_id": "329a57df198e466ba69e7367a1c37d85", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "a0a0a90b79f44db08819b52ffe94d365", + "model_id": "329a57df198e466ba69e7367a1c37d85", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -971,17 +965,17 @@ { "data": { "application/mercury+json": { - "model_id": "d98420c917944187b16188da9f1d9632", + "model_id": "d7656b3ac6ea486d9bbd7bbc100445ee", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "d98420c917944187b16188da9f1d9632", + "model_id": "d7656b3ac6ea486d9bbd7bbc100445ee", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -990,17 +984,17 @@ { "data": { "application/mercury+json": { - "model_id": "6974063a721d4119b6a7e0a268b38d7c", + "model_id": "e4668feb3c6945269734a21c44a2f88f", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "6974063a721d4119b6a7e0a268b38d7c", + "model_id": "e4668feb3c6945269734a21c44a2f88f", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1009,17 +1003,17 @@ { "data": { "application/mercury+json": { - "model_id": "02388514a78844a48b60f11d6c9f8072", + "model_id": "2ca83b8bf2f9462c89aa095244b3cdf8", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "02388514a78844a48b60f11d6c9f8072", + "model_id": "2ca83b8bf2f9462c89aa095244b3cdf8", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1028,17 +1022,17 @@ { "data": { "application/mercury+json": { - "model_id": "0b41417ad8dc4d47b8badebc3b0bc7b5", + "model_id": "8af987ef301441f4863ba06560562f8e", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "0b41417ad8dc4d47b8badebc3b0bc7b5", + "model_id": "8af987ef301441f4863ba06560562f8e", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1047,17 +1041,17 @@ { "data": { "application/mercury+json": { - "model_id": "86f9c32fe7f644b19a4c8a5b86314a02", + "model_id": "a0f8e23133534198b9341e3110019919", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "86f9c32fe7f644b19a4c8a5b86314a02", + "model_id": "a0f8e23133534198b9341e3110019919", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1066,17 +1060,17 @@ { "data": { "application/mercury+json": { - "model_id": "0fcc86148f6b49f49a82a1a2d2277811", + "model_id": "4cf69fa2ee02431b9487d1ac33d9da0d", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "0fcc86148f6b49f49a82a1a2d2277811", + "model_id": "4cf69fa2ee02431b9487d1ac33d9da0d", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1085,17 +1079,17 @@ { "data": { "application/mercury+json": { - "model_id": "6cb599e372a54fa28251fc7ad08f9354", + "model_id": "0930f610263a4b528080bf6f5ffacbd4", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "6cb599e372a54fa28251fc7ad08f9354", + "model_id": "0930f610263a4b528080bf6f5ffacbd4", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1104,17 +1098,17 @@ { "data": { "application/mercury+json": { - "model_id": "f762178850dd4f44b873f9c9f9e3c299", + "model_id": "144e9e43924d41d98f4cc4ea459a4c15", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "f762178850dd4f44b873f9c9f9e3c299", + "model_id": "144e9e43924d41d98f4cc4ea459a4c15", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1123,17 +1117,17 @@ { "data": { "application/mercury+json": { - "model_id": "6593a45ae81348d48510d1c351947d13", + "model_id": "c41f68b24eae437382accffe853e6741", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "6593a45ae81348d48510d1c351947d13", + "model_id": "c41f68b24eae437382accffe853e6741", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1142,17 +1136,17 @@ { "data": { "application/mercury+json": { - "model_id": "d7133124428841aeb00d4d4cfb92c2da", + "model_id": "95a4df8983ad4a87bedce4d05ed24866", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "d7133124428841aeb00d4d4cfb92c2da", + "model_id": "95a4df8983ad4a87bedce4d05ed24866", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1161,17 +1155,17 @@ { "data": { "application/mercury+json": { - "model_id": "ec1aaa717f20444d91a9b187238ba568", + "model_id": "e6517b09bfe94951a00d798aa5b76733", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "ec1aaa717f20444d91a9b187238ba568", + "model_id": "e6517b09bfe94951a00d798aa5b76733", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1180,17 +1174,17 @@ { "data": { "application/mercury+json": { - "model_id": "8c01f33274b44a20910ec99698895968", + "model_id": "40816a8b2fbe4b58829519ef213d29f0", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "8c01f33274b44a20910ec99698895968", + "model_id": "40816a8b2fbe4b58829519ef213d29f0", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1199,17 +1193,17 @@ { "data": { "application/mercury+json": { - "model_id": "e97c7bc947d648fc9274a52cdd0909c2", + "model_id": "7a26751f917f4d569d23a5baf63e42fd", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "e97c7bc947d648fc9274a52cdd0909c2", + "model_id": "7a26751f917f4d569d23a5baf63e42fd", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1218,17 +1212,17 @@ { "data": { "application/mercury+json": { - "model_id": "4621418f84474cbdacc48656e684d420", + "model_id": "6cb47927b18246018129730648b65681", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "4621418f84474cbdacc48656e684d420", + "model_id": "6cb47927b18246018129730648b65681", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1237,17 +1231,17 @@ { "data": { "application/mercury+json": { - "model_id": "fdd5b7191f314be683bf53cb82d4270b", + "model_id": "086a43715a9b4b6fbe3edc6d52ad0d54", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "fdd5b7191f314be683bf53cb82d4270b", + "model_id": "086a43715a9b4b6fbe3edc6d52ad0d54", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1256,17 +1250,17 @@ { "data": { "application/mercury+json": { - "model_id": "6be9d0f5638d4d028ab8e6918175faca", + "model_id": "7ec27ebdd0534bcb93b836a6969d7ee4", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "6be9d0f5638d4d028ab8e6918175faca", + "model_id": "7ec27ebdd0534bcb93b836a6969d7ee4", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1275,17 +1269,17 @@ { "data": { "application/mercury+json": { - "model_id": "31cd908ef5d044e9ad7d0d9a811f2870", + "model_id": "2ca1198978804af08c75aca447a11f72", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "31cd908ef5d044e9ad7d0d9a811f2870", + "model_id": "2ca1198978804af08c75aca447a11f72", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1294,17 +1288,17 @@ { "data": { "application/mercury+json": { - "model_id": "842b63f2ae994b12bd201b626a22a264", + "model_id": "9b6bd28767414d5dae7b74f8764ae3b0", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "842b63f2ae994b12bd201b626a22a264", + "model_id": "9b6bd28767414d5dae7b74f8764ae3b0", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1313,17 +1307,17 @@ { "data": { "application/mercury+json": { - "model_id": "2de92ccdffb84a248b4da2cd428dbd26", + "model_id": "6321bc1fde1b47cca37c80004b61b8f4", "position": "sidebar", "widget": "SelectWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "2de92ccdffb84a248b4da2cd428dbd26", + "model_id": "6321bc1fde1b47cca37c80004b61b8f4", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1332,17 +1326,17 @@ { "data": { "application/mercury+json": { - "model_id": "94a9db46ddc241c885f0810c3550517a", + "model_id": "53de763e02ae4440b57202067351a984", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "94a9db46ddc241c885f0810c3550517a", + "model_id": "53de763e02ae4440b57202067351a984", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1351,17 +1345,17 @@ { "data": { "application/mercury+json": { - "model_id": "d0f38990bdc944609aef3a89d89b68c5", + "model_id": "2d39ff2f4b0e4fa98c845d0c01176ac4", "position": "sidebar", "widget": "CheckboxWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "d0f38990bdc944609aef3a89d89b68c5", + "model_id": "2d39ff2f4b0e4fa98c845d0c01176ac4", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1370,17 +1364,17 @@ { "data": { "application/mercury+json": { - "model_id": "c0b0ca61b36a403f99e52d5ced585d24", + "model_id": "6144c1a5fb2d4353912e41aefe5a4d76", "position": "sidebar", "widget": "TextInputWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "c0b0ca61b36a403f99e52d5ced585d24", + "model_id": "6144c1a5fb2d4353912e41aefe5a4d76", "version_major": 2, "version_minor": 1 }, "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -1420,10 +1414,10 @@ "id": "24e06a3e", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.390196Z", - "iopub.status.busy": "2026-07-31T16:30:46.390138Z", - "iopub.status.idle": "2026-07-31T16:30:46.392437Z", - "shell.execute_reply": "2026-07-31T16:30:46.392185Z" + "iopub.execute_input": "2026-08-02T20:39:27.713449Z", + "iopub.status.busy": "2026-08-02T20:39:27.713183Z", + "iopub.status.idle": "2026-08-02T20:39:27.719514Z", + "shell.execute_reply": "2026-08-02T20:39:27.718792Z" } }, "outputs": [ @@ -1462,10 +1456,10 @@ "id": "c6b92da6", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.392998Z", - "iopub.status.busy": "2026-07-31T16:30:46.392944Z", - "iopub.status.idle": "2026-07-31T16:30:46.399069Z", - "shell.execute_reply": "2026-07-31T16:30:46.398859Z" + "iopub.execute_input": "2026-08-02T20:39:27.721525Z", + "iopub.status.busy": "2026-08-02T20:39:27.721307Z", + "iopub.status.idle": "2026-08-02T20:39:27.739296Z", + "shell.execute_reply": "2026-08-02T20:39:27.738584Z" }, "tags": [ "presentation" @@ -1475,12 +1469,12 @@ { "data": { "application/mercury+json": { - "model_id": "8982e788092a47f58791a19e10c54813", + "model_id": "05c26428b44a4d189eb066765885dfb0", "position": "inline", "widget": "MarkdownWidget" }, "application/vnd.jupyter.widget-view+json": { - "model_id": "8982e788092a47f58791a19e10c54813", + "model_id": "05c26428b44a4d189eb066765885dfb0", "version_major": 2, "version_minor": 0 }, @@ -1575,10 +1569,10 @@ "id": "d70653b9", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.399699Z", - "iopub.status.busy": "2026-07-31T16:30:46.399615Z", - "iopub.status.idle": "2026-07-31T16:30:46.401477Z", - "shell.execute_reply": "2026-07-31T16:30:46.401242Z" + "iopub.execute_input": "2026-08-02T20:39:27.741862Z", + "iopub.status.busy": "2026-08-02T20:39:27.741641Z", + "iopub.status.idle": "2026-08-02T20:39:27.746302Z", + "shell.execute_reply": "2026-08-02T20:39:27.745577Z" } }, "outputs": [ @@ -1791,10 +1785,10 @@ "id": "cc5c3a6e", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.402093Z", - "iopub.status.busy": "2026-07-31T16:30:46.401987Z", - "iopub.status.idle": "2026-07-31T16:30:46.405079Z", - "shell.execute_reply": "2026-07-31T16:30:46.404848Z" + "iopub.execute_input": "2026-08-02T20:39:27.748252Z", + "iopub.status.busy": "2026-08-02T20:39:27.748030Z", + "iopub.status.idle": "2026-08-02T20:39:27.757236Z", + "shell.execute_reply": "2026-08-02T20:39:27.756457Z" }, "tags": [ "gate" @@ -1868,10 +1862,10 @@ "id": "ea73c809", "metadata": { "execution": { - "iopub.execute_input": "2026-07-31T16:30:46.405613Z", - "iopub.status.busy": "2026-07-31T16:30:46.405557Z", - "iopub.status.idle": "2026-07-31T16:30:46.407945Z", - "shell.execute_reply": "2026-07-31T16:30:46.407745Z" + "iopub.execute_input": "2026-08-02T20:39:27.759474Z", + "iopub.status.busy": "2026-08-02T20:39:27.759171Z", + "iopub.status.idle": "2026-08-02T20:39:27.766251Z", + "shell.execute_reply": "2026-08-02T20:39:27.765530Z" }, "tags": [ "data-appendix" @@ -2078,1638 +2072,88 @@ "widgets": { "application/vnd.jupyter.widget-state+json": { "state": { - "02388514a78844a48b60f11d6c9f8072": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", + "042c3b8a3f214120b35767f1417d66f8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "05c26428b44a4d189eb066765885dfb0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/controls", + "_view_module_version": "2.0.0", + "_view_name": "HTMLView", "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Agent & Supervisor Environment: Locations & endpoints", - "last_changed_at": "", - "layout": "IPY_MODEL_441fc7a11c674511ac8cb2cfd1965feb", + "description": "", + "description_allow_html": false, + "layout": "IPY_MODEL_690f3f6e51b84c5aa5dc985cd1c604f5", "layout_path": null, - "n_toggles": 0, - "position": "sidebar", + "placeholder": "​", + "position": "inline", "render_slot_id": null, "source_cell_id": null, + "style": "IPY_MODEL_580c9cfbb39b4026b705ae12f7dcba5c", "tabbable": null, "tooltip": null, - "url_key": "", - "value": false + "value": "
CX Discovery — topics
8 topics · ~110 minutes
0 of 8 topics complete
Background & Organizationdiscussing now →Not started
Lines of business, channels, org structure, decision-making
  • Lines of business & scope
  • Channels supported
  • Org & reporting structure
  • Decision-making & priorities
  • Problems & causes today
CX StrategyNot started
How CX value is defined, measured, and organized to deliver
ChannelsNot started
Inbound & outbound volumes, metrics, contact reasons
Agent & Supervisor EnvironmentNot started
Locations, endpoints, desktop, applications, knowledge
Routing & AutomationNot started
Self-service, IVR, speech, virtual agents, agent assist, RPA
Workforce EngagementNot started
Call recording / QM, WFM, agent self-service, integrations
TrainingNot started
Onboarding, format, assessment, eLearning, QM integration
Reporting & InsightsNot started
Real-time & historical reporting, analytics, BI, CRM
" } }, - "05b7184872494e42abe444d32fcbea5c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "089de65ce2dd47789e7ca894f1b6def0": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.select.SelectWidget", - "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "choices": [ - "Not started", - "In progress", - "Complete", - "Skipped" - ], - "disabled": false, - "hidden": false, - "label": "Background & Organization — status", - "layout": "IPY_MODEL_9e0e72ba8ed5453a8a08cc8c81f16205", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "Not started" - } - }, - "0b41417ad8dc4d47b8badebc3b0bc7b5": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Agent & Supervisor Environment: Desktop & applications", - "last_changed_at": "", - "layout": "IPY_MODEL_11663793067643cb97e466e4676bde06", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "0bece89661cd4dceb3d3ea5dc432195d": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "0d2239669d004a0c85af805d73b5e9cd": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "CX Strategy: Insight & Voice of Customer", - "last_changed_at": "", - "layout": "IPY_MODEL_d790e920688441469dcfc7ab37244438", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "0e12f98be4b54932879481f98044977a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "0f170e571f2e41f8bf1f257cf31039af": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Background & Organization: Decision-making & priorities", - "last_changed_at": "", - "layout": "IPY_MODEL_e21c6da3e67848e7860b2938873fba8b", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "0fcc86148f6b49f49a82a1a2d2277811": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.select.SelectWidget", - "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "choices": [ - "Not started", - "In progress", - "Complete", - "Skipped" - ], - "disabled": false, - "hidden": false, - "label": "Routing & Automation — status", - "layout": "IPY_MODEL_8da9629bd3924c70b9ad955a2c82ffe6", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "Not started" - } - }, - "108dd0d7f9bc4b698f1354b5a9aa9b5a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "11663793067643cb97e466e4676bde06": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "13c050864b524a8db731230057903a46": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "1b94e1065db748daadd2cc9974094a72": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Channels: Voice & video metrics", - "last_changed_at": "", - "layout": "IPY_MODEL_cb15afe0f83a447789bd669c0d369b79", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "23b5ed7e898a4c35abdb51fad036b7e2": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Background & Organization: Problems & causes today", - "last_changed_at": "", - "layout": "IPY_MODEL_32161857a29843cd9c4a291e266c12a4", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "24a8bc961c6f4ee59efbbe4f32c76607": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2a3235f448db40ca900d312d53278a6c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2dd26079d8d540c898fd38ce31e96c3c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "2de92ccdffb84a248b4da2cd428dbd26": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.select.SelectWidget", - "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "choices": [ - "Not started", - "In progress", - "Complete", - "Skipped" - ], - "disabled": false, - "hidden": false, - "label": "Reporting & Insights — status", - "layout": "IPY_MODEL_0bece89661cd4dceb3d3ea5dc432195d", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "Not started" - } - }, - "2e8ac309308c4e5c9741ae4b38e4994e": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "CX Strategy: Employee engagement", - "last_changed_at": "", - "layout": "IPY_MODEL_b479458f18a8472d93e069a112241354", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "317687cf884547b5a8a719e51f134c4b": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "31cd908ef5d044e9ad7d0d9a811f2870": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Training: Onboarding & enablement", - "last_changed_at": "", - "layout": "IPY_MODEL_f55615cdb8b6441fabee7044abd191bf", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "32161857a29843cd9c4a291e266c12a4": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "3bb4c1106f7d4a3ca51a53268b3c6faa": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.text.TextInputWidget", - "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "CX Strategy — notes", - "layout": "IPY_MODEL_dbde403229964f3f8443b9b936f404d8", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "" - } - }, - "3cd231f40b12490bae31f1f9c1b240a9": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "441fc7a11c674511ac8cb2cfd1965feb": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "4621418f84474cbdacc48656e684d420": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Workforce Engagement: Agent self-service & integrations", - "last_changed_at": "", - "layout": "IPY_MODEL_0e12f98be4b54932879481f98044977a", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "4fad84b423d7460892d020e5311aa2e3": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "CX Strategy: Value & strategic position", - "last_changed_at": "", - "layout": "IPY_MODEL_b237b1282e1d499090a4accd367267e8", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "527e8415076442039e93265c1cebec24": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "53ab38af97e84de4a6de038be1c8aeb1": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "5e52cee7caf7437297a6899d6ff3bb28": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "64b357cfe7e647da989f8b4d7c05f1c4": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "6593a45ae81348d48510d1c351947d13": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Routing & Automation: Digital assistant apps", - "last_changed_at": "", - "layout": "IPY_MODEL_cb93e54680da4b209c481f2b1798cdd1", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "6974063a721d4119b6a7e0a268b38d7c": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.select.SelectWidget", - "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "choices": [ - "Not started", - "In progress", - "Complete", - "Skipped" - ], - "disabled": false, - "hidden": false, - "label": "Agent & Supervisor Environment — status", - "layout": "IPY_MODEL_8d29c0b965d64aff91b81922f254e9fc", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "Not started" - } - }, - "6be9d0f5638d4d028ab8e6918175faca": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.select.SelectWidget", - "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "choices": [ - "Not started", - "In progress", - "Complete", - "Skipped" - ], - "disabled": false, - "hidden": false, - "label": "Training — status", - "layout": "IPY_MODEL_05b7184872494e42abe444d32fcbea5c", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "Not started" - } - }, - "6c32fa0d17b94bf8a63b7bbc8280aa9a": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "6cb599e372a54fa28251fc7ad08f9354": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Routing & Automation: Self-service & IVR", - "last_changed_at": "", - "layout": "IPY_MODEL_728a622f7f2b4061bd0692b19c0857c7", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "728a622f7f2b4061bd0692b19c0857c7": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "73617de24aaf41d0bc328a0d7a1f7886": { + "06614b7e2c61448988fda27cc797c8fb": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -3731,7 +2175,7 @@ "hidden": false, "label": "Background & Organization: Channels supported", "last_changed_at": "", - "layout": "IPY_MODEL_c3a30ebcbe544fb0b5220d244d680702", + "layout": "IPY_MODEL_3efab565b2a840549dd01aebe6b270c1", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -3743,60 +2187,7 @@ "value": false } }, - "7d8c622ba40c4fdebe93a755865644c8": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "81bdb505a7ed45a0ae595d2d27c666a4": { + "086a43715a9b4b6fbe3edc6d52ad0d54": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -3815,8 +2206,8 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Background & Organization — notes", - "layout": "IPY_MODEL_108dd0d7f9bc4b698f1354b5a9aa9b5a", + "label": "Workforce Engagement — notes", + "layout": "IPY_MODEL_6399b4afa4e346bc91bb2e871d8d7ca6", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -3827,97 +2218,7 @@ "value": "" } }, - "842b63f2ae994b12bd201b626a22a264": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.text.TextInputWidget", - "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Training — notes", - "layout": "IPY_MODEL_2dd26079d8d540c898fd38ce31e96c3c", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "" - } - }, - "86f9c32fe7f644b19a4c8a5b86314a02": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.text.TextInputWidget", - "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Agent & Supervisor Environment — notes", - "layout": "IPY_MODEL_fffeb5b7a0a04c89a01346ba7d8741db", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "" - } - }, - "8982e788092a47f58791a19e10c54813": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "model_name": "HTMLModel", - "state": { - "_dom_classes": [], - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "2.0.0", - "_model_name": "HTMLModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/controls", - "_view_module_version": "2.0.0", - "_view_name": "HTMLView", - "cell_id": "", - "description": "", - "description_allow_html": false, - "layout": "IPY_MODEL_7d8c622ba40c4fdebe93a755865644c8", - "layout_path": null, - "placeholder": "​", - "position": "inline", - "render_slot_id": null, - "source_cell_id": null, - "style": "IPY_MODEL_fc6b90dcba87418981b0601fbb9147e8", - "tabbable": null, - "tooltip": null, - "value": "
CX Discovery — topics
8 topics · ~110 minutes
0 of 8 topics complete
Background & Organizationdiscussing now →Not started
Lines of business, channels, org structure, decision-making
  • Lines of business & scope
  • Channels supported
  • Org & reporting structure
  • Decision-making & priorities
  • Problems & causes today
CX StrategyNot started
How CX value is defined, measured, and organized to deliver
ChannelsNot started
Inbound & outbound volumes, metrics, contact reasons
Agent & Supervisor EnvironmentNot started
Locations, endpoints, desktop, applications, knowledge
Routing & AutomationNot started
Self-service, IVR, speech, virtual agents, agent assist, RPA
Workforce EngagementNot started
Call recording / QM, WFM, agent self-service, integrations
TrainingNot started
Onboarding, format, assessment, eLearning, QM integration
Reporting & InsightsNot started
Real-time & historical reporting, analytics, BI, CRM
" - } - }, - "8c01f33274b44a20910ec99698895968": { + "0930f610263a4b528080bf6f5ffacbd4": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -3937,9 +2238,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Workforce Engagement: Call recording & Quality Management", + "label": "Routing & Automation: Self-service & IVR", "last_changed_at": "", - "layout": "IPY_MODEL_64b357cfe7e647da989f8b4d7c05f1c4", + "layout": "IPY_MODEL_7133de8c2e3c41a099abaccc289b15f8", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -3951,461 +2252,7 @@ "value": false } }, - "8d29c0b965d64aff91b81922f254e9fc": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8da9629bd3924c70b9ad955a2c82ffe6": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "8e45d2735d56446c93405512d56406e8": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Background & Organization: Lines of business & scope", - "last_changed_at": "", - "layout": "IPY_MODEL_527e8415076442039e93265c1cebec24", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "93441a4e6d9b4b3ebeef1fb8d1f943da": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "94a9db46ddc241c885f0810c3550517a": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Reporting & Insights: Reporting", - "last_changed_at": "", - "layout": "IPY_MODEL_24a8bc961c6f4ee59efbbe4f32c76607", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "9941fc792dfa4a5f884a491f99133dec": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "9e0e72ba8ed5453a8a08cc8c81f16205": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "a0a0a90b79f44db08819b52ffe94d365": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Channels: Outbound", - "last_changed_at": "", - "layout": "IPY_MODEL_317687cf884547b5a8a719e51f134c4b", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "a160eb8e19d64cc68309414d4e433e9c": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "a3acd4a93864411cae3f58d185cabc59": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.checkbox.CheckboxWidget", - "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "appearance": "box", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Channels: Inbound contact reasons", - "last_changed_at": "", - "layout": "IPY_MODEL_c225c7d2cdcc46b59621f0413c964797", - "layout_path": null, - "n_toggles": 0, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": false - } - }, - "adcce9bc4b7f45c3bccef29bbe7f4729": { + "1106845f53b9482785e0f05fdb4963d0": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -4427,7 +2274,7 @@ "hidden": false, "label": "Channels: Inbound context", "last_changed_at": "", - "layout": "IPY_MODEL_d5e6c9ab841649a3943de8516d201c78", + "layout": "IPY_MODEL_a63a18948c9a404b89a77592a35e02db", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -4439,7 +2286,7 @@ "value": false } }, - "b237b1282e1d499090a4accd367267e8": { + "11701720c3c842e684702ba6be5e0ae5": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -4492,113 +2339,7 @@ "width": null } }, - "b479458f18a8472d93e069a112241354": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ba537dd6b7474fbba13d60a0c3f13df3": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "baaac2ab4371417aa66465413053f6e0": { + "13e7c5f467664ac194d261fadba4959a": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -4618,9 +2359,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "CX Strategy: Continuous improvement", + "label": "Background & Organization: Decision-making & priorities", "last_changed_at": "", - "layout": "IPY_MODEL_5e52cee7caf7437297a6899d6ff3bb28", + "layout": "IPY_MODEL_cdeec6db898247ee9e6706c1f404a122", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -4632,7 +2373,321 @@ "value": false } }, - "bc99cd23ba4848a4b81af0d99bf57828": { + "144e9e43924d41d98f4cc4ea459a4c15": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Routing & Automation: Virtual agents & assist", + "last_changed_at": "", + "layout": "IPY_MODEL_7bac4bb4567749b6bd98ef82c639b2e3", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "15064d78098745139371ee1e6018eed9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "15d3a216f34d4ca9ae6e12fc8cdd073b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "178ff3bc6dd24293a7cd7e66dbad16bd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1f9d02f3b8bb46d687a1e679f3ff2fc8": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy: Employee engagement", + "last_changed_at": "", + "layout": "IPY_MODEL_c886c6fc8ea24360af763189f94d5d15", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "24bb8dec2f8f4c78bf88b3f52373297f": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Background & Organization: Lines of business & scope", + "last_changed_at": "", + "layout": "IPY_MODEL_9a5a5c752b754a70967bd4fb8b2bb913", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "26e755f3229e44018b5518f2527e0827": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "270238fd5160455eb05c27504d2b5837": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -4658,7 +2713,7 @@ "disabled": false, "hidden": false, "label": "CX Strategy — status", - "layout": "IPY_MODEL_3cd231f40b12490bae31f1f9c1b240a9", + "layout": "IPY_MODEL_58fd6227d30d4055ba1a6f58c9f0cd27", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -4669,38 +2724,7 @@ "value": "Not started" } }, - "c0b0ca61b36a403f99e52d5ced585d24": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.text.TextInputWidget", - "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Reporting & Insights — notes", - "layout": "IPY_MODEL_2a3235f448db40ca900d312d53278a6c", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "" - } - }, - "c0ee439c71714f0ca6a1876a68f3f376": { + "2ca1198978804af08c75aca447a11f72": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -4720,9 +2744,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Channels: Digital channels", + "label": "Training: Onboarding & enablement", "last_changed_at": "", - "layout": "IPY_MODEL_13c050864b524a8db731230057903a46", + "layout": "IPY_MODEL_26e755f3229e44018b5518f2527e0827", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -4734,113 +2758,7 @@ "value": false } }, - "c225c7d2cdcc46b59621f0413c964797": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "c3a30ebcbe544fb0b5220d244d680702": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "ca5b6dc1228045c6a8226f2c6fb62751": { + "2ca83b8bf2f9462c89aa095244b3cdf8": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -4860,9 +2778,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Background & Organization: Org & reporting structure", + "label": "Agent & Supervisor Environment: Locations & endpoints", "last_changed_at": "", - "layout": "IPY_MODEL_cda09e50fb8d4f77af5508c435630da5", + "layout": "IPY_MODEL_eac43b773b394485a0af3440b5ebcb52", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -4874,166 +2792,7 @@ "value": false } }, - "cb15afe0f83a447789bd669c0d369b79": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "cb93e54680da4b209c481f2b1798cdd1": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "cda09e50fb8d4f77af5508c435630da5": { - "model_module": "@jupyter-widgets/base", - "model_module_version": "2.0.0", - "model_name": "LayoutModel", - "state": { - "_model_module": "@jupyter-widgets/base", - "_model_module_version": "2.0.0", - "_model_name": "LayoutModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "LayoutView", - "align_content": null, - "align_items": null, - "align_self": null, - "border_bottom": null, - "border_left": null, - "border_right": null, - "border_top": null, - "bottom": null, - "display": null, - "flex": null, - "flex_flow": null, - "grid_area": null, - "grid_auto_columns": null, - "grid_auto_flow": null, - "grid_auto_rows": null, - "grid_column": null, - "grid_gap": null, - "grid_row": null, - "grid_template_areas": null, - "grid_template_columns": null, - "grid_template_rows": null, - "height": null, - "justify_content": null, - "justify_items": null, - "left": null, - "margin": null, - "max_height": null, - "max_width": null, - "min_height": null, - "min_width": null, - "object_fit": null, - "object_position": null, - "order": null, - "overflow": null, - "padding": null, - "right": null, - "top": null, - "visibility": null, - "width": null - } - }, - "d0f38990bdc944609aef3a89d89b68c5": { + "2d39ff2f4b0e4fa98c845d0c01176ac4": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5055,7 +2814,7 @@ "hidden": false, "label": "Reporting & Insights: Analytics & insights", "last_changed_at": "", - "layout": "IPY_MODEL_53ab38af97e84de4a6de038be1c8aeb1", + "layout": "IPY_MODEL_15d3a216f34d4ca9ae6e12fc8cdd073b", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -5067,7 +2826,7 @@ "value": false } }, - "d249edd2c0694c5aa6d41391b4843d32": { + "329a57df198e466ba69e7367a1c37d85": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5087,9 +2846,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "CX Strategy: Organization & operating model", + "label": "Channels: Outbound", "last_changed_at": "", - "layout": "IPY_MODEL_e07465cbaea24536b245a01b9e959f7c", + "layout": "IPY_MODEL_042c3b8a3f214120b35767f1417d66f8", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -5101,7 +2860,7 @@ "value": false } }, - "d5e6c9ab841649a3943de8516d201c78": { + "3d0b27c61761414fac0ffa2d5b769f71": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5154,7 +2913,414 @@ "width": null } }, - "d5fc113c037a45fcb33cc2ee210547ce": { + "3efab565b2a840549dd01aebe6b270c1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3f415d2ef16344b5ade65b4bbd294705": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "40816a8b2fbe4b58829519ef213d29f0": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Workforce Engagement: Call recording & Quality Management", + "last_changed_at": "", + "layout": "IPY_MODEL_b38b495799c14b0bbc2edfe7c4479d75", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "446932b693df46e4b5c528c611232809": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4a47dddf95d94d02ae46f7dab34a5c14": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4cf69fa2ee02431b9487d1ac33d9da0d": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Not started", + "In progress", + "Complete", + "Skipped" + ], + "disabled": false, + "hidden": false, + "label": "Routing & Automation — status", + "layout": "IPY_MODEL_62f0010b714c4bd1bc1a885e12dc2d44", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Not started" + } + }, + "5315b040652049c09ec602b63536aebb": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Not started", + "In progress", + "Complete", + "Skipped" + ], + "disabled": false, + "hidden": false, + "label": "Background & Organization — status", + "layout": "IPY_MODEL_f5dc0c3378e04b7e8b2a152761d6f08d", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Not started" + } + }, + "53de763e02ae4440b57202067351a984": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Reporting & Insights: Reporting", + "last_changed_at": "", + "layout": "IPY_MODEL_54945ce35fac44ed963a1be39cf75c36", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "54945ce35fac44ed963a1be39cf75c36": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "57a5e081f6024b97af20e9f2884679ff": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5180,7 +3346,7 @@ "disabled": false, "hidden": false, "label": "Channels — status", - "layout": "IPY_MODEL_93441a4e6d9b4b3ebeef1fb8d1f943da", + "layout": "IPY_MODEL_ee4759a94a9442eda867cd84f94c66ca", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -5191,7 +3357,131 @@ "value": "Not started" } }, - "d7133124428841aeb00d4d4cfb92c2da": { + "580c9cfbb39b4026b705ae12f7dcba5c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "2.0.0", + "model_name": "HTMLStyleModel", + "state": { + "_model_module": "@jupyter-widgets/controls", + "_model_module_version": "2.0.0", + "_model_name": "HTMLStyleModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "StyleView", + "background": null, + "description_width": "", + "font_size": null, + "text_color": null + } + }, + "58fd6227d30d4055ba1a6f58c9f0cd27": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5fb993e6591b45839dfb08e0151d42ea": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6144c1a5fb2d4353912e41aefe5a4d76": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5210,8 +3500,8 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Routing & Automation — notes", - "layout": "IPY_MODEL_6c32fa0d17b94bf8a63b7bbc8280aa9a", + "label": "Reporting & Insights — notes", + "layout": "IPY_MODEL_e52d1a503c644650b96900733b15497f", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -5222,7 +3512,7 @@ "value": "" } }, - "d790e920688441469dcfc7ab37244438": { + "62f0010b714c4bd1bc1a885e12dc2d44": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5275,7 +3565,44 @@ "width": null } }, - "d8ea7f202d46468592c105f97da67c40": { + "6321bc1fde1b47cca37c80004b61b8f4": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Not started", + "In progress", + "Complete", + "Skipped" + ], + "disabled": false, + "hidden": false, + "label": "Reporting & Insights — status", + "layout": "IPY_MODEL_178ff3bc6dd24293a7cd7e66dbad16bd", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Not started" + } + }, + "6399b4afa4e346bc91bb2e871d8d7ca6": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5328,7 +3655,113 @@ "width": null } }, - "d98420c917944187b16188da9f1d9632": { + "6828d6211b2248bf8253c65ecbc94364": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "690f3f6e51b84c5aa5dc985cd1c604f5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6a9ae131cdb74b89bbb08ab7e641e9a5": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5347,8 +3780,8 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Channels — notes", - "layout": "IPY_MODEL_d8ea7f202d46468592c105f97da67c40", + "label": "Background & Organization — notes", + "layout": "IPY_MODEL_6828d6211b2248bf8253c65ecbc94364", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -5359,7 +3792,7 @@ "value": "" } }, - "dbde403229964f3f8443b9b936f404d8": { + "6ae23c903d36447883c2785cc8a5d48f": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5412,7 +3845,41 @@ "width": null } }, - "dd5a5914c4334377b09865d1a88e7d46": { + "6cb47927b18246018129730648b65681": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Workforce Engagement: Agent self-service & integrations", + "last_changed_at": "", + "layout": "IPY_MODEL_79b4f30d2e134b62b5c5f7c7039e32c1", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "7133de8c2e3c41a099abaccc289b15f8": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5465,7 +3932,7 @@ "width": null } }, - "e07465cbaea24536b245a01b9e959f7c": { + "74a7de61c90d47daa34d0b080bf7575f": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5518,7 +3985,75 @@ "width": null } }, - "e21c6da3e67848e7860b2938873fba8b": { + "751b7349c889461cb1966e1046bda562": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Background & Organization: Org & reporting structure", + "last_changed_at": "", + "layout": "IPY_MODEL_ba964c845b50411c97e1e7f29b836542", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "761eb4eb9fc44ed9aac2abe03647a963": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy: Value & strategic position", + "last_changed_at": "", + "layout": "IPY_MODEL_b56912697bf04dd9bee9b64559c5dede", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "79b4f30d2e134b62b5c5f7c7039e32c1": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5571,7 +4106,41 @@ "width": null } }, - "e97c7bc947d648fc9274a52cdd0909c2": { + "79e0a92fa043419a8e7647b280648806": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Background & Organization: Problems & causes today", + "last_changed_at": "", + "layout": "IPY_MODEL_a655f239072a4bc493c54b8f4b695a8d", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "7a26751f917f4d569d23a5baf63e42fd": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5593,7 +4162,7 @@ "hidden": false, "label": "Workforce Engagement: Workforce management", "last_changed_at": "", - "layout": "IPY_MODEL_dd5a5914c4334377b09865d1a88e7d46", + "layout": "IPY_MODEL_15064d78098745139371ee1e6018eed9", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -5605,7 +4174,1204 @@ "value": false } }, - "ec1aaa717f20444d91a9b187238ba568": { + "7bac4bb4567749b6bd98ef82c639b2e3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7ec27ebdd0534bcb93b836a6969d7ee4": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Not started", + "In progress", + "Complete", + "Skipped" + ], + "disabled": false, + "hidden": false, + "label": "Training — status", + "layout": "IPY_MODEL_c91a83213c24454db6a97f603c9d17f2", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Not started" + } + }, + "8af987ef301441f4863ba06560562f8e": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Agent & Supervisor Environment: Desktop & applications", + "last_changed_at": "", + "layout": "IPY_MODEL_11701720c3c842e684702ba6be5e0ae5", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "95a4df8983ad4a87bedce4d05ed24866": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.text.TextInputWidget", + "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Routing & Automation — notes", + "layout": "IPY_MODEL_6ae23c903d36447883c2785cc8a5d48f", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "" + } + }, + "9a0bd34cd0074b6bab9d054716e11db5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9a5a5c752b754a70967bd4fb8b2bb913": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9b6bd28767414d5dae7b74f8764ae3b0": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.text.TextInputWidget", + "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Training — notes", + "layout": "IPY_MODEL_3f415d2ef16344b5ade65b4bbd294705", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "" + } + }, + "a0f8e23133534198b9341e3110019919": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.text.TextInputWidget", + "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Agent & Supervisor Environment — notes", + "layout": "IPY_MODEL_fa1dd60103d849eeb292ad2d09ff2c9e", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "" + } + }, + "a63a18948c9a404b89a77592a35e02db": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a655f239072a4bc493c54b8f4b695a8d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ac45ef5a635c45948e7114eeb7d2f1db": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Channels: Inbound contact reasons", + "last_changed_at": "", + "layout": "IPY_MODEL_74a7de61c90d47daa34d0b080bf7575f", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "b38b495799c14b0bbc2edfe7c4479d75": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b56912697bf04dd9bee9b64559c5dede": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b5994a2dc9d440b7be67f2b46a901f0e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ba964c845b50411c97e1e7f29b836542": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "be7743807bfd49a089ade62b2bbf402c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c41f68b24eae437382accffe853e6741": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Routing & Automation: Digital assistant apps", + "last_changed_at": "", + "layout": "IPY_MODEL_446932b693df46e4b5c528c611232809", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "c886c6fc8ea24360af763189f94d5d15": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c91a83213c24454db6a97f603c9d17f2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cdeec6db898247ee9e6706c1f404a122": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d1fa2807b324467db7b3e394440fab4a": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy: Continuous improvement", + "last_changed_at": "", + "layout": "IPY_MODEL_b5994a2dc9d440b7be67f2b46a901f0e", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "d45855f3f9f04a77932dfaa2ab5ce04f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d7656b3ac6ea486d9bbd7bbc100445ee": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.text.TextInputWidget", + "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Channels — notes", + "layout": "IPY_MODEL_4a47dddf95d94d02ae46f7dab34a5c14", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "" + } + }, + "dbeb682621e44f94a7d9a1f9c62702a0": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy: Insight & Voice of Customer", + "last_changed_at": "", + "layout": "IPY_MODEL_3d0b27c61761414fac0ffa2d5b769f71", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "e1f98c09c21140b59bc95ff244dfefec": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Channels: Digital channels", + "last_changed_at": "", + "layout": "IPY_MODEL_d45855f3f9f04a77932dfaa2ab5ce04f", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "e4668feb3c6945269734a21c44a2f88f": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Not started", + "In progress", + "Complete", + "Skipped" + ], + "disabled": false, + "hidden": false, + "label": "Agent & Supervisor Environment — status", + "layout": "IPY_MODEL_be7743807bfd49a089ade62b2bbf402c", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Not started" + } + }, + "e52d1a503c644650b96900733b15497f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e6517b09bfe94951a00d798aa5b76733": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5631,7 +5397,7 @@ "disabled": false, "hidden": false, "label": "Workforce Engagement — status", - "layout": "IPY_MODEL_ba537dd6b7474fbba13d60a0c3f13df3", + "layout": "IPY_MODEL_f24ed3a61e0d4796801fe4eb8f1612ca", "layout_path": null, "position": "sidebar", "render_slot_id": null, @@ -5642,7 +5408,7 @@ "value": "Not started" } }, - "f55615cdb8b6441fabee7044abd191bf": { + "eac43b773b394485a0af3440b5ebcb52": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5695,7 +5461,144 @@ "width": null } }, - "f762178850dd4f44b873f9c9f9e3c299": { + "ec920e3e896942bd929d9ff381bccd22": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ecf6ea40937e4cc3ac0f7369be197134": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.text.TextInputWidget", + "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy — notes", + "layout": "IPY_MODEL_5fb993e6591b45839dfb08e0151d42ea", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "" + } + }, + "ee4759a94a9442eda867cd84f94c66ca": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f02bf16913fa4c44ac665e96bd9d6f47": { "model_module": "anywidget", "model_module_version": "~0.11.*", "model_name": "AnyModel", @@ -5715,9 +5618,9 @@ "cell_id": "", "disabled": false, "hidden": false, - "label": "Routing & Automation: Virtual agents & assist", + "label": "Channels: Voice & video metrics", "last_changed_at": "", - "layout": "IPY_MODEL_9941fc792dfa4a5f884a491f99133dec", + "layout": "IPY_MODEL_9a0bd34cd0074b6bab9d054716e11db5", "layout_path": null, "n_toggles": 0, "position": "sidebar", @@ -5729,56 +5632,7 @@ "value": false } }, - "fc6b90dcba87418981b0601fbb9147e8": { - "model_module": "@jupyter-widgets/controls", - "model_module_version": "2.0.0", - "model_name": "HTMLStyleModel", - "state": { - "_model_module": "@jupyter-widgets/controls", - "_model_module_version": "2.0.0", - "_model_name": "HTMLStyleModel", - "_view_count": null, - "_view_module": "@jupyter-widgets/base", - "_view_module_version": "2.0.0", - "_view_name": "StyleView", - "background": null, - "description_width": "", - "font_size": null, - "text_color": null - } - }, - "fdd5b7191f314be683bf53cb82d4270b": { - "model_module": "anywidget", - "model_module_version": "~0.11.*", - "model_name": "AnyModel", - "state": { - "_anywidget_id": "mercury.text.TextInputWidget", - "_css": "\n .mljar-textinput-container {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-textinput-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n text-align: left;\n width: 100%;\n font-weight: 600;\n }\n\n .mljar-textinput-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 10px;\n box-sizing: border-box;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n color: #0f172a;\n line-height: 1.4;\n\n appearance: none !important;\n background-color: #ffffff !important;\n }\n\n .mljar-textinput-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-textinput-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-textinput-input {\n min-height: 44px;\n padding: 10px 12px;\n }\n }\n ", - "_dom_classes": [], - "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-textinput-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-textinput-top-label\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-textinput-input\");\n\n container.appendChild(topLabel);\n container.appendChild(input);\n el.appendChild(container);\n\n let debounceTimer = null;\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", input.value);\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => model.save_changes(), 200);\n });\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter text\";\n input.value = model.get(\"value\") ?? \"\";\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n /*\n // ---- read cell id (no DOM modifications) ----\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", - "_model_module": "anywidget", - "_model_module_version": "~0.11.*", - "_model_name": "AnyModel", - "_view_count": null, - "_view_module": "anywidget", - "_view_module_version": "~0.11.*", - "_view_name": "AnyView", - "cell_id": "", - "disabled": false, - "hidden": false, - "label": "Workforce Engagement — notes", - "layout": "IPY_MODEL_a160eb8e19d64cc68309414d4e433e9c", - "layout_path": null, - "position": "sidebar", - "render_slot_id": null, - "source_cell_id": null, - "tabbable": null, - "tooltip": null, - "url_key": "", - "value": "" - } - }, - "fffeb5b7a0a04c89a01346ba7d8741db": { + "f24ed3a61e0d4796801fe4eb8f1612ca": { "model_module": "@jupyter-widgets/base", "model_module_version": "2.0.0", "model_name": "LayoutModel", @@ -5830,6 +5684,146 @@ "visibility": null, "width": null } + }, + "f5dc0c3378e04b7e8b2a152761d6f08d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fa1dd60103d849eeb292ad2d09ff2c9e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fbe8fdd901784c6f9c048000a06f97b0": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "box", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "CX Strategy: Organization & operating model", + "last_changed_at": "", + "layout": "IPY_MODEL_ec920e3e896942bd929d9ff381bccd22", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } } }, "version_major": 2, diff --git a/calculators/Genesys_Token_Calculator/README.md b/calculators/Genesys_Token_Calculator/README.md new file mode 100644 index 0000000..1b3a7d7 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/README.md @@ -0,0 +1,172 @@ +# Genesys Cloud AI — Token Calculator + +A **Calculator** — a reusable vendor-pricing master (see +[`docs/Calculator_Pattern_V1-00.md`](../../docs/Calculator_Pattern_V1-00.md)) +— built on the +[Mercury Notebook Deliverable Pattern](../../docs/Mercury_Notebook_Pattern_V1-00.md). +It prices what Genesys Cloud AI **costs to run**, from the vendor's own +published rate card. Unlike a TEI study it computes no benefits and no NPV: +its "numbers" are a run-rate, a per-feature breakdown, and a range. + +The Mercury stage is the screen you share with a client while you size their +AI footprint together; the consultant run-book, the verification gate, and the +machine-readable appendix live backstage and export as LLM input for a +downstream business case. + +## The anchor: the vendor's published rate card + +The meters are Genesys' own, transcribed verbatim into +[`genesyscalc/ratecard.py`](genesyscalc/ratecard.py): + +| Source | Published | What it gives | +|---|---|---| +| [Genesys Cloud tokens model](https://help.genesys.cloud/articles/genesys-cloud-tokens-model/) | **2026-07-12** | 20 token meters, the free monthly allowance, the highest-tier rule, the Copilot exclusion | +| [Genesys Enhanced TTS pricing](https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/) | **2026-05-22** | $/million characters — **not** token-metered | + +That file is **immutable**. When Genesys republishes, re-anchoring is a +deliberate act with its own protocol — see *Re-anchoring* below. + +### Four published rules the obvious arithmetic gets wrong + +Each is enforced in the engine, pinned by a test, and reported in the +notebook's warnings rather than silently applied: + +1. **The 15-second per-call round-up.** *"Genesys rounds up each call to the + next 15-second increment."* Applied per call and then summed — averaging + first understates the bill. A 40-second average bills as 45 (+12.5%); a + 5-second bot greeting bills as 15 (3× its duration). +2. **The free monthly allowance.** 250 tokens named / 350 concurrent, which + *"renew each month and do not carry over"* — so it is a real deduction, and + an unused remainder is discarded, never banked. +3. **The highest-tier rule.** *"Genesys bases charges on the highest tier + (price) resource…"* — so the deflection shares are a **partition**, not + overlapping rates. An interaction is charged once, at the highest tier it + touched. Summing tiers independently over-bills. +4. **Copilot covers Supervisor summaries.** Enabling Agent Copilot makes AI + Summary & Insights free; billing both double-charges. + +## What the client sees (the stage) + +The **published rate card** as the vendor prints it, then three KPI cards +(token cost, text-to-speech, total run-rate with a $/interaction figure), the +allowance shown as the deduction it is, a per-feature cost breakdown, the +list-price → negotiated walk, text-to-speech as its own clearly separated +line, an adoption scenario range, and a tornado of what actually moves the +number. + +Text-to-speech is **never** folded silently into the token subtotal: the grand +total always shows tokens and TTS as two labelled components. + +You drive it from the sidebar — licence model, currency, token price and +concession, the three deflection shares, average bot seconds, and a +per-feature enable. Every change re-renders the numbers (Mercury re-runs the +cells below the widgets). + +## Where the content lives: in the notebook + +Two tagged cells of +[`notebooks/genesys_token_calculator.ipynb`](notebooks/genesys_token_calculator.ipynb) +carry everything a consultant edits — **in Jupyter, never in a `.py` file**: + +- **`topic-bank`** — the **feature catalogue**: each priced capability's + client-facing title, what it does for them, and the sizing question to ask. + The `key` slugs must match published meter keys (a test pins this), and they + are stable identities the widgets, notes and JSON export all key off. +- **`engagement-data`** — the client facts: volumes by channel, headcount, + licence model, and the **negotiated** commercial terms (contracted rate, + concession). **Illustrative placeholders in this master**; filled in the + engagement copy. + +The published **rates** are deliberately *not* content — nobody in this repo +authors them, and editing them is forbidden. They are the vendor's record, so +they live in the engine as an anchor. See the Calculator Pattern for the full +boundary. + +`genesyscalc/` holds **code only** — the meter schema, the anchor, and the +arithmetic. The test suite reads the tagged cells straight out of the notebook +(no kernel) and pins content shape, so `pytest` guards the catalogue exactly +as shipped. + +## Layout + +``` +notebooks/genesys_token_calculator.ipynb # THE deliverable — content + data + presentation + # tagged: topic-bank · engagement-data · + # presentation ×8 · gate · data-appendix +genesyscalc/ # the engine — code only, no content, mypy --strict + ratecard.py # THE VERBATIM ANCHOR — published meters, 2026-07-12 + meters.py # schema: Confidence, LicenceModel, MeterBasis, Tier, Meter, TokenPrice + usage.py # volumes → units → tokens (round-up, tier partition, Copilot rule) + billing.py # tokens → dollars (ceil, allowance, price, concession) + tts.py # the NON-token line — own source, own rounding, EOS/EOL + model.py # orchestration: CalculatorInputs → CalculatorResult + sensitivity.py # sweeps + tornado data (no plotting) + appendix.py # the machine-readable JSON payload + staging.py # stage/backstage detection (+ backstage_md) +scripts/export_report.py # execute once → exports/*.html + LLM-ready *.md +tests/ # rate-card tripwire, hand-checked arithmetic, content pins, staging +exports/ # generated report sources (never committed) +``` + +## Run + +```bash +python -m venv .venv && source .venv/bin/activate +pip install -e ".[dev]" + +mercury --working-dir . # serve the stage (share this screen) +jupyter lab # analyst view +pytest # rate card + engine + content + staging pins +mypy # strict +jupyter nbconvert --to notebook --execute --inplace notebooks/genesys_token_calculator.ipynb +python scripts/export_report.py # exports/*.html + *.md for the LLM handoff +``` + +The `.md` export opens with a generated preamble (what the document is, the +engagement line, **which rate card produced the numbers**) and ends with the +**Model state (JSON)** block — the machine source of truth, carrying +`rate_card.source_date`. A cost figure without its rate-card date is not +auditable. + +## Re-anchoring — when Genesys republishes + +The rate card is a *living* anchor: immutable until the vendor changes it, +then updated deliberately. Never partially re-anchor, and never bump the date +without the pins. + +1. Diff the published table against `genesyscalc/ratecard.py`. +2. Update the meters **and** `RATE_CARD_SOURCE_DATE` in the same edit. +3. Update the pins in `tests/test_rate_card.py` in the same commit. +4. Re-execute the notebook; update the gate's live-state pins if the defaults moved. +5. Report the cost moves honestly. +6. **Show-first** — the user sees the rate diff and the KPI move before it lands. + +`tests/test_rate_card.py` is the tripwire: it pins all 20 rows by exact +feature wording *and* exact rate string, so a silent transcription drift or an +un-noticed republication breaks the build rather than quietly moving a +client-facing number. + +## Extending + +New or reshaped catalogue content is an edit to the notebook's `topic-bank` +cell in JupyterLab, then re-run the notebook (the **gate** recounts the +catalogue and checks every key against the published meters) and re-pin +`tests/test_catalogue.py`. New *logic* goes in `genesyscalc/` with pins in +`tests/`, **before** the notebook section that renders it. + +## Non-goals + +- **Not a business case.** No benefits, no NPV, no payback, no ROI, no P&L. If + a client needs those, that is a **Study** — see [`studies/`](../../studies/). + A calculator that grows a benefit model has stopped being a calculator. +- **Not contractual pricing.** A planning model at list rates unless a + contracted rate is entered. Genesys quotes; this estimates. +- **Not a platform TCO.** Genesys Cloud CX seat licensing is out of scope — + this prices the **AI** meters and Enhanced TTS only. + +**Running this for a client?** Don't fill client data into this master — +copy the directory out of Palladium, stamp it +`YYYYMM_Client_Genesys_Token_Calculator`, provision a fresh venv there, fill +the `engagement-data` cell in the copy, and treat the copy as confidential +(CLAUDE.md § Confidentiality). A test in this master fails if a client name +lands in `engagement-data` here. diff --git a/calculators/Genesys_Token_Calculator/config.toml b/calculators/Genesys_Token_Calculator/config.toml new file mode 100644 index 0000000..c39abed --- /dev/null +++ b/calculators/Genesys_Token_Calculator/config.toml @@ -0,0 +1,81 @@ +# Mercury app-shell theme — NTT DATA brand (light), modern surfaces. +# See docs/brand.md for the source palette. Loaded from the directory where +# you launch `mercury` (this calculator root); restart the server to apply. + +[main] +title = "Genesys Token Calculator" +favicon_emoji = "🪙" +footer = "Genesys Cloud AI token calculator · NTT DATA" +notebooks_button_label = "Calculators" + +[welcome] +header = "Genesys Cloud AI — Token Calculator" +message = """ +What Genesys Cloud AI costs to run — open **genesys_token_calculator.ipynb**. +Meters are the vendor's **published** rate card as of **2026-07-12** +([tokens model](https://help.genesys.cloud/articles/genesys-cloud-tokens-model/)), +with Enhanced TTS priced separately from its own published page (2026-05-22). +Set volumes and headcount, move the sidebar controls to test deflection and +commercial terms, and read the per-feature breakdown. A planning model — list +rates unless overridden, never a quote. Export with +`python scripts/export_report.py`. +""" + +[theme] +# ── Type — Georgia headings, Arial body (web-safe; no network fetch). ── +font_family = "Arial, 'Helvetica Neue', Helvetica, sans-serif" +heading_font_family = "Georgia, 'Times New Roman', Times, serif" +font_size = "15px" +font_weight = "normal" +heading_font_weight = "700" + +# ── Text — NTT ink scale ── +text_color = "#2e404d" +muted_text_color = "#586671" + +# ── Surfaces — white content on a soft neutral canvas ── +background_color = "#f4f5f6" +content_background_color = "#ffffff" +surface_color = "#ffffff" +card_background_color = "#f8f8f8" +border_color = "#d5d9db" +border_radius = "10px" + +# ── Accents — Future Blue ── +primary_color = "#0072bc" +accent_color = "#0072bc" +focus_border_color = "#0072bc" +hover_background_color = "#eef5fb" +selected_background_color = "#dcecfa" + +# ── State colors — brand Success / Warning / Error (docs/brand.md) ── +success_color = "#00cb5d" +warning_color = "#ffc400" +danger_color = "#e42600" +slider_track_color = "#e2e6e9" + +# ── Sidebar — clean white, hairline divider ── +sidebar_background_color = "#ffffff" +sidebar_text_color = "#2e404d" +sidebar_title_color = "#151d2c" +sidebar_shadow = "1px 0 0 #d5d9db" + +# ── Top bar — deep NTT navy ── +topbar_background_color = "#151d2c" +topbar_text_color = "#ffffff" +topbar_border_color = "rgba(255,255,255,0.08)" + +# ── Footer ── +footer_background_color = "#ffffff" +footer_text_color = "#586671" +footer_border_color = "#d5d9db" + +# ── Run button — subtle brand-blue gradient ── +run_button_background = "linear-gradient(180deg, #0087dc 0%, #0072bc 100%)" +run_button_background_hover = "linear-gradient(180deg, #1a93e6 0%, #0079c8 100%)" +run_button_text_color = "#ffffff" + +# ── Depth — soft, navy-tinted shadows ── +shadow_sm = "0 1px 2px rgba(21,29,44,0.05)" +shadow_md = "0 6px 18px rgba(21,29,44,0.08)" +shadow_lg = "0 16px 40px rgba(21,29,44,0.10)" diff --git a/calculators/Genesys_Token_Calculator/exports/.gitkeep b/calculators/Genesys_Token_Calculator/exports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/__init__.py b/calculators/Genesys_Token_Calculator/genesyscalc/__init__.py new file mode 100644 index 0000000..c476571 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/__init__.py @@ -0,0 +1,142 @@ +"""Genesys Cloud AI token cost calculator — the engine. + +Math only. The vendor's published rate card is the immutable anchor in +:mod:`genesyscalc.ratecard`; everything the consultant authors or the client +supplies lives in the notebook's tagged cells. See +``docs/Calculator_Pattern_V1-00.md``. +""" + +from __future__ import annotations + +from .appendix import result_json +from .billing import ( + MONTHS_PER_YEAR, + CostLine, + CostTotals, + allowance_for, + billable_tokens_monthly, + cost_lines, + total_cost, +) +from .meters import ( + Confidence, + LicenceModel, + Meter, + MeterBasis, + Tier, + TokenPrice, +) +from .model import ( + CalculatorInputs, + CalculatorResult, + calculate, + compare, + cost_per_interaction, + metered_units, +) +from .ratecard import ( + ALLOWANCE_CARRIES_OVER, + COPILOT_COVERS_SUMMARY_RULE, + FREE_TOKENS_PER_MONTH, + HIGHEST_TIER_RULE, + LIST_PRICE_BY_CURRENCY, + METERS, + METERS_VERBATIM, + RATE_CARD_SOURCE, + RATE_CARD_SOURCE_DATE, + VOICE_BOT_ROUNDUP_SECONDS, + ZERO_RATED_VERBATIM, + meter, + per_user_meter, + rate_card_rows, +) +from .sensitivity import DRIVERS, sweep, tornado +from .staging import backstage, backstage_md, on_stage +from .tts import ( + TTS_PRICE_PER_MILLION_CHARS, + TTS_SOURCE, + TTS_SOURCE_DATE, + TTS_STANDARD_END_OF_SALE, + TTS_STANDARD_END_OF_SUPPORT, + TtsLine, + TtsUsage, + tts_cost, + tts_free_share, +) +from .usage import ( + DeflectionMix, + FeatureUsage, + VoiceBotUsage, + Volumes, + apply_copilot_covers_summary, + subscription_tokens, + virtual_agent_tokens, + voice_bot_billable_minutes, + voice_bot_roundup_uplift, + voice_bot_tokens, +) + +__version__ = "0.1.0" + +__all__ = [ + "ALLOWANCE_CARRIES_OVER", + "COPILOT_COVERS_SUMMARY_RULE", + "DRIVERS", + "FREE_TOKENS_PER_MONTH", + "HIGHEST_TIER_RULE", + "LIST_PRICE_BY_CURRENCY", + "METERS", + "METERS_VERBATIM", + "MONTHS_PER_YEAR", + "RATE_CARD_SOURCE", + "RATE_CARD_SOURCE_DATE", + "TTS_PRICE_PER_MILLION_CHARS", + "TTS_SOURCE", + "TTS_SOURCE_DATE", + "TTS_STANDARD_END_OF_SALE", + "TTS_STANDARD_END_OF_SUPPORT", + "VOICE_BOT_ROUNDUP_SECONDS", + "ZERO_RATED_VERBATIM", + "CalculatorInputs", + "CalculatorResult", + "Confidence", + "CostLine", + "CostTotals", + "DeflectionMix", + "FeatureUsage", + "LicenceModel", + "Meter", + "MeterBasis", + "Tier", + "TokenPrice", + "TtsLine", + "TtsUsage", + "VoiceBotUsage", + "Volumes", + "__version__", + "allowance_for", + "apply_copilot_covers_summary", + "backstage", + "backstage_md", + "billable_tokens_monthly", + "calculate", + "compare", + "cost_lines", + "cost_per_interaction", + "meter", + "metered_units", + "on_stage", + "per_user_meter", + "rate_card_rows", + "result_json", + "subscription_tokens", + "sweep", + "tornado", + "total_cost", + "tts_cost", + "tts_free_share", + "virtual_agent_tokens", + "voice_bot_billable_minutes", + "voice_bot_roundup_uplift", + "voice_bot_tokens", +] diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/appendix.py b/calculators/Genesys_Token_Calculator/genesyscalc/appendix.py new file mode 100644 index 0000000..503a22f --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/appendix.py @@ -0,0 +1,126 @@ +"""The machine-readable model state (Mercury Notebook Pattern §5). + +Plotly figures export as JavaScript an LLM cannot read, so the exported +``.md`` ends with one JSON block carrying everything the figures showed. + +``rate_card.source_date`` is the most important field here: it tells a +consumer of the export *which* published rate card produced these numbers. +A cost figure without its rate-card date is not auditable. +""" + +from __future__ import annotations + +from typing import Any + +from .model import CalculatorInputs, CalculatorResult, cost_per_interaction +from .ratecard import ( + RATE_CARD_SOURCE, + RATE_CARD_SOURCE_DATE, + VOICE_BOT_ROUNDUP_SECONDS, +) +from .tts import TTS_SOURCE, TTS_SOURCE_DATE + + +def result_json( + result: CalculatorResult, + inputs: CalculatorInputs, + meta: dict[str, Any] | None = None, +) -> dict[str, Any]: + """The export payload: provenance, inputs, lines, totals, warnings.""" + totals = result.totals + payload: dict[str, Any] = { + "rate_card": { + "source": RATE_CARD_SOURCE, + "source_date": RATE_CARD_SOURCE_DATE, + "voice_bot_roundup_seconds": VOICE_BOT_ROUNDUP_SECONDS, + }, + "tts_source": {"source": TTS_SOURCE, "source_date": TTS_SOURCE_DATE}, + "inputs": { + "users": inputs.users, + "licence_model": inputs.licence.value, + "enabled_features": sorted(inputs.enabled), + "apply_allowance": inputs.apply_allowance, + "volumes": { + k: v for k, v in vars(inputs.volumes).items() if not k.startswith("_") + }, + "deflection_mix": { + "bot_only_share": inputs.mix.bot_only_share, + "virtual_agent_share": inputs.mix.virtual_agent_share, + "agentic_va_share": inputs.mix.agentic_va_share, + "agent_handled_share": inputs.mix.agent_handled_share, + }, + "price": { + "currency": inputs.price.currency, + "list_rate": inputs.price.list_rate, + "contracted_rate": inputs.price.contracted_rate, + "concession_pct": inputs.price.concession_pct, + "effective_rate": inputs.price.effective_rate(), + }, + "voice_bot": ( + { + "calls_per_month": inputs.voice_bot.calls_per_month, + "avg_bot_seconds_per_call": ( + inputs.voice_bot.avg_bot_seconds_per_call + ), + } + if inputs.voice_bot is not None + else None + ), + "tts": ( + { + "calls_monthly": inputs.tts.calls_monthly, + "chars_per_call": inputs.tts.chars_per_call, + "tier": inputs.tts.tier, + } + if inputs.tts is not None + else None + ), + }, + "lines": [ + { + "key": line.key, + "feature": line.feature, + "units_monthly": line.units_monthly, + "unit_label": line.unit_label, + "tokens_monthly": line.tokens_monthly, + "cost_monthly": line.cost_monthly, + "cost_annual": line.cost_annual, + "confidence": line.confidence.value, + } + for line in result.lines + ], + "totals": { + "consumption_tokens_monthly": totals.consumption_tokens_monthly, + "subscription_tokens_monthly": totals.subscription_tokens_monthly, + "gross_tokens_monthly": totals.gross_tokens_monthly, + "allowance_tokens_monthly": totals.allowance_tokens_monthly, + "billable_tokens_monthly": totals.billable_tokens_monthly, + "currency": totals.currency, + "effective_rate": totals.effective_rate, + "token_cost_monthly": totals.token_cost_monthly, + "token_cost_annual": totals.token_cost_annual, + "list_cost_annual": totals.list_cost_annual, + "concession_saving_annual": totals.concession_saving_annual, + "tts_cost_annual": result.tts_cost_annual, + "grand_total_annual": result.grand_total_annual, + "cost_per_interaction": cost_per_interaction(result, inputs.volumes), + }, + "tts": ( + { + "tier": result.tts.tier, + "rate_per_million_chars": result.tts.rate_per_million_chars, + "total_chars_monthly": result.tts.total_chars_monthly, + "free_share": result.tts.free_share, + "billable_chars_monthly": result.tts.billable_chars_monthly, + "billed_millions_monthly": result.tts.billed_millions_monthly, + "cost_monthly": result.tts.cost_monthly, + "cost_annual": result.tts.cost_annual, + } + if result.tts is not None + else None + ), + "warnings": list(result.warnings), + } + if meta: + payload["meta"] = meta + return payload diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/billing.py b/calculators/Genesys_Token_Calculator/genesyscalc/billing.py new file mode 100644 index 0000000..476640d --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/billing.py @@ -0,0 +1,151 @@ +"""Tokens → dollars. + +The order of operations is the whole content of this module, and it is +pinned by tests because getting it wrong moves the bill: + +1. Round consumption up to whole tokens, monthly. Per-user subscription + totals are exact and are NOT rounded — only metered consumption is. +2. Deduct the free monthly allowance (250 named / 350 concurrent). +3. Floor at zero. The allowance renews monthly and "do[es] not carry over + to future months", so an unused remainder is discarded, never banked. + +Everything commercial — the contracted rate, the concession — arrives via +:class:`genesyscalc.meters.TokenPrice` from the notebook's engagement-data +cell. This module knows arithmetic, not deals. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass + +from .meters import Confidence, LicenceModel, TokenPrice +from .ratecard import FREE_TOKENS_PER_MONTH, meter + +MONTHS_PER_YEAR: int = 12 + + +@dataclass(frozen=True) +class CostLine: + """One feature's monthly and annual cost, ready to render.""" + + key: str + feature: str + units_monthly: float + unit_label: str + tokens_monthly: float + cost_monthly: float + cost_annual: float + confidence: Confidence + source_url: str | None + note: str = "" + + +@dataclass(frozen=True) +class CostTotals: + """The token bill, with the allowance and the MSRP walk both visible.""" + + consumption_tokens_monthly: float + subscription_tokens_monthly: float + gross_tokens_monthly: float + allowance_tokens_monthly: int + billable_tokens_monthly: int + effective_rate: float + currency: str + token_cost_monthly: float + token_cost_annual: float + list_cost_annual: float + concession_saving_annual: float + + +def billable_tokens_monthly( + consumption_tokens: float, + subscription_tokens: float, + licence: LicenceModel, + apply_allowance: bool = True, +) -> int: + """Whole tokens actually billed in a month. + + ``ceil`` lands on the consumption subtotal (Genesys bills whole tokens); + subscription totals are already whole and exact. The allowance is then + deducted and the result floored at zero — it does not carry over, so an + unused remainder is simply lost. + """ + if consumption_tokens < 0 or subscription_tokens < 0: + raise ValueError("token counts must not be negative") + gross = math.ceil(consumption_tokens) + subscription_tokens + allowance = FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0 + return max(0, int(gross) - allowance) + + +def allowance_for(licence: LicenceModel, apply_allowance: bool = True) -> int: + """The free monthly token allowance actually in play.""" + return FREE_TOKENS_PER_MONTH[licence] if apply_allowance else 0 + + +def cost_lines( + tokens_by_key: dict[str, float], units_by_key: dict[str, float], price: TokenPrice +) -> tuple[CostLine, ...]: + """One :class:`CostLine` per meter, in published order, at the effective rate. + + Line costs are unrounded and un-allowanced: the allowance is an + org-level deduction, not a per-feature one, so attributing it to a + single line would misstate that feature's economics. It lands in + :func:`total_cost`. + """ + rate = price.effective_rate() + lines: list[CostLine] = [] + for key in sorted(tokens_by_key, key=lambda k: meter(k).feature): + m = meter(key) + tokens = tokens_by_key[key] + monthly = tokens * rate + lines.append( + CostLine( + key=key, + feature=m.feature, + units_monthly=units_by_key.get(key, 0.0), + unit_label=m.unit_label, + tokens_monthly=tokens, + cost_monthly=monthly, + cost_annual=monthly * MONTHS_PER_YEAR, + confidence=m.confidence, + source_url=m.source_url, + note=m.note, + ) + ) + return tuple(lines) + + +def total_cost( + consumption_tokens_monthly: float, + subscription_tokens_monthly: float, + licence: LicenceModel, + price: TokenPrice, + apply_allowance: bool = True, +) -> CostTotals: + """The org-level bill: allowance deducted, MSRP walk carried alongside.""" + billable = billable_tokens_monthly( + consumption_tokens_monthly, + subscription_tokens_monthly, + licence, + apply_allowance, + ) + rate = price.effective_rate() + monthly = billable * rate + annual = monthly * MONTHS_PER_YEAR + list_annual = billable * price.list_rate * MONTHS_PER_YEAR + return CostTotals( + consumption_tokens_monthly=consumption_tokens_monthly, + subscription_tokens_monthly=subscription_tokens_monthly, + gross_tokens_monthly=( + math.ceil(consumption_tokens_monthly) + subscription_tokens_monthly + ), + allowance_tokens_monthly=allowance_for(licence, apply_allowance), + billable_tokens_monthly=billable, + effective_rate=rate, + currency=price.currency, + token_cost_monthly=monthly, + token_cost_annual=annual, + list_cost_annual=list_annual, + concession_saving_annual=list_annual - annual, + ) diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/meters.py b/calculators/Genesys_Token_Calculator/genesyscalc/meters.py new file mode 100644 index 0000000..3500a59 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/meters.py @@ -0,0 +1,165 @@ +"""Schema for the rate card — types only, no rates. + +Rates live in :mod:`genesyscalc.ratecard` as an immutable verbatim anchor. +This module defines what a metered feature *is*, so the anchor can be typed +and the arithmetic can be tested without either knowing the other's +business. + +The invariants in ``__post_init__`` are the point: a meter flagged +:attr:`Confidence.CONFIRMED` that carries no source URL and date cannot be +constructed. That is what stops an unsourced number wearing a green tick on +a client-facing stage. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + + +class Confidence(Enum): + """How much weight a number carries. Rendered on stage as an icon.""" + + CONFIRMED = "confirmed" # published by the vendor, with a source URL + date + ESTIMATED = "estimated" # working assumption, stated + UNKNOWN = "unknown" # not sourced; bound it with sensitivity if material + + @property + def icon(self) -> str: + return {"confirmed": "🟢", "estimated": "🟡", "unknown": "🔴"}[self.value] + + +class LicenceModel(Enum): + """Genesys prices per-user AI features differently by licence model.""" + + NAMED = "named" + CONCURRENT = "concurrent" + + @property + def label(self) -> str: + return self.value.capitalize() + + +class MeterBasis(Enum): + """How a feature converts activity into tokens.""" + + PER_USER_PER_MONTH = "per_user_per_month" # Copilot 40/60, STA 30/45 + UNITS_PER_TOKEN = "units_per_token" # "51 sessions per token" + TOKENS_PER_UNIT = "tokens_per_unit" # "0.5 tokens per interaction" + VOICE_MINUTES = "voice_minutes" # 17 min/token, 15s per-call round-up + ZERO_RATED = "zero_rated" # "no charge for token usage" + + +class Tier(Enum): + """Ordering for the published highest-tier rule. + + "Genesys bases charges on the highest tier (price) resource that Genesys + Cloud uses during the interaction." Higher value = higher tier = wins. + An interaction is charged once, at the highest tier it touched — see + :class:`genesyscalc.usage.DeflectionMix`. + """ + + BOT = 1 + VIRTUAL_AGENT = 2 + AGENTIC_VIRTUAL_AGENT = 3 + + +@dataclass(frozen=True) +class Meter: + """One line of the vendor's published rate card. + + ``feature`` and ``published_rate`` hold the article's *exact* wording; + ``rate`` holds the parsed number the arithmetic uses. Keeping both is + what makes a transcription slip catchable — ``test_rate_card`` pins the + strings, and a separate test proves the float agrees with the string. + """ + + key: str + feature: str # the article's exact feature name + published_rate: str # the article's exact rate wording + basis: MeterBasis + rate: float = 0.0 # tokens per unit (0.0 for per-user/zero-rated) + per_user_named: float | None = None + per_user_concurrent: float | None = None + tier: Tier | None = None + confidence: Confidence = Confidence.CONFIRMED + source_url: str | None = None + source_date: str | None = None + unit_label: str = "units" + note: str = "" + + def __post_init__(self) -> None: + if self.rate < 0: + raise ValueError(f"{self.key}: rate must not be negative") + if self.confidence is Confidence.CONFIRMED and not ( + self.source_url and self.source_date + ): + raise ValueError( + f"{self.key}: a confirmed (🟢) meter must carry a source URL and date" + ) + per_user = self.basis is MeterBasis.PER_USER_PER_MONTH + has_pair = ( + self.per_user_named is not None and self.per_user_concurrent is not None + ) + if per_user != has_pair: + raise ValueError( + f"{self.key}: per-user rates are required for " + f"{MeterBasis.PER_USER_PER_MONTH.value} and forbidden otherwise" + ) + if self.basis is MeterBasis.ZERO_RATED and self.rate != 0.0: + raise ValueError(f"{self.key}: a zero-rated meter must have rate 0.0") + + def tokens_per_user_month(self, licence: LicenceModel) -> float: + """Subscription tokens for one user for one month.""" + if self.basis is not MeterBasis.PER_USER_PER_MONTH: + raise ValueError(f"{self.key} is not a per-user meter") + rate = ( + self.per_user_named + if licence is LicenceModel.NAMED + else self.per_user_concurrent + ) + assert rate is not None # guaranteed by __post_init__ + return rate + + def tokens_for(self, units: float) -> float: + """Consumption tokens for ``units`` of activity. + + Voice-bot minutes are NOT priced here — they carry a per-call + round-up that needs call counts, so + :func:`genesyscalc.usage.voice_bot_tokens` owns that path. + """ + if self.basis in (MeterBasis.ZERO_RATED,): + return 0.0 + if self.basis is MeterBasis.PER_USER_PER_MONTH: + raise ValueError(f"{self.key}: use tokens_per_user_month()") + return units * self.rate + + +@dataclass(frozen=True) +class TokenPrice: + """What one token costs, list versus negotiated. + + ``contracted_rate`` (a signed per-token rate) wins outright when set. + Otherwise the concession is a percentage off list — the lever a + negotiation actually moves. Both are client data and arrive from the + notebook's ``engagement-data`` cell, never from the anchor. + """ + + currency: str = "USD" + list_rate: float = 1.00 + contracted_rate: float | None = None + concession_pct: float = 0.0 + + def __post_init__(self) -> None: + if self.list_rate <= 0: + raise ValueError("list_rate must be positive") + if not 0.0 <= self.concession_pct < 1.0: + raise ValueError("concession_pct must be in [0, 1)") + if self.contracted_rate is not None and self.contracted_rate < 0: + raise ValueError("contracted_rate must not be negative") + + def effective_rate(self) -> float: + """The rate actually billed per token.""" + if self.contracted_rate is not None: + return self.contracted_rate + return self.list_rate * (1.0 - self.concession_pct) diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/model.py b/calculators/Genesys_Token_Calculator/genesyscalc/model.py new file mode 100644 index 0000000..c805862 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/model.py @@ -0,0 +1,270 @@ +"""Orchestration — one call in, the whole costed picture out. + +The notebook assembles :class:`CalculatorInputs` from its ``engagement-data`` +cell and its widgets, calls :func:`calculate`, and renders the result. The +engine never invents a volume and never reads a widget. + +:attr:`CalculatorResult.warnings` is deliberate: when a published rule makes +a number smaller — Copilot covering summaries, the allowance absorbing a +month, deflection zeroing TTS — the result says so. A total that shrank for +a reason nobody can see is indistinguishable from a bug. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field, replace +from typing import Any + +from .billing import CostLine, CostTotals, cost_lines, total_cost +from .meters import LicenceModel, MeterBasis, TokenPrice +from .ratecard import meter +from .tts import TtsLine, TtsUsage, tts_cost +from .usage import ( + DeflectionMix, + VoiceBotUsage, + Volumes, + apply_copilot_covers_summary, + subscription_tokens, + virtual_agent_tokens, + voice_bot_billable_minutes, + voice_bot_roundup_uplift, + voice_bot_tokens, +) + +#: Meter keys whose units come straight off a channel volume. The notebook's +#: feature catalogue decides which are switched on; this maps each to the +#: volume that drives it. +_VOLUME_DRIVEN: dict[str, str] = { + "bots_digital": "digital_sessions_monthly", + "ai_summary_and_insights": "voice_total_monthly", + "ai_scoring": "evaluations_monthly", + "ai_translate": "translations_monthly", + "predictive_routing": "routed_interactions_monthly", + "genesys_cloud_copilot": "ai_actions_monthly", + "apple_messages_for_business": "messaging_monthly", + "facebook_messenger": "messaging_monthly", + "instagram_direct_messaging": "messaging_monthly", + "whatsapp_messaging": "messaging_monthly", + "x_direct_messaging": "messaging_monthly", + "genesys_cloud_social": "social_posts_monthly", + "social_post_responses": "social_responses_monthly", + "predictive_engagement": "all_interactions_monthly", + "knowledge_queries": "all_interactions_monthly", +} + +#: The per-user subscription meters, by licence-model face. +_PER_USER_KEYS: frozenset[str] = frozenset( + { + "agent_copilot_named", + "agent_copilot_concurrent", + "speech_and_text_analytics_named", + "speech_and_text_analytics_concurrent", + } +) + +#: Deflection-driven meters — priced from the highest-tier partition, not a +#: raw volume. +_TIER_KEYS: frozenset[str] = frozenset({"virtual_agent", "agentic_virtual_agent"}) + +#: The Copilot family, in either licence face. +_COPILOT_KEYS: frozenset[str] = frozenset( + {"agent_copilot_named", "agent_copilot_concurrent"} +) + + +def _volume_for(volumes: Volumes, attr: str) -> float: + return float(getattr(volumes, attr)) + + +@dataclass(frozen=True) +class CalculatorInputs: + """Everything the notebook supplies. Client data plus widget state.""" + + volumes: Volumes + users: int + licence: LicenceModel = LicenceModel.NAMED + enabled: frozenset[str] = frozenset() + mix: DeflectionMix = field(default_factory=DeflectionMix) + price: TokenPrice = field(default_factory=TokenPrice) + voice_bot: VoiceBotUsage | None = None + tts: TtsUsage | None = None + apply_allowance: bool = True + + def __post_init__(self) -> None: + if self.users < 0: + raise ValueError("users must not be negative") + unknown = sorted(k for k in self.enabled if k not in _known_keys()) + if unknown: + raise ValueError( + f"enabled contains keys that are not published meters: {unknown}" + ) + + @property + def copilot_enabled(self) -> bool: + return bool(self.enabled & _COPILOT_KEYS) + + def with_(self, **changes: Any) -> CalculatorInputs: + """A copy with fields replaced — for scenarios and sensitivity.""" + return replace(self, **changes) + + +def _known_keys() -> frozenset[str]: + from .ratecard import METERS + + return frozenset(METERS) + + +@dataclass(frozen=True) +class CalculatorResult: + """The costed picture, plus why any number shrank.""" + + lines: tuple[CostLine, ...] + totals: CostTotals + tts: TtsLine | None + grand_total_annual: float + warnings: tuple[str, ...] + + @property + def token_cost_annual(self) -> float: + return self.totals.token_cost_annual + + @property + def tts_cost_annual(self) -> float: + return self.tts.cost_annual if self.tts is not None else 0.0 + + +def metered_units(inputs: CalculatorInputs) -> dict[str, float]: + """Monthly metered units per enabled meter key, before the Copilot rule.""" + units: dict[str, float] = {} + + for key in sorted(inputs.enabled): + if key in _PER_USER_KEYS: + units[key] = float(inputs.users) + elif key in _TIER_KEYS: + share = ( + inputs.mix.virtual_agent_share + if key == "virtual_agent" + else inputs.mix.agentic_va_share + ) + units[key] = inputs.volumes.voice_total_monthly * share + elif key == "bots_voice": + units[key] = ( + voice_bot_billable_minutes(inputs.voice_bot) + if inputs.voice_bot is not None + else 0.0 + ) + elif key in _VOLUME_DRIVEN: + units[key] = _volume_for(inputs.volumes, _VOLUME_DRIVEN[key]) + else: # pragma: no cover — every published key is classified above + raise ValueError(f"{key} has no unit driver") + + return units + + +def calculate(inputs: CalculatorInputs) -> CalculatorResult: + """Price a scenario end to end.""" + raw_units = metered_units(inputs) + usage = apply_copilot_covers_summary(raw_units, inputs.copilot_enabled) + units = usage.units + warnings: list[str] = list(usage.notes) + + tokens: dict[str, float] = {} + consumption = 0.0 + + # Per-user subscription meters — exact, never rounded. + per_user_enabled = frozenset(inputs.enabled & _PER_USER_KEYS) + subs = subscription_tokens(inputs.users, per_user_enabled, inputs.licence) + tokens.update(subs) + subscription = sum(subs.values()) + + # Voice bots — minutes with the published per-call round-up. + if "bots_voice" in inputs.enabled and inputs.voice_bot is not None: + bot_tokens = voice_bot_tokens(inputs.voice_bot) + tokens["bots_voice"] = bot_tokens + consumption += bot_tokens + uplift = voice_bot_roundup_uplift(inputs.voice_bot) + if uplift > 0: + warnings.append( + f"Voice bots: the published 15-second per-call round-up adds " + f"{uplift:.1%} to billable bot minutes at a " + f"{inputs.voice_bot.avg_bot_seconds_per_call:.0f}s average." + ) + + # Virtual agents — the highest-tier partition of voice volume. + tier_tokens = virtual_agent_tokens( + inputs.volumes.voice_total_monthly, inputs.mix + ) + for key in sorted(_TIER_KEYS & inputs.enabled): + tokens[key] = tier_tokens.get(key, 0.0) + consumption += tokens[key] + + # Everything else — units × published rate. + for key in sorted(inputs.enabled): + if key in _PER_USER_KEYS or key in _TIER_KEYS or key == "bots_voice": + continue + m = meter(key) + value = m.tokens_for(units.get(key, 0.0)) + tokens[key] = value + consumption += value + if m.basis is MeterBasis.ZERO_RATED and units.get(key, 0.0) > 0: + warnings.append( + f"{m.feature}: {m.published_rate} — shown at $0 because " + "Genesys publishes it as included." + ) + + totals = total_cost( + consumption, subscription, inputs.licence, inputs.price, inputs.apply_allowance + ) + if inputs.apply_allowance and totals.billable_tokens_monthly == 0: + warnings.append( + f"The free monthly allowance ({totals.allowance_tokens_monthly} " + f"tokens, {inputs.licence.label.lower()} org) covers this month's " + "consumption entirely." + ) + + tts_line = ( + tts_cost(inputs.tts, inputs.mix, inputs.price.concession_pct) + if inputs.tts is not None + else None + ) + if tts_line is not None: + warnings.extend(tts_line.warnings) + + lines = cost_lines(tokens, units, inputs.price) + grand_total = totals.token_cost_annual + ( + tts_line.cost_annual if tts_line is not None else 0.0 + ) + + return CalculatorResult( + lines=lines, + totals=totals, + tts=tts_line, + grand_total_annual=grand_total, + warnings=tuple(warnings), + ) + + +def cost_per_interaction(result: CalculatorResult, volumes: Volumes) -> float: + """Annual grand total ÷ annual interactions — the number clients recall.""" + annual = volumes.all_interactions_monthly * 12 + if annual == 0: + return 0.0 + return result.grand_total_annual / annual + + +def compare(scenarios: dict[str, CalculatorInputs]) -> list[dict[str, Any]]: + """One row per scenario. Cost only — a calculator does not build a case.""" + rows: list[dict[str, Any]] = [] + for name, inputs in scenarios.items(): + result = calculate(inputs) + rows.append( + { + "Scenario": name, + "Billable tokens / mo": result.totals.billable_tokens_monthly, + "Token cost / yr": result.totals.token_cost_annual, + "TTS cost / yr": result.tts_cost_annual, + "Total / yr": result.grand_total_annual, + "$ / interaction": cost_per_interaction(result, inputs.volumes), + } + ) + return rows diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/ratecard.py b/calculators/Genesys_Token_Calculator/genesyscalc/ratecard.py new file mode 100644 index 0000000..61cc623 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/ratecard.py @@ -0,0 +1,358 @@ +"""THE VERBATIM ANCHOR — Genesys Cloud tokens model, as published. + +Transcribed from +https://help.genesys.cloud/articles/genesys-cloud-tokens-model/ +**last updated 2026-07-12**. Feature names and rate strings below are the +article's own wording, character for character. + +VERBATIM — DO NOT EDIT. A vendor republication is a deliberate +**re-anchoring**, not a patch (docs/Calculator_Pattern_V1-00.md): + + 1. diff the published table against this file + 2. update the meters AND bump RATE_CARD_SOURCE_DATE in the same edit + 3. update the pins in tests/test_rate_card.py in the same commit + 4. re-execute the notebook + 5. report the cost moves honestly + 6. Show-first — the user sees the rate diff before it lands + +Never partially re-anchor; never bump the date without the pins. + +Negotiated or contracted values NEVER land here. They are the overlay, and +they live in the notebook's ``engagement-data`` cell — this file is the +vendor's record, not the client's deal. +""" + +from __future__ import annotations + +from .meters import LicenceModel, Meter, MeterBasis, Tier + +RATE_CARD_SOURCE: str = "https://help.genesys.cloud/articles/genesys-cloud-tokens-model/" +RATE_CARD_SOURCE_DATE: str = "2026-07-12" + +# Every meter below is published on the same page on the same date, so the +# source pair is spelled out once here and passed explicitly. Explicit beats +# a splat: this file is diffed against the article by eye. +_URL = RATE_CARD_SOURCE +_DATE = RATE_CARD_SOURCE_DATE + + +#: The published meter table, verbatim. Twenty rows, in the article's order. +METERS_VERBATIM: tuple[Meter, ...] = ( + Meter( + key="bots_voice", + feature="Bots (Voice)", + published_rate="17 minutes per token", + basis=MeterBasis.VOICE_MINUTES, + rate=1.0 / 17.0, + tier=Tier.BOT, + unit_label="bot minutes", + note=( + "Genesys rounds up each call to the next 15-second increment " + "(clarified 2026-07-12)." + ), + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="bots_digital", + feature="Bots (Digital)", + published_rate="51 sessions per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 51.0, + tier=Tier.BOT, + unit_label="sessions", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="virtual_agent", + feature="Virtual Agent", + published_rate="0.5 tokens per Virtual Agent interaction", + basis=MeterBasis.TOKENS_PER_UNIT, + rate=0.5, + tier=Tier.VIRTUAL_AGENT, + unit_label="interactions", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="agentic_virtual_agent", + feature="Agentic Virtual Agent", + published_rate="1.2 tokens per interaction", + basis=MeterBasis.TOKENS_PER_UNIT, + rate=1.2, + tier=Tier.AGENTIC_VIRTUAL_AGENT, + unit_label="interactions", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="agent_copilot_named", + feature="Agent Copilot [named]", + published_rate="40 tokens per user", + basis=MeterBasis.PER_USER_PER_MONTH, + per_user_named=40.0, + per_user_concurrent=60.0, + unit_label="users", + note="Enabling Copilot makes Supervisor summaries and insights free.", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="agent_copilot_concurrent", + feature="Agent Copilot [concurrent]", + published_rate="60 tokens per user", + basis=MeterBasis.PER_USER_PER_MONTH, + per_user_named=40.0, + per_user_concurrent=60.0, + unit_label="users", + note="The concurrent-licence face of the same meter.", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="ai_scoring", + feature="AI Scoring", + published_rate="20 evaluations per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 20.0, + unit_label="evaluations", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="ai_translate", + feature="AI Translate", + published_rate="2 translations per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 2.0, + unit_label="translations", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="ai_summary_and_insights", + feature="AI Summary and Insights", + published_rate="50 summaries/insights per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 50.0, + unit_label="summaries", + note=( + "Free when Agent Copilot is enabled — see " + "COPILOT_COVERS_SUMMARY_RULE." + ), + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="apple_messages_for_business", + feature="Apple Messages for Business", + published_rate="400 inbound or outbound messages per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="facebook_messenger", + feature="Facebook Messenger", + published_rate="400 messages per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="instagram_direct_messaging", + feature="Instagram Direct Messaging", + published_rate="400 messages per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="whatsapp_messaging", + feature="WhatsApp Messaging", + published_rate="400 messages per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="x_direct_messaging", + feature="X Direct Messaging", + published_rate="400 messages per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="genesys_cloud_social", + feature="Genesys Cloud Social", + published_rate="400 social post ingestions per channel per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="post ingestions", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="social_post_responses", + feature="Social Post Responses", + published_rate="400 outbound messages per channel per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 400.0, + unit_label="outbound messages", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="predictive_routing", + feature="Predictive Routing", + published_rate="17 routes per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 17.0, + unit_label="routes", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="speech_and_text_analytics_named", + feature="Speech and Text Analytics [named]", + published_rate="30 tokens per user", + basis=MeterBasis.PER_USER_PER_MONTH, + per_user_named=30.0, + per_user_concurrent=45.0, + unit_label="users", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="speech_and_text_analytics_concurrent", + feature="Speech and Text Analytics [concurrent]", + published_rate="45 tokens per user", + basis=MeterBasis.PER_USER_PER_MONTH, + per_user_named=30.0, + per_user_concurrent=45.0, + unit_label="users", + note="The concurrent-licence face of the same meter.", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="genesys_cloud_copilot", + feature="Genesys Cloud Copilot", + published_rate="20 AI actions per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0 / 20.0, + unit_label="AI actions", + note="Genesys Cloud knowledge queries are not charged.", + source_url=_URL, + source_date=_DATE, + ), +) + + +#: Published as included at no token cost. Shown on stage as explicit $0 +#: lines — a zero the vendor put in writing is a finding, not an omission. +ZERO_RATED_VERBATIM: tuple[Meter, ...] = ( + Meter( + key="predictive_engagement", + feature="Predictive Engagement", + published_rate="No charge for token usage", + basis=MeterBasis.ZERO_RATED, + unit_label="interactions", + source_url=_URL, + source_date=_DATE, + ), + Meter( + key="knowledge_queries", + feature="Genesys Cloud knowledge queries", + published_rate="no charge for Genesys Cloud knowledge queries", + basis=MeterBasis.ZERO_RATED, + unit_label="queries", + source_url=_URL, + source_date=_DATE, + ), +) + + +#: "Named organizations receive 250 tokens; concurrent organizations receive +#: 350 tokens" per month. +FREE_TOKENS_PER_MONTH: dict[LicenceModel, int] = { + LicenceModel.NAMED: 250, + LicenceModel.CONCURRENT: 350, +} + +#: "These tokens renew each month and do not carry over to future months." +ALLOWANCE_CARRIES_OVER: bool = False + +#: "Genesys rounds up each call to the next 15-second increment." +#: Clarified in the 2026-07-12 revision. Applied PER CALL, then summed. +VOICE_BOT_ROUNDUP_SECONDS: int = 15 + +#: The published highest-tier rule, verbatim. Renders on stage. +HIGHEST_TIER_RULE: str = ( + "In cases where an interaction uses multiple AI resources, such as bot " + "flows, virtual agents, and agentic virtual agents, Genesys bases charges " + "on the highest tier (price) resource that Genesys Cloud uses during the " + "interaction." +) + +#: The published Copilot exclusion, verbatim. Renders on stage. +COPILOT_COVERS_SUMMARY_RULE: str = ( + "if you enable Agent Copilot simultaneously, then Supervisor Copilot " + "summaries and insights do not consume tokens" +) + +#: List price per token. The article points to the pricing hub and cites +#: "USD 1.00 to JPY 120 per token by currency". +LIST_PRICE_BY_CURRENCY: dict[str, float] = {"USD": 1.00, "JPY": 120.0} + + +#: Every meter by key — the consumption table plus the zero-rated lines. +METERS: dict[str, Meter] = { + m.key: m for m in METERS_VERBATIM + ZERO_RATED_VERBATIM +} + + +def meter(key: str) -> Meter: + """Look up a published meter, with a useful error when the key is wrong.""" + try: + return METERS[key] + except KeyError: + raise KeyError( + f"{key!r} is not a published Genesys meter. Known keys: " + f"{', '.join(sorted(METERS))}" + ) from None + + +def per_user_meter(licence: LicenceModel, family: str) -> Meter: + """The Copilot / STA meter face matching ``licence``. + + ``family`` is ``"agent_copilot"`` or ``"speech_and_text_analytics"``. + Both rates live on both faces, so this is presentation sugar — it picks + the row whose published wording matches the licence being modelled. + """ + return meter(f"{family}_{licence.value}") + + +def rate_card_rows() -> list[dict[str, str]]: + """The published table as display rows, in publication order.""" + return [ + { + "Feature": m.feature, + "Published rate": m.published_rate, + "Confidence": m.confidence.icon, + "Source": m.source_date or "", + } + for m in METERS_VERBATIM + ZERO_RATED_VERBATIM + ] diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/sensitivity.py b/calculators/Genesys_Token_Calculator/genesyscalc/sensitivity.py new file mode 100644 index 0000000..cccfa8d --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/sensitivity.py @@ -0,0 +1,133 @@ +"""One-at-a-time sweeps and tornado data. + +Data only — the notebook draws the figures. Drivers are addressed by a +short dotted path so the notebook can name them without reaching into the +dataclasses itself. +""" + +from __future__ import annotations + +from collections.abc import Sequence +from dataclasses import replace +from typing import Any + +from .model import CalculatorInputs, CalculatorResult, calculate + +#: Driver name → (container attribute on CalculatorInputs, field on it). +#: ``None`` container means the field sits directly on CalculatorInputs. +DRIVERS: dict[str, tuple[str | None, str]] = { + "token_price": ("price", "list_rate"), + "concession_pct": ("price", "concession_pct"), + "users": (None, "users"), + "voice_volume": ("volumes", "voice_inbound_monthly"), + "bot_share": ("mix", "bot_only_share"), + "virtual_agent_share": ("mix", "virtual_agent_share"), + "agentic_va_share": ("mix", "agentic_va_share"), + "bot_seconds": ("voice_bot", "avg_bot_seconds_per_call"), + "tts_chars_per_call": ("tts", "chars_per_call"), +} + + +def driver_value(inputs: CalculatorInputs, driver: str) -> float: + """The current value of ``driver`` in ``inputs``.""" + container, attr = _resolve(driver) + target = inputs if container is None else getattr(inputs, container) + if target is None: + raise ValueError(f"{driver} is not active in this scenario") + return float(getattr(target, attr)) + + +def set_driver(inputs: CalculatorInputs, driver: str, value: float) -> CalculatorInputs: + """A copy of ``inputs`` with ``driver`` set to ``value``.""" + container, attr = _resolve(driver) + if container is None: + return inputs.with_(**{attr: _coerce(attr, value)}) + current = getattr(inputs, container) + if current is None: + raise ValueError(f"{driver} is not active in this scenario") + return inputs.with_(**{container: replace(current, **{attr: _coerce(attr, value)})}) + + +def _resolve(driver: str) -> tuple[str | None, str]: + try: + return DRIVERS[driver] + except KeyError: + raise KeyError( + f"{driver!r} is not a known driver. Known: {', '.join(sorted(DRIVERS))}" + ) from None + + +def _coerce(attr: str, value: float) -> Any: + """``users`` and ``chars_per_call`` are integers by contract.""" + if attr in ("users", "chars_per_call"): + return int(round(value)) + return float(value) + + +def sweep( + base: CalculatorInputs, driver: str, values: Sequence[float] +) -> list[dict[str, float]]: + """Grand total across a range of one driver, everything else held.""" + rows: list[dict[str, float]] = [] + for value in values: + result: CalculatorResult = calculate(set_driver(base, driver, value)) + rows.append( + { + driver: float(value), + "token_cost_annual": result.totals.token_cost_annual, + "tts_cost_annual": result.tts_cost_annual, + "grand_total_annual": result.grand_total_annual, + } + ) + return rows + + +def tornado( + base: CalculatorInputs, drivers: Sequence[str], delta: float = 0.25 +) -> list[dict[str, Any]]: + """±``delta`` on each driver in turn, sorted by the swing it causes. + + Drivers that are not active in the scenario (no TTS, no voice bot) are + skipped rather than raising — a tornado over an inactive lever is noise. + Shares are clamped to keep the highest-tier partition legal; a driver + whose low and high both clamp to the same value contributes no swing. + """ + if not 0.0 < delta < 1.0: + raise ValueError("delta must be in (0, 1)") + + baseline = calculate(base).grand_total_annual + rows: list[dict[str, Any]] = [] + + for driver in drivers: + try: + current = driver_value(base, driver) + except (ValueError, KeyError): + continue + + low_value = current * (1.0 - delta) + high_value = current * (1.0 + delta) + if driver.endswith("_share"): + low_value = max(0.0, min(1.0, low_value)) + high_value = max(0.0, min(1.0, high_value)) + + try: + low = calculate(set_driver(base, driver, low_value)).grand_total_annual + high = calculate(set_driver(base, driver, high_value)).grand_total_annual + except ValueError: + # e.g. raising a share would break the partition — skip the lever + continue + + rows.append( + { + "driver": driver, + "baseline_value": current, + "low_value": low_value, + "high_value": high_value, + "low": low, + "high": high, + "baseline": baseline, + "swing": abs(high - low), + } + ) + + return sorted(rows, key=lambda r: float(r["swing"]), reverse=True) diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/staging.py b/calculators/Genesys_Token_Calculator/genesyscalc/staging.py new file mode 100644 index 0000000..fe78cd5 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/staging.py @@ -0,0 +1,61 @@ +""" +Stage vs backstage — is this notebook render stakeholder-facing? + +The Mercury app (3.2.x) is a hybrid Jupyter server: the SAME server (and +kernel pool) can serve both the client-facing app view and JupyterLab, so a +server-level signal cannot tell who is looking. The reliable, per-view +signal is the session name: the app runs every session against a shadow +copy named ``__mercury__.ipynb`` +(``mercury_app/handlers.py``), and the kernel sees that path in +``JPY_SESSION_NAME``. JupyterLab and nbconvert sessions carry the plain +notebook path (or no session name at all). + +``MERCURY_CONFIG_DIR`` is kept as a fallback: ``mercury --working-dir …`` +exports it into the server process and every kernel inherits it. It is a +server-level signal — on such a server even JupyterLab kernels carry it, so +diagnostics are then hidden in that Lab view too (hidden, never leaked; use +a separate ``jupyter lab`` for analysis, which is the normal workflow). It +is NOT set when mercury is launched without ``--working-dir``, which is why +it cannot be the primary signal. + +Diagnostics routed through :func:`backstage` / :func:`backstage_md` stay +visible in JupyterLab and land in the nbconvert exports (where the +machine-readable appendix must appear for LLM consumption) but never render +in the Mercury app. +""" + +from __future__ import annotations + +import os +from typing import Any + + +def on_stage() -> bool: + """True when this kernel renders the client-facing Mercury app view.""" + if "__mercury__" in os.getenv("JPY_SESSION_NAME", ""): + return True # the app's shadow-copy session + return os.getenv("MERCURY_CONFIG_DIR") is not None + + +def backstage(*args: object, **kwargs: Any) -> None: + """``print`` that renders only backstage (JupyterLab, nbconvert).""" + if not on_stage(): + print(*args, **kwargs) + + +def backstage_md(text: str) -> None: + """Markdown that renders only backstage (JupyterLab, nbconvert). + + Emitted as a ``text/markdown`` display, so nbconvert's markdown export + carries it verbatim — a stream ``print`` would be indented as a code + block. Falls back to ``print`` when IPython isn't importable (plain + pytest), where ``display`` itself already degrades to ``print``. + """ + if on_stage(): + return + try: + from IPython.display import Markdown, display + except ImportError: + print(text) + return + display(Markdown(text)) # type: ignore[no-untyped-call] diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/tts.py b/calculators/Genesys_Token_Calculator/genesyscalc/tts.py new file mode 100644 index 0000000..9c04652 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/tts.py @@ -0,0 +1,147 @@ +"""Genesys Enhanced text-to-speech — billed in dollars, NOT in tokens. + +A separate published page, a separate meter, a separate subtotal: +https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/ +**last updated 2026-05-22**. + +TTS never appears in the tokens-model article and is never converted to +tokens here. It is carried because Genesys AI spend is understated without +it — but it is reported as its own line, and the grand total shows tokens +and TTS as two labelled components rather than one blended figure. + +Three published facts the obvious model misses: + +* **"The monthly billing unit is one million characters, rounded up."** + 200,000 characters bills as $5; 1,000,005 characters bills as $10. +* **Free during virtual agents.** "Genesys Enhanced TTS is included free of + charge when you use a virtual agent or agentic virtual agent during the + interaction" — so deflection cuts this line as well as the token line. +* **Standard voices are ending.** End of sale 2026-03-29, end of support + 2026-08-05. Selecting Standard for a future-state model is a finding. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass + +from .meters import Confidence +from .usage import DeflectionMix + +TTS_SOURCE: str = "https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/" +TTS_SOURCE_DATE: str = "2026-05-22" + +#: "$20 per one million TTS characters" (advanced: NTTS, neural, wavenet) +#: and "$5 per one million TTS characters" (standard). +TTS_PRICE_PER_MILLION_CHARS: dict[str, float] = {"advanced": 20.0, "standard": 5.0} + +#: "The monthly billing unit is one million characters, rounded up." +TTS_BILLING_UNIT_CHARS: int = 1_000_000 + +#: Standard Enhanced TTS lifecycle — published end-of-sale / end-of-support. +TTS_STANDARD_END_OF_SALE: str = "2026-03-29" +TTS_STANDARD_END_OF_SUPPORT: str = "2026-08-05" + +#: Published, so 🟢 — unlike the tokens article, this page states the rates. +TTS_CONFIDENCE: Confidence = Confidence.CONFIRMED + +MONTHS_PER_YEAR: int = 12 + + +@dataclass(frozen=True) +class TtsUsage: + """Synthesised speech volume. Client data, from ``engagement-data``. + + "Usage includes all TTS characters, including letters, numbers, + punctuation, and spaces." + """ + + calls_monthly: float + chars_per_call: int = 2_000 + tier: str = "advanced" + + def __post_init__(self) -> None: + if self.calls_monthly < 0: + raise ValueError("calls_monthly must not be negative") + if self.chars_per_call < 0: + raise ValueError("chars_per_call must not be negative") + if self.tier not in TTS_PRICE_PER_MILLION_CHARS: + raise ValueError( + f"unknown TTS tier {self.tier!r}; known: " + f"{', '.join(sorted(TTS_PRICE_PER_MILLION_CHARS))}" + ) + + +@dataclass(frozen=True) +class TtsLine: + """The TTS bill, reported separately from the token bill.""" + + tier: str + rate_per_million_chars: float + total_chars_monthly: float + free_share: float + free_chars_monthly: float + billable_chars_monthly: float + billed_millions_monthly: int + cost_monthly: float + cost_annual: float + confidence: Confidence + source_url: str + source_date: str + warnings: tuple[str, ...] = () + + +def tts_free_share(mix: DeflectionMix) -> float: + """Share of TTS characters Genesys does not charge for. + + "included free of charge when you use a virtual agent or agentic virtual + agent during the interaction". Reuses the same partition as the token + model, so the two lines cannot disagree about how much was deflected. + """ + return min(1.0, mix.virtual_agent_share + mix.agentic_va_share) + + +def tts_cost( + usage: TtsUsage, mix: DeflectionMix | None = None, concession_pct: float = 0.0 +) -> TtsLine: + """The monthly and annual TTS bill, with the published per-1M round-up. + + The round-up lands on the *billable* character total each month, after + the virtual-agent free share is removed. + """ + if not 0.0 <= concession_pct < 1.0: + raise ValueError("concession_pct must be in [0, 1)") + + free_share = tts_free_share(mix) if mix is not None else 0.0 + total_chars = usage.calls_monthly * usage.chars_per_call + free_chars = total_chars * free_share + billable_chars = total_chars - free_chars + + billed_millions = math.ceil(billable_chars / TTS_BILLING_UNIT_CHARS) + rate = TTS_PRICE_PER_MILLION_CHARS[usage.tier] + monthly = billed_millions * rate * (1.0 - concession_pct) + + warnings: list[str] = [] + if usage.tier == "standard": + warnings.append( + "Standard Enhanced TTS reached end of sale on " + f"{TTS_STANDARD_END_OF_SALE} and end of support on " + f"{TTS_STANDARD_END_OF_SUPPORT} — price a future state on " + "advanced voices." + ) + + return TtsLine( + tier=usage.tier, + rate_per_million_chars=rate, + total_chars_monthly=total_chars, + free_share=free_share, + free_chars_monthly=free_chars, + billable_chars_monthly=billable_chars, + billed_millions_monthly=billed_millions, + cost_monthly=monthly, + cost_annual=monthly * MONTHS_PER_YEAR, + confidence=TTS_CONFIDENCE, + source_url=TTS_SOURCE, + source_date=TTS_SOURCE_DATE, + warnings=tuple(warnings), + ) diff --git a/calculators/Genesys_Token_Calculator/genesyscalc/usage.py b/calculators/Genesys_Token_Calculator/genesyscalc/usage.py new file mode 100644 index 0000000..c55e7f0 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/genesyscalc/usage.py @@ -0,0 +1,262 @@ +"""Volumes → metered units → tokens. + +Three published rules are modelled here that the obvious arithmetic gets +wrong. Each one is traceable to a sentence in the tokens-model article: + +1. **The 15-second per-call round-up.** "Genesys rounds up each call to the + next 15-second increment." Applied per call and *then* summed — averaging + first and rounding once understates the bill, badly, on short bot touches. + +2. **The highest-tier rule.** "Genesys bases charges on the highest tier + (price) resource that Genesys Cloud uses during the interaction." So the + AI-resource shares of a volume are a **partition**, not overlapping slices: + an interaction is charged once, at the highest tier it touched. + +3. **Copilot covers Supervisor summaries.** "if you enable Agent Copilot + simultaneously, then Supervisor Copilot summaries and insights do not + consume tokens." Billing AI Summary while Copilot is on double-charges. +""" + +from __future__ import annotations + +import math +from dataclasses import dataclass, field + +from .meters import LicenceModel, MeterBasis, Tier +from .ratecard import VOICE_BOT_ROUNDUP_SECONDS, meter + +#: Meters covered by the Copilot exclusion when Agent Copilot is enabled. +COPILOT_COVERED_METERS: frozenset[str] = frozenset({"ai_summary_and_insights"}) + +#: Which per-interaction meter prices each tier of the partition. +TIER_METER: dict[Tier, str] = { + Tier.VIRTUAL_AGENT: "virtual_agent", + Tier.AGENTIC_VIRTUAL_AGENT: "agentic_virtual_agent", +} + + +@dataclass(frozen=True) +class VoiceBotUsage: + """Voice-bot activity, in the shape the round-up needs. + + The round-up is per *call*, so a call count is required — an aggregate + minute total cannot be billed correctly. + """ + + calls_per_month: float + avg_bot_seconds_per_call: float + + def __post_init__(self) -> None: + if self.calls_per_month < 0: + raise ValueError("calls_per_month must not be negative") + if self.avg_bot_seconds_per_call < 0: + raise ValueError("avg_bot_seconds_per_call must not be negative") + + +@dataclass(frozen=True) +class DeflectionMix: + """Shares of interactions by the **highest tier** each one touched. + + This is a partition, not a set of overlapping rates. ``bot_only_share`` + is the share whose highest resource was a bot flow; an interaction that + escalated bot → virtual agent counts once, in ``virtual_agent_share``. + The shares therefore sum to at most 1.0 — the remainder went straight to + an agent and consumed no AI-resource token. + + Modelling these as independent percentages (and summing the tokens) is + the over-billing bug the published rule exists to prevent. + """ + + bot_only_share: float = 0.0 + virtual_agent_share: float = 0.0 + agentic_va_share: float = 0.0 + + def __post_init__(self) -> None: + for name, value in ( + ("bot_only_share", self.bot_only_share), + ("virtual_agent_share", self.virtual_agent_share), + ("agentic_va_share", self.agentic_va_share), + ): + if not 0.0 <= value <= 1.0: + raise ValueError(f"{name} must be in [0, 1], got {value}") + if self.total_deflected_share > 1.0 + 1e-9: + raise ValueError( + "tier shares must sum to at most 1.0 — they partition the " + "volume by highest tier touched, they do not overlap " + f"(got {self.total_deflected_share})" + ) + + @property + def total_deflected_share(self) -> float: + """Share of interactions that touched any AI resource.""" + return self.bot_only_share + self.virtual_agent_share + self.agentic_va_share + + @property + def agent_handled_share(self) -> float: + """Share that reached a human without touching an AI resource.""" + return 1.0 - self.total_deflected_share + + +@dataclass(frozen=True) +class Volumes: + """Monthly interaction volumes by channel. Client data — from the + notebook's ``engagement-data`` cell, never invented by the engine.""" + + voice_inbound_monthly: float = 0.0 + voice_outbound_monthly: float = 0.0 + digital_sessions_monthly: float = 0.0 + email_monthly: float = 0.0 + messaging_monthly: float = 0.0 + social_posts_monthly: float = 0.0 + social_responses_monthly: float = 0.0 + translations_monthly: float = 0.0 + evaluations_monthly: float = 0.0 + ai_actions_monthly: float = 0.0 + routed_interactions_monthly: float = 0.0 + + def __post_init__(self) -> None: + for name, value in vars(self).items(): + if value < 0: + raise ValueError(f"{name} must not be negative") + + @property + def voice_total_monthly(self) -> float: + return self.voice_inbound_monthly + self.voice_outbound_monthly + + @property + def all_interactions_monthly(self) -> float: + return ( + self.voice_total_monthly + + self.digital_sessions_monthly + + self.email_monthly + + self.messaging_monthly + ) + + +def voice_bot_billable_minutes( + usage: VoiceBotUsage, roundup_seconds: int = VOICE_BOT_ROUNDUP_SECONDS +) -> float: + """Billable bot minutes per month, with the published per-call round-up. + + "Genesys rounds up each call to the next 15-second increment." The + round-up lands on every call, so it is applied first and summed after:: + + per_call = ceil(seconds / 15) * 15 / 60 + total = per_call * calls + + A 40-second average bills as 45 seconds — 0.75 min/call, not 0.667. The + penalty is worst on short touches: a 5-second bot greeting bills as 15 + seconds, three times its duration. + """ + if roundup_seconds <= 0: + raise ValueError("roundup_seconds must be positive") + increments = math.ceil(usage.avg_bot_seconds_per_call / roundup_seconds) + billable_seconds_per_call = increments * roundup_seconds + return billable_seconds_per_call / 60.0 * usage.calls_per_month + + +def voice_bot_roundup_uplift( + usage: VoiceBotUsage, roundup_seconds: int = VOICE_BOT_ROUNDUP_SECONDS +) -> float: + """How much the round-up adds, as a fraction of the raw duration. + + The number worth saying out loud on stage: at a 40-second average it is + 0.125 (12.5% more bot cost than the raw minutes suggest). + """ + raw_minutes = usage.avg_bot_seconds_per_call / 60.0 * usage.calls_per_month + if raw_minutes == 0: + return 0.0 + return voice_bot_billable_minutes(usage, roundup_seconds) / raw_minutes - 1.0 + + +def voice_bot_tokens( + usage: VoiceBotUsage, roundup_seconds: int = VOICE_BOT_ROUNDUP_SECONDS +) -> float: + """Voice-bot tokens per month: billable minutes ÷ 17.""" + minutes = voice_bot_billable_minutes(usage, roundup_seconds) + return minutes * meter("bots_voice").rate + + +def tier_interactions(volume: float, mix: DeflectionMix) -> dict[Tier, float]: + """Split ``volume`` across tiers by highest resource touched. + + A partition — the returned values sum to at most ``volume``, so no + interaction can be billed twice. + """ + if volume < 0: + raise ValueError("volume must not be negative") + return { + Tier.BOT: volume * mix.bot_only_share, + Tier.VIRTUAL_AGENT: volume * mix.virtual_agent_share, + Tier.AGENTIC_VIRTUAL_AGENT: volume * mix.agentic_va_share, + } + + +def virtual_agent_tokens(volume: float, mix: DeflectionMix) -> dict[str, float]: + """Tokens for the VA and Agentic VA tiers of a deflection partition. + + The bot tier is deliberately absent: voice bots are metered in minutes + with a per-call round-up, so :func:`voice_bot_tokens` owns that line. + """ + split = tier_interactions(volume, mix) + return { + TIER_METER[tier]: meter(TIER_METER[tier]).tokens_for(interactions) + for tier, interactions in split.items() + if tier in TIER_METER + } + + +@dataclass(frozen=True) +class FeatureUsage: + """Metered units per meter key, plus the notes explaining any zeroing.""" + + units: dict[str, float] = field(default_factory=dict) + notes: tuple[str, ...] = () + + def get(self, key: str) -> float: + return self.units.get(key, 0.0) + + +def apply_copilot_covers_summary( + units: dict[str, float], copilot_enabled: bool +) -> FeatureUsage: + """Zero the summary meters when Agent Copilot is enabled. + + "if you enable Agent Copilot simultaneously, then Supervisor Copilot + summaries and insights do not consume tokens." The zeroing is reported, + not silent — a number that shrank for a published reason should say so. + """ + if not copilot_enabled: + return FeatureUsage(units=dict(units)) + + adjusted = dict(units) + notes: list[str] = [] + for key in sorted(COPILOT_COVERED_METERS): + covered = adjusted.get(key, 0.0) + if covered > 0: + adjusted[key] = 0.0 + notes.append( + f"{meter(key).feature}: {covered:,.0f} units not billed — " + "Agent Copilot is enabled, and Genesys does not charge " + "Supervisor summaries and insights alongside it." + ) + return FeatureUsage(units=adjusted, notes=tuple(notes)) + + +def subscription_tokens( + users: int, enabled_per_user_keys: frozenset[str], licence: LicenceModel +) -> dict[str, float]: + """Per-user subscription tokens per month, by meter key. + + Exact — per-user totals are not rounded; only consumption is (see + :mod:`genesyscalc.billing`). + """ + if users < 0: + raise ValueError("users must not be negative") + out: dict[str, float] = {} + for key in sorted(enabled_per_user_keys): + m = meter(key) + if m.basis is not MeterBasis.PER_USER_PER_MONTH: + raise ValueError(f"{key} is not a per-user meter") + out[key] = users * m.tokens_per_user_month(licence) + return out diff --git a/calculators/Genesys_Token_Calculator/notebooks/genesys_token_calculator.ipynb b/calculators/Genesys_Token_Calculator/notebooks/genesys_token_calculator.ipynb new file mode 100644 index 0000000..e37baff --- /dev/null +++ b/calculators/Genesys_Token_Calculator/notebooks/genesys_token_calculator.ipynb @@ -0,0 +1,7300 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "af04c70c", + "metadata": {}, + "source": [ + "# Genesys Cloud AI — Token Calculator\n", + "\n", + "What the AI features cost to run, from Genesys' **published** rate card.\n", + "\n", + "Meters are the vendor's own, transcribed verbatim from the\n", + "[Genesys Cloud tokens model](https://help.genesys.cloud/articles/genesys-cloud-tokens-model/)\n", + "(last updated **2026-07-12**). Enhanced text-to-speech is priced separately,\n", + "from its own published page (2026-05-22) — it is billed per character, not in\n", + "tokens, and is reported as its own line.\n", + "\n", + "A **planning model**: list rates unless a contracted rate is entered, and never\n", + "a quote. Volumes and headcount below are illustrative placeholders." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "6de9ea36", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:30.694898Z", + "iopub.status.busy": "2026-08-02T20:41:30.694639Z", + "iopub.status.idle": "2026-08-02T20:41:31.127241Z", + "shell.execute_reply": "2026-08-02T20:41:31.124499Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [], + "source": [ + "# ── Setup ──────────────────────────────────────────────────────────\n", + "import pathlib\n", + "import sys\n", + "\n", + "_ROOT = pathlib.Path.cwd()\n", + "if not (_ROOT / \"genesyscalc\").exists(): # notebook lives in notebooks/\n", + " _ROOT = _ROOT.parent\n", + "sys.path.insert(0, str(_ROOT))\n", + "\n", + "import json as _json\n", + "\n", + "# NOTE: `dataclass` is deliberately NOT imported here — the topic-bank\n", + "# cell imports its own, because tests exec that cell by tag with no\n", + "# setup cell present. Self-containment beats DRY for content cells.\n", + "import mercury as mr\n", + "import pandas as pd\n", + "import plotly.graph_objects as go\n", + "\n", + "# The engine: schema + published anchor + arithmetic. The CONTENT (the\n", + "# feature catalogue) lives in the tagged topic-bank cell below, and the\n", + "# CLIENT DATA in the engagement-data cell — edit them there, never here.\n", + "from genesyscalc import (\n", + " COPILOT_COVERS_SUMMARY_RULE,\n", + " DRIVERS,\n", + " FREE_TOKENS_PER_MONTH,\n", + " HIGHEST_TIER_RULE,\n", + " LIST_PRICE_BY_CURRENCY,\n", + " METERS,\n", + " RATE_CARD_SOURCE,\n", + " RATE_CARD_SOURCE_DATE,\n", + " TTS_SOURCE,\n", + " TTS_SOURCE_DATE,\n", + " VOICE_BOT_ROUNDUP_SECONDS,\n", + " CalculatorInputs,\n", + " DeflectionMix,\n", + " LicenceModel,\n", + " TokenPrice,\n", + " TtsUsage,\n", + " VoiceBotUsage,\n", + " Volumes,\n", + " backstage,\n", + " backstage_md,\n", + " calculate,\n", + " compare,\n", + " cost_per_interaction,\n", + " meter,\n", + " rate_card_rows,\n", + " result_json,\n", + " tornado,\n", + ")\n", + "from IPython.display import HTML, Markdown, display\n", + "\n", + "# ── Brand (docs/brand.md) — one palette, used by every figure ────────\n", + "INK, MUTED, LINE = \"#2e404d\", \"#586671\", \"#d5d9db\"\n", + "BLUE, NAVY, GREEN, AMBER = \"#0072bc\", \"#151d2c\", \"#00cb5d\", \"#ffc400\"\n", + "SURFACE = \"#ffffff\"\n", + "BODY_FONT = \"Arial, 'Helvetica Neue', Helvetica, sans-serif\"\n", + "HEAD_FONT = \"Georgia, 'Times New Roman', Times, serif\"\n", + "\n", + "# Colour follows the entity across every figure.\n", + "TOKEN_COLOR, TTS_COLOR = BLUE, AMBER\n", + "\n", + "\n", + "def money(x, dp=0):\n", + " \"\"\"House money format — plain text.\"\"\"\n", + " return f\"${x:,.{dp}f}\"\n", + "\n", + "\n", + "def html_money(x, dp=0):\n", + " \"\"\"Money for plotly annotations: a bare $ pair triggers MathJax.\"\"\"\n", + " return f\"${x:,.{dp}f}\"\n", + "\n", + "\n", + "def chart_layout(fig, title, height=380):\n", + " \"\"\"Title top-left, recessive grid, ink text, house fonts.\"\"\"\n", + " fig.update_layout(\n", + " title=dict(text=title, x=0, xanchor=\"left\",\n", + " font=dict(family=HEAD_FONT, size=17, color=INK)),\n", + " font=dict(family=BODY_FONT, size=13, color=INK),\n", + " paper_bgcolor=SURFACE, plot_bgcolor=SURFACE,\n", + " margin=dict(l=8, r=8, t=52, b=8), height=height,\n", + " xaxis=dict(gridcolor=LINE, zerolinecolor=LINE),\n", + " yaxis=dict(gridcolor=LINE, zerolinecolor=LINE),\n", + " legend=dict(orientation=\"h\", y=-0.16, x=0),\n", + " )\n", + " return fig\n", + "\n", + "\n", + "def styled(df, **fmt):\n", + " \"\"\"A small table in house style.\"\"\"\n", + " return (df.style.format(fmt, na_rep=\"—\")\n", + " .hide(axis=\"index\")\n", + " .set_table_styles([\n", + " {\"selector\": \"th\",\n", + " \"props\": [(\"font-family\", BODY_FONT), (\"font-size\", \"13px\"),\n", + " (\"color\", MUTED), (\"text-align\", \"left\"),\n", + " (\"border-bottom\", f\"1px solid {LINE}\")]},\n", + " {\"selector\": \"td\",\n", + " \"props\": [(\"font-family\", BODY_FONT), (\"font-size\", \"13px\"),\n", + " (\"color\", INK), (\"border-bottom\", f\"1px solid {LINE}\")]},\n", + " ]))" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9fa4f1ed", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.129542Z", + "iopub.status.busy": "2026-08-02T20:41:31.129269Z", + "iopub.status.idle": "2026-08-02T20:41:31.135010Z", + "shell.execute_reply": "2026-08-02T20:41:31.134409Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "**How to drive this calculator** *(backstage — the client never sees this)*\n", + "\n", + "- **Sidebar controls** — licence model, currency, token price and concession,\n", + " the three deflection shares, average bot seconds, and per-feature enables.\n", + " Mercury re-runs only the cells BELOW the widget cell, so every `.value` is\n", + " read one cell down — never in the defining cell.\n", + "- **Where the numbers come from** — the meters are the vendor's published rate\n", + " card in `genesyscalc/ratecard.py`, immutable and pinned by\n", + " `tests/test_rate_card.py`. Volumes, headcount and commercial terms are\n", + " yours: edit the **engagement-data** cell. The feature catalogue (titles, the\n", + " questions to ask) is the **topic-bank** cell.\n", + "- **Confidence legend** — 🟢 confirmed (published, with a source date) ·\n", + " 🟡 estimated (a stated working assumption) · 🔴 unknown (flagged, and bounded\n", + " by sensitivity if it is material).\n", + "- **Three published rules do the work** the obvious arithmetic gets wrong: the\n", + " 15-second per-call round-up on voice bots, the free monthly allowance\n", + " (250 named / 350 concurrent, no carry-over), and Copilot covering Supervisor\n", + " summaries. Each is enforced in the engine and reported in the warnings.\n", + "- **Re-anchoring** — when Genesys republishes, that is a deliberate act:\n", + " diff the table, update the anchor AND its source date, update the test pins\n", + " in the same commit, re-execute, and report the cost moves. Never bump the\n", + " date without the pins.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Consultant run-book — renders backstage only, never on the stage ─\n", + "backstage_md(\"\"\"\\\n", + "**How to drive this calculator** *(backstage — the client never sees this)*\n", + "\n", + "- **Sidebar controls** — licence model, currency, token price and concession,\n", + " the three deflection shares, average bot seconds, and per-feature enables.\n", + " Mercury re-runs only the cells BELOW the widget cell, so every `.value` is\n", + " read one cell down — never in the defining cell.\n", + "- **Where the numbers come from** — the meters are the vendor's published rate\n", + " card in `genesyscalc/ratecard.py`, immutable and pinned by\n", + " `tests/test_rate_card.py`. Volumes, headcount and commercial terms are\n", + " yours: edit the **engagement-data** cell. The feature catalogue (titles, the\n", + " questions to ask) is the **topic-bank** cell.\n", + "- **Confidence legend** — 🟢 confirmed (published, with a source date) ·\n", + " 🟡 estimated (a stated working assumption) · 🔴 unknown (flagged, and bounded\n", + " by sensitivity if it is material).\n", + "- **Three published rules do the work** the obvious arithmetic gets wrong: the\n", + " 15-second per-call round-up on voice bots, the free monthly allowance\n", + " (250 named / 350 concurrent, no carry-over), and Copilot covering Supervisor\n", + " summaries. Each is enforced in the engine and reported in the warnings.\n", + "- **Re-anchoring** — when Genesys republishes, that is a deliberate act:\n", + " diff the table, update the anchor AND its source date, update the test pins\n", + " in the same commit, re-execute, and report the cost moves. Never bump the\n", + " date without the pins.\n", + "\"\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "89a4d0a9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.136801Z", + "iopub.status.busy": "2026-08-02T20:41:31.136615Z", + "iopub.status.idle": "2026-08-02T20:41:31.144519Z", + "shell.execute_reply": "2026-08-02T20:41:31.143925Z" + }, + "tags": [ + "topic-bank" + ] + }, + "outputs": [], + "source": [ + "# ── Feature catalogue — THE content: edit HERE, in this cell ─────────\n", + "# What each priced feature IS, in client language, and what you need to ask\n", + "# to size it. The RATES are not here: they are the vendor's published record\n", + "# and live in genesyscalc/ratecard.py as an immutable anchor. This cell owns\n", + "# the words; that file owns the numbers.\n", + "#\n", + "# Rules:\n", + "# * `key` MUST match a published meter key in genesyscalc.ratecard.METERS —\n", + "# a test pins this, because a rename here silently unprices a feature.\n", + "# * NEVER rename a key casually: widget labels, notes and the JSON export\n", + "# all key off it.\n", + "# * Keep this cell self-contained: tests read it by its \"topic-bank\" tag and\n", + "# exec it with no setup cell, so it imports what it needs (and no magics).\n", + "from dataclasses import dataclass\n", + "\n", + "\n", + "@dataclass(frozen=True)\n", + "class Feature:\n", + " \"\"\"One priced capability, as the client hears about it.\"\"\"\n", + " key: str # must exist in genesyscalc.ratecard.METERS\n", + " title: str # client-facing name\n", + " description: str # one line: what it does for them\n", + " ask: str # the sizing question to put to the client\n", + " default_on: bool = False\n", + "\n", + "\n", + "FEATURES: tuple[Feature, ...] = (\n", + " Feature(\n", + " key=\"agent_copilot_named\",\n", + " title=\"Agent Copilot\",\n", + " description=(\n", + " \"Live in-call guidance for agents — suggested answers, knowledge \"\n", + " \"surfacing, and automatic wrap-up notes.\"\n", + " ),\n", + " ask=\"How many agents would be licensed for Copilot?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"speech_and_text_analytics_named\",\n", + " title=\"Speech & Text Analytics\",\n", + " description=(\n", + " \"Transcription, sentiment and topic detection across recorded \"\n", + " \"interactions. Prerequisite for AI Scoring.\"\n", + " ),\n", + " ask=\"How many agents' interactions would be analysed?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"bots_voice\",\n", + " title=\"Voice Bot\",\n", + " description=(\n", + " \"Self-service in the voice channel — the caller is handled by an \"\n", + " \"automated flow before, or instead of, an agent.\"\n", + " ),\n", + " ask=(\n", + " \"What share of inbound calls reach a bot, and how long does the \"\n", + " \"bot hold them? (Billed per call, rounded up to 15 seconds.)\"\n", + " ),\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"virtual_agent\",\n", + " title=\"Virtual Agent\",\n", + " description=(\n", + " \"A conversational agent that resolves an interaction end to end \"\n", + " \"rather than routing it.\"\n", + " ),\n", + " ask=\"What share of interactions would a virtual agent fully handle?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"agentic_virtual_agent\",\n", + " title=\"Agentic Virtual Agent\",\n", + " description=(\n", + " \"A virtual agent that also takes actions in back-end systems — \"\n", + " \"the highest-tier AI resource, and the most expensive per \"\n", + " \"interaction.\"\n", + " ),\n", + " ask=\"What share of interactions need actions taken, not just answers?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"ai_summary_and_insights\",\n", + " title=\"AI Summary & Insights\",\n", + " description=(\n", + " \"Automatic interaction summaries for supervisors. Free while \"\n", + " \"Agent Copilot is enabled — the calculator enforces that.\"\n", + " ),\n", + " ask=\"How many interactions would be summarised?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"ai_scoring\",\n", + " title=\"AI Scoring\",\n", + " description=(\n", + " \"Automated quality evaluation of interactions against a scorecard. \"\n", + " \"Needs Speech & Text Analytics to be useful.\"\n", + " ),\n", + " ask=\"How many evaluations per month would be scored automatically?\",\n", + " default_on=True,\n", + " ),\n", + " Feature(\n", + " key=\"bots_digital\",\n", + " title=\"Digital Bot\",\n", + " description=\"Self-service in web chat and messaging channels.\",\n", + " ask=\"How many digital self-service sessions per month?\",\n", + " default_on=False,\n", + " ),\n", + " Feature(\n", + " key=\"ai_translate\",\n", + " title=\"AI Translate\",\n", + " description=(\n", + " \"Real-time translation of interactions, so an agent can serve a \"\n", + " \"language they do not speak.\"\n", + " ),\n", + " ask=\"How many interactions per month need translation?\",\n", + " default_on=False,\n", + " ),\n", + " Feature(\n", + " key=\"predictive_routing\",\n", + " title=\"Predictive Routing\",\n", + " description=(\n", + " \"Routes each interaction to the agent predicted to handle it best. \"\n", + " \"Priced per route, so queue design drives the bill.\"\n", + " ),\n", + " ask=\"How many interactions per month would be routed predictively?\",\n", + " default_on=False,\n", + " ),\n", + " Feature(\n", + " key=\"whatsapp_messaging\",\n", + " title=\"WhatsApp Messaging\",\n", + " description=(\n", + " \"Inbound and outbound WhatsApp, metered per message in either \"\n", + " \"direction. The other direct-messaging channels bill identically.\"\n", + " ),\n", + " ask=\"How many WhatsApp messages per month, inbound and outbound?\",\n", + " default_on=False,\n", + " ),\n", + " Feature(\n", + " key=\"genesys_cloud_copilot\",\n", + " title=\"Genesys Cloud Copilot\",\n", + " description=(\n", + " \"AI actions inside the Genesys admin and supervisor experience. \"\n", + " \"Knowledge queries are not charged.\"\n", + " ),\n", + " ask=\"How many AI actions per month would admins and supervisors run?\",\n", + " default_on=False,\n", + " ),\n", + " Feature(\n", + " key=\"predictive_engagement\",\n", + " title=\"Predictive Engagement\",\n", + " description=(\n", + " \"Web-journey scoring and proactive chat offers. Genesys publishes \"\n", + " \"this as carrying no token charge.\"\n", + " ),\n", + " ask=\"Is web engagement in scope? (No token cost either way.)\",\n", + " default_on=True,\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b2aead98", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.146190Z", + "iopub.status.busy": "2026-08-02T20:41:31.146000Z", + "iopub.status.idle": "2026-08-02T20:41:31.149854Z", + "shell.execute_reply": "2026-08-02T20:41:31.149250Z" + }, + "tags": [ + "engagement-data" + ] + }, + "outputs": [], + "source": [ + "# ── Engagement data — EDIT PER ENGAGEMENT (tagged: engagement-data) ──\n", + "# Client facts live HERE, in the notebook — never in a .py file. In the\n", + "# MASTER these are ILLUSTRATIVE PLACEHOLDERS: round numbers, no client.\n", + "# Fill them in the engagement copy (copy the directory out, rename it\n", + "# YYYYMM_Client_Genesys_Token_Calculator, fresh venv, then edit this cell).\n", + "#\n", + "# The published rate card is NOT here — it is the vendor's record, not the\n", + "# client's deal. Only the NEGOTIATED terms belong in this cell.\n", + "# Keep it self-contained (no imports, no magics): tests exec it by tag.\n", + "ENGAGEMENT: dict = {\n", + " # ── identity ────────────────────────────────────────────────────\n", + " \"client\": \"\", # e.g. \"Acme Corp\" — blank in the master\n", + " \"prepared_date\": \"\", # e.g. \"2026-08-14\"\n", + " \"prepared_by\": \"\", # e.g. \"A. Consultant\"\n", + "\n", + " # ── volumes, per month 🟡 placeholders ──────────────────────────\n", + " \"voice_inbound_monthly\": 100_000,\n", + " \"voice_outbound_monthly\": 20_000,\n", + " \"digital_sessions_monthly\": 8_000,\n", + " \"email_monthly\": 12_000,\n", + " \"messaging_monthly\": 6_000,\n", + " \"social_posts_monthly\": 0,\n", + " \"social_responses_monthly\": 0,\n", + " \"translations_monthly\": 0,\n", + " \"evaluations_monthly\": 2_000,\n", + " \"ai_actions_monthly\": 0,\n", + " \"routed_interactions_monthly\": 0,\n", + "\n", + " # ── headcount and licence model 🟡 ──────────────────────────────\n", + " \"agent_users\": 500,\n", + " \"licence_model\": \"named\", # \"named\" | \"concurrent\"\n", + "\n", + " # ── commercial terms — the overlay on the published list price ──\n", + " \"currency\": \"USD\",\n", + " \"contracted_rate_per_token\": None, # None = price at list\n", + " \"concession_pct\": 0.0, # discount off MSRP, 0.0–0.75\n", + "\n", + " # ── text-to-speech 🟡 (billed per character, not in tokens) ─────\n", + " \"tts_chars_per_call\": 2_000,\n", + " \"tts_voice_tier\": \"advanced\", # \"advanced\" | \"standard\"\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "a4b53924", + "metadata": {}, + "source": [ + "## The published rate card\n", + "\n", + "These are Genesys' own numbers, transcribed verbatim. Every line carries the\n", + "date it was published — a cost figure without its rate-card date is not\n", + "auditable." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "49a89e32", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.151531Z", + "iopub.status.busy": "2026-08-02T20:41:31.151385Z", + "iopub.status.idle": "2026-08-02T20:41:31.197032Z", + "shell.execute_reply": "2026-08-02T20:41:31.196473Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
FeaturePublished rateConfidenceSource
Bots (Voice)17 minutes per token🟢2026-07-12
Bots (Digital)51 sessions per token🟢2026-07-12
Virtual Agent0.5 tokens per Virtual Agent interaction🟢2026-07-12
Agentic Virtual Agent1.2 tokens per interaction🟢2026-07-12
Agent Copilot [named]40 tokens per user🟢2026-07-12
Agent Copilot [concurrent]60 tokens per user🟢2026-07-12
AI Scoring20 evaluations per token🟢2026-07-12
AI Translate2 translations per token🟢2026-07-12
AI Summary and Insights50 summaries/insights per token🟢2026-07-12
Apple Messages for Business400 inbound or outbound messages per token🟢2026-07-12
Facebook Messenger400 messages per token🟢2026-07-12
Instagram Direct Messaging400 messages per token🟢2026-07-12
WhatsApp Messaging400 messages per token🟢2026-07-12
X Direct Messaging400 messages per token🟢2026-07-12
Genesys Cloud Social400 social post ingestions per channel per token🟢2026-07-12
Social Post Responses400 outbound messages per channel per token🟢2026-07-12
Predictive Routing17 routes per token🟢2026-07-12
Speech and Text Analytics [named]30 tokens per user🟢2026-07-12
Speech and Text Analytics [concurrent]45 tokens per user🟢2026-07-12
Genesys Cloud Copilot20 AI actions per token🟢2026-07-12
Predictive EngagementNo charge for token usage🟢2026-07-12
Genesys Cloud knowledge queriesno charge for Genesys Cloud knowledge queries🟢2026-07-12
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "\n", + "**Source** — [https://help.genesys.cloud/articles/genesys-cloud-tokens-model/](https://help.genesys.cloud/articles/genesys-cloud-tokens-model/), last updated\n", + "**2026-07-12**. Text-to-speech is priced on a separate page\n", + "([2026-05-22](https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/)) and is **not** token-metered.\n", + "\n", + "**Two published rules change the arithmetic:**\n", + "\n", + "> In cases where an interaction uses multiple AI resources, such as bot flows, virtual agents, and agentic virtual agents, Genesys bases charges on the highest tier (price) resource that Genesys Cloud uses during the interaction.\n", + "\n", + "So the deflection shares below are a **partition**, not overlapping rates: an\n", + "interaction is charged once, at the highest tier it touched.\n", + "\n", + "> …if you enable Agent Copilot simultaneously, then Supervisor Copilot summaries and insights do not consume tokens.\n", + "\n", + "So AI Summary bills at zero whenever Agent Copilot is on.\n", + "\n", + "**Free monthly allowance** — 250 tokens\n", + "for a named organisation, 350 for a\n", + "concurrent one. It renews monthly and does **not** carry over.\n", + "\n", + "**Voice bots round up** — each call bills to the next\n", + "15-second increment, so short bot touches cost more\n", + "than their raw duration suggests.\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: the vendor's published table, verbatim ───────────────────\n", + "_rc = pd.DataFrame(rate_card_rows())\n", + "display(styled(_rc))\n", + "\n", + "display(Markdown(f\"\"\"\n", + "**Source** — [{RATE_CARD_SOURCE}]({RATE_CARD_SOURCE}), last updated\n", + "**{RATE_CARD_SOURCE_DATE}**. Text-to-speech is priced on a separate page\n", + "([{TTS_SOURCE_DATE}]({TTS_SOURCE})) and is **not** token-metered.\n", + "\n", + "**Two published rules change the arithmetic:**\n", + "\n", + "> {HIGHEST_TIER_RULE}\n", + "\n", + "So the deflection shares below are a **partition**, not overlapping rates: an\n", + "interaction is charged once, at the highest tier it touched.\n", + "\n", + "> …{COPILOT_COVERS_SUMMARY_RULE}.\n", + "\n", + "So AI Summary bills at zero whenever Agent Copilot is on.\n", + "\n", + "**Free monthly allowance** — {FREE_TOKENS_PER_MONTH[LicenceModel.NAMED]} tokens\n", + "for a named organisation, {FREE_TOKENS_PER_MONTH[LicenceModel.CONCURRENT]} for a\n", + "concurrent one. It renews monthly and does **not** carry over.\n", + "\n", + "**Voice bots round up** — each call bills to the next\n", + "{VOICE_BOT_ROUNDUP_SECONDS}-second increment, so short bot touches cost more\n", + "than their raw duration suggests.\n", + "\"\"\"))" + ] + }, + { + "cell_type": "markdown", + "id": "bee7c127", + "metadata": {}, + "source": [ + "## Set the scenario\n", + "\n", + "Volumes come from the engagement data above; the sidebar drives adoption and\n", + "commercial terms." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b92c1975", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.199072Z", + "iopub.status.busy": "2026-08-02T20:41:31.198908Z", + "iopub.status.idle": "2026-08-02T20:41:31.256838Z", + "shell.execute_reply": "2026-08-02T20:41:31.256173Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "application/mercury+json": { + "model_id": "2e97496d4b6a4c02a8f117727aa5e33d", + "position": "sidebar", + "widget": "SelectWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "2e97496d4b6a4c02a8f117727aa5e33d", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "40b7dcd02e8243d69960afbbe03d5ecf", + "position": "sidebar", + "widget": "SelectWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "40b7dcd02e8243d69960afbbe03d5ecf", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "721b91d57dc647f2a158d01e913461cb", + "position": "sidebar", + "widget": "SelectWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "721b91d57dc647f2a158d01e913461cb", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "81f6e8c010564b738258f31364b23954", + "position": "sidebar", + "widget": "NumberInputWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "81f6e8c010564b738258f31364b23954", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "41cce54706e74b598f9c9e183113a813", + "position": "sidebar", + "widget": "SliderWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "41cce54706e74b598f9c9e183113a813", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "43711e3c02e04aaeaceff10e803b68c4", + "position": "sidebar", + "widget": "NumberInputWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "43711e3c02e04aaeaceff10e803b68c4", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "e3264e3f98f54182b7b9a415aa8a6e75", + "position": "sidebar", + "widget": "SliderWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "e3264e3f98f54182b7b9a415aa8a6e75", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "aead8ab94f2140dfb450812993ce398d", + "position": "sidebar", + "widget": "SliderWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "aead8ab94f2140dfb450812993ce398d", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "93e8bc988e7c4a77b8a02288eafc28d3", + "position": "sidebar", + "widget": "SliderWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "93e8bc988e7c4a77b8a02288eafc28d3", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "07a69926b1a84dcf88d730b1f30d8747", + "position": "sidebar", + "widget": "NumberInputWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "07a69926b1a84dcf88d730b1f30d8747", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "048f3603dfae494cb523cdcc8e2dd626", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "048f3603dfae494cb523cdcc8e2dd626", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "fba8b8a942ba4efd9055fa91abe10f2a", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "fba8b8a942ba4efd9055fa91abe10f2a", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "3123a79e0e124ea7b0580f4a40819438", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "3123a79e0e124ea7b0580f4a40819438", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "11bd04950ae549028549cdaa209a8d7d", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "11bd04950ae549028549cdaa209a8d7d", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "a7276468601c4dd7b23a57237e106fe9", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "a7276468601c4dd7b23a57237e106fe9", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "531a96090cf444f084cac34ff47e9e3f", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "531a96090cf444f084cac34ff47e9e3f", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "2734f5884f124ab2a6edd5bdeab68c91", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "2734f5884f124ab2a6edd5bdeab68c91", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "6985548a90314be4b3b7cb910eed549f", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "6985548a90314be4b3b7cb910eed549f", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "bbd6f38e287443e0a123e5db21f7d79d", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "bbd6f38e287443e0a123e5db21f7d79d", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "3dec4b4b4a524e7299866052c64e7ef2", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "3dec4b4b4a524e7299866052c64e7ef2", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "36902addac574d6aa0c82e46bf5124ce", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "36902addac574d6aa0c82e46bf5124ce", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "9efffb449ca04588a15ad1cc883fd0c2", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "9efffb449ca04588a15ad1cc883fd0c2", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "5b220853ba384edc8abb71fd394d2cd1", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "5b220853ba384edc8abb71fd394d2cd1", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "8554784a0c37479cbbf5e7a1461d4948", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "8554784a0c37479cbbf5e7a1461d4948", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/mercury+json": { + "model_id": "8def1084bb7b409ead5fb3525714d047", + "position": "sidebar", + "widget": "CheckboxWidget" + }, + "application/vnd.jupyter.widget-view+json": { + "model_id": "8def1084bb7b409ead5fb3525714d047", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Scenario controls (Mercury sidebar — widgets ONLY, no other output) ─\n", + "# Mercury re-runs only cells BELOW this one, so every .value is read in the\n", + "# next cell down — never here. Seeds come from ENGAGEMENT and the published\n", + "# list price, so the notebook and the engine cannot disagree about the\n", + "# starting scenario, and headless nbconvert gets a coherent, gate-passing run.\n", + "_licence_w = mr.Select(label=\"Licence model\", value=str(ENGAGEMENT[\"licence_model\"]).capitalize(),\n", + " choices=[\"Named\", \"Concurrent\"])\n", + "_currency_w = mr.Select(label=\"Currency\", value=str(ENGAGEMENT[\"currency\"]),\n", + " choices=sorted(LIST_PRICE_BY_CURRENCY))\n", + "_tts_tier_w = mr.Select(label=\"TTS voice tier\", value=str(ENGAGEMENT[\"tts_voice_tier\"]),\n", + " choices=[\"advanced\", \"standard\"])\n", + "\n", + "_price_w = mr.NumberInput(label=\"Token list price\", value=float(\n", + " LIST_PRICE_BY_CURRENCY[str(ENGAGEMENT[\"currency\"])]),\n", + " min=0.01, max=1000.0, step=0.01)\n", + "_concession_w = mr.Slider(label=\"Concession off list (%)\",\n", + " value=int(round(float(ENGAGEMENT[\"concession_pct\"]) * 100)),\n", + " min=0, max=75)\n", + "_users_w = mr.NumberInput(label=\"Licensed agent users\", value=int(ENGAGEMENT[\"agent_users\"]),\n", + " min=0, max=100_000, step=10)\n", + "\n", + "_bot_share_w = mr.Slider(label=\"Bot-only share of voice (%)\", value=30, min=0, max=100)\n", + "_va_share_w = mr.Slider(label=\"Virtual Agent share (%)\", value=15, min=0, max=100)\n", + "_ava_share_w = mr.Slider(label=\"Agentic VA share (%)\", value=5, min=0, max=100)\n", + "_bot_secs_w = mr.NumberInput(label=\"Avg bot seconds per call\", value=40, min=0, max=1800, step=5)\n", + "\n", + "_allowance_w = mr.CheckBox(label=\"Apply free monthly allowance\", value=True)\n", + "_tts_on_w = mr.CheckBox(label=\"Include text-to-speech\", value=True)\n", + "\n", + "# One enable per catalogued feature, seeded from the catalogue's defaults.\n", + "_feature_w = {f.key: mr.CheckBox(label=f\"Enable · {f.title}\", value=f.default_on)\n", + " for f in FEATURES}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "34ac8af8", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.258834Z", + "iopub.status.busy": "2026-08-02T20:41:31.258594Z", + "iopub.status.idle": "2026-08-02T20:41:31.267833Z", + "shell.execute_reply": "2026-08-02T20:41:31.267168Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Scenario: 8 features · 500 users · Named · deflection 50% (agent-handled 50%)\n", + " · AI Summary and Insights: 120,000 units not billed — Agent Copilot is enabled, and Genesys does not charge Supervisor summaries and insights alongside it.\n", + " · Voice bots: the published 15-second per-call round-up adds 12.5% to billable bot minutes at a 40s average.\n", + " · Predictive Engagement: No charge for token usage — shown at $0 because Genesys publishes it as included.\n" + ] + } + ], + "source": [ + "# ── Scenario state (re-runs on any sidebar change) ──────────────────\n", + "# Read every widget .value and assemble the engine's inputs. No math here:\n", + "# arrangement and one engine call.\n", + "LICENCE = LicenceModel(str(_licence_w.value).lower())\n", + "CURRENCY = str(_currency_w.value)\n", + "CONCESSION_PCT = float(_concession_w.value) / 100.0\n", + "USERS = int(_users_w.value)\n", + "BOT_SHARE = float(_bot_share_w.value) / 100.0\n", + "VA_SHARE = float(_va_share_w.value) / 100.0\n", + "AVA_SHARE = float(_ava_share_w.value) / 100.0\n", + "BOT_SECONDS = float(_bot_secs_w.value)\n", + "APPLY_ALLOWANCE = bool(_allowance_w.value)\n", + "\n", + "VOLUMES = Volumes(\n", + " voice_inbound_monthly=float(ENGAGEMENT[\"voice_inbound_monthly\"]),\n", + " voice_outbound_monthly=float(ENGAGEMENT[\"voice_outbound_monthly\"]),\n", + " digital_sessions_monthly=float(ENGAGEMENT[\"digital_sessions_monthly\"]),\n", + " email_monthly=float(ENGAGEMENT[\"email_monthly\"]),\n", + " messaging_monthly=float(ENGAGEMENT[\"messaging_monthly\"]),\n", + " social_posts_monthly=float(ENGAGEMENT[\"social_posts_monthly\"]),\n", + " social_responses_monthly=float(ENGAGEMENT[\"social_responses_monthly\"]),\n", + " translations_monthly=float(ENGAGEMENT[\"translations_monthly\"]),\n", + " evaluations_monthly=float(ENGAGEMENT[\"evaluations_monthly\"]),\n", + " ai_actions_monthly=float(ENGAGEMENT[\"ai_actions_monthly\"]),\n", + " routed_interactions_monthly=float(ENGAGEMENT[\"routed_interactions_monthly\"]),\n", + ")\n", + "\n", + "# The shares partition the volume by highest tier touched, so they cannot\n", + "# sum past 1.0 — the sliders are independent, so scale them back if a\n", + "# stakeholder pushes them over rather than crashing the notebook.\n", + "_share_total = BOT_SHARE + VA_SHARE + AVA_SHARE\n", + "_SHARES_CLAMPED = _share_total > 1.0\n", + "if _SHARES_CLAMPED:\n", + " BOT_SHARE, VA_SHARE, AVA_SHARE = (s / _share_total for s in\n", + " (BOT_SHARE, VA_SHARE, AVA_SHARE))\n", + "\n", + "MIX = DeflectionMix(bot_only_share=BOT_SHARE, virtual_agent_share=VA_SHARE,\n", + " agentic_va_share=AVA_SHARE)\n", + "\n", + "PRICE = TokenPrice(\n", + " currency=CURRENCY,\n", + " list_rate=float(_price_w.value),\n", + " contracted_rate=ENGAGEMENT[\"contracted_rate_per_token\"],\n", + " concession_pct=CONCESSION_PCT,\n", + ")\n", + "\n", + "ENABLED = frozenset(k for k, w in _feature_w.items() if bool(w.value))\n", + "\n", + "VOICE_BOT = VoiceBotUsage(\n", + " calls_per_month=VOLUMES.voice_inbound_monthly * BOT_SHARE,\n", + " avg_bot_seconds_per_call=BOT_SECONDS,\n", + ")\n", + "TTS = (TtsUsage(calls_monthly=VOLUMES.voice_inbound_monthly,\n", + " chars_per_call=int(ENGAGEMENT[\"tts_chars_per_call\"]),\n", + " tier=str(_tts_tier_w.value))\n", + " if bool(_tts_on_w.value) else None)\n", + "\n", + "INPUTS = CalculatorInputs(\n", + " volumes=VOLUMES, users=USERS, licence=LICENCE, enabled=ENABLED, mix=MIX,\n", + " price=PRICE, voice_bot=VOICE_BOT, tts=TTS, apply_allowance=APPLY_ALLOWANCE,\n", + ")\n", + "RESULT = calculate(INPUTS)\n", + "\n", + "backstage(f\"Scenario: {len(ENABLED)} features · {USERS:,} users · \"\n", + " f\"{LICENCE.label} · deflection {MIX.total_deflected_share:.0%} \"\n", + " f\"(agent-handled {MIX.agent_handled_share:.0%})\")\n", + "if _SHARES_CLAMPED:\n", + " backstage(\"NOTE: tier shares exceeded 100% and were scaled to a partition.\")\n", + "for _w in RESULT.warnings:\n", + " backstage(f\" · {_w}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7f85d41b", + "metadata": {}, + "source": [ + "## What it costs" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "9ea9f75d", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.269823Z", + "iopub.status.busy": "2026-08-02T20:41:31.269651Z", + "iopub.status.idle": "2026-08-02T20:41:31.276901Z", + "shell.execute_reply": "2026-08-02T20:41:31.276205Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
Token cost
$628,488/yr
$52,374/mo
Text-to-speech
$38,400/yr
billed per character, not tokens
Total AI run-rate
$666,888/yr
$0.381 per interaction
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "Consumption **17,624** + subscription **35,000** = **52,624** tokens/month, less the **250**-token free allowance → **52,374** billable at $1.00/token." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "At a 40s average, the published 15-second per-call round-up adds 12% to billable bot minutes.\n" + ] + } + ], + "source": [ + "# ── Stage: the headline ─────────────────────────────────────────────\n", + "_t = RESULT.totals\n", + "_cards = [\n", + " (\"Token cost\", f\"{money(_t.token_cost_annual)}/yr\", f\"{money(_t.token_cost_monthly)}/mo\"),\n", + " (\"Text-to-speech\", f\"{money(RESULT.tts_cost_annual)}/yr\",\n", + " \"billed per character, not tokens\"),\n", + " (\"Total AI run-rate\", f\"{money(RESULT.grand_total_annual)}/yr\",\n", + " f\"{money(cost_per_interaction(RESULT, VOLUMES), 3)} per interaction\"),\n", + "]\n", + "_html = \"\".join(\n", + " f'
'\n", + " f'
{_label}
'\n", + " f'
{_big}
'\n", + " f'
{_sub}
'\n", + " for _label, _big, _sub in _cards\n", + ")\n", + "display(HTML(f'
'\n", + " f'{_html}
'))\n", + "\n", + "# The allowance, shown as the deduction it is.\n", + "display(Markdown(\n", + " f\"Consumption **{_t.consumption_tokens_monthly:,.0f}** + subscription \"\n", + " f\"**{_t.subscription_tokens_monthly:,.0f}** = **{_t.gross_tokens_monthly:,.0f}** \"\n", + " f\"tokens/month, less the **{_t.allowance_tokens_monthly}**-token free \"\n", + " f\"allowance → **{_t.billable_tokens_monthly:,.0f}** billable at \"\n", + " f\"{money(_t.effective_rate, 2)}/token.\"\n", + "))\n", + "\n", + "# ONE curated line for the stage — the finding a client should take away.\n", + "_uplift_pct = 0.0\n", + "if BOT_SECONDS > 0 and \"bots_voice\" in ENABLED:\n", + " import math as _math\n", + " _billed = _math.ceil(BOT_SECONDS / VOICE_BOT_ROUNDUP_SECONDS) * VOICE_BOT_ROUNDUP_SECONDS\n", + " _uplift_pct = _billed / BOT_SECONDS - 1.0\n", + "if _uplift_pct > 0:\n", + " print(f\"At a {BOT_SECONDS:.0f}s average, the published \"\n", + " f\"{VOICE_BOT_ROUNDUP_SECONDS}-second per-call round-up adds \"\n", + " f\"{_uplift_pct:.0%} to billable bot minutes.\")" + ] + }, + { + "cell_type": "markdown", + "id": "9da5dd8a", + "metadata": {}, + "source": [ + "## Where the cost sits" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0502c073", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:31.279017Z", + "iopub.status.busy": "2026-08-02T20:41:31.278740Z", + "iopub.status.idle": "2026-08-02T20:41:32.436998Z", + "shell.execute_reply": "2026-08-02T20:41:32.436442Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
FeatureUnits / moUnitTokens / moCost / yr
Agent Copilot500users20,000.0$240,000🟢
Speech & Text Analytics500users15,000.0$180,000🟢
Virtual Agent18,000interactions9,000.0$108,000🟢
Agentic Virtual Agent6,000interactions7,200.0$86,400🟢
Voice Bot22,500bot minutes1,323.5$15,882🟢
AI Scoring2,000evaluations100.0$1,200🟢
AI Summary & Insights0summaries0.0$0🟢
Predictive Engagement146,000interactions0.0$0🟢
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "cliponaxis": false, + "hoverinfo": "skip", + "marker": { + "color": "#0072bc", + "line": { + "color": "#ffffff", + "width": 2 + } + }, + "orientation": "h", + "text": [ + "$1,200", + "$15,882", + "$86,400", + "$108,000", + "$180,000", + "$240,000" + ], + "textposition": "outside", + "type": "bar", + "x": { + "bdata": "AAAAAADAkkAtLS0tLQXPQAAAAAAAGPVAAAAAAABe+kAAAAAAAPkFQQAAAAAATA1B", + "dtype": "f8" + }, + "y": [ + "AI Scoring", + "Voice Bot", + "Agentic Virtual Agent", + "Virtual Agent", + "Speech & Text Analytics", + "Agent Copilot" + ] + } + ], + "layout": { + "font": { + "color": "#2e404d", + "family": "Arial, 'Helvetica Neue', Helvetica, sans-serif", + "size": 13 + }, + "height": 264, + "legend": { + "orientation": "h", + "x": 0, + "y": -0.16 + }, + "margin": { + "b": 8, + "l": 8, + "r": 8, + "t": 52 + }, + "paper_bgcolor": "#ffffff", + "plot_bgcolor": "#ffffff", + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermap": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermap" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "color": "#2e404d", + "family": "Georgia, 'Times New Roman', Times, serif", + "size": 17 + }, + "text": "Annual token cost by feature", + "x": 0, + "xanchor": "left" + }, + "xaxis": { + "gridcolor": "#d5d9db", + "showticklabels": false, + "title": { + "text": "" + }, + "zerolinecolor": "#d5d9db" + }, + "yaxis": { + "gridcolor": "#d5d9db", + "zerolinecolor": "#d5d9db" + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "**Published at no token charge** — **Predictive Engagement**. A zero the vendor put in writing is a finding, not an omission." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: per-feature breakdown ────────────────────────────────────\n", + "_titles = {f.key: f.title for f in FEATURES}\n", + "_rows = [{\n", + " \"Feature\": _titles.get(_l.key, _l.feature),\n", + " \"Units / mo\": _l.units_monthly,\n", + " \"Unit\": _l.unit_label,\n", + " \"Tokens / mo\": _l.tokens_monthly,\n", + " \"Cost / yr\": _l.cost_annual,\n", + " \"\": _l.confidence.icon,\n", + "} for _l in RESULT.lines]\n", + "_df = pd.DataFrame(_rows).sort_values(\"Cost / yr\", ascending=False)\n", + "display(styled(_df, **{\"Units / mo\": \"{:,.0f}\", \"Tokens / mo\": \"{:,.1f}\",\n", + " \"Cost / yr\": \"${:,.0f}\"}))\n", + "\n", + "_paid = _df[_df[\"Cost / yr\"] > 0].sort_values(\"Cost / yr\")\n", + "if len(_paid):\n", + " _fig = go.Figure(go.Bar(\n", + " x=_paid[\"Cost / yr\"], y=_paid[\"Feature\"], orientation=\"h\",\n", + " marker=dict(color=TOKEN_COLOR, line=dict(color=SURFACE, width=2)),\n", + " text=[html_money(v) for v in _paid[\"Cost / yr\"]],\n", + " textposition=\"outside\", cliponaxis=False, hoverinfo=\"skip\",\n", + " ))\n", + " chart_layout(_fig, \"Annual token cost by feature\", height=60 + 34 * len(_paid))\n", + " _fig.update_xaxes(title=\"\", showticklabels=False)\n", + " _fig.show()\n", + "\n", + "_free = _df[(_df[\"Cost / yr\"] == 0) & (_df[\"Units / mo\"] > 0)]\n", + "if len(_free):\n", + " display(Markdown(\n", + " \"**Published at no token charge** — \"\n", + " + \", \".join(f\"**{r.Feature}**\" for r in _free.itertuples())\n", + " + \". A zero the vendor put in writing is a finding, not an omission.\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "22dd8526", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.440007Z", + "iopub.status.busy": "2026-08-02T20:41:32.439783Z", + "iopub.status.idle": "2026-08-02T20:41:32.445596Z", + "shell.execute_reply": "2026-08-02T20:41:32.445132Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
BasisRate / tokenToken cost / yr
At list price$1.00$628,488
After 0% concession$1.00$628,488
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: list price → what you'd actually pay ─────────────────────\n", + "_walk = pd.DataFrame([\n", + " {\"Basis\": \"At list price\", \"Rate / token\": PRICE.list_rate,\n", + " \"Token cost / yr\": _t.list_cost_annual},\n", + " {\"Basis\": (\"Contracted rate\" if PRICE.contracted_rate is not None\n", + " else f\"After {CONCESSION_PCT:.0%} concession\"),\n", + " \"Rate / token\": _t.effective_rate, \"Token cost / yr\": _t.token_cost_annual},\n", + "])\n", + "display(styled(_walk, **{\"Rate / token\": \"${:,.2f}\", \"Token cost / yr\": \"${:,.0f}\"}))\n", + "if _t.concession_saving_annual > 0:\n", + " display(Markdown(\n", + " f\"Commercial terms are worth **{money(_t.concession_saving_annual)}/yr** \"\n", + " f\"against list.\"\n", + " ))" + ] + }, + { + "cell_type": "markdown", + "id": "8afab27e", + "metadata": {}, + "source": [ + "## Text-to-speech — billed separately\n", + "\n", + "Genesys prices Enhanced TTS **per character**, on its own published page. It is\n", + "never converted to tokens, and it is never folded silently into the token\n", + "subtotal." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5195be85", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.451192Z", + "iopub.status.busy": "2026-08-02T20:41:32.450940Z", + "iopub.status.idle": "2026-08-02T20:41:32.459418Z", + "shell.execute_reply": "2026-08-02T20:41:32.458689Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MeasureValue
Characters synthesised / mo200,000,000
Free during virtual agents20% (40,000,000 chars)
Billable characters / mo160,000,000
Billed units (1M chars, rounded up)160
Rate (advanced voices)$20.00 per million characters
Cost$3,200/mo · $38,400/yr
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### Total AI run-rate\n", + "\n", + "Tokens **$628,488/yr** + text-to-speech **$38,400/yr** = **$666,888/yr**." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: the non-token line ───────────────────────────────────────\n", + "if RESULT.tts is None:\n", + " display(Markdown(\"_Text-to-speech is excluded from this scenario._\"))\n", + "else:\n", + " _x = RESULT.tts\n", + " _tts = pd.DataFrame([\n", + " {\"Measure\": \"Characters synthesised / mo\", \"Value\": f\"{_x.total_chars_monthly:,.0f}\"},\n", + " {\"Measure\": \"Free during virtual agents\",\n", + " \"Value\": f\"{_x.free_share:.0%} ({_x.free_chars_monthly:,.0f} chars)\"},\n", + " {\"Measure\": \"Billable characters / mo\", \"Value\": f\"{_x.billable_chars_monthly:,.0f}\"},\n", + " {\"Measure\": \"Billed units (1M chars, rounded up)\",\n", + " \"Value\": f\"{_x.billed_millions_monthly:,.0f}\"},\n", + " {\"Measure\": f\"Rate ({_x.tier} voices)\",\n", + " \"Value\": f\"{money(_x.rate_per_million_chars, 2)} per million characters\"},\n", + " {\"Measure\": \"Cost\", \"Value\": f\"{money(_x.cost_monthly)}/mo · {money(_x.cost_annual)}/yr\"},\n", + " ])\n", + " display(styled(_tts))\n", + " for _w in _x.warnings:\n", + " display(Markdown(f\"⚠️ {_w}\"))\n", + "\n", + "# Grand total — two labelled components, never one silent sum.\n", + "display(Markdown(\n", + " f\"### Total AI run-rate\\n\\n\"\n", + " f\"Tokens **{money(RESULT.token_cost_annual)}/yr** + text-to-speech \"\n", + " f\"**{money(RESULT.tts_cost_annual)}/yr** = \"\n", + " f\"**{money(RESULT.grand_total_annual)}/yr**.\"\n", + "))" + ] + }, + { + "cell_type": "markdown", + "id": "aebd9aac", + "metadata": {}, + "source": [ + "## Scenarios\n", + "\n", + "Adoption is the biggest uncertainty in any AI estimate, so it is priced as a\n", + "range rather than a point." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "171ee535", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.461502Z", + "iopub.status.busy": "2026-08-02T20:41:32.461259Z", + "iopub.status.idle": "2026-08-02T20:41:32.493529Z", + "shell.execute_reply": "2026-08-02T20:41:32.492655Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ScenarioBillable tokens / moToken cost / yrTTS cost / yrTotal / yr$ / interaction
Conservative41,613$499,356$44,640$543,996$0.310
Base52,374$628,488$38,400$666,888$0.381
Aggressive65,795$789,540$31,200$820,740$0.468
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "marker": { + "color": "#0072bc", + "line": { + "color": "#ffffff", + "width": 2 + } + }, + "name": "Tokens", + "type": "bar", + "x": [ + "Conservative", + "Base", + "Aggressive" + ], + "y": { + "bdata": "AAAAAHB6HkEAAAAAEC4jQQAAAABIGChB", + "dtype": "f8" + } + }, + { + "marker": { + "color": "#ffc400", + "line": { + "color": "#ffffff", + "width": 2 + } + }, + "name": "Text-to-speech", + "type": "bar", + "x": [ + "Conservative", + "Base", + "Aggressive" + ], + "y": { + "bdata": "AAAAAADM5UAAAAAAAMDiQAAAAAAAeN5A", + "dtype": "f8" + } + } + ], + "layout": { + "barmode": "stack", + "font": { + "color": "#2e404d", + "family": "Arial, 'Helvetica Neue', Helvetica, sans-serif", + "size": 13 + }, + "height": 360, + "legend": { + "orientation": "h", + "x": 0, + "y": -0.16 + }, + "margin": { + "b": 8, + "l": 8, + "r": 8, + "t": 52 + }, + "paper_bgcolor": "#ffffff", + "plot_bgcolor": "#ffffff", + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermap": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermap" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "color": "#2e404d", + "family": "Georgia, 'Times New Roman', Times, serif", + "size": 17 + }, + "text": "Annual AI run-rate by adoption scenario", + "x": 0, + "xanchor": "left" + }, + "xaxis": { + "gridcolor": "#d5d9db", + "zerolinecolor": "#d5d9db" + }, + "yaxis": { + "gridcolor": "#d5d9db", + "separatethousands": true, + "tickprefix": "$", + "zerolinecolor": "#d5d9db" + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: conservative / base / aggressive adoption ────────────────\n", + "SCENARIOS = {\n", + " \"Conservative\": INPUTS.with_(\n", + " mix=DeflectionMix(bot_only_share=0.20, virtual_agent_share=0.05,\n", + " agentic_va_share=0.02),\n", + " voice_bot=VoiceBotUsage(\n", + " calls_per_month=VOLUMES.voice_inbound_monthly * 0.20,\n", + " avg_bot_seconds_per_call=BOT_SECONDS)),\n", + " \"Base\": INPUTS,\n", + " \"Aggressive\": INPUTS.with_(\n", + " mix=DeflectionMix(bot_only_share=0.35, virtual_agent_share=0.25,\n", + " agentic_va_share=0.10),\n", + " voice_bot=VoiceBotUsage(\n", + " calls_per_month=VOLUMES.voice_inbound_monthly * 0.35,\n", + " avg_bot_seconds_per_call=BOT_SECONDS)),\n", + "}\n", + "_cmp = pd.DataFrame(compare(SCENARIOS))\n", + "display(styled(_cmp, **{\"Billable tokens / mo\": \"{:,.0f}\", \"Token cost / yr\": \"${:,.0f}\",\n", + " \"TTS cost / yr\": \"${:,.0f}\", \"Total / yr\": \"${:,.0f}\",\n", + " \"$ / interaction\": \"${:,.3f}\"}))\n", + "\n", + "_fig = go.Figure()\n", + "_fig.add_bar(name=\"Tokens\", x=_cmp[\"Scenario\"], y=_cmp[\"Token cost / yr\"],\n", + " marker=dict(color=TOKEN_COLOR, line=dict(color=SURFACE, width=2)))\n", + "_fig.add_bar(name=\"Text-to-speech\", x=_cmp[\"Scenario\"], y=_cmp[\"TTS cost / yr\"],\n", + " marker=dict(color=TTS_COLOR, line=dict(color=SURFACE, width=2)))\n", + "_fig.update_layout(barmode=\"stack\")\n", + "chart_layout(_fig, \"Annual AI run-rate by adoption scenario\", height=360)\n", + "_fig.update_yaxes(tickprefix=\"$\", separatethousands=True)\n", + "_fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "31fedefe", + "metadata": {}, + "source": [ + "## What moves the number\n", + "\n", + "Each driver moved ±25% on its own, everything else held." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9f95b24b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.496066Z", + "iopub.status.busy": "2026-08-02T20:41:32.495804Z", + "iopub.status.idle": "2026-08-02T20:41:32.529467Z", + "shell.execute_reply": "2026-08-02T20:41:32.528944Z" + }, + "tags": [ + "presentation" + ] + }, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "base": 666888.0, + "marker": { + "color": "#586671", + "line": { + "color": "#ffffff", + "width": 2 + } + }, + "name": "−25%", + "orientation": "h", + "type": "bar", + "x": { + "bdata": "AAAAAAAAAAAAAAAAAAAAAAAAAAAArLTAAAAAAADAwsAAAAAAAGTUwAAAAAAAftjAAAAAAIDG48AAAAAAgKL5wAAAAAAQLgPB", + "dtype": "f8" + }, + "y": [ + "Concession", + "Bot-only share", + "Avg bot seconds", + "TTS characters per call", + "Agentic VA share", + "Virtual Agent share", + "Inbound voice volume", + "Licensed users", + "Token list price" + ] + }, + { + "base": 666888.0, + "marker": { + "color": "#0072bc", + "line": { + "color": "#ffffff", + "width": 2 + } + }, + "name": "+25%", + "orientation": "h", + "type": "bar", + "x": { + "bdata": "AAAAAAAAAAAAAAAAAAAAAAAAAAAArLRAAAAAAADAwkAAAAAAAKDUQAAAAAAAuthAAAAAAIDG40AAAAAAgKL5QAAAAAAQLgNB", + "dtype": "f8" + }, + "y": [ + "Concession", + "Bot-only share", + "Avg bot seconds", + "TTS characters per call", + "Agentic VA share", + "Virtual Agent share", + "Inbound voice volume", + "Licensed users", + "Token list price" + ] + } + ], + "layout": { + "barmode": "overlay", + "font": { + "color": "#2e404d", + "family": "Arial, 'Helvetica Neue', Helvetica, sans-serif", + "size": 13 + }, + "height": 380, + "legend": { + "orientation": "h", + "x": 0, + "y": -0.16 + }, + "margin": { + "b": 8, + "l": 8, + "r": 8, + "t": 52 + }, + "paper_bgcolor": "#ffffff", + "plot_bgcolor": "#ffffff", + "shapes": [ + { + "line": { + "color": "#2e404d", + "dash": "dot", + "width": 1 + }, + "type": "line", + "x0": 666888.0, + "x1": 666888.0, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermap": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermap" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0.0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1.0, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "color": "#2e404d", + "family": "Georgia, 'Times New Roman', Times, serif", + "size": 17 + }, + "text": "Swing in total AI run-rate (±25% per driver)", + "x": 0, + "xanchor": "left" + }, + "xaxis": { + "gridcolor": "#d5d9db", + "separatethousands": true, + "tickprefix": "$", + "zerolinecolor": "#d5d9db" + }, + "yaxis": { + "gridcolor": "#d5d9db", + "zerolinecolor": "#d5d9db" + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "The largest single lever is **Token list price** — a ±25% move swings the annual run-rate by **$314,244**." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stage: tornado ──────────────────────────────────────────────────\n", + "_tor = pd.DataFrame(tornado(INPUTS, list(DRIVERS)))\n", + "if len(_tor):\n", + " _tor = _tor.sort_values(\"swing\")\n", + " _labels = {\"token_price\": \"Token list price\", \"concession_pct\": \"Concession\",\n", + " \"users\": \"Licensed users\", \"voice_volume\": \"Inbound voice volume\",\n", + " \"bot_share\": \"Bot-only share\", \"virtual_agent_share\": \"Virtual Agent share\",\n", + " \"agentic_va_share\": \"Agentic VA share\", \"bot_seconds\": \"Avg bot seconds\",\n", + " \"tts_chars_per_call\": \"TTS characters per call\"}\n", + " _names = [_labels.get(d, d) for d in _tor[\"driver\"]]\n", + " _base = float(_tor[\"baseline\"].iloc[0])\n", + "\n", + " _fig = go.Figure()\n", + " _fig.add_bar(name=\"−25%\", y=_names, x=_tor[\"low\"] - _base, orientation=\"h\",\n", + " base=_base, marker=dict(color=MUTED, line=dict(color=SURFACE, width=2)))\n", + " _fig.add_bar(name=\"+25%\", y=_names, x=_tor[\"high\"] - _base, orientation=\"h\",\n", + " base=_base, marker=dict(color=BLUE, line=dict(color=SURFACE, width=2)))\n", + " _fig.update_layout(barmode=\"overlay\")\n", + " chart_layout(_fig, \"Swing in total AI run-rate (±25% per driver)\",\n", + " height=110 + 30 * len(_tor))\n", + " _fig.add_vline(x=_base, line=dict(color=INK, width=1, dash=\"dot\"))\n", + " _fig.update_xaxes(tickprefix=\"$\", separatethousands=True)\n", + " _fig.show()\n", + "\n", + " _top = _tor.iloc[-1]\n", + " display(Markdown(\n", + " f\"The largest single lever is **{_labels.get(_top['driver'], _top['driver'])}** \"\n", + " f\"— a ±25% move swings the annual run-rate by \"\n", + " f\"**{money(float(_top['swing']))}**.\"\n", + " ))" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1c76ff25", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.532016Z", + "iopub.status.busy": "2026-08-02T20:41:32.531835Z", + "iopub.status.idle": "2026-08-02T20:41:32.565149Z", + "shell.execute_reply": "2026-08-02T20:41:32.564193Z" + }, + "tags": [ + "gate" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All assertions passed (at default scenario).\n" + ] + } + ], + "source": [ + "# ── Verification gate — must pass under headless nbconvert ──────────\n", + "def _assert(cond, msg):\n", + " assert cond, msg\n", + "\n", + "\n", + "def _approx(got, want, tol=0.5):\n", + " assert abs(got - want) <= tol, f\"got {got:,.4f}, want {want:,.4f}\"\n", + "\n", + "\n", + "# ── Engine pins — explicit values, independent of any widget ────────\n", + "_assert(RATE_CARD_SOURCE_DATE == \"2026-07-12\", \"rate card source date moved\")\n", + "_assert(len(METERS) == 22, f\"expected 22 published meters, got {len(METERS)}\")\n", + "_assert(VOICE_BOT_ROUNDUP_SECONDS == 15, \"voice-bot round-up is 15 seconds\")\n", + "_assert(FREE_TOKENS_PER_MONTH[LicenceModel.NAMED] == 250, \"named allowance is 250\")\n", + "_assert(FREE_TOKENS_PER_MONTH[LicenceModel.CONCURRENT] == 350, \"concurrent allowance is 350\")\n", + "_approx(meter(\"bots_voice\").rate, 1 / 17, 1e-9)\n", + "_approx(meter(\"agentic_virtual_agent\").rate, 1.2, 1e-9)\n", + "\n", + "# Hand-checked: 1,000 calls at a 40s average bill as 750 minutes, not 666.67.\n", + "from genesyscalc import voice_bot_billable_minutes as _vbm\n", + "\n", + "_approx(_vbm(VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)), 750.0, 1e-9)\n", + "\n", + "# ── Content shape — the catalogue lives in this notebook ────────────\n", + "_assert(len(FEATURES) == 13, f\"expected 13 catalogued features, got {len(FEATURES)}\")\n", + "_assert(len({f.key for f in FEATURES}) == len(FEATURES), \"duplicate feature key\")\n", + "for _f in FEATURES:\n", + " _assert(_f.key in METERS, f\"{_f.key} is not a published meter\")\n", + " _assert(_f.title and _f.description and _f.ask, f\"{_f.key} is missing content\")\n", + "\n", + "# ── Engagement shape — SHAPE only, never emptiness ──────────────────\n", + "for _k in (\"client\", \"prepared_date\", \"agent_users\", \"licence_model\",\n", + " \"voice_inbound_monthly\", \"currency\", \"concession_pct\",\n", + " \"tts_chars_per_call\", \"tts_voice_tier\"):\n", + " _assert(_k in ENGAGEMENT, f\"engagement-data is missing {_k!r}\")\n", + "\n", + "# ── Structural ties — hold at ANY widget setting ────────────────────\n", + "_assert(MIX.total_deflected_share <= 1.0 + 1e-9,\n", + " \"tier shares must partition the volume, not overlap\")\n", + "_assert(RESULT.totals.billable_tokens_monthly >= 0, \"billable tokens went negative\")\n", + "_approx(RESULT.totals.billable_tokens_monthly,\n", + " max(0, __import__(\"math\").ceil(RESULT.totals.consumption_tokens_monthly)\n", + " + RESULT.totals.subscription_tokens_monthly\n", + " - RESULT.totals.allowance_tokens_monthly), 1e-6)\n", + "_assert(RESULT.totals.token_cost_annual <= RESULT.totals.list_cost_annual + 1e-6,\n", + " \"effective cost exceeded list cost\")\n", + "_approx(RESULT.grand_total_annual,\n", + " RESULT.token_cost_annual + RESULT.tts_cost_annual, 1e-6)\n", + "_approx(sum(_l.tokens_monthly for _l in RESULT.lines),\n", + " RESULT.totals.consumption_tokens_monthly\n", + " + RESULT.totals.subscription_tokens_monthly, 1e-6)\n", + "\n", + "# The published Copilot exclusion, enforced at any setting.\n", + "if {\"agent_copilot_named\", \"agent_copilot_concurrent\"} & ENABLED:\n", + " for _l in RESULT.lines:\n", + " if _l.key == \"ai_summary_and_insights\":\n", + " _approx(_l.cost_annual, 0.0, 1e-9)\n", + "\n", + "# The export payload round-trips and names its rate card.\n", + "_payload = result_json(RESULT, INPUTS)\n", + "_assert(_json.loads(_json.dumps(_payload)) == _payload, \"appendix JSON is not serialisable\")\n", + "_assert(_payload[\"rate_card\"][\"source_date\"] == RATE_CARD_SOURCE_DATE,\n", + " \"appendix does not carry the rate-card date\")\n", + "\n", + "# ── Live-state pins — guarded, so a moved slider never crashes ──────\n", + "_at_default = (USERS == 500 and LICENCE is LicenceModel.NAMED\n", + " and abs(BOT_SHARE - 0.30) < 1e-9 and abs(VA_SHARE - 0.15) < 1e-9\n", + " and abs(AVA_SHARE - 0.05) < 1e-9 and abs(BOT_SECONDS - 40) < 1e-9\n", + " and abs(PRICE.effective_rate() - 1.0) < 1e-9\n", + " and APPLY_ALLOWANCE and TTS is not None\n", + " and ENABLED == frozenset(f.key for f in FEATURES if f.default_on))\n", + "if _at_default:\n", + " # Hand-checked at the shipped defaults: consumption 17,623.53 → ceil\n", + " # 17,624; + 35,000 subscription = 52,624; − 250 allowance = 52,374\n", + " # billable → $628,488/yr. TTS 200M chars, 20% VA-free → 160 units × $20\n", + " # → $38,400/yr. Grand total $666,888/yr.\n", + " _approx(RESULT.totals.billable_tokens_monthly, 52_374, 0.5)\n", + " _approx(RESULT.totals.token_cost_annual, 628_488.0, 0.5)\n", + " _approx(RESULT.tts_cost_annual, 38_400.0, 0.5)\n", + " _approx(RESULT.grand_total_annual, 666_888.0, 0.5)\n", + "\n", + "backstage(\"All assertions passed\"\n", + " + (\" (at default scenario).\" if _at_default else \" (scenario moved).\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "25ca9c57", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-02T20:41:32.567652Z", + "iopub.status.busy": "2026-08-02T20:41:32.567374Z", + "iopub.status.idle": "2026-08-02T20:41:32.592397Z", + "shell.execute_reply": "2026-08-02T20:41:32.587626Z" + }, + "tags": [ + "data-appendix" + ] + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "## Data appendix — the priced scenario\n", + "**Engagement** — master copy — placeholders, no client data.\n", + "**Rate card** — https://help.genesys.cloud/articles/genesys-cloud-tokens-model/ (published 2026-07-12). **TTS** — https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/ (published 2026-05-22).\n", + "\n", + "### Published rate card\n", + "| Feature | Published rate | Confidence | Source |\n", + "|:---------------------------------------|:-------------------------------------------------|:-------------|:-----------|\n", + "| Bots (Voice) | 17 minutes per token | 🟢 | 2026-07-12 |\n", + "| Bots (Digital) | 51 sessions per token | 🟢 | 2026-07-12 |\n", + "| Virtual Agent | 0.5 tokens per Virtual Agent interaction | 🟢 | 2026-07-12 |\n", + "| Agentic Virtual Agent | 1.2 tokens per interaction | 🟢 | 2026-07-12 |\n", + "| Agent Copilot [named] | 40 tokens per user | 🟢 | 2026-07-12 |\n", + "| Agent Copilot [concurrent] | 60 tokens per user | 🟢 | 2026-07-12 |\n", + "| AI Scoring | 20 evaluations per token | 🟢 | 2026-07-12 |\n", + "| AI Translate | 2 translations per token | 🟢 | 2026-07-12 |\n", + "| AI Summary and Insights | 50 summaries/insights per token | 🟢 | 2026-07-12 |\n", + "| Apple Messages for Business | 400 inbound or outbound messages per token | 🟢 | 2026-07-12 |\n", + "| Facebook Messenger | 400 messages per token | 🟢 | 2026-07-12 |\n", + "| Instagram Direct Messaging | 400 messages per token | 🟢 | 2026-07-12 |\n", + "| WhatsApp Messaging | 400 messages per token | 🟢 | 2026-07-12 |\n", + "| X Direct Messaging | 400 messages per token | 🟢 | 2026-07-12 |\n", + "| Genesys Cloud Social | 400 social post ingestions per channel per token | 🟢 | 2026-07-12 |\n", + "| Social Post Responses | 400 outbound messages per channel per token | 🟢 | 2026-07-12 |\n", + "| Predictive Routing | 17 routes per token | 🟢 | 2026-07-12 |\n", + "| Speech and Text Analytics [named] | 30 tokens per user | 🟢 | 2026-07-12 |\n", + "| Speech and Text Analytics [concurrent] | 45 tokens per user | 🟢 | 2026-07-12 |\n", + "| Genesys Cloud Copilot | 20 AI actions per token | 🟢 | 2026-07-12 |\n", + "| Predictive Engagement | No charge for token usage | 🟢 | 2026-07-12 |\n", + "| Genesys Cloud knowledge queries | no charge for Genesys Cloud knowledge queries | 🟢 | 2026-07-12 |\n", + "\n", + "### Cost by feature\n", + "| Feature | Units / mo | Unit | Tokens / mo | Cost / yr | |\n", + "|:------------------------|-------------:|:-------------|--------------:|------------:|:---|\n", + "| Agent Copilot | 500.00 | users | 20,000.00 | 240,000.00 | 🟢 |\n", + "| Speech & Text Analytics | 500.00 | users | 15,000.00 | 180,000.00 | 🟢 |\n", + "| Virtual Agent | 18,000.00 | interactions | 9,000.00 | 108,000.00 | 🟢 |\n", + "| Agentic Virtual Agent | 6,000.00 | interactions | 7,200.00 | 86,400.00 | 🟢 |\n", + "| Voice Bot | 22,500.00 | bot minutes | 1,323.53 | 15,882.35 | 🟢 |\n", + "| AI Scoring | 2,000.00 | evaluations | 100.00 | 1,200.00 | 🟢 |\n", + "| AI Summary & Insights | 0.00 | summaries | 0.00 | 0.00 | 🟢 |\n", + "| Predictive Engagement | 146,000.00 | interactions | 0.00 | 0.00 | 🟢 |\n", + "\n", + "### Scenario comparison\n", + "| Scenario | Billable tokens / mo | Token cost / yr | TTS cost / yr | Total / yr | $ / interaction |\n", + "|:-------------|-----------------------:|------------------:|----------------:|-------------:|------------------:|\n", + "| Conservative | 41613 | 499,356.00 | 44,640.00 | 543,996.00 | 0.31 |\n", + "| Base | 52374 | 628,488.00 | 38,400.00 | 666,888.00 | 0.38 |\n", + "| Aggressive | 65795 | 789,540.00 | 31,200.00 | 820,740.00 | 0.47 |\n", + "\n", + "### Applied rules and warnings\n", + "\n", + "- AI Summary and Insights: 120,000 units not billed — Agent Copilot is enabled, and Genesys does not charge Supervisor summaries and insights alongside it.\n", + "- Voice bots: the published 15-second per-call round-up adds 12.5% to billable bot minutes at a 40s average.\n", + "- Predictive Engagement: No charge for token usage — shown at $0 because Genesys publishes it as included.\n", + "\n", + "### Model state (JSON)\n", + "\n", + "```json\n", + "{\n", + " \"rate_card\": {\n", + " \"source\": \"https://help.genesys.cloud/articles/genesys-cloud-tokens-model/\",\n", + " \"source_date\": \"2026-07-12\",\n", + " \"voice_bot_roundup_seconds\": 15\n", + " },\n", + " \"tts_source\": {\n", + " \"source\": \"https://help.genesys.cloud/articles/genesys-enhanced-tts-pricing/\",\n", + " \"source_date\": \"2026-05-22\"\n", + " },\n", + " \"inputs\": {\n", + " \"users\": 500,\n", + " \"licence_model\": \"named\",\n", + " \"enabled_features\": [\n", + " \"agent_copilot_named\",\n", + " \"agentic_virtual_agent\",\n", + " \"ai_scoring\",\n", + " \"ai_summary_and_insights\",\n", + " \"bots_voice\",\n", + " \"predictive_engagement\",\n", + " \"speech_and_text_analytics_named\",\n", + " \"virtual_agent\"\n", + " ],\n", + " \"apply_allowance\": true,\n", + " \"volumes\": {\n", + " \"voice_inbound_monthly\": 100000.0,\n", + " \"voice_outbound_monthly\": 20000.0,\n", + " \"digital_sessions_monthly\": 8000.0,\n", + " \"email_monthly\": 12000.0,\n", + " \"messaging_monthly\": 6000.0,\n", + " \"social_posts_monthly\": 0.0,\n", + " \"social_responses_monthly\": 0.0,\n", + " \"translations_monthly\": 0.0,\n", + " \"evaluations_monthly\": 2000.0,\n", + " \"ai_actions_monthly\": 0.0,\n", + " \"routed_interactions_monthly\": 0.0\n", + " },\n", + " \"deflection_mix\": {\n", + " \"bot_only_share\": 0.3,\n", + " \"virtual_agent_share\": 0.15,\n", + " \"agentic_va_share\": 0.05,\n", + " \"agent_handled_share\": 0.5\n", + " },\n", + " \"price\": {\n", + " \"currency\": \"USD\",\n", + " \"list_rate\": 1.0,\n", + " \"contracted_rate\": null,\n", + " \"concession_pct\": 0.0,\n", + " \"effective_rate\": 1.0\n", + " },\n", + " \"voice_bot\": {\n", + " \"calls_per_month\": 30000.0,\n", + " \"avg_bot_seconds_per_call\": 40.0\n", + " },\n", + " \"tts\": {\n", + " \"calls_monthly\": 100000.0,\n", + " \"chars_per_call\": 2000,\n", + " \"tier\": \"advanced\"\n", + " }\n", + " },\n", + " \"lines\": [\n", + " {\n", + " \"key\": \"ai_scoring\",\n", + " \"feature\": \"AI Scoring\",\n", + " \"units_monthly\": 2000.0,\n", + " \"unit_label\": \"evaluations\",\n", + " \"tokens_monthly\": 100.0,\n", + " \"cost_monthly\": 100.0,\n", + " \"cost_annual\": 1200.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"ai_summary_and_insights\",\n", + " \"feature\": \"AI Summary and Insights\",\n", + " \"units_monthly\": 0.0,\n", + " \"unit_label\": \"summaries\",\n", + " \"tokens_monthly\": 0.0,\n", + " \"cost_monthly\": 0.0,\n", + " \"cost_annual\": 0.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"agent_copilot_named\",\n", + " \"feature\": \"Agent Copilot [named]\",\n", + " \"units_monthly\": 500.0,\n", + " \"unit_label\": \"users\",\n", + " \"tokens_monthly\": 20000.0,\n", + " \"cost_monthly\": 20000.0,\n", + " \"cost_annual\": 240000.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"agentic_virtual_agent\",\n", + " \"feature\": \"Agentic Virtual Agent\",\n", + " \"units_monthly\": 6000.0,\n", + " \"unit_label\": \"interactions\",\n", + " \"tokens_monthly\": 7200.0,\n", + " \"cost_monthly\": 7200.0,\n", + " \"cost_annual\": 86400.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"bots_voice\",\n", + " \"feature\": \"Bots (Voice)\",\n", + " \"units_monthly\": 22500.0,\n", + " \"unit_label\": \"bot minutes\",\n", + " \"tokens_monthly\": 1323.5294117647059,\n", + " \"cost_monthly\": 1323.5294117647059,\n", + " \"cost_annual\": 15882.35294117647,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"predictive_engagement\",\n", + " \"feature\": \"Predictive Engagement\",\n", + " \"units_monthly\": 146000.0,\n", + " \"unit_label\": \"interactions\",\n", + " \"tokens_monthly\": 0.0,\n", + " \"cost_monthly\": 0.0,\n", + " \"cost_annual\": 0.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"speech_and_text_analytics_named\",\n", + " \"feature\": \"Speech and Text Analytics [named]\",\n", + " \"units_monthly\": 500.0,\n", + " \"unit_label\": \"users\",\n", + " \"tokens_monthly\": 15000.0,\n", + " \"cost_monthly\": 15000.0,\n", + " \"cost_annual\": 180000.0,\n", + " \"confidence\": \"confirmed\"\n", + " },\n", + " {\n", + " \"key\": \"virtual_agent\",\n", + " \"feature\": \"Virtual Agent\",\n", + " \"units_monthly\": 18000.0,\n", + " \"unit_label\": \"interactions\",\n", + " \"tokens_monthly\": 9000.0,\n", + " \"cost_monthly\": 9000.0,\n", + " \"cost_annual\": 108000.0,\n", + " \"confidence\": \"confirmed\"\n", + " }\n", + " ],\n", + " \"totals\": {\n", + " \"consumption_tokens_monthly\": 17623.529411764706,\n", + " \"subscription_tokens_monthly\": 35000.0,\n", + " \"gross_tokens_monthly\": 52624.0,\n", + " \"allowance_tokens_monthly\": 250,\n", + " \"billable_tokens_monthly\": 52374,\n", + " \"currency\": \"USD\",\n", + " \"effective_rate\": 1.0,\n", + " \"token_cost_monthly\": 52374.0,\n", + " \"token_cost_annual\": 628488.0,\n", + " \"list_cost_annual\": 628488.0,\n", + " \"concession_saving_annual\": 0.0,\n", + " \"tts_cost_annual\": 38400.0,\n", + " \"grand_total_annual\": 666888.0,\n", + " \"cost_per_interaction\": 0.38064383561643833\n", + " },\n", + " \"tts\": {\n", + " \"tier\": \"advanced\",\n", + " \"rate_per_million_chars\": 20.0,\n", + " \"total_chars_monthly\": 200000000.0,\n", + " \"free_share\": 0.2,\n", + " \"billable_chars_monthly\": 160000000.0,\n", + " \"billed_millions_monthly\": 160,\n", + " \"cost_monthly\": 3200.0,\n", + " \"cost_annual\": 38400.0\n", + " },\n", + " \"warnings\": [\n", + " \"AI Summary and Insights: 120,000 units not billed \\u2014 Agent Copilot is enabled, and Genesys does not charge Supervisor summaries and insights alongside it.\",\n", + " \"Voice bots: the published 15-second per-call round-up adds 12.5% to billable bot minutes at a 40s average.\",\n", + " \"Predictive Engagement: No charge for token usage \\u2014 shown at $0 because Genesys publishes it as included.\"\n", + " ],\n", + " \"meta\": {\n", + " \"client\": \"\",\n", + " \"prepared_date\": \"\",\n", + " \"prepared_by\": \"\"\n", + " }\n", + "}\n", + "```\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Data appendix — LLM-readable dump of the priced scenario ────────\n", + "# Renders backstage only (JupyterLab / nbconvert exports) as ONE markdown\n", + "# display, so the .md export carries it verbatim — the tables first, the\n", + "# fenced JSON block LAST: that block is the machine-readable source of\n", + "# truth, and it names the rate card that produced these numbers.\n", + "_payload = result_json(RESULT, INPUTS, meta={\n", + " \"client\": ENGAGEMENT[\"client\"],\n", + " \"prepared_date\": ENGAGEMENT[\"prepared_date\"],\n", + " \"prepared_by\": ENGAGEMENT[\"prepared_by\"],\n", + "})\n", + "\n", + "_md = [\"## Data appendix — the priced scenario\\n\"]\n", + "_who = \" · \".join(str(ENGAGEMENT[_k]).strip() for _k in\n", + " (\"client\", \"prepared_date\", \"prepared_by\")\n", + " if str(ENGAGEMENT.get(_k, \"\")).strip())\n", + "_md.append(f\"**Engagement** — {_who or 'master copy — placeholders, no client data.'}\\n\")\n", + "_md.append(f\"**Rate card** — {RATE_CARD_SOURCE} (published {RATE_CARD_SOURCE_DATE}). \"\n", + " f\"**TTS** — {TTS_SOURCE} (published {TTS_SOURCE_DATE}).\\n\")\n", + "\n", + "_md.append(\"\\n### Published rate card\\n\")\n", + "_md.append(pd.DataFrame(rate_card_rows()).to_markdown(index=False))\n", + "\n", + "_md.append(\"\\n\\n### Cost by feature\\n\")\n", + "_md.append(_df.to_markdown(index=False, floatfmt=\",.2f\"))\n", + "\n", + "_md.append(\"\\n\\n### Scenario comparison\\n\")\n", + "_md.append(_cmp.to_markdown(index=False, floatfmt=\",.2f\"))\n", + "\n", + "if RESULT.warnings:\n", + " _md.append(\"\\n\\n### Applied rules and warnings\\n\")\n", + " _md.extend(f\"\\n- {_w}\" for _w in RESULT.warnings)\n", + "\n", + "_md.append(\"\\n\\n### Model state (JSON)\\n\")\n", + "_md.append(\"\\n```json\\n\" + _json.dumps(_payload, indent=2, default=str) + \"\\n```\\n\")\n", + "\n", + "backstage_md(\"\".join(_md))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "048f3603dfae494cb523cdcc8e2dd626": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Apply free monthly allowance", + "last_changed_at": "", + "layout": "IPY_MODEL_5cf79490530246d8b71cd10d75ddb06b", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "07a69926b1a84dcf88d730b1f30d8747": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.number.NumberInputWidget", + "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Avg bot seconds per call", + "layout": "IPY_MODEL_139bd11668064d40acd4af1dede68bdb", + "layout_path": null, + "max": 1800.0, + "min": 0.0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "step": 5.0, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 40.0 + } + }, + "11bd04950ae549028549cdaa209a8d7d": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Speech & Text Analytics", + "last_changed_at": "", + "layout": "IPY_MODEL_331d8be847a94f6ea9a0aeceff5818e0", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "139bd11668064d40acd4af1dede68bdb": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2734f5884f124ab2a6edd5bdeab68c91": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Agentic Virtual Agent", + "last_changed_at": "", + "layout": "IPY_MODEL_bdc19f9871c6487a81a7b53565a3f63f", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "29ae0e0df70e49afab66449fcf1f228f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2e97496d4b6a4c02a8f117727aa5e33d": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "Named", + "Concurrent" + ], + "disabled": false, + "hidden": false, + "label": "Licence model", + "layout": "IPY_MODEL_bc2926c100d04099b893dd3b3b4bb94e", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "Named" + } + }, + "3123a79e0e124ea7b0580f4a40819438": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Agent Copilot", + "last_changed_at": "", + "layout": "IPY_MODEL_ca6898939ee9424b8a97c11b64c92d17", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "31b644ea0ae848638778c2db4c2ce5eb": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "323378ad85a549bb9a1bd9e6ce0c08dd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "331d8be847a94f6ea9a0aeceff5818e0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "36902addac574d6aa0c82e46bf5124ce": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · AI Translate", + "last_changed_at": "", + "layout": "IPY_MODEL_c2905c6590e84a5fa0b998d68f2317cd", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "3b644ae2355e4cb1864299ceebf2bb03": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3dec4b4b4a524e7299866052c64e7ef2": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Digital Bot", + "last_changed_at": "", + "layout": "IPY_MODEL_a8f9d53b36044184978dec9d09075e57", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "40b7dcd02e8243d69960afbbe03d5ecf": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "JPY", + "USD" + ], + "disabled": false, + "hidden": false, + "label": "Currency", + "layout": "IPY_MODEL_3b644ae2355e4cb1864299ceebf2bb03", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "USD" + } + }, + "40bf51c5779a4ea9a28f57451000edcd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "41cce54706e74b598f9c9e183113a813": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.slider.SliderWidget", + "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: hidden;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\n /*const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Concession off list (%)", + "layout": "IPY_MODEL_ff81b618c2ff4412b5a6b49a93983b30", + "layout_path": null, + "max": 75, + "min": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 0 + } + }, + "43711e3c02e04aaeaceff10e803b68c4": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.number.NumberInputWidget", + "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Licensed agent users", + "layout": "IPY_MODEL_f82fcf066d5b473f91c562131fed2e24", + "layout_path": null, + "max": 100000.0, + "min": 0.0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "step": 10.0, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 500.0 + } + }, + "45b9e1696746455ca02b0901fade0340": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "531a96090cf444f084cac34ff47e9e3f": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Virtual Agent", + "last_changed_at": "", + "layout": "IPY_MODEL_56dcff5c4a5b413a8d93fe117b9f9b84", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "54ee67bbbc9b4e14ab008c4a9886282a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "56dcff5c4a5b413a8d93fe117b9f9b84": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5b220853ba384edc8abb71fd394d2cd1": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · WhatsApp Messaging", + "last_changed_at": "", + "layout": "IPY_MODEL_45b9e1696746455ca02b0901fade0340", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "5cf79490530246d8b71cd10d75ddb06b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6985548a90314be4b3b7cb910eed549f": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · AI Summary & Insights", + "last_changed_at": "", + "layout": "IPY_MODEL_e22899eb16894e1288389fc25dce2d09", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "721b91d57dc647f2a158d01e913461cb": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.select.SelectWidget", + "_css": "\n .mljar-select-container {\n position: relative;\n display: flex;\n flex-direction: column;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: visible;\n }\n\n .mljar-select-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-select-control {\n position: relative;\n display: flex;\n align-items: center;\n cursor: default;\n overflow: visible;\n }\n\n .mljar-select-container.is-open {\n z-index: 20;\n }\n\n .mljar-select-widget-input {\n width: 100%;\n min-height: 40px;\n padding: 9px 36px 9px 10px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n line-height: 1.4;\n transition: border-color 0.15s ease, box-shadow 0.15s ease;\n\n appearance: none !important;\n background-color: #ffffff !important;\n color: #0f172a !important;\n cursor: default;\n }\n\n .mljar-select-widget-input:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: none;\n cursor: text;\n }\n\n .mljar-select-caret {\n position: absolute;\n right: 12px;\n top: 50%;\n width: 8px;\n height: 8px;\n border-right: 1.5px solid #0f172a;\n border-bottom: 1.5px solid #0f172a;\n transform: translateY(-65%) rotate(45deg);\n pointer-events: auto;\n opacity: 0.5;\n transition: transform 0.18s ease, opacity 0.18s ease;\n }\n\n .mljar-select-container.is-open .mljar-select-caret {\n opacity: 1;\n transform: translateY(-35%) rotate(225deg);\n }\n\n .mljar-select-dropdown {\n display: none;\n position: fixed;\n z-index: 10000;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);\n overflow: hidden;\n }\n\n .mljar-select-list {\n max-height: 260px;\n overflow-y: auto;\n }\n\n .mljar-select-option {\n display: block;\n width: 100%;\n padding: 9px 10px;\n border: 0;\n background: transparent;\n color: #0f172a;\n text-align: left;\n cursor: pointer;\n font: inherit;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-select-option:hover {\n background: #f3f3f4;\n }\n\n .mljar-select-option:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-option.is-selected {\n background: #e6f2ff;\n color: #007bff;\n font-weight: 600;\n }\n\n .mljar-select-option.is-selected:hover,\n .mljar-select-option.is-selected:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-select-empty {\n display: none;\n padding: 10px;\n color: #616673;\n font-size: 0.95em;\n }\n\n .mljar-select-widget-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-select-control.is-disabled .mljar-select-caret {\n opacity: 0.45;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const normalize = value => String(value ?? \"\").toLowerCase().trim();\n const getChoices = () =>\n Array.isArray(model.get(\"choices\")) ? [...model.get(\"choices\")] : [];\n const isDisabled = () => !!model.get(\"disabled\");\n const isHidden = () => !!model.get(\"hidden\");\n\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-select-container\");\n\n if (model.get(\"label\")) {\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-select-label\");\n topLabel.innerHTML = model.get(\"label\");\n container.appendChild(topLabel);\n }\n\n const control = document.createElement(\"div\");\n control.classList.add(\"mljar-select-control\");\n\n const input = document.createElement(\"input\");\n input.type = \"text\";\n input.classList.add(\"mljar-select-widget-input\");\n input.autocomplete = \"off\";\n input.spellcheck = false;\n\n const caret = document.createElement(\"div\");\n caret.classList.add(\"mljar-select-caret\");\n\n control.appendChild(input);\n control.appendChild(caret);\n\n const dropdown = document.createElement(\"div\");\n dropdown.classList.add(\"mljar-select-dropdown\");\n\n const list = document.createElement(\"div\");\n list.classList.add(\"mljar-select-list\");\n\n const emptyState = document.createElement(\"div\");\n emptyState.classList.add(\"mljar-select-empty\");\n emptyState.textContent = \"No matches\";\n\n dropdown.appendChild(list);\n dropdown.appendChild(emptyState);\n\n container.appendChild(control);\n el.appendChild(container);\n\n let isOpen = false;\n let filteredChoices = [];\n let lastCommittedValue = \"\";\n let isEditing = false;\n document.body.appendChild(dropdown);\n\n const updateDropdownPosition = () => {\n if (!isOpen) {\n return;\n }\n const rect = control.getBoundingClientRect();\n dropdown.style.top = `${rect.bottom + 6}px`;\n dropdown.style.left = `${rect.left}px`;\n dropdown.style.width = `${rect.width}px`;\n };\n\n const setOpen = next => {\n if (isDisabled()) {\n isOpen = false;\n } else {\n isOpen = !!next;\n }\n container.classList.toggle(\"is-open\", isOpen);\n dropdown.style.display = isOpen ? \"block\" : \"none\";\n if (isOpen) {\n updateDropdownPosition();\n }\n };\n\n const updateDisabledState = () => {\n const disabled = isDisabled();\n input.disabled = disabled;\n control.classList.toggle(\"is-disabled\", disabled);\n };\n\n const updateHiddenState = () => {\n container.style.display = isHidden() ? \"none\" : \"\";\n };\n\n const syncInputWithValue = () => {\n const value = model.get(\"value\") || \"\";\n lastCommittedValue = value;\n if (!isEditing) {\n input.value = value;\n }\n };\n\n const filterChoices = query => {\n const normalizedQuery = normalize(query);\n const allChoices = getChoices();\n if (!normalizedQuery) {\n return allChoices;\n }\n return allChoices.filter(choice =>\n normalize(choice).includes(normalizedQuery)\n );\n };\n\n const renderList = () => {\n list.innerHTML = \"\";\n filteredChoices.forEach(choice => {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.classList.add(\"mljar-select-option\");\n if (choice === model.get(\"value\")) {\n option.classList.add(\"is-selected\");\n }\n option.textContent = choice;\n option.addEventListener(\"mousedown\", event => {\n event.preventDefault();\n event.stopPropagation();\n model.set(\"value\", choice);\n model.save_changes();\n isEditing = false;\n syncInputWithValue();\n renderList();\n setOpen(false);\n });\n list.appendChild(option);\n });\n\n const hasMatches = filteredChoices.length > 0;\n list.style.display = hasMatches ? \"block\" : \"none\";\n emptyState.style.display = hasMatches ? \"none\" : \"block\";\n };\n\n const refreshList = () => {\n filteredChoices = filterChoices(input.value);\n renderList();\n };\n\n const openWithCurrentQuery = () => {\n isEditing = true;\n input.value = \"\";\n refreshList();\n setOpen(true);\n };\n\n const closeDropdown = () => {\n isEditing = false;\n setOpen(false);\n input.value = lastCommittedValue;\n };\n\n control.addEventListener(\"click\", event => {\n event.stopPropagation();\n if (isDisabled()) {\n return;\n }\n if (event.target === caret && isOpen) {\n closeDropdown();\n input.blur();\n return;\n }\n openWithCurrentQuery();\n input.focus();\n });\n\n input.addEventListener(\"input\", () => {\n if (isDisabled()) {\n return;\n }\n refreshList();\n setOpen(true);\n });\n\n input.addEventListener(\"focus\", () => {\n if (isDisabled()) {\n return;\n }\n openWithCurrentQuery();\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n input.value = lastCommittedValue;\n });\n\n const handleDocumentClick = event => {\n if (!container.contains(event.target) && !dropdown.contains(event.target)) {\n closeDropdown();\n }\n };\n\n document.addEventListener(\"click\", handleDocumentClick);\n window.addEventListener(\"resize\", updateDropdownPosition);\n document.addEventListener(\"scroll\", updateDropdownPosition, true);\n\n model.on(\"change:value\", () => {\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:choices\", () => {\n const choices = getChoices();\n if (!choices.includes(model.get(\"value\")) && choices.length > 0) {\n model.set(\"value\", choices[0]);\n model.save_changes();\n return;\n }\n syncInputWithValue();\n refreshList();\n });\n\n model.on(\"change:disabled\", () => {\n updateDisabledState();\n if (isDisabled()) {\n closeDropdown();\n }\n });\n\n model.on(\"change:hidden\", () => {\n updateHiddenState();\n });\n\n updateDisabledState();\n updateHiddenState();\n syncInputWithValue();\n refreshList();\n setOpen(false);\n\n return () => {\n dropdown.remove();\n document.removeEventListener(\"click\", handleDocumentClick);\n window.removeEventListener(\"resize\", updateDropdownPosition);\n document.removeEventListener(\"scroll\", updateDropdownPosition, true);\n };\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "choices": [ + "advanced", + "standard" + ], + "disabled": false, + "hidden": false, + "label": "TTS voice tier", + "layout": "IPY_MODEL_b934430c4d8c492399282c7fc021db5e", + "layout_path": null, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": "advanced" + } + }, + "7cb2b15ae03040c28ca477e3ce719c19": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7f5ce91bf3d84428aff173d782f179c8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "81f6e8c010564b738258f31364b23954": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.number.NumberInputWidget", + "_css": "\n .mljar-number-container {\n display: flex;\n flex-direction: column;\n width: 100%;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n box-sizing: border-box;\n }\n\n .mljar-number-label {\n padding-top: 6px;\n margin-bottom: 4px;\n font-weight: 600;\n }\n\n .mljar-number-field-row {\n display: flex;\n align-items: stretch;\n width: 100%;\n min-height: 40px;\n border: 1px solid #cfd1d5;\n border-radius: 6px;\n background: #ffffff;\n box-sizing: border-box;\n overflow: hidden;\n }\n\n .mljar-number-input {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 100%;\n padding: 7px 10px;\n border: 0;\n border-radius: 0;\n background: #ffffff;\n box-sizing: border-box;\n background-color: #ffffff !important;\n color: #0f172a !important;\n font: inherit;\n line-height: 1.2;\n -moz-appearance: textfield;\n }\n\n .mljar-number-input::-webkit-outer-spin-button,\n .mljar-number-input::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n .mljar-number-input:disabled {\n background: #f5f5f5;\n color: #888;\n cursor: not-allowed;\n }\n\n .mljar-number-input:focus {\n outline: none;\n }\n\n .mljar-number-field-row:focus-within {\n border-color: #007bff;\n box-shadow: none;\n }\n\n .mljar-number-field-row:focus-within .mljar-number-controls {\n border-left-color: #007bff;\n }\n\n .mljar-number-controls {\n display: flex;\n align-items: stretch;\n flex: 0 0 auto;\n border-left: 1px solid #cfd1d5;\n background: #f1f1f2;\n }\n\n .mljar-number-step-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 38px;\n min-width: 38px;\n min-height: 100%;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: #0f172a;\n font: inherit;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n cursor: pointer;\n padding: 0;\n user-select: none;\n -webkit-user-select: none;\n touch-action: manipulation;\n transition: background-color 0.14s ease, color 0.14s ease;\n }\n\n .mljar-number-step-up {\n border-left: 1px solid #cfd1d5;\n }\n\n .mljar-number-step-btn:hover {\n background: #f3f3f4;\n }\n\n .mljar-number-step-btn:active {\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:focus-visible {\n outline: none;\n background: #e6f2ff;\n color: #007bff;\n }\n\n .mljar-number-step-btn:disabled {\n background: #f5f5f5;\n color: #aaa;\n cursor: not-allowed;\n }\n\n @media (max-width: 768px) {\n .mljar-number-field-row {\n min-height: 44px;\n }\n\n .mljar-number-input {\n min-height: 44px;\n padding: 8px 12px;\n }\n\n .mljar-number-step-btn {\n font-size: 19px;\n width: 44px;\n min-width: 44px;\n }\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-number-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-number-label\");\n\n const fieldRow = document.createElement(\"div\");\n fieldRow.classList.add(\"mljar-number-field-row\");\n\n const input = document.createElement(\"input\");\n input.type = \"number\";\n input.classList.add(\"mljar-number-input\");\n\n const decrementBtn = document.createElement(\"button\");\n decrementBtn.type = \"button\";\n decrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-down\");\n decrementBtn.textContent = \"-\";\n decrementBtn.setAttribute(\"aria-label\", \"Decrease value\");\n\n const incrementBtn = document.createElement(\"button\");\n incrementBtn.type = \"button\";\n incrementBtn.classList.add(\"mljar-number-step-btn\", \"mljar-number-step-up\");\n incrementBtn.textContent = \"+\";\n incrementBtn.setAttribute(\"aria-label\", \"Increase value\");\n\n const controls = document.createElement(\"div\");\n controls.classList.add(\"mljar-number-controls\");\n\n controls.appendChild(decrementBtn);\n controls.appendChild(incrementBtn);\n fieldRow.appendChild(input);\n fieldRow.appendChild(controls);\n\n container.appendChild(topLabel);\n container.appendChild(fieldRow);\n el.appendChild(container);\n\n function clamp(val, min, max) {\n if (Number.isFinite(min) && val < min) return min;\n if (Number.isFinite(max) && val > max) return max;\n return val;\n }\n\n function normalizeStep(step) {\n return Number.isFinite(step) && step > 0 ? step : 1;\n }\n\n function snapToStep(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = Math.round((value - base) / safeStep);\n const snapped = base + steps * safeStep;\n const precision = Math.max(\n 0,\n (String(safeStep).split(\".\")[1] || \"\").length\n );\n\n return Number(snapped.toFixed(precision + 2));\n }\n\n function isOnStepGrid(value, min, step) {\n const safeStep = normalizeStep(step);\n const base = Number.isFinite(min) ? min : 0;\n const steps = (value - base) / safeStep;\n const nearest = Math.round(steps);\n const epsilon = Math.max(1e-9, safeStep * 1e-9);\n\n return Math.abs(steps - nearest) <= epsilon;\n }\n\n function getCurrentBounds() {\n return {\n min: Number(model.get(\"min\")),\n max: Number(model.get(\"max\")),\n };\n }\n\n function isTransientDraft(raw) {\n return raw === \"\" || raw === \"-\" || raw === \".\" || raw === \"-.\";\n }\n\n let isEditing = false;\n const INPUT_COMMIT_DEBOUNCE_MS = 400;\n\n function clearPendingDraftCommit() {\n if (debounceTimer) clearTimeout(debounceTimer);\n pendingDraftValue = null;\n }\n\n function parseDraftValue(rawValue) {\n const raw = String(rawValue).trim();\n if (isTransientDraft(raw)) {\n return { kind: \"transient\" };\n }\n\n const value = Number(raw);\n if (!Number.isFinite(value)) {\n return { kind: \"invalid\" };\n }\n\n return { kind: \"number\", value };\n }\n\n function commitValue(nextValue, saveNow = true) {\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n const parsed = parseDraftValue(nextValue);\n if (parsed.kind !== \"number\") {\n syncFromModel();\n return;\n }\n\n let v = parsed.value;\n v = clamp(v, min, max);\n v = snapToStep(v, min, step);\n v = clamp(v, min, max);\n input.value = String(v);\n model.set(\"value\", v);\n\n if (saveNow) {\n model.save_changes();\n }\n }\n\n function syncFromModel() {\n topLabel.innerHTML = model.get(\"label\") || \"Enter number\";\n\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const step = Number(model.get(\"step\"));\n\n if (Number.isFinite(min)) input.min = String(min); else input.removeAttribute(\"min\");\n if (Number.isFinite(max)) input.max = String(max); else input.removeAttribute(\"max\");\n if (Number.isFinite(step)) input.step = String(step); else input.removeAttribute(\"step\");\n\n const v = Number(model.get(\"value\"));\n if (!isEditing) {\n input.value = Number.isFinite(v) ? String(v) : \"\";\n }\n\n const disabled = !!model.get(\"disabled\");\n input.disabled = disabled;\n incrementBtn.disabled = disabled;\n decrementBtn.disabled = disabled;\n\n const hidden = !!model.get(\"hidden\");\n container.style.display = hidden ? \"none\" : \"flex\";\n }\n\n let debounceTimer = null;\n let pendingDraftValue = null;\n input.addEventListener(\"focus\", () => {\n isEditing = true;\n });\n\n input.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n\n const parsed = parseDraftValue(input.value);\n if (parsed.kind !== \"number\") {\n clearPendingDraftCommit();\n return;\n }\n\n const v = parsed.value;\n const { min, max } = getCurrentBounds();\n const step = Number(model.get(\"step\"));\n if (Number.isFinite(min) && v < min) {\n clearPendingDraftCommit();\n return;\n }\n if (Number.isFinite(max) && v > max) {\n clearPendingDraftCommit();\n return;\n }\n if (!isOnStepGrid(v, min, step)) {\n clearPendingDraftCommit();\n return;\n }\n\n pendingDraftValue = v;\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n if (pendingDraftValue === null) return;\n model.set(\"value\", pendingDraftValue);\n model.save_changes();\n pendingDraftValue = null;\n }, INPUT_COMMIT_DEBOUNCE_MS);\n });\n\n input.addEventListener(\"blur\", () => {\n isEditing = false;\n clearPendingDraftCommit();\n commitValue(input.value, true);\n });\n\n input.addEventListener(\"keydown\", event => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n input.blur();\n }\n });\n\n incrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base + step, min, step));\n });\n\n decrementBtn.addEventListener(\"click\", () => {\n if (model.get(\"disabled\")) return;\n const current = Number(model.get(\"value\"));\n const step = normalizeStep(Number(model.get(\"step\")));\n const min = Number(model.get(\"min\"));\n const base = Number.isFinite(current) ? current : 0;\n commitValue(snapToStep(base - step, min, step));\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:step\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Token list price", + "layout": "IPY_MODEL_29ae0e0df70e49afab66449fcf1f228f", + "layout_path": null, + "max": 1000.0, + "min": 0.01, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "step": 0.01, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 1.0 + } + }, + "8554784a0c37479cbbf5e7a1461d4948": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Genesys Cloud Copilot", + "last_changed_at": "", + "layout": "IPY_MODEL_ecbc38e0291f4b278058e0c61fef161a", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "8def1084bb7b409ead5fb3525714d047": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Predictive Engagement", + "last_changed_at": "", + "layout": "IPY_MODEL_31b644ea0ae848638778c2db4c2ce5eb", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "93e8bc988e7c4a77b8a02288eafc28d3": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.slider.SliderWidget", + "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: hidden;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\n /*const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Agentic VA share (%)", + "layout": "IPY_MODEL_b7a4083a3d724d5988b4421db1188a7a", + "layout_path": null, + "max": 100, + "min": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 5 + } + }, + "98be0f8104624716bdecbadf290e403c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9efffb449ca04588a15ad1cc883fd0c2": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Predictive Routing", + "last_changed_at": "", + "layout": "IPY_MODEL_54ee67bbbc9b4e14ab008c4a9886282a", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": false + } + }, + "a7276468601c4dd7b23a57237e106fe9": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · Voice Bot", + "last_changed_at": "", + "layout": "IPY_MODEL_98be0f8104624716bdecbadf290e403c", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "a8f9d53b36044184978dec9d09075e57": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "aead8ab94f2140dfb450812993ce398d": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.slider.SliderWidget", + "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: hidden;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\n /*const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Virtual Agent share (%)", + "layout": "IPY_MODEL_7cb2b15ae03040c28ca477e3ce719c19", + "layout_path": null, + "max": 100, + "min": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 15 + } + }, + "b7a4083a3d724d5988b4421db1188a7a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b934430c4d8c492399282c7fc021db5e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bbd6f38e287443e0a123e5db21f7d79d": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Enable · AI Scoring", + "last_changed_at": "", + "layout": "IPY_MODEL_40bf51c5779a4ea9a28f57451000edcd", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "bc2926c100d04099b893dd3b3b4bb94e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bdc19f9871c6487a81a7b53565a3f63f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c2905c6590e84a5fa0b998d68f2317cd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ca6898939ee9424b8a97c11b64c92d17": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e22899eb16894e1288389fc25dce2d09": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e3264e3f98f54182b7b9a415aa8a6e75": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.slider.SliderWidget", + "_css": "\n .mljar-slider-container {\n --mljar-slider-thumb-size: 16px;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 0;\n width: 100%;\n max-width: 100%;\n min-width: 120px;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n font-size: 14px;\n font-weight: normal;\n color: #0f172a;\n padding-left: 4px;\n padding-right: 4px;\n overflow: hidden;\n box-sizing: border-box;\n }\n\n .mljar-slider-stage {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n padding-top: 20px;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-floating-value {\n position: absolute;\n top: 0;\n left: 8px;\n transform: translateX(-50%);\n color: #007bff;\n font-weight: 700;\n font-size: 0.95em;\n line-height: 1;\n text-align: center;\n white-space: nowrap;\n pointer-events: none;\n z-index: 1;\n }\n\n .mljar-slider-top-label {\n padding-top: 6px;\n margin-bottom: 6px;\n font-weight: 600;\n line-height: 1.2;\n }\n\n .mljar-slider-row {\n position: relative;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n overflow: visible;\n box-sizing: border-box;\n }\n\n .mljar-slider-minmax-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n margin-top: 6px;\n color: #616673;\n font-size: 0.9em;\n line-height: 1.2;\n box-sizing: border-box;\n }\n\n .mljar-slider-min-label,\n .mljar-slider-max-label {\n color: inherit;\n white-space: nowrap;\n }\n\n .mljar-slider-input {\n display: block;\n width: 100%;\n max-width: 100%;\n min-width: 0;\n background: transparent;\n -webkit-appearance: none;\n appearance: none;\n border: none;\n height: 24px;\n padding: 0;\n margin: 0;\n cursor: pointer;\n box-sizing: border-box;\n }\n\n .mljar-slider-input:focus {\n outline: none;\n }\n\n .mljar-slider-input:focus-visible::-webkit-slider-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n .mljar-slider-input:focus-visible::-moz-range-thumb {\n box-shadow: 0 0 0 3px #e6f2ff;\n }\n\n /* Track */\n .mljar-slider-input::-webkit-slider-runnable-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n margin: auto;\n }\n .mljar-slider-input::-moz-range-track {\n height: 6px;\n background: #e0e0e0;\n border-radius: 6px;\n }\n\n /* Thumb */\n .mljar-slider-input::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n margin-top: -5px;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n .mljar-slider-input::-moz-range-thumb {\n width: var(--mljar-slider-thumb-size);\n height: var(--mljar-slider-thumb-size);\n border-radius: 50%;\n background: #007bff;\n cursor: pointer;\n transition: transform 0.14s ease, background-color 0.14s ease;\n }\n\n .mljar-slider-input:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n }\n\n .mljar-slider-input:disabled::-webkit-slider-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:disabled::-moz-range-thumb {\n cursor: not-allowed;\n transform: none;\n }\n\n .mljar-slider-input:active::-webkit-slider-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n .mljar-slider-input:active::-moz-range-thumb {\n transform: scale(1.08);\n background: #007bff;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"div\");\n container.classList.add(\"mljar-slider-container\");\n\n const topLabel = document.createElement(\"div\");\n topLabel.classList.add(\"mljar-slider-top-label\");\n\n const sliderStage = document.createElement(\"div\");\n sliderStage.classList.add(\"mljar-slider-stage\");\n\n const floatingValueLabel = document.createElement(\"div\");\n floatingValueLabel.classList.add(\"mljar-slider-floating-value\");\n\n const sliderRow = document.createElement(\"div\");\n sliderRow.classList.add(\"mljar-slider-row\");\n\n const slider = document.createElement(\"input\");\n slider.type = \"range\";\n slider.classList.add(\"mljar-slider-input\");\n\n const minMaxRow = document.createElement(\"div\");\n minMaxRow.classList.add(\"mljar-slider-minmax-row\");\n\n const minLabel = document.createElement(\"span\");\n minLabel.classList.add(\"mljar-slider-min-label\");\n\n const maxLabel = document.createElement(\"span\");\n maxLabel.classList.add(\"mljar-slider-max-label\");\n\n function positionFloatingValue() {\n const min = Number(model.get(\"min\"));\n const max = Number(model.get(\"max\"));\n const value = Number(model.get(\"value\"));\n const range = max - min;\n const ratio = range <= 0 ? 0 : (value - min) / range;\n const clampedRatio = Math.max(0, Math.min(1, ratio));\n const computed = getComputedStyle(container);\n const thumbSize =\n parseFloat(computed.getPropertyValue(\"--mljar-slider-thumb-size\")) || 16;\n const inputWidth = slider.clientWidth || 0;\n const stageWidth = sliderStage.clientWidth || inputWidth || 0;\n const labelWidth = floatingValueLabel.offsetWidth || 0;\n\n if (inputWidth <= 0 || stageWidth <= 0) {\n return;\n }\n\n const sliderOffsetLeft = slider.offsetLeft || 0;\n const usableWidth = Math.max(0, inputWidth - thumbSize);\n const idealCenter =\n sliderOffsetLeft + thumbSize / 2 + clampedRatio * usableWidth;\n const halfLabel = labelWidth / 2;\n const minCenter = halfLabel;\n const maxCenter = Math.max(halfLabel, stageWidth - halfLabel);\n const finalCenter = Math.min(Math.max(idealCenter, minCenter), maxCenter);\n\n floatingValueLabel.style.left = `${finalCenter}px`;\n }\n\n function syncFromModel() {\n slider.min = model.get(\"min\");\n slider.max = model.get(\"max\");\n slider.value = model.get(\"value\");\n floatingValueLabel.textContent = String(model.get(\"value\"));\n minLabel.textContent = String(model.get(\"min\"));\n maxLabel.textContent = String(model.get(\"max\"));\n\n slider.disabled = !!model.get(\"disabled\");\n topLabel.textContent = model.get(\"label\") || \"Select number\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"flex\";\n positionFloatingValue();\n }\n\n let debounceTimer = null;\n slider.addEventListener(\"input\", () => {\n if (model.get(\"disabled\")) return;\n model.set(\"value\", Number(slider.value));\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n model.save_changes();\n }, 100);\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:min\", syncFromModel);\n model.on(\"change:max\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n sliderRow.appendChild(slider);\n minMaxRow.appendChild(minLabel);\n minMaxRow.appendChild(maxLabel);\n\n container.appendChild(topLabel);\n sliderStage.appendChild(floatingValueLabel);\n sliderStage.appendChild(sliderRow);\n sliderStage.appendChild(minMaxRow);\n container.appendChild(sliderStage);\n el.appendChild(container);\n\n syncFromModel();\n\n const resizeObserver = new ResizeObserver(() => {\n positionFloatingValue();\n });\n resizeObserver.observe(sliderStage);\n\n return () => {\n resizeObserver.disconnect();\n };\n\n // ---- read cell id (no DOM modifications) ----\n /*const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }*/\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Bot-only share of voice (%)", + "layout": "IPY_MODEL_323378ad85a549bb9a1bd9e6ce0c08dd", + "layout_path": null, + "max": 100, + "min": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": 30 + } + }, + "ecbc38e0291f4b278058e0c61fef161a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f82fcf066d5b473f91c562131fed2e24": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "fba8b8a942ba4efd9055fa91abe10f2a": { + "model_module": "anywidget", + "model_module_version": "~0.11.*", + "model_name": "AnyModel", + "state": { + "_anywidget_id": "mercury.checkbox.CheckboxWidget", + "_css": "\n .mljar-checkbox-container {\n display: inline-flex;\n align-items: center;\n gap: 10px;\n cursor: pointer;\n user-select: none;\n -webkit-tap-highlight-color: transparent;\n font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;\n color: #0f172a;\n padding-left: 5px;\n }\n .mljar-checkbox-container.is-disabled {\n opacity: 0.6;\n cursor: not-allowed;\n }\n\n .mljar-checkbox-input {\n position: absolute;\n opacity: 0;\n width: 0;\n height: 0;\n }\n\n .mljar-checkbox-input:focus-visible + .mljar-checkbox-control {\n box-shadow: inset 0 0 0 2px #007bff;\n }\n\n .mljar-checkbox-label {\n font-size: 14px;\n line-height: 1.2;\n padding-top: 2px;\n }\n\n /* --- Toggle style (15% smaller) --- */\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control {\n position: relative;\n width: 34px;\n height: 19px;\n border-radius: 999px;\n background: #f1f1f2;\n border: 1px solid #cfd1d5;\n transition: background 150ms ease, border-color 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control {\n background: #007bff;\n border-color: #007bff;\n }\n .mljar-checkbox-container.is-toggle .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n top: 2px;\n left: 2px;\n width: 15px;\n height: 15px;\n border-radius: 50%;\n background: #ffffff;\n box-shadow: 0 1px 2px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.04);\n transition: transform 150ms ease;\n }\n .mljar-checkbox-container.is-toggle.is-checked .mljar-checkbox-control::after {\n transform: translateX(15px);\n }\n\n /* --- Classic box style --- */\n .mljar-checkbox-container.is-box .mljar-checkbox-control {\n width: 16px;\n height: 16px;\n border-radius: 4px;\n border: 1px solid #cfd1d5;\n background: #ffffff;\n display: inline-block;\n position: relative;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control {\n border-color: #007bff;\n background: #007bff;\n }\n .mljar-checkbox-container.is-box.is-checked .mljar-checkbox-control::after {\n content: \"\";\n position: absolute;\n left: 4px;\n top: 0px;\n width: 5px;\n height: 10px;\n border: solid #fff;\n border-width: 0 2px 2px 0;\n transform: rotate(45deg);\n }\n\n .mljar-checkbox-container:not(.is-disabled):hover .mljar-checkbox-control {\n border-color: #007bff;\n background: #f3f3f4;\n }\n ", + "_dom_classes": [], + "_esm": "\n function render({ model, el }) {\n const container = document.createElement(\"label\");\n container.classList.add(\"mljar-checkbox-container\");\n\n const input = document.createElement(\"input\");\n input.type = \"checkbox\";\n input.classList.add(\"mljar-checkbox-input\");\n\n const control = document.createElement(\"span\");\n control.classList.add(\"mljar-checkbox-control\");\n\n const text = document.createElement(\"span\");\n text.classList.add(\"mljar-checkbox-label\");\n\n container.appendChild(input);\n container.appendChild(control);\n container.appendChild(text);\n el.appendChild(container);\n\n function syncFromModel() {\n // appearance\n const a = model.get(\"appearance\") || \"toggle\";\n container.classList.remove(\"is-toggle\",\"is-box\");\n container.classList.add(`is-${a}`);\n input.setAttribute(\"role\", a === \"toggle\" ? \"switch\" : \"checkbox\");\n\n // value\n const v = !!model.get(\"value\");\n if (input.checked !== v) {\n input.checked = v;\n input.setAttribute(\"aria-checked\", String(v));\n }\n container.classList.toggle(\"is-checked\", v);\n\n // disabled\n const d = !!model.get(\"disabled\");\n input.disabled = d;\n container.classList.toggle(\"is-disabled\", d);\n\n // label\n text.textContent = model.get(\"label\") || \"\";\n\n // hidden (exists but not visible)\n container.style.display = model.get(\"hidden\") ? \"none\" : \"inline-flex\";\n }\n\n input.addEventListener(\"change\", () => {\n if (model.get(\"disabled\")) return;\n\n const v = input.checked;\n if (model.get(\"value\") !== v) {\n model.set(\"value\", v);\n model.set(\"last_changed_at\", new Date().toISOString());\n model.set(\"n_toggles\", (model.get(\"n_toggles\") || 0) + 1);\n model.save_changes();\n // model.send({ type: \"changed\", value: v });\n }\n });\n\n model.on(\"change:value\", syncFromModel);\n model.on(\"change:disabled\", syncFromModel);\n model.on(\"change:appearance\", syncFromModel);\n model.on(\"change:label\", syncFromModel);\n model.on(\"change:hidden\", syncFromModel);\n\n syncFromModel();\n\n // ---- read cell id (no DOM modifications) ----\n /*\n const ID_ATTR = \"data-cell-id\";\n const hostWithId = el.closest(`[${ID_ATTR}]`);\n const cellId = hostWithId ? hostWithId.getAttribute(ID_ATTR) : null;\n\n if (cellId) {\n model.set(\"cell_id\", cellId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: cellId });\n } else {\n const mo = new MutationObserver(() => {\n const host = el.closest(`[${ID_ATTR}]`);\n const newId = host?.getAttribute(ID_ATTR);\n \n if (newId) {\n model.set(\"cell_id\", newId);\n model.save_changes();\n model.send({ type: \"cell_id_detected\", value: newId });\n mo.disconnect();\n }\n });\n mo.observe(document.body, { attributes: true, subtree: true, attributeFilter: [ID_ATTR] });\n }\n */\n }\n export default { render };\n ", + "_model_module": "anywidget", + "_model_module_version": "~0.11.*", + "_model_name": "AnyModel", + "_view_count": null, + "_view_module": "anywidget", + "_view_module_version": "~0.11.*", + "_view_name": "AnyView", + "appearance": "toggle", + "cell_id": "", + "disabled": false, + "hidden": false, + "label": "Include text-to-speech", + "last_changed_at": "", + "layout": "IPY_MODEL_7f5ce91bf3d84428aff173d782f179c8", + "layout_path": null, + "n_toggles": 0, + "position": "sidebar", + "render_slot_id": null, + "source_cell_id": null, + "tabbable": null, + "tooltip": null, + "url_key": "", + "value": true + } + }, + "ff81b618c2ff4412b5a6b49a93983b30": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "2.0.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "2.0.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "2.0.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border_bottom": null, + "border_left": null, + "border_right": null, + "border_top": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + } + }, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/calculators/Genesys_Token_Calculator/pyproject.toml b/calculators/Genesys_Token_Calculator/pyproject.toml new file mode 100644 index 0000000..098eb3c --- /dev/null +++ b/calculators/Genesys_Token_Calculator/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "genesyscalc" +version = "0.1.0" +description = "Genesys Cloud AI token cost calculator — published rate card, 2026-07-12 (Mercury Notebook Pattern)" +requires-python = ">=3.10" +# The notebook is the deliverable (served with Mercury, exported via +# nbconvert, tables via tabulate) — the whole toolchain is a required +# runtime dependency, not an extra. `pip install -e .` must be enough. +dependencies = [ + "pandas>=2.0", + "plotly>=5.18", + "mercury>=3.2", + "jupyterlab>=4.0", + "ipywidgets>=8.0", + "nbconvert>=7", + "nbformat>=5.9", + "tabulate>=0.9", +] + +[project.optional-dependencies] +dev = ["pytest>=7.4", "mypy>=1.8"] + +[tool.setuptools.packages.find] +include = ["genesyscalc*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-q" + +[tool.mypy] +strict = true +packages = ["genesyscalc"] diff --git a/calculators/Genesys_Token_Calculator/scripts/export_report.py b/calculators/Genesys_Token_Calculator/scripts/export_report.py new file mode 100644 index 0000000..3d7917d --- /dev/null +++ b/calculators/Genesys_Token_Calculator/scripts/export_report.py @@ -0,0 +1,140 @@ +"""Export the token calculator as an LLM-readable report source. + +Executes the notebook ONCE (widget defaults — or whatever scenario you've +set and saved in the notebook), then converts the executed copy twice: + + exports/genesys_token_calculator.html — human-reviewable, full presentation + exports/genesys_token_calculator.md — leanest LLM input: presentation-tagged + cells (setup, widgets, tables, charts) + are stripped, a framing preamble is + prepended, and the data appendix ends + the file with one fenced JSON block — + the machine source of truth. + +Engagement identity (client, date, preparer) is read from the notebook's +``engagement-data`` cell and stamped into the preamble, alongside the +rate-card source date — a cost figure without its rate card is not auditable. + +Run from the calculator root: python scripts/export_report.py +""" +from __future__ import annotations + +import subprocess +import sys +import tempfile +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parent.parent +NOTEBOOK = ROOT / "notebooks" / "genesys_token_calculator.ipynb" +EXPORTS = ROOT / "exports" + +# Cells tagged with any of these never reach the .md export — they are stage +# presentation (source and widget-repr noise), not the priced record. +STRIP_TAGS_FROM_MD = '{"presentation"}' + + +def engagement_data() -> dict[str, Any]: + """Exec the engagement-data cell (same trick as tests/conftest.py).""" + import nbformat + + nb = nbformat.read(NOTEBOOK, as_version=4) + cells = [c for c in nb.cells if "engagement-data" in c.metadata.get("tags", [])] + assert len(cells) == 1, "expected exactly one engagement-data cell" + ns: dict[str, Any] = {} + exec(compile(cells[0].source, f"{NOTEBOOK.name} [engagement-data]", "exec"), ns) + return ns["ENGAGEMENT"] # type: ignore[no-any-return] + + +def preamble() -> str: + sys.path.insert(0, str(ROOT)) + from genesyscalc.ratecard import RATE_CARD_SOURCE, RATE_CARD_SOURCE_DATE + from genesyscalc.tts import TTS_SOURCE, TTS_SOURCE_DATE + + eng = engagement_data() + who = " · ".join( + str(eng[k]).strip() + for k in ("client", "prepared_date", "prepared_by") + if str(eng.get(k, "")).strip() + ) + line = who or "master copy — illustrative placeholders; no client data." + return "\n".join( + [ + "", + "**What this is** — a costed Genesys Cloud AI scenario, produced from", + "the Mercury-served token calculator. It is a **planning model**: list", + "rates unless a contracted rate was entered, and never a quote.", + "", + f"**Engagement** — {line}", + "", + f"**Rate card** — [{RATE_CARD_SOURCE}]({RATE_CARD_SOURCE}), published", + f"**{RATE_CARD_SOURCE_DATE}**. Text-to-speech is priced separately from", + f"[{TTS_SOURCE}]({TTS_SOURCE}) (published {TTS_SOURCE_DATE}) and is", + "billed per character, not in tokens — it is reported as its own line", + "and never folded into the token subtotal.", + "", + "**How to read it** — first the content as annotated source (the feature", + "catalogue, then the engagement data), then the scenario state, the", + "verification gate, and finally the data appendix: the published rate", + "card, the per-feature cost table, the scenario comparison, the applied", + "rules and warnings, and — last — **Model state (JSON)**, one fenced", + "`json` block. Where prose and JSON disagree, the JSON block is the", + "source of truth; its `rate_card.source_date` says which published rate", + "card produced these numbers.", + "", + "---", + "", + "", + ] + ) + + +def main() -> None: + if len(sys.argv) > 1 and sys.argv[1] not in NOTEBOOK.name: + sys.exit(f"no notebook matches {sys.argv[1]!r}") + EXPORTS.mkdir(exist_ok=True) + + with tempfile.TemporaryDirectory() as tmp: + executed = Path(tmp) / NOTEBOOK.name + # 1. Execute once — both formats convert the same scenario state. + # (Never combine --execute with TagRemovePreprocessor in one call: + # the cell could be stripped before it runs.) + subprocess.run( + [ + sys.executable, "-m", "nbconvert", "--execute", + "--to", "notebook", "--output", str(executed), str(NOTEBOOK), + ], + check=True, cwd=ROOT, + ) + # 2. HTML — full presentation, human review artifact. + subprocess.run( + [ + sys.executable, "-m", "nbconvert", "--to", "html", + "--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem, + str(executed), + ], + check=True, cwd=ROOT, + ) + # 3. Markdown — LLM artifact: strip presentation cells. + subprocess.run( + [ + sys.executable, "-m", "nbconvert", "--to", "markdown", + "--output-dir", str(EXPORTS), "--output", NOTEBOOK.stem, + "--TagRemovePreprocessor.enabled=True", + f"--TagRemovePreprocessor.remove_cell_tags={STRIP_TAGS_FROM_MD}", + str(executed), + ], + check=True, cwd=ROOT, + ) + + # 4. Prepend the framing preamble to the markdown export. + md = EXPORTS / f"{NOTEBOOK.stem}.md" + md.write_text(preamble() + md.read_text(encoding="utf-8"), encoding="utf-8") + + for p in sorted(EXPORTS.iterdir()): + if p.suffix in (".html", ".md"): + print(f"wrote {p.relative_to(ROOT)} ({p.stat().st_size / 1024:,.0f} KB)") + + +if __name__ == "__main__": + main() diff --git a/calculators/Genesys_Token_Calculator/tests/conftest.py b/calculators/Genesys_Token_Calculator/tests/conftest.py new file mode 100644 index 0000000..21f5072 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/conftest.py @@ -0,0 +1,70 @@ +"""Test plumbing: import path + notebook content served from tagged cells. + +Content and client data live in the notebook, never in ``.py`` — the cells +tagged ``topic-bank`` (the feature catalogue) and ``engagement-data`` (the +client's volumes and commercial terms) in +``notebooks/genesys_token_calculator.ipynb``. The fixtures below read those +cells with nbformat and exec them, so pytest pins the exact content the +deliverable ships (no kernel needed — tagged cells are self-contained by +contract). + +The published Genesys rate card is deliberately NOT here: it is a vendor +record nobody in this repo authors, so it lives in ``genesyscalc/ratecard.py`` +as an immutable anchor and is pinned by ``test_rate_card.py`` (see +docs/Calculator_Pattern_V1-00.md). + +The sys.path insert makes genesyscalc importable even without the master +venv active (the normal setup is ``pip install -e ".[dev]"`` into the +master-local ``.venv/``). +""" + +from __future__ import annotations + +import pathlib +import sys +from typing import Any + +import pytest + +MASTER_ROOT = pathlib.Path(__file__).resolve().parent.parent +sys.path.insert(0, str(MASTER_ROOT)) + +NOTEBOOK = MASTER_ROOT / "notebooks" / "genesys_token_calculator.ipynb" + + +def tagged_cell_ns(tag: str) -> dict[str, Any]: + """Exec the single cell carrying ``tag`` and return its namespace.""" + import nbformat + + nb = nbformat.read(NOTEBOOK, as_version=4) + cells = [c for c in nb.cells if tag in c.metadata.get("tags", [])] + assert len(cells) == 1, ( + f"expected exactly one cell tagged {tag!r} in {NOTEBOOK.name}, " + f"found {len(cells)}" + ) + ns: dict[str, Any] = {} + exec(compile(cells[0].source, f"{NOTEBOOK.name} [{tag}]", "exec"), ns) + return ns + + +@pytest.fixture(scope="session") +def catalogue_ns() -> dict[str, Any]: + """The executed namespace of the notebook's topic-bank cell.""" + return tagged_cell_ns("topic-bank") + + +@pytest.fixture(scope="session") +def features(catalogue_ns: dict[str, Any]) -> tuple[Any, ...]: + """The FEATURES tuple as the deliverable defines it.""" + return catalogue_ns["FEATURES"] # type: ignore[no-any-return] + + +@pytest.fixture(scope="session") +def feature_by_key(features: tuple[Any, ...]) -> dict[str, Any]: + return {f.key: f for f in features} + + +@pytest.fixture(scope="session") +def engagement() -> dict[str, Any]: + """The ENGAGEMENT dict as the deliverable's engagement-data cell ships it.""" + return tagged_cell_ns("engagement-data")["ENGAGEMENT"] # type: ignore[no-any-return] diff --git a/calculators/Genesys_Token_Calculator/tests/test_billing.py b/calculators/Genesys_Token_Calculator/tests/test_billing.py new file mode 100644 index 0000000..02fac17 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_billing.py @@ -0,0 +1,154 @@ +"""Tokens → dollars: the allowance, the rounding, and the commercial levers. + +Hand-checked first, then pinned. +""" + +from __future__ import annotations + +import pytest +from genesyscalc.billing import ( + MONTHS_PER_YEAR, + allowance_for, + billable_tokens_monthly, + cost_lines, + total_cost, +) +from genesyscalc.meters import LicenceModel, TokenPrice + +NAMED = LicenceModel.NAMED +CONCURRENT = LicenceModel.CONCURRENT + + +# ── the free monthly allowance ─────────────────────────────────────────── + + +def test_free_allowance_deducted_named() -> None: + """1,000 consumption tokens, named org: 1,000 − 250 = 750 billable.""" + assert billable_tokens_monthly(1_000, 0, NAMED) == 750 + + +def test_free_allowance_deducted_concurrent() -> None: + """Same consumption, concurrent org: 1,000 − 350 = 650.""" + assert billable_tokens_monthly(1_000, 0, CONCURRENT) == 650 + + +def test_allowance_floors_at_zero() -> None: + assert billable_tokens_monthly(120, 0, NAMED) == 0 + + +def test_allowance_does_not_carry_over() -> None: + """Two quiet months do not bank credit for a busy third. + + 120 → 0 billable. 120 → 0 billable. 400 → 150 billable, NOT 0: the + unused 130 + 130 is discarded, because the allowance "renew[s] each + month and do[es] not carry over to future months". + """ + assert billable_tokens_monthly(120, 0, NAMED) == 0 + assert billable_tokens_monthly(120, 0, NAMED) == 0 + assert billable_tokens_monthly(400, 0, NAMED) == 150 + + +def test_allowance_can_be_switched_off() -> None: + """So the gap is measurable on stage rather than merely asserted.""" + assert billable_tokens_monthly(1_000, 0, NAMED, apply_allowance=False) == 1_000 + assert allowance_for(NAMED, apply_allowance=False) == 0 + assert allowance_for(NAMED) == 250 + + +def test_allowance_applies_across_consumption_and_subscription() -> None: + """It is an org-level deduction, not a per-line one. + + 44.12 consumption → ceil 45; + 20,000 subscription = 20,045; − 250 = + 19,795. + """ + assert billable_tokens_monthly(44.117647, 20_000, NAMED) == 19_795 + + +# ── rounding ───────────────────────────────────────────────────────────── + + +def test_consumption_tokens_rounded_up_monthly() -> None: + """44.117647 tokens of bot time bills as 45.""" + assert billable_tokens_monthly(44.117647, 0, NAMED, apply_allowance=False) == 45 + + +def test_per_user_tokens_are_exact_not_rounded() -> None: + """500 named users × 40 = 20,000 exactly — no ceil on subscription.""" + assert billable_tokens_monthly(0, 20_000, NAMED, apply_allowance=False) == 20_000 + + +def test_negative_tokens_rejected() -> None: + with pytest.raises(ValueError, match="must not be negative"): + billable_tokens_monthly(-1, 0, NAMED) + + +# ── price, concession, currency ────────────────────────────────────────── + + +def test_concession_applies_to_msrp() -> None: + """$1.00 list, 30% concession → $0.70/token; 1,000 tokens → $700/mo.""" + price = TokenPrice(list_rate=1.00, concession_pct=0.30) + assert price.effective_rate() == pytest.approx(0.70) + totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False) + assert totals.token_cost_monthly == pytest.approx(700.0) + assert totals.token_cost_annual == pytest.approx(8_400.0) + + +def test_msrp_walk_is_carried_alongside() -> None: + """The deck-frame column: list beside effective, and the saving.""" + price = TokenPrice(list_rate=1.00, concession_pct=0.30) + totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False) + assert totals.list_cost_annual == pytest.approx(12_000.0) + assert totals.concession_saving_annual == pytest.approx(3_600.0) + assert totals.token_cost_annual <= totals.list_cost_annual + + +def test_contracted_rate_overrides_concession() -> None: + price = TokenPrice(list_rate=1.00, contracted_rate=0.55, concession_pct=0.30) + assert price.effective_rate() == pytest.approx(0.55) + + +def test_currency_carried_through() -> None: + price = TokenPrice(currency="JPY", list_rate=120.0) + totals = total_cost(1_000, 0, NAMED, price, apply_allowance=False) + assert totals.currency == "JPY" + assert totals.token_cost_monthly == pytest.approx(120_000.0) + + +def test_invalid_concession_rejected() -> None: + with pytest.raises(ValueError, match="concession_pct"): + TokenPrice(concession_pct=1.0) + + +# ── cost lines ─────────────────────────────────────────────────────────── + + +def test_cost_lines_price_each_meter_at_the_effective_rate() -> None: + price = TokenPrice(list_rate=1.00, concession_pct=0.25) + lines = cost_lines( + {"ai_scoring": 100.0, "virtual_agent": 7_500.0}, + {"ai_scoring": 2_000.0, "virtual_agent": 15_000.0}, + price, + ) + by_key = {line.key: line for line in lines} + assert by_key["ai_scoring"].cost_monthly == pytest.approx(75.0) + assert by_key["virtual_agent"].cost_monthly == pytest.approx(5_625.0) + assert by_key["virtual_agent"].cost_annual == pytest.approx( + 5_625.0 * MONTHS_PER_YEAR + ) + assert by_key["ai_scoring"].units_monthly == pytest.approx(2_000.0) + assert by_key["ai_scoring"].unit_label == "evaluations" + + +def test_cost_lines_are_ordered_by_feature_name() -> None: + lines = cost_lines( + {"virtual_agent": 1.0, "ai_scoring": 1.0}, {}, TokenPrice() + ) + assert [line.feature for line in lines] == ["AI Scoring", "Virtual Agent"] + + +def test_totals_expose_the_allowance_that_was_applied() -> None: + totals = total_cost(1_000, 20_000, NAMED, TokenPrice()) + assert totals.allowance_tokens_monthly == 250 + assert totals.gross_tokens_monthly == pytest.approx(21_000.0) + assert totals.billable_tokens_monthly == 20_750 diff --git a/calculators/Genesys_Token_Calculator/tests/test_catalogue.py b/calculators/Genesys_Token_Calculator/tests/test_catalogue.py new file mode 100644 index 0000000..42f19cc --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_catalogue.py @@ -0,0 +1,94 @@ +"""The feature catalogue — content pins, read from the notebook by tag. + +The catalogue is the master's authored content: it lives in the notebook's +``topic-bank`` cell, and these tests exec that cell rather than importing a +module. The published RATES are not here — they are the vendor's record, in +``genesyscalc/ratecard.py``, pinned by ``test_rate_card.py``. +""" + +from __future__ import annotations + +from typing import Any + +from genesyscalc.ratecard import METERS + +EXPECTED_KEYS = ( + "agent_copilot_named", + "speech_and_text_analytics_named", + "bots_voice", + "virtual_agent", + "agentic_virtual_agent", + "ai_summary_and_insights", + "ai_scoring", + "bots_digital", + "ai_translate", + "predictive_routing", + "whatsapp_messaging", + "genesys_cloud_copilot", + "predictive_engagement", +) + + +def test_catalogue_size(features: tuple[Any, ...]) -> None: + assert len(features) == 13 + + +def test_catalogue_keys_and_order(features: tuple[Any, ...]) -> None: + """Keys are stable identities — widgets, notes and the export key off them.""" + assert tuple(f.key for f in features) == EXPECTED_KEYS + + +def test_keys_are_unique(features: tuple[Any, ...]) -> None: + assert len({f.key for f in features}) == len(features) + + +def test_every_catalogue_key_is_a_published_meter(features: tuple[Any, ...]) -> None: + """The cross-layer tie: a rename here silently unprices a feature.""" + for f in features: + assert f.key in METERS, f"{f.key} is not a published Genesys meter" + + +def test_every_feature_carries_its_content(features: tuple[Any, ...]) -> None: + for f in features: + assert f.title.strip(), f"{f.key} has no title" + assert f.description.strip(), f"{f.key} has no description" + assert f.ask.strip(), f"{f.key} has no sizing question" + assert f.ask.strip().endswith(("?", ")")), ( + f"{f.key}: `ask` should read as a question to put to the client" + ) + + +def test_titles_are_distinct(features: tuple[Any, ...]) -> None: + """Titles become widget labels; duplicates collide in the sidebar.""" + titles = [f.title for f in features] + assert len(set(titles)) == len(titles) + + +def test_default_scenario_is_a_coherent_starting_point( + features: tuple[Any, ...], +) -> None: + """Headless nbconvert takes every widget at its seed, so the defaults + must form a scenario the gate can pass — and a plausible one to show.""" + default_on = {f.key for f in features if f.default_on} + assert len(default_on) == 8 + assert "agent_copilot_named" in default_on + assert "bots_voice" in default_on + # Copilot on by default means the summary line must be visible but free — + # that pairing is the published exclusion the calculator demonstrates. + assert "ai_summary_and_insights" in default_on + + +def test_catalogue_uses_a_schema_not_loose_dicts(features: tuple[Any, ...]) -> None: + """Content is structured, so the notebook and tests agree on its shape.""" + first = features[0] + assert type(first).__name__ == "Feature" + assert {"key", "title", "description", "ask", "default_on"} <= set( + type(first).__dataclass_fields__ + ) + + +def test_catalogue_cell_is_self_contained(catalogue_ns: dict[str, Any]) -> None: + """It execs with no setup cell — the conftest fixture proves it, but pin + the contract explicitly so a stray dependency is caught here.""" + assert "FEATURES" in catalogue_ns + assert "Feature" in catalogue_ns diff --git a/calculators/Genesys_Token_Calculator/tests/test_engagement.py b/calculators/Genesys_Token_Calculator/tests/test_engagement.py new file mode 100644 index 0000000..b65daa4 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_engagement.py @@ -0,0 +1,115 @@ +"""Engagement data — SHAPE pins only. + +The master ships placeholders; an engagement copy fills them in. So these +tests pin the *shape* of the cell and never its emptiness — asserting that +``client`` is blank would fail the moment the copy is used for real, which +is precisely backwards. +""" + +from __future__ import annotations + +from typing import Any + +import nbformat + +from tests.conftest import NOTEBOOK + +IDENTITY_KEYS = {"client", "prepared_date", "prepared_by"} + +VOLUME_KEYS = { + "voice_inbound_monthly", + "voice_outbound_monthly", + "digital_sessions_monthly", + "email_monthly", + "messaging_monthly", + "social_posts_monthly", + "social_responses_monthly", + "translations_monthly", + "evaluations_monthly", + "ai_actions_monthly", + "routed_interactions_monthly", +} + +COMMERCIAL_KEYS = { + "agent_users", + "licence_model", + "currency", + "contracted_rate_per_token", + "concession_pct", +} + +TTS_KEYS = {"tts_chars_per_call", "tts_voice_tier"} + + +def test_engagement_shape(engagement: dict[str, Any]) -> None: + assert set(engagement) == ( + IDENTITY_KEYS | VOLUME_KEYS | COMMERCIAL_KEYS | TTS_KEYS + ) + + +def test_identity_fields_are_strings(engagement: dict[str, Any]) -> None: + """Blank in the master, filled in the copy — either way, strings.""" + for key in IDENTITY_KEYS: + assert isinstance(engagement[key], str) + + +def test_volumes_are_non_negative_numbers(engagement: dict[str, Any]) -> None: + for key in VOLUME_KEYS: + value = engagement[key] + assert isinstance(value, (int, float)) and not isinstance(value, bool) + assert value >= 0, f"{key} must not be negative" + + +def test_commercial_terms_are_well_formed(engagement: dict[str, Any]) -> None: + assert isinstance(engagement["agent_users"], int) + assert engagement["agent_users"] >= 0 + assert engagement["licence_model"] in ("named", "concurrent") + assert isinstance(engagement["currency"], str) and engagement["currency"] + assert 0.0 <= float(engagement["concession_pct"]) < 1.0 + contracted = engagement["contracted_rate_per_token"] + assert contracted is None or float(contracted) >= 0 + + +def test_currency_is_one_the_rate_card_publishes(engagement: dict[str, Any]) -> None: + from genesyscalc.ratecard import LIST_PRICE_BY_CURRENCY + + assert engagement["currency"] in LIST_PRICE_BY_CURRENCY + + +def test_tts_settings(engagement: dict[str, Any]) -> None: + from genesyscalc.tts import TTS_PRICE_PER_MILLION_CHARS + + assert isinstance(engagement["tts_chars_per_call"], int) + assert engagement["tts_chars_per_call"] >= 0 + assert engagement["tts_voice_tier"] in TTS_PRICE_PER_MILLION_CHARS + + +def test_master_carries_no_client_identity(engagement: dict[str, Any]) -> None: + """The one emptiness check that IS correct: masters stay client-clean. + + This guards the master in THIS repo. An engagement copy lives outside + Palladium, so it never runs this suite — filling the cell there does not + break anything (CLAUDE.md § Confidentiality). + """ + assert engagement["client"] == "", ( + "a client name in the master means the copy-out step was skipped — " + "copy the directory out of Palladium before entering client data" + ) + + +def test_engagement_cell_sits_above_the_widget_cell() -> None: + """Client data must not re-run on a sidebar change (Mercury re-runs only + cells below a changed widget).""" + nb = nbformat.read(NOTEBOOK, as_version=4) + eng = [ + i + for i, c in enumerate(nb.cells) + if "engagement-data" in c.metadata.get("tags", []) + ] + widgets = [ + i + for i, c in enumerate(nb.cells) + if c.cell_type == "code" and "mr.Select(" in c.source + ] + assert len(eng) == 1 and widgets + assert eng[0] < min(widgets) diff --git a/calculators/Genesys_Token_Calculator/tests/test_model.py b/calculators/Genesys_Token_Calculator/tests/test_model.py new file mode 100644 index 0000000..03a8cc5 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_model.py @@ -0,0 +1,263 @@ +"""End-to-end pricing, hand-checked. + +The reference scenario below is the one the notebook ships as its default. +Every figure was computed by hand from the published rates before it was +pinned here. +""" + +from __future__ import annotations + +import json + +import pytest +from genesyscalc.appendix import result_json +from genesyscalc.meters import LicenceModel, TokenPrice +from genesyscalc.model import ( + CalculatorInputs, + calculate, + compare, + cost_per_interaction, + metered_units, +) +from genesyscalc.ratecard import RATE_CARD_SOURCE_DATE +from genesyscalc.tts import TtsUsage +from genesyscalc.usage import DeflectionMix, VoiceBotUsage, Volumes + +REFERENCE_ENABLED = frozenset( + { + "agent_copilot_named", + "speech_and_text_analytics_named", + "bots_voice", + "virtual_agent", + "agentic_virtual_agent", + "ai_summary_and_insights", + "ai_scoring", + "whatsapp_messaging", + "predictive_engagement", + } +) + + +@pytest.fixture +def volumes() -> Volumes: + return Volumes( + voice_inbound_monthly=100_000, + voice_outbound_monthly=20_000, + digital_sessions_monthly=8_000, + email_monthly=12_000, + messaging_monthly=6_000, + evaluations_monthly=2_000, + ) + + +@pytest.fixture +def reference(volumes: Volumes) -> CalculatorInputs: + return CalculatorInputs( + volumes=volumes, + users=500, + licence=LicenceModel.NAMED, + enabled=REFERENCE_ENABLED, + mix=DeflectionMix( + bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05 + ), + price=TokenPrice(), + voice_bot=VoiceBotUsage( + calls_per_month=30_000, avg_bot_seconds_per_call=40 + ), + tts=TtsUsage(calls_monthly=100_000, chars_per_call=2_000), + ) + + +# ── the hand-checked reference scenario ────────────────────────────────── + + +def test_subscription_lines(reference: CalculatorInputs) -> None: + """500 users: Copilot 500×40 = 20,000; STA 500×30 = 15,000 tokens/mo.""" + by_key = {line.key: line for line in calculate(reference).lines} + assert by_key["agent_copilot_named"].tokens_monthly == pytest.approx(20_000.0) + assert by_key["speech_and_text_analytics_named"].tokens_monthly == pytest.approx( + 15_000.0 + ) + assert by_key["agent_copilot_named"].cost_annual == pytest.approx(240_000.0) + + +def test_voice_bot_line(reference: CalculatorInputs) -> None: + """30,000 calls × ceil(40/15)×15s = 45s = 0.75 min → 22,500 min. + 22,500 / 17 = 1,323.53 tokens/mo → $15,882.35/yr at $1.00.""" + by_key = {line.key: line for line in calculate(reference).lines} + assert by_key["bots_voice"].units_monthly == pytest.approx(22_500.0) + assert by_key["bots_voice"].tokens_monthly == pytest.approx(1_323.529412, rel=1e-6) + assert by_key["bots_voice"].cost_annual == pytest.approx(15_882.35, abs=0.01) + + +def test_virtual_agent_lines_use_the_partition(reference: CalculatorInputs) -> None: + """120,000 voice: VA 15% = 18,000 × 0.5 = 9,000 tokens; + AVA 5% = 6,000 × 1.2 = 7,200 tokens.""" + by_key = {line.key: line for line in calculate(reference).lines} + assert by_key["virtual_agent"].units_monthly == pytest.approx(18_000.0) + assert by_key["virtual_agent"].tokens_monthly == pytest.approx(9_000.0) + assert by_key["agentic_virtual_agent"].units_monthly == pytest.approx(6_000.0) + assert by_key["agentic_virtual_agent"].tokens_monthly == pytest.approx(7_200.0) + + +def test_summary_is_zeroed_because_copilot_is_on(reference: CalculatorInputs) -> None: + result = calculate(reference) + by_key = {line.key: line for line in result.lines} + assert by_key["ai_summary_and_insights"].units_monthly == 0.0 + assert by_key["ai_summary_and_insights"].cost_annual == 0.0 + assert any("Agent Copilot is enabled" in w for w in result.warnings) + + +def test_zero_rated_line_is_shown_not_hidden(reference: CalculatorInputs) -> None: + """A published $0 is a finding — it renders as a line, at zero.""" + by_key = {line.key: line for line in calculate(reference).lines} + assert "predictive_engagement" in by_key + assert by_key["predictive_engagement"].units_monthly > 0 + assert by_key["predictive_engagement"].cost_annual == 0.0 + + +def test_reference_totals(reference: CalculatorInputs) -> None: + """consumption 17,638.53 → ceil 17,639; + 35,000 subscription = 52,639; + − 250 allowance = 52,389 billable → $52,389/mo → $628,668/yr. + TTS 200M chars, 20% VA-free → 160M → $3,200/mo → $38,400/yr. + Grand total $667,068/yr.""" + totals = calculate(reference).totals + assert totals.consumption_tokens_monthly == pytest.approx(17_638.529412, rel=1e-6) + assert totals.subscription_tokens_monthly == pytest.approx(35_000.0) + assert totals.gross_tokens_monthly == pytest.approx(52_639.0) + assert totals.allowance_tokens_monthly == 250 + assert totals.billable_tokens_monthly == 52_389 + assert totals.token_cost_annual == pytest.approx(628_668.0) + + +def test_reference_grand_total_splits_tokens_from_tts( + reference: CalculatorInputs, +) -> None: + result = calculate(reference) + assert result.tts_cost_annual == pytest.approx(38_400.0) + assert result.grand_total_annual == pytest.approx(667_068.0) + assert result.grand_total_annual == pytest.approx( + result.token_cost_annual + result.tts_cost_annual + ) + + +def test_cost_per_interaction(reference: CalculatorInputs) -> None: + """$667,068 / (146,000 × 12) = $0.3807 per interaction.""" + result = calculate(reference) + assert cost_per_interaction(result, reference.volumes) == pytest.approx( + 0.380747, rel=1e-5 + ) + + +def test_roundup_warning_is_reported(reference: CalculatorInputs) -> None: + assert any("15-second per-call round-up" in w for w in calculate(reference).warnings) + + +# ── structural ties that must hold at any setting ──────────────────────── + + +def test_lines_sum_to_the_gross_token_count(reference: CalculatorInputs) -> None: + result = calculate(reference) + assert sum(line.tokens_monthly for line in result.lines) == pytest.approx( + result.totals.consumption_tokens_monthly + + result.totals.subscription_tokens_monthly + ) + + +def test_effective_cost_never_exceeds_list(reference: CalculatorInputs) -> None: + discounted = reference.with_(price=TokenPrice(concession_pct=0.4)) + result = calculate(discounted) + assert result.totals.token_cost_annual <= result.totals.list_cost_annual + assert result.totals.concession_saving_annual >= 0 + + +def test_allowance_only_ever_reduces_the_bill(reference: CalculatorInputs) -> None: + with_allowance = calculate(reference).totals.billable_tokens_monthly + without = calculate( + reference.with_(apply_allowance=False) + ).totals.billable_tokens_monthly + assert with_allowance == without - 250 + + +def test_allowance_absorbing_a_month_is_reported() -> None: + quiet = CalculatorInputs( + volumes=Volumes(voice_inbound_monthly=100), + users=0, + enabled=frozenset({"ai_scoring"}), + ) + result = calculate(quiet) + assert result.totals.billable_tokens_monthly == 0 + assert any("free monthly allowance" in w for w in result.warnings) + + +def test_unknown_feature_key_rejected(volumes: Volumes) -> None: + with pytest.raises(ValueError, match="not published meters"): + CalculatorInputs(volumes=volumes, users=1, enabled=frozenset({"telepathy"})) + + +def test_metered_units_covers_every_enabled_key(reference: CalculatorInputs) -> None: + assert set(metered_units(reference)) == set(reference.enabled) + + +def test_empty_scenario_costs_nothing() -> None: + result = calculate(CalculatorInputs(volumes=Volumes(), users=0)) + assert result.grand_total_annual == 0.0 + assert result.lines == () + + +# ── licence model, scenarios ───────────────────────────────────────────── + + +def test_concurrent_licence_costs_more_per_user(reference: CalculatorInputs) -> None: + """Copilot 40→60 and STA 30→45, so 35,000 → 52,500 subscription tokens.""" + concurrent = reference.with_( + licence=LicenceModel.CONCURRENT, + enabled=frozenset( + {"agent_copilot_named", "speech_and_text_analytics_named"} + ), + ) + named = reference.with_( + enabled=frozenset({"agent_copilot_named", "speech_and_text_analytics_named"}) + ) + assert calculate(named).totals.subscription_tokens_monthly == pytest.approx(35_000.0) + assert calculate(concurrent).totals.subscription_tokens_monthly == pytest.approx( + 52_500.0 + ) + + +def test_compare_is_monotone_in_deflection(reference: CalculatorInputs) -> None: + """More virtual-agent deflection means more consumption tokens.""" + rows = compare( + { + "Conservative": reference.with_( + mix=DeflectionMix(bot_only_share=0.20, virtual_agent_share=0.05) + ), + "Base": reference, + "Aggressive": reference.with_( + mix=DeflectionMix( + bot_only_share=0.35, + virtual_agent_share=0.25, + agentic_va_share=0.10, + ) + ), + } + ) + assert [r["Scenario"] for r in rows] == ["Conservative", "Base", "Aggressive"] + totals = [r["Token cost / yr"] for r in rows] + assert totals[0] < totals[1] < totals[2] + + +# ── the export payload ─────────────────────────────────────────────────── + + +def test_result_json_round_trips_and_carries_provenance( + reference: CalculatorInputs, +) -> None: + payload = result_json(calculate(reference), reference, meta={"client": "Example"}) + assert json.loads(json.dumps(payload)) == payload + assert payload["rate_card"]["source_date"] == RATE_CARD_SOURCE_DATE + assert payload["rate_card"]["voice_bot_roundup_seconds"] == 15 + assert payload["tts_source"]["source_date"] == "2026-05-22" + assert payload["totals"]["grand_total_annual"] == pytest.approx(667_068.0) + assert payload["meta"]["client"] == "Example" + assert len(payload["lines"]) == len(REFERENCE_ENABLED) diff --git a/calculators/Genesys_Token_Calculator/tests/test_rate_card.py b/calculators/Genesys_Token_Calculator/tests/test_rate_card.py new file mode 100644 index 0000000..9c872de --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_rate_card.py @@ -0,0 +1,174 @@ +"""The vendor-change tripwire. + +Every published row is pinned by its exact feature wording AND its exact +rate string, so a transcription slip or a vendor republication breaks the +build rather than quietly moving a client-facing number. When Genesys +republishes, these pins are updated in the SAME commit as the anchor — see +the re-anchoring protocol in docs/Calculator_Pattern_V1-00.md. +""" + +from __future__ import annotations + +import pytest +from genesyscalc.meters import Confidence, LicenceModel, MeterBasis +from genesyscalc.ratecard import ( + ALLOWANCE_CARRIES_OVER, + COPILOT_COVERS_SUMMARY_RULE, + FREE_TOKENS_PER_MONTH, + HIGHEST_TIER_RULE, + LIST_PRICE_BY_CURRENCY, + METERS, + METERS_VERBATIM, + RATE_CARD_SOURCE, + RATE_CARD_SOURCE_DATE, + VOICE_BOT_ROUNDUP_SECONDS, + ZERO_RATED_VERBATIM, + meter, + rate_card_rows, +) + +# The published table, as it reads on the page. Transcribed 2026-07-12. +PUBLISHED = { + "Bots (Voice)": "17 minutes per token", + "Bots (Digital)": "51 sessions per token", + "Virtual Agent": "0.5 tokens per Virtual Agent interaction", + "Agentic Virtual Agent": "1.2 tokens per interaction", + "Agent Copilot [named]": "40 tokens per user", + "Agent Copilot [concurrent]": "60 tokens per user", + "AI Scoring": "20 evaluations per token", + "AI Translate": "2 translations per token", + "AI Summary and Insights": "50 summaries/insights per token", + "Apple Messages for Business": "400 inbound or outbound messages per token", + "Facebook Messenger": "400 messages per token", + "Instagram Direct Messaging": "400 messages per token", + "WhatsApp Messaging": "400 messages per token", + "X Direct Messaging": "400 messages per token", + "Genesys Cloud Social": "400 social post ingestions per channel per token", + "Social Post Responses": "400 outbound messages per channel per token", + "Predictive Routing": "17 routes per token", + "Speech and Text Analytics [named]": "30 tokens per user", + "Speech and Text Analytics [concurrent]": "45 tokens per user", + "Genesys Cloud Copilot": "20 AI actions per token", +} + + +def test_published_meter_table_verbatim() -> None: + assert {m.feature: m.published_rate for m in METERS_VERBATIM} == PUBLISHED + + +def test_meter_count() -> None: + assert len(METERS_VERBATIM) == 20 + assert len(ZERO_RATED_VERBATIM) == 2 + + +def test_source_pinned() -> None: + """Bumping the date without updating the pins is the failure mode.""" + assert RATE_CARD_SOURCE_DATE == "2026-07-12" + assert RATE_CARD_SOURCE == ( + "https://help.genesys.cloud/articles/genesys-cloud-tokens-model/" + ) + + +def test_every_confirmed_meter_carries_source_url_and_date() -> None: + for m in METERS.values(): + if m.confidence is Confidence.CONFIRMED: + assert m.source_url, f"{m.key} is 🟢 without a source URL" + assert m.source_date, f"{m.key} is 🟢 without a source date" + + +def test_unsourced_confirmed_meter_cannot_be_constructed() -> None: + from genesyscalc.meters import Meter + + with pytest.raises(ValueError, match="source URL and date"): + Meter( + key="made_up", + feature="Made Up", + published_rate="1 per token", + basis=MeterBasis.UNITS_PER_TOKEN, + rate=1.0, + ) + + +@pytest.mark.parametrize( + ("key", "expected"), + [ + ("bots_voice", 1 / 17), + ("bots_digital", 1 / 51), + ("virtual_agent", 0.5), + ("agentic_virtual_agent", 1.2), + ("ai_scoring", 1 / 20), + ("ai_translate", 1 / 2), + ("ai_summary_and_insights", 1 / 50), + ("whatsapp_messaging", 1 / 400), + ("genesys_cloud_social", 1 / 400), + ("predictive_routing", 1 / 17), + ("genesys_cloud_copilot", 1 / 20), + ], +) +def test_numeric_rates_match_their_published_strings(key: str, expected: float) -> None: + """Catches a float drifting away from the wording beside it.""" + assert meter(key).rate == pytest.approx(expected) + + +def test_per_user_rates() -> None: + copilot = meter("agent_copilot_named") + assert copilot.tokens_per_user_month(LicenceModel.NAMED) == 40.0 + assert copilot.tokens_per_user_month(LicenceModel.CONCURRENT) == 60.0 + + sta = meter("speech_and_text_analytics_named") + assert sta.tokens_per_user_month(LicenceModel.NAMED) == 30.0 + assert sta.tokens_per_user_month(LicenceModel.CONCURRENT) == 45.0 + + +def test_free_allowance() -> None: + assert FREE_TOKENS_PER_MONTH[LicenceModel.NAMED] == 250 + assert FREE_TOKENS_PER_MONTH[LicenceModel.CONCURRENT] == 350 + assert ALLOWANCE_CARRIES_OVER is False + + +def test_voice_roundup_granularity() -> None: + assert VOICE_BOT_ROUNDUP_SECONDS == 15 + + +def test_zero_rated_features() -> None: + assert {m.key for m in ZERO_RATED_VERBATIM} == { + "predictive_engagement", + "knowledge_queries", + } + assert all(m.rate == 0.0 for m in ZERO_RATED_VERBATIM) + assert all(m.tokens_for(1_000_000) == 0.0 for m in ZERO_RATED_VERBATIM) + + +def test_highest_tier_rule_text_verbatim() -> None: + """It renders on stage, so the wording is part of the deliverable.""" + assert HIGHEST_TIER_RULE == ( + "In cases where an interaction uses multiple AI resources, such as bot " + "flows, virtual agents, and agentic virtual agents, Genesys bases " + "charges on the highest tier (price) resource that Genesys Cloud uses " + "during the interaction." + ) + + +def test_copilot_covers_summary_rule_text_verbatim() -> None: + assert COPILOT_COVERS_SUMMARY_RULE == ( + "if you enable Agent Copilot simultaneously, then Supervisor Copilot " + "summaries and insights do not consume tokens" + ) + + +def test_list_price() -> None: + assert LIST_PRICE_BY_CURRENCY["USD"] == 1.00 + assert LIST_PRICE_BY_CURRENCY["JPY"] == 120.0 + + +def test_meter_lookup_error_is_useful() -> None: + with pytest.raises(KeyError, match="not a published Genesys meter"): + meter("nope") + + +def test_rate_card_rows_cover_every_published_line() -> None: + rows = rate_card_rows() + assert len(rows) == len(METERS_VERBATIM) + len(ZERO_RATED_VERBATIM) + assert rows[0]["Feature"] == "Bots (Voice)" + assert all(r["Confidence"] == "🟢" for r in rows) + assert all(r["Source"] == RATE_CARD_SOURCE_DATE for r in rows) diff --git a/calculators/Genesys_Token_Calculator/tests/test_sensitivity.py b/calculators/Genesys_Token_Calculator/tests/test_sensitivity.py new file mode 100644 index 0000000..de4dae5 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_sensitivity.py @@ -0,0 +1,110 @@ +"""Sweeps and tornado data.""" + +from __future__ import annotations + +import pytest +from genesyscalc.meters import TokenPrice +from genesyscalc.model import CalculatorInputs, calculate +from genesyscalc.sensitivity import ( + DRIVERS, + driver_value, + set_driver, + sweep, + tornado, +) +from genesyscalc.tts import TtsUsage +from genesyscalc.usage import DeflectionMix, VoiceBotUsage, Volumes + + +@pytest.fixture +def base() -> CalculatorInputs: + return CalculatorInputs( + volumes=Volumes(voice_inbound_monthly=100_000, voice_outbound_monthly=20_000), + users=500, + enabled=frozenset( + {"agent_copilot_named", "bots_voice", "virtual_agent"} + ), + mix=DeflectionMix(bot_only_share=0.30, virtual_agent_share=0.15), + price=TokenPrice(), + voice_bot=VoiceBotUsage(calls_per_month=30_000, avg_bot_seconds_per_call=40), + tts=TtsUsage(calls_monthly=100_000), + ) + + +def test_driver_round_trip(base: CalculatorInputs) -> None: + assert driver_value(base, "users") == 500 + assert driver_value(set_driver(base, "users", 750), "users") == 750 + + +def test_nested_driver_round_trip(base: CalculatorInputs) -> None: + assert driver_value(base, "concession_pct") == 0.0 + moved = set_driver(base, "concession_pct", 0.25) + assert moved.price.concession_pct == pytest.approx(0.25) + assert base.price.concession_pct == 0.0 # inputs are frozen; no mutation + + +def test_integer_drivers_stay_integers(base: CalculatorInputs) -> None: + assert isinstance(set_driver(base, "users", 512.6).users, int) + assert set_driver(base, "users", 512.6).users == 513 + + +def test_unknown_driver_is_named(base: CalculatorInputs) -> None: + with pytest.raises(KeyError, match="not a known driver"): + driver_value(base, "vibes") + + +def test_sweep_is_monotone_in_token_price(base: CalculatorInputs) -> None: + rows = sweep(base, "token_price", [0.5, 1.0, 1.5, 2.0]) + costs = [r["token_cost_annual"] for r in rows] + assert costs == sorted(costs) + assert len(rows) == 4 + + +def test_sweep_carries_the_driver_value(base: CalculatorInputs) -> None: + rows = sweep(base, "concession_pct", [0.0, 0.25]) + assert rows[0]["concession_pct"] == 0.0 + assert rows[1]["grand_total_annual"] < rows[0]["grand_total_annual"] + + +def test_tornado_is_sorted_by_swing(base: CalculatorInputs) -> None: + rows = tornado(base, ["token_price", "users", "bot_seconds", "concession_pct"]) + swings = [r["swing"] for r in rows] + assert swings == sorted(swings, reverse=True) + assert all(r["swing"] >= 0 for r in rows) + + +def test_tornado_brackets_the_baseline(base: CalculatorInputs) -> None: + baseline = calculate(base).grand_total_annual + for row in tornado(base, ["token_price", "users"]): + assert row["low"] <= baseline <= row["high"] + assert row["baseline"] == pytest.approx(baseline) + + +def test_tornado_skips_inactive_levers() -> None: + """No TTS and no voice bot in the scenario — those levers are noise.""" + plain = CalculatorInputs( + volumes=Volumes(voice_inbound_monthly=1_000), + users=10, + enabled=frozenset({"agent_copilot_named"}), + ) + drivers = [r["driver"] for r in tornado(plain, list(DRIVERS))] + assert "bot_seconds" not in drivers + assert "tts_chars_per_call" not in drivers + assert "users" in drivers + + +def test_tornado_skips_a_lever_that_would_break_the_partition() -> None: + """Raising a share past the partition is illegal, not a data point.""" + saturated = CalculatorInputs( + volumes=Volumes(voice_inbound_monthly=1_000), + users=10, + enabled=frozenset({"virtual_agent"}), + mix=DeflectionMix(bot_only_share=0.5, virtual_agent_share=0.5), + ) + drivers = [r["driver"] for r in tornado(saturated, ["virtual_agent_share"])] + assert drivers == [] + + +def test_invalid_delta_rejected(base: CalculatorInputs) -> None: + with pytest.raises(ValueError, match="delta"): + tornado(base, ["users"], delta=1.5) diff --git a/calculators/Genesys_Token_Calculator/tests/test_staging.py b/calculators/Genesys_Token_Calculator/tests/test_staging.py new file mode 100644 index 0000000..33158c6 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_staging.py @@ -0,0 +1,61 @@ +"""Stage/backstage detection. + +Two signals mark the Mercury app view (see staging.py): the app's shadow +session name (``__mercury__`` in ``JPY_SESSION_NAME`` — per-view, primary) +and the ``MERCURY_CONFIG_DIR`` env var (server-level fallback, only set by +``mercury --working-dir``). Either one means "a client may be looking". +""" + +import pytest +from genesyscalc import staging + + +@pytest.fixture(autouse=True) +def clean_stage_env(monkeypatch): + monkeypatch.delenv("MERCURY_CONFIG_DIR", raising=False) + monkeypatch.delenv("JPY_SESSION_NAME", raising=False) + + +def test_backstage_by_default(capsys): + assert not staging.on_stage() + staging.backstage("visible") + assert capsys.readouterr().out == "visible\n" + + +def test_plain_session_name_is_backstage(monkeypatch, capsys): + # A JupyterLab or nbconvert session: the real notebook path, no marker. + monkeypatch.setenv("JPY_SESSION_NAME", "notebooks/genesys_token_calculator.ipynb") + assert not staging.on_stage() + staging.backstage("visible") + assert capsys.readouterr().out == "visible\n" + + +def test_mercury_shadow_session_is_stage(monkeypatch, capsys): + # The Mercury app runs against a shadow copy: __mercury__.ipynb + monkeypatch.setenv( + "JPY_SESSION_NAME", "notebooks/genesys_token_calculator__mercury__545e5520.ipynb" + ) + assert staging.on_stage() + staging.backstage("hidden") + assert capsys.readouterr().out == "" + + +def test_config_dir_fallback_is_stage(monkeypatch, capsys): + monkeypatch.setenv("MERCURY_CONFIG_DIR", "/tmp/app") + assert staging.on_stage() + staging.backstage("hidden") + assert capsys.readouterr().out == "" + + +def test_backstage_md_renders_only_off_stage(monkeypatch, capsys): + # Off stage it must emit SOMETHING (rich markdown under a kernel; + # IPython's display degrades to print under plain pytest) … + staging.backstage_md("**visible**") + assert capsys.readouterr().out != "" + + # … and on stage — either signal — nothing at all. + monkeypatch.setenv( + "JPY_SESSION_NAME", "notebooks/genesys_token_calculator__mercury__ab12cd34.ipynb" + ) + staging.backstage_md("**hidden**") + assert capsys.readouterr().out == "" diff --git a/calculators/Genesys_Token_Calculator/tests/test_tts.py b/calculators/Genesys_Token_Calculator/tests/test_tts.py new file mode 100644 index 0000000..a8e6920 --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_tts.py @@ -0,0 +1,114 @@ +"""Enhanced TTS — the non-token line, its round-up, and its free share.""" + +from __future__ import annotations + +import pytest +from genesyscalc.meters import Confidence +from genesyscalc.tts import ( + TTS_PRICE_PER_MILLION_CHARS, + TTS_SOURCE, + TTS_SOURCE_DATE, + TTS_STANDARD_END_OF_SALE, + TTS_STANDARD_END_OF_SUPPORT, + TtsUsage, + tts_cost, + tts_free_share, +) +from genesyscalc.usage import DeflectionMix + + +def test_published_rates() -> None: + assert TTS_PRICE_PER_MILLION_CHARS == {"advanced": 20.0, "standard": 5.0} + + +def test_source_pinned_and_distinct_from_the_tokens_article() -> None: + """TTS has its own page and its own date — it is not a token meter.""" + from genesyscalc.ratecard import RATE_CARD_SOURCE, RATE_CARD_SOURCE_DATE + + assert TTS_SOURCE_DATE == "2026-05-22" + assert TTS_SOURCE.endswith("genesys-enhanced-tts-pricing/") + assert TTS_SOURCE != RATE_CARD_SOURCE + assert TTS_SOURCE_DATE != RATE_CARD_SOURCE_DATE + + +def test_tts_is_published_so_confirmed() -> None: + line = tts_cost(TtsUsage(calls_monthly=100_000)) + assert line.confidence is Confidence.CONFIRMED + + +def test_hand_checked_advanced_cost() -> None: + """100,000 calls × 2,000 chars = 200M chars → 200 units × $20 = $4,000/mo.""" + line = tts_cost(TtsUsage(calls_monthly=100_000, chars_per_call=2_000)) + assert line.total_chars_monthly == pytest.approx(200_000_000.0) + assert line.billed_millions_monthly == 200 + assert line.cost_monthly == pytest.approx(4_000.0) + assert line.cost_annual == pytest.approx(48_000.0) + + +def test_hand_checked_standard_cost() -> None: + """Same volume on standard voices: 200 × $5 = $1,000/mo.""" + line = tts_cost( + TtsUsage(calls_monthly=100_000, chars_per_call=2_000, tier="standard") + ) + assert line.cost_monthly == pytest.approx(1_000.0) + + +def test_per_million_roundup_published_examples() -> None: + """The article's own examples: 200,000 chars = $5; 1,000,005 chars = $10. + + Both on standard voices, where the unit rate is $5. + """ + small = tts_cost(TtsUsage(calls_monthly=1, chars_per_call=200_000, tier="standard")) + assert small.billed_millions_monthly == 1 + assert small.cost_monthly == pytest.approx(5.0) + + just_over = tts_cost( + TtsUsage(calls_monthly=1, chars_per_call=1_000_005, tier="standard") + ) + assert just_over.billed_millions_monthly == 2 + assert just_over.cost_monthly == pytest.approx(10.0) + + +def test_free_share_tracks_the_virtual_agent_tiers() -> None: + """Free during VA and agentic VA — bot-only deflection does not qualify.""" + mix = DeflectionMix( + bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05 + ) + assert tts_free_share(mix) == pytest.approx(0.20) + + +def test_free_share_reduces_the_bill() -> None: + """200M chars, 20% free → 160M billable → 160 × $20 = $3,200/mo.""" + mix = DeflectionMix(virtual_agent_share=0.15, agentic_va_share=0.05) + line = tts_cost(TtsUsage(calls_monthly=100_000, chars_per_call=2_000), mix) + assert line.free_share == pytest.approx(0.20) + assert line.free_chars_monthly == pytest.approx(40_000_000.0) + assert line.billable_chars_monthly == pytest.approx(160_000_000.0) + assert line.cost_monthly == pytest.approx(3_200.0) + + +def test_standard_tier_warns_about_end_of_life() -> None: + line = tts_cost(TtsUsage(calls_monthly=1_000, tier="standard")) + assert len(line.warnings) == 1 + assert TTS_STANDARD_END_OF_SALE in line.warnings[0] + assert TTS_STANDARD_END_OF_SUPPORT in line.warnings[0] + + +def test_advanced_tier_does_not_warn() -> None: + assert tts_cost(TtsUsage(calls_monthly=1_000)).warnings == () + + +def test_concession_applies_to_tts_too() -> None: + line = tts_cost(TtsUsage(calls_monthly=100_000), concession_pct=0.25) + assert line.cost_monthly == pytest.approx(3_000.0) + + +def test_zero_volume_costs_nothing() -> None: + line = tts_cost(TtsUsage(calls_monthly=0)) + assert line.billed_millions_monthly == 0 + assert line.cost_monthly == 0.0 + + +def test_unknown_tier_rejected() -> None: + with pytest.raises(ValueError, match="unknown TTS tier"): + TtsUsage(calls_monthly=1, tier="neural-ultra") diff --git a/calculators/Genesys_Token_Calculator/tests/test_usage.py b/calculators/Genesys_Token_Calculator/tests/test_usage.py new file mode 100644 index 0000000..b65b71e --- /dev/null +++ b/calculators/Genesys_Token_Calculator/tests/test_usage.py @@ -0,0 +1,206 @@ +"""Volumes → tokens, with the three published rules that trip the naive math. + +Every number below was computed by hand first, then pinned. +""" + +from __future__ import annotations + +import pytest +from genesyscalc.meters import LicenceModel, Tier +from genesyscalc.usage import ( + DeflectionMix, + VoiceBotUsage, + Volumes, + apply_copilot_covers_summary, + subscription_tokens, + tier_interactions, + virtual_agent_tokens, + voice_bot_billable_minutes, + voice_bot_roundup_uplift, + voice_bot_tokens, +) + +# ── 1 · the 15-second per-call round-up ────────────────────────────────── + + +def test_voice_bot_15_second_roundup() -> None: + """1,000 calls at a 40s average. + + ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month. + The naive 40/60 × 1,000 = 666.67 min understates by 12.5%. + """ + usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40) + assert voice_bot_billable_minutes(usage) == pytest.approx(750.0) + assert voice_bot_tokens(usage) == pytest.approx(750.0 / 17.0) + assert voice_bot_tokens(usage) == pytest.approx(44.117647, rel=1e-6) + + +def test_roundup_uplift_is_reportable() -> None: + """The stage-facing number: 12.5% more than the raw duration implies.""" + usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40) + assert voice_bot_roundup_uplift(usage) == pytest.approx(0.125) + + +def test_roundup_is_exact_on_increment_boundaries() -> None: + """45s is exactly three increments — no inflation, uplift 0.""" + usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=45) + assert voice_bot_billable_minutes(usage) == pytest.approx(750.0) + assert voice_bot_roundup_uplift(usage) == pytest.approx(0.0) + + +def test_roundup_penalises_short_calls_hardest() -> None: + """A 5-second bot greeting bills as 15 seconds — 3× its duration. + + 1,000 calls × 5s = 83.33 raw minutes, billed as 250.0. + """ + usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=5) + assert voice_bot_billable_minutes(usage) == pytest.approx(250.0) + assert voice_bot_roundup_uplift(usage) == pytest.approx(2.0) + + +def test_zero_bot_volume_is_free_and_does_not_divide_by_zero() -> None: + usage = VoiceBotUsage(calls_per_month=0, avg_bot_seconds_per_call=0) + assert voice_bot_billable_minutes(usage) == 0.0 + assert voice_bot_roundup_uplift(usage) == 0.0 + assert voice_bot_tokens(usage) == 0.0 + + +def test_negative_bot_usage_rejected() -> None: + with pytest.raises(ValueError, match="calls_per_month"): + VoiceBotUsage(calls_per_month=-1, avg_bot_seconds_per_call=30) + + +# ── 2 · the highest-tier rule as a partition ───────────────────────────── + + +def test_tier_shares_must_partition() -> None: + """Shares summing past 1.0 would bill some interaction twice.""" + with pytest.raises(ValueError, match="sum to at most 1.0"): + DeflectionMix( + bot_only_share=0.5, virtual_agent_share=0.4, agentic_va_share=0.3 + ) + + +def test_tier_share_bounds() -> None: + with pytest.raises(ValueError, match="must be in"): + DeflectionMix(virtual_agent_share=1.4) + + +def test_agent_handled_share_is_the_remainder() -> None: + mix = DeflectionMix( + bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05 + ) + assert mix.total_deflected_share == pytest.approx(0.50) + assert mix.agent_handled_share == pytest.approx(0.50) + + +def test_highest_tier_charges_each_interaction_once() -> None: + """100,000 interactions: 30% bot, 15% VA, 5% agentic VA. + + VA = 100,000 × 0.15 × 0.5 tokens = 7,500 + AVA = 100,000 × 0.05 × 1.2 tokens = 6,000 + ------ + 13,500 + + Applying every tier to the whole volume — the additive reading — would + give 50,000 + 120,000 = far more, billing the same interaction at + several tiers at once. + """ + mix = DeflectionMix( + bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05 + ) + tokens = virtual_agent_tokens(100_000, mix) + assert tokens["virtual_agent"] == pytest.approx(7_500.0) + assert tokens["agentic_virtual_agent"] == pytest.approx(6_000.0) + assert sum(tokens.values()) == pytest.approx(13_500.0) + + +def test_tier_interactions_never_exceed_the_volume() -> None: + mix = DeflectionMix( + bot_only_share=0.40, virtual_agent_share=0.35, agentic_va_share=0.25 + ) + split = tier_interactions(100_000, mix) + assert sum(split.values()) == pytest.approx(100_000.0) + assert split[Tier.BOT] == pytest.approx(40_000.0) + + +def test_bot_tier_is_not_priced_per_interaction() -> None: + """Voice bots bill in minutes with a round-up, so they are absent here.""" + mix = DeflectionMix(bot_only_share=1.0) + assert virtual_agent_tokens(100_000, mix) == { + "virtual_agent": 0.0, + "agentic_virtual_agent": 0.0, + } + + +# ── 3 · Copilot covers Supervisor summaries ────────────────────────────── + + +def test_summary_billed_when_copilot_off() -> None: + usage = apply_copilot_covers_summary( + {"ai_summary_and_insights": 120_000.0}, copilot_enabled=False + ) + assert usage.get("ai_summary_and_insights") == pytest.approx(120_000.0) + assert usage.notes == () + + +def test_copilot_covers_summary_zeroes_the_line() -> None: + """The published exclusion, enforced — and reported, not silent.""" + usage = apply_copilot_covers_summary( + {"ai_summary_and_insights": 120_000.0}, copilot_enabled=True + ) + assert usage.get("ai_summary_and_insights") == 0.0 + assert len(usage.notes) == 1 + assert "Agent Copilot is enabled" in usage.notes[0] + + +def test_copilot_exclusion_leaves_other_meters_alone() -> None: + usage = apply_copilot_covers_summary( + {"ai_summary_and_insights": 100.0, "ai_scoring": 500.0}, copilot_enabled=True + ) + assert usage.get("ai_scoring") == pytest.approx(500.0) + + +def test_copilot_exclusion_is_quiet_when_there_is_nothing_to_zero() -> None: + usage = apply_copilot_covers_summary( + {"ai_summary_and_insights": 0.0}, copilot_enabled=True + ) + assert usage.notes == () + + +# ── subscription tokens ────────────────────────────────────────────────── + + +def test_per_user_tokens_are_exact_and_licence_sensitive() -> None: + """500 users × 40 = 20,000 named; × 60 = 30,000 concurrent.""" + keys = frozenset({"agent_copilot_named"}) + assert subscription_tokens(500, keys, LicenceModel.NAMED) == { + "agent_copilot_named": 20_000.0 + } + assert subscription_tokens(500, keys, LicenceModel.CONCURRENT) == { + "agent_copilot_named": 30_000.0 + } + + +def test_subscription_rejects_a_consumption_meter() -> None: + with pytest.raises(ValueError, match="not a per-user meter"): + subscription_tokens(10, frozenset({"ai_scoring"}), LicenceModel.NAMED) + + +# ── volumes ────────────────────────────────────────────────────────────── + + +def test_volume_totals() -> None: + v = Volumes( + voice_inbound_monthly=100_000, + voice_outbound_monthly=20_000, + digital_sessions_monthly=8_000, + email_monthly=12_000, + ) + assert v.voice_total_monthly == pytest.approx(120_000.0) + assert v.all_interactions_monthly == pytest.approx(140_000.0) + + +def test_negative_volume_rejected() -> None: + with pytest.raises(ValueError, match="voice_inbound_monthly"): + Volumes(voice_inbound_monthly=-1) diff --git a/docs/Calculator_Pattern_V1-00.md b/docs/Calculator_Pattern_V1-00.md new file mode 100644 index 0000000..dc941aa --- /dev/null +++ b/docs/Calculator_Pattern_V1-00.md @@ -0,0 +1,264 @@ +# Calculator Pattern v1.0.0 + +Standardizes **Calculators** — Palladium masters that price a vendor's product +from that vendor's own **published rate card**: a verbatim rate-card anchor, +a client overlay in tagged cells, and a re-anchoring protocol for when the +vendor republishes. + +## 🐾 Red Panda Approval™ + +This pattern follows Red Panda Approval standards (see `CLAUDE.md` for the +rubric). + +**Audience note:** written to be loaded whole as context by an LLM agent +building or modifying a Calculator. Rules are imperative (MUST/SHOULD/NEVER), +each with a one-line *why*. It **specializes** +[`Mercury_Notebook_Pattern_V1-00.md`](Mercury_Notebook_Pattern_V1-00.md) — +read that first; the shared mechanics (reactivity, widget-pairs, gate, +staging, appendix, packaging) are **not** restated here. `CLAUDE.md` is the +always-on contract and takes precedence where documents disagree. + +Reference implementation: +[`calculators/Genesys_Token_Calculator/`](../calculators/Genesys_Token_Calculator/). + +--- + +## What a Calculator is — and why it's a third kind + +| | Study | Assessment | **Calculator** | +|---|---|---|---| +| Anchored to | a **dated publication** | — (it *is* the instrument) | a **living rate card** | +| Naming | `YYYYMM_…` dated | `Instrument_Name` undated | `Vendor_Subject_Calculator` undated | +| Anchor lifetime | immutable **forever** | n/a | immutable **until the vendor republishes** | +| Its "numbers" | NPV / ROI / payback | status & progress | **run-rate, $/unit, per-feature cost** | +| Lives in | `studies/` | `assessments/` | `calculators/` | + +The distinguishing property is the **anchor's lifetime**. A Study's anchor is +frozen forever — the December 2025 Forrester PDF will never change, so a dated +directory name is honest. A Calculator's anchor is frozen *until the vendor +republishes*, at which point updating it is a deliberate, test-breaking, +dated act. A `YYYYMM_` prefix would start lying the moment the vendor updated +their page, so **Calculators are undated**; the publication date lives inside +the anchor (`RATE_CARD_SOURCE_DATE`), pinned by a test and shown on stage. + +> **Note.** Mercury Pattern *Variant 3 — Exploratory calculator* describes the +> pre-master form of this: a what-if surface inside a study. This pattern +> promotes it to a first-class master type. A Variant-3 notebook that outlives +> its study should graduate into `calculators/`. + +--- + +## Required Structure & Contracts + +Everything the Mercury pattern requires, **plus** the four below. + +### 1 · The rate card is a verbatim anchor, and it lives in `.py` + +`CLAUDE.md` says content never lives in `.py`. A published vendor rate card is +**not content**, and the boundary is who authors and edits the thing: + +> **In `.py` (the engine):** the vendor's published rate card — verbatim, with +> a source URL and a source date. A third-party record nobody in this repo +> authors. **In tagged notebook cells:** everything the consultant authors or +> the client supplies — volumes, headcount, licence model, the **negotiated** +> rate, feature enablement, adoption assumptions, scenario definitions, and +> the narrative. + +*Why:* the content rule exists because *"the notebook is the document the +consultant edits"*. Nobody edits the vendor's rate card — editing it is +**forbidden**, and changing it requires a republication plus the re-anchoring +protocol below. Putting an immutable anchor on the surface designated as +editable invites exactly the casual edit the anchor rule prevents, and makes +the most-likely-wrong numbers checkable only via a notebook parse. Precedent: +`studies/202602_TEI_Amazon_Connect/teicalc/anchor.py` holds Forrester's +published tables the same way. + +**Corollary that has teeth:** the *contracted overlay* is **client data** and +belongs in the `engagement-data` cell, never in `.py`. A Calculator's engine +MUST ship **no client volumes at all** — no `defaults.py` full of real sites. +(`studies/202607_CTM_GenesysCX/tokencalc/defaults.py` does exactly that; it is +the anti-precedent, not the model.) + +The anchor module MUST carry: + +```python +RATE_CARD_SOURCE: str # the URL +RATE_CARD_SOURCE_DATE: str # the publication's own "last updated" date +METERS_VERBATIM: tuple[Meter, ...] # feature wording AND rate string, exact +``` + +Each meter keeps the vendor's **exact wording** (`feature`) and **exact rate +string** (`published_rate`) beside the parsed number. *Why:* the string is +what makes a transcription slip catchable — a test pins the strings, and a +second test proves the float still agrees with the string beside it. + +### 2 · The `topic-bank` cell holds the catalogue, not the rates + +Since the rates are in the engine, the required `topic-bank` cell holds what a +consultant genuinely authors: the **feature catalogue** — per capability, a +client-facing title, a one-line description, and the **sizing question to +ask**. Each `key` MUST match a published meter key, pinned by a test. *Why:* +this keeps the tag taxonomy honest rather than tag-stuffed, and the cross-layer +pin catches a rename that would otherwise silently unprice a feature. + +### 3 · Costs the vendor bills outside the primary meter are a separate line + +A vendor usually bills something adjacent on a different basis (here: Enhanced +TTS, per character rather than per token). Such a line MUST have its **own +source URL and date**, its own confidence flag, and its own subtotal — and the +grand total MUST show the components labelled, never one blended figure. +*Why:* the headline is understated without it and wrong with it silently +folded in; a reader must be able to see which meter produced which dollar. + +### 4 · Every published rule that changes the arithmetic is modelled and reported + +Vendors bury billing rules in prose. Each one that moves the number MUST be +enforced in the engine, pinned with hand-checked arithmetic, and **surfaced in +the result** (a `warnings` tuple), not applied silently. *Why:* a total that +shrank for a published reason nobody can see is indistinguishable from a bug. +In the reference implementation these are the per-call round-up, the free +monthly allowance, the highest-tier rule, and the Copilot exclusion. + +--- + +## The Re-anchoring Protocol + +The section unique to this kind. A Calculator tracks a *living* rate card, so +updating the anchor is expected — but never casual. Re-anchoring is +**Show-first** (`CLAUDE.md` § Risk tier), unlike editing a Study anchor, which +is forbidden outright. + +When the vendor republishes: + +1. **Diff** the published table against the anchor module. +2. **Update the meters AND bump `RATE_CARD_SOURCE_DATE` in the same edit.** +3. **Update the test pins in the same commit.** +4. **Re-execute** the notebook; re-pin the gate's live-state numbers if the + defaults moved. +5. **Report the cost moves honestly** — say what got more expensive. +6. **Show the user the rate diff** and the KPI move before it lands. + +> Never partially re-anchor. Never bump the date without the pins. A date that +> claims a rate card the numbers don't match is worse than a stale one, because +> it is stale *and* it lies about it. + +--- + +## Standard Choice Values + +Beyond the Mercury pattern's confidence legend (🟢 confirmed / 🟡 estimated / +🔴 unknown), Calculators standardize: + +```python +class Confidence(Enum): # 🟢 requires source_url AND source_date — enforced + CONFIRMED = "confirmed" + ESTIMATED = "estimated" + UNKNOWN = "unknown" + +class LicenceModel(Enum): # vendors price per-seat features by licence model + NAMED = "named" + CONCURRENT = "concurrent" +``` + +A 🟢 meter that carries no source URL and date MUST fail to construct. *Why:* +it is the one invariant that stops an unsourced number wearing a green tick on +a client-facing stage. + +--- + +## Recommended Practices + +- **The list → negotiated walk.** Carry `list_cost` beside the effective cost + and show both. The client sees MSRP → their deal explicitly (the Mercury + pattern's deck-frame column, applied to pricing). +- **Published zeroes are findings.** A feature the vendor publishes as free + renders as an explicit `$0` line with the quoted wording — not an omission. +- **Adoption as a range.** Adoption is the largest uncertainty in any + consumption estimate; ship conservative / base / aggressive rather than a + single point. +- **A `$/unit` figure.** Cost per interaction is the number a client remembers + and the one that survives a change of volume assumptions. + +--- + +## Anti-Patterns + +- ❌ **Don't build a business case in a Calculator.** NPV, payback, ROI, a + benefit model, a P&L — any of these means the work has become a **Study**. + Move it, don't grow it here. +- ❌ **Don't edit `RATE_CARD_VERBATIM` to reflect a negotiated rate.** That is + the overlay, and it belongs in `engagement-data`. The anchor is what the + vendor published, not what the client pays. +- ❌ **Don't bump the source date without updating the pins** (see the + protocol). +- ❌ **Don't ship a rate without a source URL and a confidence flag** — the + schema should make this impossible. +- ❌ **Don't put engagement volumes in the engine.** Masters stay client-clean; + an engine with real client sites in it cannot be shared. +- ❌ **Don't fold a differently-metered cost into the headline meter's + subtotal** — separate line, separate source, labelled components. +- ❌ **Don't model a per-unit round-up by averaging first.** Round per billing + event, then sum; averaging first understates short events, sometimes by + multiples. + +--- + +## Testing + +The Mercury pattern's layers 0–4, plus **layer 1a — the rate-card tripwire**, +which is the highest-value test in a Calculator: + +```python +def test_published_meter_table_verbatim(): + """Exact feature wording AND exact rate string, every row. + + A transcription slip or an un-noticed republication breaks the build + rather than quietly moving a client-facing number. + """ + assert {m.feature: m.published_rate for m in METERS_VERBATIM} == PUBLISHED + +def test_source_pinned(): + assert RATE_CARD_SOURCE_DATE == "2026-07-12" + +def test_numeric_rates_match_their_published_strings(): + """Catches a float drifting away from the wording beside it.""" + +def test_every_confirmed_meter_carries_source_url_and_date(): ... +``` + +Then **hand-check the arithmetic before pinning it** — compute each published +rule's effect by hand, write the working into the test's docstring, and pin +the result: + +```python +def test_voice_bot_15_second_roundup(): + """1,000 calls at a 40s average. + + ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month. + The naive 40/60 × 1,000 = 666.67 understates by 12.5%. + """ + assert voice_bot_billable_minutes(VoiceBotUsage(1_000, 40)) == pytest.approx(750.0) +``` + +The in-notebook **gate** additionally asserts *structural ties that hold at any +widget setting* — the allowance identity, effective ≤ list, components summing +to the grand total, and each published exclusion — so a stakeholder moving a +slider cannot produce an incoherent number. + +--- + +## Adding a Calculator + +1. Copy `calculators/Genesys_Token_Calculator/` and rename it + `Vendor_Subject_Calculator` (underscores — directories are Python packages). +2. Rename the engine package; keep `staging.py` byte-for-byte. +3. Replace the anchor module with the new vendor's published table — + **verbatim**, with its own source URL and date — and rewrite the rate-card + tripwire against it. +4. Rewrite the `topic-bank` catalogue and reset `engagement-data` to + placeholders. +5. Register the notebook in [`tests/nbcheck.py`](../tests/nbcheck.py) + (`NOTEBOOK_FIRST`) — the completeness test fails until you do. +6. Run the full verification: `pytest`, `mypy`, headless `nbconvert --execute`, + `python scripts/export_report.py`, a stage simulation, and an actual + `mercury --working-dir .` render. diff --git a/pyproject.toml b/pyproject.toml index 8746185..cef9701 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,14 @@ dependencies = [ "plotly>=5.18", "numpy>=1.26", "nbformat>=5.9", + "openpyxl>=3.1", + "mercury>=3.2", + "jupyterlab>=4.0", + "ipywidgets>=8.0", + "nbconvert>=7", + "tabulate>=0.9", + "pydantic>=2.5", + "PyYAML>=6.0", ] [project.optional-dependencies] @@ -46,4 +54,5 @@ ignore = ["E501"] # line length handled by formatter [tool.ruff.lint.per-file-ignores] "studies/*/notebooks/*.ipynb" = ["E402"] "assessments/*/notebooks/*.ipynb" = ["E402"] +"calculators/*/notebooks/*.ipynb" = ["E402"] "tests/*" = ["F401"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 47d7a4c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -requests>=2.31 -python-dotenv>=1.0 -jupyter>=1.0 -pandas>=2.0 -plotly>=5.18 -numpy>=1.26 -nbformat>=5.9 -pytest>=7.4 -ruff>=0.1 diff --git a/tests/nbcheck.py b/tests/nbcheck.py index e56d3ac..f486ed1 100644 --- a/tests/nbcheck.py +++ b/tests/nbcheck.py @@ -28,6 +28,7 @@ REPO = Path(__file__).resolve().parent.parent # full check set, including the tagged-cell taxonomy. NOTEBOOK_FIRST = { "assessments/CX_Discovery_Workshop/notebooks/cx_discovery.ipynb", + "calculators/Genesys_Token_Calculator/notebooks/genesys_token_calculator.ipynb", } # Structural tier only, each with its recorded reason (see CLAUDE.md, @@ -63,7 +64,7 @@ UNIQUE_TAGS = ("topic-bank", "engagement-data", "gate", "data-appendix") def discover() -> list[str]: """Every notebook on disk under the master roots (repo-relative).""" found: list[str] = [] - for base in ("studies", "assessments", "template"): + for base in ("studies", "assessments", "calculators", "template"): root = REPO / base if not root.is_dir(): continue