diff options
Diffstat (limited to 'course.html')
| -rw-r--r-- | course.html | 252 |
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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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> |