Eric Xu commited on
Commit
e56613d
·
unverified ·
1 Parent(s): ff413e9

UX copy overhaul: intuitive labels, consistent terminology, plain language

Browse files

Based on Codex UX review (gpt-5.3-codex, xhigh):

Naming:
- "Entity" → "What should the panel review?"
- "Directions for your goal" → "Test what to change next"
- "Bias Audit" → "Check panel realism"
- "Suggest" → "Auto-fill"
- "Evaluate" → "Build panel and review"

Terminology (standardized on "panel"):
- "evaluators/personas/audience" → "panel members" throughout
- "positive/neutral/negative" → "would say yes/unsure/would say no"

Academic language removed:
- "persuadable middle" → "on the fence"
- "Framing effect" → "Does wording change the score?"
- "Authority bias" → "Do trust signals change the score?"
- "Probes" → "Checks"

Progress messages rewritten around user progress, not system actions.
Error messages now tell users what to do next.

Files changed (1) hide show
  1. web/static/index.html +58 -59
web/static/index.html CHANGED
@@ -306,7 +306,7 @@
306
  <div class="container">
307
  <header>
308
  <h1>Semantic Gradient Optimization</h1>
309
- <p>Evaluate anything against a synthetic panel. Find what to change first.</p>
310
  <div id="configBadge" class="config-badge">checking...</div>
311
  <div id="nemotronBadge" class="config-badge" style="margin-left:8px">checking...</div>
312
  </header>
@@ -315,15 +315,15 @@
315
  <div class="step hidden" id="nemotronSetup" style="border-color:var(--yellow)">
316
  <div class="step-header">
317
  <div class="step-num" style="border-color:var(--yellow);color:var(--yellow)">!</div>
318
- <div class="step-title">Persona dataset not found</div>
319
  </div>
320
- <p class="step-desc">SGO uses 1M census-grounded personas for realistic evaluations. Provide a path to the dataset or download it (~2GB).</p>
321
  <div class="field">
322
- <label>Dataset path</label>
323
- <input type="text" id="nemotronPath" placeholder="">
324
  </div>
325
  <div class="btn-row">
326
- <button onclick="setupNemotron()">Load or download</button>
327
  </div>
328
  <div id="nemotronStatus" class="hidden mt-16">
329
  <div class="progress-text" id="nemotronStatusText"></div>
@@ -334,9 +334,9 @@
334
  <div class="step active" id="step1">
335
  <div class="step-header">
336
  <div class="step-num">1</div>
337
- <div class="step-title">What are you optimizing?</div>
338
  </div>
339
- <p class="step-desc">Paste whatever you want evaluated landing page, resume, pitch, policy, profile, anything.</p>
340
 
341
  <div class="template-chips">
342
  <span class="template-chip" onclick="loadTemplate('product')">Product</span>
@@ -345,42 +345,42 @@
345
  </div>
346
 
347
  <div class="field">
348
- <label>Entity what are you putting out there?</label>
349
- <textarea id="entityText" placeholder="Paste your landing page, resume, pitch, profile, policy..."></textarea>
350
  </div>
351
 
352
  <div class="field">
353
  <div style="display:flex;justify-content:space-between;align-items:center">
354
- <label>Goal — what outcome do you want?</label>
355
- <button class="secondary" style="padding:4px 12px;font-size:0.75rem" onclick="inferSpec()">Suggest</button>
356
  </div>
357
  <input type="text" id="goalText" placeholder="e.g. 'Increase conversion rate' or 'Get more interview callbacks'">
358
  </div>
359
 
360
  <div class="field">
361
  <div style="display:flex;justify-content:space-between;align-items:center">
362
- <label>Audience who are you trying to reach?</label>
363
- <button class="secondary" style="padding:4px 12px;font-size:0.75rem" onclick="inferSpec()">Suggest</button>
364
  </div>
365
  <input type="text" id="cohortDesc" placeholder="e.g. 'Engineering managers at mid-stage startups' or 'US consumers aged 25-45'">
366
  </div>
367
 
368
  <details class="mb-8">
369
- <summary style="cursor:pointer;color:var(--text2);font-size:0.85rem">Advanced options</summary>
370
  <div style="padding:12px 0">
371
  <div class="field">
372
- <label>Panel size</label>
373
  <input type="number" id="panelSize" value="30" min="5" max="80"
374
  style="width:80px;padding:6px;text-align:center">
375
  </div>
376
  <label style="display:inline-flex;align-items:center;gap:8px;margin:0;font-size:0.85rem;cursor:pointer">
377
  <input type="checkbox" id="biasCalibration" checked style="width:auto;margin:0">
378
- More realistic evaluations
379
  </label>
380
  </div>
381
  </details>
382
 
383
- <button onclick="runFullPipeline()" id="goBtn">Evaluate</button>
384
 
385
  <div id="pipelineProgress" class="hidden mt-16">
386
  <div class="progress-text" id="pipelineProgressText">Starting...</div>
@@ -391,17 +391,17 @@
391
  <div id="evalResults" class="hidden mt-16">
392
  <div class="score-big" id="avgScore">0<span>/10</span></div>
393
  <div class="stats-row">
394
- <div class="stat pos"><div class="stat-val" id="posCount">0</div><div class="stat-label">positive</div></div>
395
- <div class="stat neu"><div class="stat-val" id="neuCount">0</div><div class="stat-label">neutral</div></div>
396
- <div class="stat neg"><div class="stat-val" id="negCount">0</div><div class="stat-label">negative</div></div>
397
  </div>
398
  <details>
399
  <summary style="cursor:pointer;color:var(--text2);font-size:0.9rem">Full analysis</summary>
400
  <div class="results-details" id="evalAnalysis"></div>
401
  </details>
402
  <div class="btn-row mt-16">
403
- <button onclick="runDirections()">Directions for your goal</button>
404
- <button class="secondary" onclick="goToStep(3)">Run bias audit</button>
405
  </div>
406
  </div>
407
  </div>
@@ -410,9 +410,9 @@
410
  <div class="step hidden" id="step2">
411
  <div class="step-header">
412
  <div class="step-num">2</div>
413
- <div class="step-title">Directions for your goal</div>
414
  </div>
415
- <p class="step-desc">Testing changes against the persuadable middle — the people who are <em>almost</em> convinced.</p>
416
 
417
  <div id="cfProgress" class="mt-16">
418
  <div class="progress-text" id="cfProgressText">Analyzing concerns...</div>
@@ -437,30 +437,30 @@
437
  <div class="step hidden" id="step3">
438
  <div class="step-header">
439
  <div class="step-num">3</div>
440
- <div class="step-title">Bias Audit</div>
441
  </div>
442
  <p class="step-desc">
443
- Measure cognitive biases in your evaluator panel using validated social science experiments
444
- (<a href="https://arxiv.org/abs/2509.13588" target="_blank" style="color:var(--accent2)">CoBRA, CHI'26</a>).
445
  </p>
446
 
447
  <div class="field">
448
- <label>Probes to run</label>
449
  <div style="display:flex;gap:16px;margin-bottom:12px">
450
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
451
- <input type="checkbox" id="probeFraming" checked> Framing effect
452
  </label>
453
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
454
- <input type="checkbox" id="probeAuthority" checked> Authority bias
455
  </label>
456
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
457
- <input type="checkbox" id="probeOrder" checked> Order effect
458
  </label>
459
  </div>
460
  </div>
461
 
462
  <div class="btn-row">
463
- <button onclick="runBiasAudit()" id="auditBtn">Run audit</button>
464
  <div style="flex:1"></div>
465
  <label style="display:flex;align-items:center;gap:6px;margin:0">
466
  <span style="font-size:0.8rem;color:var(--text2)">Sample size:</span>
@@ -475,10 +475,10 @@
475
  </div>
476
 
477
  <div id="auditResults" class="hidden mt-16">
478
- <h3 style="margin-bottom:12px">Bias Audit Results</h3>
479
  <table class="gradient-table" id="auditTable">
480
  <thead>
481
- <tr><th>Probe</th><th>Shifted %</th><th>Avg |Delta|</th><th>Human Baseline</th><th>Assessment</th></tr>
482
  </thead>
483
  <tbody></tbody>
484
  </table>
@@ -487,8 +487,8 @@
487
  <div class="results-details" id="auditReport"></div>
488
  </details>
489
  <div class="btn-row mt-16">
490
- <button class="secondary" onclick="rerunWithCalibration()">Re-evaluate with bias calibration</button>
491
- <button class="secondary" onclick="goToStep(2)">Continue to gradient</button>
492
  </div>
493
  </div>
494
  </div>
@@ -601,7 +601,7 @@ async function init() {
601
 
602
  async function setupNemotron() {
603
  const path = document.getElementById('nemotronPath').value.trim();
604
- if (!path) return alert('Please enter a path.');
605
 
606
  const status = document.getElementById('nemotronStatus');
607
  const text = document.getElementById('nemotronStatusText');
@@ -695,7 +695,7 @@ async function inferSpec() {
695
 
696
  async function runFullPipeline() {
697
  const text = document.getElementById('entityText').value.trim();
698
- if (!text) return alert('Please enter your entity text.');
699
 
700
  const btn = document.getElementById('goBtn');
701
  btn.disabled = true;
@@ -710,8 +710,8 @@ async function runFullPipeline() {
710
  const goalField = document.getElementById('goalText');
711
  const audienceField = document.getElementById('cohortDesc');
712
  if (!goalField.value.trim() || !audienceField.value.trim()) {
713
- document.getElementById('pipelineProgressText').textContent = 'Inferring goal and audience from entity...';
714
- logStep('Inferring goal and audience from entity...');
715
  await inferSpec();
716
  if (goalField.value) logStep(`Goal: ${goalField.value}`, 'pos');
717
  if (audienceField.value) logStep(`Audience: ${audienceField.value}`, 'pos');
@@ -723,8 +723,7 @@ async function runFullPipeline() {
723
 
724
  try {
725
  // Phase 1: Create session
726
- document.getElementById('pipelineProgressText').textContent = 'Creating session...';
727
- logStep('Creating session...');
728
  const sessResp = await fetch('/api/session', {
729
  method: 'POST',
730
  headers: {'Content-Type': 'application/json'},
@@ -732,12 +731,12 @@ async function runFullPipeline() {
732
  });
733
  const sessData = await sessResp.json();
734
  sessionId = sessData.session_id;
735
- logStep(`Session: ${sessionId}`, 'pos');
736
 
737
  // Phase 2: Suggest segments
738
- document.getElementById('pipelineProgressText').textContent = 'Analyzing entity and choosing evaluator segments...';
739
  document.getElementById('pipelineProgressBar').style.width = '10%';
740
- logStep('Analyzing entity to find the right evaluator segments...');
741
 
742
  const segResp = await fetch('/api/suggest-segments', {
743
  method: 'POST',
@@ -755,13 +754,13 @@ async function runFullPipeline() {
755
  const scale = panelSize / (totalSuggested || 1);
756
  segments.forEach(s => { s.count = Math.max(2, Math.round((s.count || 8) * scale)); });
757
 
758
- segments.forEach(s => logStep(` Segment: ${s.label} (${s.count} personas)`));
759
- logStep(`${segments.length} segments identified`, 'pos');
760
  document.getElementById('pipelineProgressBar').style.width = '20%';
761
 
762
  // Phase 3: Generate cohort
763
- document.getElementById('pipelineProgressText').textContent = 'Generating evaluator personas...';
764
- logStep('Generating realistic personas for each segment...');
765
 
766
  const desc = audienceCtx || `People evaluating: ${text.substring(0, 200)}`;
767
  const cohortResp = await fetch('/api/cohort/generate', {
@@ -789,7 +788,7 @@ async function runFullPipeline() {
789
  if (f.occupation) parts.push(f.occupation);
790
  logStep(`Filtered: ${parts.join(', ')}`, 'pos');
791
  }
792
- logStep(`${cohortData.cohort_size} evaluators ready — ${src}`, 'pos');
793
  if (panelSize < 40) {
794
  logStep(`Tip: a panel of 40-60 gives more reliable results across segments`, 'neu');
795
  }
@@ -797,7 +796,7 @@ async function runFullPipeline() {
797
 
798
  // Phase 4: Evaluate via SSE
799
  document.getElementById('pipelineProgressText').textContent = 'Evaluating...';
800
- logStep('Starting evaluation — each evaluator scores your entity...');
801
 
802
  await new Promise((resolve, reject) => {
803
  const params = new URLSearchParams({parallel: 5, bias_calibration: biasCal});
@@ -841,7 +840,7 @@ async function runFullPipeline() {
841
  resolve();
842
  });
843
 
844
- es.onerror = () => { es.close(); reject(new Error('Connection lost')); };
845
  });
846
 
847
  } catch (e) {
@@ -875,7 +874,7 @@ async function runDirections() {
875
 
876
  try {
877
  // Phase 1: Extract concerns from persuadable middle
878
- document.getElementById('cfProgressText').textContent = 'Analyzing concerns from persuadable middle...';
879
  const persuadable = evalResultsData.filter(r => r && r.score >= 4 && r.score <= 7);
880
  const concerns = [];
881
  persuadable.forEach(r => {
@@ -888,11 +887,11 @@ async function runDirections() {
888
  concerns.slice(0, 8).forEach(c => {
889
  log.innerHTML += `<div style="color:var(--text2)">Concern: ${esc(c)}</div>`;
890
  });
891
- log.innerHTML += `<div>${concerns.length} unique concerns from ${persuadable.length} persuadable evaluators</div>`;
892
  document.getElementById('cfProgressBar').style.width = '15%';
893
 
894
  // Phase 2: LLM generates candidate changes from concerns
895
- document.getElementById('cfProgressText').textContent = 'Generating candidate changes from concerns...';
896
  const suggestResp = await fetch('/api/suggest-changes', {
897
  method: 'POST',
898
  headers: {'Content-Type': 'application/json'},
@@ -974,7 +973,7 @@ async function runDirections() {
974
  resolve();
975
  });
976
 
977
- es.onerror = () => { es.close(); reject(new Error('Connection lost')); };
978
  });
979
 
980
  } catch (e) {
@@ -1086,7 +1085,7 @@ function renderGradientTable(results, changes, ranked) {
1086
  // ── Step 5: Bias Audit ──
1087
 
1088
  function runBiasAudit() {
1089
- if (!sessionId) return alert('No session.');
1090
  const probes = [];
1091
  if (document.getElementById('probeFraming').checked) probes.push('framing');
1092
  if (document.getElementById('probeAuthority').checked) probes.push('authority');
@@ -1179,7 +1178,7 @@ function runBiasAudit() {
1179
 
1180
  es.onerror = () => {
1181
  es.close();
1182
- document.getElementById('auditProgressText').textContent = 'Connection lost';
1183
  btn.disabled = false;
1184
  };
1185
  }
 
306
  <div class="container">
307
  <header>
308
  <h1>Semantic Gradient Optimization</h1>
309
+ <p>Build a panel. See what they think. Test what to change next.</p>
310
  <div id="configBadge" class="config-badge">checking...</div>
311
  <div id="nemotronBadge" class="config-badge" style="margin-left:8px">checking...</div>
312
  </header>
 
315
  <div class="step hidden" id="nemotronSetup" style="border-color:var(--yellow)">
316
  <div class="step-header">
317
  <div class="step-num" style="border-color:var(--yellow);color:var(--yellow)">!</div>
318
+ <div class="step-title">Panel data not found</div>
319
  </div>
320
+ <p class="step-desc">SGO needs its persona dataset to build realistic panels. Choose where to load it from, or download it now (~2 GB).</p>
321
  <div class="field">
322
+ <label>Folder for panel data</label>
323
+ <input type="text" id="nemotronPath" placeholder="e.g. data/nemotron">
324
  </div>
325
  <div class="btn-row">
326
+ <button onclick="setupNemotron()">Use this folder</button>
327
  </div>
328
  <div id="nemotronStatus" class="hidden mt-16">
329
  <div class="progress-text" id="nemotronStatusText"></div>
 
334
  <div class="step active" id="step1">
335
  <div class="step-header">
336
  <div class="step-num">1</div>
337
+ <div class="step-title">Build your panel review</div>
338
  </div>
339
+ <p class="step-desc">Paste what you want reviewed, set your goal and audience, and let the panel react.</p>
340
 
341
  <div class="template-chips">
342
  <span class="template-chip" onclick="loadTemplate('product')">Product</span>
 
345
  </div>
346
 
347
  <div class="field">
348
+ <label>What should the panel review?</label>
349
+ <textarea id="entityText" placeholder="Paste the text someone would actually see: landing page, resume, pitch, profile, policy..."></textarea>
350
  </div>
351
 
352
  <div class="field">
353
  <div style="display:flex;justify-content:space-between;align-items:center">
354
+ <label>What outcome are you optimizing for?</label>
355
+ <button class="secondary" style="padding:4px 12px;font-size:0.75rem" onclick="inferSpec()">Auto-fill</button>
356
  </div>
357
  <input type="text" id="goalText" placeholder="e.g. 'Increase conversion rate' or 'Get more interview callbacks'">
358
  </div>
359
 
360
  <div class="field">
361
  <div style="display:flex;justify-content:space-between;align-items:center">
362
+ <label>Who should be on the panel?</label>
363
+ <button class="secondary" style="padding:4px 12px;font-size:0.75rem" onclick="inferSpec()">Auto-fill</button>
364
  </div>
365
  <input type="text" id="cohortDesc" placeholder="e.g. 'Engineering managers at mid-stage startups' or 'US consumers aged 25-45'">
366
  </div>
367
 
368
  <details class="mb-8">
369
+ <summary style="cursor:pointer;color:var(--text2);font-size:0.85rem">Panel settings</summary>
370
  <div style="padding:12px 0">
371
  <div class="field">
372
+ <label>Panel size (more people = steadier results)</label>
373
  <input type="number" id="panelSize" value="30" min="5" max="80"
374
  style="width:80px;padding:6px;text-align:center">
375
  </div>
376
  <label style="display:inline-flex;align-items:center;gap:8px;margin:0;font-size:0.85rem;cursor:pointer">
377
  <input type="checkbox" id="biasCalibration" checked style="width:auto;margin:0">
378
+ Reduce framing and authority effects
379
  </label>
380
  </div>
381
  </details>
382
 
383
+ <button onclick="runFullPipeline()" id="goBtn">Build panel and review</button>
384
 
385
  <div id="pipelineProgress" class="hidden mt-16">
386
  <div class="progress-text" id="pipelineProgressText">Starting...</div>
 
391
  <div id="evalResults" class="hidden mt-16">
392
  <div class="score-big" id="avgScore">0<span>/10</span></div>
393
  <div class="stats-row">
394
+ <div class="stat pos"><div class="stat-val" id="posCount">0</div><div class="stat-label">would say yes</div></div>
395
+ <div class="stat neu"><div class="stat-val" id="neuCount">0</div><div class="stat-label">unsure</div></div>
396
+ <div class="stat neg"><div class="stat-val" id="negCount">0</div><div class="stat-label">would say no</div></div>
397
  </div>
398
  <details>
399
  <summary style="cursor:pointer;color:var(--text2);font-size:0.9rem">Full analysis</summary>
400
  <div class="results-details" id="evalAnalysis"></div>
401
  </details>
402
  <div class="btn-row mt-16">
403
+ <button onclick="runDirections()">Test what to change next</button>
404
+ <button class="secondary" onclick="goToStep(3)">Check panel realism</button>
405
  </div>
406
  </div>
407
  </div>
 
410
  <div class="step hidden" id="step2">
411
  <div class="step-header">
412
  <div class="step-num">2</div>
413
+ <div class="step-title">Test what to change next</div>
414
  </div>
415
+ <p class="step-desc">Testing changes with people who are <em>on the fence</em> — the ones who could go either way.</p>
416
 
417
  <div id="cfProgress" class="mt-16">
418
  <div class="progress-text" id="cfProgressText">Analyzing concerns...</div>
 
437
  <div class="step hidden" id="step3">
438
  <div class="step-header">
439
  <div class="step-num">3</div>
440
+ <div class="step-title">Check panel realism</div>
441
  </div>
442
  <p class="step-desc">
443
+ See whether your panel reacts more like real people or more like an LLM.
444
+ Based on <a href="https://arxiv.org/abs/2509.13588" target="_blank" style="color:var(--accent2)">CoBRA (CHI'26)</a>.
445
  </p>
446
 
447
  <div class="field">
448
+ <label>Checks to run</label>
449
  <div style="display:flex;gap:16px;margin-bottom:12px">
450
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
451
+ <input type="checkbox" id="probeFraming" checked> Does wording change the score?
452
  </label>
453
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
454
+ <input type="checkbox" id="probeAuthority" checked> Do trust signals change the score?
455
  </label>
456
  <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
457
+ <input type="checkbox" id="probeOrder" checked> Does section order change the score?
458
  </label>
459
  </div>
460
  </div>
461
 
462
  <div class="btn-row">
463
+ <button onclick="runBiasAudit()" id="auditBtn">Run checks</button>
464
  <div style="flex:1"></div>
465
  <label style="display:flex;align-items:center;gap:6px;margin:0">
466
  <span style="font-size:0.8rem;color:var(--text2)">Sample size:</span>
 
475
  </div>
476
 
477
  <div id="auditResults" class="hidden mt-16">
478
+ <h3 style="margin-bottom:12px">Panel Realism Check</h3>
479
  <table class="gradient-table" id="auditTable">
480
  <thead>
481
+ <tr><th>Check</th><th>Shifted %</th><th>Avg score change</th><th>Human baseline</th><th>Assessment</th></tr>
482
  </thead>
483
  <tbody></tbody>
484
  </table>
 
487
  <div class="results-details" id="auditReport"></div>
488
  </details>
489
  <div class="btn-row mt-16">
490
+ <button class="secondary" onclick="rerunWithCalibration()">Run again with realism tuning</button>
491
+ <button class="secondary" onclick="goToStep(2)">Test what to change next</button>
492
  </div>
493
  </div>
494
  </div>
 
601
 
602
  async function setupNemotron() {
603
  const path = document.getElementById('nemotronPath').value.trim();
604
+ if (!path) return alert('Enter a folder path for the panel data.');
605
 
606
  const status = document.getElementById('nemotronStatus');
607
  const text = document.getElementById('nemotronStatusText');
 
695
 
696
  async function runFullPipeline() {
697
  const text = document.getElementById('entityText').value.trim();
698
+ if (!text) return alert('Paste something for the panel to review first.');
699
 
700
  const btn = document.getElementById('goBtn');
701
  btn.disabled = true;
 
710
  const goalField = document.getElementById('goalText');
711
  const audienceField = document.getElementById('cohortDesc');
712
  if (!goalField.value.trim() || !audienceField.value.trim()) {
713
+ document.getElementById('pipelineProgressText').textContent = 'Understanding your goal and audience...';
714
+ logStep('Understanding what you want to optimize and who should judge it...');
715
  await inferSpec();
716
  if (goalField.value) logStep(`Goal: ${goalField.value}`, 'pos');
717
  if (audienceField.value) logStep(`Audience: ${audienceField.value}`, 'pos');
 
723
 
724
  try {
725
  // Phase 1: Create session
726
+ document.getElementById('pipelineProgressText').textContent = 'Setting up...';
 
727
  const sessResp = await fetch('/api/session', {
728
  method: 'POST',
729
  headers: {'Content-Type': 'application/json'},
 
731
  });
732
  const sessData = await sessResp.json();
733
  sessionId = sessData.session_id;
734
+ // session created silently
735
 
736
  // Phase 2: Suggest segments
737
+ document.getElementById('pipelineProgressText').textContent = 'Choosing panel segments...';
738
  document.getElementById('pipelineProgressBar').style.width = '10%';
739
+ logStep('Choosing the right panel segments...');
740
 
741
  const segResp = await fetch('/api/suggest-segments', {
742
  method: 'POST',
 
754
  const scale = panelSize / (totalSuggested || 1);
755
  segments.forEach(s => { s.count = Math.max(2, Math.round((s.count || 8) * scale)); });
756
 
757
+ segments.forEach(s => logStep(` Added segment: ${s.label} (${s.count} people)`));
758
+ logStep(`${segments.length} segments chosen`, 'pos');
759
  document.getElementById('pipelineProgressBar').style.width = '20%';
760
 
761
  // Phase 3: Generate cohort
762
+ document.getElementById('pipelineProgressText').textContent = 'Building your panel...';
763
+ logStep('Building panel members for each segment...');
764
 
765
  const desc = audienceCtx || `People evaluating: ${text.substring(0, 200)}`;
766
  const cohortResp = await fetch('/api/cohort/generate', {
 
788
  if (f.occupation) parts.push(f.occupation);
789
  logStep(`Filtered: ${parts.join(', ')}`, 'pos');
790
  }
791
+ logStep(`${cohortData.cohort_size} panel members ready — ${src}`, 'pos');
792
  if (panelSize < 40) {
793
  logStep(`Tip: a panel of 40-60 gives more reliable results across segments`, 'neu');
794
  }
 
796
 
797
  // Phase 4: Evaluate via SSE
798
  document.getElementById('pipelineProgressText').textContent = 'Evaluating...';
799
+ logStep('Running the panel review — each member scores what you wrote...');
800
 
801
  await new Promise((resolve, reject) => {
802
  const params = new URLSearchParams({parallel: 5, bias_calibration: biasCal});
 
840
  resolve();
841
  });
842
 
843
+ es.onerror = () => { es.close(); reject(new Error('Connection lost — you can try again without losing your draft')); };
844
  });
845
 
846
  } catch (e) {
 
874
 
875
  try {
876
  // Phase 1: Extract concerns from persuadable middle
877
+ document.getElementById('cfProgressText').textContent = 'Looking at what\u2019s holding back the on-the-fence group...';
878
  const persuadable = evalResultsData.filter(r => r && r.score >= 4 && r.score <= 7);
879
  const concerns = [];
880
  persuadable.forEach(r => {
 
887
  concerns.slice(0, 8).forEach(c => {
888
  log.innerHTML += `<div style="color:var(--text2)">Concern: ${esc(c)}</div>`;
889
  });
890
+ log.innerHTML += `<div>${concerns.length} concerns from ${persuadable.length} on-the-fence panel members</div>`;
891
  document.getElementById('cfProgressBar').style.width = '15%';
892
 
893
  // Phase 2: LLM generates candidate changes from concerns
894
+ document.getElementById('cfProgressText').textContent = 'Proposing changes to test...';
895
  const suggestResp = await fetch('/api/suggest-changes', {
896
  method: 'POST',
897
  headers: {'Content-Type': 'application/json'},
 
973
  resolve();
974
  });
975
 
976
+ es.onerror = () => { es.close(); reject(new Error('Connection lost — you can try again without losing your draft')); };
977
  });
978
 
979
  } catch (e) {
 
1085
  // ── Step 5: Bias Audit ──
1086
 
1087
  function runBiasAudit() {
1088
+ if (!sessionId) return alert('Start with Step 1 to create a panel review.');
1089
  const probes = [];
1090
  if (document.getElementById('probeFraming').checked) probes.push('framing');
1091
  if (document.getElementById('probeAuthority').checked) probes.push('authority');
 
1178
 
1179
  es.onerror = () => {
1180
  es.close();
1181
+ document.getElementById('auditProgressText').textContent = 'Connection lost — you can try again without losing your draft';
1182
  btn.disabled = false;
1183
  };
1184
  }