Spaces:
Running
Running
| 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<HTMLElement>("#calibration-view"); | |
| const assistantView = document.querySelector<HTMLElement>("#assistant-view"); | |
| const calibrationButton = | |
| document.querySelector<HTMLButtonElement>("#calibration-button"); | |
| const calibrationCopy = | |
| document.querySelector<HTMLParagraphElement>("#calibration-copy"); | |
| const calibrationPrompt = | |
| document.querySelector<HTMLParagraphElement>("#calibration-prompt"); | |
| const calibrationFill = | |
| document.querySelector<HTMLDivElement>("#calibration-fill"); | |
| const calibrationTimer = | |
| document.querySelector<HTMLParagraphElement>("#calibration-timer"); | |
| const benchmarkSummary = | |
| document.querySelector<HTMLParagraphElement>("#benchmark-summary"); | |
| const calibrationError = | |
| document.querySelector<HTMLDivElement>("#calibration-error"); | |
| const transcriptLog = document.querySelector<HTMLDivElement>("#transcript-log"); | |
| const transcriptEmpty = | |
| document.querySelector<HTMLDivElement>("#transcript-empty"); | |
| const recordButton = document.querySelector<HTMLButtonElement>("#record-button"); | |
| const assistantError = | |
| document.querySelector<HTMLDivElement>("#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<typeof store.getState>["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 = `<span>${turn.role}</span><span>${turn.audioStatus}</span>`; | |
| 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(); | |
| }); | |
| } | |