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 = `${escapeHtml(text)}
${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 = `${escapeHtml(overallNote)}
` : ""}