use crate::reply::stream::Stream;
use crate::reply::transcript::{Block, Entry, EntryKind, Transcript};
pub const HEADING: &str = "conversation";
pub const NO_CONVERSATION: &str = "pick a conversation";
pub const LIVE: &str = "«live»";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub who: String,
pub said: String,
}
pub fn rows(transcript: &Transcript, live: Option<&Stream>) -> Vec<Row> {
let mut out: Vec<Row> = transcript
.entries
.iter()
.filter(|entry| live.is_none() || !matches!(entry.kind, EntryKind::Streaming { .. }))
.flat_map(entry_rows)
.collect();
out.extend(live.into_iter().flat_map(streaming));
out
}
fn entry_rows(entry: &Entry) -> Vec<Row> {
match &entry.kind {
EntryKind::Delivered {
sender,
epitaph,
body,
} => vec![Row {
who: match epitaph {
Some(word) => format!("{sender} ({word})"),
None => sender.clone(),
},
said: body.clone(),
}],
EntryKind::Model {
model_id, blocks, ..
} => blocks.iter().filter_map(|b| block(model_id, b)).collect(),
EntryKind::ToolResult {
tool_use_id,
content,
is_error,
} => vec![Row {
who: format!(
"{tool_use_id} {}",
if *is_error { "failed" } else { "returned" }
),
said: content.clone(),
}],
EntryKind::Streaming { thinking, text } => live_rows(thinking, text),
EntryKind::Compacted {
first,
last,
summary,
} => vec![Row {
who: format!("compacted {first}–{last}"),
said: summary.clone(),
}],
EntryKind::Raw => vec![Row {
who: format!("{} (unparsed)", entry.name),
said: entry.raw.clone(),
}],
EntryKind::Unknown(word) => vec![Row {
who: format!("{} ({word}, which this seat cannot read)", entry.name),
said: entry.raw.clone(),
}],
}
}
fn block(model_id: &str, block: &Block) -> Option<Row> {
match block {
Block::Text(text) => half(model_id, "", text),
Block::Thinking(text) => half(model_id, "thinking", text),
Block::ToolUse { id, name, input } => Some(Row {
who: format!("{model_id} → {name} {id}"),
said: input.clone(),
}),
Block::Unknown(word) => Some(Row {
who: format!("{model_id} ({word}, which this seat cannot read)"),
said: String::new(),
}),
}
}
fn streaming(stream: &Stream) -> Vec<Row> {
live_rows(
stream.thinking.as_deref().unwrap_or_default(),
stream.text.as_deref().unwrap_or_default(),
)
}
fn half(speaker: &str, mark: &str, said: &str) -> Option<Row> {
(!said.is_empty()).then(|| Row {
who: if mark.is_empty() {
speaker.to_owned()
} else {
format!("{speaker} ({mark})")
},
said: said.to_owned(),
})
}
fn live_rows(thinking: &str, text: &str) -> Vec<Row> {
[("thinking", thinking), ("", text)]
.into_iter()
.filter_map(|(mark, said)| half(LIVE, mark, said))
.collect()
}
pub fn render(ui: &mut egui::Ui, model: &crate::ui::Model) {
if model.conversation.is_none() {
ui.label(NO_CONVERSATION);
return;
}
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| {
for row in rows(&model.transcript, model.live.as_ref()) {
ui.separator();
ui.strong(row.who);
ui.label(row.said);
}
});
}
#[cfg(test)]
mod tests;