Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit ·
a26a57c
1
Parent(s): 3b3730b
stage 6: metric cards
Browse filesScope: render five metric cards from dashboard state metrics with deterministic 12-bar history strips and manual footer hints.
Validation: labels/scores/bar-count/empty-state checks passed; callbacks unchanged.
app.py
CHANGED
|
@@ -603,6 +603,74 @@ TOTEM_CSS = """
|
|
| 603 |
gap: var(--space-3);
|
| 604 |
}
|
| 605 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
@media (max-width: 1200px) {
|
| 607 |
.totem-topbar {
|
| 608 |
grid-template-columns: auto minmax(200px, 1fr);
|
|
@@ -987,6 +1055,62 @@ def render_status_row(state: dict) -> str:
|
|
| 987 |
"""
|
| 988 |
|
| 989 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 990 |
def render_dashboard(state: dict) -> str:
|
| 991 |
"""
|
| 992 |
Stage 3/4 static shell wrapper.
|
|
@@ -1001,15 +1125,7 @@ def render_dashboard(state: dict) -> str:
|
|
| 1001 |
{render_topbar(s)}
|
| 1002 |
{render_hero(s)}
|
| 1003 |
{render_status_row(s)}
|
| 1004 |
-
|
| 1005 |
-
<div class="totem-row">
|
| 1006 |
-
<div class="totem-placeholder">Metrics Placeholder</div>
|
| 1007 |
-
<div class="totem-placeholder">Metrics Placeholder</div>
|
| 1008 |
-
<div class="totem-placeholder">Metrics Placeholder</div>
|
| 1009 |
-
<div class="totem-placeholder">Metrics Placeholder</div>
|
| 1010 |
-
<div class="totem-placeholder">Metrics Placeholder</div>
|
| 1011 |
-
</div>
|
| 1012 |
-
</section>
|
| 1013 |
<section class="totem-lower-grid">
|
| 1014 |
<div class="totem-placeholder">Revision Queue Placeholder</div>
|
| 1015 |
<div class="totem-placeholder">Risk Clusters Placeholder</div>
|
|
|
|
| 603 |
gap: var(--space-3);
|
| 604 |
}
|
| 605 |
|
| 606 |
+
.totem-metric-card {
|
| 607 |
+
min-height: 178px;
|
| 608 |
+
padding: 18px;
|
| 609 |
+
border-radius: 14px;
|
| 610 |
+
background: linear-gradient(180deg, rgba(16, 26, 58, 0.96), rgba(8, 18, 45, 0.98));
|
| 611 |
+
border: 1px solid rgba(174, 183, 204, 0.3);
|
| 612 |
+
font-family: var(--font-ui);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.totem-metric-head {
|
| 616 |
+
display: grid;
|
| 617 |
+
grid-template-columns: 56px 1fr;
|
| 618 |
+
gap: 12px;
|
| 619 |
+
align-items: center;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.totem-metric-icon {
|
| 623 |
+
width: 56px;
|
| 624 |
+
height: 56px;
|
| 625 |
+
border-radius: 999px;
|
| 626 |
+
display: grid;
|
| 627 |
+
place-items: center;
|
| 628 |
+
color: #fff;
|
| 629 |
+
font-size: 24px;
|
| 630 |
+
background: rgba(7, 18, 45, 0.45);
|
| 631 |
+
border: 1px solid var(--metric-color);
|
| 632 |
+
box-shadow: 0 0 22px color-mix(in srgb, var(--metric-color), transparent 55%);
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
.totem-metric-label {
|
| 636 |
+
font-size: 14px;
|
| 637 |
+
color: rgba(246, 241, 232, 0.95);
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.totem-metric-score {
|
| 641 |
+
margin-top: 12px;
|
| 642 |
+
font-size: 42px;
|
| 643 |
+
line-height: 1;
|
| 644 |
+
color: var(--metric-color);
|
| 645 |
+
font-variant-numeric: tabular-nums;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
.totem-metric-score small {
|
| 649 |
+
font-size: 17px;
|
| 650 |
+
color: rgba(174, 183, 204, 0.95);
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
.totem-metric-bars {
|
| 654 |
+
display: flex;
|
| 655 |
+
align-items: end;
|
| 656 |
+
gap: 6px;
|
| 657 |
+
height: 32px;
|
| 658 |
+
margin-top: 14px;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.totem-metric-bars span {
|
| 662 |
+
width: 4px;
|
| 663 |
+
border-radius: 3px 3px 0 0;
|
| 664 |
+
background: var(--metric-color);
|
| 665 |
+
opacity: .95;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.totem-metric-hint {
|
| 669 |
+
margin-top: 12px;
|
| 670 |
+
font-size: 12px;
|
| 671 |
+
color: rgba(174, 183, 204, 0.98);
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
@media (max-width: 1200px) {
|
| 675 |
.totem-topbar {
|
| 676 |
grid-template-columns: auto minmax(200px, 1fr);
|
|
|
|
| 1055 |
"""
|
| 1056 |
|
| 1057 |
|
| 1058 |
+
def _metric_bar_heights(score: int, history_values: list) -> list[int]:
|
| 1059 |
+
history = []
|
| 1060 |
+
if isinstance(history_values, list):
|
| 1061 |
+
for v in history_values:
|
| 1062 |
+
history.append(max(0, min(100, int(round(_state_float(v, score))))))
|
| 1063 |
+
if len(history) >= 12:
|
| 1064 |
+
values = history[-12:]
|
| 1065 |
+
else:
|
| 1066 |
+
# Deterministic fallback wave around current score when no/short history is available.
|
| 1067 |
+
base = max(0, min(100, int(score)))
|
| 1068 |
+
offsets = [-16, -10, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12]
|
| 1069 |
+
values = [max(0, min(100, base + off)) for off in offsets]
|
| 1070 |
+
if history:
|
| 1071 |
+
values[: len(history)] = history
|
| 1072 |
+
return [max(6, min(34, int(round(6 + (v * 0.28))))) for v in values]
|
| 1073 |
+
|
| 1074 |
+
|
| 1075 |
+
def render_metric_cards(state: dict) -> str:
|
| 1076 |
+
metrics = state.get("metrics", {}) if isinstance(state, dict) else {}
|
| 1077 |
+
history = state.get("metric_history", {}) if isinstance(state, dict) else {}
|
| 1078 |
+
card_defs = [
|
| 1079 |
+
("overall_publishability", "Overall Publishability", "✦", "var(--totem-gold)", "Source: viability lens"),
|
| 1080 |
+
("read_aloud_flow", "Read-Aloud Flow", "≋", "var(--totem-pink)", "Weakest live pressure"),
|
| 1081 |
+
("emotional_truth", "Emotional Truth", "❤", "var(--totem-emerald)", "Strongest story signal"),
|
| 1082 |
+
("visual_strength", "Visual Strength", "◉", "var(--totem-cyan)", "Drawable page value"),
|
| 1083 |
+
("commercial_viability", "Commercial Viability", "↗", "var(--totem-violet)", "Publisher-facing lens"),
|
| 1084 |
+
]
|
| 1085 |
+
|
| 1086 |
+
cards = []
|
| 1087 |
+
for key, label, icon, color, hint in card_defs:
|
| 1088 |
+
score = max(0, min(100, int(round(_state_float(metrics.get(key, 0), 0)))))
|
| 1089 |
+
bars = _metric_bar_heights(score, history.get(key, []) if isinstance(history, dict) else [])
|
| 1090 |
+
bars_html = "".join(f"<span style='height:{h}px'></span>" for h in bars)
|
| 1091 |
+
cards.append(
|
| 1092 |
+
f"""
|
| 1093 |
+
<article class="totem-metric-card" style="--metric-color:{color};" aria-label="{esc(label)} {score} out of 100. {esc(hint)}">
|
| 1094 |
+
<div class="totem-metric-head">
|
| 1095 |
+
<div class="totem-metric-icon">{icon}</div>
|
| 1096 |
+
<div class="totem-metric-label">{esc(label)}</div>
|
| 1097 |
+
</div>
|
| 1098 |
+
<div class="totem-metric-score">{score}<small>/100</small></div>
|
| 1099 |
+
<div class="totem-metric-bars">{bars_html}</div>
|
| 1100 |
+
<div class="totem-metric-hint">{esc(hint)}</div>
|
| 1101 |
+
</article>
|
| 1102 |
+
"""
|
| 1103 |
+
)
|
| 1104 |
+
|
| 1105 |
+
return f"""
|
| 1106 |
+
<section class="totem-metrics-grid">
|
| 1107 |
+
<div class="totem-row">
|
| 1108 |
+
{''.join(cards)}
|
| 1109 |
+
</div>
|
| 1110 |
+
</section>
|
| 1111 |
+
"""
|
| 1112 |
+
|
| 1113 |
+
|
| 1114 |
def render_dashboard(state: dict) -> str:
|
| 1115 |
"""
|
| 1116 |
Stage 3/4 static shell wrapper.
|
|
|
|
| 1125 |
{render_topbar(s)}
|
| 1126 |
{render_hero(s)}
|
| 1127 |
{render_status_row(s)}
|
| 1128 |
+
{render_metric_cards(s)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1129 |
<section class="totem-lower-grid">
|
| 1130 |
<div class="totem-placeholder">Revision Queue Placeholder</div>
|
| 1131 |
<div class="totem-placeholder">Risk Clusters Placeholder</div>
|