{kind.title()}
"""Hugging Face Space entry point for the CKB prediction package.
The public Space serves two stable programmatic endpoints (``/predict`` and
``/warmup``) as well as a browser-first Gradio experience. Model artifacts are
loaded lazily and never retain a visitor's submitted data.
"""
from __future__ import annotations
import html
import shutil
import tempfile
import threading
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.parse import quote
import gradio as gr
import pandas as pd
import spaces
from predictions.predict_single import CKBSingleSamplePredictor
MODEL_DIR = Path(__file__).resolve().parent / "predictions"
SHAP_CACHE_DIR = Path(tempfile.gettempdir()) / "ckb_space_shap"
SUPPLEMENTARY_IMAGE_DIR = Path(__file__).resolve().parent / "assets" / "supplementary"
SHAP_CACHE_TTL_SECONDS = 60 * 60
SHAP_LIGHT_CANVAS_MARKER = "ckb-force-plot-light-canvas"
SHAP_LIGHT_CANVAS_HEAD = f"""
"""
FEATURES = [
"sex",
"age",
"edu_level",
"marital_status",
"work",
"retire",
"hh_size",
"smoking",
"alcohol",
"height_cm",
"weight_kg",
"waist_cm",
"sbp_mmhg",
"dbp_mmhg",
"bp_drugs",
"self_health",
"chronic_pain",
]
FEATURE_LABELS = {
"sex": "性别编码",
"age": "年龄(岁)",
"edu_level": "教育程度编码",
"marital_status": "婚姻状态编码",
"work": "工作状态编码",
"retire": "退休状态编码",
"hh_size": "家庭人口数",
"smoking": "吸烟编码",
"alcohol": "饮酒编码",
"height_cm": "身高(cm)",
"weight_kg": "体重(kg)",
"waist_cm": "腰围(cm)",
"sbp_mmhg": "收缩压(mmHg)",
"dbp_mmhg": "舒张压(mmHg)",
"bp_drugs": "降压药使用编码",
"self_health": "自评健康编码",
"chronic_pain": "慢性疼痛编码",
}
FEATURE_HELP = {
"sex": "请使用训练数据中的原始分类编码。",
"age": "以完整岁数填写。",
"edu_level": "请使用训练数据中的原始分类编码。",
"marital_status": "请使用训练数据中的原始分类编码。",
"work": "请使用训练数据中的原始分类编码。",
"retire": "请使用训练数据中的原始分类编码。",
"hh_size": "同住家庭成员人数。",
"smoking": "请使用训练数据中的原始分类编码。",
"alcohol": "请使用训练数据中的原始分类编码。",
"height_cm": "连续数值,单位为厘米。",
"weight_kg": "连续数值,单位为千克。",
"waist_cm": "连续数值,单位为厘米。",
"sbp_mmhg": "连续数值,单位为 mmHg。",
"dbp_mmhg": "连续数值,单位为 mmHg。",
"bp_drugs": "请使用训练数据中的原始分类编码。",
"self_health": "请使用训练数据中的原始分类编码。",
"chronic_pain": "请使用训练数据中的原始分类编码。",
}
FEATURE_GROUPS = [
(
"01",
"基本资料",
"人口与社会特征",
["sex", "age", "edu_level", "marital_status", "work", "retire", "hh_size"],
),
(
"02",
"生活与感受",
"行为及健康自评",
["smoking", "alcohol", "self_health", "chronic_pain"],
),
(
"03",
"体征与用药",
"身体测量与血压信息",
["height_cm", "weight_kg", "waist_cm", "sbp_mmhg", "dbp_mmhg", "bp_drugs"],
),
]
INTEGER_FEATURES = {
"sex",
"age",
"edu_level",
"marital_status",
"work",
"retire",
"hh_size",
"smoking",
"alcohol",
"sbp_mmhg",
"dbp_mmhg",
"bp_drugs",
"self_health",
"chronic_pain",
}
# Display text is deliberately human-readable; the paired values remain the
# frozen numeric encodings expected by the trained model.
CATEGORICAL_CHOICES = {
"sex": [("Male", 0), ("Female", 1)],
"edu_level": [("Low", 1), ("Intermediate", 2), ("High", 3)],
"marital_status": [
("Other observed status", 0),
("Married or partnered", 1),
],
"work": [("No", 0), ("Yes", 1)],
"retire": [("No", 0), ("Yes", 1)],
"smoking": [("No", 0), ("Yes", 1)],
"alcohol": [("No", 0), ("Yes", 1)],
"bp_drugs": [("No", 0), ("Yes", 1)],
"self_health": [
("Very good or excellent", 1),
("Good", 2),
("Fair or regular", 3),
("Poor or very poor", 4),
],
"chronic_pain": [("No", 0), ("Yes", 1)],
}
EXAMPLE_VALUES = {
"sex": 0,
"age": 59,
"edu_level": 1,
"marital_status": 1,
"work": 1,
"retire": 0,
"hh_size": 5,
"smoking": 1,
"alcohol": 1,
"height_cm": 163.8,
"weight_kg": 59.6,
"waist_cm": 81.1,
"sbp_mmhg": 125,
"dbp_mmhg": 67,
"bp_drugs": 0,
"self_health": 2,
"chronic_pain": 0,
}
TABLE_COLUMNS = ["代码", "预测结局", "概率", "冻结阈值", "风险判定"]
DEFAULT_EN_FEATURE_LABELS = {
"sex": "Sex",
"age": "Age (years)",
"edu_level": "Education level",
"marital_status": "Married or partnered",
"work": "Currently working",
"retire": "Retired",
"hh_size": "Household size",
"smoking": "Smoking",
"alcohol": "Alcohol use",
"height_cm": "Height (cm)",
"weight_kg": "Weight (kg)",
"waist_cm": "Waist (cm)",
"sbp_mmhg": "Systolic BP (mmHg)",
"dbp_mmhg": "Diastolic BP (mmHg)",
"bp_drugs": "Blood-pressure medication",
"self_health": "Self-rated health",
"chronic_pain": "Chronic pain",
}
DEFAULT_EN_FEATURE_HELP = {feature: "" for feature in FEATURES}
TABLE_COLUMNS_EN = ["Code", "Outcome", "Probability", "Frozen threshold", "Risk decision"]
# Presentation-only labels and cumulative-incidence reference values supplied
# for the public demo. They do not participate in model inference or risk
# classification, which always uses the frozen per-outcome Youden threshold.
OUTCOME_PRESENTATION = {
"A00_B99": {
"en": "Infectious and parasitic diseases",
"zh": "感染性疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 0.7,
"low_10": 1.9,
"high_5": 1.5,
"high_10": 4.6,
},
"C00_D48": {
"en": "Neoplasms",
"zh": "肿瘤",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 1.9,
"low_10": 4.8,
"high_5": 4.3,
"high_10": 9.5,
},
"E00_E90": {
"en": "Endocrine and metabolic diseases",
"zh": "内分泌代谢疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 1.2,
"low_10": 3.6,
"high_5": 5.2,
"high_10": 12.4,
},
"F00_F99": {
"en": "Mental and behavioural disorders",
"zh": "精神和行为障碍",
"model_en": "Random Forest",
"model_zh": "随机森林",
"low_5": 0.2,
"low_10": 0.5,
"high_5": 0.6,
"high_10": 1.3,
},
"G00_G99": {
"en": "Diseases of the nervous system",
"zh": "神经系统疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 0.6,
"low_10": 2.7,
"high_5": 2.5,
"high_10": 7.7,
},
"H00_H95": {
"en": "Diseases of the eye and ear",
"zh": "眼耳疾病",
"model_en": "Random Forest",
"model_zh": "随机森林",
"low_5": 0.6,
"low_10": 2.4,
"high_5": 2.6,
"high_10": 8.9,
},
"I00_I99": {
"en": "Diseases of the circulatory system",
"zh": "循环系统疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 2.6,
"low_10": 9.9,
"high_5": 16.1,
"high_10": 38.1,
},
"J00_J99": {
"en": "Diseases of the respiratory system",
"zh": "呼吸系统疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 2.0,
"low_10": 6.4,
"high_5": 5.9,
"high_10": 18.6,
},
"K00_K93": {
"en": "Diseases of the digestive system",
"zh": "消化系统疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 3.1,
"low_10": 9.2,
"high_5": 5.6,
"high_10": 16.0,
},
"L00_L99": {
"en": "Diseases of the skin",
"zh": "皮肤病",
"model_en": "Random Forest",
"model_zh": "随机森林",
"low_5": 0.1,
"low_10": 0.5,
"high_5": 0.3,
"high_10": 1.2,
},
"M00_M99": {
"en": "Musculoskeletal diseases",
"zh": "肌肉骨骼疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 1.2,
"low_10": 5.5,
"high_5": 2.5,
"high_10": 11.0,
},
"N00_N99": {
"en": "Diseases of the genitourinary system",
"zh": "泌尿生殖系统疾病",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 1.6,
"low_10": 4.6,
"high_5": 3.2,
"high_10": 9.2,
},
"death": {
"en": "All-cause death",
"zh": "全因死亡",
"model_en": "MLP",
"model_zh": "MLP",
"low_5": 0.9,
"low_10": 2.4,
"high_5": 8.0,
"high_10": 20.2,
},
}
# Keep Chinese presentation copy in unicode escapes so it remains intact when
# the Space is edited from Windows shells with different console code pages.
_OUTCOME_ZH = {
"A00_B99": "\u611f\u67d3\u6027\u75be\u75c5",
"C00_D48": "\u80bf\u7624",
"E00_E90": "\u5185\u5206\u6ccc\u4ee3\u8c22\u75be\u75c5",
"F00_F99": "\u7cbe\u795e\u548c\u884c\u4e3a\u969c\u788d",
"G00_G99": "\u795e\u7ecf\u7cfb\u7edf\u75be\u75c5",
"H00_H95": "\u773c\u8033\u75be\u75c5",
"I00_I99": "\u5faa\u73af\u7cfb\u7edf\u75be\u75c5",
"J00_J99": "\u547c\u5438\u7cfb\u7edf\u75be\u75c5",
"K00_K93": "\u6d88\u5316\u7cfb\u7edf\u75be\u75c5",
"L00_L99": "\u76ae\u80a4\u75c5",
"M00_M99": "\u808c\u8089\u9aa8\u9abc\u75be\u75c5",
"N00_N99": "\u6ccc\u5c3f\u751f\u6b96\u7cfb\u7edf\u75be\u75c5",
"death": "\u5168\u56e0\u6b7b\u4ea1",
}
for _outcome_code, _outcome_name_zh in _OUTCOME_ZH.items():
OUTCOME_PRESENTATION[_outcome_code]["zh"] = _outcome_name_zh
OUTCOME_PRESENTATION["F00_F99"]["model_zh"] = "\u968f\u673a\u68ee\u6797"
OUTCOME_PRESENTATION["H00_H95"]["model_zh"] = "\u968f\u673a\u68ee\u6797"
OUTCOME_PRESENTATION["L00_L99"]["model_zh"] = "\u968f\u673a\u68ee\u6797"
CUSTOM_CSS = """
:root {
--ckb-blue-950: #082f49;
--ckb-blue-900: #0c4a6e;
--ckb-blue-700: #0369a1;
--ckb-blue-600: #0284c7;
--ckb-blue-100: #e0f2fe;
--ckb-blue-50: #f0f9ff;
--ckb-cyan-100: #cffafe;
--ckb-green-700: #15803d;
--ckb-green-600: #16a34a;
--ckb-green-100: #dcfce7;
--ckb-amber-700: #b45309;
--ckb-amber-100: #fef3c7;
--ckb-slate-950: #0f172a;
--ckb-slate-700: #334155;
--ckb-slate-600: #475569;
--ckb-slate-400: #94a3b8;
--ckb-slate-200: #e2e8f0;
--ckb-slate-100: #f1f5f9;
--ckb-white: #ffffff;
--ckb-shadow-sm: 0 1px 2px rgb(15 23 42 / 0.05);
--ckb-shadow-md: 0 16px 40px rgb(12 74 110 / 0.10);
--ckb-radius-lg: 20px;
--ckb-radius-md: 14px;
--ckb-radius-sm: 10px;
--ckb-motion-fast: 180ms;
--ckb-motion-base: 280ms;
--ckb-layer-field: 10;
--ckb-layer-dropdown: 40;
}
.gradio-container {
background:
radial-gradient(circle at 10% 0%, rgb(207 250 254 / 0.75), transparent 24rem),
radial-gradient(circle at 90% 9%, rgb(224 242 254 / 0.95), transparent 25rem),
var(--ckb-blue-50);
color: var(--ckb-slate-950);
color-scheme: light !important;
font-family: Aptos, "Segoe UI", "Microsoft YaHei UI", "Noto Sans SC", sans-serif;
}
#ckb-shell {
width: min(1180px, 100%);
margin: 0 auto;
padding: 24px 18px 52px;
color: var(--ckb-slate-950);
}
#ckb-shell * { box-sizing: border-box; }
.ckb-skip {
position: absolute;
left: -10000px;
top: auto;
overflow: hidden;
width: 1px;
height: 1px;
}
.ckb-skip:focus {
left: 18px;
top: 18px;
z-index: 1000;
width: auto;
height: auto;
padding: 10px 14px;
border-radius: var(--ckb-radius-sm);
background: var(--ckb-blue-950);
color: var(--ckb-white);
font-weight: 700;
}
.ckb-hero {
position: relative;
isolation: isolate;
overflow: hidden;
min-height: 286px;
padding: clamp(30px, 6vw, 68px);
border: 1px solid rgb(255 255 255 / 0.28);
border-radius: 28px;
background:
linear-gradient(123deg, var(--ckb-blue-950) 0%, var(--ckb-blue-900) 56%, var(--ckb-blue-700) 100%);
box-shadow: var(--ckb-shadow-md);
color: var(--ckb-white) !important;
}
.ckb-hero::before,
.ckb-hero::after {
position: absolute;
z-index: -1;
display: block;
border-radius: 999px;
content: "";
filter: blur(2px);
}
.ckb-hero::before {
top: -138px;
right: -58px;
width: 322px;
height: 322px;
border: 1px solid rgb(207 250 254 / 0.30);
background: radial-gradient(circle, rgb(34 211 238 / 0.30), transparent 67%);
}
.ckb-hero::after {
right: 18%;
bottom: -185px;
width: 360px;
height: 360px;
background: radial-gradient(circle, rgb(22 163 74 / 0.24), transparent 67%);
}
.ckb-hero__content { position: relative; max-width: 720px; }
.ckb-eyebrow,
.ckb-chip,
.ckb-section-kicker,
.ckb-result-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.78rem;
font-weight: 750;
letter-spacing: 0.08em;
line-height: 1;
text-transform: uppercase;
}
.ckb-hero .ckb-eyebrow {
color: var(--ckb-cyan-100) !important;
-webkit-text-fill-color: var(--ckb-cyan-100);
}
.ckb-mark {
display: inline-grid;
width: 28px;
height: 28px;
place-items: center;
border: 1px solid rgb(207 250 254 / 0.36);
border-radius: 9px;
background: rgb(255 255 255 / 0.10);
}
.ckb-mark svg { width: 17px; height: 17px; stroke: currentColor; }
.ckb-hero h1 {
max-width: 13ch;
margin: 16px 0 14px;
color: var(--ckb-white) !important;
-webkit-text-fill-color: var(--ckb-white);
font-family: Georgia, "Noto Serif SC", serif;
font-size: clamp(2.15rem, 5.1vw, 4.25rem);
font-weight: 650;
letter-spacing: -0.045em;
line-height: 1.04;
}
.ckb-hero p {
max-width: 62ch;
margin: 0;
color: #f0f9ff !important;
-webkit-text-fill-color: #f0f9ff;
font-size: clamp(1rem, 1.4vw, 1.12rem);
line-height: 1.7;
}
.ckb-hero__meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 26px;
}
.ckb-chip {
min-height: 34px;
padding: 8px 11px;
border: 1px solid rgb(255 255 255 / 0.16);
border-radius: 999px;
background: rgb(255 255 255 / 0.10);
color: var(--ckb-white) !important;
-webkit-text-fill-color: var(--ckb-white);
letter-spacing: 0.02em;
text-transform: none;
}
.ckb-chip svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; }
.ckb-trust-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin: 18px 0 24px;
}
.ckb-trust-card {
min-width: 0;
padding: 16px 18px;
border: 1px solid var(--ckb-blue-100);
border-radius: var(--ckb-radius-md);
background: rgb(255 255 255 / 0.78);
box-shadow: var(--ckb-shadow-sm);
}
.ckb-trust-card strong {
display: block;
color: var(--ckb-blue-900);
font-family: Georgia, "Noto Serif SC", serif;
font-size: 1.42rem;
font-variant-numeric: tabular-nums;
line-height: 1.1;
}
.ckb-trust-card span {
display: block;
margin-top: 5px;
color: var(--ckb-slate-600);
font-size: 0.86rem;
line-height: 1.35;
}
.ckb-language-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
margin: -4px 0 14px;
}
.ckb-language-switch,
.ckb-theme-switch {
display: inline-flex;
gap: 4px;
padding: 4px;
border: 1px solid var(--ckb-blue-100);
border-radius: 999px;
background: rgb(255 255 255 / 0.88);
box-shadow: var(--ckb-shadow-sm);
}
.ckb-language-switch button,
.ckb-theme-switch button {
min-width: 74px;
min-height: 36px;
padding: 7px 12px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--ckb-slate-600);
cursor: pointer;
font: inherit;
font-size: 0.82rem;
font-weight: 750;
transition: background var(--ckb-motion-fast) ease, color var(--ckb-motion-fast) ease, transform var(--ckb-motion-fast) ease;
}
.ckb-theme-switch button { min-width: 58px; }
.ckb-language-switch button:hover,
.ckb-theme-switch button:hover { background: var(--ckb-blue-50); color: var(--ckb-blue-900); }
.ckb-language-switch button:active,
.ckb-theme-switch button:active { transform: scale(0.98); }
.ckb-language-switch button[aria-pressed="true"],
.ckb-theme-switch button[aria-pressed="true"] { background: var(--ckb-blue-900); color: var(--ckb-white); }
.ckb-language-switch button:focus-visible,
.ckb-theme-switch button:focus-visible { outline: 3px solid rgb(2 132 199 / 0.32); outline-offset: 2px; }
.ckb-status {
display: flex;
align-items: flex-start;
gap: 10px;
min-height: 52px;
margin: 0 0 20px;
padding: 14px 16px;
border: 1px solid var(--ckb-blue-100);
border-radius: var(--ckb-radius-md);
background: rgb(255 255 255 / 0.82);
color: var(--ckb-slate-700);
box-shadow: var(--ckb-shadow-sm);
line-height: 1.5;
}
.ckb-status__dot {
flex: 0 0 auto;
width: 10px;
height: 10px;
margin-top: 7px;
border-radius: 50%;
background: var(--ckb-blue-600);
box-shadow: 0 0 0 4px var(--ckb-blue-100);
}
.ckb-status--ready { border-color: var(--ckb-green-100); background: #f6fff8; }
.ckb-status--ready .ckb-status__dot { background: var(--ckb-green-600); box-shadow: 0 0 0 4px var(--ckb-green-100); }
.ckb-status--warning { border-color: var(--ckb-amber-100); background: #fffbeb; }
.ckb-status--warning .ckb-status__dot { background: var(--ckb-amber-700); box-shadow: 0 0 0 4px var(--ckb-amber-100); }
.ckb-panel {
padding: clamp(22px, 3vw, 34px);
border: 1px solid var(--ckb-blue-100);
border-radius: var(--ckb-radius-lg);
background: rgb(255 255 255 / 0.88);
box-shadow: var(--ckb-shadow-md);
}
.ckb-section-heading {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
}
.ckb-section-kicker { color: var(--ckb-blue-700); }
.ckb-section-heading h2 {
margin: 7px 0 6px;
color: var(--ckb-slate-950);
font-family: Georgia, "Noto Serif SC", serif;
font-size: clamp(1.55rem, 3vw, 2.1rem);
letter-spacing: -0.025em;
line-height: 1.16;
}
.ckb-section-heading p {
max-width: 58ch;
margin: 0;
color: var(--ckb-slate-600);
font-size: 0.95rem;
line-height: 1.6;
}
.ckb-coding-note {
flex: 0 0 auto;
max-width: 265px;
padding: 10px 12px;
border-radius: var(--ckb-radius-sm);
background: var(--ckb-blue-50);
color: var(--ckb-blue-900);
font-size: 0.82rem;
font-weight: 650;
line-height: 1.45;
}
.ckb-input-grid { gap: 14px; }
.ckb-form-card {
height: 100%;
padding: 17px;
border: 1px solid var(--ckb-slate-200);
border-radius: var(--ckb-radius-md);
background: var(--ckb-white);
box-shadow: var(--ckb-shadow-sm);
}
.ckb-form-card .form-card__top {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 14px;
}
.ckb-form-card .form-card__index {
display: inline-grid;
flex: 0 0 auto;
width: 30px;
height: 30px;
place-items: center;
border-radius: 9px;
background: var(--ckb-blue-100);
color: var(--ckb-blue-900);
font-size: 0.78rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.ckb-form-card h3 { margin: 1px 0 3px; color: var(--ckb-slate-950); font-size: 1rem; }
.ckb-form-card p { margin: 0; color: var(--ckb-slate-600); font-size: 0.8rem; line-height: 1.4; }
#ckb-shell .ckb-form-card .block { min-width: 0; margin-bottom: 10px; }
#ckb-shell .ckb-form-card label { color: var(--ckb-slate-700); font-size: 0.86rem; font-weight: 700; }
#ckb-shell .ckb-form-card input {
min-height: 48px;
border: 1px solid var(--ckb-slate-200);
border-radius: var(--ckb-radius-sm);
background: var(--ckb-white);
color: var(--ckb-slate-950);
font-size: 1rem;
font-variant-numeric: tabular-nums;
transition: border-color var(--ckb-motion-fast) ease, box-shadow var(--ckb-motion-fast) ease;
}
#ckb-shell .ckb-form-card input:hover { border-color: var(--ckb-blue-600); }
#ckb-shell .ckb-form-card input:focus {
border-color: var(--ckb-blue-600);
box-shadow: 0 0 0 3px rgb(2 132 199 / 0.20);
outline: none;
}
.ckb-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px;
margin-top: 24px;
}
#predict-button,
#reset-button {
min-height: 48px;
border-radius: var(--ckb-radius-sm);
font-weight: 750;
transition: transform var(--ckb-motion-fast) ease, box-shadow var(--ckb-motion-fast) ease, background var(--ckb-motion-fast) ease;
}
#predict-button {
min-width: 172px;
border: 0;
background: var(--ckb-green-600);
box-shadow: 0 10px 22px rgb(22 163 74 / 0.22);
color: var(--ckb-white);
}
#predict-button:hover { background: var(--ckb-green-700); box-shadow: 0 13px 26px rgb(22 163 74 / 0.29); transform: translateY(-1px); }
#predict-button:active { transform: translateY(0) scale(0.985); }
#predict-button:focus-visible,
#reset-button:focus-visible { outline: 3px solid rgb(2 132 199 / 0.35); outline-offset: 3px; }
#reset-button {
border: 1px solid var(--ckb-slate-200);
background: var(--ckb-white);
color: var(--ckb-slate-700);
}
#reset-button:hover { border-color: var(--ckb-blue-600); color: var(--ckb-blue-900); transform: translateY(-1px); }
.ckb-privacy-copy {
display: flex;
flex: 1 1 250px;
align-items: center;
gap: 8px;
margin: 0;
color: var(--ckb-slate-600);
font-size: 0.82rem;
line-height: 1.45;
}
.ckb-privacy-copy svg { flex: 0 0 auto; width: 17px; height: 17px; stroke: var(--ckb-blue-700); stroke-width: 2; }
.ckb-results {
margin-top: 26px;
padding: clamp(22px, 3vw, 34px);
border: 1px solid var(--ckb-blue-100);
border-radius: var(--ckb-radius-lg);
background: rgb(255 255 255 / 0.90);
box-shadow: var(--ckb-shadow-md);
}
.ckb-results-header { margin-bottom: 17px; }
.ckb-results-header h2 { margin: 7px 0 0; color: var(--ckb-slate-950); font-family: Georgia, "Noto Serif SC", serif; font-size: clamp(1.5rem, 3vw, 2rem); }
.ckb-result-card,
.ckb-result-placeholder {
min-height: 122px;
padding: 20px;
border: 1px solid var(--ckb-blue-100);
border-radius: var(--ckb-radius-md);
background: linear-gradient(112deg, var(--ckb-blue-50), var(--ckb-white));
}
.ckb-result-placeholder { display: grid; place-items: center; color: var(--ckb-slate-600); text-align: center; }
.ckb-result-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
.ckb-result-kicker { color: var(--ckb-blue-700); }
.ckb-result-card h3 { margin: 6px 0 0; color: var(--ckb-blue-950); font-family: Georgia, "Noto Serif SC", serif; font-size: 1.7rem; }
.ckb-result-badge { flex: 0 0 auto; padding: 8px 10px; border-radius: 999px; background: var(--ckb-green-100); color: var(--ckb-green-700); font-size: 0.8rem; font-weight: 800; }
.ckb-result-badge--review { background: var(--ckb-amber-100); color: var(--ckb-amber-700); }
.ckb-result-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin: 18px 0 0;
}
.ckb-result-metrics div { padding: 11px; border-radius: var(--ckb-radius-sm); background: rgb(255 255 255 / 0.76); }
.ckb-result-metrics dt { color: var(--ckb-slate-600); font-size: 0.76rem; }
.ckb-result-metrics dd { margin: 4px 0 0; color: var(--ckb-slate-950); font-size: 1rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.ckb-risk-visual { margin-top: 18px; padding: 20px; border: 1px solid var(--ckb-slate-200); border-radius: var(--ckb-radius-md); background: var(--ckb-white); }
.ckb-risk-visual__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 16px; }
.ckb-risk-visual h3 { margin: 0; color: var(--ckb-slate-950); font-size: 1.05rem; }
.ckb-risk-visual p { margin: 5px 0 0; color: var(--ckb-slate-600); font-size: 0.82rem; line-height: 1.45; }
.ckb-legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--ckb-slate-600); font-size: 0.76rem; }
.ckb-legend span { display: inline-flex; align-items: center; gap: 5px; }
.ckb-legend i { width: 9px; height: 9px; border-radius: 50%; background: var(--ckb-green-600); }
.ckb-legend .ckb-legend--high { background: var(--ckb-amber-700); }
.ckb-risk-row { display: grid; grid-template-columns: minmax(88px, 0.8fr) minmax(130px, 2fr) 54px; gap: 10px; align-items: center; margin: 10px 0; }
.ckb-risk-label { overflow: hidden; color: var(--ckb-slate-700); font-size: 0.78rem; font-weight: 700; text-overflow: ellipsis; white-space: nowrap; }
.ckb-risk-track { position: relative; height: 11px; overflow: hidden; border-radius: 999px; background: var(--ckb-slate-100); }
.ckb-risk-fill { display: block; width: var(--risk-width); height: 100%; border-radius: inherit; background: var(--ckb-green-600); }
.ckb-risk-row--high .ckb-risk-fill { background: var(--ckb-amber-700); }
.ckb-risk-threshold { position: absolute; top: 0; bottom: 0; left: var(--threshold); width: 2px; background: var(--ckb-slate-950); opacity: 0.58; }
.ckb-risk-value { color: var(--ckb-slate-950); font-size: 0.8rem; font-weight: 800; font-variant-numeric: tabular-nums; text-align: right; }
#outcome-table { margin-top: 18px; overflow-x: auto; }
#outcome-table table { min-width: 680px; border-collapse: separate; border-spacing: 0; }
#outcome-table thead { background: var(--ckb-blue-50); }
#outcome-table th { color: var(--ckb-blue-900); font-size: 0.78rem; font-weight: 800; }
#outcome-table td { color: var(--ckb-slate-700); font-size: 0.84rem; }
#outcome-table th, #outcome-table td { border-color: var(--ckb-slate-200); }
#shap-result { overflow: hidden !important; margin-top: 18px; border: 1px solid var(--ckb-blue-700); border-radius: var(--ckb-radius-md); background: var(--ckb-white); box-shadow: var(--ckb-shadow-sm); }
#shap-result > button {
position: relative;
display: flex !important;
align-items: center;
width: 100%;
min-height: 64px;
padding: 14px 68px 14px 22px !important;
border: 0 !important;
border-radius: var(--ckb-radius-md);
background: linear-gradient(112deg, var(--ckb-blue-950), var(--ckb-blue-700));
color: var(--ckb-white) !important;
cursor: pointer;
font-size: 0.98rem;
font-weight: 800;
letter-spacing: 0.01em;
line-height: 1.35;
text-align: left;
touch-action: manipulation;
transition: background 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}
#shap-result > button::after {
position: absolute;
right: 26px;
width: 11px;
height: 11px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
content: "";
transform: rotate(45deg) translateY(-3px);
transform-origin: center;
transition: transform 180ms ease;
}
#shap-result > button:hover { background: linear-gradient(112deg, var(--ckb-blue-900), var(--ckb-blue-600)); box-shadow: inset 0 0 0 999px rgb(255 255 255 / 0.05); }
#shap-result > button:active { transform: scale(0.995); }
#shap-result > button:focus-visible { outline: 3px solid rgb(2 132 199 / 0.42); outline-offset: -5px; }
#shap-result > button.open { border-bottom: 1px solid var(--ckb-blue-100) !important; border-radius: var(--ckb-radius-md) var(--ckb-radius-md) 0 0; background: var(--ckb-blue-50); color: var(--ckb-blue-950) !important; }
#shap-result > button.open::after { transform: rotate(225deg) translateY(-3px); }
.ckb-shap-placeholder { display: grid; min-height: 108px; place-items: center; color: var(--ckb-slate-600); text-align: center; }
.ckb-shap-gallery { padding: 4px 2px 2px; }
.ckb-shap-gallery__header { margin: 0 0 16px; }
.ckb-shap-gallery__header .ckb-section-kicker { color: var(--ckb-blue-700); }
.ckb-shap-gallery__header h3 { margin: 6px 0 0; color: var(--ckb-slate-950); font-size: 1.2rem; }
.ckb-shap-gallery__header p { margin: 6px 0 0; color: var(--ckb-slate-600); font-size: 0.84rem; line-height: 1.5; }
.ckb-shap-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.ckb-shap-card { min-width: 0; overflow: hidden; border: 1px solid var(--ckb-slate-200); border-radius: var(--ckb-radius-sm); background: var(--ckb-white); box-shadow: var(--ckb-shadow-sm); }
.ckb-shap-card--cluster { grid-column: 1 / -1; }
.ckb-shap-card__header { padding: 13px 14px 10px; border-bottom: 1px solid var(--ckb-slate-100); background: linear-gradient(112deg, var(--ckb-blue-50), var(--ckb-white)); }
.ckb-shap-card__kind { margin: 0; color: var(--ckb-blue-700); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; }
.ckb-shap-card h4 { margin: 4px 0 0; color: var(--ckb-slate-950); font-size: 0.94rem; line-height: 1.35; }
.ckb-shap-frame { display: block; width: 100%; height: 330px; border: 0; background: var(--ckb-white); }
#raw-result { margin-top: 16px; border: 1px solid var(--ckb-slate-200); border-radius: var(--ckb-radius-sm); background: var(--ckb-white); }
#raw-result button { min-height: 44px; color: var(--ckb-slate-700); font-weight: 750; }
.ckb-footer-note { margin: 20px 0 0; color: var(--ckb-slate-600); font-size: 0.82rem; line-height: 1.6; }
.ckb-footer-note code { padding: 2px 5px; border-radius: 5px; background: var(--ckb-slate-100); color: var(--ckb-blue-900); }
.ckb-hero, .ckb-trust-card, .ckb-panel, .ckb-results { will-change: auto; }
/* The hero now carries one concise model statement; the old labels and count
tiles remain in the DOM only so legacy locale initialisation stays stable. */
.ckb-eyebrow, .ckb-hero__meta, .ckb-trust-row { display: none !important; }
.ckb-hero { min-height: 230px; display: grid; align-items: center; }
.ckb-hero p { max-width: 66ch; }
.ckb-section-heading > div > p, .ckb-coding-note { display: none !important; }
/* Gradio renders dropdown lists in a popover. Keeping animated ancestors
transform-free and elevating the portal fixes the offset seen in the form. */
#ckb-shell .ckb-form-card .block,
#ckb-shell .ckb-form-card .wrap { position: relative; overflow: visible !important; }
#ckb-shell .ckb-form-card .block { z-index: 0; }
#ckb-shell .ckb-form-card .block:focus-within { z-index: var(--ckb-layer-field); }
body .secondary-wrap,
body [role="listbox"] { z-index: var(--ckb-layer-dropdown) !important; }
#ckb-shell .ckb-form-card button,
#ckb-shell .ckb-form-card [role="combobox"] { min-height: 48px; border-radius: var(--ckb-radius-sm); }
#ckb-shell,
#outcome-table,
#outcome-table .html-container,
#shap-result,
#shap-result .wrap,
#shap-result .form,
#shap-result .html-container,
#shap-gallery,
#shap-gallery .wrap,
#shap-gallery .html-container,
.ckb-image-gallery,
.ckb-image-card,
.ckb-image-card__force { color-scheme: light !important; }
.ckb-image-frame,
.ckb-reference-figure,
.ckb-cluster-profile-figure { color-scheme: only light !important; }
#shap-result .wrap,
#shap-result .form,
#shap-result .html-container,
#shap-gallery,
#shap-gallery .wrap,
#shap-gallery .html-container { background: var(--ckb-white) !important; color: var(--ckb-slate-950) !important; }
#ckb-shell .ckb-panel .ckb-section-heading,
#ckb-shell .ckb-panel .ckb-section-heading h2,
#ckb-shell .ckb-panel .ckb-section-heading p,
#ckb-shell .ckb-status,
#ckb-shell .ckb-status strong,
#ckb-shell .ckb-results-header,
#ckb-shell .ckb-results-header h2,
#ckb-shell .ckb-result-placeholder,
#ckb-shell .ckb-footer-note { color: var(--ckb-slate-950) !important; }
#ckb-shell .ckb-panel .ckb-section-heading .ckb-section-kicker,
#ckb-shell .ckb-results-header .ckb-section-kicker { color: var(--ckb-blue-700) !important; }
#ckb-shell .ckb-status { color: var(--ckb-slate-700) !important; }
#ckb-shell .ckb-status span,
#ckb-shell .ckb-status strong { color: inherit !important; }
#ckb-shell .ckb-result-placeholder,
#ckb-shell .ckb-footer-note { color: var(--ckb-slate-600) !important; }
#ckb-shell .ckb-footer-note code { color: var(--ckb-blue-900) !important; }
#ckb-shell .ckb-form-card,
#ckb-shell .ckb-form-card > .styler,
#ckb-shell .ckb-form-card .form,
#ckb-shell .ckb-form-card .container { background: var(--ckb-white) !important; color: var(--ckb-slate-950) !important; }
#ckb-shell .ckb-form-card .block { background: transparent !important; border-color: transparent !important; }
#ckb-shell .ckb-form-card .wrap,
#ckb-shell .ckb-form-card .wrap-inner,
#ckb-shell .ckb-form-card .secondary-wrap { background: var(--ckb-white) !important; border-color: var(--ckb-slate-200) !important; color: var(--ckb-slate-950) !important; }
#ckb-shell .ckb-form-card .form-card__index { color: var(--ckb-blue-900) !important; }
#ckb-shell .ckb-form-card h3 { color: var(--ckb-slate-950) !important; }
#ckb-shell .ckb-form-card p { color: var(--ckb-slate-600) !important; }
#ckb-shell .ckb-form-card [data-testid="block-info"],
#ckb-shell .ckb-form-card label { color: var(--ckb-slate-700) !important; }
body .options[role="listbox"] { border: 1px solid var(--ckb-slate-200) !important; border-radius: var(--ckb-radius-sm) !important; background: var(--ckb-white) !important; box-shadow: var(--ckb-shadow-md) !important; }
body .options[role="listbox"] [role="option"] { background: var(--ckb-white) !important; color: var(--ckb-slate-950) !important; }
body .options[role="listbox"] [role="option"]:hover,
body .options[role="listbox"] [role="option"][aria-selected="true"] { background: var(--ckb-blue-50) !important; color: var(--ckb-blue-950) !important; }
.ckb-outcome-table { margin-top: 18px; overflow: hidden; border: 1px solid var(--ckb-slate-200); border-radius: var(--ckb-radius-md); background: var(--ckb-white); box-shadow: var(--ckb-shadow-sm); }
.ckb-outcome-table__header { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 18px 20px 15px; border-bottom: 1px solid var(--ckb-slate-100); background: linear-gradient(112deg, var(--ckb-blue-50), var(--ckb-white)); }
.ckb-outcome-table__header h3 { margin: 5px 0 0; color: var(--ckb-slate-950); font-size: 1.08rem; }
.ckb-outcome-table__header > p { max-width: 50ch; margin: 0; color: var(--ckb-slate-600); font-size: 0.8rem; line-height: 1.45; text-align: right; }
.ckb-outcome-table__scroll { overflow-x: auto; }
.ckb-outcome-table table { width: 100%; min-width: 720px; border-collapse: collapse; }
.ckb-outcome-table th, .ckb-outcome-table td { padding: 12px 14px; border-bottom: 1px solid var(--ckb-slate-100); text-align: left; }
.ckb-outcome-table th { color: var(--ckb-blue-900); background: rgb(240 249 255 / 0.72); font-size: 0.76rem; font-weight: 850; letter-spacing: 0.025em; }
.ckb-outcome-table td { color: var(--ckb-slate-700); font-size: 0.84rem; vertical-align: middle; }
.ckb-outcome-table tbody tr:last-child td { border-bottom: 0; }
.ckb-outcome-table tbody tr:hover { background: var(--ckb-blue-50); }
.ckb-outcome-table__code { color: var(--ckb-slate-950) !important; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ckb-outcome-table__decision { display: inline-flex; padding: 5px 8px; border-radius: 999px; background: var(--ckb-green-100); color: var(--ckb-green-700); font-size: 0.75rem; font-weight: 800; white-space: nowrap; }
.ckb-outcome-table__decision--high { background: var(--ckb-amber-100); color: var(--ckb-amber-700); }
.ckb-image-placeholder { display: grid; min-height: 108px; place-items: center; padding: 20px; color: var(--ckb-slate-600); text-align: center; }
.ckb-image-gallery { padding: 2px; }
.ckb-image-gallery__header { margin: 0 0 20px; }
.ckb-image-gallery__header h3 { margin: 6px 0 0; color: var(--ckb-slate-950); font-size: 1.2rem; }
.ckb-image-gallery__header p { max-width: 74ch; margin: 6px 0 0; color: var(--ckb-slate-600); font-size: 0.84rem; line-height: 1.5; }
.ckb-image-section + .ckb-image-section { margin-top: 22px; }
.ckb-image-section__header { margin: 0 0 10px; }
.ckb-image-section__header h4 { margin: 0; color: var(--ckb-blue-900); font-size: 0.9rem; font-weight: 850; }
.ckb-image-grid { display: grid; gap: 14px; }
.ckb-image-grid--clusters { grid-template-columns: 1fr; }
.ckb-image-grid--outcomes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ckb-image-card { min-width: 0; overflow: hidden; border: 1px solid var(--ckb-slate-200); border-radius: var(--ckb-radius-sm); background: var(--ckb-white); box-shadow: var(--ckb-shadow-sm); }
.ckb-image-card--selected { border-color: var(--ckb-blue-600); box-shadow: 0 0 0 2px rgb(2 132 199 / 0.14), var(--ckb-shadow-sm); }
.ckb-image-card__header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 12px 13px 10px; border-bottom: 1px solid var(--ckb-slate-100); background: linear-gradient(112deg, var(--ckb-blue-50), var(--ckb-white)); }
.ckb-image-card__kind { margin: 0; color: var(--ckb-blue-700); font-size: 0.7rem; font-weight: 850; letter-spacing: 0.06em; text-transform: uppercase; }
.ckb-image-card h4 { margin: 4px 0 0; color: var(--ckb-slate-950); font-size: 0.9rem; line-height: 1.35; }
.ckb-image-card__badge { flex: 0 0 auto; max-width: 50%; padding: 5px 8px; border-radius: 999px; background: var(--ckb-blue-100); color: var(--ckb-blue-900); font-size: 0.7rem; font-weight: 800; line-height: 1.2; text-align: center; }
.ckb-image-card__badge--high { background: var(--ckb-amber-100); color: var(--ckb-amber-700); }
.ckb-image-frame { display: block; width: 100%; height: 218px; border: 0; background: var(--ckb-white); }
.ckb-image-card__context { display: grid; grid-template-columns: minmax(205px, 0.9fr) minmax(0, 1.1fr); gap: 12px; align-items: stretch; padding: 12px; background: #fbfdff; }
.ckb-image-card__curve { min-width: 0; padding: 3px; border: 1px solid var(--ckb-slate-100); border-radius: 8px; background: var(--ckb-white); }
.ckb-reference-figure { display: block; width: 100%; height: 100%; min-height: 184px; object-fit: contain; border-radius: 6px; background: var(--ckb-white); }
.ckb-cluster-profile-figure { display: block; width: 100%; max-height: 230px; object-fit: contain; padding: 10px 12px; background: var(--ckb-white); }
.ckb-image-card__force { border-top: 1px solid var(--ckb-slate-100); background: var(--ckb-white); }
.ckb-risk-curve { display: block; width: 100%; height: auto; }
.ckb-risk-curve__grid path { fill: none; stroke: #dbeafe; stroke-width: 1; }
.ckb-risk-curve__low, .ckb-risk-curve__high { fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 3; }
.ckb-risk-curve__low { stroke: #0d9488; }
.ckb-risk-curve__high { stroke: #d97706; }
.ckb-risk-curve__legend text, .ckb-risk-curve__axis { fill: var(--ckb-slate-600); font-size: 10px; font-family: ui-sans-serif, system-ui, sans-serif; }
.ckb-risk-curve__point--low circle { fill: #0d9488; }.ckb-risk-curve__point--low text { fill: #0f766e; }.ckb-risk-curve__point--high circle { fill: #d97706; }.ckb-risk-curve__point--high text { fill: #b45309; }
.ckb-risk-curve__point text { font-size: 10px; font-weight: 800; font-family: ui-sans-serif, system-ui, sans-serif; }
.ckb-image-card__explanation { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
.ckb-image-card__explanation p { margin: 0; color: var(--ckb-slate-600); font-size: 0.78rem; line-height: 1.55; }
.ckb-image-card__explanation .ckb-image-card__risk { margin-bottom: 7px; color: var(--ckb-blue-900); font-size: 0.76rem; font-weight: 850; }
/* Theme selection is explicit: the page never follows the browser/system theme. */
html[data-ckb-theme="light"] .gradio-container { color-scheme: light !important; }
#ckb-shell[data-theme="dark"] {
--ckb-blue-950: #082f49;
--ckb-blue-900: #0c4a6e;
--ckb-blue-700: #7dd3fc;
--ckb-blue-600: #38bdf8;
--ckb-blue-100: #153b58;
--ckb-blue-50: #0f1d31;
--ckb-cyan-100: #164e63;
--ckb-green-700: #bbf7d0;
--ckb-green-600: #4ade80;
--ckb-green-100: #14532d;
--ckb-amber-700: #fcd34d;
--ckb-amber-100: #78350f;
--ckb-slate-950: #f8fafc;
--ckb-slate-700: #e2e8f0;
--ckb-slate-600: #cbd5e1;
--ckb-slate-400: #94a3b8;
--ckb-slate-200: #334155;
--ckb-slate-100: #1e293b;
--ckb-white: #111827;
--ckb-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.32);
--ckb-shadow-md: 0 16px 40px rgb(0 0 0 / 0.34);
color-scheme: dark !important;
}
html[data-ckb-theme="dark"] .gradio-container {
background:
radial-gradient(circle at 10% 0%, rgb(8 47 73 / 0.76), transparent 24rem),
radial-gradient(circle at 90% 9%, rgb(15 35 57 / 0.9), transparent 25rem),
#020617;
color: #f8fafc;
color-scheme: dark !important;
}
html[data-ckb-theme="dark"] #ckb-shell,
html[data-ckb-theme="dark"] #outcome-table,
html[data-ckb-theme="dark"] #shap-result,
html[data-ckb-theme="dark"] #shap-gallery,
html[data-ckb-theme="dark"] .ckb-image-gallery,
html[data-ckb-theme="dark"] .ckb-image-card,
html[data-ckb-theme="dark"] .ckb-image-card__force { color-scheme: dark !important; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-panel,
html[data-ckb-theme="dark"] #ckb-shell .ckb-results { background: rgb(15 23 42 / 0.94); border-color: var(--ckb-slate-200); }
html[data-ckb-theme="dark"] #ckb-shell .ckb-status--ready { background: #102a1c; border-color: #166534; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-status--warning { background: #422006; border-color: #92400e; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-result-metrics div { background: #172033; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-image-card__context { background: #111c2d; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-language-switch,
html[data-ckb-theme="dark"] #ckb-shell .ckb-theme-switch { background: #111827; border-color: var(--ckb-slate-200); }
html[data-ckb-theme="dark"] #ckb-shell .ckb-language-switch button[aria-pressed="true"],
html[data-ckb-theme="dark"] #ckb-shell .ckb-theme-switch button[aria-pressed="true"],
html[data-ckb-theme="dark"] #ckb-shell #predict-button,
html[data-ckb-theme="dark"] #ckb-shell #shap-result > button { color: #ffffff !important; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-hero,
html[data-ckb-theme="dark"] #ckb-shell .ckb-hero h1,
html[data-ckb-theme="dark"] #ckb-shell .ckb-hero p {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
html[data-ckb-theme="dark"] #ckb-shell #shap-result > button.open { color: #e0f2fe !important; }
html[data-ckb-theme="dark"] #ckb-shell .ckb-image-frame,
html[data-ckb-theme="dark"] #ckb-shell .ckb-reference-figure,
html[data-ckb-theme="dark"] #ckb-shell .ckb-cluster-profile-figure { background: #ffffff !important; color-scheme: only light !important; }
html[data-ckb-theme="dark"] body .options[role="listbox"] { border-color: #334155 !important; background: #111827 !important; }
html[data-ckb-theme="dark"] body .options[role="listbox"] [role="option"] { background: #111827 !important; color: #f8fafc !important; }
html[data-ckb-theme="dark"] body .options[role="listbox"] [role="option"]:hover,
html[data-ckb-theme="dark"] body .options[role="listbox"] [role="option"][aria-selected="true"] { background: #0f2f4a !important; color: #f8fafc !important; }
@media (max-width: 820px) {
#ckb-shell { padding: 14px 12px 36px; }
.ckb-hero { min-height: 0; border-radius: var(--ckb-radius-lg); }
.ckb-trust-row { grid-template-columns: 1fr; }
.ckb-section-heading { display: block; }
.ckb-coding-note { max-width: none; margin-top: 14px; }
.ckb-risk-row { grid-template-columns: 104px minmax(90px, 1fr) 48px; gap: 8px; }
.ckb-shap-grid { grid-template-columns: 1fr; }
.ckb-shap-card--cluster { grid-column: auto; }
.ckb-image-grid--clusters { grid-template-columns: 1fr; }
.ckb-image-grid--outcomes { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
#ckb-shell { padding-inline: 8px; }
.ckb-hero, .ckb-panel, .ckb-results { border-radius: 16px; }
.ckb-hero { padding: 28px 22px; }
.ckb-hero h1 { max-width: 11ch; }
.ckb-panel, .ckb-results { padding: 18px; }
.ckb-result-card__header, .ckb-risk-visual__header { display: block; }
.ckb-result-badge, .ckb-legend { margin-top: 10px; }
.ckb-shap-frame { height: 360px; }
.ckb-outcome-table__header { display: block; }
.ckb-outcome-table__header > p { margin-top: 7px; text-align: left; }
.ckb-image-grid--clusters { grid-template-columns: 1fr; }
.ckb-image-frame { height: 210px; }
.ckb-image-card__context { grid-template-columns: 1fr; }
.ckb-actions { align-items: stretch; }
#predict-button, #reset-button { width: 100%; }
.ckb-privacy-copy { flex-basis: 100%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; }
}
"""
HEAD = """
"""
_predictor: CKBSingleSamplePredictor | None = None
_load_lock = threading.Lock()
_inference_lock = threading.Lock()
def get_predictor() -> CKBSingleSamplePredictor:
"""Load the large model bundle once, on the first prediction request."""
global _predictor
if _predictor is None:
with _load_lock:
if _predictor is None:
_predictor = CKBSingleSamplePredictor(MODEL_DIR)
return _predictor
def _clean_expired_shap_reports() -> None:
"""Remove old, per-request SHAP files from the ephemeral Space filesystem."""
if not SHAP_CACHE_DIR.exists():
return
deadline = time.time() - SHAP_CACHE_TTL_SECONDS
for report_dir in SHAP_CACHE_DIR.iterdir():
try:
if report_dir.is_dir() and report_dir.stat().st_mtime < deadline:
shutil.rmtree(report_dir, ignore_errors=True)
except OSError:
continue
def _ensure_light_shap_canvas(plot_path: str) -> None:
"""Keep generated SHAP HTML legible when the host browser uses dark mode."""
path = Path(plot_path)
try:
document = path.read_text(encoding="utf-8")
if SHAP_LIGHT_CANVAS_MARKER in document:
return
if "" not in document:
return
path.write_text(
document.replace("", SHAP_LIGHT_CANVAS_HEAD + "", 1),
encoding="utf-8",
)
except (OSError, UnicodeError):
return
def _shap_force_plot_card(kind: str, title: str, plot_path: str, *, cluster: bool = False) -> str:
"""Embed one trusted, locally-generated SHAP HTML force plot by temporary URL."""
card_class = " ckb-shap-card--cluster" if cluster else ""
_ensure_light_shap_canvas(plot_path)
plot_url = "/gradio_api/file=" + quote(Path(plot_path).resolve().as_posix(), safe="/")
return f"""
{kind.title()}{html.escape(title)}
One force plot explains the assigned cluster; the remaining 13 explain each outcome probability surrogate.
Outcome risk panel
{risk_en}
{html.escape(explanation_en)}
Cluster attribution
Each outcome panel places its cumulative-risk reference and brief interpretation above the related SHAP force plot.
OUTCOME PREDICTIONS
Risk class is determined by the frozen threshold for each outcome.
| Code | Outcome | Probability | Frozen threshold | Risk decision |
|---|
横线标记各结局冻结的 Youden 阈值;此图用于研究结果阅读,并非临床诊断。
一次完整输入,返回冻结 K=3 聚类分配与 13 项结局风险概率。表单与 API 共用同一套模型推理逻辑。
所有 17 项均为必填。分类变量必须与训练数据使用相同编码;本服务不会重新编码、插补或保存输入。
{subtitle}
推理结束后会清除临时文件;请勿将本工具用作诊断或治疗决策。
""" ) with gr.Column(elem_classes="ckb-results"): gr.HTML( """