prediction: rewrite the prompting stack to create the ship computer as a second character, and let characters/agents operate autonomously with their own tasks and event queues
This commit is contained in:
@@ -1,23 +1,26 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use crossterm::event::{self, KeyCode, KeyModifiers};
|
||||
use ratatui::{Frame, layout::{Direction, Layout, Rect}, style::{self, }, text::Span, widgets::{BorderType, Clear, ListState, Paragraph}};
|
||||
use ratatui::{Frame, layout::{Direction, Layout, Rect}, style::{self, Style, Stylize, }, text::Span, widgets::{BorderType, Clear, List, ListState, Paragraph}};
|
||||
use throbber_widgets_tui::{Throbber, ThrobberState};
|
||||
use tokio::time::Instant;
|
||||
use tui_input::{Input, backend::crossterm::EventHandler};
|
||||
use tui_skeleton::{AnimationMode, Block, Constraint, SkeletonText};
|
||||
|
||||
use crate::{audio::AudioInputControl, prediction::{PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::ConversationEntry}, transcription::TranscriptionControl, tts::TtsControl};
|
||||
use crate::{audio::AudioInputControl, prediction::{GeneratedResponses, PredictionAction, SessionControl, SessionUpdate}, scene::{Scene, conversation::{ConversationEntry, Speaker}}, transcription::TranscriptionControl, tts::TtsControl};
|
||||
use crate::widgets::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Ui {
|
||||
scene: Scene,
|
||||
|
||||
reply_options: GeneratedResponses,
|
||||
|
||||
reply_state: ListState,
|
||||
conversation_state: ListState,
|
||||
user_input: Input,
|
||||
throbber_state: ThrobberState,
|
||||
is_requesting: bool,
|
||||
computer_is_thinking: bool,
|
||||
audio_level: f64,
|
||||
recording_audio: bool,
|
||||
focus_state: FocusState,
|
||||
@@ -45,6 +48,7 @@ impl Ui {
|
||||
user_input: Default::default(),
|
||||
throbber_state: Default::default(),
|
||||
is_requesting: false,
|
||||
computer_is_thinking: false,
|
||||
audio_level: -60.,
|
||||
audio,
|
||||
recording_audio: false,
|
||||
@@ -53,7 +57,8 @@ impl Ui {
|
||||
tts,
|
||||
predictions,
|
||||
last_tick: Instant::now(),
|
||||
conversation: vec![]
|
||||
conversation: vec![],
|
||||
reply_options: Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +72,7 @@ impl Ui {
|
||||
.block(borders);
|
||||
frame.render_widget(list, area);
|
||||
} else {
|
||||
frame.render_stateful_widget(Options(self.scene.reply_options()), area, &mut self.reply_state);
|
||||
frame.render_stateful_widget(Options(&self.reply_options.responses), area, &mut self.reply_state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +88,17 @@ impl Ui {
|
||||
fn draw_io_throbber(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let throb_area = area.centered(Constraint::Max(1), Constraint::Max(1));
|
||||
if self.is_requesting {
|
||||
let throbber = Throbber::default();
|
||||
let throbber = Throbber::default().throbber_style(Style::new().cyan());
|
||||
frame.render_stateful_widget(throbber, throb_area, &mut self.throbber_state);
|
||||
} else {
|
||||
frame.render_widget(Clear::default(), throb_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_computer_throbber(&mut self, frame: &mut Frame, area: Rect) {
|
||||
let throb_area = area.centered(Constraint::Min(1), Constraint::Min(1));
|
||||
if self.computer_is_thinking {
|
||||
let throbber = Throbber::default().throbber_style(Style::new().red()).throbber_set(throbber_widgets_tui::WHITE_SQUARE);
|
||||
frame.render_stateful_widget(throbber, throb_area, &mut self.throbber_state);
|
||||
} else {
|
||||
frame.render_widget(Clear::default(), throb_area);
|
||||
@@ -113,31 +128,34 @@ impl Ui {
|
||||
|
||||
let scene_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Fill(4), Constraint::Fill(1)])
|
||||
.constraints([Constraint::Fill(5), Constraint::Fill(2)])
|
||||
.split(layout[0]);
|
||||
|
||||
frame.render_stateful_widget(Conversation(&self.conversation), scene_layout[0], &mut self.conversation_state);
|
||||
self.draw_narration(frame, scene_layout[1]);
|
||||
//self.draw_narration(frame, scene_layout[1]);
|
||||
//self.draw_tasklist(frame, scene_layout[1]);
|
||||
frame.render_widget(TaskList(&self.scene.computer_task_list), scene_layout[1]);
|
||||
self.draw_options(frame, layout[1]);
|
||||
|
||||
let status_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Max(3), Constraint::Fill(2), Constraint::Max(13), Constraint::Min(50)])
|
||||
.constraints([Constraint::Max(3), Constraint::Max(3), Constraint::Fill(2), Constraint::Max(13), Constraint::Min(50)])
|
||||
.split(layout[3]);
|
||||
|
||||
self.draw_user_input(frame, layout[2]);
|
||||
self.draw_io_throbber(frame, status_layout[0]);
|
||||
frame.render_widget(StatusBar(&self.scene), status_layout[1]);
|
||||
frame.render_widget(RecordingStatus(self.recording_audio), status_layout[2]);
|
||||
frame.render_widget(Volume(self.audio_level, self.recording_audio), status_layout[3]);
|
||||
self.draw_computer_throbber(frame, status_layout[1]);
|
||||
frame.render_widget(StatusBar(&self.scene), status_layout[2]);
|
||||
frame.render_widget(RecordingStatus(self.recording_audio), status_layout[3]);
|
||||
frame.render_widget(Volume(self.audio_level, self.recording_audio), status_layout[4]);
|
||||
}
|
||||
|
||||
async fn insert_selected_prompt(&mut self) {
|
||||
let selected = self.scene.reply_options()[self.reply_state.selected().unwrap()].clone();
|
||||
let selected = self.reply_options.responses[self.reply_state.selected().unwrap()].clone();
|
||||
if let Some(direction) = &selected.stage_direction {
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::StageDirection(direction.clone()))).await;
|
||||
}
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::Eva(selected.text.clone()))).await;
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::Spoken(Speaker::Eva, selected.text.clone()))).await;
|
||||
}
|
||||
|
||||
async fn on_command(&mut self, command: &str) {
|
||||
@@ -174,7 +192,8 @@ impl Ui {
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::StageDirection(arg.to_string()))).await;
|
||||
},
|
||||
"/computer" => {
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::ShipComputer(arg.to_string()))).await;
|
||||
self.predictions.insert(PredictionAction::ComputerCommand(arg.to_string())).await;
|
||||
return;
|
||||
},
|
||||
_ => {
|
||||
log::error!("Unknown command. Available commands: /episode [number], /narrative [text], /event [text], /computer [text], /timer [minutes]");
|
||||
@@ -183,6 +202,7 @@ impl Ui {
|
||||
}
|
||||
|
||||
pub async fn on_event(&mut self, evt: event::Event) {
|
||||
// TODO: ctrl+l should drop all the SystemMessage entries from the log to clean up the UI
|
||||
if let Some(key) = evt.as_key_press_event() {
|
||||
match self.focus_state {
|
||||
FocusState::Conversation => {
|
||||
@@ -200,7 +220,7 @@ impl Ui {
|
||||
KeyCode::Up => self.conversation_state.select_next(),
|
||||
KeyCode::Enter => {
|
||||
let row_num = self.conversation_state.selected().unwrap();
|
||||
if let ConversationEntry::Eva(text) = &self.conversation[self.conversation.len() - 1 - row_num] {
|
||||
if let ConversationEntry::Spoken(Speaker::Eva, text) = &self.conversation[self.conversation.len() - 1 - row_num] {
|
||||
self.tts.speak(text.clone()).await;
|
||||
self.focus_state = FocusState::UserInput;
|
||||
self.conversation_state.select(None);
|
||||
@@ -240,7 +260,7 @@ impl Ui {
|
||||
if next_msg.starts_with("/") {
|
||||
self.on_command(&next_msg).await;
|
||||
} else {
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::User(next_msg))).await;
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::Spoken(Speaker::User, next_msg))).await;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -259,14 +279,20 @@ impl Ui {
|
||||
}
|
||||
|
||||
tokio::select!{
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(60)), if self.is_requesting => (),
|
||||
_ = tokio::time::sleep(std::time::Duration::from_millis(60)), if self.is_requesting || self.computer_is_thinking => (),
|
||||
next_update = self.predictions.changed() => {
|
||||
match next_update {
|
||||
SessionUpdate::Thinking(is_thinking) => self.is_requesting = is_thinking,
|
||||
SessionUpdate::Thinking(Speaker::Eva, is_thinking) => self.is_requesting = is_thinking,
|
||||
SessionUpdate::Thinking(Speaker::ShipComputer, is_thinking) => self.computer_is_thinking = is_thinking,
|
||||
SessionUpdate::Thinking(_, _) => unreachable!(),
|
||||
SessionUpdate::Scene(scene) => {
|
||||
self.scene = scene;
|
||||
self.reply_state.select_first();
|
||||
},
|
||||
SessionUpdate::Responses(responses) => {
|
||||
self.reply_options = responses;
|
||||
self.reply_state.select_first();
|
||||
}
|
||||
SessionUpdate::Conversation(conversation) => {
|
||||
self.conversation = conversation;
|
||||
},
|
||||
@@ -276,7 +302,7 @@ impl Ui {
|
||||
self.audio_level = next_volume
|
||||
},
|
||||
transcription_result = self.transcription.next() => {
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::User(transcription_result))).await;
|
||||
self.predictions.insert(PredictionAction::ConversationAppend(ConversationEntry::Spoken(Speaker::User, transcription_result))).await;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user