1use crate::{
2 analyzer::{ProjectAnalysis, FileSummary},
3 dependency_graph::DependencyAnalysis,
4 llm::{AnalysisResponse, Priority},
5};
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::{
9 fs,
10 path::PathBuf,
11};
12
13#[derive(Debug, Serialize, Deserialize)]
14pub struct Report {
15 pub metadata: ReportMetadata,
16 pub executive_summary: ExecutiveSummary,
17 pub file_analysis: FileAnalysisReport,
18 pub dependency_analysis: DependencyAnalysisReport,
19 pub llm_insights: Vec<AnalysisResponse>,
20 pub recommendations: Vec<PrioritizedRecommendation>,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24pub struct ReportMetadata {
25 pub generated_at: String,
26 pub project_name: String,
27 pub total_files: usize,
28 pub total_size: u64,
29 pub analysis_duration_ms: u128,
30 pub version: String,
31 pub llm_provider: String,
32 pub llm_model: String,
33}
34
35#[derive(Debug, Serialize, Deserialize)]
36pub struct ExecutiveSummary {
37 pub overview: String,
38 pub key_findings: Vec<String>,
39 pub critical_issues: Vec<String>,
40 pub architecture_style: String,
41 pub complexity_score: f64,
42 pub maintainability_score: f64,
43}
44
45#[derive(Debug, Serialize, Deserialize)]
46pub struct FileAnalysisReport {
47 pub summary: FileSummary,
48 pub language_breakdown: Vec<LanguageStats>,
49 pub largest_files: Vec<FileStats>,
50 pub complexity_distribution: Vec<ComplexityBucket>,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct LanguageStats {
55 pub language: String,
56 pub file_count: usize,
57 pub total_size: u64,
58 pub avg_file_size: f64,
59 pub percentage: f64,
60}
61
62#[derive(Debug, Serialize, Deserialize)]
63pub struct FileStats {
64 pub path: String,
65 pub size: u64,
66 pub language: String,
67 pub functions: usize,
68 pub classes: usize,
69 pub complexity: usize,
70}
71
72#[derive(Debug, Serialize, Deserialize)]
73pub struct ComplexityBucket {
74 pub range: String,
75 pub count: usize,
76 pub percentage: f64,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct DependencyAnalysisReport {
81 pub graph_metrics: DependencyAnalysis,
82 pub circular_dependencies: Vec<CircularDependency>,
83 pub highly_coupled_files: Vec<CouplingInfo>,
84 pub orphaned_files: Vec<String>,
85 pub dependency_depth: DependencyDepthInfo,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct CircularDependency {
90 pub files: Vec<String>,
91 pub severity: String,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct CouplingInfo {
96 pub file: String,
97 pub incoming_dependencies: usize,
98 pub outgoing_dependencies: usize,
99 pub coupling_score: f64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct DependencyDepthInfo {
104 pub max_depth: usize,
105 pub avg_depth: f64,
106 pub depth_distribution: Vec<DepthBucket>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct DepthBucket {
111 pub depth: usize,
112 pub count: usize,
113}
114
115#[derive(Debug, Serialize, Deserialize)]
116pub struct PrioritizedRecommendation {
117 pub title: String,
118 pub description: String,
119 pub priority: Priority,
120 pub category: String,
121 pub estimated_effort: String,
122 pub potential_impact: String,
123 pub action_items: Vec<String>,
124 pub affected_files: Vec<String>,
125}
126
127pub struct Reporter;
128
129impl Reporter {
130 pub fn new() -> Self {
131 Self
132 }
133
134 pub fn generate_report(&self, analysis: &ProjectAnalysis, duration_ms: u128, llm_provider: &str, llm_model: &str) -> Report {
135 let metadata = self.create_metadata(analysis, duration_ms, llm_provider, llm_model);
136 let executive_summary = self.create_executive_summary(analysis);
137 let file_analysis = self.create_file_analysis_report(analysis);
138 let dependency_analysis = self.create_dependency_analysis_report(analysis);
139 let recommendations = self.prioritize_recommendations(analysis);
140
141 Report {
142 metadata,
143 executive_summary,
144 file_analysis,
145 dependency_analysis,
146 llm_insights: analysis.llm_analysis.clone(),
147 recommendations,
148 }
149 }
150
151 fn create_metadata(&self, analysis: &ProjectAnalysis, duration_ms: u128, llm_provider: &str, llm_model: &str) -> ReportMetadata {
152 let total_size = analysis.files.iter().map(|f| f.size).sum();
153 let project_name = analysis.files.first()
154 .and_then(|f| f.path.parent())
155 .and_then(|p| p.file_name())
156 .and_then(|n| n.to_str())
157 .unwrap_or("unknown")
158 .to_string();
159
160 ReportMetadata {
161 generated_at: chrono::Utc::now().to_rfc3339(),
162 project_name,
163 total_files: analysis.files.len(),
164 total_size,
165 analysis_duration_ms: duration_ms,
166 version: env!("CARGO_PKG_VERSION").to_string(),
167 llm_provider: llm_provider.to_string(),
168 llm_model: llm_model.to_string(),
169 }
170 }
171
172 fn create_executive_summary(&self, analysis: &ProjectAnalysis) -> ExecutiveSummary {
173 let mut key_findings = Vec::new();
174 let mut critical_issues = Vec::new();
175
176 for analysis_result in &analysis.llm_analysis {
177 for insight in &analysis_result.insights {
178 key_findings.push(insight.title.clone());
179 }
180
181 for rec in &analysis_result.recommendations {
182 if matches!(rec.priority, Priority::High | Priority::Critical) {
183 critical_issues.push(rec.title.clone());
184 }
185 }
186 }
187
188 let overview = if let Some(first_analysis) = analysis.llm_analysis.first() {
189 first_analysis.analysis.clone()
190 } else {
191 "No LLM analysis available".to_string()
192 };
193
194 let complexity_score = self.calculate_complexity_score(analysis);
195 let maintainability_score = self.calculate_maintainability_score(analysis);
196
197 ExecutiveSummary {
198 overview,
199 key_findings,
200 critical_issues,
201 architecture_style: "Unknown".to_string(), complexity_score,
203 maintainability_score,
204 }
205 }
206
207 fn create_file_analysis_report(&self, analysis: &ProjectAnalysis) -> FileAnalysisReport {
208 let total_size: u64 = analysis.files.iter().map(|f| f.size).sum();
209
210 let mut language_stats: std::collections::HashMap<String, (usize, u64)> = std::collections::HashMap::new();
211 for file in &analysis.files {
212 if let Some(ref lang) = file.language {
213 let entry = language_stats.entry(lang.clone()).or_insert((0, 0));
214 entry.0 += 1;
215 entry.1 += file.size;
216 }
217 }
218
219 let language_breakdown: Vec<LanguageStats> = language_stats
220 .into_iter()
221 .map(|(lang, (count, size))| LanguageStats {
222 language: lang,
223 file_count: count,
224 total_size: size,
225 avg_file_size: size as f64 / count as f64,
226 percentage: (count as f64 / analysis.files.len() as f64) * 100.0,
227 })
228 .collect();
229
230 let mut file_stats: Vec<FileStats> = analysis.parsed_files
231 .iter()
232 .map(|pf| FileStats {
233 path: pf.file_info.path.to_string_lossy().to_string(),
234 size: pf.file_info.size,
235 language: pf.file_info.language.clone().unwrap_or_else(|| "unknown".to_string()),
236 functions: pf.functions.len(),
237 classes: pf.classes.len(),
238 complexity: pf.functions.len() + pf.classes.len() * 2,
239 })
240 .collect();
241
242 file_stats.sort_by(|a, b| b.size.cmp(&a.size));
243 let largest_files = file_stats.into_iter().take(10).collect();
244
245 let complexity_distribution = self.calculate_complexity_distribution(analysis);
246
247 FileAnalysisReport {
248 summary: FileSummary {
249 total_files: analysis.files.len(),
250 total_size,
251 language_distribution: std::collections::HashMap::new(),
252 extension_distribution: std::collections::HashMap::new(),
253 },
254 language_breakdown,
255 largest_files,
256 complexity_distribution,
257 }
258 }
259
260 fn create_dependency_analysis_report(&self, analysis: &ProjectAnalysis) -> DependencyAnalysisReport {
261 DependencyAnalysisReport {
262 graph_metrics: analysis.dependency_analysis.clone(),
263 circular_dependencies: Vec::new(), highly_coupled_files: Vec::new(), orphaned_files: Vec::new(), dependency_depth: DependencyDepthInfo {
267 max_depth: 0,
268 avg_depth: 0.0,
269 depth_distribution: Vec::new(),
270 },
271 }
272 }
273
274 fn prioritize_recommendations(&self, analysis: &ProjectAnalysis) -> Vec<PrioritizedRecommendation> {
275 let mut recommendations = Vec::new();
276
277 for analysis_result in &analysis.llm_analysis {
278 for rec in &analysis_result.recommendations {
279 recommendations.push(PrioritizedRecommendation {
280 title: rec.title.clone(),
281 description: rec.description.clone(),
282 priority: rec.priority.clone(),
283 category: "General".to_string(),
284 estimated_effort: format!("{:?}", rec.effort),
285 potential_impact: format!("{:?}", rec.impact),
286 action_items: rec.action_items.clone(),
287 affected_files: Vec::new(),
288 });
289 }
290 }
291
292 recommendations.sort_by(|a, b| {
293 use Priority::*;
294 let priority_order = |p: &Priority| match p {
295 Critical => 0,
296 High => 1,
297 Medium => 2,
298 Low => 3,
299 };
300 priority_order(&a.priority).cmp(&priority_order(&b.priority))
301 });
302
303 recommendations
304 }
305
306 fn calculate_complexity_score(&self, analysis: &ProjectAnalysis) -> f64 {
307 if analysis.parsed_files.is_empty() {
308 return 0.0;
309 }
310
311 let total_complexity: usize = analysis.parsed_files
312 .iter()
313 .map(|pf| pf.functions.len() + pf.classes.len() * 2 + pf.imports.len())
314 .sum();
315
316 (total_complexity as f64 / analysis.parsed_files.len() as f64).min(10.0)
317 }
318
319 fn calculate_maintainability_score(&self, analysis: &ProjectAnalysis) -> f64 {
320 let complexity = self.calculate_complexity_score(analysis);
321 let coupling = analysis.dependency_analysis.avg_degree;
322
323 let base_score = 10.0;
324 let complexity_penalty = complexity * 0.5;
325 let coupling_penalty = coupling * 0.3;
326
327 (base_score - complexity_penalty - coupling_penalty).max(0.0)
328 }
329
330 fn calculate_complexity_distribution(&self, analysis: &ProjectAnalysis) -> Vec<ComplexityBucket> {
331 let mut buckets = vec![
332 ComplexityBucket { range: "0-5".to_string(), count: 0, percentage: 0.0 },
333 ComplexityBucket { range: "6-15".to_string(), count: 0, percentage: 0.0 },
334 ComplexityBucket { range: "16-30".to_string(), count: 0, percentage: 0.0 },
335 ComplexityBucket { range: "31+".to_string(), count: 0, percentage: 0.0 },
336 ];
337
338 for pf in &analysis.parsed_files {
339 let complexity = pf.functions.len() + pf.classes.len() * 2;
340 match complexity {
341 0..=5 => buckets[0].count += 1,
342 6..=15 => buckets[1].count += 1,
343 16..=30 => buckets[2].count += 1,
344 _ => buckets[3].count += 1,
345 }
346 }
347
348 let total = analysis.parsed_files.len() as f64;
349 for bucket in &mut buckets {
350 bucket.percentage = (bucket.count as f64 / total) * 100.0;
351 }
352
353 buckets
354 }
355
356 pub fn export_report(&self, report: &Report, output_dir: &PathBuf) -> Result<Vec<PathBuf>> {
357 fs::create_dir_all(output_dir)?;
358 let mut exported_files = Vec::new();
359
360 let json_path = output_dir.join("analysis_report.json");
362 let json_content = serde_json::to_string_pretty(report)?;
363 fs::write(&json_path, json_content)?;
364 exported_files.push(json_path);
365
366 let html_path = output_dir.join("analysis_report.html");
368 let html_content = self.generate_html_report(report)?;
369 fs::write(&html_path, html_content)?;
370 exported_files.push(html_path);
371
372 let md_path = output_dir.join("analysis_summary.md");
374 let md_content = self.generate_markdown_summary(report)?;
375 fs::write(&md_path, md_content)?;
376 exported_files.push(md_path);
377
378 Ok(exported_files)
379 }
380
381 fn generate_html_report(&self, report: &Report) -> Result<String> {
382 let html = format!(
383 r#"<!DOCTYPE html>
384<html lang="en">
385<head>
386 <meta charset="UTF-8">
387 <meta name="viewport" content="width=device-width, initial-scale=1.0">
388 <title>Project Analysis Report - {}</title>
389 <style>
390 body {{ font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }}
391 .header {{ border-bottom: 2px solid #333; padding-bottom: 20px; }}
392 .section {{ margin: 30px 0; }}
393 .metric {{ display: inline-block; margin: 10px 20px 10px 0; padding: 10px; background: #f5f5f5; border-radius: 5px; }}
394 .recommendation {{ margin: 15px 0; padding: 15px; border-left: 4px solid #007acc; background: #f9f9f9; }}
395 .priority-high {{ border-left-color: #ff6b6b; }}
396 .priority-medium {{ border-left-color: #ffa500; }}
397 .priority-low {{ border-left-color: #28a745; }}
398 .insight {{ margin: 10px 0; padding: 10px; background: #e8f4f8; border-radius: 5px; }}
399 .insight-title {{ font-weight: bold; color: #2c3e50; }}
400 .insight-category {{ color: #7f8c8d; font-size: 0.9em; text-transform: uppercase; }}
401 .evidence {{ margin: 5px 0; font-style: italic; color: #555; }}
402 .llm-analysis {{ margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px; }}
403 .analysis-type {{ font-weight: bold; color: #495057; margin-bottom: 10px; }}
404 .analysis-summary {{ margin: 10px 0; padding: 15px; background: #fff; border-radius: 5px; line-height: 1.6; }}
405 .insights-table, .recommendations-table {{ margin: 15px 0; }}
406 .insights-table th {{ background-color: #e3f2fd; }}
407 .recommendations-table th {{ background-color: #f3e5f5; }}
408 table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
409 th, td {{ border: 1px solid #ddd; padding: 12px; text-align: left; vertical-align: top; }}
410 th {{ background-color: #f2f2f2; font-weight: bold; }}
411 .priority-high {{ background-color: #ffebee; }}
412 .priority-medium {{ background-color: #fff3e0; }}
413 .priority-low {{ background-color: #f1f8e9; }}
414 .confidence-high {{ color: #2e7d32; font-weight: bold; }}
415 .confidence-medium {{ color: #f57c00; font-weight: bold; }}
416 .confidence-low {{ color: #d32f2f; font-weight: bold; }}
417 ol {{ list-style-type: decimal; padding-left: 25px; margin: 10px 0; }}
418 ul {{ list-style-type: disc; padding-left: 25px; margin: 10px 0; }}
419 li {{ margin: 8px 0; line-height: 1.4; }}
420 .analysis-summary ul {{ margin: 15px 0; }}
421 .analysis-summary ol {{ margin: 15px 0; }}
422 .analysis-summary li {{ margin: 6px 0; padding-left: 5px; }}
423 .analysis-summary h4 {{ margin: 20px 0 10px 0; color: #2c3e50; }}
424 .analysis-summary h3 {{ margin: 25px 0 15px 0; color: #34495e; }}
425 .analysis-summary p {{ margin: 12px 0; line-height: 1.6; }}
426 </style>
427 <script>
428 function parseJsonContent(jsonText) {{
429 try {{
430 const data = JSON.parse(jsonText);
431 let html = '';
432
433 // Analysis summary
434 if (data.analysis) {{
435 html += `<div class="analysis-summary">${{data.analysis}}</div>`;
436 }}
437
438 // Insights table
439 if (data.insights && data.insights.length > 0) {{
440 html += `
441 <h4>Key Insights</h4>
442 <table class="insights-table">
443 <thead>
444 <tr>
445 <th>Insight</th>
446 <th>Category</th>
447 <th>Description</th>
448 <th>Confidence</th>
449 <th>Evidence</th>
450 </tr>
451 </thead>
452 <tbody>`;
453
454 data.insights.forEach(insight => {{
455 const confidenceClass = insight.confidence >= 0.8 ? 'confidence-high' :
456 insight.confidence >= 0.6 ? 'confidence-medium' : 'confidence-low';
457 const evidence = insight.evidence && insight.evidence.length > 0 ?
458 '• ' + insight.evidence.join('<br>• ') : 'No specific evidence';
459
460 html += `
461 <tr>
462 <td><strong>${{insight.title}}</strong></td>
463 <td>${{insight.category}}</td>
464 <td>${{insight.description}}</td>
465 <td class="${{confidenceClass}}">${{Math.round(insight.confidence * 100)}}%</td>
466 <td>${{evidence}}</td>
467 </tr>`;
468 }});
469
470 html += '</tbody></table>';
471 }}
472
473 // Recommendations table
474 if (data.recommendations && data.recommendations.length > 0) {{
475 html += `
476 <h4>Recommendations</h4>
477 <table class="recommendations-table">
478 <thead>
479 <tr>
480 <th>Title</th>
481 <th>Description</th>
482 <th>Priority</th>
483 <th>Effort</th>
484 <th>Impact</th>
485 <th>Action Items</th>
486 </tr>
487 </thead>
488 <tbody>`;
489
490 data.recommendations.forEach(rec => {{
491 const priorityClass = rec.priority === 'High' || rec.priority === 'Critical' ? 'priority-high' :
492 rec.priority === 'Medium' ? 'priority-medium' : 'priority-low';
493 const actionItems = rec.action_items && rec.action_items.length > 0 ?
494 '• ' + rec.action_items.join('<br>• ') : 'No specific actions';
495
496 html += `
497 <tr class="${{priorityClass}}">
498 <td><strong>${{rec.title}}</strong></td>
499 <td>${{rec.description}}</td>
500 <td>${{rec.priority}}</td>
501 <td>${{rec.effort}}</td>
502 <td>${{rec.impact}}</td>
503 <td>${{actionItems}}</td>
504 </tr>`;
505 }});
506
507 html += '</tbody></table>';
508 }}
509
510 return html;
511 }} catch (e) {{
512 return `<p>Error parsing JSON content: ${{e.message}}</p>`;
513 }}
514 }}
515
516 function parseMarkdownContent(markdown) {{
517 let html = markdown;
518
519 // Convert headers first
520 html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
521 html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
522 html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
523 html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
524
525 // Convert bold text
526 html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
527
528 // Process line by line for better list handling
529 let lines = html.split('\n');
530 let processedLines = [];
531 let inUnorderedList = false;
532 let inOrderedList = false;
533
534 for (let i = 0; i < lines.length; i++) {{
535 let line = lines[i];
536 let trimmedLine = line.trim();
537
538 // Look ahead to see if there are more list items coming
539 function hasMoreListItems(startIndex, listType) {{
540 for (let j = startIndex + 1; j < lines.length; j++) {{
541 let nextTrimmed = lines[j].trim();
542 if (nextTrimmed === '') continue; // Skip empty lines
543
544 if (listType === 'ordered' && nextTrimmed.match(/^\d+\.\s+/)) {{
545 return true;
546 }}
547 if (listType === 'unordered' && nextTrimmed.match(/^[-*]\s+/)) {{
548 return true;
549 }}
550
551 // Stop looking if we hit a header or substantial content
552 if (nextTrimmed.match(/^<h[1-6]>/) ||
553 nextTrimmed.match(/^### /) ||
554 nextTrimmed.match(/^## /) ||
555 nextTrimmed.match(/^#### /) ||
556 (nextTrimmed.length > 0 && !nextTrimmed.match(/^[-*\d]\s*/) && !nextTrimmed.match(/^\d+\.\s+/))) {{
557 break;
558 }}
559 }}
560 return false;
561 }}
562
563 // Handle unordered list items
564 if (trimmedLine.match(/^[-*]\s+/)) {{
565 if (!inUnorderedList) {{
566 if (inOrderedList) {{
567 processedLines.push('</ol>');
568 inOrderedList = false;
569 }}
570 processedLines.push('<ul>');
571 inUnorderedList = true;
572 }}
573 let content = trimmedLine.replace(/^[-*]\s+/, '');
574 processedLines.push(`<li>${{content}}</li>`);
575
576 // Only close if no more unordered items are coming
577 if (!hasMoreListItems(i, 'unordered')) {{
578 processedLines.push('</ul>');
579 inUnorderedList = false;
580 }}
581 }}
582 // Handle ordered list items (1. 2. 3. etc.)
583 else if (trimmedLine.match(/^\d+\.\s+/)) {{
584 if (!inOrderedList) {{
585 if (inUnorderedList) {{
586 processedLines.push('</ul>');
587 inUnorderedList = false;
588 }}
589 processedLines.push('<ol>');
590 inOrderedList = true;
591 }}
592 let content = trimmedLine.replace(/^\d+\.\s+/, '');
593 processedLines.push(`<li>${{content}}</li>`);
594
595 // Only close if no more ordered items are coming
596 if (!hasMoreListItems(i, 'ordered')) {{
597 processedLines.push('</ol>');
598 inOrderedList = false;
599 }}
600 }}
601 // Handle regular content
602 else {{
603 // Close lists when we encounter headers or substantial content
604 if (trimmedLine && (trimmedLine.startsWith('<h') ||
605 trimmedLine.match(/^### /) ||
606 trimmedLine.match(/^## /) ||
607 trimmedLine.match(/^#### /))) {{
608 // Close any open lists when we hit headers
609 if (inUnorderedList) {{
610 processedLines.push('</ul>');
611 inUnorderedList = false;
612 }}
613 if (inOrderedList) {{
614 processedLines.push('</ol>');
615 inOrderedList = false;
616 }}
617 processedLines.push(line);
618 }} else if (trimmedLine && !trimmedLine.startsWith('<ul') && !trimmedLine.startsWith('<ol') && !trimmedLine.startsWith('</')) {{
619 // Close lists for substantial paragraph content
620 if (inUnorderedList) {{
621 processedLines.push('</ul>');
622 inUnorderedList = false;
623 }}
624 if (inOrderedList) {{
625 processedLines.push('</ol>');
626 inOrderedList = false;
627 }}
628 processedLines.push(`<p>${{line}}</p>`);
629 }} else {{
630 // Empty lines and HTML elements - keep them without closing lists
631 processedLines.push(line);
632 }}
633 }}
634 }}
635
636 // Close any remaining open lists
637 if (inUnorderedList) {{
638 processedLines.push('</ul>');
639 }}
640 if (inOrderedList) {{
641 processedLines.push('</ol>');
642 }}
643
644 return processedLines.join('\n');
645 }}
646
647 document.addEventListener('DOMContentLoaded', function() {{
648 // Process JSON content in any element that contains JSON
649 function processElementForJson(element) {{
650 const text = element.textContent || element.innerText;
651 if (text.trim().startsWith('```json') && text.trim().endsWith('```')) {{
652 const jsonContent = text.trim().slice(7, -3); // Remove ```json and ```
653 const processedHtml = parseJsonContent(jsonContent);
654 element.innerHTML = processedHtml;
655 element.style.whiteSpace = 'normal';
656 return true;
657 }} else if (text.trim().startsWith('{{') && text.trim().endsWith('}}')) {{
658 // Direct JSON content without markdown code blocks
659 try {{
660 const processedHtml = parseJsonContent(text.trim());
661 element.innerHTML = processedHtml;
662 element.style.whiteSpace = 'normal';
663 return true;
664 }} catch (e) {{
665 // Not valid JSON, continue to markdown processing
666 }}
667 }}
668 return false;
669 }}
670
671 // Process all potential JSON containers
672 document.querySelectorAll('p, div, .analysis-summary, .llm-analysis div').forEach(element => {{
673 if (processElementForJson(element)) {{
674 return; // Successfully processed as JSON
675 }}
676
677 // If not JSON, try markdown processing
678 const text = element.textContent || element.innerText;
679 if (text.includes('###') || text.includes('**') || text.includes('- ') || text.includes('#### ')) {{
680 const processedHtml = parseMarkdownContent(text);
681 element.innerHTML = processedHtml;
682 element.style.whiteSpace = 'normal';
683 }}
684 }});
685 }});
686 </script>
687</head>
688<body>
689 <div class="header">
690 <h1>Project Analysis Report</h1>
691 <p><strong>Project:</strong> {}</p>
692 <p><strong>Generated:</strong> {}</p>
693 <p><strong>Analysis Duration:</strong> {}ms</p>
694 <p><strong>LLM Model:</strong> {} ({})</p>
695 </div>
696
697 <div class="section">
698 <h2>Executive Summary</h2>
699 <div class="metric">
700 <strong>Complexity Score:</strong> {:.2}
701 </div>
702 <div class="metric">
703 <strong>Maintainability Score:</strong> {:.2}
704 </div>
705 <div class="metric">
706 <strong>Total Files:</strong> {}
707 </div>
708 <div class="metric">
709 <strong>Total Size:</strong> {:.2} MB
710 </div>
711 <p>{}</p>
712 </div>
713
714 <div class="section">
715 <h2>Key Recommendations</h2>
716 {}
717 </div>
718
719 <div class="section">
720 <h2>LLM Analysis & Insights</h2>
721 {}
722 </div>
723
724 <div class="section">
725 <h2>File Analysis</h2>
726 <h3>Language Distribution</h3>
727 <table>
728 <tr><th>Language</th><th>Files</th><th>Size (MB)</th><th>Percentage</th></tr>
729 {}
730 </table>
731 </div>
732
733</body>
734</html>"#,
735 report.metadata.project_name,
736 report.metadata.project_name,
737 report.metadata.generated_at,
738 report.metadata.analysis_duration_ms,
739 report.metadata.llm_model,
740 report.metadata.llm_provider,
741 report.executive_summary.complexity_score,
742 report.executive_summary.maintainability_score,
743 report.metadata.total_files,
744 report.metadata.total_size as f64 / (1024.0 * 1024.0),
745 report.executive_summary.overview,
746 report.recommendations.iter().take(5).map(|r| {
747 let priority_class = match r.priority {
748 Priority::High | Priority::Critical => "priority-high",
749 Priority::Medium => "priority-medium",
750 Priority::Low => "priority-low",
751 };
752 format!(r#"<div class="recommendation {}"><strong>{}</strong><p>{}</p></div>"#,
753 priority_class, r.title, r.description)
754 }).collect::<Vec<_>>().join("\n"),
755 self.generate_llm_insights_html(&report.llm_insights),
756 report.file_analysis.language_breakdown.iter().map(|l| {
757 format!("<tr><td>{}</td><td>{}</td><td>{:.2}</td><td>{:.1}%</td></tr>",
758 l.language, l.file_count, l.total_size as f64 / (1024.0 * 1024.0), l.percentage)
759 }).collect::<Vec<_>>().join("\n")
760 );
761
762 Ok(html)
763 }
764
765 fn generate_llm_insights_html(&self, llm_insights: &[AnalysisResponse]) -> String {
766 if llm_insights.is_empty() {
767 return "<p>No LLM analysis was performed for this project.</p>".to_string();
768 }
769
770 let mut html = String::new();
771
772 for (index, analysis) in llm_insights.iter().enumerate() {
773 let analysis_type = match index {
775 0 => "Overview",
776 1 => "Architecture",
777 2 => "Dependencies",
778 3 => "Security",
779 4 => "Refactoring",
780 5 => "Documentation",
781 _ => "Additional Analysis",
782 };
783
784 html.push_str(&format!(r#"<div class="llm-analysis">
785 <div class="analysis-type">{} Analysis</div>"#, analysis_type));
786
787 let analysis_text = self.extract_analysis_text(&analysis.analysis);
789 html.push_str(&format!(r#"<div class="analysis-summary">{}</div>"#, analysis_text));
790
791 let insights = if !analysis.insights.is_empty() {
793 analysis.insights.clone()
794 } else {
795 self.extract_insights_from_text(&analysis.analysis)
796 };
797
798 if !insights.is_empty() {
799 html.push_str(r#"<h4>Key Insights</h4>
800 <table class="insights-table">
801 <thead>
802 <tr>
803 <th>Insight</th>
804 <th>Category</th>
805 <th>Description</th>
806 <th>Confidence</th>
807 <th>Evidence</th>
808 </tr>
809 </thead>
810 <tbody>"#);
811
812 for insight in &insights {
813 let confidence_class = if insight.confidence >= 0.8 {
814 "confidence-high"
815 } else if insight.confidence >= 0.6 {
816 "confidence-medium"
817 } else {
818 "confidence-low"
819 };
820
821 let evidence_text = if insight.evidence.is_empty() {
822 "No specific evidence".to_string()
823 } else {
824 insight.evidence.join("<br>• ")
825 };
826
827 html.push_str(&format!(r#"<tr>
828 <td><strong>{}</strong></td>
829 <td>{:?}</td>
830 <td>{}</td>
831 <td class="{}">{:.0}%</td>
832 <td>• {}</td>
833 </tr>"#,
834 insight.title, insight.category, insight.description,
835 confidence_class, insight.confidence * 100.0, evidence_text));
836 }
837
838 html.push_str("</tbody></table>");
839 }
840
841 let recommendations = if !analysis.recommendations.is_empty() {
843 analysis.recommendations.clone()
844 } else {
845 self.extract_recommendations_from_text(&analysis.analysis)
846 };
847
848 if !recommendations.is_empty() {
849 html.push_str(r#"<h4>Recommendations</h4>
850 <table class="recommendations-table">
851 <thead>
852 <tr>
853 <th>Title</th>
854 <th>Description</th>
855 <th>Priority</th>
856 <th>Effort</th>
857 <th>Impact</th>
858 <th>Action Items</th>
859 </tr>
860 </thead>
861 <tbody>"#);
862
863 for recommendation in &recommendations {
864 let priority_class = match recommendation.priority {
865 Priority::High | Priority::Critical => "priority-high",
866 Priority::Medium => "priority-medium",
867 Priority::Low => "priority-low",
868 };
869
870 let action_items_text = if recommendation.action_items.is_empty() {
871 "No specific actions".to_string()
872 } else {
873 recommendation.action_items.join("<br>• ")
874 };
875
876 html.push_str(&format!(r#"<tr class="{}">
877 <td><strong>{}</strong></td>
878 <td>{}</td>
879 <td>{:?}</td>
880 <td>{:?}</td>
881 <td>{:?}</td>
882 <td>• {}</td>
883 </tr>"#,
884 priority_class, recommendation.title, recommendation.description,
885 recommendation.priority, recommendation.effort, recommendation.impact,
886 action_items_text));
887 }
888
889 html.push_str("</tbody></table>");
890 }
891
892 html.push_str("</div>");
893 }
894
895 html
896 }
897
898 fn extract_analysis_text(&self, content: &str) -> String {
899 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(content) {
901 if let Some(analysis_text) = json_value.get("analysis").and_then(|v| v.as_str()) {
902 return analysis_text.to_string();
903 }
904 }
905
906 if content.trim().starts_with("```json") && content.trim().ends_with("```") {
908 let json_content = content.trim()
909 .strip_prefix("```json").unwrap_or(content)
910 .strip_suffix("```").unwrap_or(content)
911 .trim();
912
913 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
914 if let Some(analysis_text) = json_value.get("analysis").and_then(|v| v.as_str()) {
915 return analysis_text.to_string();
916 }
917 }
918 }
919
920 content.to_string()
922 }
923
924
925 fn extract_insights_from_text(&self, text: &str) -> Vec<crate::llm::Insight> {
926 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(text) {
928 if let Some(insights_array) = json_value.get("insights").and_then(|v| v.as_array()) {
929 let mut insights = Vec::new();
930 for insight_json in insights_array {
931 if let Ok(insight) = serde_json::from_value::<crate::llm::Insight>(insight_json.clone()) {
932 insights.push(insight);
933 }
934 }
935 return insights;
936 }
937 }
938
939 if text.trim().starts_with("```json") && text.trim().ends_with("```") {
941 let json_content = text.trim()
942 .strip_prefix("```json").unwrap_or(text)
943 .strip_suffix("```").unwrap_or(text)
944 .trim();
945
946 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
947 if let Some(insights_array) = json_value.get("insights").and_then(|v| v.as_array()) {
948 let mut insights = Vec::new();
949 for insight_json in insights_array {
950 if let Ok(insight) = serde_json::from_value::<crate::llm::Insight>(insight_json.clone()) {
951 insights.push(insight);
952 }
953 }
954 return insights;
955 }
956 }
957 }
958
959 Vec::new()
960 }
961
962 fn extract_recommendations_from_text(&self, text: &str) -> Vec<crate::llm::Recommendation> {
963 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(text) {
965 if let Some(recommendations_array) = json_value.get("recommendations").and_then(|v| v.as_array()) {
966 let mut recommendations = Vec::new();
967 for rec_json in recommendations_array {
968 if let Ok(recommendation) = serde_json::from_value::<crate::llm::Recommendation>(rec_json.clone()) {
969 recommendations.push(recommendation);
970 }
971 }
972 return recommendations;
973 }
974 }
975
976 if text.trim().starts_with("```json") && text.trim().ends_with("```") {
978 let json_content = text.trim()
979 .strip_prefix("```json").unwrap_or(text)
980 .strip_suffix("```").unwrap_or(text)
981 .trim();
982
983 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(json_content) {
984 if let Some(recommendations_array) = json_value.get("recommendations").and_then(|v| v.as_array()) {
985 let mut recommendations = Vec::new();
986 for rec_json in recommendations_array {
987 if let Ok(recommendation) = serde_json::from_value::<crate::llm::Recommendation>(rec_json.clone()) {
988 recommendations.push(recommendation);
989 }
990 }
991 return recommendations;
992 }
993 }
994 }
995
996 Vec::new()
997 }
998
999 fn generate_markdown_summary(&self, report: &Report) -> Result<String> {
1000 let mut md = format!(
1001 "# Project Analysis Summary\n\n**Project:** {}\n**Generated:** {}\n**Analysis Duration:** {}ms\n\n",
1002 report.metadata.project_name,
1003 report.metadata.generated_at,
1004 report.metadata.analysis_duration_ms
1005 );
1006
1007 md.push_str("## Executive Summary\n\n");
1008 md.push_str(&format!("- **Complexity Score:** {:.2}/10\n", report.executive_summary.complexity_score));
1009 md.push_str(&format!("- **Maintainability Score:** {:.2}/10\n", report.executive_summary.maintainability_score));
1010 md.push_str(&format!("- **Total Files:** {}\n", report.metadata.total_files));
1011 md.push_str(&format!("- **Total Size:** {:.2} MB\n\n", report.metadata.total_size as f64 / (1024.0 * 1024.0)));
1012
1013 md.push_str("## Top Recommendations\n\n");
1014 for (i, rec) in report.recommendations.iter().take(5).enumerate() {
1015 md.push_str(&format!("{}. **{}** (Priority: {:?})\n {}\n\n",
1016 i + 1, rec.title, rec.priority, rec.description));
1017 }
1018
1019 md.push_str("## Language Distribution\n\n");
1020 for lang in &report.file_analysis.language_breakdown {
1021 md.push_str(&format!("- **{}:** {} files ({:.1}%), {:.2} MB\n",
1022 lang.language, lang.file_count, lang.percentage, lang.total_size as f64 / (1024.0 * 1024.0)));
1023 }
1024
1025 Ok(md)
1026 }
1027}