akshay4 commited on
Commit
f665c1b
Β·
verified Β·
1 Parent(s): 57c73d8

Upload folder using huggingface_hub

Browse files
app.py CHANGED
@@ -829,9 +829,54 @@ body.wg-light-mode footer { display: none !important; }
829
  .wg-persona-label {
830
  font-style: italic; font-family: 'EB Garamond', serif;
831
  font-size: 0.78rem; color: var(--wg-yellow); letter-spacing: 0;
832
- font-weight: 400;
 
833
  }
834
  #wg-practice .wg-persona-label { color: #b45309 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
835
 
836
  .wg-another-take {
837
  float: right; cursor: pointer; font-family: 'EB Garamond', serif;
@@ -960,6 +1005,13 @@ body.wg-light-mode footer { display: none !important; }
960
  background: #f5f0e6; border-left: 3px solid #2d6a4f;
961
  padding: 0.75rem 1rem; border-radius: 0 10px 10px 0;
962
  }
 
 
 
 
 
 
 
963
 
964
  /* ── Arcade Character Selector ─────────────────────────────────────────────── */
965
  .wg-arcade {
@@ -1442,17 +1494,38 @@ window.wgOpenLatestTrace = function() {
1442
  }
1443
  try {
1444
  var payload = JSON.parse(raw);
 
1445
  var logs = Array.isArray(payload.logs) ? payload.logs : [];
1446
  var summary = [
1447
  ['route', payload.route || 'quick_wit'],
1448
  ['winner', payload.winning_persona || 'n/a'],
1449
- ['twist', payload.metadata && payload.metadata.twist_potential != null ? String(payload.metadata.twist_potential) : 'n/a'],
1450
  ['scenes', String((payload.retrieved_scenes || []).length)],
1451
  ['candidates', String((payload.candidates || []).length)]
1452
  ];
 
 
 
 
 
 
1453
  var summaryHtml = summary.map(function(item) {
1454
  return '<span class="wg-trace-pill"><strong>' + item[0] + '</strong> ' + item[1] + '</span>';
1455
  }).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1456
  var logHtml = logs.length ? logs.map(function(line) {
1457
  return '<div class="wg-trace-logline">' +
1458
  '<span class="wg-trace-step">' + (line.step || '') + '</span>' +
@@ -1466,9 +1539,10 @@ window.wgOpenLatestTrace = function() {
1466
  .replace(/>/g, '&gt;');
1467
  b.innerHTML =
1468
  '<div class="wg-trace-modal-head">' +
1469
- '<div><div class="wg-trace-modal-title">POST-MORTEM TRACE</div><div class="wg-trace-modal-sub">Pretty JSON + execution log for the latest turn.</div></div>' +
1470
  '</div>' +
1471
  '<div class="wg-trace-summary">' + summaryHtml + '</div>' +
 
1472
  '<div class="wg-trace-logbox"><div class="wg-trace-logtitle">EXECUTION LOG</div>' + logHtml + '</div>' +
1473
  '<div class="wg-trace-jsonbox"><div class="wg-trace-jsonhead">TRACE JSON</div><pre class="wg-trace-jsonpre">' + jsonPretty + '</pre></div>';
1474
  o.style.display = 'flex';
@@ -1520,6 +1594,10 @@ window.wgOpenScene = function(character, show, setup, response, why, avatarUrl,
1520
  + '<div class="wg-pop-bubble">' + response + '</div>'
1521
  + '</div></div>'
1522
  + '<div class="wg-pop-why">'
 
 
 
 
1523
  + '<div class="wg-pop-why-title">WHY IT WORKS</div>'
1524
  + '<div class="wg-pop-why-body">' + why + '</div>'
1525
  + '</div>';
@@ -2417,7 +2495,7 @@ def build_ui():
2417
  '<path d="M16.5 8.5l1.4-1.4 2.5 2.5-1.4 1.4M15.7 9.3l2.5 2.5M15.2 14.8l3.6-3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>'
2418
  '</svg>'
2419
  '</span>'
2420
- '<span class="wg-trace-launch-text">Open Trace</span>'
2421
  '</button>'
2422
  '</div>'
2423
  )
 
829
  .wg-persona-label {
830
  font-style: italic; font-family: 'EB Garamond', serif;
831
  font-size: 0.78rem; color: var(--wg-yellow); letter-spacing: 0;
832
+ font-weight: 400; background: transparent; border: none;
833
+ padding: 0; cursor: pointer;
834
  }
835
  #wg-practice .wg-persona-label { color: #b45309 !important; }
836
+ .wg-insight-strip {
837
+ margin-top: 0.85rem;
838
+ padding: 0.8rem 0.95rem;
839
+ border-radius: 14px;
840
+ background: linear-gradient(180deg, rgba(255,250,241,0.98), rgba(245,240,232,0.98));
841
+ border: 1px solid rgba(180,83,9,0.16);
842
+ }
843
+ .wg-insight-title {
844
+ font-family: 'Bebas Neue', sans-serif;
845
+ font-size: 0.9rem;
846
+ letter-spacing: 0.12em;
847
+ color: #92400e !important;
848
+ }
849
+ .wg-insight-sub {
850
+ margin-top: 0.2rem;
851
+ font-size: 0.88rem;
852
+ color: #6b6258 !important;
853
+ }
854
+ .wg-insight-buttons {
855
+ display: flex;
856
+ flex-wrap: wrap;
857
+ gap: 0.45rem;
858
+ margin-top: 0.65rem;
859
+ }
860
+ .wg-insight-btn {
861
+ -webkit-appearance: none !important;
862
+ appearance: none !important;
863
+ background: #fffaf1 !important;
864
+ border: 1px solid rgba(180,83,9,0.22) !important;
865
+ color: #9a3412 !important;
866
+ border-radius: 999px !important;
867
+ padding: 0.42rem 0.72rem !important;
868
+ font-family: 'Bebas Neue', sans-serif;
869
+ font-size: 0.8rem;
870
+ letter-spacing: 0.09em;
871
+ cursor: pointer;
872
+ line-height: 1;
873
+ box-shadow: none !important;
874
+ }
875
+ .wg-insight-btn:hover {
876
+ background: #ffffff !important;
877
+ border-color: rgba(180,83,9,0.42) !important;
878
+ transform: translateY(-1px);
879
+ }
880
 
881
  .wg-another-take {
882
  float: right; cursor: pointer; font-family: 'EB Garamond', serif;
 
1005
  background: #f5f0e6; border-left: 3px solid #2d6a4f;
1006
  padding: 0.75rem 1rem; border-radius: 0 10px 10px 0;
1007
  }
1008
+ .wg-pop-minihead {
1009
+ font-family: 'Bebas Neue', sans-serif;
1010
+ font-size: 0.8rem;
1011
+ letter-spacing: 0.12em;
1012
+ color: #2563eb !important;
1013
+ margin: 0.9rem 0 0.35rem;
1014
+ }
1015
 
1016
  /* ── Arcade Character Selector ─────────────────────────────────────────────── */
1017
  .wg-arcade {
 
1494
  }
1495
  try {
1496
  var payload = JSON.parse(raw);
1497
+ var meta = payload.metadata || {};
1498
  var logs = Array.isArray(payload.logs) ? payload.logs : [];
1499
  var summary = [
1500
  ['route', payload.route || 'quick_wit'],
1501
  ['winner', payload.winning_persona || 'n/a'],
1502
+ ['twist', meta.twist_potential != null ? String(meta.twist_potential) : 'n/a'],
1503
  ['scenes', String((payload.retrieved_scenes || []).length)],
1504
  ['candidates', String((payload.candidates || []).length)]
1505
  ];
1506
+ function fmt(value) {
1507
+ return String(value || '').replace(/_/g, ' ');
1508
+ }
1509
+ function title(value) {
1510
+ return fmt(value).toUpperCase();
1511
+ }
1512
  var summaryHtml = summary.map(function(item) {
1513
  return '<span class="wg-trace-pill"><strong>' + item[0] + '</strong> ' + item[1] + '</span>';
1514
  }).join('');
1515
+ var conceptHtml = '<div class="wg-pop-show">HOW WITGYM READ THIS MOMENT</div>'
1516
+ + '<div class="wg-pop-bio" style="font-style:normal">'
1517
+ + 'This is the teaching layer behind the joke: what kind of situation this is, what pressure drives it, which precedent scenes matched it, and which comic lens shaped the line.'
1518
+ + '</div>'
1519
+ + '<div class="wg-pop-minihead">SITUATION PATTERN</div>'
1520
+ + '<div class="wg-pop-bio" style="font-style:normal">' + title(meta.archetype || 'unknown') + '</div>'
1521
+ + '<div class="wg-pop-minihead">SOCIAL PRESSURE</div>'
1522
+ + '<div class="wg-pop-bio" style="font-style:normal">' + title(meta.tension_type || 'unknown') + '</div>'
1523
+ + '<div class="wg-pop-minihead">COMEDY COMPLEXITY</div>'
1524
+ + '<div class="wg-pop-bio" style="font-style:normal">' + (meta.twist_potential != null ? String(meta.twist_potential) + '/10' : 'n/a') + ' Β· Higher means more moving parts to play with.</div>'
1525
+ + '<div class="wg-pop-minihead">PERSONA LENS</div>'
1526
+ + '<div class="wg-pop-bio" style="font-style:normal">' + title(payload.winning_persona || 'n/a') + ' Β· The comic angle used for the final line.</div>'
1527
+ + '<div class="wg-pop-minihead">RETRIEVED CONTEXT</div>'
1528
+ + '<div class="wg-pop-bio" style="font-style:normal">' + String((payload.retrieved_scenes || []).length) + ' Office scene(s) matched this pattern. They are reference cases, not lines being copied.</div>';
1529
  var logHtml = logs.length ? logs.map(function(line) {
1530
  return '<div class="wg-trace-logline">' +
1531
  '<span class="wg-trace-step">' + (line.step || '') + '</span>' +
 
1539
  .replace(/>/g, '&gt;');
1540
  b.innerHTML =
1541
  '<div class="wg-trace-modal-head">' +
1542
+ '<div><div class="wg-trace-modal-title">HOW WITGYM BUILT THIS</div><div class="wg-trace-modal-sub">Student-friendly notes first, raw trace second.</div></div>' +
1543
  '</div>' +
1544
  '<div class="wg-trace-summary">' + summaryHtml + '</div>' +
1545
+ conceptHtml +
1546
  '<div class="wg-trace-logbox"><div class="wg-trace-logtitle">EXECUTION LOG</div>' + logHtml + '</div>' +
1547
  '<div class="wg-trace-jsonbox"><div class="wg-trace-jsonhead">TRACE JSON</div><pre class="wg-trace-jsonpre">' + jsonPretty + '</pre></div>';
1548
  o.style.display = 'flex';
 
1594
  + '<div class="wg-pop-bubble">' + response + '</div>'
1595
  + '</div></div>'
1596
  + '<div class="wg-pop-why">'
1597
+ + '<div class="wg-pop-why-title">WHY THIS SCENE WAS RETRIEVED</div>'
1598
+ + '<div class="wg-pop-why-body">This is a reference case with a similar comedy pattern, so the coach can borrow the mechanism without copying the line.</div>'
1599
+ + '</div>'
1600
+ + '<div class="wg-pop-why">'
1601
  + '<div class="wg-pop-why-title">WHY IT WORKS</div>'
1602
  + '<div class="wg-pop-why-body">' + why + '</div>'
1603
  + '</div>';
 
2495
  '<path d="M16.5 8.5l1.4-1.4 2.5 2.5-1.4 1.4M15.7 9.3l2.5 2.5M15.2 14.8l3.6-3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>'
2496
  '</svg>'
2497
  '</span>'
2498
+ '<span class="wg-trace-launch-text">Comedy Notes</span>'
2499
  '</button>'
2500
  '</div>'
2501
  )
notes/agent-memory/cases/2026-06-15-ui-browser-testing-normal-branch.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # UI browser testing in this repo should move from worktree to normal branch
2
+
3
+ ## Problem
4
+ - UI closed-loop testing with the in-app browser became unreliable while working from a Codex git worktree.
5
+ - The agent needed browser automation on localhost or browser-visible artifacts, but branch/worktree metadata and local runtime setup kept creating friction.
6
+
7
+ ## Why It Persisted
8
+ - The code changes themselves were frontend-only and small, but verification depended on multiple layers: git worktree state, local runtime availability, and browser URL-policy constraints.
9
+ - Detached HEAD / worktree-specific git metadata created extra branch-management friction right before UI verification.
10
+ - The browser surface blocked direct file:// navigation, which made static preview verification harder unless served over localhost.
11
+
12
+ ## Paths Tried
13
+ - Tried to continue from detached HEAD in the worktree, then corrected to a feature branch off origin/main.
14
+ - Tried to launch the full app with uv; hit cache permission issues and then a uv runtime panic unrelated to product code.
15
+ - Tried to open a static preview via file:// in the browser; browser security policy blocked that path.
16
+ - Switched to the safer localhost-preview approach.
17
+
18
+ ## Root Cause
19
+ - In this repo, UI verification from a Codex worktree can fail for operational reasons that are orthogonal to the UI change itself: shared git worktree metadata permissions, missing local dependency installation in the worktree, and browser policy restrictions on file:// URLs.
20
+
21
+ ## Pareto-Optimal Solution
22
+ - If UI testing starts fighting the agent while on a worktree, switch to a normal branch checkout for browser-based validation rather than burning time debugging worktree-specific friction.
23
+ - For static UI proof, prefer serving a preview over localhost instead of using file://, because the browser surface allows localhost but may block direct local-file navigation.
24
+
25
+ ## Evidence
26
+ - `git checkout -b ...` from the worktree required elevated access to shared worktree git metadata outside the sandbox.
27
+ - `uv run python app.py` failed first on cache permissions, then with a uv/system-configuration panic.
28
+ - `python3 app.py` failed because the worktree runtime lacked installed dependencies like `python-dotenv`.
29
+ - Browser automation rejected `file:///private/tmp/...` with an explicit URL-policy block.
30
+ - `python3 -m http.server ...` on localhost succeeded once started with elevated permission.
31
+
32
+ ## Future-Agent Rules
33
+ - If you are on a Codex worktree and UI testing starts failing for operational reasons, switch to a normal branch before spending more time on the worktree setup.
34
+ - For browser automation in this repo, prefer localhost-served previews over file:// artifacts.
35
+ - Distinguish environment/runtime failures from product regressions; do not misdiagnose launcher or policy problems as UI code bugs.
36
+
37
+ ## Follow-Ups
38
+ - A normal-branch verification path is preferred for future high-pressure UI iterations.
39
+ - If full app runtime is required, ensure the branch environment has the expected dependencies installed before starting the browser loop.
notes/agent-memory/durable_memory.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ## 2026-06-15 - UI Browser Testing On Worktrees
2
+ - Problem: browser-based UI validation in this repo became fragile from a Codex git worktree even when the UI change itself was small.
3
+ - Root cause: worktree git metadata, missing runtime dependencies in the worktree, and browser blocking of `file://` previews created verification friction unrelated to the code.
4
+ - Keep doing: if browser testing gets sticky, switch to a normal branch checkout and serve previews over localhost.
5
+ - Avoid: spending hackathon time debugging worktree-specific UI testing failures or relying on `file://` preview URLs.
witgym/debug_render.py CHANGED
@@ -52,22 +52,29 @@ def _fmt_chip(value: str) -> str:
52
  # Succinct definitions for each comedy structural property β€” shown on chip click
53
  _CHIP_DEFS: dict[str, tuple[str, str]] = {
54
  # Archetypes
55
- "status_assertion": ("STATUS ASSERTION", "Who's in charge here? Comedy exploits the gap between perceived and actual authority."),
56
- "self_delusion": ("SELF-DELUSION", "The speaker's internal narrative and external reality are on different planets. Exposure is inevitable."),
57
- "power_inversion": ("POWER INVERSION", "Expected hierarchy flips. The person who should win, loses β€” and the reversal is the joke."),
58
- "anxiety_escalation": ("ANXIETY ESCALATION", "A small worry snowballs into catastrophe in real time. Comedy lives in watching the spiral."),
59
- "social_fail": ("SOCIAL PERFORMANCE FAIL", "The attempt to appear normal backfires. The harder the try, the bigger the collapse."),
60
- "misplaced_conf": ("MISPLACED CONFIDENCE", "Maximum certainty, zero basis for it. The most dangerous form of comedy."),
61
  # Tensions
62
- "social_embarrass": ("SOCIAL EMBARRASSMENT", "The gap between how you want to appear and how you actually appear. Everyone sees it except you."),
63
- "existential": ("EXISTENTIAL ANXIETY", "The joke touches something deeper β€” identity, mortality, meaning. The laugh is a release valve."),
64
- "status_threat": ("STATUS THREAT", "Someone's rank or belonging is under attack. Comedy exploits the gap between deserved and claimed status."),
65
- "identity_expose": ("IDENTITY EXPOSURE", "A mask slips. What someone really is gets revealed against their will. Truth was always funnier."),
66
- "logic_collapse": ("LOGIC COLLAPSE", "A belief or argument implodes under its own internal contradictions. Comedy as structural failure."),
67
  # Distances
68
- "mild": ("MILD VIOLATION", "Gentle subversion. Safe for a work meeting. The laugh is a small, polite exhale."),
69
- "moderate": ("MODERATE VIOLATION", "Has an edge. Makes the room slightly uncomfortable in a productive way. The sweet spot."),
70
- "sharp": ("SHARP VIOLATION", "Cuts deep. The kind of line that makes people go quiet, then laugh harder than expected."),
 
 
 
 
 
 
 
71
  }
72
 
73
 
@@ -260,6 +267,46 @@ def _chip_onclick(value: str) -> str:
260
  return f"wgOpenChip({_jstr(title)},{_jstr(defn)})"
261
 
262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  def _meta_pass1_html(meta) -> str:
264
  arc_click = _chip_onclick(meta.archetype.value)
265
  ten_click = _chip_onclick(meta.tension_type.value)
@@ -534,7 +581,10 @@ def format_trace_html(result: WitGymResponse, user_input: str, show_debug: bool
534
 
535
  # Winning persona label β€” use pre-compression value stored in response
536
  winning_persona = result.winning_persona
537
- persona_label = f' Β· <em class="wg-persona-label">{_esc(winning_persona)}</em>' if winning_persona else ""
 
 
 
538
 
539
  # Another take button (only if alternatives exist)
540
  another_take_btn = (
@@ -571,6 +621,7 @@ def format_trace_html(result: WitGymResponse, user_input: str, show_debug: bool
571
  ]
572
  if result.explanation:
573
  parts.append(_explanation_panel_html(result.explanation))
 
574
 
575
  # ── Drill chips (coaching follow-ups) ──────────────────────────────────
576
  if is_last:
 
52
  # Succinct definitions for each comedy structural property β€” shown on chip click
53
  _CHIP_DEFS: dict[str, tuple[str, str]] = {
54
  # Archetypes
55
+ "status_assertion": ("ARCHETYPE Β· STATUS ASSERTION", "What it is: someone is trying to claim authority, status, or competence. Why it matters: the joke works by exposing the gap between the claim and the reality."),
56
+ "self_delusion": ("ARCHETYPE Β· SELF-DELUSION", "What it is: someone believes a flattering story about themselves that reality cannot support. Why it matters: comedy lands when the fantasy finally runs into the facts."),
57
+ "power_inversion": ("ARCHETYPE Β· POWER INVERSION", "What it is: the expected pecking order flips. Why it matters: surprise grows when the person who should control the room suddenly loses it."),
58
+ "anxiety_escalation": ("ARCHETYPE Β· ANXIETY ESCALATION", "What it is: a small worry spirals into a full disaster movie. Why it matters: the joke gets sharper as the reaction becomes bigger than the actual problem."),
59
+ "social_fail": ("ARCHETYPE Β· SOCIAL PERFORMANCE FAIL", "What it is: someone tries to look normal, smooth, or impressive and fumbles it. Why it matters: comedy comes from watching the performance collapse in public."),
60
+ "misplaced_conf": ("ARCHETYPE Β· MISPLACED CONFIDENCE", "What it is: someone sounds certain without having the facts. Why it matters: the joke exposes confidence that has outrun competence."),
61
  # Tensions
62
+ "social_embarrass": ("TENSION Β· SOCIAL EMBARRASSMENT", "What it is: the fear of looking foolish in front of other people. Why it matters: embarrassment raises the emotional stakes, so even a short line can hit hard."),
63
+ "existential": ("TENSION Β· EXISTENTIAL ANXIETY", "What it is: the moment quietly touches identity, meaning, or dread. Why it matters: deeper fear gives the joke weight instead of making it feel throwaway."),
64
+ "status_threat": ("TENSION Β· STATUS THREAT", "What it is: someone's rank, credibility, or belonging feels under pressure. Why it matters: status danger creates friction, and friction gives the line bite."),
65
+ "identity_expose": ("TENSION Β· IDENTITY EXPOSURE", "What it is: the person's mask is slipping. Why it matters: jokes get stronger when they reveal the truth the speaker was trying to hide."),
66
+ "logic_collapse": ("TENSION Β· LOGIC COLLAPSE", "What it is: the person's explanation breaks under its own rules. Why it matters: once the logic collapses, the line can simply point at the wreckage."),
67
  # Distances
68
+ "mild": ("SHARPNESS Β· MILD VIOLATION", "What it is: a safe, lightly subversive joke. Why it matters: this keeps the line playful instead of confrontational."),
69
+ "moderate": ("SHARPNESS Β· MODERATE VIOLATION", "What it is: a joke with some edge, but still recoverable. Why it matters: this is often the sweet spot for sounding bold without losing the room."),
70
+ "sharp": ("SHARPNESS Β· SHARP VIOLATION", "What it is: a line that cuts close to the bone. Why it matters: sharper jokes can win bigger laughs, but they also raise the risk of silence."),
71
+ }
72
+
73
+ _PERSONA_DEFS: dict[str, tuple[str, str]] = {
74
+ "cynic": ("PERSONA Β· CYNIC", "This lens says the quiet part out loud. It spots the real motive, the hidden cost, or the ugly truth underneath the situation."),
75
+ "conviction": ("PERSONA Β· CONVICTION", "This lens commits completely to a wrong belief. It is funny because the certainty exposes something true about the speaker."),
76
+ "absurdist": ("PERSONA Β· ABSURDIST", "This lens follows the situation's logic farther than a normal person would. It makes the joke by treating the spiral as perfectly reasonable."),
77
+ "bisociate": ("PERSONA Β· BISOCIATE", "This lens jumps to a different but structurally matching world. It works when the same comic pattern suddenly shows up somewhere unexpected."),
78
  }
79
 
80
 
 
267
  return f"wgOpenChip({_jstr(title)},{_jstr(defn)})"
268
 
269
 
270
+ def _persona_onclick(value: str) -> str:
271
+ title, defn = _PERSONA_DEFS.get(value, (f"PERSONA Β· {_fmt_chip(value)}", "This is the comic lens the coach used to write the line."))
272
+ return f"wgOpenChip({_jstr(title)},{_jstr(defn)})"
273
+
274
+
275
+ def _insight_button(label: str, onclick: str) -> str:
276
+ return (
277
+ f'<button class="wg-insight-btn" type="button" onclick="{_esc(onclick)}">'
278
+ f'{_esc(label)}'
279
+ '</button>'
280
+ )
281
+
282
+
283
+ def _coach_notes_html(result: WitGymResponse) -> str:
284
+ meta = result.metadata
285
+ buttons = [
286
+ _insight_button("situation pattern", _chip_onclick(meta.archetype.value)),
287
+ _insight_button("social pressure", _chip_onclick(meta.tension_type.value)),
288
+ _insight_button(
289
+ f"complexity {meta.twist_potential}/10",
290
+ f"wgOpenChip({_jstr('COMEDY COMPLEXITY')},{_jstr('What it is: how many moving parts this moment has. Why it matters: lower scores usually want one clean observation, while higher scores can support a twistier line.')})",
291
+ ),
292
+ _insight_button(
293
+ f"retrieved context Β· {len(result.retrieved_scenes)}",
294
+ f"wgOpenChip({_jstr('RETRIEVED CONTEXT')},{_jstr('These are Office scenes with a similar comedy pattern. They are reference cases for how the joke works, not lines the coach is copying.')})",
295
+ ),
296
+ ]
297
+ if result.winning_persona:
298
+ buttons.append(_insight_button("persona lens", _persona_onclick(result.winning_persona)))
299
+ return (
300
+ '<div class="wg-insight-strip">'
301
+ '<div class="wg-insight-title">How WitGym built this line</div>'
302
+ '<div class="wg-insight-sub">Tap any note for a quick explanation.</div>'
303
+ '<div class="wg-insight-buttons">'
304
+ + "".join(buttons) +
305
+ '</div>'
306
+ '</div>'
307
+ )
308
+
309
+
310
  def _meta_pass1_html(meta) -> str:
311
  arc_click = _chip_onclick(meta.archetype.value)
312
  ten_click = _chip_onclick(meta.tension_type.value)
 
581
 
582
  # Winning persona label β€” use pre-compression value stored in response
583
  winning_persona = result.winning_persona
584
+ persona_label = (
585
+ f' Β· <button class="wg-persona-label" type="button" onclick="{_esc(_persona_onclick(winning_persona))}">{_esc(winning_persona)}</button>'
586
+ if winning_persona else ""
587
+ )
588
 
589
  # Another take button (only if alternatives exist)
590
  another_take_btn = (
 
621
  ]
622
  if result.explanation:
623
  parts.append(_explanation_panel_html(result.explanation))
624
+ parts.append(_coach_notes_html(result))
625
 
626
  # ── Drill chips (coaching follow-ups) ──────────────────────────────────
627
  if is_last: