Sergei e3192ead12 Voice call improvements: single call limit, hide debug tab, remove speaker toggle
Changes:
- Add CallManager singleton to ensure only 1 call per device at a time
- Hide Debug tab from production (href: null)
- Remove speaker/earpiece toggle button (always use speaker)
- Agent uses voice_ask API (fast ~1 sec latency)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-19 23:55:27 -08:00

112 lines
3.0 KiB
TypeScript

/**
* CallManager - Singleton to manage active voice calls
*
* Ensures only ONE voice call can be active at a time per device.
* If a new call is started while another is active, the old one is disconnected first.
*
* This addresses the LiveKit concurrent agent jobs limit (5 per project).
*/
type DisconnectCallback = () => Promise<void>;
class CallManager {
private static instance: CallManager;
private activeCallId: string | null = null;
private disconnectCallback: DisconnectCallback | null = null;
private constructor() {
// Singleton
}
static getInstance(): CallManager {
if (!CallManager.instance) {
CallManager.instance = new CallManager();
}
return CallManager.instance;
}
/**
* Register a new call. If there's an existing call, disconnect it first.
* @param callId Unique ID for this call
* @param onDisconnect Callback to disconnect this call
* @returns true if this call can proceed
*/
async registerCall(
callId: string,
onDisconnect: DisconnectCallback
): Promise<boolean> {
console.log(`[CallManager] Registering call: ${callId}`);
// If there's an active call, disconnect it first
if (this.activeCallId && this.activeCallId !== callId) {
console.log(
`[CallManager] Active call exists (${this.activeCallId}), disconnecting...`
);
if (this.disconnectCallback) {
try {
await this.disconnectCallback();
console.log(`[CallManager] Previous call disconnected`);
} catch (err) {
console.error(`[CallManager] Error disconnecting previous call:`, err);
}
}
}
// Register the new call
this.activeCallId = callId;
this.disconnectCallback = onDisconnect;
console.log(`[CallManager] Call ${callId} is now active`);
return true;
}
/**
* Unregister a call when it ends
* @param callId The call ID to unregister
*/
unregisterCall(callId: string): void {
if (this.activeCallId === callId) {
console.log(`[CallManager] Unregistering call: ${callId}`);
this.activeCallId = null;
this.disconnectCallback = null;
} else {
console.log(
`[CallManager] Call ${callId} is not active, ignoring unregister`
);
}
}
/**
* Check if there's an active call
*/
hasActiveCall(): boolean {
return this.activeCallId !== null;
}
/**
* Get the current active call ID
*/
getActiveCallId(): string | null {
return this.activeCallId;
}
/**
* Force disconnect the active call (if any)
*/
async forceDisconnect(): Promise<void> {
if (this.activeCallId && this.disconnectCallback) {
console.log(`[CallManager] Force disconnecting call: ${this.activeCallId}`);
try {
await this.disconnectCallback();
} catch (err) {
console.error(`[CallManager] Error force disconnecting:`, err);
}
this.activeCallId = null;
this.disconnectCallback = null;
}
}
}
export const callManager = CallManager.getInstance();