Aspectgg commited on
Commit
8c81e76
·
1 Parent(s): 4966f78

Update UI

Browse files
app.py CHANGED
@@ -23,6 +23,7 @@ from core.api_handlers import (
23
  handle_retry_weakest_submit,
24
  handle_start_deal_phase,
25
  handle_start_session,
 
26
  handle_voice_pitch,
27
  handle_voice_turn,
28
  )
@@ -62,6 +63,11 @@ def api_start_session(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
62
  return handle_start_session(payload)
63
 
64
 
 
 
 
 
 
65
  @app.post("/api/chat-round")
66
  def api_chat_round(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
67
  return handle_chat_round(payload)
 
23
  handle_retry_weakest_submit,
24
  handle_start_deal_phase,
25
  handle_start_session,
26
+ handle_structure_pitch,
27
  handle_voice_pitch,
28
  handle_voice_turn,
29
  )
 
63
  return handle_start_session(payload)
64
 
65
 
66
+ @app.post("/api/structure-pitch")
67
+ def api_structure_pitch(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
68
+ return handle_structure_pitch(payload)
69
+
70
+
71
  @app.post("/api/chat-round")
72
  def api_chat_round(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
73
  return handle_chat_round(payload)
core/api_handlers.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
 
5
  import logging
6
  import os
 
7
  from typing import Any
8
 
9
  from dotenv import load_dotenv
@@ -30,6 +31,7 @@ from core.deal_verdict import build_judge_verdict
30
  from core.deal_phase import start_deal_phase
31
  from core.deal_flow import next_deal_round
32
  from core.deal_scoring_engine import generate_deal_scorecard
 
33
 
34
  load_dotenv()
35
 
@@ -730,6 +732,231 @@ def handle_reset_session(payload: dict[str, Any]) -> dict[str, Any]:
730
  return {"status": "reset"}
731
 
732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
733
  def handle_voice_pitch(payload: dict[str, Any]) -> dict[str, Any]:
734
  """Process opening spoken pitch audio via Nemotron Omni."""
735
  audio = payload.get("audio") or payload.get("audio_base64") or ""
 
4
 
5
  import logging
6
  import os
7
+ import re
8
  from typing import Any
9
 
10
  from dotenv import load_dotenv
 
31
  from core.deal_phase import start_deal_phase
32
  from core.deal_flow import next_deal_round
33
  from core.deal_scoring_engine import generate_deal_scorecard
34
+ from core.json_utils import parse_model_json
35
 
36
  load_dotenv()
37
 
 
732
  return {"status": "reset"}
733
 
734
 
735
+ _STARTUP_CONTEXT_FIELDS = (
736
+ "name",
737
+ "problem",
738
+ "target_users",
739
+ "solution",
740
+ "why_ai",
741
+ "traction",
742
+ "competitors",
743
+ "ask",
744
+ )
745
+
746
+ _STRUCTURE_PITCH_PROMPT = """You are structuring a founder's spoken or written startup pitch for a pitch battle app.
747
+
748
+ Extract ONLY what the founder actually said or wrote.
749
+ Do not hallucinate traction, competitors, funding, or users.
750
+ If a field was not mentioned, use an empty string and list it in missing_fields.
751
+
752
+ Return ONLY valid JSON.
753
+ First character must be {.
754
+ Last character must be }.
755
+ No markdown.
756
+ No explanation.
757
+ No reasoning.
758
+
759
+ Required JSON:
760
+ {
761
+ "startup_context": {
762
+ "name": "",
763
+ "problem": "",
764
+ "target_users": "",
765
+ "solution": "",
766
+ "why_ai": "",
767
+ "traction": "",
768
+ "competitors": "",
769
+ "ask": ""
770
+ },
771
+ "missing_fields": [],
772
+ "confidence": "low",
773
+ "brief_summary": ""
774
+ }
775
+
776
+ confidence must be one of: low, medium, high
777
+ brief_summary: one sentence summary of the pitch in the founder's words."""
778
+
779
+
780
+ def _normalize_startup_context(raw: dict[str, Any] | None) -> dict[str, str]:
781
+ ctx = raw if isinstance(raw, dict) else {}
782
+ return {field: str(ctx.get(field, "")).strip() for field in _STARTUP_CONTEXT_FIELDS}
783
+
784
+
785
+ def _missing_startup_fields(ctx: dict[str, str]) -> list[str]:
786
+ return [field for field in _STARTUP_CONTEXT_FIELDS if not ctx.get(field)]
787
+
788
+
789
+ def _confidence_from_fill(ctx: dict[str, str]) -> str:
790
+ filled = sum(1 for field in _STARTUP_CONTEXT_FIELDS if ctx.get(field))
791
+ if filled >= 5:
792
+ return "high"
793
+ if filled >= 3:
794
+ return "medium"
795
+ return "low"
796
+
797
+
798
+ def _structure_pitch_local_fallback(pitch_text: str) -> dict[str, Any]:
799
+ """Heuristic extraction when Nemotron is unavailable."""
800
+ text = pitch_text.strip()
801
+ lower = text.lower()
802
+ sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
803
+ ctx = {field: "" for field in _STARTUP_CONTEXT_FIELDS}
804
+
805
+ name_patterns = [
806
+ r"(?:called|named)\s+([A-Z][A-Za-z0-9]+(?:\s+[A-Z][A-Za-z0-9]+){0,3})",
807
+ r"(?:building|build|creating|launching)\s+([A-Z][A-Za-z0-9]+(?:\s+[A-Z][A-Za-z0-9]+){0,2})",
808
+ r"\b([A-Z][A-Za-z0-9]*(?:\s+[A-Z][A-Za-z0-9]*){0,2}\s+AI)\b",
809
+ r"(?:we(?:'re| are))\s+([A-Z][A-Za-z0-9]+(?:\s+[A-Z][A-Za-z0-9]+){0,2})",
810
+ ]
811
+ for pattern in name_patterns:
812
+ match = re.search(pattern, text)
813
+ if match:
814
+ ctx["name"] = match.group(1).strip()
815
+ break
816
+
817
+ problem_kw = ("problem", "pain", "miss", "struggle", "hard to", "difficult", "scattered", "frustrat")
818
+ for sentence in sentences:
819
+ sl = sentence.lower()
820
+ if any(kw in sl for kw in problem_kw):
821
+ ctx["problem"] = sentence
822
+ break
823
+
824
+ user_kw = ("students", "founders", "users", "customers", "developers", "teams", "college")
825
+ for sentence in sentences:
826
+ sl = sentence.lower()
827
+ if any(kw in sl for kw in user_kw):
828
+ ctx["target_users"] = sentence
829
+ break
830
+ if not ctx["target_users"]:
831
+ for kw in user_kw:
832
+ if kw in lower:
833
+ ctx["target_users"] = f"Targeting {kw}."
834
+ break
835
+
836
+ solution_kw = ("we build", "we're building", "we are building", "platform", "app", "product", "tool")
837
+ for sentence in sentences:
838
+ sl = sentence.lower()
839
+ if any(kw in sl for kw in solution_kw):
840
+ ctx["solution"] = sentence
841
+ break
842
+
843
+ if "ai" in lower or "machine learning" in lower or "model" in lower:
844
+ for sentence in sentences:
845
+ sl = sentence.lower()
846
+ if "ai" in sl or "model" in sl or "machine learning" in sl:
847
+ ctx["why_ai"] = sentence
848
+ break
849
+ if not ctx["why_ai"]:
850
+ ctx["why_ai"] = "Uses AI as described in the pitch."
851
+
852
+ traction_patterns = [
853
+ r"\b\d[\d,]*\+?\s*(?:users|students|customers|signups|downloads|pilots?)\b",
854
+ r"\b(?:tested with|pilot with|revenue|mrr|arr)\b[^.?!]*[.?!]?",
855
+ r"\b\d+%\b[^.?!]*[.?!]?",
856
+ ]
857
+ for pattern in traction_patterns:
858
+ match = re.search(pattern, text, re.IGNORECASE)
859
+ if match:
860
+ ctx["traction"] = match.group(0).strip().rstrip(".")
861
+ break
862
+
863
+ competitor_kw = ("competitor", "versus", " vs ", "compared to", "alternative", "luma", "linkedin")
864
+ for sentence in sentences:
865
+ sl = sentence.lower()
866
+ if any(kw in sl for kw in competitor_kw):
867
+ ctx["competitors"] = sentence
868
+ break
869
+
870
+ ask_kw = ("funding", "invest", "mentor", "pilot", "sponsor", "partnership", "raise", "support")
871
+ for sentence in sentences:
872
+ sl = sentence.lower()
873
+ if any(kw in sl for kw in ask_kw):
874
+ ctx["ask"] = sentence
875
+ break
876
+
877
+ missing = _missing_startup_fields(ctx)
878
+ summary = " ".join(sentences[:2])[:220] if sentences else text[:220]
879
+
880
+ return {
881
+ "ok": True,
882
+ "startup_context": ctx,
883
+ "missing_fields": missing,
884
+ "confidence": _confidence_from_fill(ctx),
885
+ "brief_summary": summary,
886
+ "source": "local_fallback",
887
+ }
888
+
889
+
890
+ def _parse_structure_pitch_response(raw: str) -> dict[str, Any] | None:
891
+ parsed, _ = parse_model_json(raw)
892
+ if not isinstance(parsed, dict):
893
+ return None
894
+
895
+ ctx = _normalize_startup_context(parsed.get("startup_context"))
896
+ missing = parsed.get("missing_fields")
897
+ if not isinstance(missing, list):
898
+ missing = _missing_startup_fields(ctx)
899
+ else:
900
+ missing = [str(f).strip() for f in missing if str(f).strip() in _STARTUP_CONTEXT_FIELDS]
901
+
902
+ confidence = str(parsed.get("confidence", "")).strip().lower()
903
+ if confidence not in ("low", "medium", "high"):
904
+ confidence = _confidence_from_fill(ctx)
905
+
906
+ summary = str(parsed.get("brief_summary", "")).strip()
907
+ if not summary:
908
+ summary = ctx.get("solution") or ctx.get("problem") or ""
909
+
910
+ return {
911
+ "startup_context": ctx,
912
+ "missing_fields": missing,
913
+ "confidence": confidence,
914
+ "brief_summary": summary,
915
+ }
916
+
917
+
918
+ def handle_structure_pitch(payload: dict[str, Any]) -> dict[str, Any]:
919
+ """Structure free-form pitch text into startup_context fields."""
920
+ pitch_text = str(payload.get("pitch_text", "")).strip()
921
+ if not pitch_text:
922
+ return {"ok": False, "error": "pitch_text is required and must be non-empty."}
923
+
924
+ if len(pitch_text) < 20:
925
+ return {"ok": False, "error": "pitch_text is too short. Add a few more details about your startup."}
926
+
927
+ model_mode = payload.get("model_mode", "premium_nvidia")
928
+ messages = [
929
+ {"role": "system", "content": _STRUCTURE_PITCH_PROMPT},
930
+ {"role": "user", "content": f"Founder pitch:\n\n{pitch_text[:6000]}"},
931
+ ]
932
+
933
+ try:
934
+ result = model_router.generate_structure_pitch_response(messages, model_mode=model_mode)
935
+ if result.get("ok") and result.get("content"):
936
+ structured = _parse_structure_pitch_response(result["content"])
937
+ if structured is None:
938
+ repair = model_router.generate_structure_pitch_repair_response(
939
+ result["content"], model_mode=model_mode
940
+ )
941
+ if repair.get("ok") and repair.get("content"):
942
+ structured = _parse_structure_pitch_response(repair["content"])
943
+
944
+ if structured is not None:
945
+ return {
946
+ "ok": True,
947
+ "startup_context": structured["startup_context"],
948
+ "missing_fields": structured["missing_fields"],
949
+ "confidence": structured["confidence"],
950
+ "brief_summary": structured["brief_summary"],
951
+ "source": "nemotron",
952
+ }
953
+ logger.warning("structure_pitch: Nemotron returned unparseable JSON — using local fallback")
954
+ except Exception as exc:
955
+ logger.warning("structure_pitch: Nemotron call failed — using local fallback: %s", exc)
956
+
957
+ return _structure_pitch_local_fallback(pitch_text)
958
+
959
+
960
  def handle_voice_pitch(payload: dict[str, Any]) -> dict[str, Any]:
961
  """Process opening spoken pitch audio via Nemotron Omni."""
962
  audio = payload.get("audio") or payload.get("audio_base64") or ""
core/model_router.py CHANGED
@@ -598,6 +598,35 @@ def _call_nvidia_repair_mode(
598
  return {"ok": False, "model_mode": mode, "provider": "nvidia", "content": "", "error": str(exc)}
599
 
600
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
  def generate_deal_verdict_response(
602
  messages: list[dict[str, str]],
603
  model_mode: str | None = None,
 
598
  return {"ok": False, "model_mode": mode, "provider": "nvidia", "content": "", "error": str(exc)}
599
 
600
 
601
+ def generate_structure_pitch_response(
602
+ messages: list[dict[str, str]],
603
+ model_mode: str | None = None,
604
+ ) -> dict[str, Any]:
605
+ """Extract structured startup_context from free-form pitch text."""
606
+ return _call_nvidia_json_mode(messages, "structure_pitch", model_mode, "structure pitch")
607
+
608
+
609
+ def generate_structure_pitch_repair_response(
610
+ raw_bad_content: str,
611
+ model_mode: str | None = None,
612
+ ) -> dict[str, Any]:
613
+ repair_messages = [
614
+ {
615
+ "role": "system",
616
+ "content": (
617
+ "Convert input to JSON. Return ONLY valid JSON.\n"
618
+ '{"startup_context":{"name":"","problem":"","target_users":"",'
619
+ '"solution":"","why_ai":"","traction":"","competitors":"","ask":""},'
620
+ '"missing_fields":[],"confidence":"low","brief_summary":""}'
621
+ ),
622
+ },
623
+ {"role": "user", "content": "Output JSON only:\n\n" + raw_bad_content[:4000]},
624
+ ]
625
+ return _call_nvidia_repair_mode(
626
+ repair_messages, "structure_pitch_repair", model_mode, "structure pitch"
627
+ )
628
+
629
+
630
  def generate_deal_verdict_response(
631
  messages: list[dict[str, str]],
632
  model_mode: str | None = None,
core/nvidia_client.py CHANGED
@@ -206,6 +206,20 @@ _TASK_DEFAULTS: dict[str, dict[str, Any]] = {
206
  "temperature": 0.0,
207
  "top_p": 0.95,
208
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  }
210
 
211
  _VALID_AUDIO_FORMATS = frozenset({"webm", "wav", "mp3", "m4a", "ogg"})
 
206
  "temperature": 0.0,
207
  "top_p": 0.95,
208
  },
209
+ "structure_pitch": {
210
+ "enable_thinking": False,
211
+ "reasoning_budget": 0,
212
+ "max_tokens": 900,
213
+ "temperature": 0.1,
214
+ "top_p": 0.95,
215
+ },
216
+ "structure_pitch_repair": {
217
+ "enable_thinking": False,
218
+ "reasoning_budget": 0,
219
+ "max_tokens": 800,
220
+ "temperature": 0.0,
221
+ "top_p": 0.95,
222
+ },
223
  }
224
 
225
  _VALID_AUDIO_FORMATS = frozenset({"webm", "wav", "mp3", "m4a", "ogg"})
frontend/index.html CHANGED
@@ -312,44 +312,152 @@
312
 
313
  <!-- Setup -->
314
  <section id="screen-setup" class="screen">
315
- <div class="briefing-shell">
316
  <header class="briefing-header">
317
  <div>
318
  <p class="results-eyebrow">Pre-Fight Briefing</p>
319
  <h2 class="briefing-title">Founder Briefing</h2>
320
- <p class="briefing-subtitle">Give the AI judge enough context to attack your pitch properly.</p>
321
- <p class="briefing-helper">Sharper inputs create tougher questions.</p>
322
  </div>
323
  <button id="btn-back-landing" class="btn btn-ghost">Back</button>
324
  </header>
325
 
326
- <div class="briefing-grid">
327
- <div class="panel glass briefing-panel">
328
- <h3 class="briefing-section-title">Startup Identity</h3>
329
- <form id="startup-form" class="startup-form briefing-form">
330
- <div class="briefing-group">
331
- <label>Name<input name="name" type="text" placeholder="EventRadar AI" required /></label>
332
- </div>
333
- <div class="briefing-group briefing-group-problem">
334
- <h4 class="briefing-group-label">Problem + Users</h4>
335
- <label>Problem<textarea name="problem" rows="2" required placeholder="What pain are you solving?"></textarea></label>
336
- <label>Target Users<input name="target_users" type="text" required placeholder="Who feels this pain most?" /></label>
337
- </div>
338
- <div class="briefing-group briefing-group-solution">
339
- <h4 class="briefing-group-label">Solution + Why AI</h4>
340
- <label>Solution<textarea name="solution" rows="2" required placeholder="What do you build?"></textarea></label>
341
- <label>Why AI<textarea name="why_ai" rows="2" required placeholder="Why AI instead of rules or manual work?"></textarea></label>
 
 
 
 
 
 
342
  </div>
343
- <div class="briefing-group briefing-group-traction">
344
- <h4 class="briefing-group-label">Traction + Competitors</h4>
345
- <label>Competitors<input name="competitors" type="text" required placeholder="Who else solves this?" /></label>
346
- <label>Traction<input name="traction" type="text" required placeholder="Users, pilots, revenue, demos…" /></label>
 
 
 
 
 
 
 
 
347
  </div>
348
- <div class="briefing-group briefing-group-ask">
349
- <h4 class="briefing-group-label">Ask / Desired Outcome</h4>
350
- <label>Ask<input name="ask" type="text" required placeholder="Funding, pilot, mentorship, sponsorship…" /></label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  </div>
352
- </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  </div>
354
 
355
  <div class="panel glass briefing-panel briefing-opponent-panel">
@@ -391,7 +499,10 @@
391
  </button>
392
  </div>
393
  </div>
394
- <button id="btn-start-battle" class="btn btn-primary btn-wide btn-arena-start" type="button">Start Pitch Battle</button>
 
 
 
395
  </div>
396
  </div>
397
  </div>
@@ -631,130 +742,161 @@
631
  </div>
632
  </section>
633
 
634
- <!-- Scorecard -->
635
- <section id="screen-scorecard" class="screen">
636
- <div class="results-shell results-compact">
637
- <article class="results-hero-card glass">
638
- <header class="results-hero-header">
639
- <div class="results-hero-header-left">
640
- <p class="results-eyebrow">Pitch Battle Result</p>
641
- <button id="btn-view-conversation" class="btn btn-conversation-review" type="button">View Conversation</button>
642
- </div>
643
- <p id="scorecard-source-badge" class="source-chip" hidden></p>
644
- </header>
645
- <p id="scorecard-fallback-warning" class="results-fallback-warn" hidden></p>
646
-
647
- <div class="results-hero-grid">
648
- <div class="score-orb-wrap">
649
- <div class="score-orb" aria-label="Overall pitch score">
650
  <strong id="overall-score" class="score-orb-value">0</strong>
651
  <span id="overall-label" class="score-orb-label"></span>
652
  </div>
653
  </div>
654
- <div class="results-hero-center">
655
- <p id="pitch-readout" class="result-verdict-text">Your pitch battle is complete.</p>
656
- <div class="insight-grid score-triad" id="pitch-stat-chips">
657
- <span class="source-chip chip-strong" id="chip-strongest-dim">Strongest: —</span>
658
- <span class="source-chip chip-weak" id="chip-weakest-dim">Weakest: —</span>
659
- <span class="source-chip chip-source" id="chip-score-source" hidden>Nemotron Judge</span>
 
 
 
 
 
 
660
  </div>
661
  </div>
 
 
 
662
  </div>
 
663
 
664
- <div class="result-next-action result-next-action-compact" id="pitch-next-action" hidden>
665
- <span class="result-next-action-label">Next</span>
666
- <p id="pitch-next-action-text" class="result-next-action-text"></p>
667
- </div>
668
 
669
- <div class="scorecard-actions results-hero-actions results-hero-actions-compact">
670
- <button id="btn-path-to-80" class="btn btn-path80" type="button">View Path to 80+</button>
671
- <button id="btn-reset" class="btn btn-primary" type="button">New Battle</button>
672
- <button id="btn-back-setup" class="btn btn-secondary" type="button">Edit Startup</button>
673
- </div>
674
- </article>
675
 
676
- <article id="judge-verdict-hero" class="judge-verdict-hero glass" hidden>
677
- <p class="results-eyebrow">Judge Verdict</p>
678
- <div id="judge-verdict-section" class="judge-verdict-panel">
679
- <article id="judge-verdict-card" class="judge-verdict-card glass-inner">
680
- <div class="judge-verdict-head">
681
- <span id="verdict-persona-badge" class="verdict-persona-badge result-verdict-badge"></span>
682
- <span id="verdict-interest-badge" class="verdict-interest-badge"></span>
683
- </div>
684
- <blockquote id="verdict-reaction" class="verdict-reaction verdict-reaction-compact"></blockquote>
685
- <div class="verdict-meta-grid verdict-meta-compact">
686
- <div><span>Deal Type</span><strong id="verdict-deal-type"></strong></div>
687
- <div><span>Why</span><strong id="verdict-why" class="clamp-text clamp-short"></strong></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  </div>
689
- <p id="verdict-opening-offer" class="verdict-opening-offer" hidden></p>
690
- <div id="verdict-actions" class="verdict-actions"></div>
691
- </article>
692
- </div>
693
- </article>
694
 
695
- <nav class="result-tabs" id="pitch-result-tabs" role="tablist" aria-label="Pitch scorecard sections">
696
- <button type="button" class="result-tab active" role="tab" aria-selected="true" data-tab="overview">Overview</button>
697
- <button type="button" class="result-tab" role="tab" aria-selected="false" data-tab="dimensions">Dimensions</button>
698
- <button type="button" class="result-tab" role="tab" aria-selected="false" data-tab="coaching">Coaching</button>
699
- <button type="button" class="result-tab" role="tab" aria-selected="false" data-tab="voice" id="tab-voice-delivery" hidden>Voice Delivery</button>
700
- </nav>
701
-
702
- <div class="result-panels" id="pitch-result-panels">
703
- <section class="result-panel active" data-panel="overview" role="tabpanel">
704
- <div class="answer-compare overview-answers overview-answers-compact">
705
- <article class="feedback-card answer-card answer-best coaching-card">
706
- <div class="feedback-card-head">
707
- <span class="answer-badge answer-badge-best">Strongest Answer</span>
708
- <span id="best-answer-round" class="round-badge" hidden></span>
709
  </div>
710
- <blockquote id="best-answer" class="answer-quote clamp-text clamp-short"></blockquote>
711
- </article>
712
- <article class="feedback-card answer-card answer-weak coaching-card">
713
- <div class="feedback-card-head">
714
- <span class="answer-badge answer-badge-weak">Weakest Answer</span>
715
- <span id="weakest-answer-round" class="round-badge" hidden></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
716
  </div>
717
- <blockquote id="weakest-answer" class="answer-quote answer-quote-weak clamp-text clamp-short"></blockquote>
718
- <p id="why-weak" class="why-weak-note clamp-text clamp-short" hidden></p>
719
- </article>
720
- </div>
721
- <div id="signals-summary" class="insight-grid signals-chips signals-chips-compact" hidden></div>
722
- </section>
723
 
724
- <section class="result-panel" data-panel="dimensions" role="tabpanel" hidden>
725
- <div id="score-bars" class="dimension-list"></div>
726
- </section>
 
 
 
 
 
 
727
 
728
- <section class="result-panel" data-panel="coaching" role="tabpanel" hidden>
729
- <div class="feedback-grid coaching-grid">
730
- <article class="feedback-card highlight coaching-card">
731
- <div class="feedback-card-head">
732
- <span class="feedback-icon" aria-hidden="true"></span>
733
- <h3>Improved Answer</h3>
734
  </div>
735
- <p id="improved-answer" class="feedback-body clamp-text"></p>
736
- </article>
737
- <article class="feedback-card highlight coaching-card">
738
- <div class="feedback-card-head">
739
- <span class="feedback-icon" aria-hidden="true"></span>
740
- <h3>Improved Pitch</h3>
 
 
 
 
741
  </div>
742
- <p id="improved-pitch" class="feedback-body clamp-text"></p>
743
- </article>
744
- </div>
745
- <div class="coaching-card prep-questions">
746
- <h3 class="scorecard-section-title">Top 3 Prep Questions</h3>
747
- <ol id="top-questions" class="prep-list prep-checklist"></ol>
748
- </div>
749
- </section>
750
 
751
- <section class="result-panel" data-panel="voice" role="tabpanel" hidden>
752
- <div id="voice-delivery-section" class="voice-delivery-panel">
753
- <p class="scorecard-section-desc">Observable delivery cues from your spoken answers.</p>
754
- <div id="voice-delivery-content" class="voice-delivery-grid"></div>
755
- </div>
756
- </section>
 
 
 
 
757
  </div>
 
758
  </div>
759
  </section>
760
 
 
312
 
313
  <!-- Setup -->
314
  <section id="screen-setup" class="screen">
315
+ <div class="briefing-shell briefing-shell-wide">
316
  <header class="briefing-header">
317
  <div>
318
  <p class="results-eyebrow">Pre-Fight Briefing</p>
319
  <h2 class="briefing-title">Founder Briefing</h2>
320
+ <p class="briefing-subtitle" id="briefing-subtitle">Pitch naturally. We'll structure it before the judge attacks it.</p>
 
321
  </div>
322
  <button id="btn-back-landing" class="btn btn-ghost">Back</button>
323
  </header>
324
 
325
+ <div class="briefing-mode-tabs" role="tablist" aria-label="Briefing mode">
326
+ <button id="tab-quick-pitch" class="briefing-mode-tab active" type="button" role="tab" aria-selected="true">Quick Pitch</button>
327
+ <button id="tab-advanced-briefing" class="briefing-mode-tab" type="button" role="tab" aria-selected="false">Advanced Briefing</button>
328
+ </div>
329
+
330
+ <div class="briefing-grid briefing-grid-wide">
331
+ <div class="briefing-left-col">
332
+ <!-- Quick Pitch -->
333
+ <div id="panel-quick-pitch" class="panel glass briefing-panel quick-pitch-panel">
334
+ <label class="quick-pitch-label" for="quick-pitch-text">Pitch your startup idea</label>
335
+ <textarea
336
+ id="quick-pitch-text"
337
+ class="quick-pitch-textarea"
338
+ rows="9"
339
+ placeholder="We're building EventRadar AI for students who miss hackathons because events are scattered across WhatsApp, LinkedIn, and college groups. We collect events in one place and recommend the best ones based on student interests. We tested with 80 students and want mentorship and pilot support."
340
+ ></textarea>
341
+ <div class="quick-pitch-actions">
342
+ <button id="btn-structure-pitch" class="btn btn-primary btn-wide" type="button">Structure My Pitch</button>
343
+ <div class="quick-pitch-secondary-row">
344
+ <button id="btn-record-voice-setup" class="btn btn-secondary" type="button">Record Voice Pitch</button>
345
+ <button id="btn-load-sample-setup" class="btn btn-secondary" type="button">Load Demo Founder</button>
346
+ </div>
347
  </div>
348
+ <p id="structure-pitch-hint" class="quick-pitch-hint" hidden></p>
349
+ </div>
350
+
351
+ <!-- AI brief preview -->
352
+ <div id="brief-preview-panel" class="panel glass briefing-panel brief-preview-panel" hidden>
353
+ <div class="brief-preview-header">
354
+ <div class="brief-preview-title-row">
355
+ <h3 class="briefing-section-title">AI-Structured Founder Brief</h3>
356
+ <span id="brief-preview-confidence" class="brief-confidence-chip"></span>
357
+ </div>
358
+ <p class="brief-preview-helper">AI extracted this from your pitch. Review and confirm.</p>
359
+ <p id="brief-preview-hint" class="brief-preview-hint" hidden></p>
360
  </div>
361
+ <form id="brief-preview-form" class="brief-preview-grid">
362
+ <div class="brief-read-card brief-read-short" data-field="name">
363
+ <div class="brief-read-head">
364
+ <span class="brief-read-label">Startup</span>
365
+ <button type="button" class="brief-read-edit" aria-label="Edit Startup">✎</button>
366
+ </div>
367
+ <p class="brief-read-value is-empty">Not specified</p>
368
+ <input type="text" name="name" class="brief-read-input" hidden />
369
+ </div>
370
+ <div class="brief-read-card brief-read-short" data-field="target_users">
371
+ <div class="brief-read-head">
372
+ <span class="brief-read-label">Target Users</span>
373
+ <button type="button" class="brief-read-edit" aria-label="Edit Target Users">✎</button>
374
+ </div>
375
+ <p class="brief-read-value is-empty">Not specified</p>
376
+ <input type="text" name="target_users" class="brief-read-input" hidden />
377
+ </div>
378
+ <div class="brief-read-card brief-read-wide" data-field="problem">
379
+ <div class="brief-read-head">
380
+ <span class="brief-read-label">Problem</span>
381
+ <button type="button" class="brief-read-edit" aria-label="Edit Problem">✎</button>
382
+ </div>
383
+ <p class="brief-read-value is-empty">Not specified</p>
384
+ <textarea name="problem" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
385
+ </div>
386
+ <div class="brief-read-card brief-read-wide" data-field="solution">
387
+ <div class="brief-read-head">
388
+ <span class="brief-read-label">Solution</span>
389
+ <button type="button" class="brief-read-edit" aria-label="Edit Solution">✎</button>
390
+ </div>
391
+ <p class="brief-read-value is-empty">Not specified</p>
392
+ <textarea name="solution" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
393
+ </div>
394
+ <div class="brief-read-card brief-read-wide" data-field="why_ai">
395
+ <div class="brief-read-head">
396
+ <span class="brief-read-label">Why AI</span>
397
+ <button type="button" class="brief-read-edit" aria-label="Edit Why AI">✎</button>
398
+ </div>
399
+ <p class="brief-read-value is-empty">Not specified</p>
400
+ <textarea name="why_ai" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
401
+ </div>
402
+ <div class="brief-read-card brief-read-short" data-field="traction">
403
+ <div class="brief-read-head">
404
+ <span class="brief-read-label">Traction</span>
405
+ <button type="button" class="brief-read-edit" aria-label="Edit Traction">✎</button>
406
+ </div>
407
+ <p class="brief-read-value is-empty">Not specified</p>
408
+ <input type="text" name="traction" class="brief-read-input" hidden />
409
+ </div>
410
+ <div class="brief-read-card brief-read-short" data-field="competitors">
411
+ <div class="brief-read-head">
412
+ <span class="brief-read-label">Competitors</span>
413
+ <button type="button" class="brief-read-edit" aria-label="Edit Competitors">✎</button>
414
+ </div>
415
+ <p class="brief-read-value is-empty">Not specified</p>
416
+ <input type="text" name="competitors" class="brief-read-input" hidden />
417
+ </div>
418
+ <div class="brief-read-card brief-read-short" data-field="ask">
419
+ <div class="brief-read-head">
420
+ <span class="brief-read-label">Ask</span>
421
+ <button type="button" class="brief-read-edit" aria-label="Edit Ask">✎</button>
422
+ </div>
423
+ <p class="brief-read-value is-empty">Not specified</p>
424
+ <input type="text" name="ask" class="brief-read-input" hidden />
425
+ </div>
426
+ </form>
427
+ <div class="brief-preview-actions">
428
+ <button id="btn-looks-good-start" class="btn btn-primary btn-wide" type="button">Looks Good — Start Battle</button>
429
+ <button id="btn-restructure-pitch" class="btn btn-secondary btn-wide" type="button">Re-structure Pitch</button>
430
  </div>
431
+ </div>
432
+
433
+ <!-- Advanced Briefing (tab only — no duplicate collapsed section) -->
434
+ <div id="panel-advanced-briefing" class="panel glass briefing-panel advanced-briefing-panel" hidden>
435
+ <p class="advanced-briefing-helper">Want full control? Edit every field manually.</p>
436
+ <form id="startup-form" class="startup-form briefing-form">
437
+ <div class="briefing-group">
438
+ <label>Name<input name="name" type="text" placeholder="EventRadar AI" /></label>
439
+ </div>
440
+ <div class="briefing-group briefing-group-problem">
441
+ <h4 class="briefing-group-label">Problem + Users</h4>
442
+ <label>Problem<textarea name="problem" rows="3" placeholder="What pain are you solving?"></textarea></label>
443
+ <label>Target Users<input name="target_users" type="text" placeholder="Who feels this pain most?" /></label>
444
+ </div>
445
+ <div class="briefing-group briefing-group-solution">
446
+ <h4 class="briefing-group-label">Solution + Why AI</h4>
447
+ <label>Solution<textarea name="solution" rows="3" placeholder="What do you build?"></textarea></label>
448
+ <label>Why AI<textarea name="why_ai" rows="2" placeholder="Why AI instead of rules or manual work?"></textarea></label>
449
+ </div>
450
+ <div class="briefing-group briefing-group-traction">
451
+ <h4 class="briefing-group-label">Traction + Competitors</h4>
452
+ <label>Competitors<input name="competitors" type="text" placeholder="Who else solves this?" /></label>
453
+ <label>Traction<input name="traction" type="text" placeholder="Users, pilots, revenue, demos…" /></label>
454
+ </div>
455
+ <div class="briefing-group briefing-group-ask">
456
+ <h4 class="briefing-group-label">Ask / Desired Outcome</h4>
457
+ <label>Ask<input name="ask" type="text" placeholder="Funding, pilot, mentorship, sponsorship…" /></label>
458
+ </div>
459
+ </form>
460
+ </div>
461
  </div>
462
 
463
  <div class="panel glass briefing-panel briefing-opponent-panel">
 
499
  </button>
500
  </div>
501
  </div>
502
+ <div class="briefing-opponent-footer">
503
+ <button id="btn-start-battle" class="btn btn-primary btn-wide btn-arena-start" type="button">Start Pitch Battle</button>
504
+ <p class="briefing-opponent-note">Pick your opponent and difficulty, then start the battle.</p>
505
+ </div>
506
  </div>
507
  </div>
508
  </div>
 
742
  </div>
743
  </section>
744
 
745
+ <!-- Scorecard — unified single-page layout (no tabs) -->
746
+ <section id="screen-scorecard" class="screen sc-page">
747
+ <div class="sc-shell">
748
+ <header class="sc-hero">
749
+ <div class="sc-hero-row">
750
+ <div class="sc-ring-wrap">
751
+ <div class="score-orb sc-ring" aria-label="Overall pitch score">
 
 
 
 
 
 
 
 
 
752
  <strong id="overall-score" class="score-orb-value">0</strong>
753
  <span id="overall-label" class="score-orb-label"></span>
754
  </div>
755
  </div>
756
+ <div class="sc-hero-meta">
757
+ <p id="sc-hero-label" class="sc-hero-kicker">Pitch Battle Result</p>
758
+ <p id="pitch-readout" class="sc-hero-summary">Your pitch battle is complete.</p>
759
+ <div class="sc-chips" id="pitch-stat-chips">
760
+ <span class="sc-chip sc-chip-strong" id="chip-strongest-dim">↑ Strongest: —</span>
761
+ <span class="sc-chip sc-chip-weak" id="chip-weakest-dim"> Weakest: —</span>
762
+ <span class="sc-chip sc-chip-model" id="chip-score-source" hidden>⚡ Nemotron</span>
763
+ </div>
764
+ <div class="sc-hero-actions">
765
+ <button id="btn-path-to-80" class="btn sc-btn-gold" type="button">View Path to 80+</button>
766
+ <button id="btn-scorecard-retry" class="btn sc-btn-secondary" type="button">Retry Weakest Question</button>
767
+ <button id="btn-reset" class="btn sc-btn-secondary" type="button">New Battle</button>
768
  </div>
769
  </div>
770
+ <div class="sc-hero-conversation">
771
+ <button id="btn-view-conversation" class="btn sc-btn-conversation" type="button">View Conversation</button>
772
+ </div>
773
  </div>
774
+ </header>
775
 
776
+ <p id="scorecard-fallback-warning" class="sc-fallback-warn" hidden></p>
777
+ <span id="scorecard-source-badge" hidden aria-hidden="true"></span>
 
 
778
 
779
+ <div class="sc-body-grid">
780
+ <aside class="sc-left-col">
781
+ <p class="sc-section-label">Dimension Breakdown</p>
782
+ <div id="score-bars" class="sc-dim-list"></div>
 
 
783
 
784
+ <div class="sc-signals-block">
785
+ <p class="sc-section-label">Signals Detected</p>
786
+ <div id="signals-summary" class="sc-signals-groups"></div>
787
+ <p id="signals-empty" class="sc-signals-empty">No concrete signals detected in this session.</p>
788
+ </div>
789
+ </aside>
790
+
791
+ <div class="sc-right-col">
792
+ <nav class="sc-tabs" id="scorecard-tabs" role="tablist" aria-label="Scorecard sections">
793
+ <button type="button" class="sc-tab active" data-tab="overview" role="tab" aria-selected="true">Overview</button>
794
+ <button type="button" class="sc-tab" data-tab="answers" role="tab" aria-selected="false">Answers</button>
795
+ <button type="button" class="sc-tab" data-tab="prep" role="tab" aria-selected="false">Prep</button>
796
+ </nav>
797
+
798
+ <div class="sc-tab-panels">
799
+ <div class="sc-tab-panel active" data-panel="overview" role="tabpanel">
800
+ <div class="sc-tab-block">
801
+ <p class="sc-tab-eyebrow">Why you scored this</p>
802
+ <p id="score-why-scored" class="sc-tab-text"></p>
803
+ </div>
804
+ <div class="sc-tab-divider"></div>
805
+ <div class="sc-tab-block">
806
+ <p class="sc-tab-eyebrow">What stopped 80</p>
807
+ <p id="score-what-stopped" class="sc-tab-text"></p>
808
+ </div>
809
+ <div class="sc-tab-divider"></div>
810
+ <div class="sc-tab-block">
811
+ <p class="sc-tab-eyebrow">Next</p>
812
+ <p id="pitch-next-action-text" class="sc-tab-text sc-tab-text-gold"></p>
813
+ </div>
814
+ <article id="voice-delivery-section" class="sc-voice-inline" hidden>
815
+ <div class="sc-tab-divider"></div>
816
+ <p class="sc-tab-eyebrow">Voice Delivery</p>
817
+ <div id="voice-delivery-content" class="voice-delivery-grid"></div>
818
+ </article>
819
  </div>
 
 
 
 
 
820
 
821
+ <div class="sc-tab-panel" data-panel="answers" role="tabpanel" hidden>
822
+ <div id="answers-empty-state" class="sc-answers-empty" hidden>
823
+ <p class="sc-empty-title">No battle answers recorded.</p>
824
+ <p class="sc-empty-sub">You ended the battle before responding.</p>
825
+ <div class="sc-empty-actions">
826
+ <button type="button" id="btn-answers-retry" class="btn sc-btn-gold">Retry Weakest Question</button>
827
+ <button type="button" id="btn-answers-new-battle" class="btn sc-btn-secondary">New Battle</button>
828
+ </div>
 
 
 
 
 
 
829
  </div>
830
+ <div id="answers-content" class="sc-answers-stack">
831
+ <article class="sc-answer-card sc-answer-cyan">
832
+ <p class="sc-answer-eyebrow sc-eyebrow-cyan">Best Answer</p>
833
+ <span id="best-answer-round" class="sc-round-tag" hidden></span>
834
+ <blockquote id="best-answer" class="sc-answer-text"></blockquote>
835
+ </article>
836
+ <article class="sc-answer-card sc-answer-red">
837
+ <p class="sc-answer-eyebrow sc-eyebrow-weak">Weakest Answer</p>
838
+ <span id="weakest-answer-round" class="sc-round-tag" hidden></span>
839
+ <blockquote id="weakest-answer" class="sc-answer-text"></blockquote>
840
+ <p id="why-weak" class="sc-why-hurt" hidden></p>
841
+ </article>
842
+ <article class="sc-answer-card sc-answer-gold">
843
+ <p class="sc-answer-eyebrow sc-eyebrow-gold">Improved Answer</p>
844
+ <p id="improved-answer" class="sc-answer-text"></p>
845
+ </article>
846
+ <article class="sc-answer-card sc-answer-dim">
847
+ <p class="sc-answer-eyebrow sc-eyebrow-dim">Improved Pitch</p>
848
+ <p id="improved-pitch" class="sc-answer-text"></p>
849
+ </article>
850
  </div>
851
+ </div>
 
 
 
 
 
852
 
853
+ <div class="sc-tab-panel" data-panel="prep" role="tabpanel" hidden>
854
+ <p class="sc-tab-eyebrow">Top 3 questions to practice</p>
855
+ <ol id="top-questions" class="sc-prep-list"></ol>
856
+ <div class="sc-prep-retry-box">
857
+ <p id="sc-prep-retry-text" class="sc-prep-retry-prompt">Your weakest answer needs more proof. Practice it again?</p>
858
+ <button type="button" id="btn-prep-retry" class="btn sc-btn-gold">Retry Weakest Question</button>
859
+ </div>
860
+ </div>
861
+ </div>
862
 
863
+ <section id="judge-verdict-hero" class="sc-verdict-section sc-verdict-inline" hidden>
864
+ <p class="sc-section-label sc-verdict-heading">Judge Verdict</p>
865
+ <div id="judge-verdict-section" class="sc-verdict-card">
866
+ <div class="sc-verdict-head">
867
+ <span id="verdict-persona-badge" class="sc-verdict-judge"></span>
868
+ <span id="verdict-interest-badge" class="sc-verdict-pill"></span>
869
  </div>
870
+ <blockquote id="verdict-reaction" class="sc-verdict-quote"></blockquote>
871
+ <div class="sc-verdict-meta">
872
+ <div class="sc-verdict-meta-item">
873
+ <span class="sc-meta-label">Deal Type</span>
874
+ <strong id="verdict-deal-type" class="sc-meta-value"></strong>
875
+ </div>
876
+ <div class="sc-verdict-meta-item sc-verdict-meta-wide">
877
+ <span class="sc-meta-label">Why</span>
878
+ <strong id="verdict-why" class="sc-meta-value"></strong>
879
+ </div>
880
  </div>
881
+ <p id="verdict-opening-offer" class="sc-verdict-offer" hidden></p>
882
+ <div id="verdict-actions" class="sc-verdict-actions"></div>
883
+ </div>
884
+ </section>
885
+ </div>
886
+ </div>
 
 
887
 
888
+ <article id="score-explanation-card" hidden aria-hidden="true"></article>
889
+ <span id="pitch-next-action" hidden aria-hidden="true"></span>
890
+
891
+ <!-- Legacy panel hooks for JS (hidden, unused) -->
892
+ <div id="pitch-result-tabs" hidden aria-hidden="true"></div>
893
+ <div id="pitch-result-panels" hidden aria-hidden="true">
894
+ <section data-panel="overview"></section>
895
+ <section data-panel="dimensions"></section>
896
+ <section data-panel="coaching"></section>
897
+ <section data-panel="voice"></section>
898
  </div>
899
+ <button type="button" id="tab-voice-delivery" hidden aria-hidden="true"></button>
900
  </div>
901
  </section>
902
 
frontend/script.js CHANGED
@@ -23,6 +23,10 @@ const state = {
23
  dealBattleLog: [],
24
  battleMetaSnapshot: null,
25
  scorecardSnapshot: null,
 
 
 
 
26
  };
27
 
28
  let liveJudgeTurn = { text: "", meta: "" };
@@ -47,6 +51,10 @@ const dealInput = document.getElementById("deal-input");
47
  const dealStatus = document.getElementById("deal-status");
48
 
49
  const startupForm = document.getElementById("startup-form");
 
 
 
 
50
  const chatWindow = document.getElementById("chat-window");
51
  const userInput = document.getElementById("user-input");
52
  const loadingOverlay = document.getElementById("loading-overlay");
@@ -58,6 +66,9 @@ function showScreen(name) {
58
  Object.entries(screens).forEach(([key, el]) => {
59
  el.classList.toggle("active", key === name);
60
  });
 
 
 
61
  if (name === "landing" && landingIntroComplete) {
62
  finalizeLandingIntroStatic();
63
  }
@@ -191,24 +202,323 @@ function hideErrorBanner() {
191
  errorBanner.textContent = "";
192
  }
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  function getStartupPayload() {
 
 
 
195
  const data = new FormData(startupForm);
196
  return Object.fromEntries(data.entries());
197
  }
198
 
199
  function fillStartupForm(startup) {
200
  Object.entries(startup).forEach(([key, value]) => {
201
- const field = startupForm.elements.namedItem(key);
202
  if (field) field.value = value ?? "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  });
204
  }
205
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  const PERSONA_LABELS = {
207
  skeptical_vc: "Skeptical VC",
208
  technical_judge: "Technical Judge",
209
  hackathon_judge: "Hackathon Judge",
210
  };
211
 
 
 
 
 
 
 
212
  function pressureMeterLevel(data) {
213
  const label = String(data.pressure_label ?? data.pressure_level ?? "").toLowerCase();
214
  if (label.includes("extreme")) return { pct: 100, tier: "extreme" };
@@ -322,16 +632,40 @@ function updatePressureCore(tier, label) {
322
  if (judgePressure && label) judgePressure.textContent = label;
323
  }
324
 
325
- function updateConfidenceMeter(pressurePct) {
326
  const fill = document.getElementById("confidence-meter-fill");
327
  if (!fill) return;
328
- const confidence = Math.max(8, Math.min(100, 100 - pressurePct));
329
  fill.style.width = `${confidence}%`;
330
  fill.classList.toggle("confidence-low", confidence < 35);
331
  fill.classList.toggle("confidence-mid", confidence >= 35 && confidence < 65);
332
  fill.classList.toggle("confidence-high", confidence >= 65);
333
  }
334
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  function updateComboMeter(round) {
336
  const meter = document.getElementById("combo-meter");
337
  if (!meter) return;
@@ -631,7 +965,7 @@ function updateBattleMeta(data) {
631
  fill.style.width = `${pct}%`;
632
  fill.className = `pressure-meter-fill pressure-${tier}`;
633
  }
634
- updateConfidenceMeter(pct);
635
  updateComboMeter(round);
636
  const progressFill = document.getElementById("battle-progress-fill");
637
  if (progressFill) {
@@ -725,7 +1059,22 @@ export async function loadSample() {
725
  try {
726
  setGlobalLoading(true, "Loading demo founder…");
727
  const data = await apiPost("/api/load-sample");
728
- fillStartupForm(data.startup);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  showScreen("setup");
730
  hideErrorBanner();
731
  } catch (error) {
@@ -753,6 +1102,11 @@ export async function startSession() {
753
  document.getElementById("battle-coach-bar")?.setAttribute("hidden", "");
754
  document.getElementById("voice-turn-preview")?.setAttribute("hidden", "");
755
 
 
 
 
 
 
756
  const payload = {
757
  mode: "pitch_battle",
758
  startup: getStartupPayload(),
@@ -834,6 +1188,9 @@ export async function sendMessage(messageOverride, voiceMeta = null) {
834
  }
835
 
836
  updateBattleMeta(data);
 
 
 
837
  const chatMeta = data.model_ok
838
  ? `${data.attack_tag} · Round ${data.round} · ⚡ Premium Nemotron`
839
  : `${data.attack_tag} · Round ${data.round}`;
@@ -907,6 +1264,8 @@ export async function resetBattle() {
907
  state.uiMode = "pitch";
908
  state.pendingDealVoiceTurn = null;
909
  state.startMode = "text";
 
 
910
  state.conversationLog = [];
911
  state.dealConversationLog = [];
912
  state.battleLog = [];
@@ -971,16 +1330,58 @@ function attachShowMore(el) {
971
  function buildDimensionRow(key, value, opts = {}) {
972
  const s = value?.score ?? 0;
973
  const band = scoreBand(s);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
974
  const row = document.createElement("div");
975
  row.className = `dimension-row score-row ${band}`;
976
- const dimLabel = formatDimLabel(key);
977
  const labelHtml = value?.label
978
- ? `<span class="score-label">${escapeHtml(value.label)}</span>`
979
  : "";
980
  const quote = value?.quote && opts.showQuote
981
  ? `<span class="quote-chip">"${escapeHtml(value.quote)}"</span>`
982
  : "";
983
- const reason = value?.reason ?? "";
984
  row.innerHTML = `
985
  <div class="dimension-row-head score-row-head">
986
  <span class="dimension-name">${dimLabel}${labelHtml}</span>
@@ -1029,6 +1430,79 @@ function switchResultTab(tabsRootId, tabName) {
1029
  tab?.click();
1030
  }
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  function renderScorecard(data) {
1033
  const overall = data.overall ?? 0;
1034
  const overallEl = document.getElementById("overall-score");
@@ -1042,42 +1516,54 @@ function renderScorecard(data) {
1042
 
1043
  const scores = data.scores ?? {};
1044
  const { strongest, weakest } = getStrongestWeakest(scores);
 
 
 
1045
 
 
 
 
 
 
 
 
 
 
 
1046
  const readoutEl = document.getElementById("pitch-readout");
1047
  if (readoutEl) {
1048
- const se = data.score_explanation ?? {};
1049
- readoutEl.textContent =
1050
- data.improved_pitch?.split(/[.!?]/)[0]?.trim()
1051
- || se.why_you_scored_this?.split(/[.!?]/)[0]?.trim()
1052
- || (data.overall_label ? `${data.overall_label} pitch review dimensions below.` : "Your pitch battle is complete.");
 
1053
  }
1054
 
1055
  const strongChip = document.getElementById("chip-strongest-dim");
1056
  const weakChip = document.getElementById("chip-weakest-dim");
1057
  if (strongChip) {
1058
  strongChip.textContent = strongest
1059
- ? `Strongest: ${formatDimLabel(strongest[0])}`
1060
- : "Strongest: —";
1061
  }
1062
  if (weakChip) {
1063
  weakChip.textContent = weakest
1064
- ? `Weakest: ${formatDimLabel(weakest[0])}`
1065
- : "Weakest: —";
1066
  }
1067
 
1068
- const sourceBadgeEl = document.getElementById("scorecard-source-badge");
1069
  const chipSource = document.getElementById("chip-score-source");
1070
- const fallbackWarnEl = document.getElementById("scorecard-fallback-warning");
1071
- const nemotronScored = isNemotronScorecardSource(data.scorecard_source);
1072
- if (sourceBadgeEl) {
1073
- sourceBadgeEl.textContent = nemotronScored ? "Powered by NVIDIA Nemotron" : "";
1074
- sourceBadgeEl.hidden = !nemotronScored;
1075
- }
1076
  if (chipSource) {
1077
- chipSource.textContent = nemotronScored ? "Nemotron Judge" : "";
1078
- chipSource.hidden = !nemotronScored;
1079
- chipSource.classList.toggle("chip-source", nemotronScored);
 
 
1080
  }
 
 
1081
  if (fallbackWarnEl) {
1082
  fallbackWarnEl.textContent = "";
1083
  fallbackWarnEl.hidden = true;
@@ -1086,55 +1572,36 @@ function renderScorecard(data) {
1086
  console.warn("Scorecard model note (not shown to user):", data.model_error);
1087
  }
1088
 
1089
- const nextWrap = document.getElementById("pitch-next-action");
1090
- const nextText = document.getElementById("pitch-next-action-text");
1091
- const se = data.score_explanation ?? {};
1092
- const atr = se.answer_to_retry ?? {};
1093
- const nextLine =
1094
- atr.retry_advice
1095
- || se.what_stopped_80?.split(/[.!?]/)[0]?.trim()
1096
- || (weakest ? `Retry your ${formatDimLabel(weakest[0])} answer with one concrete proof point.` : "");
1097
- if (nextWrap && nextText) {
1098
- if (nextLine) {
1099
- nextText.textContent = nextLine.endsWith(".") ? nextLine : `${nextLine}.`;
1100
- nextWrap.hidden = false;
1101
- } else {
1102
- nextWrap.hidden = true;
1103
- }
1104
- }
1105
-
1106
  const bars = document.getElementById("score-bars");
1107
  if (bars) {
1108
  bars.innerHTML = "";
1109
  Object.entries(scores).forEach(([key, value]) => {
1110
- bars.appendChild(buildDimensionRow(key, value, { showQuote: true }));
 
 
 
 
 
 
1111
  });
1112
  }
1113
 
1114
- const sigEl = document.getElementById("signals-summary");
1115
- if (sigEl) {
1116
- const css = data.concrete_signals_summary ?? {};
1117
- const allSigs = [
1118
- ...(css.numbers ?? []),
1119
- ...(css.validation ?? []),
1120
- ...(css.competitors ?? []),
1121
- ...(css.revenue_signals ?? []),
1122
- ...(css.technical_mechanisms ?? []),
1123
- ].slice(0, 8);
1124
- sigEl.innerHTML = "";
1125
- if (allSigs.length > 0) {
1126
- allSigs.forEach((sig) => {
1127
- const chip = document.createElement("span");
1128
- chip.className = "source-chip signal-chip";
1129
- chip.textContent = sig;
1130
- sigEl.appendChild(chip);
1131
- });
1132
- sigEl.hidden = false;
1133
- } else {
1134
- sigEl.hidden = true;
1135
- }
1136
  }
1137
 
 
 
 
 
 
1138
  const setText = (id, text) => {
1139
  const el = document.getElementById(id);
1140
  if (!el) return;
@@ -1149,10 +1616,6 @@ function renderScorecard(data) {
1149
  setText("best-answer", data.best_answer);
1150
  setText("weakest-answer", data.weakest_answer);
1151
 
1152
- ["improved-answer", "improved-pitch", "best-answer", "weakest-answer"].forEach((id) => {
1153
- attachShowMore(document.getElementById(id));
1154
- });
1155
-
1156
  const setRoundBadge = (id, round) => {
1157
  const el = document.getElementById(id);
1158
  if (!el) return;
@@ -1172,7 +1635,6 @@ function renderScorecard(data) {
1172
  const why = data.why_weak ?? "";
1173
  whyWeakEl.textContent = why ? `Why it hurt: ${why}` : "";
1174
  whyWeakEl.hidden = !why;
1175
- attachShowMore(whyWeakEl);
1176
  }
1177
 
1178
  const list = document.getElementById("top-questions");
@@ -1185,25 +1647,35 @@ function renderScorecard(data) {
1185
  });
1186
  }
1187
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
  state.scoreExplanation = data.score_explanation ?? null;
1189
 
1190
  const pathBtn = document.getElementById("btn-path-to-80");
1191
- const hasExplanation = Boolean(state.scoreExplanation);
1192
- if (pathBtn) pathBtn.hidden = !hasExplanation;
1193
 
1194
  renderVoiceDelivery(data.voice_delivery);
1195
  renderJudgeVerdict(data.judge_verdict);
1196
  state.judgeVerdict = data.judge_verdict ?? null;
1197
 
1198
- initResultTabs("pitch-result-tabs", "pitch-result-panels");
1199
- switchResultTab("pitch-result-tabs", "overview");
1200
-
1201
- const orb = document.querySelector(".score-orb");
1202
  if (orb) {
1203
  orb.classList.remove("score-high", "score-mid", "score-low");
1204
  orb.classList.add(scoreBand(overall));
1205
  }
1206
 
 
 
1207
  state.scorecardSnapshot = {
1208
  overall,
1209
  overallLabel: data.overall_label ?? "",
@@ -1234,7 +1706,7 @@ function verdictNegotiationCta(label, fallback = "Start Negotiation →") {
1234
 
1235
  function renderJudgeVerdict(verdict) {
1236
  const heroWrap = document.getElementById("judge-verdict-hero");
1237
- if (!verdict) {
1238
  if (heroWrap) heroWrap.hidden = true;
1239
  return;
1240
  }
@@ -1246,7 +1718,7 @@ function renderJudgeVerdict(verdict) {
1246
  document.getElementById("verdict-persona-badge").textContent = personaLine;
1247
  const badge = document.getElementById("verdict-interest-badge");
1248
  badge.textContent = verdict.interest_label || interest.replaceAll("_", " ");
1249
- badge.className = `verdict-interest-badge result-verdict-badge interest-${interest}`;
1250
 
1251
  const reactionEl = document.getElementById("verdict-reaction");
1252
  if (reactionEl) {
@@ -1279,25 +1751,9 @@ function renderJudgeVerdict(verdict) {
1279
  btn.addEventListener("click", startDealPhase);
1280
  actions.appendChild(btn);
1281
  } else if (interest === "too_early") {
1282
- const btn = document.createElement("button");
1283
- btn.className = "btn btn-secondary";
1284
- btn.type = "button";
1285
- btn.textContent = verdictNegotiationCta(verdict.next_step_label, "Practice More — Negotiate Later");
1286
- btn.addEventListener("click", () => showScreen("setup"));
1287
- actions.appendChild(btn);
1288
  } else if (interest === "no_interest") {
1289
- const retryBtn = document.createElement("button");
1290
- retryBtn.className = "btn btn-path80";
1291
- retryBtn.type = "button";
1292
- retryBtn.textContent = "View Path to 80+";
1293
- retryBtn.addEventListener("click", openPath80);
1294
- actions.appendChild(retryBtn);
1295
- const newBtn = document.createElement("button");
1296
- newBtn.className = "btn btn-ghost";
1297
- newBtn.type = "button";
1298
- newBtn.textContent = "New Battle";
1299
- newBtn.addEventListener("click", resetBattle);
1300
- actions.appendChild(newBtn);
1301
  } else if (interest === "mild_interest" || interest === "strong_interest") {
1302
  if (!verdict.can_continue_to_deal) {
1303
  const btn = document.createElement("button");
@@ -1308,12 +1764,7 @@ function renderJudgeVerdict(verdict) {
1308
  actions.appendChild(btn);
1309
  }
1310
  } else if (verdict.deal_type === "verdict_only") {
1311
- const gapBtn = document.createElement("button");
1312
- gapBtn.className = "btn btn-path80";
1313
- gapBtn.type = "button";
1314
- gapBtn.textContent = verdict.next_step_label || "View Path to 80+";
1315
- gapBtn.addEventListener("click", openPath80);
1316
- actions.appendChild(gapBtn);
1317
  }
1318
  }
1319
 
@@ -1648,13 +2099,16 @@ function closeNegotiationModal() {
1648
 
1649
  function renderVoiceDelivery(vd) {
1650
  const content = document.getElementById("voice-delivery-content");
 
1651
  const tab = document.getElementById("tab-voice-delivery");
1652
  if (!content) return;
1653
  if (!vd || typeof vd !== "object") {
 
1654
  if (tab) tab.hidden = true;
1655
  content.innerHTML = "";
1656
  return;
1657
  }
 
1658
  if (tab) tab.hidden = false;
1659
 
1660
  const fillers = (vd.filler_word_list ?? []).slice(0, 6).join(", ") || "None detected";
@@ -1927,8 +2381,30 @@ function openRetryFromPath80() {
1927
  }
1928
 
1929
  document.getElementById("btn-load-sample").addEventListener("click", loadSample);
1930
- document.getElementById("btn-go-setup").addEventListener("click", () => showScreen("startMethod"));
1931
- document.getElementById("btn-back-landing").addEventListener("click", () => showScreen("landing"));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1932
  document.getElementById("btn-start-back-landing").addEventListener("click", () => showScreen("landing"));
1933
 
1934
  document.getElementById("btn-start-text").addEventListener("click", () => {
@@ -1945,17 +2421,27 @@ document.getElementById("btn-continue-start").addEventListener("click", () => {
1945
  if (state.startMode === "voice") showScreen("voicePitch");
1946
  else showScreen("setup");
1947
  });
1948
- document.getElementById("btn-voice-pitch-back").addEventListener("click", () => showScreen("startMethod"));
1949
  document.getElementById("btn-voice-edit-manual").addEventListener("click", () => {
1950
  const form = document.getElementById("voice-extract-form");
1951
  const data = new FormData(form);
1952
- fillStartupForm(Object.fromEntries(data.entries()));
 
 
 
 
 
1953
  showScreen("setup");
1954
  });
1955
  document.getElementById("btn-voice-looks-right").addEventListener("click", () => {
1956
  const form = document.getElementById("voice-extract-form");
1957
  const data = new FormData(form);
1958
- fillStartupForm(Object.fromEntries(data.entries()));
 
 
 
 
 
1959
  showScreen("setup");
1960
  });
1961
  document.getElementById("btn-voice-turn-send").addEventListener("click", () => {
@@ -1973,11 +2459,15 @@ document.getElementById("btn-voice-turn-send").addEventListener("click", () => {
1973
  document.getElementById("btn-start-battle").addEventListener("click", startSession);
1974
  document.getElementById("btn-end-battle").addEventListener("click", endBattle);
1975
  document.getElementById("btn-reset").addEventListener("click", resetBattle);
1976
- document.getElementById("btn-back-setup").addEventListener("click", () => showScreen("setup"));
1977
  document.getElementById("btn-path-to-80").addEventListener("click", openPath80);
1978
  document.getElementById("btn-close-path80").addEventListener("click", closePath80);
1979
  document.getElementById("btn-close-path80-bottom").addEventListener("click", closePath80);
1980
  document.getElementById("btn-retry-question")?.addEventListener("click", openRetryFromPath80);
 
 
 
 
1981
  document.getElementById("path80-overlay").addEventListener("click", (e) => {
1982
  if (e.target === e.currentTarget) closePath80();
1983
  });
@@ -2126,15 +2616,18 @@ function boot() {
2126
  console.log("PitchFight frontend booting...");
2127
  setGlobalLoading(false);
2128
  hideErrorBanner();
 
 
2129
  initLandingIntro();
 
2130
 
2131
  initVoiceUI({
2132
  getSessionId: () => state.sessionId,
2133
  getUiMode: () => state.uiMode,
2134
  onPitchComplete: (data) => {
2135
- state.pendingVoicePitch = data;
2136
  fillVoiceExtractForm(data);
2137
- showScreen("voiceConfirm");
 
2138
  hideErrorBanner();
2139
  },
2140
  onTurnComplete: (data) => showVoiceTurnPreview(data),
 
23
  dealBattleLog: [],
24
  battleMetaSnapshot: null,
25
  scorecardSnapshot: null,
26
+ briefingMode: "quick",
27
+ briefStructured: false,
28
+ pitchExtractionConfidence: null,
29
+ battleConfidencePct: null,
30
  };
31
 
32
  let liveJudgeTurn = { text: "", meta: "" };
 
51
  const dealStatus = document.getElementById("deal-status");
52
 
53
  const startupForm = document.getElementById("startup-form");
54
+ const briefPreviewForm = document.getElementById("brief-preview-form");
55
+ const quickPitchPanel = document.getElementById("panel-quick-pitch");
56
+ const briefPreviewPanel = document.getElementById("brief-preview-panel");
57
+ const briefingLeftCol = document.querySelector(".briefing-left-col");
58
  const chatWindow = document.getElementById("chat-window");
59
  const userInput = document.getElementById("user-input");
60
  const loadingOverlay = document.getElementById("loading-overlay");
 
66
  Object.entries(screens).forEach(([key, el]) => {
67
  el.classList.toggle("active", key === name);
68
  });
69
+ const app = document.getElementById("app");
70
+ app?.classList.toggle("app-arena-fullwidth", name === "battle" || name === "deal" || name === "scorecard");
71
+ app?.classList.toggle("app-scorecard-fullwidth", name === "scorecard");
72
  if (name === "landing" && landingIntroComplete) {
73
  finalizeLandingIntroStatic();
74
  }
 
202
  errorBanner.textContent = "";
203
  }
204
 
205
+ const BRIEF_FIELD_KEYS = [
206
+ "name", "target_users", "problem", "solution", "why_ai", "traction", "competitors", "ask",
207
+ ];
208
+
209
+ function setBriefFieldValue(key, value) {
210
+ const card = briefPreviewForm?.querySelector(`[data-field="${key}"]`);
211
+ if (!card) return;
212
+ const display = card.querySelector(".brief-read-value");
213
+ const input = card.querySelector(".brief-read-input");
214
+ const text = String(value ?? "").trim();
215
+ if (input) input.value = text;
216
+ if (display) {
217
+ display.textContent = text || "Not specified";
218
+ display.classList.toggle("is-empty", !text);
219
+ }
220
+ }
221
+
222
+ let _briefEditSnapshot = "";
223
+
224
+ function closeBriefFieldExpanded(card) {
225
+ if (!card) return;
226
+ const input = card.querySelector(".brief-read-input");
227
+ const display = card.querySelector(".brief-read-value");
228
+ card.classList.remove("is-editing", "is-editing-expanded");
229
+ if (input) input.hidden = true;
230
+ if (display) display.hidden = false;
231
+ briefPreviewForm?.classList.remove("is-editing-active");
232
+ briefPreviewPanel?.classList.remove("is-editing-active");
233
+ }
234
+
235
+ function commitBriefFieldEdit(card) {
236
+ if (!card) return;
237
+ const input = card.querySelector(".brief-read-input");
238
+ const display = card.querySelector(".brief-read-value");
239
+ const text = (input?.value || "").trim();
240
+ if (display) {
241
+ display.textContent = text || "Not specified";
242
+ display.classList.toggle("is-empty", !text);
243
+ }
244
+ closeBriefFieldExpanded(card);
245
+ syncBriefToStartupForm();
246
+ }
247
+
248
+ function cancelBriefFieldEdit(card) {
249
+ if (!card) return;
250
+ const input = card.querySelector(".brief-read-input");
251
+ if (input) input.value = _briefEditSnapshot;
252
+ closeBriefFieldExpanded(card);
253
+ }
254
+
255
+ function startBriefFieldEdit(card) {
256
+ if (!card) return;
257
+
258
+ briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing-expanded").forEach((open) => {
259
+ if (open !== card) cancelBriefFieldEdit(open);
260
+ });
261
+
262
+ const input = card.querySelector(".brief-read-input");
263
+ const display = card.querySelector(".brief-read-value");
264
+ _briefEditSnapshot = input?.value ?? "";
265
+
266
+ card.classList.add("is-editing", "is-editing-expanded");
267
+ briefPreviewForm?.classList.add("is-editing-active");
268
+ briefPreviewPanel?.classList.add("is-editing-active");
269
+
270
+ if (display) display.hidden = true;
271
+ if (input) {
272
+ input.hidden = false;
273
+ if (input.classList.contains("brief-read-textarea")) {
274
+ input.rows = 5;
275
+ }
276
+ requestAnimationFrame(() => {
277
+ input.focus();
278
+ if (typeof input.select === "function" && input.tagName === "INPUT") {
279
+ input.select();
280
+ } else if (input.tagName === "TEXTAREA") {
281
+ input.setSelectionRange(input.value.length, input.value.length);
282
+ }
283
+ card.scrollIntoView({ behavior: "smooth", block: "center" });
284
+ });
285
+ }
286
+ }
287
+
288
+ function initBriefPreviewEditors() {
289
+ briefPreviewForm?.querySelectorAll(".brief-read-card").forEach((card) => {
290
+ if (!card.querySelector(".brief-read-edit-actions")) {
291
+ const actions = document.createElement("div");
292
+ actions.className = "brief-read-edit-actions";
293
+ actions.innerHTML = `
294
+ <button type="button" class="btn btn-ghost btn-sm brief-read-cancel">Cancel</button>
295
+ <button type="button" class="btn btn-primary btn-sm brief-read-done">Done</button>
296
+ `;
297
+ card.appendChild(actions);
298
+ }
299
+
300
+ card.querySelector(".brief-read-edit")?.addEventListener("click", (e) => {
301
+ e.stopPropagation();
302
+ if (card.classList.contains("is-editing-expanded")) {
303
+ commitBriefFieldEdit(card);
304
+ } else {
305
+ startBriefFieldEdit(card);
306
+ }
307
+ });
308
+ card.querySelector(".brief-read-done")?.addEventListener("mousedown", (e) => e.preventDefault());
309
+ card.querySelector(".brief-read-done")?.addEventListener("click", (e) => {
310
+ e.stopPropagation();
311
+ commitBriefFieldEdit(card);
312
+ });
313
+ card.querySelector(".brief-read-cancel")?.addEventListener("mousedown", (e) => e.preventDefault());
314
+ card.querySelector(".brief-read-cancel")?.addEventListener("click", (e) => {
315
+ e.stopPropagation();
316
+ cancelBriefFieldEdit(card);
317
+ });
318
+ card.querySelector(".brief-read-input")?.addEventListener("keydown", (e) => {
319
+ if (e.key === "Escape") {
320
+ e.preventDefault();
321
+ cancelBriefFieldEdit(card);
322
+ }
323
+ if (e.key === "Enter" && e.ctrlKey) {
324
+ e.preventDefault();
325
+ commitBriefFieldEdit(card);
326
+ }
327
+ });
328
+ });
329
+ }
330
+
331
+ function syncBriefToStartupForm() {
332
+ if (!briefPreviewForm || !startupForm) return;
333
+ BRIEF_FIELD_KEYS.forEach((key) => {
334
+ const input = briefPreviewForm.querySelector(`[name="${key}"]`);
335
+ const field = startupForm.elements.namedItem(key);
336
+ if (field && input) field.value = input.value ?? "";
337
+ });
338
+ }
339
+
340
  function getStartupPayload() {
341
+ if (briefPreviewPanel && !briefPreviewPanel.hidden) {
342
+ syncBriefToStartupForm();
343
+ }
344
  const data = new FormData(startupForm);
345
  return Object.fromEntries(data.entries());
346
  }
347
 
348
  function fillStartupForm(startup) {
349
  Object.entries(startup).forEach(([key, value]) => {
350
+ const field = startupForm?.elements.namedItem(key);
351
  if (field) field.value = value ?? "";
352
+ if (BRIEF_FIELD_KEYS.includes(key)) setBriefFieldValue(key, value);
353
+ });
354
+ }
355
+
356
+ function setBriefingMode(mode) {
357
+ state.briefingMode = mode;
358
+ const isQuick = mode === "quick";
359
+ const previewVisible = state.briefStructured;
360
+
361
+ document.getElementById("tab-quick-pitch")?.classList.toggle("active", isQuick);
362
+ document.getElementById("tab-advanced-briefing")?.classList.toggle("active", !isQuick);
363
+ document.getElementById("tab-quick-pitch")?.setAttribute("aria-selected", String(isQuick));
364
+ document.getElementById("tab-advanced-briefing")?.setAttribute("aria-selected", String(!isQuick));
365
+ briefingLeftCol?.classList.toggle("mode-quick", isQuick);
366
+ briefingLeftCol?.classList.toggle("mode-advanced", !isQuick);
367
+
368
+ const advancedPanel = document.getElementById("panel-advanced-briefing");
369
+ if (advancedPanel) advancedPanel.hidden = isQuick;
370
+
371
+ if (isQuick) {
372
+ if (quickPitchPanel) quickPitchPanel.hidden = previewVisible;
373
+ if (briefPreviewPanel) briefPreviewPanel.hidden = !previewVisible;
374
+ } else {
375
+ if (quickPitchPanel) quickPitchPanel.hidden = true;
376
+ if (briefPreviewPanel) briefPreviewPanel.hidden = true;
377
+ syncBriefToStartupForm();
378
+ }
379
+
380
+ const subtitle = document.getElementById("briefing-subtitle");
381
+ if (subtitle) {
382
+ subtitle.textContent = isQuick
383
+ ? "Pitch naturally. We'll structure it before the judge attacks it."
384
+ : "Want full control? Edit every field manually.";
385
+ }
386
+ }
387
+
388
+ function showBriefPreview(meta = {}) {
389
+ if (quickPitchPanel) quickPitchPanel.hidden = true;
390
+ if (briefPreviewPanel) briefPreviewPanel.hidden = false;
391
+ const advancedPanel = document.getElementById("panel-advanced-briefing");
392
+ if (advancedPanel) advancedPanel.hidden = true;
393
+
394
+ const confEl = document.getElementById("brief-preview-confidence");
395
+ if (confEl) {
396
+ const conf = meta.confidence || "medium";
397
+ confEl.textContent = `Confidence: ${String(conf).toUpperCase()}`;
398
+ confEl.className = `brief-confidence-chip confidence-${conf}`;
399
+ }
400
+
401
+ const hintEl = document.getElementById("brief-preview-hint");
402
+ if (hintEl) {
403
+ if (meta.source === "local_fallback") {
404
+ hintEl.textContent = "Quick structure applied — tap ✎ on any field to edit.";
405
+ hintEl.hidden = false;
406
+ } else if (Array.isArray(meta.missing_fields) && meta.missing_fields.length) {
407
+ hintEl.textContent = `Not in your pitch: ${meta.missing_fields.join(", ")}`;
408
+ hintEl.hidden = false;
409
+ } else {
410
+ hintEl.hidden = true;
411
+ hintEl.textContent = "";
412
+ }
413
+ }
414
+ }
415
+
416
+ function hideBriefPreview() {
417
+ state.briefStructured = false;
418
+ if (quickPitchPanel) quickPitchPanel.hidden = false;
419
+ if (briefPreviewPanel) briefPreviewPanel.hidden = true;
420
+ document.getElementById("structure-pitch-hint")?.setAttribute("hidden", "");
421
+ document.getElementById("brief-preview-hint")?.setAttribute("hidden", "");
422
+ briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing").forEach((card) => {
423
+ commitBriefFieldEdit(card);
424
  });
425
  }
426
 
427
+ function extractionConfidenceLevel(meta = {}) {
428
+ const raw = meta.confidence ?? meta.extraction_confidence ?? "";
429
+ const key = String(raw).toLowerCase();
430
+ if (key === "high" || key === "medium" || key === "low") return key;
431
+ return null;
432
+ }
433
+
434
+ function confidenceFromStartupFields(startup = {}) {
435
+ const fields = ["name", "problem", "solution", "why_ai", "target_users", "traction", "competitors", "ask"];
436
+ const filled = fields.filter((field) => String(startup?.[field] ?? "").trim()).length;
437
+ if (filled >= 5) return "high";
438
+ if (filled >= 3) return "medium";
439
+ return "low";
440
+ }
441
+
442
+ function confidenceLevelToPct(level) {
443
+ const key = String(level || "medium").toLowerCase();
444
+ if (key === "high") return 76;
445
+ if (key === "low") return 22;
446
+ return 50;
447
+ }
448
+
449
+ function syncPitchExtractionConfidence(meta = {}, startup = null) {
450
+ const level = extractionConfidenceLevel(meta) || confidenceFromStartupFields(startup) || "medium";
451
+ state.pitchExtractionConfidence = level;
452
+ return level;
453
+ }
454
+
455
+ function fillBriefPreview(startup, meta = {}) {
456
+ state.briefStructured = true;
457
+ syncPitchExtractionConfidence(meta, startup);
458
+ fillStartupForm(startup);
459
+ showBriefPreview(meta);
460
+ }
461
+
462
+ function resetSetupScreen() {
463
+ state.briefStructured = false;
464
+ state.pitchExtractionConfidence = null;
465
+ setBriefingMode("quick");
466
+ hideBriefPreview();
467
+ const quickText = document.getElementById("quick-pitch-text");
468
+ if (quickText) quickText.value = "";
469
+ state.pendingVoicePitch = null;
470
+ state.startMode = "text";
471
+ }
472
+
473
+ async function structurePitch() {
474
+ const pitchText = document.getElementById("quick-pitch-text")?.value?.trim();
475
+ if (!pitchText) {
476
+ showErrorBanner("Type or paste your pitch first.");
477
+ return;
478
+ }
479
+ try {
480
+ setGlobalLoading(true, "Structuring your pitch…");
481
+ state.startMode = "text";
482
+ const data = await apiPost("/api/structure-pitch", { pitch_text: pitchText });
483
+ if (!data.ok) {
484
+ showErrorBanner(data.error || "Could not structure your pitch. Try Advanced Briefing.");
485
+ return;
486
+ }
487
+ fillBriefPreview(data.startup_context, data);
488
+ hideErrorBanner();
489
+ } catch (error) {
490
+ console.error(error);
491
+ showErrorBanner("We couldn't structure that pitch. Try Advanced Briefing or edit manually.");
492
+ } finally {
493
+ setGlobalLoading(false);
494
+ }
495
+ }
496
+
497
+ function applyVoicePitchToBriefing(data) {
498
+ state.pendingVoicePitch = data;
499
+ state.startMode = "voice";
500
+ const extracted = data.extracted ?? {};
501
+ fillBriefPreview(extracted, {
502
+ brief_summary: (data.transcript || "").slice(0, 240),
503
+ confidence: data.extraction_confidence || "medium",
504
+ });
505
+ const quickText = document.getElementById("quick-pitch-text");
506
+ if (quickText && data.transcript) quickText.value = data.transcript;
507
+ setBriefingMode("quick");
508
+ }
509
+
510
  const PERSONA_LABELS = {
511
  skeptical_vc: "Skeptical VC",
512
  technical_judge: "Technical Judge",
513
  hackathon_judge: "Hackathon Judge",
514
  };
515
 
516
+ const DIFFICULTY_LABELS = {
517
+ practice: "Practice Mode",
518
+ judge: "Judge Mode",
519
+ investor: "Investor Mode",
520
+ };
521
+
522
  function pressureMeterLevel(data) {
523
  const label = String(data.pressure_label ?? data.pressure_level ?? "").toLowerCase();
524
  if (label.includes("extreme")) return { pct: 100, tier: "extreme" };
 
632
  if (judgePressure && label) judgePressure.textContent = label;
633
  }
634
 
635
+ function renderConfidenceMeter(confidencePct) {
636
  const fill = document.getElementById("confidence-meter-fill");
637
  if (!fill) return;
638
+ const confidence = Math.max(8, Math.min(92, Math.round(confidencePct)));
639
  fill.style.width = `${confidence}%`;
640
  fill.classList.toggle("confidence-low", confidence < 35);
641
  fill.classList.toggle("confidence-mid", confidence >= 35 && confidence < 65);
642
  fill.classList.toggle("confidence-high", confidence >= 65);
643
  }
644
 
645
+ function refreshBattleConfidenceFromPressure(pressurePct, round = 1) {
646
+ const base = confidenceLevelToPct(state.pitchExtractionConfidence || "medium");
647
+ const pressureDrag = Math.round(Number(pressurePct) * 0.25);
648
+ const roundDrag = Math.max(0, Number(round) - 1) * 2;
649
+ const ceiling = Math.max(8, base - pressureDrag - roundDrag);
650
+ if (state.battleConfidencePct == null) {
651
+ state.battleConfidencePct = ceiling;
652
+ } else {
653
+ state.battleConfidencePct = Math.min(state.battleConfidencePct, ceiling);
654
+ }
655
+ renderConfidenceMeter(state.battleConfidencePct);
656
+ }
657
+
658
+ function adjustBattleConfidenceFromAnswer(quality) {
659
+ const deltas = { strong: 8, partial: -2, weak: -12, non_answer: -20 };
660
+ const delta = deltas[String(quality || "").toLowerCase()] ?? 0;
661
+ if (!delta) return;
662
+ state.battleConfidencePct = Math.max(
663
+ 8,
664
+ Math.min(92, (state.battleConfidencePct ?? confidenceLevelToPct(state.pitchExtractionConfidence)) + delta),
665
+ );
666
+ renderConfidenceMeter(state.battleConfidencePct);
667
+ }
668
+
669
  function updateComboMeter(round) {
670
  const meter = document.getElementById("combo-meter");
671
  if (!meter) return;
 
965
  fill.style.width = `${pct}%`;
966
  fill.className = `pressure-meter-fill pressure-${tier}`;
967
  }
968
+ refreshBattleConfidenceFromPressure(pct, round);
969
  updateComboMeter(round);
970
  const progressFill = document.getElementById("battle-progress-fill");
971
  if (progressFill) {
 
1059
  try {
1060
  setGlobalLoading(true, "Loading demo founder…");
1061
  const data = await apiPost("/api/load-sample");
1062
+ state.startMode = "text";
1063
+ setBriefingMode("quick");
1064
+ fillBriefPreview(data.startup, {
1065
+ brief_summary: data.startup?.solution || data.startup?.problem || "Demo founder loaded.",
1066
+ confidence: "high",
1067
+ });
1068
+ const quickText = document.getElementById("quick-pitch-text");
1069
+ if (quickText) {
1070
+ quickText.value = [
1071
+ data.startup?.name,
1072
+ data.startup?.problem,
1073
+ data.startup?.solution,
1074
+ data.startup?.traction,
1075
+ data.startup?.ask,
1076
+ ].filter(Boolean).join(" ");
1077
+ }
1078
  showScreen("setup");
1079
  hideErrorBanner();
1080
  } catch (error) {
 
1102
  document.getElementById("battle-coach-bar")?.setAttribute("hidden", "");
1103
  document.getElementById("voice-turn-preview")?.setAttribute("hidden", "");
1104
 
1105
+ if (!state.pitchExtractionConfidence) {
1106
+ syncPitchExtractionConfidence({}, getStartupPayload());
1107
+ }
1108
+ state.battleConfidencePct = null;
1109
+
1110
  const payload = {
1111
  mode: "pitch_battle",
1112
  startup: getStartupPayload(),
 
1188
  }
1189
 
1190
  updateBattleMeta(data);
1191
+ if (data.answer_quality) {
1192
+ adjustBattleConfidenceFromAnswer(data.answer_quality);
1193
+ }
1194
  const chatMeta = data.model_ok
1195
  ? `${data.attack_tag} · Round ${data.round} · ⚡ Premium Nemotron`
1196
  : `${data.attack_tag} · Round ${data.round}`;
 
1264
  state.uiMode = "pitch";
1265
  state.pendingDealVoiceTurn = null;
1266
  state.startMode = "text";
1267
+ state.pitchExtractionConfidence = null;
1268
+ state.battleConfidencePct = null;
1269
  state.conversationLog = [];
1270
  state.dealConversationLog = [];
1271
  state.battleLog = [];
 
1330
  function buildDimensionRow(key, value, opts = {}) {
1331
  const s = value?.score ?? 0;
1332
  const band = scoreBand(s);
1333
+ const dimLabel = formatDimLabel(key);
1334
+ const labelSlug = String(value?.label || "")
1335
+ .toLowerCase()
1336
+ .replace(/[^a-z0-9]+/g, "-")
1337
+ .replace(/^-|-$/g, "");
1338
+ const reason = value?.reason ?? "";
1339
+ const layout = opts.layout ?? "default";
1340
+
1341
+ if (layout === "scorecard") {
1342
+ const row = document.createElement("div");
1343
+ const fillSlug = labelSlug || "developing";
1344
+ let highlightClass = "";
1345
+ if (opts.weakestKey === key) highlightClass = " sc-dim-weakest";
1346
+ else if (opts.strongestKey === key) highlightClass = " sc-dim-strongest";
1347
+ row.className = `dimension-row score-row dimension-row-scorecard sc-dim-card ${band}${highlightClass}`;
1348
+ const flagHtml =
1349
+ opts.weakestKey === key
1350
+ ? `<span class="sc-dim-flag sc-dim-flag-weak">Weakest</span>`
1351
+ : opts.strongestKey === key
1352
+ ? `<span class="sc-dim-flag sc-dim-flag-strong">Strongest</span>`
1353
+ : "";
1354
+ const badgeHtml = value?.label
1355
+ ? `<span class="score-label dimension-badge score-label-${escapeHtml(labelSlug)}">${escapeHtml(value.label)}</span>`
1356
+ : "";
1357
+ row.innerHTML = `
1358
+ <div class="sc-dim-head">
1359
+ <div class="sc-dim-title">
1360
+ <span class="dimension-name">${dimLabel}</span>
1361
+ ${flagHtml}
1362
+ </div>
1363
+ <div class="sc-dim-meta">
1364
+ <strong class="dimension-score">${s}</strong>
1365
+ ${badgeHtml}
1366
+ </div>
1367
+ </div>
1368
+ <div class="dimension-bar bar-track"><div class="dimension-bar-fill bar-fill dim-fill-${escapeHtml(fillSlug)}" style="width:0%" data-width="${s}%"></div></div>
1369
+ `;
1370
+ requestAnimationFrame(() => {
1371
+ const fill = row.querySelector(".dimension-bar-fill");
1372
+ if (fill) fill.style.width = fill.dataset.width || `${s}%`;
1373
+ });
1374
+ return row;
1375
+ }
1376
+
1377
  const row = document.createElement("div");
1378
  row.className = `dimension-row score-row ${band}`;
 
1379
  const labelHtml = value?.label
1380
+ ? `<span class="score-label score-label-${escapeHtml(labelSlug)}">${escapeHtml(value.label)}</span>`
1381
  : "";
1382
  const quote = value?.quote && opts.showQuote
1383
  ? `<span class="quote-chip">"${escapeHtml(value.quote)}"</span>`
1384
  : "";
 
1385
  row.innerHTML = `
1386
  <div class="dimension-row-head score-row-head">
1387
  <span class="dimension-name">${dimLabel}${labelHtml}</span>
 
1430
  tab?.click();
1431
  }
1432
 
1433
+ function initScorecardTabs() {
1434
+ const root = document.getElementById("scorecard-tabs");
1435
+ if (!root || root.dataset.init === "1") return;
1436
+ root.dataset.init = "1";
1437
+ root.addEventListener("click", (e) => {
1438
+ const tab = e.target.closest(".sc-tab");
1439
+ if (!tab) return;
1440
+ activateScorecardTab(tab.dataset.tab);
1441
+ });
1442
+ }
1443
+
1444
+ function activateScorecardTab(tabName) {
1445
+ document.querySelectorAll("#scorecard-tabs .sc-tab").forEach((tab) => {
1446
+ const active = tab.dataset.tab === tabName;
1447
+ tab.classList.toggle("active", active);
1448
+ tab.setAttribute("aria-selected", active ? "true" : "false");
1449
+ });
1450
+ document.querySelectorAll(".sc-tab-panels .sc-tab-panel").forEach((panel) => {
1451
+ const active = panel.dataset.panel === tabName;
1452
+ panel.classList.toggle("active", active);
1453
+ panel.hidden = !active;
1454
+ });
1455
+ }
1456
+
1457
+ function renderSignalsGroups(css) {
1458
+ const container = document.getElementById("signals-summary");
1459
+ const emptyEl = document.getElementById("signals-empty");
1460
+ if (!container) return false;
1461
+
1462
+ const groups = [
1463
+ { key: "numbers", label: "Numbers" },
1464
+ { key: "validation", label: "Validation" },
1465
+ { key: "competitors", label: "Competitors" },
1466
+ { key: "revenue_signals", label: "Revenue" },
1467
+ { key: "technical_mechanisms", label: "Mechanisms" },
1468
+ ];
1469
+
1470
+ container.innerHTML = "";
1471
+ let hasAny = false;
1472
+
1473
+ groups.forEach(({ key, label }) => {
1474
+ const items = (css?.[key] ?? []).filter((s) => String(s).trim());
1475
+ if (!items.length) return;
1476
+ hasAny = true;
1477
+ const group = document.createElement("div");
1478
+ group.className = "sc-signal-group";
1479
+ const groupLabel = document.createElement("p");
1480
+ groupLabel.className = "sc-signal-group-label";
1481
+ groupLabel.textContent = label;
1482
+ group.appendChild(groupLabel);
1483
+ const chips = document.createElement("div");
1484
+ chips.className = "sc-signal-chips";
1485
+ items.forEach((sig) => {
1486
+ const chip = document.createElement("span");
1487
+ chip.className = "sc-signal-chip";
1488
+ chip.textContent = sig;
1489
+ chips.appendChild(chip);
1490
+ });
1491
+ group.appendChild(chips);
1492
+ container.appendChild(group);
1493
+ });
1494
+
1495
+ if (emptyEl) emptyEl.hidden = hasAny;
1496
+ return hasAny;
1497
+ }
1498
+
1499
+ function hasNoBattleAnswers(data) {
1500
+ const scores = data.scores ?? {};
1501
+ const allZero = Object.values(scores).every((v) => (v?.score ?? 0) === 0);
1502
+ const noBest = !String(data.best_answer ?? "").trim();
1503
+ return allZero || noBest;
1504
+ }
1505
+
1506
  function renderScorecard(data) {
1507
  const overall = data.overall ?? 0;
1508
  const overallEl = document.getElementById("overall-score");
 
1516
 
1517
  const scores = data.scores ?? {};
1518
  const { strongest, weakest } = getStrongestWeakest(scores);
1519
+ const se = data.score_explanation ?? {};
1520
+ const explanation = se;
1521
+ const nemotronScored = isNemotronScorecardSource(data.scorecard_source);
1522
 
1523
+ const opponent = PERSONA_LABELS[state.persona] ?? data.opponent ?? "AI Judge";
1524
+ const mode = DIFFICULTY_LABELS[state.difficultyProfile]
1525
+ ?? data.difficulty_label
1526
+ ?? "Practice Mode";
1527
+ const heroLabel = document.getElementById("sc-hero-label");
1528
+ if (heroLabel) {
1529
+ heroLabel.textContent = `Pitch Battle Result · ${opponent} · ${mode}`;
1530
+ }
1531
+
1532
+ const whySentence = se.why_you_scored_this?.split(/[.!?]/)[0]?.trim() ?? "";
1533
  const readoutEl = document.getElementById("pitch-readout");
1534
  if (readoutEl) {
1535
+ const parts = [];
1536
+ if (data.overall_label) parts.push(data.overall_label);
1537
+ if (whySentence) parts.push(whySentence);
1538
+ readoutEl.textContent = parts.length
1539
+ ? parts.join(" — ")
1540
+ : "Your pitch battle is complete.";
1541
  }
1542
 
1543
  const strongChip = document.getElementById("chip-strongest-dim");
1544
  const weakChip = document.getElementById("chip-weakest-dim");
1545
  if (strongChip) {
1546
  strongChip.textContent = strongest
1547
+ ? `Strongest: ${formatDimLabel(strongest[0])}`
1548
+ : "Strongest: —";
1549
  }
1550
  if (weakChip) {
1551
  weakChip.textContent = weakest
1552
+ ? `Weakest: ${formatDimLabel(weakest[0])}`
1553
+ : "Weakest: —";
1554
  }
1555
 
 
1556
  const chipSource = document.getElementById("chip-score-source");
1557
+ const sourceBadgeEl = document.getElementById("scorecard-source-badge");
 
 
 
 
 
1558
  if (chipSource) {
1559
+ chipSource.textContent = nemotronScored ? " Nemotron" : "⚡ Local";
1560
+ chipSource.hidden = false;
1561
+ }
1562
+ if (sourceBadgeEl) {
1563
+ sourceBadgeEl.textContent = nemotronScored ? "Powered by NVIDIA Nemotron" : "Local scoring";
1564
  }
1565
+
1566
+ const fallbackWarnEl = document.getElementById("scorecard-fallback-warning");
1567
  if (fallbackWarnEl) {
1568
  fallbackWarnEl.textContent = "";
1569
  fallbackWarnEl.hidden = true;
 
1572
  console.warn("Scorecard model note (not shown to user):", data.model_error);
1573
  }
1574
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1575
  const bars = document.getElementById("score-bars");
1576
  if (bars) {
1577
  bars.innerHTML = "";
1578
  Object.entries(scores).forEach(([key, value]) => {
1579
+ bars.appendChild(
1580
+ buildDimensionRow(key, value, {
1581
+ layout: "scorecard",
1582
+ strongestKey: strongest?.[0] ?? null,
1583
+ weakestKey: weakest?.[0] ?? null,
1584
+ }),
1585
+ );
1586
  });
1587
  }
1588
 
1589
+ renderSignalsGroups(data.concrete_signals_summary ?? {});
1590
+
1591
+ const atr = se.answer_to_retry ?? {};
1592
+ const nextLine = atr.retry_advice ?? "";
1593
+ const nextText = document.getElementById("pitch-next-action-text");
1594
+ if (nextText) {
1595
+ nextText.textContent = nextLine
1596
+ ? (nextLine.endsWith(".") ? nextLine : `${nextLine}.`)
1597
+ : "";
 
 
 
 
 
 
 
 
 
 
 
 
 
1598
  }
1599
 
1600
+ const whyScoredEl = document.getElementById("score-why-scored");
1601
+ const whatStoppedEl = document.getElementById("score-what-stopped");
1602
+ if (whyScoredEl) whyScoredEl.textContent = explanation.why_you_scored_this ?? "";
1603
+ if (whatStoppedEl) whatStoppedEl.textContent = explanation.what_stopped_80 ?? "";
1604
+
1605
  const setText = (id, text) => {
1606
  const el = document.getElementById(id);
1607
  if (!el) return;
 
1616
  setText("best-answer", data.best_answer);
1617
  setText("weakest-answer", data.weakest_answer);
1618
 
 
 
 
 
1619
  const setRoundBadge = (id, round) => {
1620
  const el = document.getElementById(id);
1621
  if (!el) return;
 
1635
  const why = data.why_weak ?? "";
1636
  whyWeakEl.textContent = why ? `Why it hurt: ${why}` : "";
1637
  whyWeakEl.hidden = !why;
 
1638
  }
1639
 
1640
  const list = document.getElementById("top-questions");
 
1647
  });
1648
  }
1649
 
1650
+ const prepRetryText = document.getElementById("sc-prep-retry-text");
1651
+ if (prepRetryText) {
1652
+ const weakDim = weakest ? formatDimLabel(weakest[0]).toLowerCase() : "your weakest dimension";
1653
+ prepRetryText.textContent = `Your weakest answer was on ${weakDim}. Practice it again?`;
1654
+ }
1655
+
1656
+ const noAnswers = hasNoBattleAnswers(data);
1657
+ const answersEmpty = document.getElementById("answers-empty-state");
1658
+ const answersContent = document.getElementById("answers-content");
1659
+ if (answersEmpty) answersEmpty.hidden = !noAnswers;
1660
+ if (answersContent) answersContent.hidden = noAnswers;
1661
+
1662
  state.scoreExplanation = data.score_explanation ?? null;
1663
 
1664
  const pathBtn = document.getElementById("btn-path-to-80");
1665
+ if (pathBtn) pathBtn.hidden = !Boolean(state.scoreExplanation);
 
1666
 
1667
  renderVoiceDelivery(data.voice_delivery);
1668
  renderJudgeVerdict(data.judge_verdict);
1669
  state.judgeVerdict = data.judge_verdict ?? null;
1670
 
1671
+ const orb = document.querySelector("#screen-scorecard .sc-ring");
 
 
 
1672
  if (orb) {
1673
  orb.classList.remove("score-high", "score-mid", "score-low");
1674
  orb.classList.add(scoreBand(overall));
1675
  }
1676
 
1677
+ activateScorecardTab("overview");
1678
+
1679
  state.scorecardSnapshot = {
1680
  overall,
1681
  overallLabel: data.overall_label ?? "",
 
1706
 
1707
  function renderJudgeVerdict(verdict) {
1708
  const heroWrap = document.getElementById("judge-verdict-hero");
1709
+ if (!verdict || !verdict.interest_level) {
1710
  if (heroWrap) heroWrap.hidden = true;
1711
  return;
1712
  }
 
1718
  document.getElementById("verdict-persona-badge").textContent = personaLine;
1719
  const badge = document.getElementById("verdict-interest-badge");
1720
  badge.textContent = verdict.interest_label || interest.replaceAll("_", " ");
1721
+ badge.className = `sc-verdict-pill interest-${interest}`;
1722
 
1723
  const reactionEl = document.getElementById("verdict-reaction");
1724
  if (reactionEl) {
 
1751
  btn.addEventListener("click", startDealPhase);
1752
  actions.appendChild(btn);
1753
  } else if (interest === "too_early") {
1754
+ /* Footer sc-cta-row handles retry / setup */
 
 
 
 
 
1755
  } else if (interest === "no_interest") {
1756
+ /* Footer sc-cta-row handles Path to 80+ and New Battle */
 
 
 
 
 
 
 
 
 
 
 
1757
  } else if (interest === "mild_interest" || interest === "strong_interest") {
1758
  if (!verdict.can_continue_to_deal) {
1759
  const btn = document.createElement("button");
 
1764
  actions.appendChild(btn);
1765
  }
1766
  } else if (verdict.deal_type === "verdict_only") {
1767
+ /* Footer sc-cta-row handles Path to 80+ */
 
 
 
 
 
1768
  }
1769
  }
1770
 
 
2099
 
2100
  function renderVoiceDelivery(vd) {
2101
  const content = document.getElementById("voice-delivery-content");
2102
+ const section = document.getElementById("voice-delivery-section");
2103
  const tab = document.getElementById("tab-voice-delivery");
2104
  if (!content) return;
2105
  if (!vd || typeof vd !== "object") {
2106
+ if (section) section.hidden = true;
2107
  if (tab) tab.hidden = true;
2108
  content.innerHTML = "";
2109
  return;
2110
  }
2111
+ if (section) section.hidden = false;
2112
  if (tab) tab.hidden = false;
2113
 
2114
  const fillers = (vd.filler_word_list ?? []).slice(0, 6).join(", ") || "None detected";
 
2381
  }
2382
 
2383
  document.getElementById("btn-load-sample").addEventListener("click", loadSample);
2384
+ document.getElementById("btn-load-sample-setup")?.addEventListener("click", loadSample);
2385
+ document.getElementById("btn-go-setup").addEventListener("click", () => {
2386
+ resetSetupScreen();
2387
+ showScreen("setup");
2388
+ });
2389
+ document.getElementById("btn-back-landing").addEventListener("click", () => {
2390
+ resetSetupScreen();
2391
+ showScreen("landing");
2392
+ });
2393
+ document.getElementById("tab-quick-pitch")?.addEventListener("click", () => setBriefingMode("quick"));
2394
+ document.getElementById("tab-advanced-briefing")?.addEventListener("click", () => setBriefingMode("advanced"));
2395
+ document.getElementById("btn-structure-pitch")?.addEventListener("click", structurePitch);
2396
+ document.getElementById("btn-record-voice-setup")?.addEventListener("click", () => {
2397
+ state.startMode = "voice";
2398
+ showScreen("voicePitch");
2399
+ });
2400
+ document.getElementById("btn-looks-good-start")?.addEventListener("click", () => {
2401
+ syncBriefToStartupForm();
2402
+ startSession();
2403
+ });
2404
+ document.getElementById("btn-restructure-pitch")?.addEventListener("click", () => {
2405
+ hideBriefPreview();
2406
+ document.getElementById("quick-pitch-text")?.focus();
2407
+ });
2408
  document.getElementById("btn-start-back-landing").addEventListener("click", () => showScreen("landing"));
2409
 
2410
  document.getElementById("btn-start-text").addEventListener("click", () => {
 
2421
  if (state.startMode === "voice") showScreen("voicePitch");
2422
  else showScreen("setup");
2423
  });
2424
+ document.getElementById("btn-voice-pitch-back").addEventListener("click", () => showScreen("setup"));
2425
  document.getElementById("btn-voice-edit-manual").addEventListener("click", () => {
2426
  const form = document.getElementById("voice-extract-form");
2427
  const data = new FormData(form);
2428
+ applyVoicePitchToBriefing({
2429
+ extracted: Object.fromEntries(data.entries()),
2430
+ transcript: document.getElementById("voice-confirm-transcript")?.textContent || "",
2431
+ extraction_confidence: "medium",
2432
+ });
2433
+ setBriefingMode("advanced");
2434
  showScreen("setup");
2435
  });
2436
  document.getElementById("btn-voice-looks-right").addEventListener("click", () => {
2437
  const form = document.getElementById("voice-extract-form");
2438
  const data = new FormData(form);
2439
+ applyVoicePitchToBriefing({
2440
+ extracted: Object.fromEntries(data.entries()),
2441
+ transcript: document.getElementById("voice-confirm-transcript")?.textContent || "",
2442
+ extraction_confidence: document.getElementById("voice-confirm-confidence")?.textContent?.replace("Confidence: ", "") || "medium",
2443
+ delivery_observations: {},
2444
+ });
2445
  showScreen("setup");
2446
  });
2447
  document.getElementById("btn-voice-turn-send").addEventListener("click", () => {
 
2459
  document.getElementById("btn-start-battle").addEventListener("click", startSession);
2460
  document.getElementById("btn-end-battle").addEventListener("click", endBattle);
2461
  document.getElementById("btn-reset").addEventListener("click", resetBattle);
2462
+ document.getElementById("btn-back-setup")?.addEventListener("click", () => showScreen("setup"));
2463
  document.getElementById("btn-path-to-80").addEventListener("click", openPath80);
2464
  document.getElementById("btn-close-path80").addEventListener("click", closePath80);
2465
  document.getElementById("btn-close-path80-bottom").addEventListener("click", closePath80);
2466
  document.getElementById("btn-retry-question")?.addEventListener("click", openRetryFromPath80);
2467
+ document.getElementById("btn-scorecard-retry")?.addEventListener("click", startRetryDrill);
2468
+ document.getElementById("btn-prep-retry")?.addEventListener("click", startRetryDrill);
2469
+ document.getElementById("btn-answers-retry")?.addEventListener("click", startRetryDrill);
2470
+ document.getElementById("btn-answers-new-battle")?.addEventListener("click", resetBattle);
2471
  document.getElementById("path80-overlay").addEventListener("click", (e) => {
2472
  if (e.target === e.currentTarget) closePath80();
2473
  });
 
2616
  console.log("PitchFight frontend booting...");
2617
  setGlobalLoading(false);
2618
  hideErrorBanner();
2619
+ setBriefingMode("quick");
2620
+ initBriefPreviewEditors();
2621
  initLandingIntro();
2622
+ initScorecardTabs();
2623
 
2624
  initVoiceUI({
2625
  getSessionId: () => state.sessionId,
2626
  getUiMode: () => state.uiMode,
2627
  onPitchComplete: (data) => {
 
2628
  fillVoiceExtractForm(data);
2629
+ applyVoicePitchToBriefing(data);
2630
+ showScreen("setup");
2631
  hideErrorBanner();
2632
  },
2633
  onTurnComplete: (data) => showVoiceTurnPreview(data),
frontend/styles.css CHANGED
The diff for this file is too large to render. See raw diff