Skip to content

Commit f60f926

Browse files
feat(listener): enhance loading state events with context data and frontend wiring
- Add AudioMode enum for serializing ChannelMode - Enhance AudioReady event with mode and device fields - Add AudioError event for mic/device failures - Enhance Connected event with adapter field - Replace retry_count with is_retryable in ConnectionError - Wire up all new events in frontend general.ts - Add loadingPhase and lastError fields to live state Co-Authored-By: yujonglee <[email protected]>
1 parent 630bdab commit f60f926

File tree

4 files changed

+118
-10
lines changed

4 files changed

+118
-10
lines changed

apps/desktop/src/store/zustand/listener/general.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,25 @@ const hasSessionId = (
3434
): payload is SessionEvent & { session_id: string } =>
3535
"session_id" in payload && typeof payload.session_id === "string";
3636

37+
export type LoadingPhase =
38+
| "idle"
39+
| "initializing_audio"
40+
| "audio_ready"
41+
| "connecting"
42+
| "connected";
43+
3744
export type GeneralState = {
3845
live: {
3946
sessionEventUnlisten?: () => void;
4047
loading: boolean;
48+
loadingPhase: LoadingPhase;
4149
status: LiveSessionStatus;
4250
amplitude: { mic: number; speaker: number };
4351
seconds: number;
4452
intervalId?: NodeJS.Timeout;
4553
sessionId: string | null;
4654
muted: boolean;
55+
lastError: string | null;
4756
};
4857
};
4958

@@ -65,10 +74,12 @@ const initialState: GeneralState = {
6574
live: {
6675
status: "inactive",
6776
loading: false,
77+
loadingPhase: "idle",
6878
amplitude: { mic: 0, speaker: 0 },
6979
seconds: 0,
7080
sessionId: null,
7181
muted: false,
82+
lastError: null,
7283
},
7384
};
7485

@@ -155,6 +166,7 @@ export const createGeneralSlice = <
155166
mutate(state, (draft) => {
156167
draft.live.status = "running_active";
157168
draft.live.loading = false;
169+
draft.live.loadingPhase = "idle";
158170
draft.live.seconds = 0;
159171
draft.live.intervalId = intervalId;
160172
draft.live.sessionId = targetSessionId;
@@ -181,15 +193,55 @@ export const createGeneralSlice = <
181193
mutate(state, (draft) => {
182194
draft.live.status = "inactive";
183195
draft.live.loading = false;
196+
draft.live.loadingPhase = "idle";
184197
draft.live.sessionId = null;
185198
draft.live.sessionEventUnlisten = undefined;
199+
draft.live.lastError = null;
186200
}),
187201
);
188202

189203
get().resetTranscript();
190204
} else if (payload.type === "streamResponse") {
191205
const response = payload.response;
192206
get().handleTranscriptResponse(response as unknown as StreamResponse);
207+
} else if (payload.type === "initializingAudio") {
208+
set((state) =>
209+
mutate(state, (draft) => {
210+
draft.live.loadingPhase = "initializing_audio";
211+
draft.live.lastError = null;
212+
}),
213+
);
214+
} else if (payload.type === "audioReady") {
215+
set((state) =>
216+
mutate(state, (draft) => {
217+
draft.live.loadingPhase = "audio_ready";
218+
}),
219+
);
220+
} else if (payload.type === "audioError") {
221+
set((state) =>
222+
mutate(state, (draft) => {
223+
draft.live.lastError = payload.error;
224+
draft.live.loading = false;
225+
}),
226+
);
227+
} else if (payload.type === "connecting") {
228+
set((state) =>
229+
mutate(state, (draft) => {
230+
draft.live.loadingPhase = "connecting";
231+
}),
232+
);
233+
} else if (payload.type === "connected") {
234+
set((state) =>
235+
mutate(state, (draft) => {
236+
draft.live.loadingPhase = "connected";
237+
}),
238+
);
239+
} else if (payload.type === "connectionError") {
240+
set((state) =>
241+
mutate(state, (draft) => {
242+
draft.live.lastError = payload.error;
243+
}),
244+
);
193245
}
194246
};
195247

@@ -259,11 +311,13 @@ export const createGeneralSlice = <
259311

260312
draft.live.sessionEventUnlisten = undefined;
261313
draft.live.loading = false;
314+
draft.live.loadingPhase = "idle";
262315
draft.live.status = "inactive";
263316
draft.live.amplitude = { mic: 0, speaker: 0 };
264317
draft.live.seconds = 0;
265318
draft.live.sessionId = null;
266319
draft.live.muted = initialState.live.muted;
320+
draft.live.lastError = null;
267321
}),
268322
);
269323
},

plugins/listener/src/actors/listener.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ impl Actor for ListenerActor {
9696
tracing::error!(?error, "failed_to_emit_connecting");
9797
}
9898

99-
let (tx, rx_task, shutdown_tx) = spawn_rx_task(args.clone(), myself).await?;
99+
let (tx, rx_task, shutdown_tx, adapter_name) = spawn_rx_task(args.clone(), myself).await?;
100100

101101
if let Err(error) = (SessionEvent::Connected {
102102
session_id: args.session_id.clone(),
103+
adapter: adapter_name,
103104
})
104105
.emit(&args.app)
105106
{
@@ -213,13 +214,24 @@ async fn spawn_rx_task(
213214
ChannelSender,
214215
tokio::task::JoinHandle<()>,
215216
tokio::sync::oneshot::Sender<()>,
217+
String, // adapter name
216218
),
217219
ActorProcessingErr,
218220
> {
219221
let adapter_kind = AdapterKind::from_url_and_languages(&args.base_url, &args.languages);
220222
let is_dual = matches!(args.mode, crate::actors::ChannelMode::MicAndSpeaker);
221223

222-
match (adapter_kind, is_dual) {
224+
let adapter_name = match adapter_kind {
225+
AdapterKind::Argmax => "Argmax",
226+
AdapterKind::Soniox => "Soniox",
227+
AdapterKind::Fireworks => "Fireworks",
228+
AdapterKind::Deepgram => "Deepgram",
229+
AdapterKind::AssemblyAI => "AssemblyAI",
230+
AdapterKind::OpenAI => "OpenAI",
231+
AdapterKind::Gladia => "Gladia",
232+
};
233+
234+
let result = match (adapter_kind, is_dual) {
223235
(AdapterKind::Argmax, false) => {
224236
spawn_rx_task_single_with_adapter::<ArgmaxAdapter>(args, myself).await
225237
}
@@ -262,7 +274,9 @@ async fn spawn_rx_task(
262274
(AdapterKind::Gladia, true) => {
263275
spawn_rx_task_dual_with_adapter::<GladiaAdapter>(args, myself).await
264276
}
265-
}
277+
}?;
278+
279+
Ok((result.0, result.1, result.2, adapter_name.to_string()))
266280
}
267281

268282
fn build_listen_params(args: &ListenerArgs) -> owhisper_interface::ListenParams {
@@ -330,7 +344,7 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
330344
let _ = (SessionEvent::ConnectionError {
331345
session_id: args.session_id.clone(),
332346
error: "listen_ws_connect_timeout".to_string(),
333-
retry_count: 0,
347+
is_retryable: true,
334348
})
335349
.emit(&args.app);
336350
return Err(actor_error("listen_ws_connect_timeout"));
@@ -340,7 +354,7 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
340354
let _ = (SessionEvent::ConnectionError {
341355
session_id: args.session_id.clone(),
342356
error: format!("listen_ws_connect_failed: {:?}", e),
343-
retry_count: 0,
357+
is_retryable: true,
344358
})
345359
.emit(&args.app);
346360
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));
@@ -402,7 +416,7 @@ async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
402416
let _ = (SessionEvent::ConnectionError {
403417
session_id: args.session_id.clone(),
404418
error: "listen_ws_connect_timeout".to_string(),
405-
retry_count: 0,
419+
is_retryable: true,
406420
})
407421
.emit(&args.app);
408422
return Err(actor_error("listen_ws_connect_timeout"));
@@ -412,7 +426,7 @@ async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
412426
let _ = (SessionEvent::ConnectionError {
413427
session_id: args.session_id.clone(),
414428
error: format!("listen_ws_connect_failed: {:?}", e),
415-
retry_count: 0,
429+
is_retryable: true,
416430
})
417431
.emit(&args.app);
418432
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));

plugins/listener/src/actors/source.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,12 @@ impl Actor for SourceActor {
195195
}
196196
SourceMsg::StreamFailed(reason) => {
197197
tracing::error!(%reason, "source_stream_failed_stopping");
198+
let _ = (SessionEvent::AudioError {
199+
session_id: st.session_id.clone(),
200+
error: reason.clone(),
201+
device: st.mic_device.clone(),
202+
})
203+
.emit(&st.app);
198204
myself.stop(Some(reason));
199205
}
200206
}
@@ -240,6 +246,8 @@ async fn start_source_loop(
240246
if result.is_ok() {
241247
if let Err(error) = (SessionEvent::AudioReady {
242248
session_id: st.session_id.clone(),
249+
mode: st.current_mode.into(),
250+
device: st.mic_device.clone(),
243251
})
244252
.emit(&st.app)
245253
{

plugins/listener/src/events.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@ macro_rules! common_event_derives {
88
};
99
}
1010

11+
/// Audio channel mode for the session
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
13+
#[serde(rename_all = "snake_case")]
14+
pub enum AudioMode {
15+
MicOnly,
16+
SpeakerOnly,
17+
MicAndSpeaker,
18+
}
19+
20+
impl From<crate::actors::ChannelMode> for AudioMode {
21+
fn from(mode: crate::actors::ChannelMode) -> Self {
22+
match mode {
23+
crate::actors::ChannelMode::MicOnly => AudioMode::MicOnly,
24+
crate::actors::ChannelMode::SpeakerOnly => AudioMode::SpeakerOnly,
25+
crate::actors::ChannelMode::MicAndSpeaker => AudioMode::MicAndSpeaker,
26+
}
27+
}
28+
}
29+
1130
common_event_derives! {
1231
#[serde(tag = "type")]
1332
pub enum SessionEvent {
@@ -33,16 +52,29 @@ common_event_derives! {
3352
#[serde(rename = "initializingAudio")]
3453
InitializingAudio { session_id: String },
3554
#[serde(rename = "audioReady")]
36-
AudioReady { session_id: String },
55+
AudioReady {
56+
session_id: String,
57+
mode: AudioMode,
58+
device: Option<String>,
59+
},
60+
#[serde(rename = "audioError")]
61+
AudioError {
62+
session_id: String,
63+
error: String,
64+
device: Option<String>,
65+
},
3766
#[serde(rename = "connecting")]
3867
Connecting { session_id: String },
3968
#[serde(rename = "connected")]
40-
Connected { session_id: String },
69+
Connected {
70+
session_id: String,
71+
adapter: String,
72+
},
4173
#[serde(rename = "connectionError")]
4274
ConnectionError {
4375
session_id: String,
4476
error: String,
45-
retry_count: u32,
77+
is_retryable: bool,
4678
},
4779
ExitRequested
4880
}

0 commit comments

Comments
 (0)