feat(frontend): render Vocero screens and controls with React

This commit is contained in:
2026-10-06 16:24:50 -03:00
parent 833788b8d8
commit 36efd5e13c
18 changed files with 7977 additions and 18 deletions
+13 -6
View File
@@ -132,6 +132,8 @@ export class RealtimeClient {
url = grant.connect_url;
}
this.ensureOpen();
this.socket = new WebSocket(url);
this.socket.binaryType = "arraybuffer";
const ready = new Promise<void>((resolve, reject) => {
this.resolveReady = resolve;
this.rejectReady = reject;
@@ -150,8 +152,6 @@ export class RealtimeClient {
},
() => this.currentStatus === "ai-speaking",
);
this.socket = new WebSocket(url);
this.socket.binaryType = "arraybuffer";
this.socket.addEventListener("message", (event: MessageEvent<unknown>) => {
this.incoming = this.incoming
.then(async () => {
@@ -182,8 +182,9 @@ export class RealtimeClient {
await Promise.all([audioReady, ready]);
this.ensureOpen();
} catch (error) {
const failure = this.closed ? new SessionError("aborted", "La conexión se canceló.") : error;
await this.close();
throw error;
throw failure;
}
}
@@ -386,7 +387,8 @@ export class RealtimeClient {
break;
case "conversation.item.input_audio_transcription.delta": {
const itemId = event.item_id ?? "user-current";
const text = (this.userSegments.get(itemId) ?? "") + (event.delta ?? "");
// This server sends the complete speculative transcription in each delta.
const text = event.delta ?? "";
this.userSegments.set(itemId, text);
this.emit("transcript", { role: "user", text, partial: true, id: itemId });
break;
@@ -445,7 +447,9 @@ export class RealtimeClient {
if (this.activeResponse === id) {
this.activeResponse = "";
}
this.setStatus("listening");
if (this.currentStatus === "ai-speaking" || this.currentStatus === "processing") {
this.setStatus("listening");
}
this.flush();
break;
}
@@ -509,6 +513,9 @@ export class RealtimeClient {
});
}
requestResponse() {
if (this.closed || !this.configured) {
return;
}
if (this.busy()) {
this.pendingCreates++;
return;
@@ -536,7 +543,7 @@ export class RealtimeClient {
renderExternalAnswer(text: string) {
if (this.activeResponse) {
this.suppressed.add(this.activeResponse);
} else if (this.createInFlight) {
} else {
this.suppressNext = true;
}
this.audio.clear();