const state = { packet: null, view: "ready", mode: "hinglish", recorder: null, chunks: [], startedAt: 0, lastTrace: null, transcript: "", modelMessage: "", modelRuntime: "hf_space", transcriptOpen: false, simulateNextSendFailure: false, }; const els = { brandName: document.querySelector("#brand-name"), runtimePillLabel: document.querySelector("#runtime-pill-label"), heading: document.querySelector("#widget-heading"), subheading: document.querySelector("#widget-subheading"), modeButtons: [...document.querySelectorAll("[data-mode]")], runtimeSwitch: document.querySelector("#runtime-switch"), composePanel: document.querySelector("#compose-panel"), micPanel: document.querySelector("#mic-panel"), recordingPanel: document.querySelector("#recording-panel"), processingPanel: document.querySelector("#processing-panel"), reviewPanel: document.querySelector("#review-panel"), successPanel: document.querySelector("#success-panel"), simpleErrorPanel: document.querySelector("#simple-error-panel"), draftErrorPanel: document.querySelector("#draft-error-panel"), stateLabel: document.querySelector("#state-label"), helperCopy: document.querySelector("#helper-copy"), recordButton: document.querySelector("#record-button"), recordDisabledReason: document.querySelector("#record-disabled-reason"), stopButton: document.querySelector("#stop-button"), waveform: document.querySelector("#waveform"), progressItems: [...document.querySelectorAll(".progress-list li")], reviewHead: document.querySelector("#review-head"), sendErrorBanner: document.querySelector("#send-error-banner"), messageEditor: document.querySelector("#message-editor"), transcriptToggle: document.querySelector("#transcript-toggle"), transcriptBox: document.querySelector("#transcript-box"), transcriptMode: document.querySelector("#transcript-mode"), transcriptOutput: document.querySelector("#transcript-output"), reviewActions: document.querySelector("#review-actions"), sendingActions: document.querySelector("#sending-actions"), sendFailureActions: document.querySelector("#send-failure-actions"), sendButton: document.querySelector("#send-button"), retrySendButton: document.querySelector("#retry-send-button"), copyButton: document.querySelector("#copy-button"), copyConfirmation: document.querySelector("#copy-confirmation"), recordAgainButton: document.querySelector("#record-again-button"), terminalTitle: document.querySelector("#terminal-title"), terminalCopy: document.querySelector("#terminal-copy"), newMessageButton: document.querySelector("#new-message-button"), errorTitle: document.querySelector("#error-title"), errorCopy: document.querySelector("#error-copy"), simpleErrorIcon: document.querySelector("#simple-error-icon"), retryButton: document.querySelector("#retry-button"), draftTranscriptMode: document.querySelector("#draft-transcript-mode"), draftTranscriptOutput: document.querySelector("#draft-transcript-output"), copyTranscriptButton: document.querySelector("#copy-transcript-button"), retryDraftButton: document.querySelector("#retry-draft-button"), draftRecordAgainButton: document.querySelector("#draft-record-again-button"), transcriptCopyConfirmation: document.querySelector("#transcript-copy-confirmation"), debugDetails: document.querySelector("#debug-details"), debugGrid: document.querySelector("#debug-grid"), debugButtons: [...document.querySelectorAll("[data-simulate]")], }; const viewCopy = { ready: ["Ready to record", "Review happens before anything is sent."], editing: ["Editing", "Your changes will be sent with the same trace id."], }; const errorCopy = { "err-mic": { title: "Microphone access is blocked.", body: "Allow microphone access in your browser settings, then retry.", icon: "mic", }, "err-empty": { title: "We did not catch that.", body: "Record again and speak for at least a moment.", icon: "alert", }, "err-asr": { title: "We could not understand the recording.", body: "Record again.", icon: "alert", }, }; function setHidden(element, hidden) { if (element) element.hidden = hidden; } function setView(view) { state.view = view; setHidden(els.composePanel, view !== "ready"); setHidden(els.micPanel, view !== "mic-pending"); setHidden(els.recordingPanel, view !== "recording"); setHidden(els.processingPanel, view !== "processing"); setHidden(els.reviewPanel, !["review", "editing", "sending", "err-send"].includes(view)); setHidden(els.successPanel, view !== "success"); setHidden(els.simpleErrorPanel, !["err-mic", "err-empty", "err-asr"].includes(view)); setHidden(els.draftErrorPanel, view !== "err-draft"); renderReviewState(); renderSimpleError(); updateDebug({ ui_state: view }); } function renderReadyCopy() { const [label, helper] = viewCopy[state.view] || viewCopy.ready; els.stateLabel.textContent = label; els.helperCopy.textContent = helper; } function renderSimpleError() { const data = errorCopy[state.view]; if (!data) return; els.errorTitle.textContent = data.title; els.errorCopy.textContent = data.body; els.simpleErrorIcon.replaceChildren(errorIcon(data.icon)); } function errorIcon(kind) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 24 24"); if (kind === "mic") { [ ["line", { x1: "3", y1: "3", x2: "21", y2: "21" }], ["path", { d: "M9 9v3a3 3 0 0 0 5.12 2.12M15 9.34V5a3 3 0 0 0-5.94-.6" }], ["path", { d: "M17 16.95A7 7 0 0 1 5 12v-1m14 0v1a7 7 0 0 1-.11 1.23" }], ["line", { x1: "12", y1: "19", x2: "12", y2: "22" }], ].forEach(([tag, attrs]) => svg.append(svgChild(tag, attrs))); return svg; } [ ["circle", { cx: "12", cy: "12", r: "9" }], ["line", { x1: "12", y1: "8", x2: "12", y2: "12.5" }], ["line", { x1: "12", y1: "16", x2: "12", y2: "16" }], ].forEach(([tag, attrs]) => svg.append(svgChild(tag, attrs))); return svg; } function svgChild(tag, attrs) { const child = document.createElementNS("http://www.w3.org/2000/svg", tag); Object.entries(attrs).forEach(([key, value]) => child.setAttribute(key, value)); return child; } function renderReviewState() { const view = state.view; const sendFailed = view === "err-send"; setHidden(els.reviewHead, sendFailed); setHidden(els.sendErrorBanner, !sendFailed); setHidden(els.reviewActions, !["review", "editing"].includes(view)); setHidden(els.sendingActions, view !== "sending"); setHidden(els.sendFailureActions, !sendFailed); } function modeLabel() { return state.mode === "hindi" ? "Hindi" : "Hinglish"; } function selectMode(mode) { state.mode = mode === "hindi" ? "hindi" : "hinglish"; els.modeButtons.forEach((button) => { const selected = button.dataset.mode === state.mode; button.classList.toggle("is-selected", selected); button.setAttribute("aria-checked", String(selected)); }); els.transcriptMode.textContent = `${modeLabel()} transcript`; els.draftTranscriptMode.textContent = `${modeLabel()} transcript`; els.transcriptBox.dataset.mode = state.mode; updateDebug(); } function resetForModeChange(mode) { if (state.view !== "ready") resetFlow(); selectMode(mode); } function renderRuntimeControls(controls = {}) { const options = Array.isArray(controls.options) ? controls.options : []; els.runtimeSwitch.replaceChildren(); options.forEach((option) => { const button = document.createElement("button"); button.className = "runtime-button"; button.type = "button"; button.role = "radio"; button.dataset.runtime = option.value; button.disabled = !option.enabled; button.title = option.disabled_reason || option.label; button.setAttribute("aria-checked", "false"); const name = document.createElement("span"); name.className = "runtime-name"; name.textContent = option.label; button.append(name); if (option.note) { const note = document.createElement("span"); note.className = "runtime-note"; note.textContent = option.note; button.append(note); } button.addEventListener("click", () => selectModelRuntime(option.value)); els.runtimeSwitch.append(button); }); selectModelRuntime(controls.selected || "hf_space", { silent: true }); } function selectModelRuntime(runtime, { silent = false } = {}) { const buttons = [...els.runtimeSwitch.querySelectorAll("[data-runtime]")]; const requested = buttons.find((button) => button.dataset.runtime === runtime); const selectedRuntime = requested && !requested.disabled ? runtime : "hf_space"; state.modelRuntime = selectedRuntime; buttons.forEach((button) => { const selected = button.dataset.runtime === selectedRuntime; button.classList.toggle("is-selected", selected); button.setAttribute("aria-checked", String(selected)); }); if (!silent) resetFlow(); renderRuntimePill(); updateRecordAvailability(); updateDebug({ model_runtime_changed: !silent }); } function runtimePillText() { if (state.modelRuntime === "modal") { const option = selectedRuntimeOption(); return option?.available === false ? "Modal setup" : "Modal warm"; } if (state.modelRuntime === "hf_personal_space") return "HF personal"; if (state.modelRuntime === "hf_space") return "HF model"; return "Voice"; } function renderRuntimePill() { if (els.runtimePillLabel) els.runtimePillLabel.textContent = runtimePillText(); } function applyTokens(tokens) { const root = document.documentElement; Object.entries(tokens || {}).forEach(([key, value]) => { root.style.setProperty(`--${key.replaceAll("_", "-")}`, value); }); } function applyPacket(packet) { state.packet = packet; applyTokens(packet.design_tokens); els.brandName.textContent = packet.brand_name; els.heading.textContent = packet.copy.heading; els.subheading.textContent = packet.copy.subheading; els.recordButton.querySelector("span").textContent = packet.copy.record_cta; selectMode(packet.locale_defaults.default_speech_mode); renderRuntimeControls(packet.runtime_controls); renderReadyCopy(); updateRecordAvailability(); updateDebug({ packet_loaded: true }); } async function api(path, payload) { const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await response.json(); if (!response.ok) { const error = new Error(data.error || data.detail || data.blocker || "Request failed"); error.status = response.status; error.data = data; throw error; } return data; } function sleep(ms) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const value = String(reader.result || ""); resolve(value.includes(",") ? value.split(",", 2)[1] : value); }; reader.onerror = reject; reader.readAsDataURL(blob); }); } function selectedRuntimeOption() { const options = state.packet?.runtime_controls?.options || []; return options.find((option) => option.value === state.modelRuntime) || null; } function recordDisabledReason() { if (state.recorder?.state === "recording") return ""; const option = selectedRuntimeOption(); if (option && option.available === false) { return option.disabled_reason || "Selected model runtime is not available."; } return ""; } function updateRecordAvailability() { if (!state.packet) return; const reason = recordDisabledReason(); els.recordButton.disabled = Boolean(reason); els.recordDisabledReason.hidden = !reason; els.recordDisabledReason.textContent = reason ? `Why it's disabled: ${reason}` : ""; updateDebug({ record_disabled_reason: reason || null }); } function visitorNoticeShown() { return false; } function setProgressStep(activeIndex) { els.progressItems.forEach((item, index) => { item.classList.toggle("is-done", index < activeIndex); item.classList.toggle("is-active", index === activeIndex); }); } async function mirrorPendingWork(requestPromise) { const checkpoints = [900, 2400]; for (let index = 0; index < checkpoints.length; index += 1) { const result = await Promise.race([ requestPromise.then(() => "done", () => "done"), sleep(checkpoints[index]).then(() => "pending"), ]); if (result === "done") return; setProgressStep(index + 1); } } function buildWaveform() { const bars = []; for (let index = 0; index < 30; index += 1) { const bar = document.createElement("span"); bar.style.animationDelay = `${(index * 0.045).toFixed(3)}s`; bars.push(bar); } els.waveform.replaceChildren(...bars); } async function startRecording() { if (recordDisabledReason()) { updateRecordAvailability(); return; } if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === "undefined") { showError("err-mic"); return; } state.transcriptOpen = false; setView("mic-pending"); try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); state.chunks = []; state.recorder = new MediaRecorder(stream); state.recorder.ondataavailable = (event) => { if (event.data.size > 0) state.chunks.push(event.data); }; state.recorder.onstop = () => finishRecording(stream); state.startedAt = Date.now(); state.recorder.start(); setView("recording"); } catch (_error) { updateRecordAvailability(); showError("err-mic"); } } function stopRecording() { if (state.recorder?.state === "recording") { els.stopButton.disabled = true; state.recorder.stop(); } } async function finishRecording(stream) { stream.getTracks().forEach((track) => track.stop()); const durationMs = Date.now() - state.startedAt; const mimeType = state.recorder?.mimeType || "audio/webm"; const blob = new Blob(state.chunks, { type: mimeType }); state.recorder = null; els.stopButton.disabled = false; updateRecordAvailability(); if (durationMs < 700 || blob.size < 128) { showError("err-empty"); return; } const audioBase64 = await blobToBase64(blob); await processRecording({ duration_ms: durationMs, audio_size_bytes: blob.size, audio_mime: blob.type || mimeType, audio_base64: audioBase64, }); } async function processRecording(extra = {}) { try { setView("processing"); setProgressStep(0); const requestPromise = api("/api/process", { speech_mode: state.mode, model_runtime: state.modelRuntime, visitor_notice_shown: visitorNoticeShown(), ...extra, }); await mirrorPendingWork(requestPromise); const result = await requestPromise; showReviewResult(result); } catch (error) { if (extra.simulate === "text_failure") { state.transcript = state.transcript || "Namaste, mujhe metime.to ke baare mein founders se baat karni hai."; showDraftError(error.message); return; } showError("err-asr", error.message); } } async function processTextDraft() { if (!state.transcript) { resetFlow(); return; } try { setView("processing"); setProgressStep(2); const result = await api("/api/text-smoke", { speech_mode: state.mode, model_runtime: state.modelRuntime, transcript: state.transcript, visitor_notice_shown: visitorNoticeShown(), }); showReviewResult(result); } catch (error) { showDraftError(error.message); } } function showReviewResult(result) { state.lastTrace = result.trace; state.transcript = result.transcript || ""; state.modelMessage = result.model_message_en || ""; els.messageEditor.value = state.modelMessage; els.transcriptOutput.textContent = state.transcript; els.draftTranscriptOutput.textContent = state.transcript; setProgressStep(3); renderTranscript(); setView("review"); updateDebug({ transcript: state.transcript }); } function showDraftError(detail = "") { els.draftTranscriptOutput.textContent = state.transcript || "Transcript unavailable."; renderTranscript(); setView("err-draft"); updateDebug({ draft_error: detail || null }); } async function sendMessage() { setView("sending"); try { const result = await api("/api/send", { site_id: state.packet.site_id, contact_email: state.packet.contact_email, model_message_en: state.modelMessage, final_message_en: els.messageEditor.value, trace_id: state.lastTrace?.trace_id, simulate: state.simulateNextSendFailure ? "send_failure" : null, }); state.simulateNextSendFailure = false; els.terminalTitle.textContent = "Message sent."; els.terminalCopy.textContent = result.visitor_copy || "Thanks. The team will get back to you."; setView("success"); updateDebug({ delivery: "demo_ledger", send_result: result.status }); } catch (error) { state.simulateNextSendFailure = false; setView("err-send"); updateDebug({ send_error: error.message }); } } function showError(kind, detail = "") { const data = errorCopy[kind] || errorCopy["err-asr"]; els.errorTitle.textContent = data.title; els.errorCopy.textContent = detail ? `${data.body} ${detail}` : data.body; els.simpleErrorIcon.replaceChildren(errorIcon(data.icon)); setView(kind); } function resetFlow() { state.chunks = []; state.recorder = null; state.startedAt = 0; state.lastTrace = null; state.transcript = ""; state.modelMessage = ""; state.transcriptOpen = false; state.simulateNextSendFailure = false; els.messageEditor.value = ""; els.transcriptOutput.textContent = ""; els.draftTranscriptOutput.textContent = ""; els.copyConfirmation.hidden = true; els.transcriptCopyConfirmation.hidden = true; renderTranscript(); renderReadyCopy(); setView("ready"); updateRecordAvailability(); } function renderTranscript() { els.transcriptToggle.setAttribute("aria-expanded", String(state.transcriptOpen)); els.transcriptToggle.querySelector("span").textContent = state.transcriptOpen ? "Hide transcript" : "Show transcript"; els.transcriptBox.hidden = !state.transcriptOpen; els.transcriptOutput.textContent = state.transcript || ""; els.draftTranscriptOutput.textContent = state.transcript || ""; els.transcriptMode.textContent = `${modeLabel()} transcript`; els.draftTranscriptMode.textContent = `${modeLabel()} transcript`; els.transcriptBox.dataset.mode = state.mode; } async function copyText(value, confirmationElement, copiedLabel = null) { await navigator.clipboard.writeText(value || ""); confirmationElement.hidden = false; if (copiedLabel) copiedLabel.textContent = "Copied"; window.setTimeout(() => { confirmationElement.hidden = true; if (copiedLabel) copiedLabel.textContent = "Copy message"; }, 1200); } function debugRows(extra = {}) { const runtime = state.packet?.provenance || {}; const trace = state.lastTrace || {}; return { "site id": state.packet?.site_id || "loading", "packet": state.packet?.schema_version || "loading", "speech mode": state.mode, "selected runtime": state.modelRuntime, "runtime allowed": String(Boolean(state.packet?.runtime_controls?.allow_switch)), "request runtime": trace.model_runtime || state.modelRuntime || "unknown", "server default": runtime.model_runtime || "unknown", "backend": trace.model_backend || runtime.model_backend || "unknown", "fallback": String(trace.fallback_used ?? runtime.fallback_used ?? "unknown"), "trace id": trace.trace_id || "none", "timings": JSON.stringify(trace.timings_ms || {}), "eval retention": String(Boolean(state.packet?.retention?.retain_raw_audio_for_eval)), "asr model": trace.asr_model_id || runtime.asr_model_id || "unknown", "text model": trace.text_model_id || runtime.text_model_id || "unknown", ...extra, }; } function updateDebug(extra = {}) { const rows = debugRows(extra); const fragments = Object.entries(rows).map(([key, value]) => { const row = document.createElement("div"); const dt = document.createElement("dt"); const dd = document.createElement("dd"); dt.textContent = key; dd.textContent = String(value ?? ""); row.append(dt, dd); return row; }); els.debugGrid.replaceChildren(...fragments); } function wireEvents() { els.modeButtons.forEach((button) => { button.addEventListener("click", () => resetForModeChange(button.dataset.mode)); }); els.recordButton.addEventListener("click", startRecording); els.stopButton.addEventListener("click", stopRecording); els.messageEditor.addEventListener("input", () => { state.view = "editing"; renderReviewState(); updateDebug({ ui_state: "editing" }); }); els.transcriptToggle.addEventListener("click", () => { state.transcriptOpen = !state.transcriptOpen; renderTranscript(); }); els.sendButton.addEventListener("click", sendMessage); els.retrySendButton.addEventListener("click", sendMessage); els.recordAgainButton.addEventListener("click", resetFlow); els.draftRecordAgainButton.addEventListener("click", resetFlow); els.newMessageButton.addEventListener("click", resetFlow); els.retryButton.addEventListener("click", resetFlow); els.retryDraftButton.addEventListener("click", processTextDraft); els.copyButton.addEventListener("click", () => copyText(els.messageEditor.value, els.copyConfirmation, els.copyButton)); els.copyTranscriptButton.addEventListener("click", () => copyText(state.transcript, els.transcriptCopyConfirmation)); els.debugButtons.forEach((button) => { button.addEventListener("click", async () => { const simulation = button.dataset.simulate; if (simulation === "mic-denied") showError("err-mic"); if (simulation === "empty-audio") showError("err-empty"); if (simulation === "asr-failure") await processRecording({ simulate: "asr_failure" }); if (simulation === "text-failure") { state.transcript = "Namaste, mujhe metime.to ke baare mein founders se baat karni hai."; await processRecording({ simulate: "text_failure", transcript: state.transcript }); } if (simulation === "send-failure") { state.simulateNextSendFailure = true; if (!state.lastTrace) { state.lastTrace = { trace_id: "debug-send-failure" }; state.modelMessage = "Hello metime.to team,\n\nPlease contact me about metime.to.\n\nThank you."; state.transcript = "Hi, main metime.to ke baare mein founders se connect karna chahta hoon."; els.messageEditor.value = state.modelMessage; } await sendMessage(); } }); }); } async function boot() { buildWaveform(); wireEvents(); updateDebug(); const packet = await fetch("/api/packet").then((response) => response.json()); applyPacket(packet); setView("ready"); } boot().catch((error) => { els.errorTitle.textContent = "Widget could not start."; els.errorCopy.textContent = error.message; els.simpleErrorIcon.replaceChildren(errorIcon("alert")); setView("err-asr"); });