Skip to main content

llm_token_visualizer/
renderer.rs

1use anyhow::Result;
2use colored::{ColoredString, Colorize};
3
4use crate::data::{ConfidenceLevel, FlagType, TokenAnalysis, TokenInfo, VisualizationConfig};
5
6pub trait Renderer {
7    fn render(
8        &self,
9        text: &str,
10        analysis: &TokenAnalysis,
11        config: &VisualizationConfig,
12    ) -> Result<String>;
13}
14
15pub struct TerminalRenderer;
16
17impl Default for TerminalRenderer {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl TerminalRenderer {
24    pub fn new() -> Self {
25        Self
26    }
27
28    fn format_token(
29        &self,
30        token: &TokenInfo,
31        token_index: usize,
32        analysis: &TokenAnalysis,
33    ) -> ColoredString {
34        let confidence_level = ConfidenceLevel::from(token.confidence);
35        let flags = analysis.get_flags_for_token(token_index);
36
37        let mut formatted = match confidence_level {
38            ConfidenceLevel::VeryLow => token.text.red().bold(),
39            ConfidenceLevel::Low => token.text.yellow(),
40            ConfidenceLevel::Medium => token.text.normal(),
41            ConfidenceLevel::High => token.text.cyan(),
42            ConfidenceLevel::VeryHigh => token.text.green().bold(),
43        };
44
45        // Apply flag formatting
46        for flag in flags {
47            let flag_type = FlagType::from(flag.flag.as_str());
48            formatted = match flag_type {
49                FlagType::Fact => formatted.on_green(),
50                FlagType::Uncertain => formatted.underline(),
51                FlagType::Overconfident => formatted.on_yellow(),
52                FlagType::Hallucination => formatted.on_red().bold(),
53                FlagType::Other(_) => formatted.italic(),
54            };
55        }
56
57        formatted
58    }
59}
60
61impl Renderer for TerminalRenderer {
62    fn render(
63        &self,
64        _text: &str,
65        analysis: &TokenAnalysis,
66        config: &VisualizationConfig,
67    ) -> Result<String> {
68        analysis.validate().map_err(|e| anyhow::anyhow!(e))?;
69
70        let mut output = String::new();
71
72        // Header
73        output.push_str("LLM Token Analysis\n");
74        output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
75
76        if config.verbose {
77            let (min_conf, max_conf, avg_conf) = analysis.get_confidence_stats();
78            output.push_str(&format!(
79                "Confidence Stats: Min: {:.2}, Max: {:.2}, Avg: {:.2}\n",
80                min_conf, max_conf, avg_conf
81            ));
82            output.push_str(&format!("Flags: {}\n", analysis.flags.len()));
83            output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
84        }
85
86        // Legend
87        output.push_str("Color Legend:\n");
88        output.push_str(&format!(
89            "   {} Very Low Confidence (0.0-0.3)\n",
90            "██".red().bold()
91        ));
92        output.push_str(&format!("   {} Low Confidence (0.3-0.5)\n", "██".yellow()));
93        output.push_str(&format!(
94            "   {} Medium Confidence (0.5-0.7)\n",
95            "██".normal()
96        ));
97        output.push_str(&format!("   {} High Confidence (0.7-0.9)\n", "██".cyan()));
98        output.push_str(&format!(
99            "   {} Very High Confidence (0.9-1.0)\n",
100            "██".green().bold()
101        ));
102        output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
103
104        // Render tokens
105        output.push_str("Token Visualization:\n\n");
106        for (i, token) in analysis.tokens.iter().enumerate() {
107            let formatted_token = self.format_token(token, i, analysis);
108            output.push_str(&format!("{}", formatted_token));
109        }
110        output.push_str("\n\n");
111
112        // Show token details if verbose
113        if config.verbose && config.show_confidence_scores {
114            output.push_str("Token Details:\n");
115            output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
116            for (i, token) in analysis.tokens.iter().enumerate() {
117                let flags = analysis.get_flags_for_token(i);
118                let flag_str = if flags.is_empty() {
119                    "None".to_string()
120                } else {
121                    flags
122                        .iter()
123                        .map(|f| f.flag.as_str())
124                        .collect::<Vec<_>>()
125                        .join(", ")
126                };
127                output.push_str(&format!(
128                    "{:3}: '{}' (conf: {:.3}, flags: {})\n",
129                    i, token.text, token.confidence, flag_str
130                ));
131            }
132            output.push('\n');
133        }
134
135        // Show flags if enabled
136        if config.show_flags && !analysis.flags.is_empty() {
137            output.push_str("Flags:\n");
138            output.push_str("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
139            for flag in &analysis.flags {
140                let token_text: String = analysis.tokens[flag.start..flag.end]
141                    .iter()
142                    .map(|t| t.text.as_str())
143                    .collect();
144                let desc = flag.description.as_deref().unwrap_or("No description");
145                output.push_str(&format!(
146                    "  [{}] '{}' (tokens {}-{}): {}\n",
147                    flag.flag,
148                    token_text,
149                    flag.start,
150                    flag.end - 1,
151                    desc
152                ));
153            }
154        }
155
156        Ok(output)
157    }
158}
159
160pub struct HtmlRenderer;
161
162impl Default for HtmlRenderer {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl HtmlRenderer {
169    pub fn new() -> Self {
170        Self
171    }
172
173    fn confidence_to_color(&self, confidence: f64) -> String {
174        let level = ConfidenceLevel::from(confidence);
175        match level {
176            ConfidenceLevel::VeryLow => "#ff4444".to_string(),
177            ConfidenceLevel::Low => "#ffaa44".to_string(),
178            ConfidenceLevel::Medium => "#888888".to_string(),
179            ConfidenceLevel::High => "#44aaff".to_string(),
180            ConfidenceLevel::VeryHigh => "#44ff44".to_string(),
181        }
182    }
183}
184
185impl Renderer for HtmlRenderer {
186    fn render(
187        &self,
188        _text: &str,
189        analysis: &TokenAnalysis,
190        config: &VisualizationConfig,
191    ) -> Result<String> {
192        analysis.validate().map_err(|e| anyhow::anyhow!(e))?;
193
194        let mut html = String::new();
195
196        html.push_str(r#"<!DOCTYPE html>
197<html lang="en">
198<head>
199    <meta charset="UTF-8">
200    <meta name="viewport" content="width=device-width, initial-scale=1.0">
201    <title>LLM Token Analysis</title>
202    <style>
203        body { font-family: 'Courier New', monospace; margin: 20px; background-color: #1a1a1a; color: #ffffff; }
204        .container { max-width: 1200px; margin: 0 auto; }
205        .header { border-bottom: 2px solid #444; padding-bottom: 10px; margin-bottom: 20px; }
206        .legend { margin: 20px 0; padding: 15px; background-color: #2a2a2a; border-radius: 5px; }
207        .legend-item { display: inline-block; margin-right: 20px; margin-bottom: 5px; }
208        .color-box { display: inline-block; width: 20px; height: 20px; margin-right: 5px; vertical-align: middle; }
209        .token-container { line-height: 1.8; font-size: 16px; margin: 20px 0; }
210        .token { position: relative; padding: 2px 4px; border-radius: 3px; cursor: pointer; }
211        .token:hover { opacity: 0.8; }
212        .token.fact { box-shadow: 0 -3px 0 0 #44ff44 inset; }
213        .token.uncertain { text-decoration: underline; }
214        .token.overconfident { box-shadow: 0 -3px 0 0 #ffaa44 inset; }
215        .token.hallucination { box-shadow: 0 -3px 0 0 #ff4444 inset; font-weight: bold; }
216        .flags { margin-top: 30px; }
217        .flag-item { background-color: #2a2a2a; padding: 10px; margin: 5px 0; border-radius: 5px; }
218        .stats { margin: 20px 0; padding: 15px; background-color: #2a2a2a; border-radius: 5px; }
219    </style>
220</head>
221<body>
222    <div class="container">
223        <div class="header">
224            <h1>LLM Token Analysis</h1>
225        </div>
226"#);
227
228        if config.verbose {
229            let (min_conf, max_conf, avg_conf) = analysis.get_confidence_stats();
230            html.push_str(&format!(
231                r#"
232        <div class="stats">
233            <h3>Statistics</h3>
234            <p>Confidence Range: {:.3} - {:.3} (Average: {:.3})</p>
235            <p>Total Tokens: {}</p>
236            <p>Total Flags: {}</p>
237        </div>
238"#,
239                min_conf,
240                max_conf,
241                avg_conf,
242                analysis.tokens.len(),
243                analysis.flags.len()
244            ));
245        }
246
247        html.push_str(r#"
248        <div class="legend">
249            <h3>Confidence Legend</h3>
250            <div class="legend-item"><span class="color-box" style="background-color: #ff4444;"></span>Very Low (0.0-0.3)</div>
251            <div class="legend-item"><span class="color-box" style="background-color: #ffaa44;"></span>Low (0.3-0.5)</div>
252            <div class="legend-item"><span class="color-box" style="background-color: #888888;"></span>Medium (0.5-0.7)</div>
253            <div class="legend-item"><span class="color-box" style="background-color: #44aaff;"></span>High (0.7-0.9)</div>
254            <div class="legend-item"><span class="color-box" style="background-color: #44ff44;"></span>Very High (0.9-1.0)</div>
255        </div>
256        
257        <div class="token-container">
258            <h3>Token Visualization</h3>
259"#);
260
261        for (i, token) in analysis.tokens.iter().enumerate() {
262            let color = self.confidence_to_color(token.confidence);
263            let flags = analysis.get_flags_for_token(i);
264            let flag_classes: Vec<String> = flags.iter().map(|f| f.flag.clone()).collect();
265            let class_str = flag_classes.join(" ");
266            let title = format!(
267                "Token: '{}' | Confidence: {:.3}",
268                token.text, token.confidence
269            );
270
271            html.push_str(&format!(
272                r#"<span class="token {}" style="color: {};" title="{}">{}</span>"#,
273                class_str, color, title, token.text
274            ));
275        }
276
277        html.push_str("</div>");
278
279        if config.show_flags && !analysis.flags.is_empty() {
280            html.push_str(
281                r#"
282        <div class="flags">
283            <h3>Flags</h3>
284"#,
285            );
286
287            for flag in &analysis.flags {
288                let token_text: String = analysis.tokens[flag.start..flag.end]
289                    .iter()
290                    .map(|t| t.text.as_str())
291                    .collect();
292                let desc = flag.description.as_deref().unwrap_or("No description");
293
294                html.push_str(&format!(
295                    r#"<div class="flag-item">
296                <strong>[{}]</strong> "{}" (tokens {}-{})<br>
297                <small>{}</small>
298            </div>"#,
299                    flag.flag,
300                    token_text,
301                    flag.start,
302                    flag.end - 1,
303                    desc
304                ));
305            }
306
307            html.push_str("</div>");
308        }
309
310        html.push_str(
311            r#"
312    </div>
313</body>
314</html>"#,
315        );
316
317        Ok(html)
318    }
319}
320
321pub struct MarkdownRenderer;
322
323impl Default for MarkdownRenderer {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329impl MarkdownRenderer {
330    pub fn new() -> Self {
331        Self
332    }
333}
334
335impl Renderer for MarkdownRenderer {
336    fn render(
337        &self,
338        _text: &str,
339        analysis: &TokenAnalysis,
340        config: &VisualizationConfig,
341    ) -> Result<String> {
342        analysis.validate().map_err(|e| anyhow::anyhow!(e))?;
343
344        let mut md = String::new();
345
346        md.push_str("# LLM Token Analysis\n\n");
347
348        if config.verbose {
349            let (min_conf, max_conf, avg_conf) = analysis.get_confidence_stats();
350            md.push_str("## Statistics\n\n");
351            md.push_str(&format!(
352                "- **Confidence Range**: {:.3} - {:.3} (Average: {:.3})\n",
353                min_conf, max_conf, avg_conf
354            ));
355            md.push_str(&format!("- **Total Tokens**: {}\n", analysis.tokens.len()));
356            md.push_str(&format!("- **Total Flags**: {}\n\n", analysis.flags.len()));
357        }
358
359        md.push_str("## Confidence Legend\n\n");
360        md.push_str("- 🔴 Very Low (0.0-0.3)\n");
361        md.push_str("- 🟡 Low (0.3-0.5)\n");
362        md.push_str("- ⚪ Medium (0.5-0.7)\n");
363        md.push_str("- 🔵 High (0.7-0.9)\n");
364        md.push_str("- 🟢 Very High (0.9-1.0)\n\n");
365
366        md.push_str("## Token Visualization\n\n");
367
368        for (i, token) in analysis.tokens.iter().enumerate() {
369            let confidence_level = ConfidenceLevel::from(token.confidence);
370            let emoji = match confidence_level {
371                ConfidenceLevel::VeryLow => "🔴",
372                ConfidenceLevel::Low => "🟡",
373                ConfidenceLevel::Medium => "⚪",
374                ConfidenceLevel::High => "🔵",
375                ConfidenceLevel::VeryHigh => "🟢",
376            };
377
378            let flags = analysis.get_flags_for_token(i);
379            let token_display = if flags.is_empty() {
380                format!("{}{}", emoji, token.text)
381            } else {
382                format!("{}**{}**", emoji, token.text)
383            };
384
385            md.push_str(&token_display);
386        }
387
388        md.push_str("\n\n");
389
390        if config.show_flags && !analysis.flags.is_empty() {
391            md.push_str("## Flags\n\n");
392
393            for flag in &analysis.flags {
394                let token_text: String = analysis.tokens[flag.start..flag.end]
395                    .iter()
396                    .map(|t| t.text.as_str())
397                    .collect();
398                let desc = flag.description.as_deref().unwrap_or("No description");
399
400                md.push_str(&format!(
401                    "- **[{}]** \"{}\" (tokens {}-{}): {}\n",
402                    flag.flag,
403                    token_text,
404                    flag.start,
405                    flag.end - 1,
406                    desc
407                ));
408            }
409        }
410
411        if config.verbose && config.show_confidence_scores {
412            md.push_str("\n## Detailed Token Information\n\n");
413            md.push_str("| Index | Token | Confidence | Flags |\n");
414            md.push_str("|-------|-------|------------|-------|\n");
415
416            for (i, token) in analysis.tokens.iter().enumerate() {
417                let flags = analysis.get_flags_for_token(i);
418                let flag_str = if flags.is_empty() {
419                    "None".to_string()
420                } else {
421                    flags
422                        .iter()
423                        .map(|f| f.flag.as_str())
424                        .collect::<Vec<_>>()
425                        .join(", ")
426                };
427                md.push_str(&format!(
428                    "| {} | `{}` | {:.3} | {} |\n",
429                    i, token.text, token.confidence, flag_str
430                ));
431            }
432        }
433
434        Ok(md)
435    }
436}