import { initVoiceUI } from "./voice.js"; const state = { sessionId: null, persona: "hackathon_judge", difficultyProfile: "practice", round: 1, scoreExplanation: null, startMode: "text", pendingVoicePitch: null, pendingVoiceTurn: null, retryDrill: null, pendingRetryVoiceTurn: null, judgeVerdict: null, dealContext: null, dealRound: 1, uiMode: "pitch", pendingDealVoiceTurn: null, negotiationTranscript: [], conversationLog: [], dealConversationLog: [], battleLog: [], dealBattleLog: [], battleMetaSnapshot: null, scorecardSnapshot: null, dealScorecardData: null, voiceEntrySource: "arena", briefingMode: "quick", briefStructured: false, pitchExtractionConfidence: null, pitchExtractionConfidenceScore: null, lastStructuredPitchText: null, lastStructuredPitchData: null, battleConfidencePct: null, }; let liveJudgeTurn = { text: "", meta: "" }; let liveFounderReply = ""; let liveDealJudgeTurn = { text: "", meta: "" }; let liveDealFounderReply = ""; const screens = { landing: document.getElementById("screen-landing"), startMethod: document.getElementById("screen-start-method"), voicePitch: document.getElementById("screen-voice-pitch"), voiceConfirm: document.getElementById("screen-voice-confirm"), setup: document.getElementById("screen-setup"), battle: document.getElementById("screen-battle"), scorecard: document.getElementById("screen-scorecard"), deal: document.getElementById("screen-deal"), dealScorecard: document.getElementById("screen-deal-scorecard"), }; const dealChatWindow = document.getElementById("deal-chat-window"); const dealInput = document.getElementById("deal-input"); const dealStatus = document.getElementById("deal-status"); const startupForm = document.getElementById("startup-form"); const briefPreviewForm = document.getElementById("brief-preview-form"); const quickPitchPanel = document.getElementById("panel-quick-pitch"); const briefPreviewPanel = document.getElementById("brief-preview-panel"); const briefingLeftCol = document.querySelector(".briefing-left-col"); const chatWindow = document.getElementById("chat-window"); const userInput = document.getElementById("user-input"); const loadingOverlay = document.getElementById("loading-overlay"); const loadingText = document.getElementById("loading-message"); const battleStatus = document.getElementById("battle-status"); const errorBanner = document.getElementById("error-banner"); function bindClick(id, handler) { document.getElementById(id)?.addEventListener("click", handler); } function showScreen(name) { Object.entries(screens).forEach(([key, el]) => { el.classList.toggle("active", key === name); }); const app = document.getElementById("app"); app?.classList.toggle("app-arena-fullwidth", name === "battle" || name === "deal" || name === "scorecard" || name === "dealScorecard"); app?.classList.toggle("app-scorecard-fullwidth", name === "scorecard"); if (name === "landing") { syncLandingViewport(); if (landingIntroComplete) { finalizeLandingIntroStatic(); } } } /* ---- Landing viewport sync (HF Spaces iframe-safe) ---- */ function syncLandingViewport() { const vh = window.innerHeight; if (!vh) return; document.documentElement.style.setProperty("--landing-vh", `${vh}px`); } function initLandingViewport() { syncLandingViewport(); window.addEventListener("resize", syncLandingViewport, { passive: true }); window.addEventListener("orientationchange", syncLandingViewport, { passive: true }); if (window.visualViewport) { window.visualViewport.addEventListener("resize", syncLandingViewport, { passive: true }); } } /* ---- Landing typewriter intro (Pass 1 — isolated, no API impact) ---- */ let landingIntroComplete = false; let landingIntroRunning = false; function prefersReducedMotion() { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } function finalizeLandingIntroStatic() { const line1Text = document.querySelector("#landing-type-line-1 .arena-type-text"); const line2Text = document.querySelector("#landing-type-line-2 .arena-type-text"); const line1 = document.getElementById("landing-type-line-1"); const line2 = document.getElementById("landing-type-line-2"); const landing = document.querySelector(".arena-landing"); if (line1Text?.dataset.text) line1Text.textContent = line1Text.dataset.text; if (line2Text?.dataset.text) line2Text.textContent = line2Text.dataset.text; line1?.classList.add("done"); line2?.classList.add("done"); document.getElementById("landing-typewriter")?.classList.add("intro-complete"); landing?.classList.add("intro-complete"); document.getElementById("landing-cta-row")?.classList.add("visible"); document.getElementById("landing-feature-chips")?.classList.add("visible"); landingIntroComplete = true; } function typeText(el, text, speedMs, onDone) { if (!el || !text) { onDone?.(); return; } el.textContent = ""; let i = 0; const step = () => { if (i < text.length) { el.textContent += text.charAt(i); i += 1; setTimeout(step, speedMs); } else { onDone?.(); } }; step(); } /* ---- Landing currency fall canvas ---- */ const PF_FALL_BILL_COUNT = 38; function initPfFallCanvas() { const canvas = document.getElementById("pfFallCanvas"); const landingScreen = document.getElementById("screen-landing"); const host = canvas?.parentElement; if (!canvas || !host || !landingScreen) return; const ctx = canvas.getContext("2d"); if (!ctx) return; let width = 0; let height = 0; let bills = []; let rafId = 0; let running = false; const reducedMotion = prefersReducedMotion(); const rand = (min, max) => min + Math.random() * (max - min); const roundRectPath = (c, x, y, w, h, r) => { const radius = Math.min(r, w / 2, h / 2); c.beginPath(); c.moveTo(x + radius, y); c.lineTo(x + w - radius, y); c.quadraticCurveTo(x + w, y, x + w, y + radius); c.lineTo(x + w, y + h - radius); c.quadraticCurveTo(x + w, y + h, x + w - radius, y + h); c.lineTo(x + radius, y + h); c.quadraticCurveTo(x, y + h, x, y + h - radius); c.lineTo(x, y + radius); c.quadraticCurveTo(x, y, x + radius, y); c.closePath(); }; const createBill = () => { const gold = Math.random() < 0.7; return { x: rand(0, width), y: rand(-30, height), scale: rand(0.5, 1.2), vy: rand(0.25, 0.8), vx: rand(-0.2, 0.2), vr: rand(-0.025, 0.025), alpha: rand(0.12, 0.35), rot: rand(0, Math.PI * 2), rgb: gold ? [245, 200, 66] : [0, 230, 200], strokeAlpha: gold ? 0.35 : 0.25, }; }; const drawBill = (bill) => { const w = 48 * bill.scale; const h = 20 * bill.scale; const [r, g, b] = bill.rgb; const inset = 3 * bill.scale; const radius = 3 * bill.scale; ctx.save(); ctx.translate(bill.x, bill.y); ctx.rotate(bill.rot); ctx.globalAlpha = bill.alpha; roundRectPath(ctx, -w / 2, -h / 2, w, h, radius); ctx.fillStyle = `rgba(${r},${g},${b},0.04)`; ctx.fill(); ctx.strokeStyle = `rgba(${r},${g},${b},${bill.strokeAlpha})`; ctx.lineWidth = 1; ctx.stroke(); roundRectPath(ctx, -w / 2 + inset, -h / 2 + inset, w - inset * 2, h - inset * 2, Math.max(1, radius - inset)); ctx.strokeStyle = `rgba(${r},${g},${b},0.2)`; ctx.stroke(); ctx.font = `${Math.max(8, 11 * bill.scale)}px monospace`; ctx.fillStyle = `rgba(${r},${g},${b},0.4)`; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText("₹", -w / 2 + inset + 2, 0); ctx.restore(); }; const drawFrame = () => { ctx.clearRect(0, 0, width, height); for (const bill of bills) { drawBill(bill); } }; const tick = () => { if (!running) return; ctx.clearRect(0, 0, width, height); for (const bill of bills) { bill.y += bill.vy; bill.x += bill.vx; bill.rot += bill.vr; if (bill.y > height + 30) { bill.y = -30; bill.x = rand(0, width); } drawBill(bill); } rafId = requestAnimationFrame(tick); }; const resize = () => { const rect = host.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; width = Math.max(1, rect.width); height = Math.max(1, rect.height); canvas.width = Math.floor(width * dpr); canvas.height = Math.floor(height * dpr); canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); bills = Array.from({ length: PF_FALL_BILL_COUNT }, () => createBill()); if (reducedMotion || !running) { drawFrame(); } }; const startAnimation = () => { if (reducedMotion || running) return; running = true; cancelAnimationFrame(rafId); rafId = requestAnimationFrame(tick); }; const stopAnimation = () => { running = false; cancelAnimationFrame(rafId); rafId = 0; }; const syncVisibility = () => { if (landingScreen.classList.contains("active")) { resize(); startAnimation(); } else { stopAnimation(); } }; resize(); window.addEventListener("resize", resize); const observer = new MutationObserver(syncVisibility); observer.observe(landingScreen, { attributes: true, attributeFilter: ["class"] }); if (landingScreen.classList.contains("active")) { if (reducedMotion) { drawFrame(); } else { startAnimation(); } } } function initLandingIntro() { if (landingIntroRunning || landingIntroComplete) return; landingIntroRunning = true; const line1Text = document.querySelector("#landing-type-line-1 .arena-type-text"); const line2Text = document.querySelector("#landing-type-line-2 .arena-type-text"); const line1 = document.getElementById("landing-type-line-1"); const line2 = document.getElementById("landing-type-line-2"); if (!line1Text || !line2Text) { landingIntroRunning = false; return; } if (prefersReducedMotion()) { finalizeLandingIntroStatic(); landingIntroRunning = false; return; } const text1 = line1Text.dataset.text || ""; const text2 = line2Text.dataset.text || ""; typeText(line1Text, text1, 32, () => { line1?.classList.add("done"); line2?.classList.add("active"); setTimeout(() => { typeText(line2Text, text2, 28, () => { line2?.classList.add("done"); setTimeout(() => { finalizeLandingIntroStatic(); landingIntroRunning = false; }, 180); }); }, 220); }); } function setGlobalLoading(isLoading, message = "Loading...") { if (loadingText) loadingText.textContent = message; loadingOverlay.hidden = !isLoading; } const NEMOTRON_SCORECARD_SOURCES = new Set(["nemotron_full", "nemotron", "nemotron_repaired"]); function isNemotronScorecardSource(src) { return NEMOTRON_SCORECARD_SOURCES.has(String(src ?? "").trim()); } function getScorecardSourceDisplay(data = {}) { const src = String(data.scorecard_source ?? "").trim().toLowerCase(); const provider = String(data.provider ?? "").trim().toLowerCase(); const modelOk = data.model_ok === true; if (isNemotronScorecardSource(src) || (modelOk && provider === "nvidia")) { return { show: true, chip: "⚡ Nemotron", badge: "Powered by NVIDIA Nemotron", title: "Scoring source: NVIDIA Nemotron", }; } return { show: false }; } const INTERNAL_ERROR_PATTERNS = [ /nvidia/i, /max_tokens/i, /reasoning model/i, /empty response/i, /mock fallback/i, /model scoring fallback/i, /local fallback/i, /server logs/i, /nemotron omni/i, /api_key/i, ]; function isInternalErrorMessage(message) { const text = String(message ?? "").trim(); if (!text) return true; return INTERNAL_ERROR_PATTERNS.some((pattern) => pattern.test(text)); } function showErrorBanner(message) { if (isInternalErrorMessage(message)) { console.warn("[PitchFight] suppressed internal error from UI:", message); return; } errorBanner.textContent = message; errorBanner.hidden = false; } function hideErrorBanner() { errorBanner.hidden = true; errorBanner.textContent = ""; } const BRIEF_FIELD_KEYS = [ "name", "target_users", "problem", "solution", "why_ai", "traction", "competitors", "ask", ]; function setBriefFieldValue(key, value) { const card = briefPreviewForm?.querySelector(`[data-field="${key}"]`); if (!card) return; const display = card.querySelector(".brief-read-value"); const input = card.querySelector(".brief-read-input"); const text = String(value ?? "").trim(); if (input) input.value = text; if (display) { display.textContent = text || "Not specified"; display.classList.toggle("is-empty", !text); } } let _briefEditSnapshot = ""; function closeBriefFieldExpanded(card) { if (!card) return; const input = card.querySelector(".brief-read-input"); const display = card.querySelector(".brief-read-value"); card.classList.remove("is-editing", "is-editing-expanded"); if (input) input.hidden = true; if (display) display.hidden = false; briefPreviewForm?.classList.remove("is-editing-active"); briefPreviewPanel?.classList.remove("is-editing-active"); } function commitBriefFieldEdit(card) { if (!card) return; const input = card.querySelector(".brief-read-input"); const display = card.querySelector(".brief-read-value"); const text = (input?.value || "").trim(); if (display) { display.textContent = text || "Not specified"; display.classList.toggle("is-empty", !text); } closeBriefFieldExpanded(card); syncBriefToStartupForm(); recomputeBriefConfidence(); } function cancelBriefFieldEdit(card) { if (!card) return; const input = card.querySelector(".brief-read-input"); if (input) input.value = _briefEditSnapshot; closeBriefFieldExpanded(card); } function startBriefFieldEdit(card) { if (!card) return; briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing-expanded").forEach((open) => { if (open !== card) cancelBriefFieldEdit(open); }); const input = card.querySelector(".brief-read-input"); const display = card.querySelector(".brief-read-value"); _briefEditSnapshot = input?.value ?? ""; card.classList.add("is-editing", "is-editing-expanded"); briefPreviewForm?.classList.add("is-editing-active"); briefPreviewPanel?.classList.add("is-editing-active"); if (display) display.hidden = true; if (input) { input.hidden = false; if (input.classList.contains("brief-read-textarea")) { input.rows = 5; } requestAnimationFrame(() => { input.focus(); if (typeof input.select === "function" && input.tagName === "INPUT") { input.select(); } else if (input.tagName === "TEXTAREA") { input.setSelectionRange(input.value.length, input.value.length); } card.scrollIntoView({ behavior: "smooth", block: "center" }); }); } } function initBriefPreviewEditors() { briefPreviewForm?.querySelectorAll(".brief-read-card").forEach((card) => { if (!card.querySelector(".brief-read-edit-actions")) { const actions = document.createElement("div"); actions.className = "brief-read-edit-actions"; actions.innerHTML = ` `; card.appendChild(actions); } card.querySelector(".brief-read-edit")?.addEventListener("click", (e) => { e.stopPropagation(); if (card.classList.contains("is-editing-expanded")) { commitBriefFieldEdit(card); } else { startBriefFieldEdit(card); } }); card.querySelector(".brief-read-done")?.addEventListener("mousedown", (e) => e.preventDefault()); card.querySelector(".brief-read-done")?.addEventListener("click", (e) => { e.stopPropagation(); commitBriefFieldEdit(card); }); card.querySelector(".brief-read-cancel")?.addEventListener("mousedown", (e) => e.preventDefault()); card.querySelector(".brief-read-cancel")?.addEventListener("click", (e) => { e.stopPropagation(); cancelBriefFieldEdit(card); }); card.querySelector(".brief-read-input")?.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); cancelBriefFieldEdit(card); } if (e.key === "Enter" && e.ctrlKey) { e.preventDefault(); commitBriefFieldEdit(card); } }); }); } function syncBriefToStartupForm() { if (!briefPreviewForm || !startupForm) return; BRIEF_FIELD_KEYS.forEach((key) => { const input = briefPreviewForm.querySelector(`[name="${key}"]`); const field = startupForm.elements.namedItem(key); if (field && input) field.value = input.value ?? ""; }); } function getStartupPayload() { if (state.briefStructured) { syncBriefToStartupForm(); } const data = new FormData(startupForm); return Object.fromEntries(data.entries()); } function fillStartupForm(startup) { Object.entries(startup).forEach(([key, value]) => { const field = startupForm?.elements.namedItem(key); if (field) field.value = value ?? ""; if (BRIEF_FIELD_KEYS.includes(key)) setBriefFieldValue(key, value); }); } function setBriefingMode(mode) { state.briefingMode = mode; const isQuick = mode === "quick"; const previewVisible = isQuick && state.briefStructured; document.getElementById("tab-quick-pitch")?.classList.toggle("active", isQuick); document.getElementById("tab-advanced-briefing")?.classList.toggle("active", !isQuick); document.getElementById("tab-quick-pitch")?.setAttribute("aria-selected", String(isQuick)); document.getElementById("tab-advanced-briefing")?.setAttribute("aria-selected", String(!isQuick)); briefingLeftCol?.classList.toggle("mode-quick", isQuick); briefingLeftCol?.classList.toggle("mode-advanced", !isQuick); briefingLeftCol?.classList.toggle("mode-structured", previewVisible); const advancedPanel = document.getElementById("panel-advanced-briefing"); if (advancedPanel) advancedPanel.hidden = isQuick; if (isQuick) { if (quickPitchPanel) quickPitchPanel.hidden = previewVisible; if (briefPreviewPanel) briefPreviewPanel.hidden = !previewVisible; } else { if (quickPitchPanel) quickPitchPanel.hidden = true; if (briefPreviewPanel) briefPreviewPanel.hidden = true; syncBriefToStartupForm(); } const subtitle = document.getElementById("briefing-subtitle"); if (subtitle) { subtitle.textContent = isQuick ? (previewVisible ? "Review your structured brief, then pick opponent and start." : "Pitch naturally. We'll structure it before the judge attacks it.") : "Want full control? Edit every field manually."; } } function showBriefPreview(meta = {}) { if (quickPitchPanel) quickPitchPanel.hidden = true; if (briefPreviewPanel) briefPreviewPanel.hidden = false; briefingLeftCol?.classList.add("mode-structured"); const hintEl = document.getElementById("brief-preview-hint"); if (hintEl) { if (meta.source === "local_fallback") { hintEl.textContent = "Quick structure applied — tap ✎ on any field to edit."; hintEl.hidden = false; } else if (Array.isArray(meta.missing_fields) && meta.missing_fields.length) { hintEl.textContent = `Not in your pitch: ${meta.missing_fields.join(", ")}`; hintEl.hidden = false; } else { hintEl.hidden = true; hintEl.textContent = ""; } } } function hideBriefPreview() { state.briefStructured = false; if (quickPitchPanel) quickPitchPanel.hidden = false; if (briefPreviewPanel) briefPreviewPanel.hidden = true; briefingLeftCol?.classList.remove("mode-structured"); document.getElementById("brief-preview-hint")?.setAttribute("hidden", ""); briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing").forEach((card) => { commitBriefFieldEdit(card); }); } function extractionConfidenceLevel(meta = {}) { const raw = meta.confidence ?? meta.extraction_confidence ?? ""; const key = String(raw).toLowerCase(); if (key === "high" || key === "medium" || key === "low") return key; return null; } function confidenceFromStartupFields(startup = {}) { const fields = ["name", "problem", "solution", "why_ai", "target_users", "traction", "competitors", "ask"]; const filled = fields.filter((field) => String(startup?.[field] ?? "").trim()).length; if (filled >= 5) return "high"; if (filled >= 3) return "medium"; return "low"; } // JS mirror of Python calculate_structure_confidence — same weights, caps, regexes. const _CONF_FIELD_WEIGHTS = { name:10, problem:15, target_users:12, solution:15, why_ai:10, traction:15, competitors:8, ask:10 }; const _CONF_FIELD_CAPS = [["problem",60],["solution",60],["target_users",70],["traction",74],["competitors",92],["why_ai",90],["ask",85]]; const _CONF_FILLER = new Set([ "not specified","n/a","none","unknown","tbd","-","", "idk","i don't know","i dont know","not sure","na","no idea", "dunno","nothing","?","??","???","yes","no","nope","yep", "to be determined","to be decided","will update","coming soon", ]); // Minimum word count for description fields — single-word noise like "idk" fails this. const _CONF_MIN_WORDS = { problem:2, solution:2, why_ai:2, traction:2, target_users:2 }; const _CONF_USER_SEG_RE = /\b(college students?|university students?|indie developers?|small businesses?|enterprise|founders?|educators?|teachers?|researchers?|professionals?|teams?|parents?|teenagers?|consumers?|startup founders?)\b/i; const _CONF_CONCRETE_ASK_RE = /(\$[\d,]+[kKmM]?|\d+[kK]\s*(?:usd|dollars?)?|mentorship|campus pilot|equity partner|co.?founder|sponsorship|strategic partner)/i; function _calcConfidenceFromCtx(ctx, rawText) { let score = 0; const missing = []; for (const [field, weight] of Object.entries(_CONF_FIELD_WEIGHTS)) { const val = String(ctx[field] || "").trim(); const valLower = val.toLowerCase(); const minWords = _CONF_MIN_WORDS[field] ?? 1; const filled = val && !_CONF_FILLER.has(valLower) && val.split(/\s+/).length >= minWords; if (filled) score += weight; else missing.push(field); } const text = String(rawText || "").trim(); let bonus = 0; const numHits = (text.match(/\b\d[\d,]*\b/g) || []).length; if (numHits >= 3) bonus += 10; else if (numHits >= 1) bonus += 5; if (_CONF_USER_SEG_RE.test(text)) bonus += 5; if (_CONF_CONCRETE_ASK_RE.test(text)) bonus += 5; bonus = Math.min(bonus, 20); score = Math.min(score + bonus, 100); for (const [field, cap] of _CONF_FIELD_CAPS) { if (missing.includes(field)) score = Math.min(score, cap); } score = Math.max(0, Math.min(100, score)); return { confidence: score >= 75 ? "high" : score >= 45 ? "medium" : "low", confidence_score: score, missing_fields: missing }; } function recomputeBriefConfidence() { if (!briefPreviewForm) return; const ctx = {}; for (const key of BRIEF_FIELD_KEYS) { const inp = briefPreviewForm.querySelector(`[name="${key}"]`); ctx[key] = (inp?.value || "").trim(); } const result = _calcConfidenceFromCtx(ctx, state.lastStructuredPitchText || ""); state.pitchExtractionConfidence = result.confidence; state.pitchExtractionConfidenceScore = result.confidence_score; const hintEl = document.getElementById("brief-preview-hint"); if (hintEl) { if (result.missing_fields.length) { hintEl.textContent = `Not in your pitch: ${result.missing_fields.join(", ")}`; hintEl.hidden = false; } else { hintEl.hidden = true; } } } function confidenceLevelToPct(level) { const key = String(level || "medium").toLowerCase(); if (key === "high") return 76; if (key === "low") return 22; return 50; } function syncPitchExtractionConfidence(meta = {}, startup = null) { const level = extractionConfidenceLevel(meta) || confidenceFromStartupFields(startup) || "medium"; state.pitchExtractionConfidence = level; // Prefer numeric score from backend (0-100) so battle HUD has a precise baseline. state.pitchExtractionConfidenceScore = typeof meta.confidence_score === "number" ? meta.confidence_score : confidenceLevelToPct(level); return level; } function fillBriefPreview(startup, meta = {}) { state.briefStructured = true; syncPitchExtractionConfidence(meta, startup); fillStartupForm(startup); syncBriefToStartupForm(); showBriefPreview(meta); } function resetSetupScreen() { state.briefStructured = false; state.pitchExtractionConfidence = null; state.pitchExtractionConfidenceScore = null; state.lastStructuredPitchText = null; state.lastStructuredPitchData = null; setBriefingMode("quick"); hideBriefPreview(); const quickText = document.getElementById("quick-pitch-text"); if (quickText) quickText.value = ""; state.pendingVoicePitch = null; state.startMode = "text"; } async function structurePitch() { const pitchText = document.getElementById("quick-pitch-text")?.value?.trim(); if (!pitchText) { showErrorBanner("Type or paste your pitch first."); return; } // Return cached result when the same pitch text is re-submitted — confidence must not // change on repeated clicks for the same input (Part C: stable re-structure). if (state.lastStructuredPitchText === pitchText && state.lastStructuredPitchData) { fillBriefPreview(state.lastStructuredPitchData.startup_context, state.lastStructuredPitchData); hideErrorBanner(); return; } try { setGlobalLoading(true, "Structuring your pitch…"); state.startMode = "text"; const data = await apiPost("/api/structure-pitch", { pitch_text: pitchText }); if (!data.ok) { showErrorBanner(data.error || "Could not structure your pitch. Try Advanced Briefing."); return; } state.lastStructuredPitchText = pitchText; state.lastStructuredPitchData = data; fillBriefPreview(data.startup_context, data); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("We couldn't structure that pitch. Try Advanced Briefing or edit manually."); } finally { setGlobalLoading(false); } } function applyVoicePitchToBriefing(data) { state.pendingVoicePitch = data; state.startMode = "voice"; const extracted = data.extracted ?? {}; fillBriefPreview(extracted, { brief_summary: (data.transcript || "").slice(0, 240), confidence: data.extraction_confidence || "medium", }); const quickText = document.getElementById("quick-pitch-text"); if (quickText && data.transcript) quickText.value = data.transcript; setBriefingMode("quick"); } const PERSONA_LABELS = { skeptical_vc: "Skeptical VC", technical_judge: "Technical Judge", hackathon_judge: "Hackathon Judge", }; const DIFFICULTY_LABELS = { practice: "Practice Mode", judge: "Judge Mode", investor: "Investor Mode", }; function pressureMeterLevel(data) { const label = String(data.pressure_label ?? data.pressure_level ?? "").toLowerCase(); if (label.includes("extreme")) return { pct: 100, tier: "extreme" }; if (label.includes("high")) return { pct: 78, tier: "high" }; if (label.includes("focused") || label.includes("medium")) return { pct: 52, tier: "focused" }; if (label.includes("warm")) return { pct: 28, tier: "warmup" }; const phase = String(data.battle_phase ?? "").toLowerCase(); if (phase.includes("extreme") || phase.includes("pressure")) return { pct: 78, tier: "high" }; if (phase.includes("challenge")) return { pct: 52, tier: "focused" }; const round = Number(data.round ?? state.round ?? 1); if (round >= 4) return { pct: 100, tier: "extreme" }; if (round === 3) return { pct: 78, tier: "high" }; if (round === 2) return { pct: 52, tier: "focused" }; return { pct: 28, tier: "warmup" }; } const ATTACK_PROGRESSION = [ "User Pain", "Novelty", "MVP Strength", "Business Model", "Objection Handling", ]; function normalizeAttackKey(tag) { return String(tag || "").trim().toLowerCase(); } function updateProgressStrip(round, attack, mode = "battle") { const roundStrip = document.getElementById( mode === "deal" ? "deal-progress-rounds" : "battle-progress-rounds", ); if (roundStrip) { roundStrip.querySelectorAll(".progress-node").forEach((node) => { const n = Number(node.dataset.round); node.classList.remove("progress-node-active", "progress-node-done"); if (n < round) node.classList.add("progress-node-done"); else if (n === round) node.classList.add("progress-node-active"); }); } if (mode !== "battle") return; const attackStrip = document.getElementById("battle-progress-attacks"); if (!attackStrip) return; const key = normalizeAttackKey(attack); let activeIdx = ATTACK_PROGRESSION.findIndex((a) => { const ak = normalizeAttackKey(a); return key === ak || key.includes(ak.split(" ")[0]) || ak.includes(key); }); if (activeIdx < 0 && key) activeIdx = Math.min(round - 1, ATTACK_PROGRESSION.length - 1); attackStrip.querySelectorAll(".progress-attack-tag").forEach((tag, idx) => { tag.classList.remove("progress-attack-active", "progress-attack-done"); if (activeIdx >= 0 && idx < activeIdx) tag.classList.add("progress-attack-done"); if (activeIdx >= 0 && idx === activeIdx) tag.classList.add("progress-attack-active"); }); } function addBattleLogEntry(roundNum, judge, founderReply, mode = "battle") { const log = mode === "deal" ? state.dealBattleLog : state.battleLog; const entry = { roundNum, judge: { ...judge }, founderReply }; log.push(entry); const ribbonId = mode === "deal" ? "deal-log-ribbon" : "battle-log-ribbon"; const tabsId = mode === "deal" ? "deal-log-tabs" : "battle-log-tabs"; const detailId = mode === "deal" ? "deal-log-detail" : "battle-log-detail"; const ribbon = document.getElementById(ribbonId); const tabs = document.getElementById(tabsId); if (!ribbon || !tabs) return; /* Ribbon stays hidden — log only opens via drawer */ const btn = document.createElement("button"); btn.type = "button"; btn.className = "battle-log-tab"; btn.textContent = mode === "deal" ? `D${roundNum}` : `R${roundNum}`; btn.dataset.round = roundNum; btn.addEventListener("click", () => { tabs.querySelectorAll(".battle-log-tab").forEach((t) => t.classList.remove("active")); btn.classList.add("active"); const detail = document.getElementById(detailId); if (!detail) return; detail.hidden = false; detail.innerHTML = `

${escapeHtml(simplifyJudgeMeta(judge.meta) || "—")}

Judge

${escapeHtml(judge.text)}

${founderReply ? `

You

${escapeHtml(founderReply)}

` : ""}`; }); tabs.appendChild(btn); } function updateJudgeAttackPill(attack, mode = "battle") { const pill = document.getElementById(mode === "deal" ? "deal-judge-attack-pill" : "judge-attack-pill"); if (pill) pill.textContent = attack && attack !== "—" ? attack : "Pressing"; } function simplifyJudgeMeta(meta) { if (!meta) return ""; const attack = String(meta).split("·")[0]?.trim(); return attack || meta; } function updatePressureCore(tier, label) { const ring = document.querySelector(".duel-node-ring"); if (ring) { ring.className = `duel-node-ring pressure-core-${tier}`; } const lbl = document.getElementById("duel-pressure-label"); if (lbl && label) lbl.textContent = label; const judgePressure = document.getElementById("judge-stat-pressure"); if (judgePressure && label) judgePressure.textContent = label; } function renderConfidenceMeter(confidencePct) { const fill = document.getElementById("confidence-meter-fill"); if (!fill) return; const confidence = Math.max(8, Math.min(92, Math.round(confidencePct))); fill.style.width = `${confidence}%`; fill.classList.toggle("confidence-low", confidence < 35); fill.classList.toggle("confidence-mid", confidence >= 35 && confidence < 65); fill.classList.toggle("confidence-high", confidence >= 65); } function refreshBattleConfidenceFromPressure(pressurePct, round = 1) { // Use the numeric confidence_score (0-100) from structure-pitch when available so // the battle readiness meter reflects actual brief quality, not just high/mid/low buckets. const base = state.pitchExtractionConfidenceScore != null ? Math.max(25, Math.min(95, state.pitchExtractionConfidenceScore)) : confidenceLevelToPct(state.pitchExtractionConfidence || "medium"); const pressureDrag = Math.round(Number(pressurePct) * 0.25); const roundDrag = Math.max(0, Number(round) - 1) * 2; const ceiling = Math.max(8, base - pressureDrag - roundDrag); if (state.battleConfidencePct == null) { state.battleConfidencePct = ceiling; } else { state.battleConfidencePct = Math.min(state.battleConfidencePct, ceiling); } renderConfidenceMeter(state.battleConfidencePct); } function adjustBattleConfidenceFromAnswer(quality) { const deltas = { strong: 8, partial: -2, weak: -12, non_answer: -20 }; const delta = deltas[String(quality || "").toLowerCase()] ?? 0; if (!delta) return; const fallbackBase = state.pitchExtractionConfidenceScore != null ? state.pitchExtractionConfidenceScore : confidenceLevelToPct(state.pitchExtractionConfidence); state.battleConfidencePct = Math.max( 8, Math.min(92, (state.battleConfidencePct ?? fallbackBase) + delta), ); renderConfidenceMeter(state.battleConfidencePct); } function updateComboMeter(round) { const meter = document.getElementById("combo-meter"); if (!meter) return; const streak = Math.max(0, Math.min(5, Number(round) - 1)); meter.querySelectorAll(".combo-pip").forEach((pip) => { const n = Number(pip.dataset.pip); pip.classList.toggle("combo-pip-lit", n <= streak); }); } function updateJudgeSignalChips(attack, pressureLabel) { const focus = document.getElementById("judge-stat-focus"); if (focus) focus.textContent = attack && attack !== "—" ? attack : "Scanning"; const pressure = document.getElementById("judge-stat-pressure"); if (pressure && pressureLabel) pressure.textContent = pressureLabel; const judgeName = document.getElementById("judge-fighter-name"); const sidebarName = document.getElementById("sidebar-persona-name"); const persona = PERSONA_LABELS[state.persona] ?? "AI Judge"; if (judgeName) judgeName.textContent = persona; if (sidebarName) sidebarName.textContent = persona; } function updateJudgeLiveCard(text, meta) { const q = document.getElementById("judge-question-text"); const metaEl = document.getElementById("judge-question-meta"); const card = document.getElementById("judge-live-card"); const attack = simplifyJudgeMeta(meta); if (q) q.textContent = text || "AI judge is preparing the next attack…"; if (metaEl) metaEl.textContent = attack; updateJudgeAttackPill(attack, "battle"); const pressureLbl = document.getElementById("judge-stat-pressure")?.textContent; updateJudgeSignalChips(attack !== "—" ? attack : simplifyJudgeMeta(meta), pressureLbl); if (card) { card.classList.remove("judge-attack-enter"); void card.offsetWidth; card.classList.add("judge-attack-enter"); } } function updateDealJudgeCard(text, meta) { const q = document.getElementById("deal-judge-text"); const metaEl = document.getElementById("deal-judge-meta"); const card = document.getElementById("deal-judge-card"); if (q) q.textContent = text || "Preparing deal terms…"; if (metaEl) metaEl.textContent = simplifyJudgeMeta(meta); const attack = simplifyJudgeMeta(meta); updateJudgeAttackPill(attack !== "—" ? attack : "Terms", "deal"); if (card) { card.classList.remove("judge-attack-enter"); void card.offsetWidth; card.classList.add("judge-attack-enter"); } } function refreshCoachBar() { const dockHint = document.getElementById("dock-assist-hint"); const coach = (document.getElementById("micro-coach")?.textContent || "").trim(); const hintRaw = (document.getElementById("answer-hint")?.textContent || "").trim(); if (dockHint) { if (coach) { dockHint.textContent = coach; } else if (hintRaw) { dockHint.textContent = `Tip: ${hintRaw}`; } else { dockHint.textContent = "Tip: use numbers"; } } const bar = document.getElementById("battle-coach-bar"); if (bar) { bar.hidden = true; bar.textContent = ""; } } function extractRoundFromMeta(meta) { const match = String(meta || "").match(/Round\s+(\d+)/i); return match ? match[1] : null; } function archiveBattleRound(_container, judge, founderReply, mode = "battle") { if (!judge.text) return; const roundNum = extractRoundFromMeta(judge.meta) ?? Math.max(1, (state.round || 1) - 1); addBattleLogEntry(roundNum, judge, founderReply, mode); updateTimelineVisibility(mode, mode === "deal" ? "deal-timeline-count" : "timeline-count"); } function updateTimelineVisibility(mode = "battle", countId) { const count = mode === "deal" ? (state.dealBattleLog?.length ?? 0) : (state.battleLog?.length ?? 0); const countEl = document.getElementById(countId); if (countEl) countEl.textContent = count ? `(${count})` : ""; const toggleId = mode === "deal" ? "btn-open-deal-rounds" : "btn-open-battle-rounds"; const toggle = document.getElementById(toggleId); if (toggle) toggle.hidden = count === 0; } const CONV_FOUNDER_AVATAR = ``; const CONV_JUDGE_AVATAR = ``; function renderConversationMessage(role, text, meta = "") { const isJudge = role === "ai"; const speaker = isJudge ? (PERSONA_LABELS[state.persona] ?? "AI Judge") : "You · Founder"; const row = document.createElement("div"); row.className = `conv-message ${isJudge ? "conv-message-judge" : "conv-message-founder"}`; row.innerHTML = `
${isJudge ? CONV_JUDGE_AVATAR : CONV_FOUNDER_AVATAR}
${escapeHtml(speaker)} ${meta ? `${escapeHtml(meta)}` : ""}

${escapeHtml(text)}

`; return row; } function renderConversationThread(log, container) { if (!container) return; container.innerHTML = ""; (log || []).forEach(({ role, text, meta }) => { container.appendChild(renderConversationMessage(role, text, meta)); }); container.scrollTop = container.scrollHeight; } function renderConversationSidebar(context = "battle") { const sidebar = document.getElementById("conversation-sidebar"); if (!sidebar) return; const meta = state.battleMetaSnapshot ?? {}; const score = state.scorecardSnapshot; const persona = PERSONA_LABELS[meta.persona ?? state.persona] ?? "AI Judge"; const rounds = state.battleLog?.length ?? Math.max(0, Math.floor((state.conversationLog?.length ?? 0) / 2)); const attacks = ATTACK_PROGRESSION.map((label) => { const done = state.battleLog?.some((e) => simplifyJudgeMeta(e.judge?.meta) === label); const active = meta.attack === label; return `
  • ${escapeHtml(label)}
  • `; }).join(""); sidebar.innerHTML = ` ${context === "scorecard" && score ? ` ` : ""} `; } function openBattleConversationLog(fromScorecard = false) { renderConversationSidebar(fromScorecard ? "scorecard" : "battle"); renderConversationThread(state.conversationLog, chatWindow); openRoundsDrawer("battle-rounds-drawer"); document.getElementById("conversation-sidebar")?.scrollTo(0, 0); chatWindow?.scrollTo(0, chatWindow.scrollHeight); } function openRoundsDrawer(drawerId) { const drawer = document.getElementById(drawerId); if (!drawer) return; drawer.hidden = false; document.body.classList.add("rounds-drawer-open"); } function closeRoundsDrawer(drawerId) { const drawer = document.getElementById(drawerId); if (!drawer) return; drawer.hidden = true; if (!document.querySelector(".previous-rounds-drawer:not([hidden])")) { document.body.classList.remove("rounds-drawer-open"); } } function appendFounderBubble(container, text, meta = "", extraClass = "") { const bubble = document.createElement("div"); bubble.className = `message message-founder user${extraClass ? ` ${extraClass}` : ""}`; const preview = text.length > 160 ? `${text.slice(0, 157)}…` : text; bubble.innerHTML = meta ? `${escapeHtml(meta)}

    ${escapeHtml(preview)}

    ` : `

    ${escapeHtml(preview)}

    `; bubble.title = text; container.appendChild(bubble); container.scrollTop = container.scrollHeight; } function rebuildConversationView() { openBattleConversationLog(true); } function resetLiveTurnTracking() { liveJudgeTurn = { text: "", meta: "" }; liveFounderReply = ""; liveDealJudgeTurn = { text: "", meta: "" }; liveDealFounderReply = ""; } function appendMessage(role, text, meta = "") { state.conversationLog.push({ role, text, meta }); if (role === "ai") { if (liveJudgeTurn.text) { archiveBattleRound(null, liveJudgeTurn, liveFounderReply, "battle"); liveFounderReply = ""; } liveJudgeTurn = { text, meta }; updateJudgeLiveCard(text, meta); updateTimelineVisibility("battle", "timeline-count"); return; } liveFounderReply = text; updateTimelineVisibility("battle", "timeline-count"); } function appendDealMessage(role, text, meta = "") { if (!dealChatWindow) return; state.dealConversationLog.push({ role, text, meta }); if (role === "ai") { dealChatWindow?.querySelector(".live-pending")?.remove(); if (liveDealJudgeTurn.text) { archiveBattleRound(null, liveDealJudgeTurn, liveDealFounderReply, "deal"); liveDealFounderReply = ""; } liveDealJudgeTurn = { text, meta }; updateDealJudgeCard(text, meta); updateTimelineVisibility("deal", "deal-timeline-count"); return; } liveDealFounderReply = text; dealChatWindow?.querySelector(".live-pending")?.remove(); updateTimelineVisibility("deal", "deal-timeline-count"); } // "Minimum viable answer" recipe shown above the input for the current question. function setAnswerHint(hint) { const el = document.getElementById("answer-hint"); if (!el) return; el.textContent = (hint || "").trim(); el.hidden = true; refreshCoachBar(); } // Gentle after-round nudge — shown only in the response dock hint line. function setMicroCoach(tip) { const el = document.getElementById("micro-coach"); if (!el) return; el.textContent = (tip || "").trim(); el.hidden = true; refreshCoachBar(); } function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function updateBattleMeta(data) { const round = data.round ?? state.round ?? 1; document.getElementById("round-counter").textContent = round; const roundDisplay = document.getElementById("round-display"); if (roundDisplay) roundDisplay.textContent = String(round).padStart(2, "0"); const roundDuel = document.getElementById("round-counter-duel"); if (roundDuel) roundDuel.textContent = String(round).padStart(2, "0"); state.round = round; const pressureDisplay = data.pressure_label ?? data.pressure_level ?? "Warm-up"; const pressureEl = document.getElementById("pressure-level"); if (pressureEl) { pressureEl.textContent = pressureDisplay; const { tier } = pressureMeterLevel(data); pressureEl.className = `pressure-${tier}`; } const { pct, tier } = pressureMeterLevel(data); const fill = document.getElementById("pressure-meter-fill"); if (fill) { fill.style.width = `${pct}%`; fill.className = `pressure-meter-fill pressure-${tier}`; } refreshBattleConfidenceFromPressure(pct, round); updateComboMeter(round); const progressFill = document.getElementById("battle-progress-fill"); if (progressFill) { progressFill.style.width = `${pct}%`; progressFill.className = `battle-progress-fill pressure-${tier}`; } const attack = data.attack_tag ?? "—"; document.getElementById("attack-tag").textContent = attack; const attackChip = document.getElementById("attack-tag-chip"); if (attackChip) attackChip.textContent = attack; const pressureChip = document.getElementById("pressure-chip"); if (pressureChip) pressureChip.textContent = pressureDisplay; const diffLabel = data.difficulty_label ?? "Practice Mode"; const diffLabelEl = document.getElementById("difficulty-label"); if (diffLabelEl) diffLabelEl.textContent = diffLabel; const modeChip = document.getElementById("mode-chip"); if (modeChip) modeChip.textContent = diffLabel.replace(" Mode", ""); const phaseEl = document.getElementById("battle-phase"); if (phaseEl && data.battle_phase) phaseEl.textContent = data.battle_phase; const sidebarMode = document.getElementById("sidebar-persona-mode"); if (sidebarMode) sidebarMode.textContent = diffLabel; const sidebarPersona = document.getElementById("sidebar-persona-name"); if (sidebarPersona) { sidebarPersona.textContent = PERSONA_LABELS[state.persona] ?? "AI Judge"; } updateProgressStrip(round, attack, "battle"); updateJudgeAttackPill(attack, "battle"); updatePressureCore(tier, pressureDisplay); updateJudgeSignalChips(attack, pressureDisplay); state.battleMetaSnapshot = { round, attack, pressure: pressureDisplay, mode: diffLabel.replace(" Mode", ""), persona: state.persona, }; } function updateBattleReadiness(data) { const prompt = document.getElementById("battle-readiness-prompt"); const text = document.getElementById("battle-readiness-text"); if (!prompt) return; const show = data.soft_round_limit_reached || data.recommended_action === "end_battle"; if (!show) { prompt.hidden = true; return; } if (text) { text.textContent = data.completion_message ?? data.readiness?.reason ?? "Enough signal collected. Ready for your scorecard?"; } prompt.hidden = false; } function hideBattleReadiness() { document.getElementById("battle-readiness-prompt")?.setAttribute("hidden", ""); } async function apiPost(path, body = undefined) { console.log(`API POST ${path}`, body ?? {}); const options = { method: "POST", headers: {} }; if (body !== undefined) { options.headers["Content-Type"] = "application/json"; options.body = JSON.stringify(body); } const response = await fetch(path, options); const data = await response.json().catch(() => ({})); if (!response.ok) { const detail = data.detail || data.error || response.statusText; throw new Error(`${path} failed: ${detail}`); } console.log(`API POST ${path} OK`, data); return data; } export async function loadSample() { try { setGlobalLoading(true, "Loading demo founder…"); const data = await apiPost("/api/load-sample"); state.startMode = "text"; setBriefingMode("quick"); fillBriefPreview(data.startup, { brief_summary: data.startup?.solution || data.startup?.problem || "Demo founder loaded.", confidence: "high", }); const quickText = document.getElementById("quick-pitch-text"); if (quickText) { quickText.value = [ data.startup?.name, data.startup?.problem, data.startup?.solution, data.startup?.traction, data.startup?.ask, ].filter(Boolean).join(" "); } showScreen("setup"); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Failed to load demo startup. Check backend logs."); } finally { setGlobalLoading(false); } } export async function startSession() { try { if (state.briefingMode === "quick" && !state.briefStructured) { showErrorBanner("Structure your pitch first, or switch to Advanced Briefing."); return; } setGlobalLoading(true, "AI judge is preparing the first attack…"); battleStatus.hidden = true; hideBattleReadiness(); chatWindow.innerHTML = ""; state.conversationLog = []; state.battleLog = []; resetLiveTurnTracking(); document.getElementById("battle-log-tabs") && (document.getElementById("battle-log-tabs").innerHTML = ""); document.getElementById("battle-log-detail")?.setAttribute("hidden", ""); document.getElementById("battle-log-ribbon")?.setAttribute("hidden", ""); document.getElementById("btn-open-battle-rounds")?.setAttribute("hidden", ""); document.getElementById("battle-rounds-drawer")?.setAttribute("hidden", ""); document.getElementById("battle-coach-bar")?.setAttribute("hidden", ""); document.getElementById("voice-turn-preview")?.setAttribute("hidden", ""); if (!state.pitchExtractionConfidence) { syncPitchExtractionConfidence({}, getStartupPayload()); } state.battleConfidencePct = null; const payload = { mode: "pitch_battle", startup: getStartupPayload(), persona: state.persona, difficulty_profile: state.difficultyProfile, difficulty: state.difficultyProfile, // keep for backward compat input_mode: state.startMode === "voice" ? "voice" : "text", model_mode: "premium_nvidia", }; if (state.startMode === "voice" && state.pendingVoicePitch) { payload.voice_pitch = { transcript: state.pendingVoicePitch.transcript, delivery_observations: state.pendingVoicePitch.delivery_observations, extraction_confidence: state.pendingVoicePitch.extraction_confidence, }; } const data = await apiPost("/api/start-session", payload); if (data.error) { showErrorBanner(data.error); return; } state.sessionId = data.session_id; state.round = data.round ?? 1; state.uiMode = "pitch"; userInput.disabled = false; const submitBtn = document.getElementById("chat-form").querySelector("button[type=submit]"); if (submitBtn) submitBtn.disabled = false; updateBattleMeta(data); const startMeta = data.model_ok ? `${data.attack_tag} · Round ${data.round} · ⚡ Premium Nemotron` : `${data.attack_tag} · Round ${data.round}`; if (data.model_error) { console.warn("Opponent model fallback (not shown to user):", data.model_error); } appendMessage("ai", data.ai_message, startMeta); setMicroCoach(""); setAnswerHint(data.answer_hint); showScreen("battle"); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Failed to start battle. Check backend logs."); } finally { setGlobalLoading(false); } } export async function sendMessage(messageOverride, voiceMeta = null) { const message = (messageOverride ?? userInput.value).trim(); if (!message || !state.sessionId) return; try { setGlobalLoading(true, "Scoring your answer…"); userInput.value = ""; appendMessage("user", message); const chatPayload = { session_id: state.sessionId, user_message: message, }; if (voiceMeta?.voice_turn_id) { chatPayload.input_mode = "voice"; chatPayload.voice_turn_id = voiceMeta.voice_turn_id; if (voiceMeta.delivery_metadata) { chatPayload.delivery_metadata = voiceMeta.delivery_metadata; } } const data = await apiPost("/api/chat-round", chatPayload); if (data.error) { battleStatus.hidden = false; battleStatus.textContent = data.ai_message || data.error; return; } updateBattleMeta(data); if (data.answer_quality) { adjustBattleConfidenceFromAnswer(data.answer_quality); } const chatMeta = data.model_ok ? `${data.attack_tag} · Round ${data.round} · ⚡ Premium Nemotron` : `${data.attack_tag} · Round ${data.round}`; if (data.model_error) { console.warn("Opponent model fallback (not shown to user):", data.model_error); } appendMessage("ai", data.ai_message, chatMeta); setMicroCoach(data.micro_coach); setAnswerHint(data.answer_hint); if (data.soft_round_limit_reached) { updateBattleReadiness(data); } else { hideBattleReadiness(); } state.pendingVoiceTurn = null; document.getElementById("voice-turn-preview")?.setAttribute("hidden", ""); } catch (error) { console.error(error); battleStatus.hidden = false; battleStatus.textContent = "Message failed. Try again."; } finally { setGlobalLoading(false); } } export async function endBattle() { if (!state.sessionId) return; try { setGlobalLoading(true, "Building scorecard…"); const data = await apiPost("/api/end-battle", { session_id: state.sessionId, }); if (data.error) { showErrorBanner(data.error); return; } renderScorecard(data); showScreen("scorecard"); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Failed to generate scorecard. Check backend logs."); } finally { setGlobalLoading(false); } } export async function resetBattle() { if (state.sessionId) { try { await apiPost("/api/reset-session", { session_id: state.sessionId }); } catch (error) { console.error(error); } } state.sessionId = null; state.round = 1; state.pendingVoicePitch = null; state.pendingVoiceTurn = null; state.retryDrill = null; state.pendingRetryVoiceTurn = null; state.judgeVerdict = null; state.dealContext = null; state.dealRound = 1; state.uiMode = "pitch"; state.pendingDealVoiceTurn = null; state.startMode = "text"; state.pitchExtractionConfidence = null; state.pitchExtractionConfidenceScore = null; state.battleConfidencePct = null; state.conversationLog = []; state.dealConversationLog = []; state.battleLog = []; state.dealBattleLog = []; state.dealScorecardData = null; resetLiveTurnTracking(); chatWindow.innerHTML = ""; if (dealChatWindow) dealChatWindow.innerHTML = ""; ["battle-log-tabs", "deal-log-tabs"].forEach((id) => { const el = document.getElementById(id); if (el) el.innerHTML = ""; }); document.getElementById("battle-log-ribbon")?.setAttribute("hidden", ""); document.getElementById("deal-log-ribbon")?.setAttribute("hidden", ""); document.getElementById("btn-open-battle-rounds")?.setAttribute("hidden", ""); document.getElementById("battle-rounds-drawer")?.setAttribute("hidden", ""); document.getElementById("btn-open-deal-rounds")?.setAttribute("hidden", ""); document.getElementById("deal-rounds-drawer")?.setAttribute("hidden", ""); document.getElementById("battle-coach-bar")?.setAttribute("hidden", ""); hideBattleReadiness(); updateJudgeLiveCard("", ""); updateDealJudgeCard("", ""); userInput.value = ""; showScreen("landing"); } function formatDimLabel(dim) { return String(dim ?? "").replaceAll("_", " "); } function scoreBand(score) { const s = Number(score) || 0; if (s >= 70) return "score-high"; if (s >= 50) return "score-mid"; return "score-low"; } function getStrongestWeakest(scores) { const entries = Object.entries(scores || {}).filter(([, v]) => v && typeof v.score === "number"); if (!entries.length) return { strongest: null, weakest: null }; const sorted = [...entries].sort((a, b) => (b[1].score ?? 0) - (a[1].score ?? 0)); return { strongest: sorted[0], weakest: sorted[sorted.length - 1] }; } function attachShowMore(el) { if (!el || !el.textContent?.trim()) return; el.classList.add("clamp-text"); if (el.nextElementSibling?.classList?.contains("show-more-btn")) return; requestAnimationFrame(() => { if (el.scrollHeight <= el.clientHeight + 2) return; const btn = document.createElement("button"); btn.type = "button"; btn.className = "show-more-btn"; btn.textContent = "Show more"; btn.addEventListener("click", () => { const expanded = el.classList.toggle("expanded"); btn.textContent = expanded ? "Show less" : "Show more"; }); el.insertAdjacentElement("afterend", btn); }); } function buildDimensionRow(key, value, opts = {}) { const s = value?.score ?? 0; const band = scoreBand(s); const dimLabel = formatDimLabel(key); const labelSlug = String(value?.label || "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); const reason = value?.reason ?? ""; const layout = opts.layout ?? "default"; if (layout === "scorecard") { const row = document.createElement("div"); const fillSlug = labelSlug || "developing"; let highlightClass = ""; if (opts.weakestKey === key) highlightClass = " sc-dim-weakest"; else if (opts.strongestKey === key) highlightClass = " sc-dim-strongest"; row.className = `dimension-row score-row dimension-row-scorecard sc-dim-card ${band}${highlightClass}`; const flagHtml = opts.weakestKey === key ? `Weakest` : opts.strongestKey === key ? `Strongest` : ""; const badgeHtml = value?.label ? `${escapeHtml(value.label)}` : ""; row.innerHTML = `
    ${dimLabel} ${flagHtml}
    ${s} ${badgeHtml}
    `; requestAnimationFrame(() => { const fill = row.querySelector(".dimension-bar-fill"); if (fill) fill.style.width = fill.dataset.width || `${s}%`; }); return row; } const row = document.createElement("div"); row.className = `dimension-row score-row ${band}`; const labelHtml = value?.label ? `${escapeHtml(value.label)}` : ""; const quote = value?.quote && opts.showQuote ? `"${escapeHtml(value.quote)}"` : ""; row.innerHTML = `
    ${dimLabel}${labelHtml} ${s}

    ${escapeHtml(reason)}

    ${quote} `; requestAnimationFrame(() => { const fill = row.querySelector(".dimension-bar-fill"); if (fill) fill.style.width = fill.dataset.width || `${s}%`; }); return row; } function initResultTabs(tabsRootId, panelsRootId) { const tabsRoot = document.getElementById(tabsRootId); const panelsRoot = document.getElementById(panelsRootId); if (!tabsRoot || !panelsRoot || tabsRoot.dataset.tabsInit === "1") return; tabsRoot.dataset.tabsInit = "1"; const activate = (tabName) => { tabsRoot.querySelectorAll(".result-tab").forEach((tab) => { const active = tab.dataset.tab === tabName; tab.classList.toggle("active", active); tab.setAttribute("aria-selected", active ? "true" : "false"); }); panelsRoot.querySelectorAll(".result-panel").forEach((panel) => { const active = panel.dataset.panel === tabName; panel.classList.toggle("active", active); panel.hidden = !active; }); }; tabsRoot.addEventListener("click", (e) => { const tab = e.target.closest(".result-tab"); if (!tab || tab.hidden) return; activate(tab.dataset.tab); }); } function switchResultTab(tabsRootId, tabName) { const tabsRoot = document.getElementById(tabsRootId); const tab = tabsRoot?.querySelector(`.result-tab[data-tab="${tabName}"]`); tab?.click(); } function initScorecardTabs() { const root = document.getElementById("scorecard-tabs"); if (!root || root.dataset.init === "1") return; root.dataset.init = "1"; root.addEventListener("click", (e) => { const tab = e.target.closest(".sc-tab"); if (!tab) return; activateScorecardTab(tab.dataset.tab); }); } function activateScorecardTab(tabName) { document.querySelectorAll("#scorecard-tabs .sc-tab").forEach((tab) => { const active = tab.dataset.tab === tabName; tab.classList.toggle("active", active); tab.setAttribute("aria-selected", active ? "true" : "false"); }); document.querySelectorAll(".sc-tab-panels .sc-tab-panel").forEach((panel) => { const active = panel.dataset.panel === tabName; panel.classList.toggle("active", active); panel.hidden = !active; }); } function renderSignalsGroups(css) { const container = document.getElementById("signals-summary"); const emptyEl = document.getElementById("signals-empty"); if (!container) return false; const groups = [ { key: "numbers", label: "Numbers" }, { key: "validation", label: "Validation" }, { key: "competitors", label: "Competitors" }, { key: "revenue_signals", label: "Revenue" }, { key: "technical_mechanisms", label: "Mechanisms" }, ]; container.innerHTML = ""; let hasAny = false; groups.forEach(({ key, label }) => { const items = (css?.[key] ?? []).filter((s) => String(s).trim()); if (!items.length) return; hasAny = true; const group = document.createElement("div"); group.className = "sc-signal-group"; const groupLabel = document.createElement("p"); groupLabel.className = "sc-signal-group-label"; groupLabel.textContent = label; group.appendChild(groupLabel); const chips = document.createElement("div"); chips.className = "sc-signal-chips"; items.forEach((sig) => { const chip = document.createElement("span"); chip.className = "sc-signal-chip"; chip.textContent = sig; chips.appendChild(chip); }); group.appendChild(chips); container.appendChild(group); }); if (emptyEl) emptyEl.hidden = hasAny; return hasAny; } function hasNoBattleAnswers(data) { const scores = data.scores ?? {}; const allZero = Object.values(scores).every((v) => (v?.score ?? 0) === 0); const noBest = !String(data.best_answer ?? "").trim(); return allZero || noBest; } function renderScorecard(data) { const overall = data.overall ?? 0; const overallEl = document.getElementById("overall-score"); if (overallEl) overallEl.textContent = overall; const overallLabelEl = document.getElementById("overall-label"); if (overallLabelEl) { overallLabelEl.textContent = data.overall_label ?? ""; overallLabelEl.hidden = !data.overall_label; } const scores = data.scores ?? {}; const { strongest, weakest } = getStrongestWeakest(scores); const se = data.score_explanation ?? {}; const explanation = se; const sourceDisplay = getScorecardSourceDisplay(data); const chipSource = document.getElementById("chip-score-source"); const sourceBadgeEl = document.getElementById("scorecard-source-badge"); if (chipSource) { if (sourceDisplay.show) { chipSource.textContent = sourceDisplay.chip; chipSource.title = sourceDisplay.title ?? ""; chipSource.hidden = false; } else { chipSource.textContent = ""; chipSource.hidden = true; } } if (sourceBadgeEl) { sourceBadgeEl.hidden = true; sourceBadgeEl.textContent = ""; } if (!sourceDisplay.show && data.fallback_reason) { console.info("Scorecard used local fallback:", data.fallback_reason, data.model_error || ""); } const opponent = PERSONA_LABELS[state.persona] ?? data.opponent ?? "AI Judge"; const mode = DIFFICULTY_LABELS[state.difficultyProfile] ?? data.difficulty_label ?? "Practice Mode"; const heroLabel = document.getElementById("sc-hero-label"); if (heroLabel) { heroLabel.textContent = `Pitch Battle Result · ${opponent} · ${mode}`; } const whySentence = se.why_you_scored_this?.split(/[.!?]/)[0]?.trim() ?? ""; const readoutEl = document.getElementById("pitch-readout"); if (readoutEl) { const parts = []; if (data.overall_label) parts.push(data.overall_label); if (whySentence) parts.push(whySentence); readoutEl.textContent = parts.length ? parts.join(" — ") : "Your pitch battle is complete."; } const strongChip = document.getElementById("chip-strongest-dim"); const weakChip = document.getElementById("chip-weakest-dim"); if (strongChip) { strongChip.textContent = strongest ? `↑ Strongest: ${formatDimLabel(strongest[0])}` : "↑ Strongest: —"; } if (weakChip) { weakChip.textContent = weakest ? `↓ Weakest: ${formatDimLabel(weakest[0])}` : "↓ Weakest: —"; } const fallbackWarnEl = document.getElementById("scorecard-fallback-warning"); if (fallbackWarnEl) { fallbackWarnEl.textContent = ""; fallbackWarnEl.hidden = true; } if (data.model_error) { console.warn("Scorecard model note (not shown to user):", data.model_error); } const bars = document.getElementById("score-bars"); if (bars) { bars.innerHTML = ""; Object.entries(scores).forEach(([key, value]) => { bars.appendChild( buildDimensionRow(key, value, { layout: "scorecard", strongestKey: strongest?.[0] ?? null, weakestKey: weakest?.[0] ?? null, }), ); }); } renderSignalsGroups(data.concrete_signals_summary ?? {}); const atr = se.answer_to_retry ?? {}; const nextLine = atr.retry_advice ?? ""; const nextText = document.getElementById("pitch-next-action-text"); if (nextText) { nextText.textContent = nextLine ? (nextLine.endsWith(".") ? nextLine : `${nextLine}.`) : ""; } const whyScoredEl = document.getElementById("score-why-scored"); const whatStoppedEl = document.getElementById("score-what-stopped"); if (whyScoredEl) whyScoredEl.textContent = explanation.why_you_scored_this ?? ""; if (whatStoppedEl) whatStoppedEl.textContent = explanation.what_stopped_80 ?? ""; const setText = (id, text) => { const el = document.getElementById(id); if (!el) return; el.textContent = text ?? ""; el.classList.remove("expanded"); const next = el.nextElementSibling; if (next?.classList?.contains("show-more-btn")) next.remove(); }; setText("improved-answer", data.improved_answer); setText("improved-pitch", data.improved_pitch); setText("best-answer", data.best_answer); setText("weakest-answer", data.weakest_answer); const setRoundBadge = (id, round) => { const el = document.getElementById(id); if (!el) return; if (round) { el.textContent = `Round ${round}`; el.hidden = false; } else { el.textContent = ""; el.hidden = true; } }; setRoundBadge("best-answer-round", data.best_answer_round); setRoundBadge("weakest-answer-round", data.weakest_answer_round); const whyWeakEl = document.getElementById("why-weak"); if (whyWeakEl) { const why = data.why_weak ?? ""; whyWeakEl.textContent = why ? `Why it hurt: ${why}` : ""; whyWeakEl.hidden = !why; } const list = document.getElementById("top-questions"); if (list) { list.innerHTML = ""; (data.top_3_questions ?? []).forEach((q) => { const li = document.createElement("li"); li.textContent = q; list.appendChild(li); }); } const prepRetryText = document.getElementById("sc-prep-retry-text"); if (prepRetryText) { const weakDim = weakest ? formatDimLabel(weakest[0]).toLowerCase() : "your weakest dimension"; prepRetryText.textContent = `Your weakest answer was on ${weakDim}. Practice it again?`; } const noAnswers = hasNoBattleAnswers(data); const answersEmpty = document.getElementById("answers-empty-state"); const answersContent = document.getElementById("answers-content"); if (answersEmpty) answersEmpty.hidden = !noAnswers; if (answersContent) answersContent.hidden = noAnswers; if (noAnswers) { if (whyScoredEl && !whyScoredEl.textContent?.trim()) { whyScoredEl.textContent = "You ended before responding, so the scorecard can only grade the startup brief."; } if (whatStoppedEl && !whatStoppedEl.textContent?.trim()) { whatStoppedEl.textContent = "Submit at least one battle answer to unlock answer-level coaching."; } if (nextText && !nextText.textContent?.trim()) { nextText.textContent = "Start a new battle and defend at least one question."; } const prepPanel = document.querySelector('.sc-tab-panel[data-panel="prep"]'); if (prepPanel) prepPanel.classList.add("sc-prep-empty"); } else { document.querySelector('.sc-tab-panel[data-panel="prep"]')?.classList.remove("sc-prep-empty"); } state.scoreExplanation = data.score_explanation ?? null; const pathBtn = document.getElementById("btn-path-to-80"); if (pathBtn) pathBtn.hidden = !Boolean(state.scoreExplanation); renderVoiceDelivery(data.voice_delivery); renderJudgeVerdict(data.judge_verdict); state.judgeVerdict = data.judge_verdict ?? null; const orb = document.querySelector("#screen-scorecard .sc-ring"); if (orb) { orb.classList.remove("score-high", "score-mid", "score-low"); orb.classList.add(scoreBand(overall)); } activateScorecardTab("overview"); state.scorecardSnapshot = { overall, overallLabel: data.overall_label ?? "", strongest: strongest ? formatDimLabel(strongest[0]) : null, weakest: weakest ? formatDimLabel(weakest[0]) : null, roundsCompleted: state.battleLog?.length ?? 0, }; updateScorecardCrossNav(); } const DEAL_TYPE_LABELS = { equity: "Equity Negotiation", mentorship: "Mentorship Terms", pilot: "Pilot Agreement", sponsorship: "Sponsorship Terms", verdict_only: "Hackathon Verdict", none: "General Discussion", }; function verdictNegotiationCta(label, fallback = "Start Negotiation →") { const text = String(label || "").trim(); if (!text || /pitch practice/i.test(text)) return fallback; return text .replace(/Continue to Deal Practice/i, "Start Negotiation") .replace(/Continue to Deal Round/i, "Start Negotiation") .replace(/Deal Practice/i, "Negotiation") .replace(/Deal Round/i, "Negotiation"); } function canContinueToDealFromVerdict(verdict) { if (!verdict) return false; if (verdict.can_continue_to_deal) return true; const interest = verdict.interest_level || ""; if ((interest === "mild_interest" || interest === "strong_interest") && verdict.deal_type && verdict.deal_type !== "none") { return true; } return false; } function openVerdictModal() { const overlay = document.getElementById("verdict-overlay"); if (!overlay || !state.judgeVerdict?.interest_level) return; overlay.hidden = false; document.body.style.overflow = "hidden"; } function closeVerdictModal() { const overlay = document.getElementById("verdict-overlay"); if (overlay) overlay.hidden = true; document.body.style.overflow = ""; } function renderJudgeVerdict(verdict) { const verdictBtn = document.getElementById("btn-view-judge-verdict"); if (!verdict || !verdict.interest_level) { if (verdictBtn) verdictBtn.hidden = true; return; } if (verdictBtn) verdictBtn.hidden = false; const interest = verdict.interest_level || "no_interest"; const personaLine = `${verdict.persona_name || "Judge"} — ${verdict.persona_type || ""}`.trim(); document.getElementById("verdict-persona-badge").textContent = personaLine; const badge = document.getElementById("verdict-interest-badge"); badge.textContent = verdict.interest_label || interest.replaceAll("_", " "); badge.className = `sc-verdict-pill interest-${interest}`; const reactionEl = document.getElementById("verdict-reaction"); if (reactionEl) { reactionEl.textContent = verdict.judge_reaction || ""; } document.getElementById("verdict-deal-type").textContent = DEAL_TYPE_LABELS[verdict.deal_type] || verdict.deal_type || "—"; const whyEl = document.getElementById("verdict-why"); if (whyEl) whyEl.textContent = verdict.why_this_verdict || ""; const offerEl = document.getElementById("verdict-opening-offer"); if (offerEl) { if (verdict.deal_opening_offer) { offerEl.textContent = `Opening offer: ${verdict.deal_opening_offer}`; offerEl.hidden = false; } else { offerEl.hidden = true; } } const lockedMsg = document.getElementById("verdict-deal-locked-msg"); const dealAvailable = canContinueToDealFromVerdict(verdict); if (lockedMsg) { lockedMsg.hidden = dealAvailable || interest === "verdict_only"; } const actions = document.getElementById("verdict-actions"); if (!actions) return; actions.innerHTML = ""; if (dealAvailable) { const btn = document.createElement("button"); btn.className = "btn btn-deal-continue sc-btn-gold"; btn.textContent = verdictNegotiationCta(verdict.next_step_label, "Continue to Deal Phase →"); btn.type = "button"; btn.addEventListener("click", () => { closeVerdictModal(); startDealPhase(); }); actions.appendChild(btn); } } function renderDealArena(data) { state.uiMode = "deal"; state.dealContext = data.deal_context || {}; state.dealRound = data.round ?? 1; document.getElementById("deal-round-counter").textContent = state.dealRound; const dealRoundDisplay = document.getElementById("deal-round-display"); if (dealRoundDisplay) dealRoundDisplay.textContent = String(state.dealRound).padStart(2, "0"); const dealRoundDuel = document.getElementById("deal-round-counter-duel"); if (dealRoundDuel) dealRoundDuel.textContent = state.dealRound; const dealType = DEAL_TYPE_LABELS[data.deal_type] || data.deal_type || "—"; document.getElementById("deal-type-label").textContent = dealType; const typeChip = document.getElementById("deal-type-chip"); if (typeChip) typeChip.textContent = dealType; const focus = data.negotiation_tag || "—"; document.getElementById("deal-negotiation-tag").textContent = focus; const focusChip = document.getElementById("deal-focus-chip"); if (focusChip) focusChip.textContent = focus; document.getElementById("deal-persona-name").textContent = `${data.persona_name || ""} — ${data.persona_role || ""}`.trim(); document.getElementById("deal-opening-offer").textContent = data.deal_context?.opening_offer || data.deal_context?.judge_position || "—"; document.getElementById("deal-your-ask").textContent = data.deal_context?.ask || "—"; updateProgressStrip(state.dealRound, focus, "deal"); updateJudgeAttackPill(focus, "deal"); } export async function startDealPhase() { if (!state.sessionId) return; try { setGlobalLoading(true, "Preparing deal terms…"); const data = await apiPost("/api/start-deal-phase", { session_id: state.sessionId }); if (data.error) { showErrorBanner(data.error); return; } if (dealChatWindow) dealChatWindow.innerHTML = ""; state.dealConversationLog = []; state.dealBattleLog = []; liveDealJudgeTurn = { text: "", meta: "" }; liveDealFounderReply = ""; document.getElementById("deal-log-tabs") && (document.getElementById("deal-log-tabs").innerHTML = ""); document.getElementById("deal-log-ribbon")?.setAttribute("hidden", ""); document.getElementById("btn-open-deal-rounds")?.setAttribute("hidden", ""); document.getElementById("deal-rounds-drawer")?.setAttribute("hidden", ""); if (dealInput) dealInput.value = ""; dealStatus.hidden = true; document.getElementById("deal-readiness-prompt")?.setAttribute("hidden", ""); renderDealArena(data); appendDealMessage( "ai", data.ai_message, `${data.negotiation_tag} · Round ${data.round}`, ); showScreen("deal"); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Could not start deal phase."); } finally { setGlobalLoading(false); } } export async function sendDealRound(messageOverride, voiceMeta = null) { const message = (messageOverride ?? dealInput?.value ?? "").trim(); if (!message || !state.sessionId) return; try { setGlobalLoading(true, "Scoring your counter…"); if (dealInput) dealInput.value = ""; appendDealMessage("user", message); const payload = { session_id: state.sessionId, user_message: message, input_mode: voiceMeta?.voice_turn_id ? "voice" : "text", }; if (voiceMeta?.voice_turn_id) payload.voice_turn_id = voiceMeta.voice_turn_id; const data = await apiPost("/api/deal-round", payload); if (data.error) { if (dealStatus) { dealStatus.hidden = false; dealStatus.textContent = data.error; } return; } state.dealRound = data.round ?? state.dealRound; document.getElementById("deal-round-counter").textContent = state.dealRound; const dealRoundDisplay = document.getElementById("deal-round-display"); if (dealRoundDisplay) dealRoundDisplay.textContent = String(state.dealRound).padStart(2, "0"); const dealRoundDuel = document.getElementById("deal-round-counter-duel"); if (dealRoundDuel) dealRoundDuel.textContent = state.dealRound; document.getElementById("deal-negotiation-tag").textContent = data.negotiation_tag || "—"; updateProgressStrip(state.dealRound, data.negotiation_tag, "deal"); appendDealMessage( "ai", data.ai_message, `${data.negotiation_tag} · Round ${data.round}`, ); updateDealReadiness(data.readiness, data.soft_limit_reached, data.completion_message); state.pendingDealVoiceTurn = null; document.getElementById("deal-voice-preview")?.setAttribute("hidden", ""); } catch (error) { console.error(error); dealStatus.hidden = false; dealStatus.textContent = "Deal round failed. Try again."; } finally { setGlobalLoading(false); } } function updateDealReadiness(readiness, softLimit, completionMessage) { const prompt = document.getElementById("deal-readiness-prompt"); const text = document.getElementById("deal-readiness-text"); const action = readiness?.recommended_action; const shouldShow = action === "recommend_end" || action === "force_end" || softLimit; if (!prompt) return; if (!shouldShow) { prompt.hidden = true; return; } if (text) { text.textContent = readiness?.reason || completionMessage || "You have enough negotiation signal for a scorecard. You can end now or continue one more round."; } // At the hard cap, hide the "continue" option. const continueBtn = document.getElementById("btn-deal-readiness-continue"); if (continueBtn) continueBtn.style.display = action === "force_end" ? "none" : ""; prompt.hidden = false; } function showDealVoicePreview(data) { state.pendingDealVoiceTurn = data; const preview = document.getElementById("deal-voice-preview"); const transcriptEl = document.getElementById("deal-voice-transcript"); if (preview) preview.hidden = false; if (transcriptEl) transcriptEl.value = data.transcript ?? ""; if (dealInput) dealInput.value = data.transcript ?? ""; } export async function endDeal() { if (!state.sessionId) return; try { setGlobalLoading(true, "Building deal scorecard…"); const data = await apiPost("/api/end-deal", { session_id: state.sessionId }); if (data.error) { showErrorBanner(data.error); return; } state.negotiationTranscript = data.negotiation_transcript || []; renderDealScorecard(data); showScreen("dealScorecard"); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Failed to generate deal scorecard."); } finally { setGlobalLoading(false); } } function updateScorecardCrossNav() { const btn = document.getElementById("btn-view-deal-scorecard"); if (!btn) return; btn.hidden = !state.dealScorecardData; } function showDealScorecardScreen() { if (!state.dealScorecardData) return; renderDealScorecard(state.dealScorecardData); showScreen("dealScorecard"); } function renderDealScorecard(data) { state.dealScorecardData = data; updateScorecardCrossNav(); const combined = data.combined_scorecard || {}; const deal = data.deal_scorecard || {}; const pitch = combined.pitch_overall ?? 0; const dealScore = combined.deal_overall ?? 0; const overall = combined.combined_overall ?? 0; document.getElementById("combined-pitch").textContent = pitch; document.getElementById("combined-deal").textContent = dealScore; document.getElementById("combined-overall").textContent = overall; ["combined-pitch-tab", "combined-deal-tab", "combined-overall-tab"].forEach((id, i) => { const el = document.getElementById(id); if (el) el.textContent = [pitch, dealScore, overall][i]; }); const profile = combined.founder_profile ?? ""; document.getElementById("combined-profile").textContent = profile; const profileTab = document.getElementById("combined-profile-tab"); if (profileTab) profileTab.textContent = profile; const summaryEl = document.getElementById("combined-summary"); if (summaryEl) { summaryEl.textContent = combined.summary ?? ""; attachShowMore(summaryEl); } const nextEl = document.getElementById("combined-next-action"); if (nextEl) { nextEl.textContent = combined.next_best_action ?? "Review deal dimensions and prep points below."; } const outcomeEl = document.getElementById("deal-outcome-badge"); if (outcomeEl) { outcomeEl.textContent = (deal.deal_outcome || "balanced").replaceAll("_", " "); outcomeEl.className = `deal-outcome-badge result-verdict-badge outcome-${deal.deal_outcome || "balanced"}`; } document.getElementById("deal-overall-label").textContent = `${deal.overall ?? 0} — ${deal.overall_label ?? ""}`; const dealWeakest = getStrongestWeakest(deal.scores || {}).weakest; const weakestLine = document.getElementById("deal-summary-weakest"); if (weakestLine) { weakestLine.textContent = dealWeakest ? `Weakest negotiation skill: ${formatDimLabel(dealWeakest[0])} (${dealWeakest[1].score ?? 0})` : ""; } const bars = document.getElementById("deal-score-bars"); if (bars) { bars.innerHTML = ""; Object.entries(deal.scores || {}).forEach(([key, value]) => { bars.appendChild(buildDimensionRow(key, value, { showQuote: true })); }); } const WEAK_FALLBACK = "No major single weak move detected; the main weakness was lack of alternatives/leverage."; const bestEl = document.getElementById("deal-best-move"); const weakEl = document.getElementById("deal-weakest-move"); const improvedEl = document.getElementById("deal-improved-response"); if (bestEl) { bestEl.textContent = cleanMoveText(deal.best_move) || "No standout negotiation move was recorded."; attachShowMore(bestEl); } if (weakEl) { weakEl.textContent = cleanMoveText(deal.weakest_move) || WEAK_FALLBACK; attachShowMore(weakEl); } if (improvedEl) { improvedEl.textContent = deal.improved_response ?? ""; attachShowMore(improvedEl); } const prep = document.getElementById("deal-prep-points"); if (prep) { prep.innerHTML = ""; (deal.top_3_prep_points || []).forEach((p) => { const li = document.createElement("li"); li.textContent = p; prep.appendChild(li); }); } const metaEl = document.getElementById("deal-scorecard-meta"); if (metaEl) { const nemotronScored = isNemotronScorecardSource(deal.scorecard_source); metaEl.textContent = nemotronScored ? "Powered by NVIDIA Nemotron" : ""; metaEl.hidden = !nemotronScored; } initResultTabs("deal-result-tabs", "deal-result-panels"); switchResultTab("deal-result-tabs", "deal-summary"); } // Reject empty / single-word / obviously truncated move text so the UI never // shows a lone "sure" or a half-sentence. function cleanMoveText(text) { const t = (text || "").trim(); if (!t) return ""; const bareWords = ["sure", "ok", "okay", "yes", "fine", "yeah"]; if (bareWords.includes(t.toLowerCase().replace(/[.!?]$/, ""))) return ""; if (t.length < 12) return ""; return t; } function renderNegotiationTranscript() { const container = document.getElementById("negotiation-transcript"); if (!container) return; container.innerHTML = ""; const transcript = state.negotiationTranscript || []; if (!transcript.length) { container.innerHTML = `

    No negotiation messages were recorded.

    `; return; } transcript.forEach((turn) => { const row = document.createElement("div"); const role = turn.role === "judge" ? "judge" : "founder"; row.className = `negotiation-turn negotiation-${role}`; const speaker = role === "judge" ? "Judge" : "Founder"; const badges = []; if (turn.negotiation_tag) { badges.push(`${escapeHtml(turn.negotiation_tag)}`); } if (turn.answer_quality) { badges.push(`${escapeHtml(turn.answer_quality)}`); } if ((turn.input_mode || "") === "voice") { badges.push(`Voice`); } if (turn.action_taken) { badges.push(`${escapeHtml(turn.action_taken)}`); } row.innerHTML = `
    ${turn.round ? `Round ${turn.round} · ` : ""}${speaker} ${badges.join("")}

    ${escapeHtml(turn.message || "")}

    `; container.appendChild(row); }); container.scrollTop = container.scrollHeight; } function openNegotiationModal() { renderNegotiationTranscript(); const overlay = document.getElementById("negotiation-overlay"); if (overlay) overlay.hidden = false; } function closeNegotiationModal() { const overlay = document.getElementById("negotiation-overlay"); if (overlay) overlay.hidden = true; } function renderVoiceDelivery(vd) { const content = document.getElementById("voice-delivery-content"); const section = document.getElementById("voice-delivery-section"); const tab = document.getElementById("tab-voice-delivery"); if (!content) return; if (!vd || typeof vd !== "object") { if (section) section.hidden = true; if (tab) tab.hidden = true; content.innerHTML = ""; return; } if (section) section.hidden = false; if (tab) tab.hidden = false; const fillers = (vd.filler_word_list ?? []).slice(0, 6).join(", ") || "None detected"; const overallNote = vd.overall_delivery_feedback || (vd.delivery_notes ?? []).find((n) => String(n).trim()) || ""; content.innerHTML = `
    Voice turns ${vd.total_voice_turns ?? 0}
    Filler words ${vd.total_filler_words ?? 0}
    Common fillers ${escapeHtml(fillers)}
    Pace ${escapeHtml(vd.average_pace ?? "—")}
    ${overallNote ? `

    ${escapeHtml(overallNote)}

    ` : ""}
    `; const notes = (vd.delivery_notes ?? []).filter((n) => { const t = String(n || "").trim().toLowerCase(); const overallLower = String(overallNote).trim().toLowerCase(); return t && t !== "clean delivery." && t !== "clean delivery" && t !== overallLower; }); const uniqueNotes = [...new Set(notes.map((n) => String(n).trim()))].slice(0, 3); if (uniqueNotes.length) { const ul = document.createElement("ul"); ul.className = "voice-delivery-notes"; uniqueNotes.forEach((n) => { const li = document.createElement("li"); li.textContent = n; ul.appendChild(li); }); content.appendChild(ul); } } function fillVoiceExtractForm(data) { const form = document.getElementById("voice-extract-form"); if (!form || !data) return; const extracted = data.extracted ?? {}; Object.entries(extracted).forEach(([key, value]) => { const field = form.elements.namedItem(key); if (field) field.value = value ?? ""; }); const transcriptEl = document.getElementById("voice-confirm-transcript"); if (transcriptEl) transcriptEl.textContent = data.transcript ?? ""; const deliveryEl = document.getElementById("voice-confirm-delivery"); const obs = data.delivery_observations ?? {}; if (deliveryEl) { deliveryEl.textContent = obs.delivery_note ? `Delivery: ${obs.delivery_note}` : ""; } const confEl = document.getElementById("voice-confirm-confidence"); if (confEl) { const conf = data.extraction_confidence ?? "medium"; confEl.textContent = `Confidence: ${conf}`; confEl.className = `delivery-chip confidence-${conf}`; } const warn = document.getElementById("voice-confidence-warning"); if (warn) warn.hidden = (data.extraction_confidence ?? "medium") !== "low"; } function showVoiceTurnPreview(data) { state.pendingVoiceTurn = data; const preview = document.getElementById("voice-turn-preview"); const transcriptEl = document.getElementById("voice-turn-transcript"); const deliveryEl = document.getElementById("voice-turn-delivery"); if (preview) preview.hidden = false; if (transcriptEl) transcriptEl.value = data.transcript ?? ""; if (deliveryEl) { deliveryEl.textContent = data.delivery_note ? `Delivery: ${data.delivery_note}` : ""; } } function openPath80() { const ex = state.scoreExplanation; if (!ex) return; const esif = ex.estimated_score_if_fixed ?? {}; const atr = ex.answer_to_retry ?? {}; const setEl = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text ?? ""; }; setEl("p80-current-score", esif.current_overall ?? "—"); setEl("p80-estimated-score", esif.estimated_new_overall ?? "—"); setEl("p80-estimate-reason", esif.reason ?? ""); setEl("p80-why-scored", ex.why_you_scored_this ?? ""); setEl("p80-what-stopped", ex.what_stopped_80 ?? ""); const dimBadge = document.getElementById("p80-retry-dim"); if (dimBadge) dimBadge.textContent = (atr.dimension ?? "").replaceAll("_", " "); const roundTag = document.getElementById("p80-retry-round"); if (roundTag) roundTag.textContent = atr.round ? `Round ${atr.round}` : ""; setEl("p80-original-answer", atr.original_answer ?? ""); setEl("p80-why-it-hurt", atr.why_it_hurt ?? ""); setEl("p80-retry-advice", atr.retry_advice ?? ""); setEl("p80-sample-answer", atr.sample_stronger_answer ?? ""); ["p80-why-scored", "p80-what-stopped", "p80-original-answer", "p80-why-it-hurt", "p80-retry-advice"].forEach((id) => { attachShowMore(document.getElementById(id)); }); document.getElementById("path80-overlay").hidden = false; document.body.style.overflow = "hidden"; } function closePath80() { document.getElementById("path80-overlay").hidden = true; document.body.style.overflow = ""; } function showRetryDrillView() { document.getElementById("retry-drill-view").hidden = false; document.getElementById("retry-result-view").hidden = true; } function showRetryResultView() { document.getElementById("retry-drill-view").hidden = true; document.getElementById("retry-result-view").hidden = false; } function populateRetryDrill(data) { const q = data.retry_question || data.original_question || ""; document.getElementById("retry-original-question").textContent = q; document.getElementById("retry-original-answer").textContent = data.original_answer ?? ""; document.getElementById("retry-why-hurt").textContent = data.why_it_hurt ?? ""; document.getElementById("retry-sample").textContent = data.sample_stronger_answer ?? ""; const input = document.getElementById("retry-answer-input"); if (input) input.value = ""; document.getElementById("retry-voice-preview")?.setAttribute("hidden", ""); state.pendingRetryVoiceTurn = null; showRetryDrillView(); } async function startRetryDrill() { if (!state.sessionId) return; try { setGlobalLoading(true, "Preparing retry drill..."); const data = await apiPost("/api/retry-weakest-question/start", { session_id: state.sessionId, }); if (data.error) { showErrorBanner(data.error); return; } state.retryDrill = data; closePath80(); populateRetryDrill(data); document.getElementById("retry-overlay").hidden = false; document.body.style.overflow = "hidden"; hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Could not start retry drill. Try again."); } finally { setGlobalLoading(false); } } function showRetryVoicePreview(data) { state.pendingRetryVoiceTurn = data; const preview = document.getElementById("retry-voice-preview"); const transcriptEl = document.getElementById("retry-voice-transcript"); const input = document.getElementById("retry-answer-input"); if (preview) preview.hidden = false; if (transcriptEl) transcriptEl.value = data.transcript ?? ""; if (input) input.value = data.transcript ?? ""; } function renderRetryResult(data) { const comp = data.comparison ?? {}; const proj = data.projection ?? {}; document.getElementById("retry-result-old").textContent = comp.old_answer_summary ?? data.original_answer ?? ""; document.getElementById("retry-result-new").textContent = comp.new_answer_summary ?? data.retry_answer ?? ""; document.getElementById("retry-what-improved").textContent = comp.what_improved ?? ""; document.getElementById("retry-still-missing").textContent = comp.still_missing ?? ""; document.getElementById("retry-specific-tip").textContent = comp.specific_tip ?? ""; // Prefer normalized projection fields; fall back to legacy comparison fields. const before = proj.old_dimension_score ?? comp.estimated_dimension_before ?? 0; const after = proj.new_dimension_score ?? comp.estimated_dimension_after ?? before; document.getElementById("retry-dim-estimate").textContent = `${formatDimLabel(data.dimension)}: ${before} → ${after}`; // Safety floor: never display a baseline lower than what the scorecard shows. // If the backend returns a projection baseline below the visible scorecard overall, // clamp up to the scorecard snapshot so the user never sees "score went down". const snapshotOverall = state.scorecardSnapshot?.overall ?? null; const rawOriginalOverall = proj.original_overall_score ?? snapshotOverall ?? "—"; const originalOverall = ( typeof rawOriginalOverall === "number" && typeof snapshotOverall === "number" && rawOriginalOverall < snapshotOverall ) ? snapshotOverall : rawOriginalOverall; const rawProjected = proj.projected_overall_score ?? originalOverall; const projectedOverall = ( typeof rawProjected === "number" && typeof originalOverall === "number" ) ? Math.max(rawProjected, originalOverall) : rawProjected; const projectedDelta = ( typeof projectedOverall === "number" && typeof originalOverall === "number" ) ? Math.max(0, projectedOverall - originalOverall) : (proj.projected_overall_delta ?? 0); const liftEl = document.getElementById("retry-overall-lift"); if (liftEl) { if (projectedDelta > 0) { liftEl.textContent = `Projected overall: ${originalOverall} → ${projectedOverall} (+${projectedDelta})`; } else { liftEl.textContent = "No projected lift yet — keep practicing this dimension."; } } const noteEl = document.getElementById("retry-projection-note"); if (noteEl) { noteEl.textContent = "Training projection only — your original scorecard stays unchanged."; noteEl.hidden = false; } const verdictEl = document.getElementById("retry-verdict-badge"); const verdict = projectedDelta > 0 ? (projectedDelta >= 3 ? "improved" : "slightly_improved") : (comp.verdict ?? "needs_more_work"); const verdictLabels = { improved: "Improved", slightly_improved: "Slightly improved", needs_more_work: "No lift yet", }; if (verdictEl) { verdictEl.textContent = verdictLabels[verdict] ?? verdict; verdictEl.className = `retry-verdict-badge verdict-${verdict}`; } const nextPrompt = document.getElementById("retry-next-prompt"); if (nextPrompt) { nextPrompt.textContent = data.next_practice_prompt ? `Next practice: ${data.next_practice_prompt}` : ""; } showRetryResultView(); } async function submitRetryAnswer() { if (!state.sessionId || !state.retryDrill?.retry_id) return; const voiceTranscript = document.getElementById("retry-voice-transcript")?.value?.trim(); const typed = document.getElementById("retry-answer-input")?.value?.trim(); const answer = voiceTranscript || typed; if (!answer) return; const payload = { session_id: state.sessionId, retry_id: state.retryDrill.retry_id, retry_answer: answer, input_mode: state.pendingRetryVoiceTurn ? "voice" : "text", }; if (state.pendingRetryVoiceTurn?.voice_turn_id) { payload.voice_turn_id = state.pendingRetryVoiceTurn.voice_turn_id; } try { setGlobalLoading(true, "Comparing your retry answer..."); const data = await apiPost("/api/retry-weakest-question/submit", payload); if (data.error) { showErrorBanner(data.error); return; } renderRetryResult(data); hideErrorBanner(); } catch (error) { console.error(error); showErrorBanner("Retry submission failed. Try again."); } finally { setGlobalLoading(false); } } function closeRetryOverlay() { document.getElementById("retry-overlay").hidden = true; document.body.style.overflow = ""; state.pendingRetryVoiceTurn = null; } function openRetryFromPath80() { startRetryDrill(); } bindClick("btn-load-sample", loadSample); bindClick("btn-load-sample-setup", loadSample); bindClick("btn-go-setup", () => { showScreen("startMethod"); }); bindClick("btn-back-landing", () => { resetSetupScreen(); showScreen("landing"); }); bindClick("tab-quick-pitch", () => setBriefingMode("quick")); bindClick("tab-advanced-briefing", () => setBriefingMode("advanced")); bindClick("btn-structure-pitch", structurePitch); bindClick("btn-record-voice-setup", () => { state.startMode = "voice"; state.voiceEntrySource = "briefing"; showScreen("voicePitch"); }); bindClick("btn-looks-good-start", () => { syncBriefToStartupForm(); startSession(); }); bindClick("btn-restructure-pitch", () => { hideBriefPreview(); document.getElementById("quick-pitch-text")?.focus(); }); bindClick("btn-start-back-landing", () => showScreen("landing")); bindClick("btn-start-text", () => { state.startMode = "text"; state.voiceEntrySource = "arena"; resetSetupScreen(); showScreen("setup"); }); bindClick("btn-start-voice", () => { state.startMode = "voice"; state.voiceEntrySource = "arena"; showScreen("voicePitch"); }); bindClick("btn-voice-pitch-back", () => { if (state.voiceEntrySource === "briefing") { showScreen("setup"); return; } showScreen("startMethod"); }); bindClick("btn-voice-edit-manual", () => { const form = document.getElementById("voice-extract-form"); const data = new FormData(form); applyVoicePitchToBriefing({ extracted: Object.fromEntries(data.entries()), transcript: document.getElementById("voice-confirm-transcript")?.textContent || "", extraction_confidence: "medium", }); setBriefingMode("advanced"); showScreen("setup"); }); bindClick("btn-voice-looks-right", () => { const form = document.getElementById("voice-extract-form"); const data = new FormData(form); applyVoicePitchToBriefing({ extracted: Object.fromEntries(data.entries()), transcript: document.getElementById("voice-confirm-transcript")?.textContent || "", extraction_confidence: document.getElementById("voice-confirm-confidence")?.textContent?.replace("Confidence: ", "") || "medium", delivery_observations: {}, }); showScreen("setup"); }); bindClick("btn-voice-turn-send", () => { const transcript = document.getElementById("voice-turn-transcript")?.value?.trim(); if (!transcript || !state.pendingVoiceTurn) return; sendMessage(transcript, { voice_turn_id: state.pendingVoiceTurn.voice_turn_id, delivery_metadata: { delivery_note: state.pendingVoiceTurn.delivery_note, delivery_cues: state.pendingVoiceTurn.delivery_cues, word_count: state.pendingVoiceTurn.word_count, }, }); }); bindClick("btn-start-battle", startSession); bindClick("btn-end-battle", endBattle); bindClick("btn-reset", resetBattle); bindClick("btn-back-setup", () => showScreen("setup")); bindClick("btn-path-to-80", openPath80); bindClick("btn-close-path80", closePath80); bindClick("btn-close-path80-bottom", closePath80); bindClick("btn-retry-question", openRetryFromPath80); bindClick("btn-scorecard-retry", startRetryDrill); bindClick("btn-prep-retry", startRetryDrill); bindClick("btn-answers-retry", startRetryDrill); bindClick("btn-answers-new-battle", resetBattle); bindClick("btn-view-judge-verdict", openVerdictModal); bindClick("btn-view-deal-scorecard", showDealScorecardScreen); bindClick("btn-close-verdict", closeVerdictModal); bindClick("btn-close-verdict-bottom", closeVerdictModal); bindClick("btn-verdict-retry", () => { closeVerdictModal(); startRetryDrill(); }); bindClick("btn-verdict-new-battle", () => { closeVerdictModal(); resetBattle(); }); document.getElementById("path80-overlay")?.addEventListener("click", (e) => { if (e.target === e.currentTarget) closePath80(); }); document.getElementById("verdict-overlay")?.addEventListener("click", (e) => { if (e.target === e.currentTarget) closeVerdictModal(); }); bindClick("btn-close-retry", closeRetryOverlay); bindClick("btn-submit-retry", submitRetryAnswer); bindClick("btn-retry-again", () => { if (state.retryDrill) populateRetryDrill(state.retryDrill); }); bindClick("btn-retry-back-scorecard", () => { closeRetryOverlay(); showScreen("scorecard"); }); bindClick("btn-retry-new-battle", () => { closeRetryOverlay(); resetBattle(); }); document.getElementById("retry-overlay")?.addEventListener("click", (e) => { if (e.target === e.currentTarget) closeRetryOverlay(); }); bindClick("btn-view-conversation", () => { document.getElementById("btn-end-battle").hidden = true; document.getElementById("btn-back-scorecard").hidden = false; document.getElementById("chat-form").hidden = true; showScreen("battle"); openBattleConversationLog(true); }); bindClick("btn-back-scorecard", () => { closeRoundsDrawer("battle-rounds-drawer"); document.getElementById("btn-end-battle").hidden = false; document.getElementById("btn-back-scorecard").hidden = true; document.getElementById("chat-form").hidden = false; showScreen("scorecard"); }); document.getElementById("btn-open-battle-rounds")?.addEventListener("click", () => { openBattleConversationLog(false); }); document.getElementById("btn-close-battle-rounds")?.addEventListener("click", () => { closeRoundsDrawer("battle-rounds-drawer"); }); document.getElementById("btn-close-battle-rounds-x")?.addEventListener("click", () => { closeRoundsDrawer("battle-rounds-drawer"); }); document.getElementById("btn-open-deal-rounds")?.addEventListener("click", () => { openRoundsDrawer("deal-rounds-drawer"); }); document.getElementById("btn-close-deal-rounds")?.addEventListener("click", () => { closeRoundsDrawer("deal-rounds-drawer"); }); document.getElementById("btn-close-deal-rounds-x")?.addEventListener("click", () => { closeRoundsDrawer("deal-rounds-drawer"); }); document.querySelectorAll(".persona-card").forEach((card) => { card.addEventListener("click", () => { document.querySelectorAll(".persona-card").forEach((c) => c.classList.remove("selected")); card.classList.add("selected"); state.persona = card.dataset.persona; }); }); document.querySelectorAll(".difficulty-card").forEach((card) => { card.addEventListener("click", () => { document.querySelectorAll(".difficulty-card").forEach((c) => c.classList.remove("selected")); card.classList.add("selected"); state.difficultyProfile = card.dataset.difficulty; }); }); document.getElementById("chat-form")?.addEventListener("submit", (event) => { event.preventDefault(); sendMessage(); }); function bindEnterToSubmit(textarea, onSubmit) { textarea?.addEventListener("keydown", (event) => { if (event.key !== "Enter" || event.shiftKey || event.isComposing) return; event.preventDefault(); onSubmit(); }); } bindEnterToSubmit(userInput, () => sendMessage()); bindEnterToSubmit(document.getElementById("deal-input"), () => sendDealRound()); document.getElementById("deal-form")?.addEventListener("submit", (event) => { event.preventDefault(); sendDealRound(); }); document.getElementById("btn-end-deal")?.addEventListener("click", endDeal); document.getElementById("btn-deal-back-scorecard")?.addEventListener("click", () => showScreen("scorecard")); document.getElementById("btn-deal-new-battle")?.addEventListener("click", resetBattle); document.getElementById("btn-deal-view-pitch-scorecard")?.addEventListener("click", () => showScreen("scorecard")); document.getElementById("btn-deal-readiness-end")?.addEventListener("click", () => { document.getElementById("deal-readiness-prompt")?.setAttribute("hidden", ""); endDeal(); }); document.getElementById("btn-deal-readiness-continue")?.addEventListener("click", () => { document.getElementById("deal-readiness-prompt")?.setAttribute("hidden", ""); document.getElementById("deal-input")?.focus(); }); document.getElementById("btn-battle-readiness-end")?.addEventListener("click", () => { hideBattleReadiness(); endBattle(); }); document.getElementById("btn-battle-readiness-continue")?.addEventListener("click", () => { hideBattleReadiness(); userInput?.focus(); }); document.querySelectorAll(".hint-chip").forEach((chip) => { chip.addEventListener("click", () => { const hint = chip.dataset.hint; if (!hint || !userInput) return; const val = userInput.value.trim(); userInput.value = val ? `${val} ${hint}`.trim() : hint.trim(); userInput.focus(); }); }); document.getElementById("btn-deal-view-negotiation")?.addEventListener("click", openNegotiationModal); document.getElementById("btn-close-negotiation")?.addEventListener("click", closeNegotiationModal); document.getElementById("btn-negotiation-back")?.addEventListener("click", closeNegotiationModal); document.getElementById("negotiation-overlay")?.addEventListener("click", (event) => { if (event.target?.id === "negotiation-overlay") closeNegotiationModal(); }); document.getElementById("btn-deal-voice-send")?.addEventListener("click", () => { const transcript = document.getElementById("deal-voice-transcript")?.value?.trim(); if (!transcript || !state.pendingDealVoiceTurn) return; sendDealRound(transcript, { voice_turn_id: state.pendingDealVoiceTurn.voice_turn_id }); }); document.getElementById("btn-deal-voice-cancel")?.addEventListener("click", () => { document.getElementById("deal-voice-preview")?.setAttribute("hidden", ""); state.pendingDealVoiceTurn = null; }); function boot() { console.log("PitchFight frontend booting..."); setGlobalLoading(false); hideErrorBanner(); setBriefingMode("quick"); initBriefPreviewEditors(); initLandingViewport(); initLandingIntro(); initScorecardTabs(); initVoiceUI({ getSessionId: () => state.sessionId, getUiMode: () => state.uiMode, onPitchComplete: (data) => { fillVoiceExtractForm(data); applyVoicePitchToBriefing(data); showScreen("setup"); hideErrorBanner(); }, onTurnComplete: (data) => showVoiceTurnPreview(data), onRetryTurnComplete: (data) => showRetryVoicePreview(data), onDealTurnComplete: (data) => showDealVoicePreview(data), onError: (msg) => showErrorBanner(msg), }); fetch("/health") .then((response) => response.json()) .then((data) => console.log("Backend health:", data)) .catch((error) => { console.warn("Health check failed:", error); showErrorBanner( "Backend health check failed. Run python app.py and refresh this page." ); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } initPfFallCanvas();