import "./styles.css"; import { ConversationController } from "./app/controller"; import { AppStore } from "./app/store"; import { CALIBRATION_INSTRUCTIONS, CALIBRATION_PROMPT, createInitialState, } from "./app/types"; import { TransformersAsrAdapter } from "./adapters/asr"; import { WebLLMChatAdapter } from "./adapters/llm"; import { PocketTTSAdapter } from "./adapters/tts"; import { BrowserAudioCaptureService } from "./services/audio-capture"; import { BrowserCapabilitiesService } from "./services/capabilities"; import { LocalPersistenceService } from "./services/persistence"; import { BrowserPlaybackService } from "./services/playback"; const store = new AppStore(createInitialState()); const controller = new ConversationController(store, { capabilities: new BrowserCapabilitiesService(), audioCapture: new BrowserAudioCaptureService(), asr: new TransformersAsrAdapter(store.getState().config.asrModelId), llm: new WebLLMChatAdapter(store.getState().config.llmModelId), tts: new PocketTTSAdapter(store.getState().config.ttsModelId), playback: new BrowserPlaybackService(), persistence: new LocalPersistenceService(), }); type ViewName = "calibration" | "assistant"; let currentView: ViewName = "calibration"; const calibrationView = document.querySelector("#calibration-view"); const assistantView = document.querySelector("#assistant-view"); const calibrationButton = document.querySelector("#calibration-button"); const calibrationCopy = document.querySelector("#calibration-copy"); const calibrationPrompt = document.querySelector("#calibration-prompt"); const calibrationFill = document.querySelector("#calibration-fill"); const calibrationTimer = document.querySelector("#calibration-timer"); const benchmarkSummary = document.querySelector("#benchmark-summary"); const calibrationError = document.querySelector("#calibration-error"); const transcriptLog = document.querySelector("#transcript-log"); const transcriptEmpty = document.querySelector("#transcript-empty"); const recordButton = document.querySelector("#record-button"); const assistantError = document.querySelector("#assistant-error"); if ( !calibrationView || !assistantView || !calibrationButton || !calibrationCopy || !calibrationPrompt || !calibrationFill || !calibrationTimer || !benchmarkSummary || !calibrationError || !transcriptLog || !transcriptEmpty || !recordButton || !assistantError ) { throw new Error("The demo shell is missing required DOM nodes."); } const isAssistantActivePhase = (phase: ReturnType["phase"]) => phase === "arming" || phase === "listening" || phase === "thinking" || phase === "speaking"; const render = () => { const state = store.getState(); calibrationView.hidden = currentView !== "calibration"; assistantView.hidden = currentView !== "assistant"; calibrationCopy.textContent = state.runtimeReady ? "Calibration is complete. Continue when you're ready to open the assistant." : state.phase === "benchmarking" ? "Calibration is running now. Keep reading the prompt clearly until the timer ends." : CALIBRATION_INSTRUCTIONS; calibrationPrompt.textContent = CALIBRATION_PROMPT; calibrationFill.style.width = `${Math.round(state.calibrationProgress * 100)}%`; calibrationTimer.textContent = state.phase === "benchmarking" ? state.calibrationRecording ? `${state.calibrationSecondsRemaining ?? 0}s remaining` : "Finalizing calibration..." : state.runtimeReady ? "Calibration complete." : "Calibration required."; benchmarkSummary.hidden = !state.benchmarkSummary; benchmarkSummary.textContent = state.benchmarkSummary ?? ""; calibrationButton.disabled = state.phase === "benchmarking"; calibrationButton.textContent = state.phase === "benchmarking" ? "Calibrating..." : state.runtimeReady ? "Continue" : "Calibrate"; transcriptLog.replaceChildren(); if (state.turns.length === 0) { transcriptEmpty.hidden = false; transcriptEmpty.textContent = state.runtimeReady ? 'Press Record and say "Hello, my name is _____."' : "Complete calibration first."; transcriptLog.append(transcriptEmpty); } else { transcriptEmpty.hidden = true; for (const turn of state.turns) { const element = document.createElement("article"); element.className = "turn"; element.dataset.role = turn.role; const header = document.createElement("div"); header.className = "turn-header"; header.innerHTML = `${turn.role}${turn.audioStatus}`; const body = document.createElement("div"); body.className = "turn-body"; body.textContent = turn.transcript || "…"; element.append(header, body); transcriptLog.append(element); } } recordButton.disabled = !state.runtimeReady || state.phase === "benchmarking"; recordButton.textContent = state.phase === "listening" || state.phase === "arming" ? "Stop Recording" : state.phase === "thinking" || state.phase === "speaking" ? "Stop" : "Record"; calibrationError.hidden = currentView !== "calibration" || !state.errorMessage; calibrationError.textContent = currentView === "calibration" ? state.errorMessage ?? "" : ""; assistantError.hidden = currentView !== "assistant" || !state.errorMessage; assistantError.textContent = currentView === "assistant" ? state.errorMessage ?? "" : ""; }; calibrationButton.addEventListener("click", () => { controller.setConsentAccepted(true); if (store.getState().runtimeReady) { currentView = "assistant"; render(); return; } void controller.runBenchmark(); }); recordButton.addEventListener("click", () => { controller.setConsentAccepted(true); void controller.start(); }); window.addEventListener("beforeunload", () => { void controller.dispose(); }); store.subscribe(() => { render(); }); void controller.bootstrap().then(() => { render(); }); if (import.meta.hot) { import.meta.hot.accept(); import.meta.hot.dispose(() => { void controller.dispose(); }); }