1use anyhow::Result;
2use colored::{ColoredString, Colorize};
3
4use crate::data::{ConfidenceLevel, FlagType, TokenAnalysis, TokenInfo, VisualizationConfig};
5
6pub trait Renderer {
8 fn render(
10 &self,
11 text: &str,
12 analysis: &TokenAnalysis,
13 config: &VisualizationConfig,
14 ) -> Result<String>;
15}
16
17pub struct TerminalRenderer;
19
20impl Default for TerminalRenderer {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl TerminalRenderer {
27 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 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 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 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 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 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 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
164pub struct HtmlRenderer;
166
167impl Default for HtmlRenderer {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl HtmlRenderer {
174 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
327pub struct MarkdownRenderer;
329
330impl Default for MarkdownRenderer {
331 fn default() -> Self {
332 Self::new()
333 }
334}
335
336impl MarkdownRenderer {
337 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}