supercode_harness/
human_export.rs1use supercode_interchange::session::Session;
21use supercode_interchange::{ChatMessage, Role};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum HumanExportFormat {
27 #[default]
30 Text,
31 Html,
34}
35
36impl HumanExportFormat {
37 pub fn parse(s: &str) -> Option<HumanExportFormat> {
42 match s {
43 "text" => Some(HumanExportFormat::Text),
44 "html" => Some(HumanExportFormat::Html),
45 _ => None,
46 }
47 }
48}
49
50pub fn render_transcript(session: &Session, format: HumanExportFormat) -> String {
56 render_messages(
57 &session.messages,
58 session.meta.session_id.as_deref(),
59 session.meta.model.as_deref(),
60 format,
61 )
62}
63
64pub fn render_messages(
71 messages: &[ChatMessage],
72 session_id: Option<&str>,
73 model: Option<&str>,
74 format: HumanExportFormat,
75) -> String {
76 match format {
77 HumanExportFormat::Text => render_text(messages, session_id, model),
78 HumanExportFormat::Html => render_html(messages, session_id, model),
79 }
80}
81
82fn role_label(role: Role) -> &'static str {
83 match role {
84 Role::System => "System",
85 Role::User => "User",
86 Role::Assistant => "Assistant",
87 Role::Tool => "Tool",
88 }
89}
90
91fn message_body(msg: &ChatMessage) -> String {
96 let mut parts = Vec::new();
97 if let Some(content) = &msg.content {
98 if !content.is_empty() {
99 parts.push(content.clone());
100 }
101 }
102 if let Some(calls) = &msg.tool_calls {
103 for call in calls {
104 parts.push(format!(
105 "[tool call: {}({})]",
106 call.function.name, call.function.arguments
107 ));
108 }
109 }
110 if parts.is_empty() {
111 parts.push("(empty)".to_string());
112 }
113 parts.join("\n")
114}
115
116fn render_text(messages: &[ChatMessage], session_id: Option<&str>, model: Option<&str>) -> String {
117 let mut out = String::new();
118 if let Some(id) = session_id {
119 out.push_str(&format!("Session: {id}\n"));
120 }
121 if let Some(model) = model {
122 out.push_str(&format!("Model: {model}\n"));
123 }
124 if !out.is_empty() {
125 out.push('\n');
126 }
127 for (i, msg) in messages.iter().enumerate() {
128 if i > 0 {
129 out.push('\n');
130 }
131 out.push_str(&format!("## {}\n", role_label(msg.role)));
132 out.push_str(&message_body(msg));
133 out.push('\n');
134 }
135 out
136}
137
138fn html_escape(s: &str) -> String {
139 s.replace('&', "&")
140 .replace('<', "<")
141 .replace('>', ">")
142 .replace('"', """)
143}
144
145fn render_html(messages: &[ChatMessage], session_id: Option<&str>, model: Option<&str>) -> String {
146 let mut out = String::new();
147 out.push_str("<!doctype html>\n<html><head><meta charset=\"utf-8\">\n");
148 out.push_str("<title>Volter Harness session export</title>\n");
149 out.push_str(
150 "<style>body{font-family:monospace;max-width:60rem;margin:2rem auto;padding:0 1rem}\
151 .msg{border-left:3px solid #ccc;margin:1rem 0;padding:0 1rem;white-space:pre-wrap}\
152 .role{font-weight:bold}</style>\n",
153 );
154 out.push_str("</head><body>\n");
155 if let Some(id) = session_id {
156 out.push_str(&format!("<p>Session: {}</p>\n", html_escape(id)));
157 }
158 if let Some(model) = model {
159 out.push_str(&format!("<p>Model: {}</p>\n", html_escape(model)));
160 }
161 for msg in messages {
162 out.push_str("<div class=\"msg\"><div class=\"role\">");
163 out.push_str(role_label(msg.role));
164 out.push_str("</div><div class=\"body\">");
165 out.push_str(&html_escape(&message_body(msg)));
166 out.push_str("</div></div>\n");
167 }
168 out.push_str("</body></html>\n");
169 out
170}