use super::{Kind, Row, trail};
use crate::settings::{Settings, Toggle};
use crate::ui::theme::{self, Speaker, glyph};
pub const UNFOLD_HELP: &str = "opens what the turn did — every step it hid: the calls, their results, the thoughts and the prose between";
pub const FOLD_HELP: &str = "folds the working back to its one line";
pub const STEP_HELP: &str = "one step of the working; its result stands elided beneath";
pub fn counted(steps: &[Row]) -> String {
["tool call", "thought", "intermediate response", "note"]
.into_iter()
.enumerate()
.map(|(kind, noun)| (steps.iter().filter(|row| sort(row) == kind).count(), noun))
.filter(|(count, _)| *count > 0)
.map(|(count, noun)| crate::render::counted(count as u64, noun))
.collect::<Vec<_>>()
.join(&format!(" {} ", glyph::NOTE))
}
fn sort(row: &Row) -> usize {
if is_call(row) {
0
} else if row.kind == Kind::Thinking {
1
} else if is_response(row) {
2
} else {
3
}
}
fn is_response(row: &Row) -> bool {
row.kind == Kind::Said && row.weight == Speaker::Model
}
fn is_call(row: &Row) -> bool {
matches!(row.kind, Kind::Call | Kind::Answered | Kind::Result)
}
fn in_flight(row: &Row, last: bool) -> bool {
row.kind == Kind::Call || (row.live && last)
}
pub(super) fn working(ui: &mut egui::Ui, at: usize, steps: &[Row], settings: &Settings) -> bool {
let id = ui.make_persistent_id(("working", at));
let live = steps.iter().any(|row| row.live) || !settings.on(Toggle::Fold);
let mut open = ui.data(|d| d.get_temp::<bool>(id)).unwrap_or(live);
let door = theme::paint::Row {
mark: (
if open { glyph::OPEN } else { glyph::SHUT }.to_owned(),
theme::INK_WEAK,
),
words: counted(steps),
ink: theme::INK_WEAK,
state: None,
chosen: false,
indent: 0.0,
aside: None,
beneath: Vec::new(),
door: None,
help: if open { FOLD_HELP } else { UNFOLD_HELP }.to_owned(),
};
if door.paint(ui).clicked() {
open = !open;
ui.data_mut(|d| d.insert_temp(id, open));
}
if !open {
return false;
}
let x = ui.cursor().min.x + theme::space::S;
for (i, row) in steps.iter().enumerate() {
let top = ui.cursor().min.y;
let last = i + 1 == steps.len();
ui.horizontal_top(|ui| {
ui.add_space(theme::space::L);
if in_flight(row, last) {
trail::step(ui, at + i, row, false, settings.on(Toggle::Bubbles));
} else {
finished(ui, at + i, row, settings);
}
});
let bottom = ui.min_rect().max.y;
let middle = top + theme::space::S;
theme::paint::elbow(ui, x, top, middle, x + theme::space::S);
if !last {
theme::paint::rail(ui, x, middle, bottom);
}
}
true
}
fn finished(ui: &mut egui::Ui, at: usize, row: &Row, settings: &Settings) {
let id = ui.make_persistent_id(("step", at));
let open = ui
.data(|d| d.get_temp::<bool>(id))
.unwrap_or(!settings.on(Toggle::Fold));
let toggled = if open {
trail::step(ui, at, row, true, settings.on(Toggle::Bubbles))
} else {
compact(ui, row).clicked()
};
if toggled {
ui.data_mut(|d| d.insert_temp(id, !open));
}
}
fn compact(ui: &mut egui::Ui, row: &Row) -> egui::Response {
let red = theme::accent(theme::State::Error);
let first = row.said.lines().next().unwrap_or_default();
let weak = theme::INK_WEAK;
let (mark, words, beneath) = if is_call(row) {
let mark = if row.failed {
(glyph::FAILED, red)
} else {
(glyph::DONE, weak)
};
let beneath = if first.is_empty() {
Vec::new()
} else {
vec![(first.to_owned(), weak, egui::TextStyle::Body)]
};
(mark, row.who.clone(), beneath)
} else {
let mark = if row.weight == Speaker::Model && row.kind == Kind::Said {
glyph::MODEL
} else {
glyph::NOTE
};
let words = format!("{} {} {first}", row.who, glyph::NOTE);
((mark, weak), words, Vec::new())
};
theme::paint::Row {
mark: (mark.0.to_owned(), mark.1),
words,
ink: if row.failed { red } else { weak },
state: row.failed.then_some(red),
chosen: false,
indent: 0.0,
aside: None,
beneath,
door: None,
help: STEP_HELP.to_owned(),
}
.paint(ui)
}