Skip to content

Commit 630bdab

Browse files
feat(listener): add loading state events for better UI feedback
Add new SessionEvent variants to provide visibility into the connection and initialization phases between 'start listening' and 'got first transcript': - InitializingAudio: emitted when SourceActor starts audio initialization - AudioReady: emitted after audio streams are successfully set up - Connecting: emitted when ListenerActor starts WebSocket connection - Connected: emitted after successful WebSocket connection - ConnectionError: emitted when connection fails (with error details) These events allow the UI to show appropriate loading states during the 1-8 second window that was previously a blind spot. Co-Authored-By: yujonglee <[email protected]>
1 parent 1d16ec8 commit 630bdab

File tree

3 files changed

+77
-1
lines changed

3 files changed

+77
-1
lines changed

plugins/listener/src/actors/listener.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,24 @@ impl Actor for ListenerActor {
8888
myself: ActorRef<Self::Msg>,
8989
args: Self::Arguments,
9090
) -> Result<Self::State, ActorProcessingErr> {
91+
if let Err(error) = (SessionEvent::Connecting {
92+
session_id: args.session_id.clone(),
93+
})
94+
.emit(&args.app)
95+
{
96+
tracing::error!(?error, "failed_to_emit_connecting");
97+
}
98+
9199
let (tx, rx_task, shutdown_tx) = spawn_rx_task(args.clone(), myself).await?;
92100

101+
if let Err(error) = (SessionEvent::Connected {
102+
session_id: args.session_id.clone(),
103+
})
104+
.emit(&args.app)
105+
{
106+
tracing::error!(?error, "failed_to_emit_connected");
107+
}
108+
93109
let state = ListenerState {
94110
args,
95111
tx,
@@ -311,10 +327,22 @@ async fn spawn_rx_task_single_with_adapter<A: RealtimeSttAdapter>(
311327
timeout_secs = LISTEN_CONNECT_TIMEOUT.as_secs_f32(),
312328
"listen_ws_connect_timeout(single)"
313329
);
330+
let _ = (SessionEvent::ConnectionError {
331+
session_id: args.session_id.clone(),
332+
error: "listen_ws_connect_timeout".to_string(),
333+
retry_count: 0,
334+
})
335+
.emit(&args.app);
314336
return Err(actor_error("listen_ws_connect_timeout"));
315337
}
316338
Ok(Err(e)) => {
317339
tracing::error!(error = ?e, "listen_ws_connect_failed(single)");
340+
let _ = (SessionEvent::ConnectionError {
341+
session_id: args.session_id.clone(),
342+
error: format!("listen_ws_connect_failed: {:?}", e),
343+
retry_count: 0,
344+
})
345+
.emit(&args.app);
318346
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));
319347
}
320348
Ok(Ok(res)) => res,
@@ -371,10 +399,22 @@ async fn spawn_rx_task_dual_with_adapter<A: RealtimeSttAdapter>(
371399
timeout_secs = LISTEN_CONNECT_TIMEOUT.as_secs_f32(),
372400
"listen_ws_connect_timeout(dual)"
373401
);
402+
let _ = (SessionEvent::ConnectionError {
403+
session_id: args.session_id.clone(),
404+
error: "listen_ws_connect_timeout".to_string(),
405+
retry_count: 0,
406+
})
407+
.emit(&args.app);
374408
return Err(actor_error("listen_ws_connect_timeout"));
375409
}
376410
Ok(Err(e)) => {
377411
tracing::error!(error = ?e, "listen_ws_connect_failed(dual)");
412+
let _ = (SessionEvent::ConnectionError {
413+
session_id: args.session_id.clone(),
414+
error: format!("listen_ws_connect_failed: {:?}", e),
415+
retry_count: 0,
416+
})
417+
.emit(&args.app);
378418
return Err(actor_error(format!("listen_ws_connect_failed: {:?}", e)));
379419
}
380420
Ok(Ok(res)) => res,

plugins/listener/src/actors/source.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ pub struct SourceArgs {
4343
}
4444

4545
pub struct SourceState {
46+
app: tauri::AppHandle,
4647
session_id: String,
4748
mic_device: Option<String>,
4849
onboarding: bool,
@@ -123,6 +124,14 @@ impl Actor for SourceActor {
123124
myself: ActorRef<Self::Msg>,
124125
args: Self::Arguments,
125126
) -> Result<Self::State, ActorProcessingErr> {
127+
if let Err(error) = (SessionEvent::InitializingAudio {
128+
session_id: args.session_id.clone(),
129+
})
130+
.emit(&args.app)
131+
{
132+
tracing::error!(?error, "failed_to_emit_initializing_audio");
133+
}
134+
126135
let device_watcher = DeviceChangeWatcher::spawn(myself.clone());
127136

128137
let silence_stream_tx = Some(hypr_audio::AudioOutput::silence());
@@ -134,6 +143,7 @@ impl Actor for SourceActor {
134143
let pipeline = Pipeline::new(args.app.clone(), args.session_id.clone());
135144

136145
let mut st = SourceState {
146+
app: args.app,
137147
session_id: args.session_id,
138148
mic_device,
139149
onboarding: args.onboarding,
@@ -221,11 +231,23 @@ async fn start_source_loop(
221231

222232
st.pipeline.reset();
223233

224-
match new_mode {
234+
let result = match new_mode {
225235
ChannelMode::MicOnly => start_source_loop_mic_only(myself, st).await,
226236
ChannelMode::SpeakerOnly => start_source_loop_speaker_only(myself, st).await,
227237
ChannelMode::MicAndSpeaker => start_source_loop_mic_and_speaker(myself, st).await,
238+
};
239+
240+
if result.is_ok() {
241+
if let Err(error) = (SessionEvent::AudioReady {
242+
session_id: st.session_id.clone(),
243+
})
244+
.emit(&st.app)
245+
{
246+
tracing::error!(?error, "failed_to_emit_audio_ready");
247+
}
228248
}
249+
250+
result
229251
}
230252

231253
async fn start_source_loop_mic_only(

plugins/listener/src/events.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@ common_event_derives! {
3030
session_id: String,
3131
response: Box<StreamResponse>,
3232
},
33+
#[serde(rename = "initializingAudio")]
34+
InitializingAudio { session_id: String },
35+
#[serde(rename = "audioReady")]
36+
AudioReady { session_id: String },
37+
#[serde(rename = "connecting")]
38+
Connecting { session_id: String },
39+
#[serde(rename = "connected")]
40+
Connected { session_id: String },
41+
#[serde(rename = "connectionError")]
42+
ConnectionError {
43+
session_id: String,
44+
error: String,
45+
retry_count: u32,
46+
},
3347
ExitRequested
3448
}
3549
}

0 commit comments

Comments
 (0)