use super::{Fold, fold};
use crate::ui::theme::Speaker;
mod live;
mod pair;
use crate::reply::stream::Stream;
use crate::reply::transcript::{Block, Entry, EntryKind, Transcript};
use live::{half, live_rows, streaming};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Kind {
#[default]
Said,
Call,
Answered,
Result,
Thinking,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub who: String,
pub said: String,
pub weight: Speaker,
pub failed: bool,
pub fold: Option<Fold>,
pub kind: Kind,
pub call: Option<String>,
}
impl Row {
pub(super) fn plain(who: String, said: String, weight: Speaker) -> Self {
Self {
who,
said,
weight,
failed: false,
fold: None,
kind: Kind::Said,
call: None,
}
}
pub(super) fn as_(mut self, kind: Kind) -> Self {
self.kind = kind;
self
}
pub(super) fn call(name: &str, input: &str, id: &str) -> Self {
Self {
call: Some(id.to_owned()),
..Self::plain(pair::head(name, input), String::new(), Speaker::Model).as_(Kind::Call)
}
}
pub(super) fn result(id: &str, said: String, failed: bool, fold: Option<Fold>) -> Self {
Self {
who: if failed { FAILED } else { RETURNED }.to_owned(),
said,
weight: Speaker::Peer,
failed,
fold,
kind: Kind::Result,
call: Some(id.to_owned()),
}
}
}
const OPERATOR: [&str; 2] = ["user", "op"];
fn delivered(sender: &str, epitaph: Option<&String>) -> Speaker {
if OPERATOR.contains(&sender) {
Speaker::Operator
} else if epitaph.is_some() {
Speaker::Ended
} else {
Speaker::Peer
}
}
pub fn rows(transcript: &Transcript, live: Option<&Stream>) -> Vec<Row> {
pair::met(
transcript
.entries
.iter()
.flat_map(|entry| match (&entry.kind, live) {
(EntryKind::Streaming { .. }, Some(fold)) => streaming(fold),
_ => entry_rows(entry),
})
.collect(),
)
}
const RETURNED: &str = "returned";
const FAILED: &str = "failed";
fn entry_rows(entry: &Entry) -> Vec<Row> {
match &entry.kind {
EntryKind::Delivered {
sender,
sender_name,
epitaph,
body,
} => {
let who = crate::reply::transcript::said_by(sender, sender_name.as_deref());
let weight = delivered(sender, epitaph.as_ref());
vec![Row::plain(
match epitaph {
Some(word) => format!("{who} ({word})"),
None => who,
},
body.clone(),
weight,
)]
}
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::result(
tool_use_id,
content.clone(),
*is_error,
fold::of(content),
)],
EntryKind::Streaming { thinking, text } => live_rows(thinking, text),
EntryKind::Compacted {
first,
last,
summary,
} => vec![Row::plain(
format!("compacted {first}–{last}"),
summary.clone(),
Speaker::Peer,
)],
EntryKind::Raw => vec![Row::plain(
format!("{} (unparsed)", entry.name),
entry.raw.clone(),
Speaker::Peer,
)],
EntryKind::Unknown(word) => vec![Row::plain(
format!("{} ({word}, which this seat cannot read)", entry.name),
entry.raw.clone(),
Speaker::Peer,
)],
}
}
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).map(|row| row.as_(Kind::Thinking))
}
Block::ToolUse { id, name, input } => Some(Row::call(name, input, id)),
Block::Unknown(word) => Some(Row::plain(
format!("{model_id} ({word}, which this seat cannot read)"),
String::new(),
Speaker::Model,
)),
}
}