Update UI
Browse files- app.py +6 -0
- core/api_handlers.py +227 -0
- core/model_router.py +29 -0
- core/nvidia_client.py +14 -0
- frontend/index.html +274 -132
- frontend/script.js +605 -112
- frontend/styles.css +0 -0
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">
|
| 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-
|
| 327 |
-
<
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
</div>
|
| 343 |
-
<
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
</div>
|
| 348 |
-
<
|
| 349 |
-
<
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
</div>
|
| 352 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
</div>
|
| 354 |
|
| 355 |
<div class="panel glass briefing-panel briefing-opponent-panel">
|
|
@@ -391,7 +499,10 @@
|
|
| 391 |
</button>
|
| 392 |
</div>
|
| 393 |
</div>
|
| 394 |
-
<
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 637 |
-
<
|
| 638 |
-
<
|
| 639 |
-
<div class="
|
| 640 |
-
<
|
| 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="
|
| 655 |
-
<p id="
|
| 656 |
-
<
|
| 657 |
-
|
| 658 |
-
<span class="
|
| 659 |
-
<span class="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
</div>
|
| 661 |
</div>
|
|
|
|
|
|
|
|
|
|
| 662 |
</div>
|
|
|
|
| 663 |
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
<p id="pitch-next-action-text" class="result-next-action-text"></p>
|
| 667 |
-
</div>
|
| 668 |
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
<
|
| 672 |
-
<
|
| 673 |
-
</div>
|
| 674 |
-
</article>
|
| 675 |
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
<
|
| 686 |
-
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 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 |
-
<
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 716 |
</div>
|
| 717 |
-
|
| 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 |
-
|
| 725 |
-
|
| 726 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
<
|
| 731 |
-
<div class="
|
| 732 |
-
<span
|
| 733 |
-
<
|
| 734 |
</div>
|
| 735 |
-
<
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
</div>
|
| 742 |
-
<p id="
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
<
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
</div>
|
| 749 |
-
</section>
|
| 750 |
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 326 |
const fill = document.getElementById("confidence-meter-fill");
|
| 327 |
if (!fill) return;
|
| 328 |
-
const confidence = Math.max(8, Math.min(
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
|
|
|
| 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
|
| 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 ? "
|
| 1078 |
-
chipSource.hidden =
|
| 1079 |
-
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
});
|
| 1112 |
}
|
| 1113 |
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
.
|
| 1122 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 = `
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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-
|
| 1931 |
-
document.getElementById("btn-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 1949 |
document.getElementById("btn-voice-edit-manual").addEventListener("click", () => {
|
| 1950 |
const form = document.getElementById("voice-extract-form");
|
| 1951 |
const data = new FormData(form);
|
| 1952 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
|
|