flowcat_core/
transcript.rs1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13pub enum Speaker {
14 User,
16 Bot,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct TranscriptLine {
23 pub speaker: Speaker,
25 pub text: String,
27}
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize)]
31pub struct Transcript {
32 pub lines: Vec<TranscriptLine>,
34}
35
36impl Transcript {
37 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn push_user(&mut self, text: &str) {
48 self.push(Speaker::User, text);
49 }
50
51 pub fn push_bot(&mut self, text: &str) {
53 self.push(Speaker::Bot, text);
54 }
55
56 fn push(&mut self, speaker: Speaker, text: &str) {
59 if text.is_empty() {
60 return;
61 }
62 match self.lines.last_mut() {
63 Some(last) if last.speaker == speaker => last.text.push_str(text),
64 _ => self.lines.push(TranscriptLine {
65 speaker,
66 text: text.to_owned(),
67 }),
68 }
69 }
70
71 pub fn user_turns(&self) -> u64 {
74 self.lines
75 .iter()
76 .filter(|l| l.speaker == Speaker::User)
77 .count() as u64
78 }
79
80 pub fn bot_turns(&self) -> u64 {
84 self.lines
85 .iter()
86 .filter(|l| l.speaker == Speaker::Bot)
87 .count() as u64
88 }
89
90 pub fn render(&self) -> Vec<u8> {
96 let turns: Vec<Turn> = self
97 .lines
98 .iter()
99 .map(|l| Turn {
100 role: match l.speaker {
101 Speaker::User => "user",
102 Speaker::Bot => "bot",
103 },
104 text: &l.text,
105 })
106 .collect();
107 serde_json::to_vec(&turns).unwrap_or_else(|_| b"[]".to_vec())
108 }
109}
110
111#[derive(Debug, Serialize)]
113struct Turn<'a> {
114 role: &'a str,
116 text: &'a str,
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use serde_json::Value;
124
125 #[test]
126 fn empty_transcript_renders_empty_json_array() {
127 let t = Transcript::new();
128 assert_eq!(t.render(), b"[]".to_vec());
129 }
130
131 #[test]
132 fn consecutive_same_speaker_deltas_merge_into_one_line() {
133 let mut t = Transcript::new();
134 t.push_user("hel");
135 t.push_user("lo ");
136 t.push_user("there");
137 assert_eq!(t.lines.len(), 1);
138 assert_eq!(t.lines[0].speaker, Speaker::User);
139 assert_eq!(t.lines[0].text, "hello there");
140 }
141
142 #[test]
143 fn speaker_switch_starts_a_new_line() {
144 let mut t = Transcript::new();
145 t.push_user("hi");
146 t.push_bot("hello, how can I help?");
147 t.push_user("a question");
148 assert_eq!(t.lines.len(), 3);
149 assert_eq!(t.lines[0].speaker, Speaker::User);
150 assert_eq!(t.lines[1].speaker, Speaker::Bot);
151 assert_eq!(t.lines[2].speaker, Speaker::User);
152 }
153
154 #[test]
155 fn empty_deltas_are_dropped() {
156 let mut t = Transcript::new();
157 t.push_user("");
158 t.push_bot("");
159 assert!(t.lines.is_empty());
160 t.push_user("ab");
162 t.push_user("");
163 t.push_user("cd");
164 assert_eq!(t.lines.len(), 1);
165 assert_eq!(t.lines[0].text, "abcd");
166 }
167
168 #[test]
169 fn turn_counts_tally_lines_per_speaker() {
170 let mut t = Transcript::new();
171 t.push_bot("hello, how can I help?");
172 t.push_user("book an appointment");
173 t.push_bot("sure, what day?");
174 t.push_user("tuesday"); t.push_user(" afternoon");
176 assert_eq!(t.user_turns(), 2, "two user lines");
177 assert_eq!(t.bot_turns(), 2, "two bot lines");
178 let empty = Transcript::new();
179 assert_eq!(empty.user_turns(), 0);
180 assert_eq!(empty.bot_turns(), 0);
181 }
182
183 #[test]
184 fn render_is_role_text_json_array() {
185 let mut t = Transcript::new();
186 t.push_user("hi");
187 t.push_bot("hello");
188 let json: Value = serde_json::from_slice(&t.render()).unwrap();
189 assert_eq!(
190 json,
191 serde_json::json!([
192 { "role": "user", "text": "hi" },
193 { "role": "bot", "text": "hello" },
194 ])
195 );
196 }
197}