97 lines
3.0 KiB
Rust
97 lines
3.0 KiB
Rust
use crate::types::Message;
|
|
|
|
/// Formats messages for display across different clients.
|
|
#[derive(Debug, Clone)]
|
|
pub struct MessageFormatter {
|
|
wrap_width: usize,
|
|
show_role_labels: bool,
|
|
preserve_empty_lines: bool,
|
|
}
|
|
|
|
impl MessageFormatter {
|
|
/// Create a new formatter
|
|
pub fn new(wrap_width: usize, show_role_labels: bool) -> Self {
|
|
Self {
|
|
wrap_width: wrap_width.max(20),
|
|
show_role_labels,
|
|
preserve_empty_lines: false,
|
|
}
|
|
}
|
|
|
|
/// Override whether empty lines should be preserved
|
|
pub fn with_preserve_empty(mut self, preserve: bool) -> Self {
|
|
self.preserve_empty_lines = preserve;
|
|
self
|
|
}
|
|
|
|
/// Update the wrap width
|
|
pub fn set_wrap_width(&mut self, width: usize) {
|
|
self.wrap_width = width.max(20);
|
|
}
|
|
|
|
/// Whether role labels should be shown alongside messages
|
|
pub fn show_role_labels(&self) -> bool {
|
|
self.show_role_labels
|
|
}
|
|
|
|
pub fn format_message(&self, message: &Message) -> Vec<String> {
|
|
message
|
|
.content
|
|
.trim()
|
|
.lines()
|
|
.map(|s| s.to_string())
|
|
.collect()
|
|
}
|
|
|
|
/// Extract thinking content from <think> tags, returning (content_without_think, thinking_content)
|
|
/// This handles both complete and incomplete (streaming) think tags.
|
|
pub fn extract_thinking(&self, content: &str) -> (String, Option<String>) {
|
|
let mut result = String::new();
|
|
let mut thinking = String::new();
|
|
let mut current_pos = 0;
|
|
|
|
while let Some(start_pos) = content[current_pos..].find("<think>") {
|
|
let abs_start = current_pos + start_pos;
|
|
|
|
// Add content before <think> tag to result
|
|
result.push_str(&content[current_pos..abs_start]);
|
|
|
|
// Find closing tag
|
|
if let Some(end_pos) = content[abs_start..].find("</think>") {
|
|
let abs_end = abs_start + end_pos;
|
|
let think_content = &content[abs_start + 7..abs_end]; // 7 = len("<think>")
|
|
|
|
if !thinking.is_empty() {
|
|
thinking.push_str("\n\n");
|
|
}
|
|
thinking.push_str(think_content.trim());
|
|
|
|
current_pos = abs_end + 8; // 8 = len("</think>")
|
|
} else {
|
|
// Unclosed tag - this is streaming content
|
|
// Extract everything after <think> as thinking content
|
|
let think_content = &content[abs_start + 7..]; // 7 = len("<think>")
|
|
|
|
if !thinking.is_empty() {
|
|
thinking.push_str("\n\n");
|
|
}
|
|
thinking.push_str(think_content);
|
|
|
|
current_pos = content.len();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add remaining content
|
|
result.push_str(&content[current_pos..]);
|
|
|
|
let thinking_result = if thinking.is_empty() {
|
|
None
|
|
} else {
|
|
Some(thinking)
|
|
};
|
|
|
|
(result, thinking_result)
|
|
}
|
|
}
|