aboutsummaryrefslogtreecommitdiff
path: root/course.html
diff options
context:
space:
mode:
Diffstat (limited to 'course.html')
-rw-r--r--course.html252
1 files changed, 252 insertions, 0 deletions
diff --git a/course.html b/course.html
new file mode 100644
index 0000000..c61a901
--- /dev/null
+++ b/course.html
@@ -0,0 +1,252 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="theme-color" content="#0a0a14">
+<title>newOS4 — Interactive Course</title>
+<style>
+:root{--bg:#0a0a14;--bg2:#141428;--bg3:#1c1c36;--text:#e8e8ff;--text2:#b0b0cc;--accent:#818cf8;--accent2:#a78bfa;
+--green:#34d399;--cyan:#22d3ee;--amber:#fbbf24;--rose:#fb7185;--border:#3a3a5a;--radius:12px;
+--mono:'JetBrains Mono','Fira Code',monospace;--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,Roboto,sans-serif}
+*{box-sizing:border-box;margin:0;padding:0}
+body{background:radial-gradient(1200px 600px at 20% -10%,#1b1b3a 0,var(--bg) 55%);color:var(--text);font-family:var(--sans);min-height:100vh}
+a{color:var(--accent)} code,pre{font-family:var(--mono)}
+header{position:sticky;top:0;z-index:20;display:flex;gap:10px;align-items:center;padding:12px 20px;background:rgba(10,10,20,.86);backdrop-filter:blur(14px);border-bottom:1px solid var(--border);flex-wrap:wrap}
+header h1{font-size:1.05rem;letter-spacing:.3px}
+.chip{font-size:.72rem;padding:3px 9px;border:1px solid var(--border);border-radius:999px;color:var(--text2)}
+.btn{background:var(--bg3);border:1px solid var(--border);color:var(--text);border-radius:9px;padding:7px 12px;cursor:pointer;font-size:.82rem;text-decoration:none;display:inline-block}
+.btn:hover{border-color:var(--accent);color:#fff}
+.btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));border:0;color:#0b0b18;font-weight:700}
+.grow{flex:1}
+.layout{display:grid;grid-template-columns:280px minmax(0,1fr) 300px;gap:18px;padding:18px;max-width:1500px;margin:0 auto}
+@media(max-width:1050px){.layout{grid-template-columns:1fr}.rail{display:none}}
+.panel{background:rgba(20,20,40,.72);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
+.side h3,.rail h3{font-size:.75rem;text-transform:uppercase;letter-spacing:.12em;color:var(--text2);margin-bottom:10px}
+.chapter{display:flex;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:9px;cursor:pointer;font-size:.85rem}
+.chapter:hover,.chapter.active{background:var(--bg3)}
+.chapter small{color:var(--text2)}
+.lesson{padding:6px 10px 6px 22px;font-size:.8rem;color:var(--text2);cursor:pointer;border-left:2px solid transparent}
+.lesson:hover{color:var(--text)} .lesson.done{color:var(--green);border-left-color:var(--green)}
+.bar{height:8px;background:var(--bg3);border-radius:99px;overflow:hidden}
+.bar>i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--accent),var(--cyan));transition:width .3s}
+main h2{font-size:1.35rem;margin-bottom:6px}
+.meta{color:var(--text2);font-size:.82rem;margin-bottom:16px}
+.block{margin:12px 0;line-height:1.65}
+.block.cmd{background:#0d0d1c;border:1px solid var(--border);border-left:3px solid var(--green);border-radius:8px;padding:10px 12px;font-family:var(--mono);color:#b8f5d8;display:flex;justify-content:space-between;gap:10px;align-items:center;flex-wrap:wrap}
+.block.tip,.block.warn{border-radius:8px;padding:10px 12px;font-size:.9rem}
+.block.tip{background:rgba(52,211,153,.08);border:1px solid rgba(52,211,153,.3)}
+.block.warn{background:rgba(251,113,133,.08);border:1px solid rgba(251,113,133,.35)}
+.block.table{background:rgba(34,211,238,.06);border:1px solid rgba(34,211,238,.25);border-radius:8px;padding:10px 12px;font-size:.85rem}
+.exercise{margin-top:18px;background:rgba(129,140,248,.07);border:1px solid rgba(129,140,248,.35);border-radius:10px;padding:14px}
+.quiz{margin-top:14px;border-top:1px dashed var(--border);padding-top:14px}
+.opt{display:block;width:100%;text-align:left;margin:6px 0;padding:8px 10px;background:var(--bg3);border:1px solid var(--border);border-radius:8px;color:var(--text);cursor:pointer;font-size:.85rem}
+.opt:hover{border-color:var(--accent)}
+.rail .search{width:100%;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:9px;color:var(--text);font-size:.82rem}
+.atlas-item{padding:7px 0;border-bottom:1px solid rgba(58,58,90,.4);font-size:.8rem}
+.atlas-item code{color:var(--cyan)} .atlas-item span{color:var(--text2);display:block;margin-top:2px}
+#tutor-log{max-height:260px;overflow:auto;font-size:.8rem;color:var(--text2);margin:8px 0}
+.small{font-size:.75rem;color:var(--text2)}
+</style>
+</head>
+<body>
+<header>
+ <h1>📚 newOS4 <span style="color:var(--accent)">Course</span></h1>
+ <span class="chip" id="ver">loading…</span>
+ <span class="chip" id="xp">0 XP</span>
+ <div class="grow"></div>
+ <button class="btn" onclick="showProjects()">Projects</button>
+ <button class="btn" onclick="showFeed()">Daily</button>
+ <a class="btn" href="main.html">⌨ Terminal</a>
+ <a class="btn" href="help.html">Help</a>
+ <a class="btn" href="modules.html">Modules</a>
+</header>
+<div class="layout">
+ <aside class="panel side">
+ <h3>Curriculum</h3>
+ <div class="bar" style="margin-bottom:12px"><i id="pbar"></i></div>
+ <div id="chapters"><p class="small">Loading…</p></div>
+ <div style="margin-top:14px;border-top:1px solid var(--border);padding-top:10px">
+ <label class="small" style="display:flex;gap:6px;align-items:center;cursor:pointer">
+ <input type="checkbox" id="telemetry" onchange="setTelemetry(this.checked)"> Share anonymous progress
+ </label>
+ <button class="btn" style="width:100%;margin-top:8px" onclick="exportProgress()">Export progress</button>
+ </div>
+ </aside>
+ <main class="panel" id="reader"><p class="small">Loading course…</p></main>
+ <aside class="panel rail">
+ <h3>Command atlas</h3>
+ <input class="search" id="atlas-q" placeholder="filter 344 commands…" autocomplete="off">
+ <div id="atlas" style="margin-top:8px;max-height:400px;overflow:auto"></div>
+ <h3 style="margin-top:16px">Ask the tutor</h3>
+ <div id="tutor-log"></div>
+ <input class="search" id="tutor-q" placeholder="ask about this lesson…" autocomplete="off">
+ <button class="btn primary" style="width:100%;margin-top:8px" id="tutor-send">Ask AI tutor</button>
+ </aside>
+</div>
+<script>
+const BASE = '/spaces/newos4/course/';
+const KEY = 'webx11_course_progress_v1';
+const V = Date.now().toString(36);
+let MANIFEST = null, ATLAS = null, ACTIVE = null;
+const $ = id => document.getElementById(id);
+const esc = s => String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
+const prog = () => { try { return JSON.parse(localStorage.getItem(KEY)) || {completed:{},xp:0}; } catch(e){ return {completed:{},xp:0}; } };
+const save = p => localStorage.setItem(KEY, JSON.stringify(p));
+
+async function boot(){
+ try{
+ const [m,a] = await Promise.all([
+ fetch(BASE+'course.json?v='+V).then(r=>r.json()),
+ fetch(BASE+'atlas.json?v='+V).then(r=>r.json()).catch(()=>null)
+ ]);
+ MANIFEST = m; ATLAS = a;
+ $('ver').textContent = 'v' + m.version;
+ $('telemetry').checked = localStorage.getItem('webx11_course_telemetry') !== 'off';
+ renderChapters(); renderAtlas(); renderProgress();
+ }catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Course content unreachable: '+esc(e.message)+'</p>'; }
+}
+function renderProgress(){
+ const p = prog(), total = (MANIFEST.chapters||[]).reduce((n,c)=>n+(c.lessons||[]).length,0);
+ $('xp').textContent = (p.xp||0) + ' XP';
+ $('pbar').style.width = total ? Math.round(Object.keys(p.completed).length/total*100)+'%' : '0%';
+}
+function renderAtlas(q){
+ if(!ATLAS){ $('atlas').innerHTML='<p class="small" style="color:var(--rose)">atlas.json missing</p>'; return; }
+ const items = Object.entries(ATLAS.commands).filter(([c,e])=> !q || (c+' '+e.desc+' '+e.module+' '+e.category).toLowerCase().includes(q.toLowerCase()));
+ $('atlas').innerHTML = '<p class="small" style="margin-bottom:6px">'+items.length+' / '+Object.keys(ATLAS.commands).length+' commands</p>'
+ + items.slice(0,60).map(([c,e])=>'<div class="atlas-item"><code>'+esc(c)+'</code><span>'+esc(e.desc||e.module)+' · '+esc(e.tier)+'</span></div>').join('');
+}
+async function renderChapters(){
+ const p = prog(), wrap = $('chapters'); wrap.innerHTML = '';
+ for(const ch of MANIFEST.chapters||[]){
+ const d = document.createElement('div'); d.className='chapter';
+ d.innerHTML = '<span>'+esc(ch.icon||'📘')+' '+esc(ch.title)+'</span><small>'+ (ch.lessons||[]).filter(l=>p.completed[l.id]).length +'/'+(ch.lessons||[]).length+'</small>';
+ wrap.appendChild(d);
+ const box = document.createElement('div'); wrap.appendChild(box);
+ d.onclick = () => { d.classList.toggle('active'); box.style.display = box.style.display==='none'?'block':'none'; };
+ for(const l of ch.lessons||[]){
+ const e = document.createElement('div'); e.className='lesson'+(p.completed[l.id]?' done':''); e.textContent = l.id+' · '+l.title;
+ e.onclick = () => openLesson(ch.id, l.id); box.appendChild(e);
+ }
+ }
+ const p2 = prog(); const all = (MANIFEST.chapters||[]).flatMap(c=>(c.lessons||[]).map(l=>({c,l})));
+ const first = all.find(x=>!p2.completed[x.l.id]) || all[0];
+ if(first) openLesson(first.c.id, first.l.id);
+}
+async function openLesson(chId, lessonId){
+ $('reader').innerHTML = '<p class="small">Loading lesson…</p>';
+ try{
+ const ch = await fetch(BASE+'chapters/'+chId+'.json?v='+V).then(r=>r.json());
+ const l = (ch.lessons||[]).find(x=>x.id===lessonId); if(!l) throw new Error('lesson not found');
+ ACTIVE = {ch, l};
+ const p = prog();
+ let html = '<h2>'+esc(l.title)+'</h2><div class="meta">'+esc(l.objective||'')+' · ⏱ '+(l.minutes||5)+' min · 📘 '+esc(ch.title)+'</div>';
+ for(const b of l.blocks||[]){
+ if(b.type==='cmd') html += '<div class="block cmd"><span>$ '+esc(b.text)+'</span><button class="btn" data-copy="'+esc(b.text)+'">Copy</button></div>';
+ else if(b.type==='code') html += '<pre class="block cmd">'+esc(b.text)+'</pre>';
+ else if(b.type==='tip') html += '<div class="block tip">💡 '+esc(b.text)+'</div>';
+ else if(b.type==='warn') html += '<div class="block warn">⚠️ '+esc(b.text)+'</div>';
+ else if(b.type==='table') html += '<div class="block table">'+esc(b.text)+'</div>';
+ else html += '<p class="block">'+esc(b.text)+'</p>';
+ }
+ if(l.exercise){
+ html += '<div class="exercise"><strong>🏋️ Exercise</strong><p style="margin:6px 0">'+esc(l.exercise.instruction)+'</p>'
+ + '<button class="btn" id="hint-btn">Show hint</button> <button class="btn primary" id="done-btn">Mark done'+(p.completed[l.id]?' ✓':'')+'</button>'
+ + '<p id="hint" style="display:none;margin-top:8px;color:var(--amber)">💡 '+esc(l.exercise.hint||'')+'</p>'
+ + '<p class="small" style="margin-top:8px">Same XP as the terminal: run it in <a href="main.html">main.html</a> with <code>course open '+esc(l.id)+'</code>.</p></div>';
+ }
+ if(l.quiz){
+ html += '<div class="quiz"><strong>❓ '+esc(l.quiz.q)+'</strong>';
+ (l.quiz.options||[]).forEach((o,i)=>{ html += '<button class="opt" data-opt="'+i+'">'+(i+1)+') '+esc(o)+'</button>'; });
+ html += '<p id="quiz-msg" style="margin-top:6px"></p></div>';
+ }
+ $('reader').innerHTML = html;
+ $('reader').querySelectorAll('[data-copy]').forEach(b=>b.onclick=()=>{ navigator.clipboard&&navigator.clipboard.writeText(b.dataset.copy); b.textContent='Copied'; });
+ if($('hint-btn')) $('hint-btn').onclick=()=>{ $('hint').style.display='block'; };
+ if($('done-btn')) $('done-btn').onclick=()=>{ completeLesson(l.id); };
+ $('reader').querySelectorAll('[data-opt]').forEach(b=>b.onclick=()=>{
+ const ok = parseInt(b.dataset.opt,10) === l.quiz.answer;
+ $('quiz-msg').textContent = ok ? '✅ Correct (+5 XP)' : '❌ Not quite — re-read the lesson.';
+ $('quiz-msg').style.color = ok ? 'var(--green)' : 'var(--rose)';
+ if(ok) completeLesson(l.id, 5);
+ });
+ track('lesson_open', {lesson:l.id});
+ }catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Failed: '+esc(e.message)+'</p>'; }
+}
+function completeLesson(id, xp){
+ const p = prog(); if(p.completed[id]) return;
+ p.completed[id]=new Date().toISOString(); p.xp=(p.xp||0)+(xp||10); save(p); renderProgress(); renderChapters();
+ track('lesson_complete',{lesson:id});
+}
+function track(type, detail){
+ try{
+ if(localStorage.getItem('webx11_course_telemetry')==='off') return;
+ const sid = localStorage.getItem('webx11_course_sid') || (Math.random().toString(36).slice(2)+Date.now().toString(36));
+ localStorage.setItem('webx11_course_sid', sid);
+ fetch('/spaces/newos4/api/course/event.php',{method:'POST',headers:{'Content-Type':'application/json'},keepalive:true,
+ body:JSON.stringify({sid,type,detail,version:(MANIFEST&&MANIFEST.version)||''})}).catch(()=>{});
+ }catch(e){}
+}
+function setTelemetry(on){ localStorage.setItem('webx11_course_telemetry', on?'on':'off'); }
+function exportProgress(){
+ const blob = new Blob([JSON.stringify(prog(), null, 2)], {type:'application/json'});
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'newos4-course-progress.json'; a.click();
+}
+function tutorContext(){
+ if(!ACTIVE) return 'No lesson selected.';
+ const l = ACTIVE.l;
+ return ['LESSON: '+l.title,'OBJECTIVE: '+l.objective,'COMMANDS: '+(l.commands||[]).join(', ')]
+ .concat((l.blocks||[]).map(b=>b.type.toUpperCase()+': '+b.text))
+ .concat((l.exercise?['EXERCISE: '+l.exercise.instruction+' | HINT: '+l.exercise.hint]:[])).join('\n').slice(0,6000);
+}
+async function askTutor(){
+ const q = $('tutor-q').value.trim(); if(!q) return;
+ const log = $('tutor-log'); log.innerHTML += '<div style="margin:6px 0;color:var(--text)"><b>You:</b> '+esc(q)+'</div>';
+ $('tutor-q').value='';
+ const tok = localStorage.getItem('webx11_session_id') || '';
+ const headers = {'Content-Type':'application/json'};
+ if(tok) headers.Authorization = ['Bearer', tok].join(' ');
+ const sys = 'You are the newOS4 course tutor. Answer in <=120 words using only the lesson context. If not covered, say so and point to `help <cmd>` — never invent commands.\n\n'+tutorContext();
+ try{
+ const r = await fetch('/ai/v1/chat/completions',{method:'POST',headers,
+ body:JSON.stringify({model:'gemma4:31b-cloud',stream:false,messages:[{role:'system',content:sys},{role:'user',content:q}]})});
+ if(!r.ok) throw new Error('HTTP '+r.status+(r.status===401?' — log in first':''));
+ const d = await r.json();
+ const a = (d.choices&&d.choices[0]&&d.choices[0].message&&d.choices[0].message.content)||'(empty)';
+ log.innerHTML += '<div style="margin:6px 0;color:var(--cyan)"><b>Tutor:</b> '+esc(a)+'</div>';
+ track('question',{q:q.slice(0,240),lesson:ACTIVE?ACTIVE.l.id:null});
+ }catch(e){ log.innerHTML += '<div style="margin:6px 0;color:var(--rose)">'+esc(e.message)+'</div>'; }
+ log.scrollTop = log.scrollHeight;
+}
+async function showProjects(){
+ $('reader').innerHTML = '<h2>🛠 Example projects</h2>';
+ for(const id of MANIFEST.projects||[]){
+ const pr = await fetch(BASE+'projects/'+id+'.json?v='+V).then(r=>r.json()).catch(()=>null); if(!pr) continue;
+ const div = document.createElement('div'); div.className='exercise'; div.style.marginTop='12px';
+ div.innerHTML = '<strong>'+esc(pr.title)+'</strong> <span class="chip">'+esc(pr.level||'beginner')+'</span> <span class="chip">'+pr.minutes+' min</span>'
+ + '<p class="small" style="margin:6px 0">'+esc(pr.goal)+'</p>'
+ + (pr.steps||[]).map((s,i)=>'<div class="block" style="margin:4px 0">'+(i+1)+'. '+esc(s.instruction)+(s.command?' <code style="color:var(--green)">'+esc(s.command)+'</code>':'')+'</div>').join('');
+ $('reader').appendChild(div);
+ track('project_view',{project:id});
+ }
+}
+async function showFeed(){
+ $('reader').innerHTML = '<h2>📰 Daily lessons</h2><p class="small">AI-expanded micro-lessons, newest first.</p>';
+ const f = await fetch(BASE+'feed.json?v='+V).then(r=>r.json()).catch(()=>({items:[]}));
+ if(!(f.items||[]).length) $('reader').innerHTML += '<p class="small" style="margin-top:10px">No daily lessons yet — the generator adds one at the next 05:15 run.</p>';
+ for(const it of f.items||[]){
+ const d = document.createElement('div'); d.className='exercise'; d.style.marginTop='12px';
+ d.innerHTML = '<strong>'+esc(it.title)+'</strong> <span class="chip">'+esc(it.date)+'</span>'
+ + (it.blocks||[]).map(b=> b.type==='cmd' ? '<div class="block cmd">$ '+esc(b.text)+'</div>' : '<p class="block">'+esc(b.text)+'</p>').join('')
+ + (it.sources&&it.sources.length ? '<p class="small">Sources: '+it.sources.map(s=>'<a href="'+esc(s)+'" target="_blank" rel="noopener">'+esc(s.replace(/^https?:\/\//,'').slice(0,40))+'</a>').join(' · ')+'</p>' : '');
+ $('reader').appendChild(d);
+ }
+}
+$('atlas-q').addEventListener('input', e => renderAtlas(e.target.value.trim()));
+$('tutor-send').onclick = askTutor;
+$('tutor-q').addEventListener('keydown', e => { if(e.key==='Enter') askTutor(); });
+boot();
+</script>
+</body>
+</html>