From 326817733a6cdfa58fc31e5ea0fb500aa14a42b7 Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Tue, 2 Jun 2026 11:28:25 +0200 Subject: [PATCH] main: implement the ability to replay eva utterances via list selection --- src/main.rs | 170 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 110 insertions(+), 60 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3c12c15..b7659e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -81,6 +81,13 @@ struct App { audio_level: f64, recording_audio: bool, audio_control_sink: tokio::sync::mpsc::Sender, + focus_state: FocusState +} + +#[derive(Debug)] +enum FocusState { + Conversation, + UserInput } impl App { @@ -89,6 +96,7 @@ impl App { scene: Scene::default(), next_reply_options: Vec::new(), reply_state: ListState::default(), + conversation_state: ListState::default(), user_input: Input::default(), end_time: Utc::now() + Duration::hours(2), throbber_state: ThrobberState::default(), @@ -97,10 +105,11 @@ impl App { audio_level: -60., recording_audio: false, audio_control_sink, + focus_state: FocusState::UserInput } } - fn draw_conversation(&self, frame: &mut Frame, area: Rect) { + fn draw_conversation(&mut self, frame: &mut Frame, area: Rect) { let items: Vec = self.scene.conversation.iter().rev().map(|entry| { match entry { ConversationEntry::User(text) => Line::from_iter([Span::from("Argee: ").style(ratatui::style::Color::Magenta), Span::from(text)]), @@ -110,7 +119,17 @@ impl App { ConversationEntry::SystemMessage(text) => Line::from_iter([text]).style(ratatui::style::Color::DarkGray) } }).collect(); - frame.render_widget(List::new(items).block(Block::bordered().border_style(style::Color::LightYellow).title("Conversation")).direction(ListDirection::BottomToTop), area); + // FIXME: We need to somehow make long list items wrap. https://github.com/ratatui/ratatui/issues/128#issuecomment-1613918499 + // TODO: Would be nice to be able to scroll a longer conversation with the scroll wheel, or with page up/down + frame.render_stateful_widget( + List::new(items) + .block(Block::bordered().border_style(style::Color::LightYellow).title("Conversation (Press Tab to select and read Eva's lines aloud)")) + .direction(ListDirection::BottomToTop) + .highlight_symbol("> ") + .highlight_style(style::Style::new().bold().fg(style::Color::Cyan)), + area, + &mut self.conversation_state + ); } fn draw_options(&mut self, frame: &mut Frame, area: Rect) { @@ -261,67 +280,98 @@ impl App { async fn on_event(&mut self, evt: event::Event) { if let Some(key) = evt.as_key_press_event() { - match key.code { - KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => self.regenerate_responses(), - KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => { - if self.recording_audio { - self.recording_audio = false; - self.audio_control_sink.send(AudioRecordRequest::Finish).await.unwrap(); - self.is_requesting = true; - } else { - self.recording_audio = true; - self.audio_control_sink.send(AudioRecordRequest::Start).await.unwrap(); - } - }, - KeyCode::Down => self.reply_state.select_next(), - KeyCode::Up => self.reply_state.select_previous(), - KeyCode::Tab => { - self.insert_selected_prompt(); - }, - KeyCode::Enter => { - let next_msg = self.user_input.value_and_reset(); - if next_msg.trim().is_empty() { - self.insert_selected_prompt(); - } else { - if next_msg.starts_with("/") { - let mut parts = next_msg.splitn(2, " "); - let command = parts.next().unwrap(); - let arg = parts.next().unwrap_or(""); - match command { - "/bandcamp" => { - self.add_bandcamp_artifact(arg).await; - self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Added Bandcamp artifact from {}", arg))); - }, - "/episode" => { - if let Ok(episode_number) = arg.trim().parse::() { - self.scene.direction.episode_number = episode_number; - self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number))); - self.reload_mixxx_playlist(); - } else { - self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into())); - } - }, - "/reset" => { - self.scene = Scene::default(); - return; - }, - "/narrative" => { - self.scene.direction.narrative = arg.to_string(); - self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated stage direction: {}", self.scene.direction.narrative))); - }, - _ => { - self.scene.insert_conversation(ConversationEntry::SystemMessage("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset".into())); - return; - } + match self.focus_state { + FocusState::Conversation => { + match key.code { + KeyCode::Tab => { + self.focus_state = FocusState::UserInput; + self.conversation_state.select(None); + }, + KeyCode::PageUp => self.conversation_state.scroll_down_by(5), + KeyCode::PageDown => self.conversation_state.scroll_up_by(5), + KeyCode::Home => self.conversation_state.select_last(), + KeyCode::End => self.conversation_state.select_first(), + KeyCode::Down => self.conversation_state.select_previous(), + KeyCode::Up => self.conversation_state.select_next(), + KeyCode::Enter => { + let row_num = self.conversation_state.selected().unwrap(); + if let ConversationEntry::Eva(text) = &self.scene.conversation[self.scene.conversation.len() - 1 - row_num] { + self.speak(text.clone().as_str()); + self.focus_state = FocusState::UserInput; + self.conversation_state.select(None); } - } else { - self.scene.insert_conversation(ConversationEntry::User(next_msg)); - } - self.save(); - self.regenerate_responses(); + }, + _ => () } }, - _ => {self.user_input.handle_event(&evt);}, + FocusState::UserInput => { + match key.code { + KeyCode::Tab => { + self.focus_state = FocusState::Conversation; + self.conversation_state.select_first(); + }, + KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => self.regenerate_responses(), + KeyCode::Char('x') if key.modifiers.contains(KeyModifiers::CONTROL) => { + if self.recording_audio { + self.recording_audio = false; + self.audio_control_sink.send(AudioRecordRequest::Finish).await.unwrap(); + self.is_requesting = true; + } else { + self.recording_audio = true; + self.audio_control_sink.send(AudioRecordRequest::Start).await.unwrap(); + } + }, + KeyCode::Down => self.reply_state.select_next(), + KeyCode::Up => self.reply_state.select_previous(), + KeyCode::Enter if key.modifiers.contains(KeyModifiers::CONTROL) => { + self.insert_selected_prompt(); + }, + KeyCode::Enter => { + let next_msg = self.user_input.value_and_reset(); + if next_msg.trim().is_empty() { + self.insert_selected_prompt(); + } else { + if next_msg.starts_with("/") { + let mut parts = next_msg.splitn(2, " "); + let command = parts.next().unwrap(); + let arg = parts.next().unwrap_or(""); + match command { + "/bandcamp" => { + self.add_bandcamp_artifact(arg).await; + self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Added Bandcamp artifact from {}", arg))); + }, + "/episode" => { + if let Ok(episode_number) = arg.trim().parse::() { + self.scene.direction.episode_number = episode_number; + self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated episode number: {}", self.scene.direction.episode_number))); + self.reload_mixxx_playlist(); + } else { + self.scene.insert_conversation(ConversationEntry::SystemMessage("Invalid episode number format. Use /episode [number]".into())); + } + }, + "/reset" => { + self.scene = Scene::default(); + return; + }, + "/narrative" => { + self.scene.direction.narrative = arg.to_string(); + self.scene.insert_conversation(ConversationEntry::SystemMessage(format!("Updated stage direction: {}", self.scene.direction.narrative))); + }, + _ => { + self.scene.insert_conversation(ConversationEntry::SystemMessage("Unknown command. Available commands: /bandcamp [url], /episode [number], /narrative [text], /reset".into())); + return; + } + } + } else { + self.scene.insert_conversation(ConversationEntry::User(next_msg)); + } + self.save(); + self.regenerate_responses(); + } + }, + _ => {self.user_input.handle_event(&evt);}, + } + } } } }