use crate::reply::stream::Stream;
use crate::reply::transcript::{Block, Entry, EntryKind, Transcript};
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().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) -> Row {
match block {
Block::Text(text) => Row {
who: model_id.to_owned(),
said: text.clone(),
},
Block::Thinking(text) => Row {
who: format!("{model_id} (thinking)"),
said: text.clone(),
},
Block::ToolUse { id, name, input } => Row {
who: format!("{model_id} → {name} {id}"),
said: input.clone(),
},
Block::Unknown(word) => 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 live_rows(thinking: &str, text: &str) -> Vec<Row> {
[("thinking", thinking), ("", text)]
.into_iter()
.filter(|(_, said)| !said.is_empty())
.map(|(mark, said)| Row {
who: if mark.is_empty() {
LIVE.to_owned()
} else {
format!("{LIVE} ({mark})")
},
said: said.to_owned(),
})
.collect()
}
pub fn render(ui: &mut egui::Ui, model: &crate::ui::Model) {
ui.heading("conversation");
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;