Pointf5ive commited on
Commit
a26a57c
·
1 Parent(s): 3b3730b

stage 6: metric cards

Browse files

Scope: 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.

Files changed (1) hide show
  1. app.py +125 -9
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
- <section class="totem-metrics-grid">
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>