use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::world::DeferredWorld;
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Clear, Widget};
use rig_core::conversation::Conversation;
use rig_ecs::agent::Conversation as AgentConversation;
use rig_harness::main_agent::ShownAgent;
use super::keys::{InputAction, KeyBinding, KeyPress, KeyScope};
use super::panel::{PanelCanvas, Placement, TuiPanel};
use super::render::{ShownConversation, Sources, TranscriptLayout, decorate, lay_out_transcript};
use super::theme::Theme;
use super::transcript::{Below, Part, Scroll, Transcript};
use super::view::{DrawnTranscript, TranscriptView};
#[derive(Component, Clone, Debug)]
#[require(TuiPanel = TuiPanel::new(Placement::Rest), TranscriptView, PaneState)]
pub struct TranscriptPane {
pub source: TranscriptSource,
pub title: Option<String>,
}
impl TranscriptPane {
pub fn new(source: TranscriptSource) -> Self {
Self {
source,
title: None,
}
}
pub fn titled(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
}
#[derive(Clone, Debug)]
pub enum TranscriptSource {
Shown,
Agent(Entity),
Value(Conversation),
}
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct MainPane;
pub type MainTranscript<'w, 's> = Single<'w, 's, &'static mut TranscriptView, With<MainPane>>;
#[derive(Component, Clone, Copy, Debug, Default)]
#[component(on_add = bind_pane_keys)]
pub struct PaneKeys;
#[derive(Component, Default)]
pub(crate) struct PaneState {
pub(crate) transcript: Transcript,
pub(crate) scroll: Scroll,
pub(crate) parts: Vec<Part>,
pub(crate) rows: Vec<Line<'static>>,
pub(crate) below: Below,
pub(crate) area: Rect,
}
impl PaneState {
pub(crate) fn page(&self) -> usize {
usize::from(self.area.height).saturating_sub(1).max(1)
}
}
fn bind_pane_keys(mut world: DeferredWorld, context: HookContext) {
let pane = context.entity;
let alt = KeyModifiers::ALT;
let keys = [
(
KeyPress::key(KeyCode::Up),
InputAction::ScrollUp,
"Scroll up",
),
(
KeyPress::key(KeyCode::Down),
InputAction::ScrollDown,
"Scroll down",
),
(
KeyPress::key(KeyCode::PageUp),
InputAction::PageUp,
"Page up",
),
(
KeyPress::key(KeyCode::PageDown),
InputAction::PageDown,
"Page down",
),
(
KeyPress::new(KeyCode::Up, alt),
InputAction::SelectUp,
"Select the item above",
),
(
KeyPress::new(KeyCode::Down, alt),
InputAction::SelectDown,
"Select the item below",
),
];
let mut commands = world.commands();
for (press, action, label) in keys {
let binding = KeyBinding::new(press, label).in_scope(KeyScope::Panel(pane));
let name = Name::new(format!("key:{}", binding.keys));
commands.spawn((name, binding, action, ChildOf(pane)));
}
}
pub(crate) fn lay_out_panes(
from: TranscriptLayout,
shown: Res<ShownAgent>,
changed: Query<(), Changed<AgentConversation>>,
mut panes: Query<(
Entity,
Ref<TranscriptPane>,
&mut TranscriptView,
&mut PaneState,
&PanelCanvas,
Option<&mut DrawnTranscript>,
)>,
) {
let looks_changed = from.looks.changed();
for (entity, pane, view, mut state, canvas, drawn) in &mut panes {
let area = inner(&pane, canvas.area());
if area.is_empty() {
continue;
}
let state = state.bypass_change_detection();
if looks_changed {
state.transcript.clear();
}
let agent = match &pane.source {
TranscriptSource::Shown => shown.0,
TranscriptSource::Agent(agent) => Some(*agent),
TranscriptSource::Value(_) => None,
};
let value = match &pane.source {
TranscriptSource::Value(conversation) => {
let mut value = ShownConversation::value(conversation, entity);
value.changed = pane.is_changed();
Some(value)
}
_ => None,
};
let live = agent.and_then(|agent| from.agent(agent, changed.contains(agent)));
let shown = value.or(live);
lay_out_pane(&from, entity, (view, state, drawn), shown.as_ref(), area);
}
}
pub(crate) fn lay_out_pane(
from: &TranscriptLayout,
entity: Entity,
(mut view, state, drawn): (
Mut<TranscriptView>,
&mut PaneState,
Option<Mut<DrawnTranscript>>,
),
shown: Option<&ShownConversation<'_>>,
area: Rect,
) {
let renderers = from.looks.by_kind(Some(entity));
let sources = Sources {
view: &view,
notices: from.notices(),
renderers: &renderers,
facts: from.facts(),
theme: from.theme(),
};
let parts = lay_out_transcript(&mut state.transcript, &sources, shown, area.width);
let height = usize::from(area.height);
let highlights = &view.highlights;
let visible =
(state.transcript).visible(&parts, (area.width, height), &mut state.scroll, highlights);
if let Some(mut drawn) = drawn {
let agent = shown.and_then(|shown| shown.id.map(|_| shown.entity));
let on_screen = visible.first..visible.first + visible.rows.len();
let laid_out = state
.transcript
.laid_out(agent, &parts, highlights, on_screen);
drawn.set_if_neq(laid_out.at(area));
}
let selected = view.selected.as_ref();
let selected = selected.and_then(|item| state.transcript.rows_of(&parts, item));
state.rows = decorate(visible.rows, visible.first, selected, from.theme());
state.below = visible.below;
state.parts = parts;
state.area = area;
if view.top() != visible.top.as_ref() {
view.set_top(visible.top);
}
if view.width() != Some(area.width) {
view.set_width(area.width);
}
}
fn inner(pane: &TranscriptPane, area: Rect) -> Rect {
match pane.title {
Some(_) => Block::new().title("").inner(area),
None => area,
}
}
pub(crate) fn draw_panes(
mut panes: Query<(&TranscriptPane, &mut PaneState, &mut PanelCanvas)>,
theme: Res<Theme>,
) {
for (pane, mut state, mut canvas) in &mut panes {
let state = state.bypass_change_detection();
let area = canvas.area();
if area.is_empty() {
continue;
}
let buffer = canvas.buffer_mut();
if let Some((title, row)) = pane.title.as_ref().zip(area.rows().next()) {
Line::from(format!(" {title}"))
.bold()
.bg(theme.panel_surface)
.render(row, buffer);
}
for (row, area) in std::mem::take(&mut state.rows)
.iter()
.zip(state.area.rows())
{
buffer.set_style(area, row.style);
row.render(area, buffer);
}
draw_below(buffer, state.below, state.area, &theme);
}
}
fn draw_below(buffer: &mut ratatui::buffer::Buffer, below: Below, area: Rect, theme: &Theme) {
let text = match below {
Below::Nothing => return,
Below::More => " ↓ more below (PgDn) ",
Below::New => " ↓ new output (PgDn) ",
};
let width = u16::try_from(Span::from(text).width()).unwrap_or(u16::MAX);
if area.height == 0 || area.width < width {
return;
}
let marker = Rect::new(area.right() - width, area.bottom() - 1, width, 1);
Clear.render(marker, buffer);
Line::from(text)
.bold()
.style(theme.selection())
.render(marker, buffer);
}
#[cfg(test)]
mod tests;