feat(frontend): port realtime audio and session handling to TypeScript

This commit is contained in:
2026-10-06 16:10:30 -03:00
parent 36e2bee885
commit 833788b8d8
13 changed files with 2302 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import { z } from "zod";
export const recordSchema = z.record(z.string(), z.unknown());
export type Profile = z.infer<typeof recordSchema>;
export const configSchema = z.object({
search: z.boolean().default(false),
lb: z.boolean().default(false),
allowDirect: z.boolean().default(true),
s2sUrl: z.string().default(""),
primaryPrompt: z.string().default(""),
reasoningType: z.enum(["PROXY", "OPENAI", "NODE_RED"]).catch("PROXY"),
auth: z.boolean().default(false),
injectProfile: z.boolean().default(false),
debugPrompt: z.boolean().default(false),
startSessionMode: z.string().default(""),
theme: z.string().default(""),
apipilot: z.boolean().default(false),
});
export type ServerConfig = z.infer<typeof configSchema>;
export const defaultConfig = configSchema.parse({});
export const reportsSchema = z.object({
count: z.number().default(0),
hints: z.array(z.string()).default([]),
entries: z
.array(
z.object({
sourceScheduleLabel: z.string().optional(),
date: z.union([z.string(), z.number()]).nullish(),
}),
)
.default([]),
reason: z.string().optional(),
truncated: z.boolean().optional(),
note: z.string().optional(),
});
export type Reports = z.infer<typeof reportsSchema>;
export const accountSchema = z.object({
enabled: z.boolean(),
loggedIn: z.boolean().default(false),
auth: z.boolean().default(false),
tier: z.string().default("anon"),
username: z.string().nullish(),
avatar: z.string().nullish(),
loginUrl: z.string().nullish(),
logoutUrl: z.string().nullish(),
remainingSec: z.number().nullish(),
limitSec: z.number().nullish(),
});
export type AccountInfo = z.infer<typeof accountSchema>;
export const toolSchema = z.object({
name: z.string(),
description: z.string().default(""),
parameters: recordSchema.default({}),
requires_auth: z.boolean().default(false),
});
export type DynamicTool = z.infer<typeof toolSchema>;
export interface FunctionTool {
type: "function";
name: string;
description: string;
parameters: Record<string, unknown>;
}
export const toolsEnvelopeSchema = z.object({
success: z.boolean(),
data: z.object({ tools: z.array(toolSchema), count: z.number().optional() }).nullish(),
error: z.unknown().optional(),
});
export const grantSchema = z.object({
session_id: z.string(),
connect_url: z.string(),
pending_timeout_s: z.number().default(60),
tier: z.string().default("anon"),
limited: z.boolean().default(false),
heartbeatSec: z.number().default(5),
remainingSec: z.number().nullish(),
});
export type SessionGrant = z.infer<typeof grantSchema>;
export const queueSchema = z.object({
state: z.literal("queued"),
queue_id: z.string(),
position: z.number().default(0),
poll_interval_s: z.number().default(2),
});
export const sessionSchema = z.union([queueSchema, grantSchema]);
export type ConnectionStatus =
| "idle"
| "connecting"
| "queued"
| "your-turn"
| "listening"
| "user-speaking"
| "processing"
| "ai-speaking"
| "closed"
| "error";
export interface Transcript {
role: "user" | "assistant";
text: string;
partial: boolean;
id: string;
}
export interface FinishedResponse {
responseId: string;
status: string;
transcript: string;
audible: boolean;
}
export interface ToolCall {
name: string;
arguments: string;
callId: string;
}
export interface NoiseGate {
enabled: boolean;
thresholdDb: number;
}
const contentSchema = z.object({
type: z.string().optional(),
text: z.string().optional(),
transcript: z.string().optional(),
});
// The wire protocol is additive; unknown event fields are ignored, while consumed fields are validated.
export const realtimeEventSchema = z.object({
type: z.string(),
response_id: z.string().optional(),
item_id: z.string().optional(),
delta: z.string().optional(),
transcript: z.string().optional(),
text: z.string().optional(),
name: z.string().optional(),
arguments: z.string().optional(),
call_id: z.string().optional(),
response: z
.object({
id: z.string().optional(),
status: z.string().optional(),
output: z.array(z.object({ content: z.array(contentSchema).optional() })).optional(),
})
.optional(),
part: contentSchema.optional(),
error: z
.object({ type: z.string().optional(), code: z.string().optional(), message: z.string().optional() })
.optional(),
});
export type RealtimeEvent = z.infer<typeof realtimeEventSchema>;