Viney Claude Sonnet 4.6 commited on
Commit
67da450
Β·
1 Parent(s): c60e11b

feat: add eyebrow_label/importance_marker/meta_row/sort_by_impact helpers; neutralize card surfaces (Task 2)

Browse files

- Add FS_BODY, FS_META, FS_EYEBROW to imports from dashboard.theme
- New eyebrow_label(): centralises uppercase micro-label pattern using FS_EYEBROW
- New importance_marker(): monochrome HIGH/MEDIUM/LOW dot+word, no green/red
- New meta_row(): single attribution line replacing 3-badge stacks (reliability/source/impact)
- New sort_by_impact(): stable HIGH→MEDIUM→LOW→unknown sort on lists of dicts
- interpretation_card: background AI_BG→BG (white), keep violet left edge + ai_badge
- fact_card: replace 3-badge div with meta_row(); apply FS_BODY; add max-width:680px
- tension_card: outer BG white + amber left edge; inner panels BG_MUTED + BORDER;
move ai_badge to outer header (one, not two); replace badge rows with meta_row()
- earnings_quality_card: background always BG (white); drop ai_badge("AI assessment");
replace 3-badge row with meta_row()
- delta_card: background BG (white), keep kind-colored left edge + existing pill row
- numbered_list: apply FS_BODY to item text; add max-width:680px to container

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. dashboard/components.py +121 -43
dashboard/components.py CHANGED
@@ -9,6 +9,7 @@ from dashboard.theme import (
9
  PURPLE, AI_BG, AI_BORDER, AI_COLOR, AI_BADGE_BG,
10
  IMPACT_HIGH_BG, IMPACT_MED_BG, IMPACT_LOW_TEXT,
11
  RADIUS_CHIP,
 
12
  )
13
 
14
  # ── Reliability badge ─────────────────────────────────────────────────────────
@@ -65,11 +66,11 @@ def interpretation_card(
65
  badge_label: str = "AI Synthesis",
66
  ) -> str:
67
  return (
68
- f'<div class="primer-card" style="background:{AI_BG};border:1px solid {AI_BORDER};'
69
  f'border-left:4px solid {PURPLE};border-radius:0 12px 12px 0;'
70
  f'padding:20px 24px;margin-bottom:4px;">'
71
  f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">'
72
- f'<span style="font-size:0.62rem;font-weight:700;letter-spacing:0.1em;'
73
  f'text-transform:uppercase;color:{TEXT_MUTED};">{header_label}</span>'
74
  f'{ai_badge(badge_label)}'
75
  f'</div>'
@@ -104,6 +105,99 @@ def source_badge(s: str) -> str:
104
  )
105
 
106
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  # ── Section page header ───────────────────────────────────────────────────────
108
 
109
  def section_header(title: str, subtitle: str = "") -> str:
@@ -144,21 +238,17 @@ def evidence_quote(snippet: str, bg: str = "#fafaf9", border_color: str = "#e5e7
144
  # ── Sourced-fact card ─────────────────────────────────────────────────────────
145
 
146
  def fact_card(fact: dict, accent: str = GREEN) -> None:
147
- """Render a sourced-fact card with left accent border, evidence quote, and badges."""
148
  st.markdown(
149
  f"""
150
- <div class="primer-card" style="padding:18px 20px;margin-bottom:8px;
151
  background:{BG};border:1px solid {BORDER};
152
- border-left:3px solid {accent};border-radius:0 8px 8px 0;">
153
- <div style="font-size:0.9rem;line-height:1.5;margin-bottom:8px;color:{TEXT};">
154
  {fact.get("text","")}
155
  </div>
156
  {evidence_quote(fact.get("evidence_snippet",""), bg=BG_MUTED, border_color=BORDER)}
157
- <div style="margin-top:6px;display:flex;gap:5px;flex-wrap:wrap;align-items:center;">
158
- {reliability_badge(fact.get("reliability",""))}
159
- {source_badge(fact.get("source",""))}
160
- {impact_badge(fact.get("impact","") or "")}
161
- </div>
162
  </div>
163
  """,
164
  unsafe_allow_html=True,
@@ -190,49 +280,42 @@ def tension_card(tension: dict) -> None:
190
  )
191
 
192
  bull_side = (
193
- f'<div style="flex:1;padding:12px 14px;background:{BULL_BG};border:1px solid {BULL_BORDER};'
194
  f'border-radius:8px;min-width:0;">'
195
  f'<div style="display:flex;align-items:center;gap:5px;margin-bottom:6px;">'
196
- f'<span style="font-size:0.58rem;font-weight:700;text-transform:uppercase;'
197
  f'letter-spacing:0.09em;color:#059669;">Surface reading</span>'
198
- f'{ai_badge("AI")}'
199
  f'</div>'
200
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};margin-bottom:8px;">'
201
  f'{bullish_reading}</div>'
202
- f'{evidence_quote(bull_ev.get("evidence_snippet","") if isinstance(bull_ev, dict) else "", bg=BG, border_color=BULL_BORDER)}'
203
- f'<div style="margin-top:6px;display:flex;gap:5px;flex-wrap:wrap;align-items:center;">'
204
- f'{reliability_badge(bull_ev.get("reliability","") if isinstance(bull_ev, dict) else "")}'
205
- f'{source_badge(bull_ev.get("source","") if isinstance(bull_ev, dict) else "")}'
206
- f'{impact_badge((bull_ev.get("impact","") or "") if isinstance(bull_ev, dict) else "")}'
207
- f'</div>'
208
  f'</div>'
209
  )
210
  bear_side = (
211
- f'<div style="flex:1;padding:12px 14px;background:{BEAR_BG};border:1px solid {BEAR_BORDER};'
212
  f'border-radius:8px;min-width:0;">'
213
  f'<div style="display:flex;align-items:center;gap:5px;margin-bottom:6px;">'
214
- f'<span style="font-size:0.58rem;font-weight:700;text-transform:uppercase;'
215
  f'letter-spacing:0.09em;color:#dc2626;">Deeper reading</span>'
216
- f'{ai_badge("AI")}'
217
  f'</div>'
218
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};margin-bottom:8px;">'
219
  f'{bearish_reading}</div>'
220
- f'{evidence_quote(bear_ev.get("evidence_snippet","") if isinstance(bear_ev, dict) else "", bg=BG, border_color=BEAR_BORDER)}'
221
- f'<div style="margin-top:6px;display:flex;gap:5px;flex-wrap:wrap;align-items:center;">'
222
- f'{reliability_badge(bear_ev.get("reliability","") if isinstance(bear_ev, dict) else "")}'
223
- f'{source_badge(bear_ev.get("source","") if isinstance(bear_ev, dict) else "")}'
224
- f'{impact_badge((bear_ev.get("impact","") or "") if isinstance(bear_ev, dict) else "")}'
225
- f'</div>'
226
  f'</div>'
227
  )
228
 
229
  st.markdown(
230
- f'<div class="primer-card" style="background:{WARN_BG};border:1px solid {WARN_BORDER};'
231
- f'border-radius:10px;padding:14px 16px;margin-bottom:10px;">'
232
  f'<div style="display:flex;align-items:flex-start;justify-content:space-between;'
233
  f'gap:8px;margin-bottom:10px;">'
234
  f'<div style="font-size:0.88rem;font-weight:600;color:{TEXT};line-height:1.4;">{headline}</div>'
 
235
  f'{weight_chip}'
 
 
236
  f'</div>'
237
  f'<div style="display:flex;gap:10px;">{bull_side}{bear_side}</div>'
238
  f'</div>',
@@ -366,15 +449,11 @@ def earnings_quality_card(signal: dict) -> str:
366
  f'border-left:2px solid {border};padding-left:8px;line-height:1.4;">'
367
  f'&ldquo;{snippet}&rdquo;</div>'
368
  )
369
- badges_html = ""
370
- if ev_rel or ev_source or ev_impact:
371
- badges_html = (
372
- f'<div style="margin-top:8px;display:flex;gap:5px;flex-wrap:wrap;align-items:center;">'
373
- f'{reliability_badge(ev_rel)}{source_badge(ev_source)}{impact_badge(ev_impact)}</div>'
374
- )
375
 
376
  return (
377
- f'<div class="primer-card" style="background:{bg};border:1px solid {border};'
378
  f'border-top:3px solid {color};border-radius:10px;padding:14px 16px;margin-bottom:8px;">'
379
  f'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">'
380
  f'<span style="font-size:0.65rem;font-weight:700;text-transform:uppercase;'
@@ -384,10 +463,9 @@ def earnings_quality_card(signal: dict) -> str:
384
  f'border-radius:4px;padding:2px 8px;font-size:0.65rem;font-weight:700;">'
385
  f'{assessment_icon}&nbsp;{assessment_text}</span>'
386
  f'</div>'
387
- f'<div style="margin-bottom:6px;">{ai_badge("AI assessment")}</div>'
388
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};">{rationale}</div>'
389
  f'{ev_html}'
390
- f'{badges_html}'
391
  f'</div>'
392
  )
393
 
@@ -430,7 +508,7 @@ def numbered_list(
430
  f'<span style="display:inline-flex;align-items:center;justify-content:center;'
431
  f'width:22px;height:22px;border-radius:50%;flex-shrink:0;margin-top:1px;'
432
  f'background:{GRAY};color:#fff;font-size:0.7rem;font-weight:700;">{i}</span>'
433
- f'<span style="font-size:0.9rem;line-height:1.55;color:{TEXT};">{item}</span>'
434
  f'</div>'
435
  )
436
  footer_html = ""
@@ -441,7 +519,7 @@ def numbered_list(
441
  )
442
  return (
443
  f'<div style="border:1px solid {BORDER};border-radius:12px;'
444
- f'padding:4px 16px;background:{BG};">'
445
  f'{rows_html}{footer_html}'
446
  f'</div>'
447
  )
@@ -472,7 +550,7 @@ def delta_card(delta: dict) -> None:
472
  ) if metric else ""
473
 
474
  st.markdown(
475
- f'<div class="primer-card" style="background:{bg};border:1px solid {fg}44;'
476
  f'border-left:4px solid {fg};border-radius:0 10px 10px 0;'
477
  f'padding:14px 16px;margin-bottom:8px;">'
478
  f'<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap;">'
 
9
  PURPLE, AI_BG, AI_BORDER, AI_COLOR, AI_BADGE_BG,
10
  IMPACT_HIGH_BG, IMPACT_MED_BG, IMPACT_LOW_TEXT,
11
  RADIUS_CHIP,
12
+ FS_BODY, FS_META, FS_EYEBROW,
13
  )
14
 
15
  # ── Reliability badge ─────────────────────────────────────────────────────────
 
66
  badge_label: str = "AI Synthesis",
67
  ) -> str:
68
  return (
69
+ f'<div class="primer-card" style="background:{BG};border:1px solid {AI_BORDER};'
70
  f'border-left:4px solid {PURPLE};border-radius:0 12px 12px 0;'
71
  f'padding:20px 24px;margin-bottom:4px;">'
72
  f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">'
73
+ f'<span style="font-size:{FS_EYEBROW};font-weight:700;letter-spacing:0.1em;'
74
  f'text-transform:uppercase;color:{TEXT_MUTED};">{header_label}</span>'
75
  f'{ai_badge(badge_label)}'
76
  f'</div>'
 
105
  )
106
 
107
 
108
+ # ── Eyebrow label ─────────────────────────────────────────────────────────────
109
+
110
+ def eyebrow_label(text: str, color: str = TEXT_MUTED) -> str:
111
+ """Uppercase micro-label used as a section/card eyebrow."""
112
+ return (
113
+ f'<span style="font-size:{FS_EYEBROW};font-weight:700;text-transform:uppercase;'
114
+ f'letter-spacing:0.1em;color:{color};">{text}</span>'
115
+ )
116
+
117
+
118
+ # ── Importance marker ─────────────────────────────────────────────────────────
119
+
120
+ _IMPORTANCE_MAP: dict[str, tuple[str, str, str, str, str]] = {
121
+ # impact β†’ (dot_char, dot_color, text_label, text_color, font_weight)
122
+ "HIGH": ("●", "#0f172a", "Critical", "#0f172a", "600"),
123
+ "MEDIUM": ("●", "#64748b", "Important", "#64748b", "500"),
124
+ "LOW": ("β—‹", "#94a3b8", "Context", "#94a3b8", "500"),
125
+ }
126
+
127
+ def importance_marker(impact: str) -> str:
128
+ """Monochrome dot + word marker for impact level. No green/red β€” never competes with bull/bear."""
129
+ spec = _IMPORTANCE_MAP.get((impact or "").upper())
130
+ if not spec:
131
+ return ""
132
+ dot_char, dot_color, text_label, text_color, fw = spec
133
+ return (
134
+ f'<span style="font-size:{FS_META};font-weight:{fw};color:{text_color};'
135
+ f'display:inline-flex;align-items:center;gap:3px;">'
136
+ f'<span style="color:{dot_color};">{dot_char}</span>'
137
+ f'{text_label}'
138
+ f'</span>'
139
+ )
140
+
141
+
142
+ # ── Meta row ─────────────────────────────────────────────────────────────────
143
+
144
+ _RELIABILITY_LABELS: dict[str, str] = {
145
+ "HIGH": "High confidence",
146
+ "MEDIUM": "Confidence med.",
147
+ "LOW": "Low confidence",
148
+ }
149
+
150
+ def meta_row(reliability: str, source: str, impact: str) -> str:
151
+ """Single subtle attribution line replacing the 3-badge stack.
152
+
153
+ Example output: ● Critical Β· 10-Q Β· High confidence
154
+ Returns "" if all args are empty/None.
155
+ """
156
+ if not reliability and not source and not impact:
157
+ return ""
158
+
159
+ parts: list[str] = []
160
+
161
+ impact_str = (impact or "").upper()
162
+ marker_html = importance_marker(impact_str)
163
+ if marker_html:
164
+ parts.append(marker_html)
165
+
166
+ if source:
167
+ parts.append(
168
+ f'<span style="font-size:{FS_META};color:{TEXT_MUTED};">{source}</span>'
169
+ )
170
+
171
+ if reliability:
172
+ rel_label = _RELIABILITY_LABELS.get(reliability.upper(), reliability.title())
173
+ parts.append(
174
+ f'<span style="font-size:{FS_META};color:{TEXT_MUTED};">{rel_label}</span>'
175
+ )
176
+
177
+ if not parts:
178
+ return ""
179
+
180
+ sep = f'<span style="font-size:{FS_META};color:{TEXT_MUTED};margin:0 2px;">Β·</span>'
181
+ inner = sep.join(parts)
182
+ return (
183
+ f'<div style="font-size:{FS_META};color:{TEXT_MUTED};margin-top:8px;'
184
+ f'display:flex;align-items:center;gap:4px;flex-wrap:wrap;">'
185
+ f'{inner}'
186
+ f'</div>'
187
+ )
188
+
189
+
190
+ # ── Sort by impact ────────────────────────────────────────────────────────────
191
+
192
+ _IMPACT_ORDER: dict[str, int] = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
193
+
194
+ def sort_by_impact(items: list[dict]) -> list[dict]:
195
+ """Stable sort: HIGH β†’ MEDIUM β†’ LOW β†’ None/unknown. Returns a new list."""
196
+ def _key(item: dict) -> int:
197
+ return _IMPACT_ORDER.get((item.get("impact") or "").upper(), 3)
198
+ return sorted(items, key=_key)
199
+
200
+
201
  # ── Section page header ───────────────────────────────────────────────────────
202
 
203
  def section_header(title: str, subtitle: str = "") -> str:
 
238
  # ── Sourced-fact card ─────────────────────────────────────────────────────────
239
 
240
  def fact_card(fact: dict, accent: str = GREEN) -> None:
241
+ """Render a sourced-fact card with left accent border, evidence quote, and meta row."""
242
  st.markdown(
243
  f"""
244
+ <div class="primer-card primer-prose" style="padding:18px 20px;margin-bottom:8px;
245
  background:{BG};border:1px solid {BORDER};
246
+ border-left:3px solid {accent};border-radius:0 8px 8px 0;max-width:680px;">
247
+ <div style="font-size:{FS_BODY};line-height:1.5;margin-bottom:8px;color:{TEXT};">
248
  {fact.get("text","")}
249
  </div>
250
  {evidence_quote(fact.get("evidence_snippet",""), bg=BG_MUTED, border_color=BORDER)}
251
+ {meta_row(fact.get("reliability",""), fact.get("source",""), fact.get("impact","") or "")}
 
 
 
 
252
  </div>
253
  """,
254
  unsafe_allow_html=True,
 
280
  )
281
 
282
  bull_side = (
283
+ f'<div style="flex:1;padding:12px 14px;background:{BG_MUTED};border:1px solid {BORDER};'
284
  f'border-radius:8px;min-width:0;">'
285
  f'<div style="display:flex;align-items:center;gap:5px;margin-bottom:6px;">'
286
+ f'<span style="font-size:{FS_EYEBROW};font-weight:700;text-transform:uppercase;'
287
  f'letter-spacing:0.09em;color:#059669;">Surface reading</span>'
 
288
  f'</div>'
289
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};margin-bottom:8px;">'
290
  f'{bullish_reading}</div>'
291
+ f'{evidence_quote(bull_ev.get("evidence_snippet","") if isinstance(bull_ev, dict) else "", bg=BG, border_color=BORDER)}'
292
+ f'{meta_row(bull_ev.get("reliability","") if isinstance(bull_ev, dict) else "", bull_ev.get("source","") if isinstance(bull_ev, dict) else "", (bull_ev.get("impact","") or "") if isinstance(bull_ev, dict) else "")}'
 
 
 
 
293
  f'</div>'
294
  )
295
  bear_side = (
296
+ f'<div style="flex:1;padding:12px 14px;background:{BG_MUTED};border:1px solid {BORDER};'
297
  f'border-radius:8px;min-width:0;">'
298
  f'<div style="display:flex;align-items:center;gap:5px;margin-bottom:6px;">'
299
+ f'<span style="font-size:{FS_EYEBROW};font-weight:700;text-transform:uppercase;'
300
  f'letter-spacing:0.09em;color:#dc2626;">Deeper reading</span>'
 
301
  f'</div>'
302
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};margin-bottom:8px;">'
303
  f'{bearish_reading}</div>'
304
+ f'{evidence_quote(bear_ev.get("evidence_snippet","") if isinstance(bear_ev, dict) else "", bg=BG, border_color=BORDER)}'
305
+ f'{meta_row(bear_ev.get("reliability","") if isinstance(bear_ev, dict) else "", bear_ev.get("source","") if isinstance(bear_ev, dict) else "", (bear_ev.get("impact","") or "") if isinstance(bear_ev, dict) else "")}'
 
 
 
 
306
  f'</div>'
307
  )
308
 
309
  st.markdown(
310
+ f'<div class="primer-card" style="background:{BG};border:1px solid {WARN_BORDER};'
311
+ f'border-left:4px solid {AMBER};border-radius:10px;padding:14px 16px;margin-bottom:10px;">'
312
  f'<div style="display:flex;align-items:flex-start;justify-content:space-between;'
313
  f'gap:8px;margin-bottom:10px;">'
314
  f'<div style="font-size:0.88rem;font-weight:600;color:{TEXT};line-height:1.4;">{headline}</div>'
315
+ f'<div style="display:flex;align-items:center;gap:6px;flex-shrink:0;">'
316
  f'{weight_chip}'
317
+ f'{ai_badge("AI")}'
318
+ f'</div>'
319
  f'</div>'
320
  f'<div style="display:flex;gap:10px;">{bull_side}{bear_side}</div>'
321
  f'</div>',
 
449
  f'border-left:2px solid {border};padding-left:8px;line-height:1.4;">'
450
  f'&ldquo;{snippet}&rdquo;</div>'
451
  )
452
+
453
+ meta_html = meta_row(ev_rel, ev_source, ev_impact)
 
 
 
 
454
 
455
  return (
456
+ f'<div class="primer-card" style="background:{BG};border:1px solid {border};'
457
  f'border-top:3px solid {color};border-radius:10px;padding:14px 16px;margin-bottom:8px;">'
458
  f'<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">'
459
  f'<span style="font-size:0.65rem;font-weight:700;text-transform:uppercase;'
 
463
  f'border-radius:4px;padding:2px 8px;font-size:0.65rem;font-weight:700;">'
464
  f'{assessment_icon}&nbsp;{assessment_text}</span>'
465
  f'</div>'
 
466
  f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};">{rationale}</div>'
467
  f'{ev_html}'
468
+ f'{meta_html}'
469
  f'</div>'
470
  )
471
 
 
508
  f'<span style="display:inline-flex;align-items:center;justify-content:center;'
509
  f'width:22px;height:22px;border-radius:50%;flex-shrink:0;margin-top:1px;'
510
  f'background:{GRAY};color:#fff;font-size:0.7rem;font-weight:700;">{i}</span>'
511
+ f'<span style="font-size:{FS_BODY};line-height:1.55;color:{TEXT};">{item}</span>'
512
  f'</div>'
513
  )
514
  footer_html = ""
 
519
  )
520
  return (
521
  f'<div style="border:1px solid {BORDER};border-radius:12px;'
522
+ f'padding:4px 16px;background:{BG};max-width:680px;">'
523
  f'{rows_html}{footer_html}'
524
  f'</div>'
525
  )
 
550
  ) if metric else ""
551
 
552
  st.markdown(
553
+ f'<div class="primer-card" style="background:{BG};border:1px solid {fg}44;'
554
  f'border-left:4px solid {fg};border-radius:0 10px 10px 0;'
555
  f'padding:14px 16px;margin-bottom:8px;">'
556
  f'<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;flex-wrap:wrap;">'