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