const state = { data: null, selectedRequestId: "", selectedSlotId: "", selectedPrice: 0, activeScenario: "normal", activeSlotFilter: "morning", mobilePanel: "phone", variationIndex: 0, }; const els = { shell: document.querySelector(".app-shell"), todayLabel: document.querySelector("#todayLabel"), channelStatus: document.querySelector("#channelStatus"), channelCopy: document.querySelector("#channelCopy"), pendingCount: document.querySelector("#pendingCount"), scenarioButtons: document.querySelector("#scenarioButtons"), requestQueue: document.querySelector("#requestQueue"), chatPhone: document.querySelector("#chatPhone"), chatThread: document.querySelector("#chatThread"), detectedIntent: document.querySelector("#detectedIntent"), confidenceValue: document.querySelector("#confidenceValue"), extractedSlots: document.querySelector("#extractedSlots"), missingList: document.querySelector("#missingList"), traceList: document.querySelector("#traceList"), timelineState: document.querySelector("#timelineState"), bookingId: document.querySelector("#bookingId"), availabilityBoard: document.querySelector("#availabilityBoard"), packetList: document.querySelector("#packetList"), groupA: document.querySelector("#groupA"), groupB: document.querySelector("#groupB"), suggestedPrice: document.querySelector("#suggestedPrice"), integrationsList: document.querySelector("#integrationsList"), replyDraft: document.querySelector("#replyDraft"), charCount: document.querySelector("#charCount"), decisionState: document.querySelector("#decisionState"), approveButton: document.querySelector("#approveButton"), clarifyButton: document.querySelector("#clarifyButton"), rejectButton: document.querySelector("#rejectButton"), resetDemoButton: document.querySelector("#resetDemoButton"), suggestButton: document.querySelector("#suggestButton"), }; function formatPrice(value) { return new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0, }).format(value); } function slotKey(venueId, slotId) { return `${venueId}:${slotId}`; } function getSelectedRequest() { return state.data?.requests.find((request) => request.id === state.selectedRequestId) || null; } function getSelectedSlot() { if (!state.data || !state.selectedSlotId) return null; const [venueId, slotId] = state.selectedSlotId.split(":"); const venue = state.data.venues.find((item) => item.id === venueId); const slot = state.data.slots.find((item) => item.id === slotId); const inventory = venue?.slots?.[slotId]; if (!venue || !slot || !inventory) return null; return { venue, slot, inventory }; } async function loadScenario(scenario = "normal") { const response = await fetch(`/api/bootstrap?scenario=${encodeURIComponent(scenario)}`); state.data = await response.json(); state.activeScenario = state.data.active_scenario; state.selectedRequestId = state.data.decision.selected_request_id; state.selectedSlotId = state.data.decision.selected_slot_id; state.selectedPrice = state.data.decision.selected_price; state.activeSlotFilter = state.selectedSlotId.split(":")[1] || "morning"; state.variationIndex = 0; render(); } function render() { renderRuntime(); renderScenarios(); renderRequestSelect(); renderPhone(); renderAgentPanel(); renderOwnerPanel(); renderDecisionBar(); renderMobilePanel(); } function renderRuntime() { const runtime = state.data.runtime; els.todayLabel.textContent = getSelectedRequest()?.date_label || "25 May 2025, Sun"; els.channelStatus.textContent = runtime.channel_label; els.channelCopy.textContent = runtime.channel_copy; els.pendingCount.textContent = `${runtime.pending_count} pending`; els.shell.classList.toggle("is-provider-error", runtime.channel_status === "provider-error"); } function renderScenarios() { els.scenarioButtons.replaceChildren( ...state.data.scenarios.map((scenario) => { const button = document.createElement("button"); button.type = "button"; button.textContent = scenario.label; button.className = scenario.id === state.activeScenario ? "is-active" : ""; button.addEventListener("click", () => loadScenario(scenario.id)); return button; }) ); } function renderRequestSelect() { if (!state.data.requests.length) { const option = new Option("No booking requests", ""); els.requestQueue.replaceChildren(option); els.requestQueue.disabled = true; return; } els.requestQueue.disabled = false; els.requestQueue.replaceChildren( ...state.data.requests.map((request) => { const label = `${request.customer_name} · ${request.activity} · ${stateLabel(request.status)}`; const option = new Option(label, request.id, false, request.id === state.selectedRequestId); return option; }) ); } function renderPhone() { const request = getSelectedRequest(); els.chatPhone.textContent = request?.phone || "No active conversation"; if (!request) { els.chatThread.replaceChildren(emptyPanel("No active booking chat. Pick a scenario or wait for a new request.")); return; } const selected = getSelectedSlot(); const locationText = selected?.venue.name || "North Field or South Field"; const transcript = [ { who: "customer", text: `Hi, need a ground for ${request.activity.toLowerCase()} ${request.date_label.toLowerCase()}.`, time: "11:34 AM" }, { who: "agent", text: "Hi! Sure, what time are you looking for?", time: "11:35 AM" }, { who: "customer", text: `${selected?.slot.label || "8 AM - 12 PM"}. For ${request.players} players.`, time: "11:35 AM" }, { who: "agent", text: "Which location?", time: "11:36 AM" }, { who: "customer", text: `${locationText} works.`, time: "11:36 AM" }, { who: "agent", text: "Any surface preference?", time: "11:37 AM" }, { who: "customer", text: `${selected?.venue.surface || "Grass"} is fine. Budget around ${formatPrice(state.selectedPrice || selected?.inventory.rate || 6000)}.`, time: "11:38 AM" }, { who: "agent", text: "Got it. Let me check availability and send options.", time: "11:38 AM" }, ]; const day = document.createElement("div"); day.className = "chat-day"; day.textContent = "Today"; els.chatThread.replaceChildren(day, ...transcript.map(chatBubble)); } function chatBubble(message) { const div = document.createElement("div"); div.className = `bubble ${message.who === "agent" ? "agent" : ""}`; div.innerHTML = `${message.text}`; return div; } function renderAgentPanel() { const request = getSelectedRequest(); const selected = getSelectedSlot(); const confidence = confidenceFor(request); els.detectedIntent.textContent = request ? "BOOK_SLOT" : "IDLE"; els.confidenceValue.textContent = confidence.toFixed(2); document.querySelector(".confidence-meter span").style.width = `${Math.round(confidence * 100)}%`; if (!request) { els.extractedSlots.replaceChildren(); els.missingList.replaceChildren(emptyListItem("Waiting for the next booking request.")); els.traceList.replaceChildren(...state.data.trace.map(traceItem)); els.timelineState.textContent = "Idle"; return; } const extracted = [ ["date", request.date_label], ["time", selected?.slot.label || "Not selected"], ["players", String(request.players)], ["activity", request.activity], ["location", selected?.venue.name || "Any venue"], ["surface", selected ? `${selected.venue.surface} · ${selected.venue.grass_type}` : "Grass"], ["budget", `${formatPrice(state.selectedPrice || selected?.inventory.rate || 0)} approx`], ]; els.extractedSlots.replaceChildren(...extracted.flatMap(([label, value]) => slotRow(label, value))); const missing = request.missing_fields.length ? request.missing_fields : ["Backup slot preference", "Add-ons final confirmation"]; els.missingList.replaceChildren(...missing.map((item) => listItem(item))); els.traceList.replaceChildren(...buildTimeline(request).map(traceItem)); els.timelineState.textContent = state.data.decision.state === "sent" ? "Sent" : timelineCopy(request); } function renderOwnerPanel() { const request = getSelectedRequest(); els.bookingId.textContent = request ? `Booking ID: BK-${request.id.slice(0, 2).toUpperCase()}51872` : "Booking ID: none"; renderAvailabilityBoard(); renderPacket(); renderGroups(request); renderIntegrations(); } function renderAvailabilityBoard() { const slot = state.data.slots.find((item) => item.id === state.activeSlotFilter) || state.data.slots[0]; document.querySelectorAll("[data-slot-filter]").forEach((button) => { button.classList.toggle("is-active", button.dataset.slotFilter === slot.id); }); const rows = state.data.venues.map((venue) => { const inventory = venue.slots[slot.id]; const key = slotKey(venue.id, slot.id); const button = document.createElement("button"); button.type = "button"; button.className = `venue-row ${key === state.selectedSlotId ? "is-selected" : ""}`; button.innerHTML = ` ${venue.name} ${venue.surface} · ${venue.grass_type} ${formatPrice(inventory.rate)} ${venue.grass_type} ${inventoryLabel(inventory)} `; button.addEventListener("click", () => { state.selectedSlotId = key; state.selectedPrice = inventory.rate; render(); }); return button; }); els.availabilityBoard.replaceChildren(...rows); } function renderPacket() { const selected = getSelectedSlot(); const packet = [ ["Umpire", "₹700"], ["Scorekeeper", "₹300"], ["Match ball", "₹250"], ["Water", "₹200"], ]; els.packetList.replaceChildren( ...packet.map(([label, price]) => { const item = document.createElement("div"); item.className = "packet-item"; item.innerHTML = `${label}${price}`; return item; }) ); const base = selected?.inventory.rate || 0; els.suggestedPrice.textContent = `Slot ${formatPrice(base)} · Total add-ons: ${formatPrice(1450)}`; } function renderGroups(request) { const groupA = request ? [ ["Group name", request.group_type], ["Primary contact", request.phone], ["Activity", request.activity], ["Players", String(request.players)], ] : [["State", "No request selected"]]; const groupB = request ? [ ["Group name", "To be confirmed"], ["Status", request.missing_fields.length ? "Needs clarification" : "Ready"], ["Venue", getSelectedSlot()?.venue.name || "Not selected"], ["Slot", getSelectedSlot()?.slot.label || "Not selected"], ] : [["State", "No request selected"]]; els.groupA.replaceChildren(...definitionRows(groupA)); els.groupB.replaceChildren(...definitionRows(groupB)); } function renderIntegrations() { els.integrationsList.replaceChildren( ...state.data.integrations.map((integration) => { const row = document.createElement("div"); row.className = "integration-row"; row.innerHTML = `${integration.label}${integration.copy}`; return row; }) ); } function renderDecisionBar() { const request = getSelectedRequest(); const providerError = state.data.runtime.channel_status === "provider-error"; if (document.activeElement !== els.replyDraft) { els.replyDraft.value = request?.reply_draft || "Pick a request to review the reply draft."; } updateCharCount(); els.decisionState.textContent = decisionCopy(state.data.decision.state, request); const disabled = !request || providerError; els.approveButton.disabled = disabled; els.clarifyButton.disabled = !request; els.rejectButton.disabled = !request; } function slotRow(label, value) { const dt = document.createElement("dt"); const dd = document.createElement("dd"); const ok = document.createElement("span"); dt.textContent = label; dd.textContent = value; ok.className = "ok-mark"; ok.textContent = "✓"; return [dt, dd, ok]; } function definitionRows(rows) { return rows.flatMap(([label, value]) => { const dt = document.createElement("dt"); const dd = document.createElement("dd"); dt.textContent = label; dd.textContent = value; return [dt, dd]; }); } function listItem(text) { const li = document.createElement("li"); li.textContent = text; return li; } function emptyListItem(text) { return listItem(text); } function emptyPanel(text) { const div = document.createElement("div"); div.className = "empty-state"; div.textContent = text; return div; } function traceItem(event) { const li = document.createElement("li"); li.className = event.tone; li.innerHTML = `${event.label}`; return li; } function buildTimeline(request) { const selected = getSelectedSlot(); return [ { id: "hello", label: `Message received from ${request.customer_name}`, tone: "neutral", timestamp_label: "11:34:12" }, { id: "time", label: `Extracted ${selected?.slot.label || "requested slot"}`, tone: "success", timestamp_label: "11:35:04" }, { id: "players", label: `User: ${request.players} players`, tone: "success", timestamp_label: "11:35:16" }, { id: "venue", label: `Venue candidate: ${selected?.venue.name || "Any venue"}`, tone: "success", timestamp_label: "11:36:02" }, ...state.data.trace, ]; } function confidenceFor(request) { if (!request) return 0; if (state.data.runtime.channel_status === "provider-error") return 0.64; if (request.status === "clarification") return 0.78; if (request.status === "conflict") return 0.86; return 0.92; } function timelineCopy(request) { if (!request) return "Idle"; if (request.status === "conflict") return "Alternatives Needed"; if (request.missing_fields.length) return "Clarify First"; return "Ready for Reply"; } function stateLabel(status) { const labels = { new: "New", clarification: "Clarify", conflict: "Conflict", sent: "Sent", rejected: "Rejected", }; return labels[status] || status; } function inventoryLabel(inventory) { if (inventory.status === "partial") return `${inventory.request_count} holds`; if (inventory.status === "conflict") return "Conflict"; if (inventory.status === "booked") return "Booked"; return "Available"; } function decisionCopy(decisionState, request) { if (!request) return "Idle. No outbound reply can be sent."; if (decisionState === "sent") return "Sent via simulator. No live WhatsApp proof claimed."; if (decisionState === "rejected") return "Rejected by owner."; if (decisionState === "error") return "Provider error. Retry before sending."; if (request.missing_fields.length) return "Clarification needed before confirmation."; if (request.status === "conflict") return "Conflict detected. Offer alternatives."; return "Pending owner approval."; } function renderMobilePanel() { document.querySelectorAll("[data-panel]").forEach((panel) => { panel.classList.toggle("is-mobile-active", panel.dataset.panel === state.mobilePanel); }); document.querySelectorAll("[data-mobile-panel-target]").forEach((tab) => { tab.classList.toggle("is-active", tab.dataset.mobilePanelTarget === state.mobilePanel); }); } function updateCharCount() { els.charCount.textContent = `Characters: ${els.replyDraft.value.length}`; } function attachEvents() { document.querySelectorAll("[data-mobile-panel-target]").forEach((tab) => { tab.addEventListener("click", () => { state.mobilePanel = tab.dataset.mobilePanelTarget; renderMobilePanel(); }); }); document.querySelectorAll("[data-slot-filter]").forEach((button) => { button.addEventListener("click", () => { state.activeSlotFilter = button.dataset.slotFilter; renderAvailabilityBoard(); }); }); els.requestQueue.addEventListener("change", () => { state.selectedRequestId = els.requestQueue.value; const request = getSelectedRequest(); if (request) { state.selectedSlotId = request.requested_slot_id; state.activeSlotFilter = state.selectedSlotId.split(":")[1] || "morning"; const selected = getSelectedSlot(); state.selectedPrice = selected?.inventory.rate || 0; } render(); }); els.replyDraft.addEventListener("input", updateCharCount); els.suggestButton.addEventListener("click", () => { const request = getSelectedRequest(); const selected = getSelectedSlot(); if (!request || !selected) return; const variants = [ `Great, ${selected.venue.name} is available for ${request.activity.toLowerCase()} on ${request.date_label} from ${selected.slot.label}. The slot is ${formatPrice(state.selectedPrice)}. Should I hold it after owner approval?`, `I can offer ${selected.venue.name}, ${selected.slot.label}, ${selected.venue.grass_type} surface at ${formatPrice(state.selectedPrice)}. Please confirm and we will proceed after owner approval.`, `Available option: ${selected.venue.name} on ${request.date_label}, ${selected.slot.label}, for ${request.players} players. Estimated slot cost is ${formatPrice(state.selectedPrice)} plus selected add-ons.`, ]; state.variationIndex = (state.variationIndex + 1) % variants.length; els.replyDraft.value = variants[state.variationIndex]; updateCharCount(); }); els.approveButton.addEventListener("click", () => { if (!state.data || state.data.runtime.channel_status === "provider-error") return; const request = getSelectedRequest(); if (!request) return; state.data.decision.state = "sending"; state.data.trace.push({ id: "sending", label: "Owner approved; simulator send in progress", tone: "neutral", timestamp_label: "now" }); renderDecisionBar(); renderAgentPanel(); setTimeout(() => { state.data.decision.state = "sent"; request.status = "sent"; request.tone = "ready"; request.reply_draft = els.replyDraft.value; state.data.trace.push({ id: "sent", label: "Reply marked sent via simulator", tone: "success", timestamp_label: "now" }); render(); }, 650); }); els.clarifyButton.addEventListener("click", () => { const request = getSelectedRequest(); if (!request) return; request.status = "clarification"; request.tone = "clarification"; request.reply_draft = els.replyDraft.value || `Hi ${request.customer_name.split(" ")[0]}, could you confirm the missing details before I ask the owner to hold this slot?`; state.data.trace.push({ id: "edited-send", label: "Edited reply prepared for simulator send", tone: "warning", timestamp_label: "now" }); render(); }); els.rejectButton.addEventListener("click", () => { const request = getSelectedRequest(); if (!request) return; request.status = "rejected"; request.tone = "rejected"; state.data.decision.state = "rejected"; state.data.trace.push({ id: "rejected", label: "Owner rejected the outbound reply", tone: "error", timestamp_label: "now" }); render(); }); els.resetDemoButton.addEventListener("click", () => loadScenario("normal")); } attachEvents(); loadScenario("normal");