Eric Xu commited on
Commit
92e2b88
·
unverified ·
1 Parent(s): 0694cd1

Simplify web UX: one-click evaluate, no segment configuration

Browse files

Users paste their entity and click Evaluate. The system automatically:
1. Analyzes the entity to determine appropriate evaluator segments
2. Generates personas for each segment
3. Runs evaluation with live progress

All intermediate steps (segment selection, cohort generation, persona
details) are shown in the progress log so users see what's happening
under the hood, but don't need to configure anything.

Segment config, panel size, bias calibration, and audience context
are available under Advanced Options for power users.

Files changed (1) hide show
  1. web/static/index.html +152 -235
web/static/index.html CHANGED
@@ -310,13 +310,13 @@
310
  <div id="configBadge" class="config-badge">checking...</div>
311
  </header>
312
 
313
- <!-- STEP 1: Entity -->
314
  <div class="step active" id="step1">
315
  <div class="step-header">
316
  <div class="step-num">1</div>
317
- <div class="step-title">Describe what you're optimizing</div>
318
  </div>
319
- <p class="step-desc">Paste your landing page copy, resume, pitch deck text, policy document, or any other entity.</p>
320
 
321
  <div class="template-chips">
322
  <span class="template-chip" onclick="loadTemplate('product')">Product</span>
@@ -325,71 +325,33 @@
325
  </div>
326
 
327
  <div class="field">
328
- <textarea id="entityText" placeholder="Paste your entity here... (landing page, resume, pitch, policy, etc.)"></textarea>
329
- </div>
330
- <button onclick="saveEntity()">Continue</button>
331
- </div>
332
-
333
- <!-- STEP 2: Cohort -->
334
- <div class="step hidden" id="step2">
335
- <div class="step-header">
336
- <div class="step-num">2</div>
337
- <div class="step-title">Build your evaluator panel</div>
338
- </div>
339
- <p class="step-desc">Define audience segments. The LLM generates realistic personas for each.</p>
340
-
341
- <div class="field">
342
- <label>Audience context</label>
343
- <input type="text" id="cohortDesc" placeholder="e.g. 'Potential customers for a B2B SaaS analytics tool'">
344
- </div>
345
-
346
- <label>Segments</label>
347
- <div id="segmentsList" class="segments-list"></div>
348
- <div class="btn-row">
349
- <button class="secondary" onclick="addSegment()">+ Add segment</button>
350
- <button class="secondary" onclick="suggestSegments()">Re-suggest</button>
351
- <button onclick="generateCohort()" id="genCohortBtn">Generate cohort</button>
352
- </div>
353
-
354
- <div id="cohortProgress" class="hidden mt-16">
355
- <div class="progress-text" id="cohortProgressText">Generating personas...</div>
356
- <div class="progress-bar"><div class="progress-fill" id="cohortProgressBar"></div></div>
357
  </div>
358
 
359
- <div id="cohortResult" class="hidden mt-16">
360
- <div class="stat text-center">
361
- <div class="stat-val" id="cohortCount">0</div>
362
- <div class="stat-label">evaluators generated</div>
 
 
 
 
 
 
 
 
 
 
 
 
363
  </div>
364
- <button class="mt-12" onclick="goToStep(3)">Continue to evaluation</button>
365
- </div>
366
- </div>
367
 
368
- <!-- STEP 3: Evaluate -->
369
- <div class="step hidden" id="step3">
370
- <div class="step-header">
371
- <div class="step-num">3</div>
372
- <div class="step-title">Run evaluation</div>
373
- </div>
374
- <p class="step-desc">Each evaluator scores your entity 1-10 with reasoning.</p>
375
 
376
- <div class="btn-row">
377
- <button onclick="runEval()" id="evalBtn">Evaluate</button>
378
- <label style="display:flex;align-items:center;gap:6px;margin:0">
379
- <input type="checkbox" id="biasCalibration">
380
- <span style="font-size:0.8rem;color:var(--text2)">Bias calibration</span>
381
- </label>
382
- <div style="flex:1"></div>
383
- <label style="display:flex;align-items:center;gap:6px;margin:0">
384
- <span style="font-size:0.8rem;color:var(--text2)">Parallel:</span>
385
- <input type="number" id="evalParallel" value="5" min="1" max="20"
386
- style="width:60px;padding:6px;text-align:center">
387
- </label>
388
- </div>
389
-
390
- <div id="evalProgress" class="hidden mt-16">
391
- <div class="progress-text" id="evalProgressText">Evaluating...</div>
392
- <div class="progress-bar"><div class="progress-fill" id="evalProgressBar"></div></div>
393
  <div class="eval-log" id="evalLog"></div>
394
  </div>
395
 
@@ -405,16 +367,16 @@
405
  <div class="results-details" id="evalAnalysis"></div>
406
  </details>
407
  <div class="btn-row mt-16">
408
- <button onclick="goToStep(4)">Continue to gradient</button>
409
- <button class="secondary" onclick="goToStep(5)">Run bias audit</button>
410
  </div>
411
  </div>
412
  </div>
413
 
414
- <!-- STEP 4: Counterfactual -->
415
- <div class="step hidden" id="step4">
416
  <div class="step-header">
417
- <div class="step-num">4</div>
418
  <div class="step-title">Find the highest-impact changes</div>
419
  </div>
420
  <p class="step-desc">List hypothetical changes. Undecided evaluators (scores 4-7) re-evaluate each one.</p>
@@ -455,10 +417,10 @@
455
  </div>
456
  </div>
457
 
458
- <!-- STEP 5: Bias Audit -->
459
- <div class="step hidden" id="step5">
460
  <div class="step-header">
461
- <div class="step-num">5</div>
462
  <div class="step-title">Bias Audit</div>
463
  </div>
464
  <p class="step-desc">
@@ -509,8 +471,8 @@
509
  <div class="results-details" id="auditReport"></div>
510
  </details>
511
  <div class="btn-row mt-16">
512
- <button class="secondary" onclick="goToStep(3)">Re-evaluate with bias calibration</button>
513
- <button class="secondary" onclick="goToStep(4)">Continue to gradient</button>
514
  </div>
515
  </div>
516
  </div>
@@ -594,7 +556,6 @@ async function init() {
594
  badge.className = 'config-badge warn';
595
  }
596
 
597
- // Changes will be populated with defaults
598
  addChange('', '');
599
  addChange('', '');
600
  }
@@ -608,7 +569,7 @@ function loadTemplate(name) {
608
  // ── Step navigation ──
609
 
610
  function goToStep(n) {
611
- for (let i = 1; i <= 5; i++) {
612
  const el = document.getElementById(`step${i}`);
613
  if (!el) continue;
614
  if (i < n) {
@@ -622,200 +583,156 @@ function goToStep(n) {
622
  el.classList.remove('active', 'done');
623
  }
624
  }
625
- // When going to bias audit, keep steps 4/5 both visible context
626
- if (n === 5) {
627
- const step3 = document.getElementById('step3');
628
- step3.classList.remove('hidden');
629
- step3.classList.add('done');
630
- }
631
  }
632
 
633
- // ── Step 1: Entity ──
634
 
635
- async function saveEntity() {
636
- const text = document.getElementById('entityText').value.trim();
637
- if (!text) return alert('Please enter your entity text.');
 
 
638
 
639
- const resp = await fetch('/api/session', {
640
- method: 'POST',
641
- headers: {'Content-Type': 'application/json'},
642
- body: JSON.stringify({entity_text: text}),
643
- });
644
- const data = await resp.json();
645
- sessionId = data.session_id;
646
 
647
- // Pre-fill cohort description from first line
648
- const firstLine = text.split('\n').find(l => l.trim().length > 2) || '';
649
- const desc = document.getElementById('cohortDesc');
650
- if (!desc.value) desc.value = `People evaluating: ${firstLine.replace(/^#+\s*/, '').trim()}`;
651
 
652
- goToStep(2);
 
653
 
654
- // Auto-suggest segments via LLM
655
- await suggestSegments();
656
- }
 
 
657
 
658
- async function suggestSegments() {
659
- const entityText = document.getElementById('entityText').value.trim();
660
  const audienceCtx = document.getElementById('cohortDesc').value.trim();
661
- if (!entityText) return;
662
-
663
- // Clear existing segments and show loading
664
- const list = document.getElementById('segmentsList');
665
- list.innerHTML = '<div style="color:var(--text2);font-size:0.85rem;padding:8px">Suggesting segments...</div>';
666
 
667
  try {
668
- const resp = await fetch('/api/suggest-segments', {
 
 
 
669
  method: 'POST',
670
  headers: {'Content-Type': 'application/json'},
671
- body: JSON.stringify({entity_text: entityText, audience_context: audienceCtx}),
672
- });
673
- const data = await resp.json();
674
- list.innerHTML = '';
675
- (data.segments || []).forEach(seg => {
676
- addSegment(seg.label, seg.count || 8);
677
  });
678
- } catch (e) {
679
- // Fallback to empty segments on error
680
- list.innerHTML = '';
681
- addSegment('', 8);
682
- addSegment('', 8);
683
- }
684
- }
685
 
686
- // ── Step 2: Cohort ──
 
 
 
687
 
688
- function addSegment(label = '', count = 8) {
689
- const list = document.getElementById('segmentsList');
690
- const row = document.createElement('div');
691
- row.className = 'seg-row';
692
- row.innerHTML = `
693
- <input type="text" placeholder="Segment description" value="${label}">
694
- <input type="number" value="${count}" min="1" max="20">
695
- <button onclick="this.parentElement.remove()" title="Remove">&times;</button>
696
- `;
697
- list.appendChild(row);
698
- }
699
 
700
- async function generateCohort() {
701
- const desc = document.getElementById('cohortDesc').value.trim();
702
- if (!desc) return alert('Please add audience context.');
703
-
704
- const rows = document.querySelectorAll('#segmentsList .seg-row');
705
- const segments = [];
706
- rows.forEach(row => {
707
- const inputs = row.querySelectorAll('input');
708
- const label = inputs[0].value.trim();
709
- const count = parseInt(inputs[1].value) || 5;
710
- if (label) segments.push({label, count});
711
- });
712
 
713
- if (segments.length === 0) return alert('Add at least one segment.');
 
 
714
 
715
- const btn = document.getElementById('genCohortBtn');
716
- btn.disabled = true;
717
- const progress = document.getElementById('cohortProgress');
718
- progress.classList.remove('hidden');
719
- document.getElementById('cohortProgressBar').style.width = '30%';
720
- document.getElementById('cohortProgressText').textContent = 'Generating personas (this may take 30-60s)...';
721
 
722
- try {
723
- const resp = await fetch('/api/cohort/generate', {
724
  method: 'POST',
725
  headers: {'Content-Type': 'application/json'},
726
  body: JSON.stringify({description: desc, segments, parallel: 3}),
727
  });
728
- const data = await resp.json();
729
- sessionId = data.session_id;
730
-
731
- // Update entity text in session
732
- const entityText = document.getElementById('entityText').value.trim();
733
- if (entityText) {
734
- await fetch('/api/session', {
735
- method: 'POST',
736
- headers: {'Content-Type': 'application/json'},
737
- body: JSON.stringify({entity_text: entityText}),
738
- }).then(r => r.json()).then(d => { sessionId = d.session_id; });
739
- // Upload cohort to new session
740
- await fetch(`/api/cohort/upload/${sessionId}`, {
741
- method: 'POST',
742
- headers: {'Content-Type': 'application/json'},
743
- body: JSON.stringify(data.cohort),
744
- });
745
- }
746
 
747
- document.getElementById('cohortProgressBar').style.width = '100%';
748
- document.getElementById('cohortCount').textContent = data.cohort_size;
749
- document.getElementById('cohortResult').classList.remove('hidden');
750
- document.getElementById('cohortProgressText').textContent = 'Done!';
751
- } catch (e) {
752
- alert('Error generating cohort: ' + e.message);
753
- document.getElementById('cohortProgressText').textContent = 'Failed';
754
- } finally {
755
- btn.disabled = false;
756
- }
757
- }
758
 
759
- // ── Step 3: Evaluate ──
 
760
 
761
- function runEval() {
762
- if (!sessionId) return alert('No session.');
763
- const parallel = parseInt(document.getElementById('evalParallel').value) || 5;
764
- const biasCal = document.getElementById('biasCalibration').checked;
765
- const btn = document.getElementById('evalBtn');
766
- btn.disabled = true;
767
 
768
- document.getElementById('evalProgress').classList.remove('hidden');
769
- document.getElementById('evalResults').classList.add('hidden');
770
- document.getElementById('evalLog').innerHTML = '';
771
 
772
- const params = new URLSearchParams({parallel, bias_calibration: biasCal});
773
- const es = new EventSource(`/api/evaluate/stream/${sessionId}?${params}`);
 
 
 
 
774
 
775
- es.addEventListener('start', (e) => {
776
- const d = JSON.parse(e.data);
777
- document.getElementById('evalProgressText').textContent =
778
- `Evaluating ${d.total} evaluators (${d.model})...`;
779
- });
 
 
 
 
 
 
 
 
780
 
781
- es.addEventListener('progress', (e) => {
782
- const d = JSON.parse(e.data);
783
- const pct = Math.round(d.done / d.total * 100);
784
- document.getElementById('evalProgressBar').style.width = pct + '%';
785
- document.getElementById('evalProgressText').textContent =
786
- `${d.done}/${d.total} evaluated`;
787
-
788
- const log = document.getElementById('evalLog');
789
- const cls = d.error ? 'err' : d.action === 'positive' ? 'pos' : d.action === 'negative' ? 'neg' : 'neu';
790
- const icon = d.error ? 'ERR' : d.action === 'positive' ? '+' : d.action === 'negative' ? '-' : '~';
791
- const score = d.score != null ? `${d.score}/10` : '';
792
- log.innerHTML += `<div class="${cls}">[${d.done}/${d.total}] ${d.name}: ${icon} ${score}</div>`;
793
- log.scrollTop = log.scrollHeight;
794
- });
 
 
795
 
796
- es.addEventListener('complete', (e) => {
797
- es.close();
798
- const d = JSON.parse(e.data);
799
- evalResultsData = d.results;
800
-
801
- document.getElementById('evalProgressBar').style.width = '100%';
802
- document.getElementById('evalProgressText').textContent =
803
- `Done in ${d.elapsed}s`;
804
-
805
- document.getElementById('avgScore').innerHTML = `${d.avg_score}<span>/10</span>`;
806
- document.getElementById('posCount').textContent = d.positive;
807
- document.getElementById('neuCount').textContent = d.neutral;
808
- document.getElementById('negCount').textContent = d.negative;
809
- document.getElementById('evalAnalysis').textContent = d.analysis;
810
- document.getElementById('evalResults').classList.remove('hidden');
811
- btn.disabled = false;
812
- });
813
 
814
- es.onerror = () => {
815
- es.close();
816
- document.getElementById('evalProgressText').textContent = 'Connection lost';
 
817
  btn.disabled = false;
818
- };
 
 
 
 
 
 
819
  }
820
 
821
  // ── Step 4: Counterfactual ──
 
310
  <div id="configBadge" class="config-badge">checking...</div>
311
  </header>
312
 
313
+ <!-- STEP 1: Entity + Evaluate (one click) -->
314
  <div class="step active" id="step1">
315
  <div class="step-header">
316
  <div class="step-num">1</div>
317
+ <div class="step-title">What are you optimizing?</div>
318
  </div>
319
+ <p class="step-desc">Paste whatever you want evaluated — landing page, resume, pitch, dating profile, policy, anything.</p>
320
 
321
  <div class="template-chips">
322
  <span class="template-chip" onclick="loadTemplate('product')">Product</span>
 
325
  </div>
326
 
327
  <div class="field">
328
+ <textarea id="entityText" placeholder="Paste your entity here..."></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  </div>
330
 
331
+ <details class="mb-8">
332
+ <summary style="cursor:pointer;color:var(--text2);font-size:0.85rem">Advanced options</summary>
333
+ <div style="padding:12px 0">
334
+ <div class="field">
335
+ <label>Audience context (optional — auto-detected if blank)</label>
336
+ <input type="text" id="cohortDesc" placeholder="e.g. 'Women aged 25-35 evaluating a dating profile'">
337
+ </div>
338
+ <div class="field">
339
+ <label>Panel size</label>
340
+ <input type="number" id="panelSize" value="30" min="5" max="80"
341
+ style="width:80px;padding:6px;text-align:center">
342
+ </div>
343
+ <label style="display:flex;align-items:center;gap:6px;margin:0;font-size:0.85rem">
344
+ <input type="checkbox" id="biasCalibration">
345
+ Bias calibration (reduces framing/authority artifacts)
346
+ </label>
347
  </div>
348
+ </details>
 
 
349
 
350
+ <button onclick="runFullPipeline()" id="goBtn">Evaluate</button>
 
 
 
 
 
 
351
 
352
+ <div id="pipelineProgress" class="hidden mt-16">
353
+ <div class="progress-text" id="pipelineProgressText">Starting...</div>
354
+ <div class="progress-bar"><div class="progress-fill" id="pipelineProgressBar"></div></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  <div class="eval-log" id="evalLog"></div>
356
  </div>
357
 
 
367
  <div class="results-details" id="evalAnalysis"></div>
368
  </details>
369
  <div class="btn-row mt-16">
370
+ <button onclick="goToStep(2)">Find what to change</button>
371
+ <button class="secondary" onclick="goToStep(3)">Run bias audit</button>
372
  </div>
373
  </div>
374
  </div>
375
 
376
+ <!-- STEP 2: Counterfactual -->
377
+ <div class="step hidden" id="step2">
378
  <div class="step-header">
379
+ <div class="step-num">2</div>
380
  <div class="step-title">Find the highest-impact changes</div>
381
  </div>
382
  <p class="step-desc">List hypothetical changes. Undecided evaluators (scores 4-7) re-evaluate each one.</p>
 
417
  </div>
418
  </div>
419
 
420
+ <!-- STEP 3: Bias Audit -->
421
+ <div class="step hidden" id="step3">
422
  <div class="step-header">
423
+ <div class="step-num">3</div>
424
  <div class="step-title">Bias Audit</div>
425
  </div>
426
  <p class="step-desc">
 
471
  <div class="results-details" id="auditReport"></div>
472
  </details>
473
  <div class="btn-row mt-16">
474
+ <button class="secondary" onclick="rerunWithCalibration()">Re-evaluate with bias calibration</button>
475
+ <button class="secondary" onclick="goToStep(2)">Continue to gradient</button>
476
  </div>
477
  </div>
478
  </div>
 
556
  badge.className = 'config-badge warn';
557
  }
558
 
 
559
  addChange('', '');
560
  addChange('', '');
561
  }
 
569
  // ── Step navigation ──
570
 
571
  function goToStep(n) {
572
+ for (let i = 1; i <= 3; i++) {
573
  const el = document.getElementById(`step${i}`);
574
  if (!el) continue;
575
  if (i < n) {
 
583
  el.classList.remove('active', 'done');
584
  }
585
  }
 
 
 
 
 
 
586
  }
587
 
588
+ // ── Logging helper ──
589
 
590
+ function logStep(msg, cls = '') {
591
+ const log = document.getElementById('evalLog');
592
+ log.innerHTML += `<div class="${cls}">${msg}</div>`;
593
+ log.scrollTop = log.scrollHeight;
594
+ }
595
 
596
+ // ── Step 1: Full pipeline (one click) ──
 
 
 
 
 
 
597
 
598
+ async function runFullPipeline() {
599
+ const text = document.getElementById('entityText').value.trim();
600
+ if (!text) return alert('Please enter your entity text.');
 
601
 
602
+ const btn = document.getElementById('goBtn');
603
+ btn.disabled = true;
604
 
605
+ const progress = document.getElementById('pipelineProgress');
606
+ progress.classList.remove('hidden');
607
+ document.getElementById('evalResults').classList.add('hidden');
608
+ document.getElementById('evalLog').innerHTML = '';
609
+ document.getElementById('pipelineProgressBar').style.width = '5%';
610
 
611
+ const biasCal = document.getElementById('biasCalibration').checked;
612
+ const panelSize = parseInt(document.getElementById('panelSize').value) || 30;
613
  const audienceCtx = document.getElementById('cohortDesc').value.trim();
 
 
 
 
 
614
 
615
  try {
616
+ // Phase 1: Create session
617
+ document.getElementById('pipelineProgressText').textContent = 'Creating session...';
618
+ logStep('Creating session...');
619
+ const sessResp = await fetch('/api/session', {
620
  method: 'POST',
621
  headers: {'Content-Type': 'application/json'},
622
+ body: JSON.stringify({entity_text: text}),
 
 
 
 
 
623
  });
624
+ const sessData = await sessResp.json();
625
+ sessionId = sessData.session_id;
626
+ logStep(`Session: ${sessionId}`, 'pos');
 
 
 
 
627
 
628
+ // Phase 2: Suggest segments
629
+ document.getElementById('pipelineProgressText').textContent = 'Analyzing entity and choosing evaluator segments...';
630
+ document.getElementById('pipelineProgressBar').style.width = '10%';
631
+ logStep('Analyzing entity to find the right evaluator segments...');
632
 
633
+ const segResp = await fetch('/api/suggest-segments', {
634
+ method: 'POST',
635
+ headers: {'Content-Type': 'application/json'},
636
+ body: JSON.stringify({
637
+ entity_text: text,
638
+ audience_context: audienceCtx || `People who would evaluate: ${text.substring(0, 200)}`,
639
+ }),
640
+ });
641
+ const segData = await segResp.json();
642
+ const segments = segData.segments || [];
 
643
 
644
+ // Scale segment counts to match requested panel size
645
+ const totalSuggested = segments.reduce((a, s) => a + (s.count || 8), 0);
646
+ const scale = panelSize / (totalSuggested || 1);
647
+ segments.forEach(s => { s.count = Math.max(2, Math.round((s.count || 8) * scale)); });
 
 
 
 
 
 
 
 
648
 
649
+ segments.forEach(s => logStep(` Segment: ${s.label} (${s.count} personas)`));
650
+ logStep(`${segments.length} segments identified`, 'pos');
651
+ document.getElementById('pipelineProgressBar').style.width = '20%';
652
 
653
+ // Phase 3: Generate cohort
654
+ document.getElementById('pipelineProgressText').textContent = 'Generating evaluator personas...';
655
+ logStep('Generating realistic personas for each segment...');
 
 
 
656
 
657
+ const desc = audienceCtx || `People evaluating: ${text.substring(0, 200)}`;
658
+ const cohortResp = await fetch('/api/cohort/generate', {
659
  method: 'POST',
660
  headers: {'Content-Type': 'application/json'},
661
  body: JSON.stringify({description: desc, segments, parallel: 3}),
662
  });
663
+ const cohortData = await cohortResp.json();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
+ // Upload cohort to our session
666
+ await fetch(`/api/cohort/upload/${sessionId}`, {
667
+ method: 'POST',
668
+ headers: {'Content-Type': 'application/json'},
669
+ body: JSON.stringify(cohortData.cohort),
670
+ });
 
 
 
 
 
671
 
672
+ logStep(`${cohortData.cohort_size} evaluators generated`, 'pos');
673
+ document.getElementById('pipelineProgressBar').style.width = '35%';
674
 
675
+ // Phase 4: Evaluate via SSE
676
+ document.getElementById('pipelineProgressText').textContent = 'Evaluating...';
677
+ logStep('Starting evaluation each evaluator scores your entity...');
 
 
 
678
 
679
+ await new Promise((resolve, reject) => {
680
+ const params = new URLSearchParams({parallel: 5, bias_calibration: biasCal});
681
+ const es = new EventSource(`/api/evaluate/stream/${sessionId}?${params}`);
682
 
683
+ es.addEventListener('start', (e) => {
684
+ const d = JSON.parse(e.data);
685
+ const calLabel = d.bias_calibration ? ' (bias-calibrated)' : '';
686
+ document.getElementById('pipelineProgressText').textContent =
687
+ `Evaluating ${d.total} personas${calLabel}...`;
688
+ });
689
 
690
+ es.addEventListener('progress', (e) => {
691
+ const d = JSON.parse(e.data);
692
+ const base = 35;
693
+ const pct = base + Math.round(d.done / d.total * (100 - base));
694
+ document.getElementById('pipelineProgressBar').style.width = pct + '%';
695
+ document.getElementById('pipelineProgressText').textContent =
696
+ `${d.done}/${d.total} evaluated`;
697
+
698
+ const cls = d.error ? 'err' : d.action === 'positive' ? 'pos' : d.action === 'negative' ? 'neg' : 'neu';
699
+ const icon = d.error ? 'ERR' : d.action === 'positive' ? '+' : d.action === 'negative' ? '-' : '~';
700
+ const score = d.score != null ? `${d.score}/10` : '';
701
+ logStep(`[${d.done}/${d.total}] ${d.name}: ${icon} ${score}`, cls);
702
+ });
703
 
704
+ es.addEventListener('complete', (e) => {
705
+ es.close();
706
+ const d = JSON.parse(e.data);
707
+ evalResultsData = d.results;
708
+
709
+ document.getElementById('pipelineProgressBar').style.width = '100%';
710
+ document.getElementById('pipelineProgressText').textContent = `Done in ${d.elapsed}s`;
711
+
712
+ document.getElementById('avgScore').innerHTML = `${d.avg_score}<span>/10</span>`;
713
+ document.getElementById('posCount').textContent = d.positive;
714
+ document.getElementById('neuCount').textContent = d.neutral;
715
+ document.getElementById('negCount').textContent = d.negative;
716
+ document.getElementById('evalAnalysis').textContent = d.analysis;
717
+ document.getElementById('evalResults').classList.remove('hidden');
718
+ resolve();
719
+ });
720
 
721
+ es.onerror = () => { es.close(); reject(new Error('Connection lost')); };
722
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
 
724
+ } catch (e) {
725
+ document.getElementById('pipelineProgressText').textContent = `Error: ${e.message}`;
726
+ logStep(`Error: ${e.message}`, 'err');
727
+ } finally {
728
  btn.disabled = false;
729
+ }
730
+ }
731
+
732
+ function rerunWithCalibration() {
733
+ document.getElementById('biasCalibration').checked = true;
734
+ goToStep(1);
735
+ runFullPipeline();
736
  }
737
 
738
  // ── Step 4: Counterfactual ──