ricecoder_modes/
thinking_display.rs

1use crate::error::Result;
2use crate::models::{ModeAction, ModeResponse, ThinkingDepth};
3
4/// Formats and displays thinking content for user consumption
5#[derive(Debug, Clone)]
6pub struct ThinkingDisplay;
7
8impl ThinkingDisplay {
9    /// Format thinking content for display
10    pub fn format_thinking(content: &str, depth: ThinkingDepth) -> String {
11        let header = match depth {
12            ThinkingDepth::Light => "šŸ’­ Quick Thinking",
13            ThinkingDepth::Medium => "🧠 Thinking",
14            ThinkingDepth::Deep => "šŸ”¬ Deep Analysis",
15        };
16
17        format!(
18            "{}\n{}\n{}\n{}",
19            "─".repeat(50),
20            header,
21            "─".repeat(50),
22            content
23        )
24    }
25
26    /// Add thinking content to a response
27    pub fn add_thinking_to_response(
28        response: &mut ModeResponse,
29        thinking_content: &str,
30        depth: ThinkingDepth,
31    ) -> Result<()> {
32        // Add thinking as metadata
33        response.metadata.think_more_used = true;
34        response.metadata.thinking_content = Some(thinking_content.to_string());
35
36        // Add thinking as an action for display
37        let formatted = Self::format_thinking(thinking_content, depth);
38        response.add_action(ModeAction::DisplayThinking { content: formatted });
39
40        Ok(())
41    }
42
43    /// Format thinking content with line numbers for readability
44    pub fn format_thinking_with_line_numbers(content: &str, depth: ThinkingDepth) -> String {
45        let header = match depth {
46            ThinkingDepth::Light => "šŸ’­ Quick Thinking",
47            ThinkingDepth::Medium => "🧠 Thinking",
48            ThinkingDepth::Deep => "šŸ”¬ Deep Analysis",
49        };
50
51        let lines: Vec<&str> = content.lines().collect();
52        let max_line_num = lines.len().to_string().len();
53
54        let formatted_lines: Vec<String> = lines
55            .iter()
56            .enumerate()
57            .map(|(i, line)| format!("{:width$} | {}", i + 1, line, width = max_line_num))
58            .collect();
59
60        format!(
61            "{}\n{}\n{}\n{}",
62            "─".repeat(50),
63            header,
64            "─".repeat(50),
65            formatted_lines.join("\n")
66        )
67    }
68
69    /// Format thinking content as a collapsible section
70    pub fn format_thinking_collapsible(content: &str, depth: ThinkingDepth) -> String {
71        let header = match depth {
72            ThinkingDepth::Light => "šŸ’­ Quick Thinking",
73            ThinkingDepth::Medium => "🧠 Thinking",
74            ThinkingDepth::Deep => "šŸ”¬ Deep Analysis",
75        };
76
77        format!("ā–¼ {}\n{}\n{}", header, "─".repeat(50), content)
78    }
79
80    /// Extract key insights from thinking content
81    pub fn extract_insights(content: &str) -> Vec<String> {
82        content
83            .lines()
84            .filter(|line| {
85                let trimmed = line.trim();
86                // Extract lines that look like conclusions or key points
87                trimmed.starts_with("→")
88                    || trimmed.starts_with("•")
89                    || trimmed.starts_with("āœ“")
90                    || trimmed.starts_with("Key:")
91                    || trimmed.starts_with("Conclusion:")
92            })
93            .map(|line| line.to_string())
94            .collect()
95    }
96
97    /// Summarize thinking content
98    pub fn summarize_thinking(content: &str, max_lines: usize) -> String {
99        let lines: Vec<&str> = content.lines().collect();
100        if lines.len() <= max_lines {
101            return content.to_string();
102        }
103
104        let mut summary = lines[..max_lines].join("\n");
105        summary.push_str(&format!("\n... ({} more lines)", lines.len() - max_lines));
106        summary
107    }
108
109    /// Format thinking content with emphasis on important sections
110    pub fn format_thinking_with_emphasis(content: &str, depth: ThinkingDepth) -> String {
111        let header = match depth {
112            ThinkingDepth::Light => "šŸ’­ Quick Thinking",
113            ThinkingDepth::Medium => "🧠 Thinking",
114            ThinkingDepth::Deep => "šŸ”¬ Deep Analysis",
115        };
116
117        let emphasized_lines: Vec<String> = content
118            .lines()
119            .map(|line| {
120                let trimmed = line.trim();
121                if trimmed.starts_with("→") || trimmed.starts_with("āœ“") {
122                    format!("  ⭐ {}", trimmed)
123                } else if trimmed.starts_with("Key:") || trimmed.starts_with("Conclusion:") {
124                    format!("  šŸ”‘ {}", trimmed)
125                } else {
126                    format!("     {}", trimmed)
127                }
128            })
129            .collect();
130
131        format!(
132            "{}\n{}\n{}\n{}",
133            "─".repeat(50),
134            header,
135            "─".repeat(50),
136            emphasized_lines.join("\n")
137        )
138    }
139
140    /// Check if thinking content is empty or minimal
141    pub fn is_empty_or_minimal(content: &str) -> bool {
142        content.trim().is_empty() || content.lines().count() < 2
143    }
144
145    /// Get thinking statistics
146    pub fn get_statistics(content: &str) -> ThinkingStatistics {
147        let lines = content.lines().count();
148        let words = content.split_whitespace().count();
149        let chars = content.len();
150
151        ThinkingStatistics {
152            lines,
153            words,
154            chars,
155        }
156    }
157}
158
159/// Statistics about thinking content
160#[derive(Debug, Clone)]
161pub struct ThinkingStatistics {
162    /// Number of lines
163    pub lines: usize,
164    /// Number of words
165    pub words: usize,
166    /// Number of characters
167    pub chars: usize,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_format_thinking_light() {
176        let content = "This is a quick thought";
177        let formatted = ThinkingDisplay::format_thinking(content, ThinkingDepth::Light);
178        assert!(formatted.contains("šŸ’­ Quick Thinking"));
179        assert!(formatted.contains(content));
180    }
181
182    #[test]
183    fn test_format_thinking_medium() {
184        let content = "This is a medium thought";
185        let formatted = ThinkingDisplay::format_thinking(content, ThinkingDepth::Medium);
186        assert!(formatted.contains("🧠 Thinking"));
187        assert!(formatted.contains(content));
188    }
189
190    #[test]
191    fn test_format_thinking_deep() {
192        let content = "This is a deep thought";
193        let formatted = ThinkingDisplay::format_thinking(content, ThinkingDepth::Deep);
194        assert!(formatted.contains("šŸ”¬ Deep Analysis"));
195        assert!(formatted.contains(content));
196    }
197
198    #[test]
199    fn test_format_thinking_with_line_numbers() {
200        let content = "Line 1\nLine 2\nLine 3";
201        let formatted =
202            ThinkingDisplay::format_thinking_with_line_numbers(content, ThinkingDepth::Medium);
203        assert!(formatted.contains("1 |"));
204        assert!(formatted.contains("2 |"));
205        assert!(formatted.contains("3 |"));
206    }
207
208    #[test]
209    fn test_format_thinking_collapsible() {
210        let content = "This is collapsible";
211        let formatted =
212            ThinkingDisplay::format_thinking_collapsible(content, ThinkingDepth::Medium);
213        assert!(formatted.contains("ā–¼"));
214        assert!(formatted.contains("🧠 Thinking"));
215    }
216
217    #[test]
218    fn test_extract_insights() {
219        let content = "Some text\n→ Key insight 1\nMore text\n• Key insight 2\nāœ“ Conclusion";
220        let insights = ThinkingDisplay::extract_insights(content);
221        assert_eq!(insights.len(), 3);
222        assert!(insights[0].contains("→"));
223    }
224
225    #[test]
226    fn test_summarize_thinking() {
227        let content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5";
228        let summary = ThinkingDisplay::summarize_thinking(content, 3);
229        assert!(summary.contains("Line 1"));
230        assert!(summary.contains("Line 3"));
231        assert!(summary.contains("2 more lines"));
232    }
233
234    #[test]
235    fn test_format_thinking_with_emphasis() {
236        let content = "Normal line\n→ Important point\nKey: This is important";
237        let formatted =
238            ThinkingDisplay::format_thinking_with_emphasis(content, ThinkingDepth::Medium);
239        assert!(formatted.contains("⭐"));
240        assert!(formatted.contains("šŸ”‘"));
241    }
242
243    #[test]
244    fn test_is_empty_or_minimal() {
245        assert!(ThinkingDisplay::is_empty_or_minimal(""));
246        assert!(ThinkingDisplay::is_empty_or_minimal("   "));
247        assert!(!ThinkingDisplay::is_empty_or_minimal("Line 1\nLine 2"));
248    }
249
250    #[test]
251    fn test_get_statistics() {
252        let content = "Line 1\nLine 2\nLine 3";
253        let stats = ThinkingDisplay::get_statistics(content);
254        assert_eq!(stats.lines, 3);
255        assert!(stats.words > 0);
256        assert!(stats.chars > 0);
257    }
258
259    #[test]
260    fn test_add_thinking_to_response() {
261        let mut response = ModeResponse::new("Test response".to_string(), "test-mode".to_string());
262        ThinkingDisplay::add_thinking_to_response(
263            &mut response,
264            "Test thinking",
265            ThinkingDepth::Medium,
266        )
267        .unwrap();
268
269        assert!(response.metadata.think_more_used);
270        assert!(response.metadata.thinking_content.is_some());
271        assert_eq!(response.actions.len(), 1);
272    }
273}