learn_bash_from_session_data 1.0.6 → 1.0.7
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.
- package/bin/learn-bash.js +17 -14
- package/package.json +1 -1
- package/scripts/html_generator.py +124 -11
- package/scripts/quiz_generator.py +114 -40
- package/bash-learner-output/run-2026-02-05-154214/index.html +0 -3848
- package/bash-learner-output/run-2026-02-05-154214/summary.json +0 -148
- package/bash-learner-output/run-2026-02-05-155427/index.html +0 -3900
- package/bash-learner-output/run-2026-02-05-155427/summary.json +0 -157
- package/bash-learner-output/run-2026-02-05-155949/index.html +0 -4514
- package/bash-learner-output/run-2026-02-05-155949/summary.json +0 -163
- package/vectors.db +0 -0
package/bin/learn-bash.js
CHANGED
|
@@ -240,26 +240,29 @@ function checkPython() {
|
|
|
240
240
|
*/
|
|
241
241
|
function openInBrowser(filePath) {
|
|
242
242
|
const platform = os.platform();
|
|
243
|
+
const isWindows = platform === 'win32' ||
|
|
244
|
+
process.env.MSYSTEM != null ||
|
|
245
|
+
(process.env.OSTYPE && process.env.OSTYPE.includes('msys')) ||
|
|
246
|
+
isWSL();
|
|
243
247
|
let cmd;
|
|
244
248
|
let args;
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
// Linux and others
|
|
257
|
-
cmd = 'xdg-open';
|
|
258
|
-
args = [filePath];
|
|
250
|
+
if (platform === 'darwin') {
|
|
251
|
+
cmd = 'open';
|
|
252
|
+
args = [filePath];
|
|
253
|
+
} else if (isWindows) {
|
|
254
|
+
// Works from native Windows, MSYS, Git Bash, and WSL
|
|
255
|
+
cmd = process.env.COMSPEC || 'cmd.exe';
|
|
256
|
+
args = ['/c', 'start', '', filePath.replace(/\//g, '\\')];
|
|
257
|
+
} else {
|
|
258
|
+
cmd = 'xdg-open';
|
|
259
|
+
args = [filePath];
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
try {
|
|
262
|
-
spawn(cmd, args, { detached: true, stdio: 'ignore' })
|
|
263
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
264
|
+
child.on('error', () => {}); // Suppress async spawn errors
|
|
265
|
+
child.unref();
|
|
263
266
|
return true;
|
|
264
267
|
} catch (e) {
|
|
265
268
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "learn_bash_from_session_data",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Learn bash from your Claude Code sessions - extracts commands and generates interactive HTML lessons with 400+ commands, quizzes, and comprehensive coverage",
|
|
5
5
|
"main": "bin/learn-bash.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,16 @@ from pathlib import Path
|
|
|
12
12
|
import html
|
|
13
13
|
import json
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
from scripts.knowledge_base import COMMAND_DB, get_flags_for_command, get_command_info
|
|
17
|
+
except ImportError:
|
|
18
|
+
try:
|
|
19
|
+
from knowledge_base import COMMAND_DB, get_flags_for_command, get_command_info
|
|
20
|
+
except ImportError:
|
|
21
|
+
COMMAND_DB = {}
|
|
22
|
+
def get_flags_for_command(cmd): return {}
|
|
23
|
+
def get_command_info(cmd): return None
|
|
24
|
+
|
|
15
25
|
|
|
16
26
|
def _generate_html_impl(analysis_result: dict[str, Any], quizzes: list[dict[str, Any]]) -> str:
|
|
17
27
|
"""
|
|
@@ -376,7 +386,7 @@ def render_commands_tab(commands: list[dict]) -> str:
|
|
|
376
386
|
# Syntax highlighted command
|
|
377
387
|
highlighted = _syntax_highlight(cmd.get("full_command", ""))
|
|
378
388
|
|
|
379
|
-
# Flags breakdown
|
|
389
|
+
# Flags breakdown with descriptions
|
|
380
390
|
flags = cmd.get("flags", [])
|
|
381
391
|
flags_html = ""
|
|
382
392
|
if flags:
|
|
@@ -384,9 +394,27 @@ def render_commands_tab(commands: list[dict]) -> str:
|
|
|
384
394
|
for flag in flags:
|
|
385
395
|
flag_name = html.escape(flag.get("flag", ""))
|
|
386
396
|
flag_desc = html.escape(flag.get("description", ""))
|
|
387
|
-
|
|
397
|
+
if flag_desc:
|
|
398
|
+
flags_html += f'<li><code class="flag">{flag_name}</code> <span class="flag-desc">{flag_desc}</span></li>'
|
|
399
|
+
else:
|
|
400
|
+
flags_html += f'<li><code class="flag">{flag_name}</code></li>'
|
|
388
401
|
flags_html += '</ul></div>'
|
|
389
402
|
|
|
403
|
+
# Subcommand description
|
|
404
|
+
subcommand_desc = cmd.get("subcommand_desc", "")
|
|
405
|
+
subcmd_html = ""
|
|
406
|
+
if subcommand_desc:
|
|
407
|
+
subcmd_html = f'<div class="subcmd-section"><span class="subcmd-label">Subcommand:</span> {html.escape(subcommand_desc)}</div>'
|
|
408
|
+
|
|
409
|
+
# Common patterns / examples from knowledge base
|
|
410
|
+
common_patterns = cmd.get("common_patterns", [])
|
|
411
|
+
patterns_html = ""
|
|
412
|
+
if common_patterns:
|
|
413
|
+
patterns_html = '<div class="patterns-section"><h5>Common Patterns:</h5><ul class="patterns-list">'
|
|
414
|
+
for pattern in common_patterns[:5]:
|
|
415
|
+
patterns_html += f'<li><code>{html.escape(pattern)}</code></li>'
|
|
416
|
+
patterns_html += '</ul></div>'
|
|
417
|
+
|
|
390
418
|
# Output preview
|
|
391
419
|
output_preview = cmd.get("output_preview", "")
|
|
392
420
|
output_html = ""
|
|
@@ -418,7 +446,9 @@ def render_commands_tab(commands: list[dict]) -> str:
|
|
|
418
446
|
<h5>Description:</h5>
|
|
419
447
|
<p>{description}</p>
|
|
420
448
|
</div>
|
|
449
|
+
{subcmd_html}
|
|
421
450
|
{flags_html}
|
|
451
|
+
{patterns_html}
|
|
422
452
|
{output_html}
|
|
423
453
|
</div>
|
|
424
454
|
</div>'''
|
|
@@ -512,13 +542,37 @@ def render_lessons_tab(categories: dict, commands: list[dict]) -> str:
|
|
|
512
542
|
complexity = cmd.get("complexity", "simple")
|
|
513
543
|
highlighted = _syntax_highlight(cmd.get("full_command", ""))
|
|
514
544
|
|
|
545
|
+
# Get flags and patterns for this command from COMMAND_DB
|
|
546
|
+
cmd_flags = cmd.get("flags", [])
|
|
547
|
+
lesson_flags_html = ""
|
|
548
|
+
if cmd_flags:
|
|
549
|
+
lesson_flags_html = '<div class="lesson-flags"><strong>Flags used:</strong> '
|
|
550
|
+
flag_parts = []
|
|
551
|
+
for flag in cmd_flags:
|
|
552
|
+
fname = html.escape(flag.get("flag", "") if isinstance(flag, dict) else str(flag))
|
|
553
|
+
fdesc = html.escape(flag.get("description", "") if isinstance(flag, dict) else "")
|
|
554
|
+
if fdesc:
|
|
555
|
+
flag_parts.append(f'<code class="flag">{fname}</code> ({fdesc})')
|
|
556
|
+
else:
|
|
557
|
+
flag_parts.append(f'<code class="flag">{fname}</code>')
|
|
558
|
+
lesson_flags_html += ', '.join(flag_parts) + '</div>'
|
|
559
|
+
|
|
560
|
+
# Subcommand info
|
|
561
|
+
subcmd_desc = cmd.get("subcommand_desc", "")
|
|
562
|
+
lesson_subcmd = ""
|
|
563
|
+
if subcmd_desc:
|
|
564
|
+
lesson_subcmd = f'<div class="lesson-subcmd"><em>{html.escape(subcmd_desc)}</em></div>'
|
|
565
|
+
|
|
515
566
|
cat_commands_html += f'''
|
|
516
567
|
<div class="lesson-command">
|
|
517
568
|
<div class="lesson-command-header">
|
|
518
569
|
<code class="cmd">{base_cmd}</code>
|
|
570
|
+
<span class="lesson-complexity complexity-{complexity}">{complexity}</span>
|
|
519
571
|
</div>
|
|
520
572
|
<pre class="syntax-highlighted">{highlighted}</pre>
|
|
521
573
|
<p class="lesson-description">{description}</p>
|
|
574
|
+
{lesson_subcmd}
|
|
575
|
+
{lesson_flags_html}
|
|
522
576
|
</div>'''
|
|
523
577
|
|
|
524
578
|
# Patterns observed
|
|
@@ -1384,6 +1438,22 @@ def get_inline_css() -> str:
|
|
|
1384
1438
|
color: #34a853;
|
|
1385
1439
|
}
|
|
1386
1440
|
|
|
1441
|
+
.flag-desc { color: #6c757d; margin-left: 4px; }
|
|
1442
|
+
.flags-list li { margin: 4px 0; line-height: 1.5; }
|
|
1443
|
+
.subcmd-section { background: #f0f7ff; padding: 8px 12px; border-radius: 6px; margin: 8px 0; border-left: 3px solid #4a9eff; }
|
|
1444
|
+
.subcmd-label { font-weight: 600; color: #4a9eff; }
|
|
1445
|
+
.patterns-section { margin: 8px 0; }
|
|
1446
|
+
.patterns-section h5 { margin: 4px 0; color: #666; font-size: 0.85em; }
|
|
1447
|
+
.patterns-list { list-style: none; padding: 0; margin: 4px 0; }
|
|
1448
|
+
.patterns-list li { padding: 3px 0; }
|
|
1449
|
+
.patterns-list code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; }
|
|
1450
|
+
.lesson-flags { margin: 6px 0; font-size: 0.9em; color: #555; }
|
|
1451
|
+
.lesson-subcmd { font-size: 0.9em; color: #4a9eff; margin: 4px 0; }
|
|
1452
|
+
.lesson-complexity { font-size: 0.75em; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
|
1453
|
+
.complexity-simple { background: #e8f5e9; color: #2e7d32; }
|
|
1454
|
+
.complexity-intermediate { background: #fff3e0; color: #e65100; }
|
|
1455
|
+
.complexity-advanced { background: #fce4ec; color: #c62828; }
|
|
1456
|
+
|
|
1387
1457
|
.syntax-highlighted .string {
|
|
1388
1458
|
color: #ff6d01;
|
|
1389
1459
|
}
|
|
@@ -2107,27 +2177,70 @@ def generate_html_files(
|
|
|
2107
2177
|
# Transform commands to expected format
|
|
2108
2178
|
formatted_commands = []
|
|
2109
2179
|
for cmd in analyzed_commands:
|
|
2110
|
-
|
|
2180
|
+
cmd_str = cmd.get('command', '')
|
|
2181
|
+
base_cmd = cmd.get('base_command', cmd_str.split()[0] if cmd_str else '')
|
|
2182
|
+
complexity_score = cmd.get('complexity', 1)
|
|
2183
|
+
|
|
2184
|
+
# Look up COMMAND_DB info for this command
|
|
2185
|
+
cmd_info = COMMAND_DB.get(base_cmd, {})
|
|
2186
|
+
kb_flags = get_flags_for_command(base_cmd)
|
|
2187
|
+
|
|
2188
|
+
# Convert flags to expected format WITH descriptions from knowledge base
|
|
2111
2189
|
raw_flags = cmd.get('flags', [])
|
|
2112
2190
|
formatted_flags = []
|
|
2113
2191
|
for f in raw_flags:
|
|
2114
|
-
if isinstance(f, dict):
|
|
2115
|
-
|
|
2192
|
+
if isinstance(f, dict) and 'flag' in f:
|
|
2193
|
+
# Already formatted - but enrich description if empty
|
|
2194
|
+
flag_name = f.get('flag', '')
|
|
2195
|
+
flag_desc = f.get('description', '')
|
|
2196
|
+
if not flag_desc and flag_name in kb_flags:
|
|
2197
|
+
flag_desc = kb_flags[flag_name]
|
|
2198
|
+
formatted_flags.append({'flag': flag_name, 'description': flag_desc})
|
|
2116
2199
|
elif isinstance(f, str):
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2200
|
+
# Raw flag string - look up description from knowledge base
|
|
2201
|
+
flag_desc = kb_flags.get(f, '')
|
|
2202
|
+
# For combined flags like -la, try individual characters
|
|
2203
|
+
if not flag_desc and len(f) > 2 and f.startswith('-') and not f.startswith('--'):
|
|
2204
|
+
char_descs = []
|
|
2205
|
+
for char in f[1:]:
|
|
2206
|
+
single = f'-{char}'
|
|
2207
|
+
if single in kb_flags:
|
|
2208
|
+
char_descs.append(f'{single}: {kb_flags[single]}')
|
|
2209
|
+
if char_descs:
|
|
2210
|
+
flag_desc = '; '.join(char_descs)
|
|
2211
|
+
formatted_flags.append({'flag': f, 'description': flag_desc})
|
|
2212
|
+
|
|
2213
|
+
# Get educational description from COMMAND_DB if session description is empty/generic
|
|
2214
|
+
session_desc = cmd.get('description', '')
|
|
2215
|
+
kb_desc = cmd_info.get('description', '')
|
|
2216
|
+
description = session_desc if session_desc else kb_desc
|
|
2217
|
+
|
|
2218
|
+
# Get subcommand info (for commands like git, docker, npm)
|
|
2219
|
+
subcommands = cmd_info.get('subcommands', {})
|
|
2220
|
+
# Try to identify the subcommand from the full command
|
|
2221
|
+
cmd_tokens = cmd_str.split() if cmd_str else []
|
|
2222
|
+
subcommand_desc = ''
|
|
2223
|
+
if subcommands and len(cmd_tokens) > 1:
|
|
2224
|
+
for token in cmd_tokens[1:]:
|
|
2225
|
+
if not token.startswith('-') and token in subcommands:
|
|
2226
|
+
subcommand_desc = subcommands[token]
|
|
2227
|
+
break
|
|
2228
|
+
|
|
2229
|
+
# Get common patterns from COMMAND_DB
|
|
2230
|
+
common_patterns = cmd_info.get('common_patterns', [])
|
|
2121
2231
|
|
|
2122
2232
|
formatted_commands.append({
|
|
2123
|
-
'base_command':
|
|
2233
|
+
'base_command': base_cmd,
|
|
2124
2234
|
'full_command': cmd_str,
|
|
2125
2235
|
'category': cmd.get('category', 'Other'),
|
|
2126
2236
|
'complexity': complexity_to_label(complexity_score),
|
|
2127
2237
|
'complexity_score': complexity_score,
|
|
2128
2238
|
'frequency': frequency_map.get(cmd_str, 1),
|
|
2129
|
-
'description':
|
|
2239
|
+
'description': description,
|
|
2130
2240
|
'flags': formatted_flags,
|
|
2241
|
+
'subcommand_desc': subcommand_desc,
|
|
2242
|
+
'common_patterns': common_patterns[:6],
|
|
2243
|
+
'args': cmd.get('args', []),
|
|
2131
2244
|
'is_new': False,
|
|
2132
2245
|
})
|
|
2133
2246
|
|
|
@@ -729,20 +729,44 @@ def _generate_distractor_flags(cmd: str, correct_flag: str, count: int = 3) -> l
|
|
|
729
729
|
|
|
730
730
|
|
|
731
731
|
def _generate_distractor_descriptions(correct_desc: str, count: int = 3) -> list[str]:
|
|
732
|
-
"""Generate plausible wrong descriptions."""
|
|
732
|
+
"""Generate plausible wrong descriptions using command-level descriptions for length parity."""
|
|
733
733
|
distractors = []
|
|
734
734
|
|
|
735
|
-
#
|
|
736
|
-
|
|
737
|
-
for
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
735
|
+
# First: collect command-level descriptions from COMMAND_DB (similar length to correct answer)
|
|
736
|
+
cmd_descriptions = []
|
|
737
|
+
for cmd_name in COMMAND_DB:
|
|
738
|
+
cmd_info = COMMAND_DB[cmd_name]
|
|
739
|
+
desc = cmd_info.get('description', '')
|
|
740
|
+
if desc and desc.lower() != correct_desc.lower():
|
|
741
|
+
# Truncate very long descriptions to similar length as correct answer
|
|
742
|
+
max_len = max(len(correct_desc) + 40, 80)
|
|
743
|
+
if len(desc) > max_len:
|
|
744
|
+
desc = desc[:max_len].rsplit(' ', 1)[0] + '...'
|
|
745
|
+
cmd_descriptions.append(desc)
|
|
746
|
+
|
|
747
|
+
if cmd_descriptions:
|
|
748
|
+
random.shuffle(cmd_descriptions)
|
|
749
|
+
distractors.extend(cmd_descriptions[:count])
|
|
750
|
+
|
|
751
|
+
# Fallback: use flag descriptions if not enough command descriptions
|
|
752
|
+
if len(distractors) < count:
|
|
753
|
+
all_flag_descs = []
|
|
754
|
+
for cmd in _get_all_flagged_commands():
|
|
755
|
+
all_flag_descs.extend(_get_flags_for_cmd(cmd).values())
|
|
756
|
+
all_flag_descs = list(set(all_flag_descs))
|
|
757
|
+
all_flag_descs = [d for d in all_flag_descs if d.lower() != correct_desc.lower()]
|
|
758
|
+
random.shuffle(all_flag_descs)
|
|
759
|
+
distractors.extend(all_flag_descs[:count - len(distractors)])
|
|
760
|
+
|
|
761
|
+
# Remove duplicates
|
|
762
|
+
seen = set()
|
|
763
|
+
unique = []
|
|
764
|
+
for d in distractors:
|
|
765
|
+
dl = d.lower()
|
|
766
|
+
if dl not in seen:
|
|
767
|
+
seen.add(dl)
|
|
768
|
+
unique.append(d)
|
|
769
|
+
return unique[:count]
|
|
746
770
|
|
|
747
771
|
|
|
748
772
|
def generate_what_does_quiz(
|
|
@@ -793,9 +817,27 @@ def generate_what_does_quiz(
|
|
|
793
817
|
for rel_cmd in related_cmds[:3 - len(distractor_descriptions)]:
|
|
794
818
|
distractor_descriptions.append(f"Runs {rel_cmd} to process files")
|
|
795
819
|
|
|
796
|
-
# Ensure we have exactly 3 distractors
|
|
820
|
+
# Ensure we have exactly 3 distractors with plausible alternatives
|
|
821
|
+
fallback_actions = [
|
|
822
|
+
f"List directory contents with detailed file information",
|
|
823
|
+
f"Search recursively through files for matching patterns",
|
|
824
|
+
f"Display or modify file permissions and ownership",
|
|
825
|
+
f"Compress or archive files for storage and transfer",
|
|
826
|
+
f"Monitor system processes and resource usage",
|
|
827
|
+
f"Download files from a remote server or URL",
|
|
828
|
+
f"Edit configuration files in the default text editor",
|
|
829
|
+
f"Install or update packages from the package manager",
|
|
830
|
+
]
|
|
831
|
+
random.shuffle(fallback_actions)
|
|
832
|
+
fb_idx = 0
|
|
797
833
|
while len(distractor_descriptions) < 3:
|
|
798
|
-
|
|
834
|
+
if fb_idx < len(fallback_actions):
|
|
835
|
+
fallback = fallback_actions[fb_idx]
|
|
836
|
+
if fallback.lower() != correct_desc.lower():
|
|
837
|
+
distractor_descriptions.append(fallback)
|
|
838
|
+
fb_idx += 1
|
|
839
|
+
else:
|
|
840
|
+
distractor_descriptions.append(f"Run a system utility to process input data")
|
|
799
841
|
|
|
800
842
|
# Create options (shuffle positions)
|
|
801
843
|
options = []
|
|
@@ -929,19 +971,20 @@ def generate_build_command_quiz(
|
|
|
929
971
|
parsed = _parse_command(cmd_string)
|
|
930
972
|
base_cmd = parsed["base"]
|
|
931
973
|
|
|
932
|
-
#
|
|
933
|
-
|
|
934
|
-
correct_answer = " ".join(correct_components)
|
|
974
|
+
# Use the original command string as correct answer (preserves flag-argument ordering)
|
|
975
|
+
correct_answer = cmd_string.strip()
|
|
935
976
|
|
|
936
|
-
# Generate wrong arrangements
|
|
977
|
+
# Generate wrong arrangements using parsed components
|
|
978
|
+
all_parts = [base_cmd] + parsed["flags"] + parsed["args"]
|
|
937
979
|
distractors = []
|
|
938
980
|
|
|
939
|
-
# Distractor 1: Wrong order
|
|
940
|
-
if len(
|
|
941
|
-
wrong_order =
|
|
981
|
+
# Distractor 1: Wrong order of components
|
|
982
|
+
if len(all_parts) > 2:
|
|
983
|
+
wrong_order = all_parts.copy()
|
|
942
984
|
random.shuffle(wrong_order)
|
|
943
|
-
|
|
944
|
-
|
|
985
|
+
wrong_str = " ".join(wrong_order)
|
|
986
|
+
if wrong_str != correct_answer:
|
|
987
|
+
distractors.append(wrong_str)
|
|
945
988
|
|
|
946
989
|
# Distractor 2: Missing flag
|
|
947
990
|
if parsed["flags"]:
|
|
@@ -961,15 +1004,31 @@ def generate_build_command_quiz(
|
|
|
961
1004
|
wrong_cmd = [related[0]] + parsed["flags"] + parsed["args"]
|
|
962
1005
|
distractors.append(" ".join(wrong_cmd))
|
|
963
1006
|
|
|
964
|
-
# Ensure we have exactly 3 distractors
|
|
1007
|
+
# Ensure we have exactly 3 distractors with plausible alternatives
|
|
1008
|
+
# Use real flags from the knowledge base as fallback distractors
|
|
1009
|
+
all_cmd_flags = list(_get_flags_for_cmd(base_cmd).keys())
|
|
1010
|
+
random.shuffle(all_cmd_flags)
|
|
1011
|
+
fb_flag_idx = 0
|
|
965
1012
|
while len(distractors) < 3:
|
|
966
|
-
|
|
967
|
-
|
|
1013
|
+
if fb_flag_idx < len(all_cmd_flags):
|
|
1014
|
+
fallback_flag = all_cmd_flags[fb_flag_idx]
|
|
1015
|
+
fallback_cmd = f"{base_cmd} {fallback_flag} {' '.join(parsed['args'])}"
|
|
1016
|
+
if fallback_cmd.strip() != correct_answer:
|
|
1017
|
+
distractors.append(fallback_cmd.strip())
|
|
1018
|
+
fb_flag_idx += 1
|
|
1019
|
+
else:
|
|
1020
|
+
# Use a related command as last resort
|
|
1021
|
+
related = _get_related_commands(base_cmd)
|
|
1022
|
+
if related:
|
|
1023
|
+
rel = related[len(distractors) % len(related)]
|
|
1024
|
+
distractors.append(f"{rel} {' '.join(parsed['flags'])} {' '.join(parsed['args'])}".strip())
|
|
1025
|
+
else:
|
|
1026
|
+
distractors.append(f"{base_cmd} {' '.join(parsed['args'])}".strip())
|
|
968
1027
|
|
|
969
1028
|
# Remove duplicates and correct answer from distractors
|
|
970
1029
|
distractors = list(set(d for d in distractors if d != correct_answer))[:3]
|
|
971
1030
|
while len(distractors) < 3:
|
|
972
|
-
distractors.append(f"{base_cmd}
|
|
1031
|
+
distractors.append(f"{base_cmd} {' '.join(parsed['args'])}".strip())
|
|
973
1032
|
|
|
974
1033
|
# Create options
|
|
975
1034
|
all_answers = [correct_answer] + distractors[:3]
|
|
@@ -1203,7 +1262,13 @@ def generate_quiz_set(
|
|
|
1203
1262
|
target_build = max(1, int(count * 0.2))
|
|
1204
1263
|
target_spot_diff = max(1, int(count * 0.15))
|
|
1205
1264
|
|
|
1206
|
-
|
|
1265
|
+
# Track used commands per quiz type to avoid repeating the same command
|
|
1266
|
+
used_per_type = {
|
|
1267
|
+
QuizType.WHAT_DOES: set(),
|
|
1268
|
+
QuizType.WHICH_FLAG: set(),
|
|
1269
|
+
QuizType.BUILD_COMMAND: set(),
|
|
1270
|
+
QuizType.SPOT_DIFFERENCE: set(),
|
|
1271
|
+
}
|
|
1207
1272
|
|
|
1208
1273
|
# Generate "What does this do?" questions
|
|
1209
1274
|
random.shuffle(weighted_commands)
|
|
@@ -1211,38 +1276,47 @@ def generate_quiz_set(
|
|
|
1211
1276
|
if len([q for q in questions if q.quiz_type == QuizType.WHAT_DOES]) >= target_what_does:
|
|
1212
1277
|
break
|
|
1213
1278
|
cmd_id = cmd.get("command", "")
|
|
1214
|
-
if cmd_id not in
|
|
1279
|
+
if cmd_id not in used_per_type[QuizType.WHAT_DOES]:
|
|
1215
1280
|
q = generate_what_does_quiz(cmd)
|
|
1216
1281
|
questions.append(q)
|
|
1217
|
-
|
|
1282
|
+
used_per_type[QuizType.WHAT_DOES].add(cmd_id)
|
|
1218
1283
|
|
|
1219
1284
|
# Generate "Which flag?" questions
|
|
1220
1285
|
random.shuffle(weighted_commands)
|
|
1221
1286
|
for cmd in weighted_commands:
|
|
1222
1287
|
if len([q for q in questions if q.quiz_type == QuizType.WHICH_FLAG]) >= target_which_flag:
|
|
1223
1288
|
break
|
|
1224
|
-
|
|
1225
|
-
if
|
|
1226
|
-
|
|
1289
|
+
cmd_id = cmd.get("command", "")
|
|
1290
|
+
if cmd_id not in used_per_type[QuizType.WHICH_FLAG]:
|
|
1291
|
+
q = generate_which_flag_quiz(cmd)
|
|
1292
|
+
if q:
|
|
1293
|
+
questions.append(q)
|
|
1294
|
+
used_per_type[QuizType.WHICH_FLAG].add(cmd_id)
|
|
1227
1295
|
|
|
1228
1296
|
# Generate "Build the command" questions
|
|
1229
1297
|
random.shuffle(weighted_commands)
|
|
1230
1298
|
for cmd in weighted_commands:
|
|
1231
1299
|
if len([q for q in questions if q.quiz_type == QuizType.BUILD_COMMAND]) >= target_build:
|
|
1232
1300
|
break
|
|
1233
|
-
|
|
1234
|
-
|
|
1301
|
+
cmd_id = cmd.get("command", "")
|
|
1302
|
+
if cmd_id not in used_per_type[QuizType.BUILD_COMMAND]:
|
|
1303
|
+
q = generate_build_command_quiz(cmd)
|
|
1304
|
+
questions.append(q)
|
|
1305
|
+
used_per_type[QuizType.BUILD_COMMAND].add(cmd_id)
|
|
1235
1306
|
|
|
1236
1307
|
# Generate "Spot the difference" questions
|
|
1237
1308
|
random.shuffle(weighted_commands)
|
|
1238
1309
|
for cmd in weighted_commands:
|
|
1239
1310
|
if len([q for q in questions if q.quiz_type == QuizType.SPOT_DIFFERENCE]) >= target_spot_diff:
|
|
1240
1311
|
break
|
|
1241
|
-
|
|
1242
|
-
if
|
|
1243
|
-
|
|
1244
|
-
if
|
|
1245
|
-
|
|
1312
|
+
cmd_id = cmd.get("command", "")
|
|
1313
|
+
if cmd_id not in used_per_type[QuizType.SPOT_DIFFERENCE]:
|
|
1314
|
+
variant = _create_similar_command_variant(cmd)
|
|
1315
|
+
if variant:
|
|
1316
|
+
q = generate_spot_difference_quiz(cmd, variant)
|
|
1317
|
+
if q:
|
|
1318
|
+
questions.append(q)
|
|
1319
|
+
used_per_type[QuizType.SPOT_DIFFERENCE].add(cmd_id)
|
|
1246
1320
|
|
|
1247
1321
|
# Shuffle final questions
|
|
1248
1322
|
random.shuffle(questions)
|