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 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 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 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 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 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 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}