aboutsummaryrefslogtreecommitdiff
path: root/modules/ext
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext')
-rw-r--r--modules/ext/editor.js4
-rw-r--r--modules/ext/tutorial.js277
2 files changed, 281 insertions, 0 deletions
diff --git a/modules/ext/editor.js b/modules/ext/editor.js
index c597dd5..8c3ac0f 100644
--- a/modules/ext/editor.js
+++ b/modules/ext/editor.js
@@ -44,6 +44,10 @@ class EditorModule {
const ok = this.terminal.fs.writeFile(this._vfsPath, code);
if (ok) {
this.terminal.print('✅ Saved to ' + this._vfsPath, 'success');
+ // Notify tutorial module for quest 4 (edit + save)
+ if (this.terminal.tutorialModule) {
+ this.terminal.tutorialModule.onFileSaved(this._vfsPath);
+ }
} else {
this.terminal.print('❌ Failed to save ' + this._vfsPath, 'error');
}
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');
+}