diff options
Diffstat (limited to 'modules/ext/tutorial.js')
| -rw-r--r-- | modules/ext/tutorial.js | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/modules/ext/tutorial.js b/modules/ext/tutorial.js new file mode 100644 index 0000000..8983322 --- /dev/null +++ b/modules/ext/tutorial.js @@ -0,0 +1,277 @@ +class TutorialModule { + constructor() { + this.commands = {}; + this.terminal = null; + this.STORAGE_KEY = 'webx11_tutorial_state'; + this.COMPLETE_KEY = 'webx11_tutorial_complete'; + this.SKIP_KEY = 'webx11_tutorial_skipped'; + this._maxTries = 3; + this._initState(); + } + + _initState() { + // Restore saved state or start fresh + const saved = localStorage.getItem(this.STORAGE_KEY); + if (saved) { + try { + const s = JSON.parse(saved); + this._completed = new Set(s.completed || []); + this._activeIndex = s.activeIndex || 0; + this._tries = s.tries || {}; + this._started = s.started || false; + return; + } catch(e) { /* corrupt, reset */ } + } + this._completed = new Set(); + this._activeIndex = 0; + this._tries = {}; + this._started = false; + } + + _saveState() { + try { + localStorage.setItem(this.STORAGE_KEY, JSON.stringify({ + completed: Array.from(this._completed), + activeIndex: this._activeIndex, + tries: this._tries, + started: this._started + })); + } catch(e) { /* quota */ } + } + + _getQuests() { + return [ + { id: 1, title: 'Your First Command', desc: 'Type `help` to see every command available in newOS4.', + check: (cmd) => cmd === 'help', hint: 'Just type: help' }, + { id: 2, title: 'Explore the Filesystem', desc: 'Type `ls` to list files in the current directory.', + check: (cmd) => cmd === 'ls', hint: 'Type: ls' }, + { id: 3, title: 'Create Something', desc: 'Use `touch hello.txt` to create your first file.', + check: (cmd) => cmd === 'touch', + hint: 'Type: touch hello.txt' }, + { id: 4, title: 'Write to a File', desc: 'Open the editor with `edit hello.txt`, type something, and click Save.', + check: (cmd, term) => { const c = term && term.fs && term.fs.readFile('hello.txt'); return c && c.length > 0; }, + hint: 'Type: edit hello.txt, then write something and click 💾 Save & Test' }, + { id: 5, title: 'Talk to the AI', desc: 'Type `ai "hello"` to greet the AI assistant.', + check: (cmd) => cmd === 'ai' || cmd === 'ask', + hint: 'Type: ai "hello"' }, + { id: 6, title: 'Check Your Progress', desc: 'Type `status` to see how the system is doing.', + check: (cmd) => cmd === 'status', + hint: 'Type: status' }, + ]; + } + + requirements() { + return { met: true, missing: [] }; + } + + getHelpInfo() { + return { + 'tutorial': 'tutorial [start|skip|status] — Interactive guided tour of newOS4 (6 quests)', + }; + } + + registerCommands() { + this.commands['tutorial'] = (args) => this._handleTutorial(args); + } + + // ── Public API called by terminal.js ── + + /** Called after every command execution to check quest progress */ + onCommandExecuted(cmd, args) { + if (!this._started || this.isComplete()) return; + const quests = this._getQuests(); + const current = quests[this._activeIndex]; + if (!current || this._completed.has(current.id)) return; + + // Special handling: quest 4 (edit) — don't count as wrong, wait for save + if (current.id === 4 && cmd === 'edit') { + this.terminal.print(' ✅ Editor opened! Write something and click Save.', 'success'); + return; // Don't fail, don't complete — wait for onFileSaved + } + + // Check if this command completes the current quest + if (current.check(cmd, this.terminal)) { + this._completeCurrentQuest(); + } else { + // Wrong command — increment try count + const count = (this._tries[current.id] || 0) + 1; + this._tries[current.id] = count; + this._saveState(); + if (count >= this._maxTries) { + this.terminal.print(`💡 Hint: ${current.hint}`, 'warning'); + this._tries[current.id] = 0; // reset so hint repeats + } + } + } + + /** Called by editor module after a file is saved via the modal */ + onFileSaved(path) { + if (!this._started || this.isComplete()) return; + const quests = this._getQuests(); + const current = quests[this._activeIndex]; + if (!current || current.id !== 4 || this._completed.has(current.id)) return; + + if (current.check('__file_saved__', this.terminal)) { + this._completeCurrentQuest(); + } + } + + _completeCurrentQuest() { + const quests = this._getQuests(); + const current = quests[this._activeIndex]; + this._completed.add(current.id); + const nextIdx = this._activeIndex + 1; + const next = quests[nextIdx]; + if (next) { + this._activeIndex = nextIdx; + this._saveState(); + this._showQuestNext(next); + } else { + this._activeIndex = nextIdx; + this._saveState(); + this._showCompletion(); + } + } + + isComplete() { + return localStorage.getItem(this.COMPLETE_KEY) !== null; + } + + isSkipped() { + return localStorage.getItem(this.SKIP_KEY) !== null; + } + + isStarted() { + return this._started; + } + + /** Called by welcome modal to begin the tutorial */ + startTutorial() { + this._started = true; + this._completed = new Set(); + this._activeIndex = 0; + this._tries = {}; + this._saveState(); + localStorage.removeItem(this.COMPLETE_KEY); + localStorage.removeItem(this.SKIP_KEY); + this.terminal.print('╔══════════════════════════════════════╗', 'success'); + this.terminal.print('║ 🎮 newOS4 Tutorial Started! ║', 'success'); + this.terminal.print('╚══════════════════════════════════════╝', 'success'); + this.terminal.print('Follow the quests below. Type the correct command to advance.', 'info'); + this._showQuestNext(this._getQuests()[0]); + } + + skipTutorial() { + localStorage.setItem(this.SKIP_KEY, '1'); + this._started = false; + this._completed = new Set(); + this._activeIndex = 0; + this._tries = {}; + localStorage.removeItem(this.STORAGE_KEY); + this.terminal.print('Tutorial skipped. Type `tutorial start` to begin anytime, or `help` to see all commands.', 'info'); + } + + /** Show status of tutorial progress */ + showStatus() { + const quests = this._getQuests(); + const total = quests.length; + const done = this._completed.size; + + if (!this._started && !this.isComplete()) { + this.terminal.print('📋 Tutorial has not been started. Type `tutorial start` to begin!', 'info'); + return; + } + if (this.isComplete()) { + this.terminal.print('🎉 Tutorial complete! All ' + total + ' quests finished.', 'success'); + return; + } + + this.terminal.print('📋 Tutorial Progress: ' + done + '/' + total + ' quests', 'info'); + quests.forEach((q, i) => { + const status = this._completed.has(q.id) ? '✅' : (i === this._activeIndex ? '👉' : '⬜'); + this.terminal.print(' ' + status + ' Quest ' + q.id + ': ' + q.title); + }); + if (this._activeIndex < total) { + this.terminal.print(''); + this._showQuestNext(quests[this._activeIndex]); + } + } + + // ── Internal ── + + _handleTutorial(args) { + if (!args || args.length === 0) { + // No args — show status or start if first time + if (!this._started && !this.isComplete()) { + this.terminal.print('╔══════════════════════════════════════╗', 'info'); + this.terminal.print('║ 🎮 newOS4 Interactive Tutorial ║', 'info'); + this.terminal.print('╚══════════════════════════════════════╝', 'info'); + this.terminal.print('Learn newOS4 by completing 6 short quests.', 'info'); + this.terminal.print(''); + this.terminal.print('Commands:', 'info'); + this.terminal.print(' tutorial start — Begin or restart the tutorial'); + this.terminal.print(' tutorial status — Show your progress'); + this.terminal.print(' tutorial skip — Dismiss the tutorial'); + return; + } + this.showStatus(); + return; + } + + const sub = args[0].toLowerCase(); + if (sub === 'start') this.startTutorial(); + else if (sub === 'skip') this.skipTutorial(); + else if (sub === 'status') this.showStatus(); + else this.terminal.print('Usage: tutorial [start|skip|status]', 'warning'); + } + + _showQuestNext(quest) { + this.terminal.print('── Quest ' + quest.id + '/' + this._getQuests().length + ': ' + quest.title + ' ──', 'info'); + this.terminal.print(' ' + quest.desc, 'text-muted'); + this.terminal.print(' 💡 ' + quest.hint, 'warning'); + } + + _showCompletion() { + this.terminal.print(''); + this.terminal.print('🎉🎉🎉 CONGRATULATIONS! 🎉🎉🎉', 'success'); + this.terminal.print('═'.repeat(40), 'success'); + this.terminal.print(' All 6 quests complete!', 'success'); + this.terminal.print(' You now know the basics of newOS4.', 'success'); + this.terminal.print('═'.repeat(40), 'success'); + this.terminal.print(''); + this.terminal.print('What next?', 'info'); + this.terminal.print(' • Explore commands: help', 'info'); + this.terminal.print(' • Full docs: help.html', 'info'); + this.terminal.print(' • AI assistant: ai \"your question\"', 'info'); + this.terminal.print(' • Schedule tasks: cron-every 60 echo hello', 'info'); + this.terminal.print(' • Install packages: wpm list', 'info'); + this.terminal.print(' • Manage files: edit, find, grep, files', 'info'); + + localStorage.setItem(this.COMPLETE_KEY, Date.now().toString()); + localStorage.removeItem(this.STORAGE_KEY); + + // Fire confetti + this._fireConfetti(); + } + + _fireConfetti() { + const emojis = ['🎉', '✨', '⭐', '🌟', '💫', '🎊', '🔷', '🟢', '🔶']; + const container = document.body; + if (!container) return; + for (let i = 0; i < 40; i++) { + const el = document.createElement('div'); + el.className = 'confetti-piece'; + el.textContent = emojis[i % emojis.length]; + el.style.left = (Math.random() * 100) + 'vw'; + el.style.animationDuration = (2 + Math.random() * 2) + 's'; + el.style.animationDelay = (Math.random() * 1.5) + 's'; + el.style.fontSize = (16 + Math.random() * 20) + 'px'; + container.appendChild(el); + setTimeout(() => el.remove(), 5000); + } + } +} + +if (typeof window.webx11 !== 'undefined' && window.webx11) { + webx11.moduleManager.registerModule(new TutorialModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/tutorial.js'); +} |