learn_bash_from_session_data 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2029 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ HTML Generator for Bash Learning Report
4
+
5
+ Generates a single self-contained HTML file with all CSS and JS inline.
6
+ No external dependencies - pure Python standard library.
7
+ """
8
+
9
+ from typing import Any
10
+ from datetime import datetime
11
+ import html
12
+ import json
13
+
14
+
15
+ def generate_html(analysis_result: dict[str, Any], quizzes: list[dict[str, Any]]) -> str:
16
+ """
17
+ Generate complete HTML report from analysis results and quizzes.
18
+
19
+ Args:
20
+ analysis_result: Dictionary containing parsed commands, stats, categories
21
+ quizzes: List of quiz question dictionaries
22
+
23
+ Returns:
24
+ Complete HTML string ready to write to file
25
+ """
26
+ stats = analysis_result.get("stats", {})
27
+ commands = analysis_result.get("commands", [])
28
+ categories = analysis_result.get("categories", {})
29
+
30
+ overview_html = render_overview_tab(stats, commands, categories)
31
+ commands_html = render_commands_tab(commands)
32
+ lessons_html = render_lessons_tab(categories, commands)
33
+ quiz_html = render_quiz_tab(quizzes)
34
+
35
+ inline_css = get_inline_css()
36
+ inline_js = get_inline_js(quizzes)
37
+
38
+ generation_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
39
+
40
+ return f'''<!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8">
44
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
45
+ <title>Bash Command Learning Report</title>
46
+ <style>
47
+ {inline_css}
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <div class="container">
52
+ <header class="header">
53
+ <div class="header-content">
54
+ <h1>Bash Command Learning Report</h1>
55
+ <p class="subtitle">Generated: {generation_time}</p>
56
+ </div>
57
+ <button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark mode">
58
+ <span class="theme-icon light-icon">&#9728;</span>
59
+ <span class="theme-icon dark-icon">&#9790;</span>
60
+ </button>
61
+ </header>
62
+
63
+ <nav class="tabs" role="tablist">
64
+ <button class="tab active" data-tab="overview" role="tab" aria-selected="true" aria-controls="panel-overview">
65
+ <span class="tab-icon">&#9776;</span>
66
+ <span class="tab-label">Overview</span>
67
+ <span class="tab-key">1</span>
68
+ </button>
69
+ <button class="tab" data-tab="commands" role="tab" aria-selected="false" aria-controls="panel-commands">
70
+ <span class="tab-icon">&#10095;</span>
71
+ <span class="tab-label">Commands</span>
72
+ <span class="tab-key">2</span>
73
+ </button>
74
+ <button class="tab" data-tab="lessons" role="tab" aria-selected="false" aria-controls="panel-lessons">
75
+ <span class="tab-icon">&#128218;</span>
76
+ <span class="tab-label">Lessons</span>
77
+ <span class="tab-key">3</span>
78
+ </button>
79
+ <button class="tab" data-tab="quiz" role="tab" aria-selected="false" aria-controls="panel-quiz">
80
+ <span class="tab-icon">&#10067;</span>
81
+ <span class="tab-label">Quiz</span>
82
+ <span class="tab-key">4</span>
83
+ </button>
84
+ </nav>
85
+
86
+ <main class="content">
87
+ <section id="panel-overview" class="panel active" role="tabpanel" aria-labelledby="tab-overview">
88
+ {overview_html}
89
+ </section>
90
+
91
+ <section id="panel-commands" class="panel" role="tabpanel" aria-labelledby="tab-commands">
92
+ {commands_html}
93
+ </section>
94
+
95
+ <section id="panel-lessons" class="panel" role="tabpanel" aria-labelledby="tab-lessons">
96
+ {lessons_html}
97
+ </section>
98
+
99
+ <section id="panel-quiz" class="panel" role="tabpanel" aria-labelledby="tab-quiz">
100
+ {quiz_html}
101
+ </section>
102
+ </main>
103
+
104
+ <footer class="footer">
105
+ <p>Learn Bash from Session Data | Press 1-4 to switch tabs</p>
106
+ </footer>
107
+ </div>
108
+
109
+ <script>
110
+ {inline_js}
111
+ </script>
112
+ </body>
113
+ </html>'''
114
+
115
+
116
+ def render_overview_tab(stats: dict[str, Any], commands: list[dict], categories: dict) -> str:
117
+ """Render the overview/dashboard tab content."""
118
+ total_commands = stats.get("total_commands", 0)
119
+ unique_commands = stats.get("unique_commands", 0)
120
+ unique_utilities = stats.get("unique_utilities", 0)
121
+ date_range = stats.get("date_range", {"start": "N/A", "end": "N/A"})
122
+ complexity_dist = stats.get("complexity_distribution", {"simple": 0, "intermediate": 0, "advanced": 0})
123
+
124
+ # Calculate percentages for complexity bars
125
+ total_for_pct = sum(complexity_dist.values()) or 1
126
+ simple_pct = (complexity_dist.get("simple", 0) / total_for_pct) * 100
127
+ intermediate_pct = (complexity_dist.get("intermediate", 0) / total_for_pct) * 100
128
+ advanced_pct = (complexity_dist.get("advanced", 0) / total_for_pct) * 100
129
+
130
+ # Top 10 commands by frequency
131
+ sorted_commands = sorted(commands, key=lambda x: x.get("frequency", 0), reverse=True)[:10]
132
+ top_commands_html = ""
133
+ max_freq = sorted_commands[0].get("frequency", 1) if sorted_commands else 1
134
+
135
+ for cmd in sorted_commands:
136
+ freq = cmd.get("frequency", 0)
137
+ bar_width = (freq / max_freq) * 100
138
+ cmd_name = html.escape(cmd.get("base_command", "unknown"))
139
+ top_commands_html += f'''
140
+ <div class="top-command-item">
141
+ <div class="top-command-name">
142
+ <code class="cmd">{cmd_name}</code>
143
+ </div>
144
+ <div class="top-command-bar-container">
145
+ <div class="top-command-bar" style="width: {bar_width}%"></div>
146
+ </div>
147
+ <div class="top-command-count">{freq}</div>
148
+ </div>'''
149
+
150
+ # New commands (first appearances)
151
+ new_commands = [c for c in commands if c.get("is_new", False)][:8]
152
+ new_commands_html = ""
153
+ for cmd in new_commands:
154
+ cmd_name = html.escape(cmd.get("base_command", "unknown"))
155
+ first_seen = cmd.get("first_seen", "")
156
+ new_commands_html += f'''
157
+ <div class="new-command-chip">
158
+ <code class="cmd">{cmd_name}</code>
159
+ <span class="first-seen">{first_seen}</span>
160
+ </div>'''
161
+
162
+ if not new_commands_html:
163
+ new_commands_html = '<p class="empty-state">No new commands detected in this session</p>'
164
+
165
+ # Category breakdown for pie chart
166
+ category_data = []
167
+ cat_colors = [
168
+ "#4285f4", "#ea4335", "#fbbc05", "#34a853", "#ff6d01",
169
+ "#46bdc6", "#7baaf7", "#f07b72", "#fcd04f", "#81c995"
170
+ ]
171
+ sorted_cats = sorted(categories.items(), key=lambda x: len(x[1]), reverse=True)[:8]
172
+
173
+ for idx, (cat_name, cat_cmds) in enumerate(sorted_cats):
174
+ color = cat_colors[idx % len(cat_colors)]
175
+ count = len(cat_cmds)
176
+ category_data.append({
177
+ "name": cat_name,
178
+ "count": count,
179
+ "color": color
180
+ })
181
+
182
+ # Generate SVG pie chart
183
+ pie_svg = _generate_pie_chart(category_data)
184
+
185
+ # Category legend
186
+ category_legend = ""
187
+ for cat in category_data:
188
+ category_legend += f'''
189
+ <div class="legend-item">
190
+ <span class="legend-color" style="background: {cat['color']}"></span>
191
+ <span class="legend-label">{html.escape(cat['name'])}</span>
192
+ <span class="legend-count">{cat['count']}</span>
193
+ </div>'''
194
+
195
+ return f'''
196
+ <div class="dashboard">
197
+ <div class="stats-grid">
198
+ <div class="stat-card">
199
+ <div class="stat-value">{total_commands}</div>
200
+ <div class="stat-label">Total Commands</div>
201
+ </div>
202
+ <div class="stat-card">
203
+ <div class="stat-value">{unique_commands}</div>
204
+ <div class="stat-label">Unique Commands</div>
205
+ </div>
206
+ <div class="stat-card">
207
+ <div class="stat-value">{unique_utilities}</div>
208
+ <div class="stat-label">Unique Utilities</div>
209
+ </div>
210
+ <div class="stat-card">
211
+ <div class="stat-value date-value">{date_range.get("start", "N/A")}</div>
212
+ <div class="stat-label">to {date_range.get("end", "N/A")}</div>
213
+ </div>
214
+ </div>
215
+
216
+ <div class="charts-row">
217
+ <div class="chart-card">
218
+ <h3>Complexity Distribution</h3>
219
+ <div class="complexity-bars">
220
+ <div class="complexity-row">
221
+ <span class="complexity-label simple">Simple</span>
222
+ <div class="complexity-bar-bg">
223
+ <div class="complexity-bar simple" style="width: {simple_pct}%"></div>
224
+ </div>
225
+ <span class="complexity-count">{complexity_dist.get("simple", 0)}</span>
226
+ </div>
227
+ <div class="complexity-row">
228
+ <span class="complexity-label intermediate">Intermediate</span>
229
+ <div class="complexity-bar-bg">
230
+ <div class="complexity-bar intermediate" style="width: {intermediate_pct}%"></div>
231
+ </div>
232
+ <span class="complexity-count">{complexity_dist.get("intermediate", 0)}</span>
233
+ </div>
234
+ <div class="complexity-row">
235
+ <span class="complexity-label advanced">Advanced</span>
236
+ <div class="complexity-bar-bg">
237
+ <div class="complexity-bar advanced" style="width: {advanced_pct}%"></div>
238
+ </div>
239
+ <span class="complexity-count">{complexity_dist.get("advanced", 0)}</span>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <div class="chart-card">
245
+ <h3>Category Breakdown</h3>
246
+ <div class="pie-container">
247
+ {pie_svg}
248
+ <div class="category-legend">
249
+ {category_legend}
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="charts-row">
256
+ <div class="chart-card wide">
257
+ <h3>Top 10 Most-Used Commands</h3>
258
+ <div class="top-commands">
259
+ {top_commands_html}
260
+ </div>
261
+ </div>
262
+
263
+ <div class="chart-card">
264
+ <h3>New Commands</h3>
265
+ <div class="new-commands">
266
+ {new_commands_html}
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>'''
271
+
272
+
273
+ def _generate_pie_chart(category_data: list[dict]) -> str:
274
+ """Generate an SVG pie chart."""
275
+ if not category_data:
276
+ return '<div class="empty-state">No category data</div>'
277
+
278
+ total = sum(c["count"] for c in category_data)
279
+ if total == 0:
280
+ return '<div class="empty-state">No commands to display</div>'
281
+
282
+ # SVG pie chart
283
+ cx, cy, r = 80, 80, 70
284
+ paths = []
285
+ current_angle = -90 # Start from top
286
+
287
+ for cat in category_data:
288
+ pct = cat["count"] / total
289
+ angle = pct * 360
290
+
291
+ # Calculate arc
292
+ start_rad = current_angle * 3.14159 / 180
293
+ end_rad = (current_angle + angle) * 3.14159 / 180
294
+
295
+ x1 = cx + r * (end_rad - start_rad > 0 and 1 or -1) * __import__('math').cos(start_rad)
296
+ y1 = cy + r * __import__('math').sin(start_rad)
297
+ x2 = cx + r * __import__('math').cos(end_rad)
298
+ y2 = cy + r * __import__('math').sin(end_rad)
299
+
300
+ large_arc = 1 if angle > 180 else 0
301
+
302
+ # Create path
303
+ import math
304
+ x1 = cx + r * math.cos(start_rad)
305
+ y1 = cy + r * math.sin(start_rad)
306
+ x2 = cx + r * math.cos(end_rad)
307
+ y2 = cy + r * math.sin(end_rad)
308
+
309
+ path = f'M {cx} {cy} L {x1:.2f} {y1:.2f} A {r} {r} 0 {large_arc} 1 {x2:.2f} {y2:.2f} Z'
310
+ paths.append(f'<path d="{path}" fill="{cat["color"]}" stroke="#fff" stroke-width="2"><title>{html.escape(cat["name"])}: {cat["count"]}</title></path>')
311
+
312
+ current_angle += angle
313
+
314
+ return f'''<svg viewBox="0 0 160 160" class="pie-chart">
315
+ {''.join(paths)}
316
+ </svg>'''
317
+
318
+
319
+ def render_commands_tab(commands: list[dict]) -> str:
320
+ """Render the commands reference tab."""
321
+ # Group by category for filter chips
322
+ categories_set = set()
323
+ for cmd in commands:
324
+ categories_set.add(cmd.get("category", "Other"))
325
+
326
+ category_chips = ""
327
+ for cat in sorted(categories_set):
328
+ category_chips += f'<button class="filter-chip" data-category="{html.escape(cat)}">{html.escape(cat)}</button>'
329
+
330
+ # Generate command cards
331
+ commands_html = ""
332
+ for idx, cmd in enumerate(commands):
333
+ cmd_id = f"cmd-{idx}"
334
+ base_cmd = html.escape(cmd.get("base_command", "unknown"))
335
+ full_cmd = html.escape(cmd.get("full_command", ""))
336
+ category = html.escape(cmd.get("category", "Other"))
337
+ complexity = cmd.get("complexity", "simple")
338
+ frequency = cmd.get("frequency", 0)
339
+ description = html.escape(cmd.get("description", "No description available"))
340
+
341
+ # Syntax highlighted command
342
+ highlighted = _syntax_highlight(cmd.get("full_command", ""))
343
+
344
+ # Flags breakdown
345
+ flags = cmd.get("flags", [])
346
+ flags_html = ""
347
+ if flags:
348
+ flags_html = '<div class="flags-section"><h5>Flags:</h5><ul class="flags-list">'
349
+ for flag in flags:
350
+ flag_name = html.escape(flag.get("flag", ""))
351
+ flag_desc = html.escape(flag.get("description", ""))
352
+ flags_html += f'<li><code class="flag">{flag_name}</code> - {flag_desc}</li>'
353
+ flags_html += '</ul></div>'
354
+
355
+ # Output preview
356
+ output_preview = cmd.get("output_preview", "")
357
+ output_html = ""
358
+ if output_preview:
359
+ output_html = f'''
360
+ <div class="output-section">
361
+ <h5>Example Output:</h5>
362
+ <pre class="output-preview">{html.escape(output_preview)}</pre>
363
+ </div>'''
364
+
365
+ commands_html += f'''
366
+ <div class="command-card" data-category="{category}" data-complexity="{complexity}" data-frequency="{frequency}" data-name="{base_cmd}">
367
+ <div class="command-header" onclick="toggleCommand('{cmd_id}')">
368
+ <div class="command-main">
369
+ <code class="cmd">{base_cmd}</code>
370
+ <span class="complexity-badge {complexity}">{complexity}</span>
371
+ <span class="category-badge">{category}</span>
372
+ </div>
373
+ <div class="command-meta">
374
+ <span class="frequency">Used {frequency}x</span>
375
+ <span class="expand-icon">&#9660;</span>
376
+ </div>
377
+ </div>
378
+ <div class="command-details" id="{cmd_id}">
379
+ <div class="full-command">
380
+ <h5>Full Command:</h5>
381
+ <pre class="syntax-highlighted">{highlighted}</pre>
382
+ </div>
383
+ <div class="description">
384
+ <h5>Description:</h5>
385
+ <p>{description}</p>
386
+ </div>
387
+ {flags_html}
388
+ {output_html}
389
+ </div>
390
+ </div>'''
391
+
392
+ return f'''
393
+ <div class="commands-container">
394
+ <div class="commands-toolbar">
395
+ <div class="search-box">
396
+ <input type="text" id="command-search" placeholder="Search commands..." oninput="filterCommands()">
397
+ </div>
398
+ <div class="sort-controls">
399
+ <label>Sort by:</label>
400
+ <select id="sort-select" onchange="sortCommands()">
401
+ <option value="frequency">Frequency</option>
402
+ <option value="complexity">Complexity</option>
403
+ <option value="category">Category</option>
404
+ <option value="name">Alphabetical</option>
405
+ </select>
406
+ </div>
407
+ </div>
408
+
409
+ <div class="filter-chips">
410
+ <button class="filter-chip active" data-category="all">All</button>
411
+ {category_chips}
412
+ </div>
413
+
414
+ <div class="commands-list" id="commands-list">
415
+ {commands_html}
416
+ </div>
417
+ </div>'''
418
+
419
+
420
+ def _syntax_highlight(command: str) -> str:
421
+ """Apply syntax highlighting to a bash command."""
422
+ if not command:
423
+ return ""
424
+
425
+ import re
426
+
427
+ # Escape HTML first
428
+ escaped = html.escape(command)
429
+
430
+ # Patterns for highlighting (order matters)
431
+ patterns = [
432
+ # Strings (single and double quoted)
433
+ (r'(&quot;[^&]*?&quot;|&#x27;[^&]*?&#x27;)', r'<span class="string">\1</span>'),
434
+ # Paths (starting with / or ./ or ~/)
435
+ (r'(\s|^)((?:/[\w.-]+)+|\.{1,2}/[\w./-]*|~/[\w./-]*)', r'\1<span class="path">\2</span>'),
436
+ # Flags (long and short)
437
+ (r'(\s)(--?[\w-]+)', r'\1<span class="flag">\2</span>'),
438
+ # Operators and redirects
439
+ (r'(\||&amp;&amp;|&gt;|&lt;|&gt;&gt;|\$\(|\))', r'<span class="operator">\1</span>'),
440
+ # Variables
441
+ (r'(\$[\w{}]+)', r'<span class="variable">\1</span>'),
442
+ ]
443
+
444
+ result = escaped
445
+ for pattern, replacement in patterns:
446
+ result = re.sub(pattern, replacement, result)
447
+
448
+ # Highlight the base command (first word)
449
+ result = re.sub(r'^([\w.-]+)', r'<span class="cmd">\1</span>', result)
450
+
451
+ return result
452
+
453
+
454
+ def render_lessons_tab(categories: dict, commands: list[dict]) -> str:
455
+ """Render the categorized lessons tab."""
456
+ if not categories:
457
+ return '<div class="empty-state">No categories found in the session data</div>'
458
+
459
+ # Sort categories by command count
460
+ sorted_cats = sorted(categories.items(), key=lambda x: len(x[1]), reverse=True)
461
+
462
+ lessons_html = ""
463
+ for cat_name, cat_commands in sorted_cats:
464
+ if not cat_commands:
465
+ continue
466
+
467
+ # Get full command data for this category
468
+ cat_cmd_data = [c for c in commands if c.get("category") == cat_name]
469
+
470
+ # Concept overview based on category
471
+ concept = _get_category_concept(cat_name)
472
+
473
+ # Commands in this category
474
+ cat_commands_html = ""
475
+ for cmd in cat_cmd_data[:10]: # Limit to 10 per category
476
+ base_cmd = html.escape(cmd.get("base_command", ""))
477
+ description = html.escape(cmd.get("description", ""))
478
+ complexity = cmd.get("complexity", "simple")
479
+ highlighted = _syntax_highlight(cmd.get("full_command", ""))
480
+
481
+ cat_commands_html += f'''
482
+ <div class="lesson-command">
483
+ <div class="lesson-command-header">
484
+ <code class="cmd">{base_cmd}</code>
485
+ <span class="complexity-badge {complexity}">{complexity}</span>
486
+ </div>
487
+ <pre class="syntax-highlighted">{highlighted}</pre>
488
+ <p class="lesson-description">{description}</p>
489
+ </div>'''
490
+
491
+ # Patterns observed
492
+ patterns = _extract_patterns(cat_cmd_data)
493
+ patterns_html = ""
494
+ if patterns:
495
+ patterns_html = '<div class="patterns"><h4>Patterns Observed:</h4><ul>'
496
+ for pattern in patterns:
497
+ patterns_html += f'<li>{html.escape(pattern)}</li>'
498
+ patterns_html += '</ul></div>'
499
+
500
+ lessons_html += f'''
501
+ <div class="lesson-section">
502
+ <h2 class="lesson-title">
503
+ <span class="lesson-icon">{_get_category_icon(cat_name)}</span>
504
+ {html.escape(cat_name)}
505
+ <span class="lesson-count">({len(cat_commands)} commands)</span>
506
+ </h2>
507
+ <div class="lesson-content">
508
+ <div class="concept-overview">
509
+ <h4>Concept Overview:</h4>
510
+ <p>{html.escape(concept)}</p>
511
+ </div>
512
+ <div class="lesson-commands">
513
+ <h4>Commands:</h4>
514
+ {cat_commands_html}
515
+ </div>
516
+ {patterns_html}
517
+ </div>
518
+ </div>'''
519
+
520
+ return f'''
521
+ <div class="lessons-container">
522
+ {lessons_html}
523
+ </div>'''
524
+
525
+
526
+ def _get_category_concept(category: str) -> str:
527
+ """Get concept overview for a category."""
528
+ concepts = {
529
+ "File Management": "Commands for creating, copying, moving, deleting, and organizing files and directories in the filesystem.",
530
+ "Text Processing": "Tools for searching, filtering, transforming, and manipulating text content in files and streams.",
531
+ "System Administration": "Commands for managing system resources, processes, users, and system configuration.",
532
+ "Network": "Utilities for network communication, file transfer, and connectivity diagnostics.",
533
+ "Package Management": "Tools for installing, updating, and managing software packages on the system.",
534
+ "Version Control": "Git and other version control commands for tracking changes and collaborating on code.",
535
+ "Process Management": "Commands for viewing, controlling, and managing running processes.",
536
+ "User Management": "Tools for managing user accounts, permissions, and access control.",
537
+ "Disk Management": "Utilities for managing disk space, partitions, and storage devices.",
538
+ "Shell Scripting": "Built-in shell commands and constructs for scripting and automation.",
539
+ "Development": "Programming tools, compilers, interpreters, and development utilities.",
540
+ "Compression": "Tools for compressing, archiving, and extracting files.",
541
+ "Search": "Commands for finding files, searching content, and locating resources.",
542
+ "Permissions": "Tools for managing file permissions, ownership, and access control.",
543
+ }
544
+ return concepts.get(category, f"Commands related to {category.lower()} operations and utilities.")
545
+
546
+
547
+ def _get_category_icon(category: str) -> str:
548
+ """Get icon for a category."""
549
+ icons = {
550
+ "File Management": "&#128193;",
551
+ "Text Processing": "&#128196;",
552
+ "System Administration": "&#9881;",
553
+ "Network": "&#127760;",
554
+ "Package Management": "&#128230;",
555
+ "Version Control": "&#128202;",
556
+ "Process Management": "&#9654;",
557
+ "User Management": "&#128100;",
558
+ "Disk Management": "&#128191;",
559
+ "Shell Scripting": "&#10095;",
560
+ "Development": "&#128187;",
561
+ "Compression": "&#128230;",
562
+ "Search": "&#128269;",
563
+ "Permissions": "&#128274;",
564
+ }
565
+ return icons.get(category, "&#128204;")
566
+
567
+
568
+ def _extract_patterns(commands: list[dict]) -> list[str]:
569
+ """Extract common patterns from a list of commands."""
570
+ patterns = []
571
+
572
+ # Check for piping patterns
573
+ piped = [c for c in commands if "|" in c.get("full_command", "")]
574
+ if piped:
575
+ patterns.append(f"Piping output between commands ({len(piped)} instances)")
576
+
577
+ # Check for redirection
578
+ redirected = [c for c in commands if any(r in c.get("full_command", "") for r in [">", ">>", "<"])]
579
+ if redirected:
580
+ patterns.append(f"Output/input redirection ({len(redirected)} instances)")
581
+
582
+ # Check for flag usage
583
+ with_flags = [c for c in commands if c.get("flags")]
584
+ if with_flags:
585
+ common_flags = {}
586
+ for cmd in with_flags:
587
+ for flag in cmd.get("flags", []):
588
+ f = flag.get("flag", "")
589
+ common_flags[f] = common_flags.get(f, 0) + 1
590
+ if common_flags:
591
+ top_flag = max(common_flags.items(), key=lambda x: x[1])
592
+ if top_flag[1] > 1:
593
+ patterns.append(f"Common flag: {top_flag[0]} (used {top_flag[1]} times)")
594
+
595
+ # Check for glob patterns
596
+ globbed = [c for c in commands if any(g in c.get("full_command", "") for g in ["*", "?", "["])]
597
+ if globbed:
598
+ patterns.append(f"Glob/wildcard patterns ({len(globbed)} instances)")
599
+
600
+ return patterns[:4] # Limit to 4 patterns
601
+
602
+
603
+ def render_quiz_tab(quizzes: list[dict]) -> str:
604
+ """Render the quiz tab."""
605
+ if not quizzes:
606
+ return '''
607
+ <div class="quiz-container">
608
+ <div class="empty-state">No quiz questions available</div>
609
+ </div>'''
610
+
611
+ questions_html = ""
612
+ for idx, quiz in enumerate(quizzes):
613
+ q_id = f"q{idx}"
614
+ question = html.escape(quiz.get("question", ""))
615
+ options = quiz.get("options", [])
616
+ correct = quiz.get("correct_answer", 0)
617
+ explanation = html.escape(quiz.get("explanation", ""))
618
+
619
+ options_html = ""
620
+ for opt_idx, option in enumerate(options):
621
+ opt_letter = chr(65 + opt_idx) # A, B, C, D
622
+ options_html += f'''
623
+ <label class="quiz-option" data-question="{q_id}" data-index="{opt_idx}">
624
+ <input type="radio" name="{q_id}" value="{opt_idx}" onchange="checkAnswer('{q_id}', {opt_idx}, {correct})">
625
+ <span class="option-letter">{opt_letter}</span>
626
+ <span class="option-text">{html.escape(option)}</span>
627
+ </label>'''
628
+
629
+ questions_html += f'''
630
+ <div class="quiz-question" id="question-{q_id}">
631
+ <div class="question-number">Question {idx + 1}</div>
632
+ <div class="question-text">{question}</div>
633
+ <div class="quiz-options">
634
+ {options_html}
635
+ </div>
636
+ <div class="quiz-feedback" id="feedback-{q_id}">
637
+ <div class="feedback-result"></div>
638
+ <div class="feedback-explanation">{explanation}</div>
639
+ </div>
640
+ </div>'''
641
+
642
+ return f'''
643
+ <div class="quiz-container">
644
+ <div class="quiz-header">
645
+ <h2>Test Your Knowledge</h2>
646
+ <p>Answer the following questions to test your understanding of the bash commands.</p>
647
+ <div class="quiz-score">
648
+ <span>Score: </span>
649
+ <span id="score-current">0</span>
650
+ <span> / </span>
651
+ <span id="score-total">{len(quizzes)}</span>
652
+ </div>
653
+ </div>
654
+
655
+ <div class="quiz-questions">
656
+ {questions_html}
657
+ </div>
658
+
659
+ <div class="quiz-actions">
660
+ <button class="btn btn-secondary" onclick="resetQuiz()">Try Again</button>
661
+ </div>
662
+ </div>'''
663
+
664
+
665
+ def get_inline_css() -> str:
666
+ """Return all CSS styles."""
667
+ return '''
668
+ /* CSS Reset and Base */
669
+ *, *::before, *::after {
670
+ box-sizing: border-box;
671
+ margin: 0;
672
+ padding: 0;
673
+ }
674
+
675
+ :root {
676
+ --bg-primary: #ffffff;
677
+ --bg-secondary: #f8f9fa;
678
+ --bg-tertiary: #e9ecef;
679
+ --text-primary: #212529;
680
+ --text-secondary: #6c757d;
681
+ --text-muted: #adb5bd;
682
+ --border-color: #dee2e6;
683
+ --accent-primary: #4285f4;
684
+ --accent-success: #34a853;
685
+ --accent-warning: #fbbc05;
686
+ --accent-danger: #ea4335;
687
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
688
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
689
+ --shadow-lg: 0 10px 25px rgba(0,0,0,0.15);
690
+ --radius-sm: 4px;
691
+ --radius-md: 8px;
692
+ --radius-lg: 12px;
693
+ --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
694
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
695
+ --transition-fast: 150ms ease;
696
+ --transition-normal: 250ms ease;
697
+ }
698
+
699
+ [data-theme="dark"] {
700
+ --bg-primary: #1a1a2e;
701
+ --bg-secondary: #16213e;
702
+ --bg-tertiary: #0f3460;
703
+ --text-primary: #eaeaea;
704
+ --text-secondary: #a0a0a0;
705
+ --text-muted: #666666;
706
+ --border-color: #2d2d4a;
707
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
708
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.3);
709
+ --shadow-lg: 0 10px 25px rgba(0,0,0,0.4);
710
+ }
711
+
712
+ html {
713
+ font-size: 16px;
714
+ scroll-behavior: smooth;
715
+ }
716
+
717
+ body {
718
+ font-family: var(--font-sans);
719
+ background: var(--bg-secondary);
720
+ color: var(--text-primary);
721
+ line-height: 1.6;
722
+ min-height: 100vh;
723
+ }
724
+
725
+ /* Container */
726
+ .container {
727
+ max-width: 1400px;
728
+ margin: 0 auto;
729
+ padding: 20px;
730
+ }
731
+
732
+ /* Header */
733
+ .header {
734
+ display: flex;
735
+ justify-content: space-between;
736
+ align-items: center;
737
+ padding: 24px 32px;
738
+ background: var(--bg-primary);
739
+ border-radius: var(--radius-lg);
740
+ box-shadow: var(--shadow-md);
741
+ margin-bottom: 20px;
742
+ }
743
+
744
+ .header h1 {
745
+ font-size: 1.75rem;
746
+ font-weight: 700;
747
+ color: var(--text-primary);
748
+ }
749
+
750
+ .subtitle {
751
+ color: var(--text-secondary);
752
+ font-size: 0.9rem;
753
+ margin-top: 4px;
754
+ }
755
+
756
+ .theme-toggle {
757
+ background: var(--bg-tertiary);
758
+ border: none;
759
+ border-radius: 50%;
760
+ width: 44px;
761
+ height: 44px;
762
+ cursor: pointer;
763
+ display: flex;
764
+ align-items: center;
765
+ justify-content: center;
766
+ transition: var(--transition-fast);
767
+ }
768
+
769
+ .theme-toggle:hover {
770
+ transform: scale(1.05);
771
+ box-shadow: var(--shadow-sm);
772
+ }
773
+
774
+ .theme-icon {
775
+ font-size: 1.25rem;
776
+ }
777
+
778
+ .dark-icon { display: none; }
779
+ [data-theme="dark"] .light-icon { display: none; }
780
+ [data-theme="dark"] .dark-icon { display: inline; }
781
+
782
+ /* Tabs */
783
+ .tabs {
784
+ display: flex;
785
+ gap: 8px;
786
+ padding: 8px;
787
+ background: var(--bg-primary);
788
+ border-radius: var(--radius-lg);
789
+ box-shadow: var(--shadow-sm);
790
+ margin-bottom: 20px;
791
+ }
792
+
793
+ .tab {
794
+ flex: 1;
795
+ display: flex;
796
+ align-items: center;
797
+ justify-content: center;
798
+ gap: 8px;
799
+ padding: 14px 20px;
800
+ background: transparent;
801
+ border: none;
802
+ border-radius: var(--radius-md);
803
+ color: var(--text-secondary);
804
+ font-size: 0.95rem;
805
+ font-weight: 500;
806
+ cursor: pointer;
807
+ transition: var(--transition-fast);
808
+ position: relative;
809
+ }
810
+
811
+ .tab:hover {
812
+ background: var(--bg-tertiary);
813
+ color: var(--text-primary);
814
+ }
815
+
816
+ .tab.active {
817
+ background: var(--accent-primary);
818
+ color: white;
819
+ }
820
+
821
+ .tab-icon {
822
+ font-size: 1.1rem;
823
+ }
824
+
825
+ .tab-key {
826
+ position: absolute;
827
+ top: 4px;
828
+ right: 8px;
829
+ font-size: 0.7rem;
830
+ opacity: 0.5;
831
+ font-family: var(--font-mono);
832
+ }
833
+
834
+ /* Content Panels */
835
+ .content {
836
+ background: var(--bg-primary);
837
+ border-radius: var(--radius-lg);
838
+ box-shadow: var(--shadow-md);
839
+ min-height: 500px;
840
+ }
841
+
842
+ .panel {
843
+ display: none;
844
+ padding: 32px;
845
+ animation: fadeIn 0.3s ease;
846
+ }
847
+
848
+ .panel.active {
849
+ display: block;
850
+ }
851
+
852
+ @keyframes fadeIn {
853
+ from { opacity: 0; transform: translateY(10px); }
854
+ to { opacity: 1; transform: translateY(0); }
855
+ }
856
+
857
+ /* Dashboard / Overview */
858
+ .dashboard {
859
+ display: flex;
860
+ flex-direction: column;
861
+ gap: 24px;
862
+ }
863
+
864
+ .stats-grid {
865
+ display: grid;
866
+ grid-template-columns: repeat(4, 1fr);
867
+ gap: 16px;
868
+ }
869
+
870
+ .stat-card {
871
+ background: var(--bg-secondary);
872
+ padding: 24px;
873
+ border-radius: var(--radius-md);
874
+ text-align: center;
875
+ transition: var(--transition-fast);
876
+ }
877
+
878
+ .stat-card:hover {
879
+ transform: translateY(-2px);
880
+ box-shadow: var(--shadow-sm);
881
+ }
882
+
883
+ .stat-value {
884
+ font-size: 2.5rem;
885
+ font-weight: 700;
886
+ color: var(--accent-primary);
887
+ line-height: 1.2;
888
+ }
889
+
890
+ .stat-value.date-value {
891
+ font-size: 1rem;
892
+ font-family: var(--font-mono);
893
+ }
894
+
895
+ .stat-label {
896
+ font-size: 0.9rem;
897
+ color: var(--text-secondary);
898
+ margin-top: 8px;
899
+ }
900
+
901
+ .charts-row {
902
+ display: grid;
903
+ grid-template-columns: repeat(2, 1fr);
904
+ gap: 24px;
905
+ }
906
+
907
+ .chart-card {
908
+ background: var(--bg-secondary);
909
+ padding: 24px;
910
+ border-radius: var(--radius-md);
911
+ }
912
+
913
+ .chart-card.wide {
914
+ grid-column: span 1;
915
+ }
916
+
917
+ .chart-card h3 {
918
+ font-size: 1.1rem;
919
+ font-weight: 600;
920
+ margin-bottom: 20px;
921
+ color: var(--text-primary);
922
+ }
923
+
924
+ /* Complexity Bars */
925
+ .complexity-bars {
926
+ display: flex;
927
+ flex-direction: column;
928
+ gap: 16px;
929
+ }
930
+
931
+ .complexity-row {
932
+ display: grid;
933
+ grid-template-columns: 100px 1fr 50px;
934
+ align-items: center;
935
+ gap: 12px;
936
+ }
937
+
938
+ .complexity-label {
939
+ font-size: 0.85rem;
940
+ font-weight: 500;
941
+ }
942
+
943
+ .complexity-label.simple { color: var(--accent-success); }
944
+ .complexity-label.intermediate { color: var(--accent-warning); }
945
+ .complexity-label.advanced { color: var(--accent-danger); }
946
+
947
+ .complexity-bar-bg {
948
+ height: 24px;
949
+ background: var(--bg-tertiary);
950
+ border-radius: var(--radius-sm);
951
+ overflow: hidden;
952
+ }
953
+
954
+ .complexity-bar {
955
+ height: 100%;
956
+ border-radius: var(--radius-sm);
957
+ transition: width 0.5s ease;
958
+ }
959
+
960
+ .complexity-bar.simple { background: var(--accent-success); }
961
+ .complexity-bar.intermediate { background: var(--accent-warning); }
962
+ .complexity-bar.advanced { background: var(--accent-danger); }
963
+
964
+ .complexity-count {
965
+ font-size: 0.9rem;
966
+ font-weight: 600;
967
+ text-align: right;
968
+ color: var(--text-secondary);
969
+ }
970
+
971
+ /* Pie Chart */
972
+ .pie-container {
973
+ display: flex;
974
+ align-items: center;
975
+ gap: 24px;
976
+ }
977
+
978
+ .pie-chart {
979
+ width: 160px;
980
+ height: 160px;
981
+ flex-shrink: 0;
982
+ }
983
+
984
+ .category-legend {
985
+ display: flex;
986
+ flex-direction: column;
987
+ gap: 8px;
988
+ flex: 1;
989
+ }
990
+
991
+ .legend-item {
992
+ display: flex;
993
+ align-items: center;
994
+ gap: 8px;
995
+ font-size: 0.85rem;
996
+ }
997
+
998
+ .legend-color {
999
+ width: 12px;
1000
+ height: 12px;
1001
+ border-radius: 2px;
1002
+ flex-shrink: 0;
1003
+ }
1004
+
1005
+ .legend-label {
1006
+ flex: 1;
1007
+ color: var(--text-primary);
1008
+ }
1009
+
1010
+ .legend-count {
1011
+ color: var(--text-secondary);
1012
+ font-weight: 500;
1013
+ }
1014
+
1015
+ /* Top Commands */
1016
+ .top-commands {
1017
+ display: flex;
1018
+ flex-direction: column;
1019
+ gap: 12px;
1020
+ }
1021
+
1022
+ .top-command-item {
1023
+ display: grid;
1024
+ grid-template-columns: 120px 1fr 50px;
1025
+ align-items: center;
1026
+ gap: 12px;
1027
+ }
1028
+
1029
+ .top-command-name code {
1030
+ font-size: 0.85rem;
1031
+ }
1032
+
1033
+ .top-command-bar-container {
1034
+ height: 20px;
1035
+ background: var(--bg-tertiary);
1036
+ border-radius: var(--radius-sm);
1037
+ overflow: hidden;
1038
+ }
1039
+
1040
+ .top-command-bar {
1041
+ height: 100%;
1042
+ background: linear-gradient(90deg, var(--accent-primary), #7baaf7);
1043
+ border-radius: var(--radius-sm);
1044
+ transition: width 0.5s ease;
1045
+ }
1046
+
1047
+ .top-command-count {
1048
+ font-size: 0.9rem;
1049
+ font-weight: 600;
1050
+ text-align: right;
1051
+ color: var(--text-secondary);
1052
+ }
1053
+
1054
+ /* New Commands */
1055
+ .new-commands {
1056
+ display: flex;
1057
+ flex-wrap: wrap;
1058
+ gap: 12px;
1059
+ }
1060
+
1061
+ .new-command-chip {
1062
+ display: flex;
1063
+ flex-direction: column;
1064
+ gap: 4px;
1065
+ padding: 12px 16px;
1066
+ background: var(--bg-tertiary);
1067
+ border-radius: var(--radius-md);
1068
+ border-left: 3px solid var(--accent-success);
1069
+ }
1070
+
1071
+ .new-command-chip code {
1072
+ font-size: 0.9rem;
1073
+ }
1074
+
1075
+ .first-seen {
1076
+ font-size: 0.75rem;
1077
+ color: var(--text-muted);
1078
+ }
1079
+
1080
+ /* Commands Tab */
1081
+ .commands-container {
1082
+ display: flex;
1083
+ flex-direction: column;
1084
+ gap: 20px;
1085
+ }
1086
+
1087
+ .commands-toolbar {
1088
+ display: flex;
1089
+ gap: 16px;
1090
+ align-items: center;
1091
+ }
1092
+
1093
+ .search-box {
1094
+ flex: 1;
1095
+ }
1096
+
1097
+ .search-box input {
1098
+ width: 100%;
1099
+ padding: 12px 16px;
1100
+ border: 2px solid var(--border-color);
1101
+ border-radius: var(--radius-md);
1102
+ font-size: 0.95rem;
1103
+ background: var(--bg-secondary);
1104
+ color: var(--text-primary);
1105
+ transition: var(--transition-fast);
1106
+ }
1107
+
1108
+ .search-box input:focus {
1109
+ outline: none;
1110
+ border-color: var(--accent-primary);
1111
+ }
1112
+
1113
+ .sort-controls {
1114
+ display: flex;
1115
+ align-items: center;
1116
+ gap: 8px;
1117
+ }
1118
+
1119
+ .sort-controls label {
1120
+ font-size: 0.9rem;
1121
+ color: var(--text-secondary);
1122
+ }
1123
+
1124
+ .sort-controls select {
1125
+ padding: 10px 14px;
1126
+ border: 2px solid var(--border-color);
1127
+ border-radius: var(--radius-md);
1128
+ background: var(--bg-secondary);
1129
+ color: var(--text-primary);
1130
+ font-size: 0.9rem;
1131
+ cursor: pointer;
1132
+ }
1133
+
1134
+ .filter-chips {
1135
+ display: flex;
1136
+ flex-wrap: wrap;
1137
+ gap: 8px;
1138
+ }
1139
+
1140
+ .filter-chip {
1141
+ padding: 8px 16px;
1142
+ border: 2px solid var(--border-color);
1143
+ border-radius: 20px;
1144
+ background: transparent;
1145
+ color: var(--text-secondary);
1146
+ font-size: 0.85rem;
1147
+ cursor: pointer;
1148
+ transition: var(--transition-fast);
1149
+ }
1150
+
1151
+ .filter-chip:hover {
1152
+ border-color: var(--accent-primary);
1153
+ color: var(--accent-primary);
1154
+ }
1155
+
1156
+ .filter-chip.active {
1157
+ background: var(--accent-primary);
1158
+ border-color: var(--accent-primary);
1159
+ color: white;
1160
+ }
1161
+
1162
+ .commands-list {
1163
+ display: flex;
1164
+ flex-direction: column;
1165
+ gap: 12px;
1166
+ }
1167
+
1168
+ .command-card {
1169
+ border: 1px solid var(--border-color);
1170
+ border-radius: var(--radius-md);
1171
+ overflow: hidden;
1172
+ transition: var(--transition-fast);
1173
+ }
1174
+
1175
+ .command-card:hover {
1176
+ box-shadow: var(--shadow-sm);
1177
+ }
1178
+
1179
+ .command-card.hidden {
1180
+ display: none;
1181
+ }
1182
+
1183
+ .command-header {
1184
+ display: flex;
1185
+ justify-content: space-between;
1186
+ align-items: center;
1187
+ padding: 16px 20px;
1188
+ background: var(--bg-secondary);
1189
+ cursor: pointer;
1190
+ transition: var(--transition-fast);
1191
+ }
1192
+
1193
+ .command-header:hover {
1194
+ background: var(--bg-tertiary);
1195
+ }
1196
+
1197
+ .command-main {
1198
+ display: flex;
1199
+ align-items: center;
1200
+ gap: 12px;
1201
+ }
1202
+
1203
+ .command-meta {
1204
+ display: flex;
1205
+ align-items: center;
1206
+ gap: 16px;
1207
+ }
1208
+
1209
+ .frequency {
1210
+ font-size: 0.85rem;
1211
+ color: var(--text-secondary);
1212
+ }
1213
+
1214
+ .expand-icon {
1215
+ color: var(--text-muted);
1216
+ transition: transform var(--transition-fast);
1217
+ }
1218
+
1219
+ .command-card.expanded .expand-icon {
1220
+ transform: rotate(180deg);
1221
+ }
1222
+
1223
+ .command-details {
1224
+ display: none;
1225
+ padding: 20px;
1226
+ border-top: 1px solid var(--border-color);
1227
+ background: var(--bg-primary);
1228
+ }
1229
+
1230
+ .command-details.show {
1231
+ display: block;
1232
+ }
1233
+
1234
+ .command-details h5 {
1235
+ font-size: 0.85rem;
1236
+ font-weight: 600;
1237
+ color: var(--text-secondary);
1238
+ margin-bottom: 8px;
1239
+ text-transform: uppercase;
1240
+ letter-spacing: 0.5px;
1241
+ }
1242
+
1243
+ .command-details > div {
1244
+ margin-bottom: 20px;
1245
+ }
1246
+
1247
+ .command-details > div:last-child {
1248
+ margin-bottom: 0;
1249
+ }
1250
+
1251
+ .flags-list {
1252
+ list-style: none;
1253
+ display: flex;
1254
+ flex-direction: column;
1255
+ gap: 8px;
1256
+ }
1257
+
1258
+ .flags-list li {
1259
+ display: flex;
1260
+ align-items: baseline;
1261
+ gap: 8px;
1262
+ font-size: 0.9rem;
1263
+ }
1264
+
1265
+ .output-preview {
1266
+ background: var(--bg-secondary);
1267
+ padding: 16px;
1268
+ border-radius: var(--radius-md);
1269
+ font-family: var(--font-mono);
1270
+ font-size: 0.85rem;
1271
+ overflow-x: auto;
1272
+ white-space: pre-wrap;
1273
+ }
1274
+
1275
+ /* Syntax Highlighting */
1276
+ code, pre {
1277
+ font-family: var(--font-mono);
1278
+ }
1279
+
1280
+ code.cmd, .syntax-highlighted .cmd {
1281
+ color: #4285f4;
1282
+ font-weight: 600;
1283
+ }
1284
+
1285
+ code.flag, .syntax-highlighted .flag {
1286
+ color: #34a853;
1287
+ }
1288
+
1289
+ .syntax-highlighted .string {
1290
+ color: #ff6d01;
1291
+ }
1292
+
1293
+ .syntax-highlighted .operator {
1294
+ color: #9c27b0;
1295
+ font-weight: 600;
1296
+ }
1297
+
1298
+ .syntax-highlighted .path {
1299
+ color: #6c757d;
1300
+ }
1301
+
1302
+ .syntax-highlighted .variable {
1303
+ color: #ea4335;
1304
+ }
1305
+
1306
+ .syntax-highlighted {
1307
+ background: var(--bg-secondary);
1308
+ padding: 12px 16px;
1309
+ border-radius: var(--radius-md);
1310
+ font-size: 0.9rem;
1311
+ overflow-x: auto;
1312
+ white-space: pre-wrap;
1313
+ }
1314
+
1315
+ /* Badges */
1316
+ .complexity-badge {
1317
+ padding: 4px 10px;
1318
+ border-radius: 12px;
1319
+ font-size: 0.75rem;
1320
+ font-weight: 600;
1321
+ text-transform: uppercase;
1322
+ }
1323
+
1324
+ .complexity-badge.simple {
1325
+ background: rgba(52, 168, 83, 0.15);
1326
+ color: var(--accent-success);
1327
+ }
1328
+
1329
+ .complexity-badge.intermediate {
1330
+ background: rgba(251, 188, 5, 0.15);
1331
+ color: #d49a00;
1332
+ }
1333
+
1334
+ .complexity-badge.advanced {
1335
+ background: rgba(234, 67, 53, 0.15);
1336
+ color: var(--accent-danger);
1337
+ }
1338
+
1339
+ .category-badge {
1340
+ padding: 4px 10px;
1341
+ border-radius: 12px;
1342
+ font-size: 0.75rem;
1343
+ background: var(--bg-tertiary);
1344
+ color: var(--text-secondary);
1345
+ }
1346
+
1347
+ /* Lessons Tab */
1348
+ .lessons-container {
1349
+ display: flex;
1350
+ flex-direction: column;
1351
+ gap: 32px;
1352
+ }
1353
+
1354
+ .lesson-section {
1355
+ border: 1px solid var(--border-color);
1356
+ border-radius: var(--radius-lg);
1357
+ overflow: hidden;
1358
+ }
1359
+
1360
+ .lesson-title {
1361
+ display: flex;
1362
+ align-items: center;
1363
+ gap: 12px;
1364
+ padding: 20px 24px;
1365
+ background: var(--bg-secondary);
1366
+ font-size: 1.25rem;
1367
+ font-weight: 600;
1368
+ border-bottom: 1px solid var(--border-color);
1369
+ }
1370
+
1371
+ .lesson-icon {
1372
+ font-size: 1.5rem;
1373
+ }
1374
+
1375
+ .lesson-count {
1376
+ font-size: 0.9rem;
1377
+ font-weight: 400;
1378
+ color: var(--text-secondary);
1379
+ margin-left: auto;
1380
+ }
1381
+
1382
+ .lesson-content {
1383
+ padding: 24px;
1384
+ }
1385
+
1386
+ .concept-overview {
1387
+ margin-bottom: 24px;
1388
+ padding: 16px;
1389
+ background: var(--bg-secondary);
1390
+ border-radius: var(--radius-md);
1391
+ border-left: 4px solid var(--accent-primary);
1392
+ }
1393
+
1394
+ .concept-overview h4 {
1395
+ font-size: 0.9rem;
1396
+ font-weight: 600;
1397
+ margin-bottom: 8px;
1398
+ color: var(--accent-primary);
1399
+ }
1400
+
1401
+ .concept-overview p {
1402
+ color: var(--text-secondary);
1403
+ line-height: 1.7;
1404
+ }
1405
+
1406
+ .lesson-commands h4 {
1407
+ font-size: 1rem;
1408
+ font-weight: 600;
1409
+ margin-bottom: 16px;
1410
+ }
1411
+
1412
+ .lesson-command {
1413
+ padding: 16px;
1414
+ background: var(--bg-secondary);
1415
+ border-radius: var(--radius-md);
1416
+ margin-bottom: 12px;
1417
+ }
1418
+
1419
+ .lesson-command:last-child {
1420
+ margin-bottom: 0;
1421
+ }
1422
+
1423
+ .lesson-command-header {
1424
+ display: flex;
1425
+ align-items: center;
1426
+ gap: 12px;
1427
+ margin-bottom: 12px;
1428
+ }
1429
+
1430
+ .lesson-description {
1431
+ margin-top: 12px;
1432
+ color: var(--text-secondary);
1433
+ font-size: 0.9rem;
1434
+ }
1435
+
1436
+ .patterns {
1437
+ margin-top: 24px;
1438
+ padding-top: 20px;
1439
+ border-top: 1px solid var(--border-color);
1440
+ }
1441
+
1442
+ .patterns h4 {
1443
+ font-size: 0.95rem;
1444
+ font-weight: 600;
1445
+ margin-bottom: 12px;
1446
+ }
1447
+
1448
+ .patterns ul {
1449
+ list-style: none;
1450
+ display: flex;
1451
+ flex-direction: column;
1452
+ gap: 8px;
1453
+ }
1454
+
1455
+ .patterns li {
1456
+ display: flex;
1457
+ align-items: center;
1458
+ gap: 8px;
1459
+ font-size: 0.9rem;
1460
+ color: var(--text-secondary);
1461
+ }
1462
+
1463
+ .patterns li::before {
1464
+ content: "\\2022";
1465
+ color: var(--accent-primary);
1466
+ font-weight: bold;
1467
+ }
1468
+
1469
+ /* Quiz Tab */
1470
+ .quiz-container {
1471
+ max-width: 800px;
1472
+ margin: 0 auto;
1473
+ }
1474
+
1475
+ .quiz-header {
1476
+ text-align: center;
1477
+ margin-bottom: 32px;
1478
+ }
1479
+
1480
+ .quiz-header h2 {
1481
+ font-size: 1.5rem;
1482
+ margin-bottom: 8px;
1483
+ }
1484
+
1485
+ .quiz-header p {
1486
+ color: var(--text-secondary);
1487
+ margin-bottom: 20px;
1488
+ }
1489
+
1490
+ .quiz-score {
1491
+ display: inline-flex;
1492
+ align-items: center;
1493
+ gap: 4px;
1494
+ padding: 12px 24px;
1495
+ background: var(--bg-secondary);
1496
+ border-radius: var(--radius-md);
1497
+ font-size: 1.1rem;
1498
+ font-weight: 600;
1499
+ }
1500
+
1501
+ #score-current {
1502
+ color: var(--accent-primary);
1503
+ }
1504
+
1505
+ .quiz-questions {
1506
+ display: flex;
1507
+ flex-direction: column;
1508
+ gap: 24px;
1509
+ }
1510
+
1511
+ .quiz-question {
1512
+ background: var(--bg-secondary);
1513
+ border-radius: var(--radius-md);
1514
+ padding: 24px;
1515
+ }
1516
+
1517
+ .question-number {
1518
+ font-size: 0.8rem;
1519
+ font-weight: 600;
1520
+ text-transform: uppercase;
1521
+ color: var(--accent-primary);
1522
+ margin-bottom: 8px;
1523
+ }
1524
+
1525
+ .question-text {
1526
+ font-size: 1.1rem;
1527
+ font-weight: 500;
1528
+ margin-bottom: 20px;
1529
+ line-height: 1.5;
1530
+ }
1531
+
1532
+ .quiz-options {
1533
+ display: flex;
1534
+ flex-direction: column;
1535
+ gap: 12px;
1536
+ }
1537
+
1538
+ .quiz-option {
1539
+ display: flex;
1540
+ align-items: center;
1541
+ gap: 12px;
1542
+ padding: 14px 18px;
1543
+ background: var(--bg-primary);
1544
+ border: 2px solid var(--border-color);
1545
+ border-radius: var(--radius-md);
1546
+ cursor: pointer;
1547
+ transition: var(--transition-fast);
1548
+ }
1549
+
1550
+ .quiz-option:hover {
1551
+ border-color: var(--accent-primary);
1552
+ }
1553
+
1554
+ .quiz-option input {
1555
+ display: none;
1556
+ }
1557
+
1558
+ .option-letter {
1559
+ width: 28px;
1560
+ height: 28px;
1561
+ display: flex;
1562
+ align-items: center;
1563
+ justify-content: center;
1564
+ background: var(--bg-tertiary);
1565
+ border-radius: 50%;
1566
+ font-size: 0.85rem;
1567
+ font-weight: 600;
1568
+ flex-shrink: 0;
1569
+ }
1570
+
1571
+ .option-text {
1572
+ flex: 1;
1573
+ }
1574
+
1575
+ .quiz-option.correct {
1576
+ border-color: var(--accent-success);
1577
+ background: rgba(52, 168, 83, 0.1);
1578
+ }
1579
+
1580
+ .quiz-option.correct .option-letter {
1581
+ background: var(--accent-success);
1582
+ color: white;
1583
+ }
1584
+
1585
+ .quiz-option.incorrect {
1586
+ border-color: var(--accent-danger);
1587
+ background: rgba(234, 67, 53, 0.1);
1588
+ }
1589
+
1590
+ .quiz-option.incorrect .option-letter {
1591
+ background: var(--accent-danger);
1592
+ color: white;
1593
+ }
1594
+
1595
+ .quiz-option.disabled {
1596
+ pointer-events: none;
1597
+ opacity: 0.7;
1598
+ }
1599
+
1600
+ .quiz-feedback {
1601
+ display: none;
1602
+ margin-top: 16px;
1603
+ padding: 16px;
1604
+ border-radius: var(--radius-md);
1605
+ }
1606
+
1607
+ .quiz-feedback.show {
1608
+ display: block;
1609
+ }
1610
+
1611
+ .quiz-feedback.correct {
1612
+ background: rgba(52, 168, 83, 0.1);
1613
+ border-left: 4px solid var(--accent-success);
1614
+ }
1615
+
1616
+ .quiz-feedback.incorrect {
1617
+ background: rgba(234, 67, 53, 0.1);
1618
+ border-left: 4px solid var(--accent-danger);
1619
+ }
1620
+
1621
+ .feedback-result {
1622
+ font-weight: 600;
1623
+ margin-bottom: 8px;
1624
+ }
1625
+
1626
+ .feedback-explanation {
1627
+ color: var(--text-secondary);
1628
+ font-size: 0.9rem;
1629
+ line-height: 1.6;
1630
+ }
1631
+
1632
+ .quiz-actions {
1633
+ display: flex;
1634
+ justify-content: center;
1635
+ margin-top: 32px;
1636
+ }
1637
+
1638
+ .btn {
1639
+ padding: 14px 28px;
1640
+ border: none;
1641
+ border-radius: var(--radius-md);
1642
+ font-size: 1rem;
1643
+ font-weight: 600;
1644
+ cursor: pointer;
1645
+ transition: var(--transition-fast);
1646
+ }
1647
+
1648
+ .btn-secondary {
1649
+ background: var(--bg-tertiary);
1650
+ color: var(--text-primary);
1651
+ }
1652
+
1653
+ .btn-secondary:hover {
1654
+ background: var(--border-color);
1655
+ }
1656
+
1657
+ /* Footer */
1658
+ .footer {
1659
+ text-align: center;
1660
+ padding: 20px;
1661
+ margin-top: 20px;
1662
+ color: var(--text-muted);
1663
+ font-size: 0.85rem;
1664
+ }
1665
+
1666
+ /* Empty State */
1667
+ .empty-state {
1668
+ text-align: center;
1669
+ padding: 48px 24px;
1670
+ color: var(--text-secondary);
1671
+ }
1672
+
1673
+ /* Print Styles */
1674
+ @media print {
1675
+ body {
1676
+ background: white;
1677
+ color: black;
1678
+ }
1679
+
1680
+ .header, .tabs, .footer, .theme-toggle, .quiz-actions {
1681
+ display: none;
1682
+ }
1683
+
1684
+ .content {
1685
+ box-shadow: none;
1686
+ }
1687
+
1688
+ .panel {
1689
+ display: block !important;
1690
+ page-break-inside: avoid;
1691
+ }
1692
+
1693
+ .panel::before {
1694
+ content: attr(aria-labelledby);
1695
+ display: block;
1696
+ font-size: 1.5rem;
1697
+ font-weight: bold;
1698
+ margin-bottom: 1rem;
1699
+ padding-bottom: 0.5rem;
1700
+ border-bottom: 2px solid #333;
1701
+ }
1702
+
1703
+ .command-details {
1704
+ display: block !important;
1705
+ }
1706
+ }
1707
+
1708
+ /* Responsive */
1709
+ @media (max-width: 1024px) {
1710
+ .stats-grid {
1711
+ grid-template-columns: repeat(2, 1fr);
1712
+ }
1713
+
1714
+ .charts-row {
1715
+ grid-template-columns: 1fr;
1716
+ }
1717
+
1718
+ .pie-container {
1719
+ flex-direction: column;
1720
+ }
1721
+ }
1722
+
1723
+ @media (max-width: 768px) {
1724
+ .container {
1725
+ padding: 12px;
1726
+ }
1727
+
1728
+ .header {
1729
+ padding: 16px 20px;
1730
+ }
1731
+
1732
+ .header h1 {
1733
+ font-size: 1.25rem;
1734
+ }
1735
+
1736
+ .tab-label {
1737
+ display: none;
1738
+ }
1739
+
1740
+ .tab {
1741
+ padding: 12px;
1742
+ }
1743
+
1744
+ .tab-icon {
1745
+ font-size: 1.25rem;
1746
+ }
1747
+
1748
+ .panel {
1749
+ padding: 20px;
1750
+ }
1751
+
1752
+ .stats-grid {
1753
+ grid-template-columns: repeat(2, 1fr);
1754
+ gap: 12px;
1755
+ }
1756
+
1757
+ .stat-value {
1758
+ font-size: 1.75rem;
1759
+ }
1760
+
1761
+ .commands-toolbar {
1762
+ flex-direction: column;
1763
+ }
1764
+
1765
+ .command-header {
1766
+ flex-direction: column;
1767
+ align-items: flex-start;
1768
+ gap: 12px;
1769
+ }
1770
+
1771
+ .command-meta {
1772
+ width: 100%;
1773
+ justify-content: space-between;
1774
+ }
1775
+ }
1776
+ '''
1777
+
1778
+
1779
+ def get_inline_js(quizzes: list[dict]) -> str:
1780
+ """Return all JavaScript code."""
1781
+ quiz_data = json.dumps(quizzes)
1782
+
1783
+ return f'''
1784
+ // Quiz data
1785
+ const quizData = {quiz_data};
1786
+ let score = 0;
1787
+ let answeredQuestions = new Set();
1788
+
1789
+ // Tab Navigation
1790
+ document.querySelectorAll('.tab').forEach(tab => {{
1791
+ tab.addEventListener('click', () => {{
1792
+ switchTab(tab.dataset.tab);
1793
+ }});
1794
+ }});
1795
+
1796
+ function switchTab(tabName) {{
1797
+ // Update tabs
1798
+ document.querySelectorAll('.tab').forEach(t => {{
1799
+ t.classList.remove('active');
1800
+ t.setAttribute('aria-selected', 'false');
1801
+ }});
1802
+ document.querySelector(`[data-tab="${{tabName}}"]`).classList.add('active');
1803
+ document.querySelector(`[data-tab="${{tabName}}"]`).setAttribute('aria-selected', 'true');
1804
+
1805
+ // Update panels
1806
+ document.querySelectorAll('.panel').forEach(p => {{
1807
+ p.classList.remove('active');
1808
+ }});
1809
+ document.getElementById(`panel-${{tabName}}`).classList.add('active');
1810
+ }}
1811
+
1812
+ // Keyboard navigation
1813
+ document.addEventListener('keydown', (e) => {{
1814
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
1815
+
1816
+ const tabs = ['overview', 'commands', 'lessons', 'quiz'];
1817
+ const key = e.key;
1818
+
1819
+ if (key >= '1' && key <= '4') {{
1820
+ e.preventDefault();
1821
+ switchTab(tabs[parseInt(key) - 1]);
1822
+ }}
1823
+ }});
1824
+
1825
+ // Theme Toggle
1826
+ function toggleTheme() {{
1827
+ const html = document.documentElement;
1828
+ const currentTheme = html.getAttribute('data-theme');
1829
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
1830
+ html.setAttribute('data-theme', newTheme);
1831
+ localStorage.setItem('theme', newTheme);
1832
+ }}
1833
+
1834
+ // Load saved theme
1835
+ (function() {{
1836
+ const savedTheme = localStorage.getItem('theme') || 'light';
1837
+ document.documentElement.setAttribute('data-theme', savedTheme);
1838
+ }})();
1839
+
1840
+ // Command expansion
1841
+ function toggleCommand(cmdId) {{
1842
+ const details = document.getElementById(cmdId);
1843
+ const card = details.closest('.command-card');
1844
+
1845
+ details.classList.toggle('show');
1846
+ card.classList.toggle('expanded');
1847
+ }}
1848
+
1849
+ // Command filtering
1850
+ function filterCommands() {{
1851
+ const searchTerm = document.getElementById('command-search').value.toLowerCase();
1852
+ const activeCategory = document.querySelector('.filter-chip.active').dataset.category;
1853
+
1854
+ document.querySelectorAll('.command-card').forEach(card => {{
1855
+ const name = card.dataset.name.toLowerCase();
1856
+ const category = card.dataset.category;
1857
+
1858
+ const matchesSearch = name.includes(searchTerm);
1859
+ const matchesCategory = activeCategory === 'all' || category === activeCategory;
1860
+
1861
+ card.classList.toggle('hidden', !(matchesSearch && matchesCategory));
1862
+ }});
1863
+ }}
1864
+
1865
+ // Category filter chips
1866
+ document.querySelectorAll('.filter-chip').forEach(chip => {{
1867
+ chip.addEventListener('click', () => {{
1868
+ document.querySelectorAll('.filter-chip').forEach(c => c.classList.remove('active'));
1869
+ chip.classList.add('active');
1870
+ filterCommands();
1871
+ }});
1872
+ }});
1873
+
1874
+ // Command sorting
1875
+ function sortCommands() {{
1876
+ const sortBy = document.getElementById('sort-select').value;
1877
+ const list = document.getElementById('commands-list');
1878
+ const cards = Array.from(list.querySelectorAll('.command-card'));
1879
+
1880
+ cards.sort((a, b) => {{
1881
+ switch(sortBy) {{
1882
+ case 'frequency':
1883
+ return parseInt(b.dataset.frequency) - parseInt(a.dataset.frequency);
1884
+ case 'complexity':
1885
+ const order = {{'simple': 1, 'intermediate': 2, 'advanced': 3}};
1886
+ return order[a.dataset.complexity] - order[b.dataset.complexity];
1887
+ case 'category':
1888
+ return a.dataset.category.localeCompare(b.dataset.category);
1889
+ case 'name':
1890
+ return a.dataset.name.localeCompare(b.dataset.name);
1891
+ default:
1892
+ return 0;
1893
+ }}
1894
+ }});
1895
+
1896
+ cards.forEach(card => list.appendChild(card));
1897
+ }}
1898
+
1899
+ // Quiz functions
1900
+ function checkAnswer(questionId, selectedIndex, correctIndex) {{
1901
+ if (answeredQuestions.has(questionId)) return;
1902
+ answeredQuestions.add(questionId);
1903
+
1904
+ const question = document.getElementById(`question-${{questionId}}`);
1905
+ const options = question.querySelectorAll('.quiz-option');
1906
+ const feedback = document.getElementById(`feedback-${{questionId}}`);
1907
+
1908
+ const isCorrect = selectedIndex === correctIndex;
1909
+
1910
+ // Mark options
1911
+ options.forEach((opt, idx) => {{
1912
+ opt.classList.add('disabled');
1913
+ if (idx === correctIndex) {{
1914
+ opt.classList.add('correct');
1915
+ }} else if (idx === selectedIndex && !isCorrect) {{
1916
+ opt.classList.add('incorrect');
1917
+ }}
1918
+ }});
1919
+
1920
+ // Show feedback
1921
+ feedback.classList.add('show');
1922
+ feedback.classList.add(isCorrect ? 'correct' : 'incorrect');
1923
+ feedback.querySelector('.feedback-result').textContent = isCorrect ? 'Correct!' : 'Incorrect';
1924
+
1925
+ // Update score
1926
+ if (isCorrect) {{
1927
+ score++;
1928
+ document.getElementById('score-current').textContent = score;
1929
+ }}
1930
+ }}
1931
+
1932
+ function resetQuiz() {{
1933
+ score = 0;
1934
+ answeredQuestions.clear();
1935
+ document.getElementById('score-current').textContent = '0';
1936
+
1937
+ document.querySelectorAll('.quiz-question').forEach(q => {{
1938
+ q.querySelectorAll('.quiz-option').forEach(opt => {{
1939
+ opt.classList.remove('correct', 'incorrect', 'disabled');
1940
+ opt.querySelector('input').checked = false;
1941
+ }});
1942
+
1943
+ const feedback = q.querySelector('.quiz-feedback');
1944
+ feedback.classList.remove('show', 'correct', 'incorrect');
1945
+ }});
1946
+ }}
1947
+
1948
+ // Smooth scrolling for internal links
1949
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {{
1950
+ anchor.addEventListener('click', function(e) {{
1951
+ e.preventDefault();
1952
+ document.querySelector(this.getAttribute('href')).scrollIntoView({{
1953
+ behavior: 'smooth'
1954
+ }});
1955
+ }});
1956
+ }});
1957
+ '''
1958
+
1959
+
1960
+ if __name__ == "__main__":
1961
+ # Test with sample data
1962
+ sample_analysis = {
1963
+ "stats": {
1964
+ "total_commands": 150,
1965
+ "unique_commands": 45,
1966
+ "unique_utilities": 28,
1967
+ "date_range": {"start": "2025-01-01", "end": "2025-02-05"},
1968
+ "complexity_distribution": {"simple": 80, "intermediate": 50, "advanced": 20}
1969
+ },
1970
+ "commands": [
1971
+ {
1972
+ "base_command": "ls",
1973
+ "full_command": "ls -la /home/user",
1974
+ "category": "File Management",
1975
+ "complexity": "simple",
1976
+ "frequency": 25,
1977
+ "description": "List directory contents with details",
1978
+ "flags": [{"flag": "-l", "description": "Long format"}, {"flag": "-a", "description": "Show hidden files"}],
1979
+ "is_new": False
1980
+ },
1981
+ {
1982
+ "base_command": "grep",
1983
+ "full_command": "grep -r 'pattern' ./src",
1984
+ "category": "Text Processing",
1985
+ "complexity": "intermediate",
1986
+ "frequency": 18,
1987
+ "description": "Search for patterns in files",
1988
+ "flags": [{"flag": "-r", "description": "Recursive search"}],
1989
+ "is_new": True,
1990
+ "first_seen": "2025-01-15"
1991
+ },
1992
+ {
1993
+ "base_command": "find",
1994
+ "full_command": "find . -name '*.py' -exec grep 'import' {} +",
1995
+ "category": "Search",
1996
+ "complexity": "advanced",
1997
+ "frequency": 8,
1998
+ "description": "Find files and execute commands on them",
1999
+ "flags": [{"flag": "-name", "description": "Match filename pattern"}, {"flag": "-exec", "description": "Execute command on results"}],
2000
+ "is_new": True,
2001
+ "first_seen": "2025-01-20"
2002
+ }
2003
+ ],
2004
+ "categories": {
2005
+ "File Management": ["ls", "cd", "mkdir", "cp", "mv"],
2006
+ "Text Processing": ["grep", "sed", "awk", "cat"],
2007
+ "Search": ["find", "locate"],
2008
+ "Network": ["curl", "wget"]
2009
+ }
2010
+ }
2011
+
2012
+ sample_quizzes = [
2013
+ {
2014
+ "question": "What does the -l flag do in the 'ls' command?",
2015
+ "options": ["List only files", "Long format with details", "List hidden files", "List in reverse order"],
2016
+ "correct_answer": 1,
2017
+ "explanation": "The -l flag displays files in long format, showing permissions, owner, size, and modification date."
2018
+ },
2019
+ {
2020
+ "question": "Which command is used to search for text patterns in files?",
2021
+ "options": ["find", "grep", "locate", "which"],
2022
+ "correct_answer": 1,
2023
+ "explanation": "grep (Global Regular Expression Print) searches for text patterns in files using regular expressions."
2024
+ }
2025
+ ]
2026
+
2027
+ html_output = generate_html(sample_analysis, sample_quizzes)
2028
+ print(f"Generated HTML length: {len(html_output)} characters")
2029
+ print("HTML generation complete!")