use crate::{ Session, Entry, ContentBlock, MessageContent, EntryType, Result, Error };
use std::io::Write;
use std::path::Path;
use std::fs::File;
#[derive( Debug, Clone, Copy, PartialEq, Eq )]
pub enum ExportFormat
{
Markdown,
Json,
Text,
}
impl ExportFormat
{
#[must_use]
#[inline]
pub fn extension( &self ) -> &'static str
{
match self
{
ExportFormat::Markdown => "md",
ExportFormat::Json => "json",
ExportFormat::Text => "txt",
}
}
#[ allow( clippy::should_implement_trait ) ]
#[inline]
pub fn from_str( s : &str ) -> Result< Self >
{
match s.to_lowercase().as_str()
{
"markdown" | "md" => Ok( ExportFormat::Markdown ),
"json" => Ok( ExportFormat::Json ),
"text" | "txt" => Ok( ExportFormat::Text ),
_ =>
{
Err
(
Error::WriteFailed
{
target : "export format".into(),
reason : format!
(
"unknown format '{s}'; valid values: markdown (or md), json, text (or txt)"
),
}
)
}
}
}
}
#[inline]
pub fn export_session< W : Write >
(
session : &mut Session,
format : ExportFormat,
writer : &mut W,
) -> Result< () >
{
match format
{
ExportFormat::Markdown => export_markdown( session, writer ),
ExportFormat::Json => export_json( session, writer ),
ExportFormat::Text => export_text( session, writer ),
}
}
#[inline]
pub fn export_session_to_file
(
session : &mut Session,
format : ExportFormat,
output_path : &Path,
) -> Result< () >
{
let mut file = File::create( output_path )
.map_err( | e | Error::io( e, format!( "create output file '{}'", output_path.display() ) ) )?;
export_session( session, format, &mut file )?;
file.sync_all()
.map_err( | e | Error::io( e, format!( "flush output file '{}'", output_path.display() ) ) )?;
Ok( () )
}
fn export_markdown< W : Write >
(
session : &mut Session,
writer : &mut W,
) -> Result< () >
{
let session_id = session.id().to_string();
let storage_path = session.storage_path().to_path_buf();
let stats = session.stats()?;
let first_timestamp = stats.first_timestamp.clone();
let last_timestamp = stats.last_timestamp.clone();
let total_entries = stats.total_entries;
let entries = session.entries()?;
writeln!( writer, "# Session: {session_id}\n" )?;
writeln!( writer, "**Path**: `{}`", storage_path.display() )?;
writeln!( writer, "**Entries**: {total_entries}" )?;
if let Some( first ) = first_timestamp
{
writeln!( writer, "**Created**: {first}" )?;
}
if let Some( last ) = last_timestamp
{
writeln!( writer, "**Last Updated**: {last}" )?;
}
writeln!( writer, "\n---\n" )?;
for ( idx, entry ) in entries.iter().enumerate()
{
write_markdown_entry( writer, entry, idx + 1 )?;
}
Ok( () )
}
fn write_markdown_entry< W : Write >
(
writer : &mut W,
entry : &Entry,
entry_num : usize,
) -> Result< () >
{
let role_name = match entry.entry_type
{
EntryType::User => "User",
EntryType::Assistant => "Assistant",
};
writeln!( writer, "## Entry {entry_num} - {role_name}" )?;
writeln!( writer, "*{}*\n", entry.timestamp )?;
match &entry.message
{
MessageContent::User( user_msg ) =>
{
writeln!( writer, "{}\n", user_msg.content )?;
}
MessageContent::Assistant( assistant_msg ) =>
{
for block in &assistant_msg.content
{
match block
{
ContentBlock::Thinking { thinking, .. } =>
{
let token_count = thinking.split_whitespace().count();
writeln!( writer, "<details>" )?;
writeln!( writer, "<summary>Thinking ({token_count} tokens)</summary>\n" )?;
writeln!( writer, "{thinking}" )?;
writeln!( writer, "</details>\n" )?;
}
ContentBlock::Text { text } =>
{
writeln!( writer, "{text}\n" )?;
}
ContentBlock::ToolUse { name, input, .. } =>
{
writeln!( writer, "**Tool Use**: `{name}`" )?;
writeln!( writer, "```json" )?;
writeln!( writer, "{input:#?}" )?;
writeln!( writer, "```\n" )?;
}
ContentBlock::ToolResult { content, .. } =>
{
writeln!( writer, "**Tool Result**:" )?;
writeln!( writer, "```" )?;
writeln!( writer, "{content}" )?;
writeln!( writer, "```\n" )?;
}
}
}
}
}
writeln!( writer, "---\n" )?;
Ok( () )
}
fn export_json< W : Write >
(
session : &mut Session,
writer : &mut W,
) -> Result< () >
{
use std::io::{ BufRead, BufReader };
use std::fs::File as StdFile;
let session_id = session.id().to_string();
let storage_path = session.storage_path().to_path_buf();
let path_json = storage_path.to_string_lossy()
.replace( '\\', "\\\\" )
.replace( '"', "\\\"" );
let file = StdFile::open( &storage_path )?;
let reader = BufReader::new( file );
let lines : Vec< String > = reader.lines()
.map_while( std::io::Result::ok )
.filter( | l | !l.trim().is_empty() )
.collect();
let entries_json = lines.join( "," );
writeln!
(
writer,
"{{\"session_id\":\"{session_id}\",\"storage_path\":\"{path_json}\",\"entries\":[{entries_json}]}}"
)?;
Ok( () )
}
fn export_text< W : Write >
(
session : &mut Session,
writer : &mut W,
) -> Result< () >
{
let session_id = session.id().to_string();
let storage_path = session.storage_path().to_path_buf();
let stats = session.stats()?;
let total_entries = stats.total_entries;
let entries = session.entries()?;
writeln!( writer, "Session: {session_id}" )?;
writeln!( writer, "Path: {}", storage_path.display() )?;
writeln!( writer, "Entries: {total_entries}" )?;
writeln!( writer, "\n---\n" )?;
for entry in entries
{
write_text_entry( writer, entry )?;
}
Ok( () )
}
fn write_text_entry< W : Write >
(
writer : &mut W,
entry : &Entry,
) -> Result< () >
{
let role_name = match entry.entry_type
{
EntryType::User => "User",
EntryType::Assistant => "Assistant",
};
writeln!( writer, "[{}] {}", role_name, entry.timestamp )?;
match &entry.message
{
MessageContent::User( user_msg ) =>
{
writeln!( writer, "{}\n", user_msg.content )?;
}
MessageContent::Assistant( assistant_msg ) =>
{
for block in &assistant_msg.content
{
if let ContentBlock::Text { text } = block
{
writeln!( writer, "{text}" )?;
}
}
writeln!( writer )?;
}
}
writeln!( writer, "---\n" )?;
Ok( () )
}