Spaces:
Running
Running
Download terminal.js from IntelligenceLab/AI-for-Science: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/spaces/IntelligenceLab/AI-for-Science/resolve/main/terminal.js
- Command line
-
hf download hf://spaces/IntelligenceLab/AI-for-Science/terminal.js
-
curl -L -o terminal.js https://huggingface.co/spaces/IntelligenceLab/AI-for-Science/resolve/main/terminal.js
13.5 kB
| (() => { | |
| 'use strict'; | |
| const $ = (sel, root = document) => root.querySelector(sel); | |
| const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); | |
| const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
| const source = $('#terminal-data'); | |
| if (!source) return; | |
| let DATA; | |
| try { | |
| DATA = JSON.parse(source.textContent); | |
| } catch (err) { | |
| return; | |
| } | |
| const contactHref = (subject) => { | |
| const link = $('[data-contact]'); | |
| const href = link ? link.getAttribute('href') || '' : ''; | |
| const address = href.startsWith('mailto:') ? href.slice(7).split('?')[0] : ''; | |
| return address ? `mailto:${address}?subject=${encodeURIComponent(subject)}` : '#contact'; | |
| }; | |
| const VERIFY = [ | |
| ['tx-sys', 'Running verifier in a fresh sandbox…'], | |
| ['tx-ok', '✓ All checks passed'], | |
| ]; | |
| class Replayer { | |
| constructor(screen, lines) { | |
| this.screen = screen; | |
| this.code = $('code', screen) || screen; | |
| this.lines = lines; | |
| this.timer = 0; | |
| this.running = false; | |
| this.caret = document.createElement('i'); | |
| this.caret.className = 'tx-caret'; | |
| this.reset(); | |
| } | |
| reset() { | |
| this.code.replaceChildren(); | |
| this.line = 0; | |
| this.char = 0; | |
| this.phase = 'type'; | |
| this.verified = 0; | |
| this.text = null; | |
| } | |
| prompt(symbol) { | |
| const span = document.createElement('span'); | |
| span.className = 'tx-cmd'; | |
| const b = document.createElement('b'); | |
| b.textContent = symbol; | |
| const text = document.createTextNode(''); | |
| span.append(b, ' ', text); | |
| this.code.append(span); | |
| return { span, text }; | |
| } | |
| status(cls, message) { | |
| const span = document.createElement('span'); | |
| span.className = cls; | |
| span.textContent = message; | |
| this.code.append(span); | |
| } | |
| renderStatic() { | |
| this.stop(); | |
| this.reset(); | |
| this.lines.forEach(([symbol, line]) => { | |
| this.prompt(symbol).text.data = line; | |
| }); | |
| VERIFY.forEach(([cls, message]) => this.status(cls, message)); | |
| } | |
| start(delay = 0) { | |
| if (this.running) return; | |
| this.running = true; | |
| this.schedule(delay); | |
| } | |
| stop() { | |
| this.running = false; | |
| clearTimeout(this.timer); | |
| } | |
| schedule(ms) { | |
| clearTimeout(this.timer); | |
| if (this.running) this.timer = setTimeout(() => this.tick(), ms); | |
| } | |
| scroll() { | |
| this.screen.scrollTop = this.screen.scrollHeight; | |
| } | |
| tick() { | |
| if (!this.running) return; | |
| if (this.phase === 'type') { | |
| if (this.line >= this.lines.length) { | |
| this.caret.remove(); | |
| this.phase = 'verify'; | |
| this.schedule(420); | |
| return; | |
| } | |
| const [symbol, line] = this.lines[this.line]; | |
| if (!this.text) { | |
| const { span, text } = this.prompt(symbol); | |
| span.append(this.caret); | |
| this.text = text; | |
| this.scroll(); | |
| this.schedule(160); | |
| return; | |
| } | |
| this.char = Math.min(line.length, this.char + Math.max(1, Math.ceil(line.length / 40))); | |
| this.text.data = line.slice(0, this.char); | |
| if (this.char >= line.length) { | |
| this.line += 1; | |
| this.char = 0; | |
| this.text = null; | |
| this.scroll(); | |
| this.schedule(300); | |
| return; | |
| } | |
| this.schedule(22); | |
| return; | |
| } | |
| if (this.phase === 'verify') { | |
| if (this.verified >= VERIFY.length) { | |
| this.prompt('$').span.append(this.caret); | |
| this.scroll(); | |
| this.phase = 'hold'; | |
| this.schedule(3200); | |
| return; | |
| } | |
| const [cls, message] = VERIFY[this.verified]; | |
| this.verified += 1; | |
| this.status(cls, message); | |
| this.scroll(); | |
| this.schedule(this.verified === 1 ? 1000 : 200); | |
| return; | |
| } | |
| this.reset(); | |
| this.schedule(260); | |
| } | |
| } | |
| function initCards(tasks) { | |
| const grid = $('#tx-grid'); | |
| if (!grid) return; | |
| const cards = $$('.tx-card', grid); | |
| const players = cards.map((card) => { | |
| const task = tasks[Number(card.dataset.index)]; | |
| return task ? new Replayer($('.tx-screen', card), task.replay) : null; | |
| }); | |
| if (reduceMotion || !('IntersectionObserver' in window)) { | |
| players.forEach((p) => p && p.renderStatic()); | |
| } else { | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach((entry) => { | |
| const i = cards.indexOf(entry.target); | |
| const player = players[i]; | |
| if (!player) return; | |
| if (entry.isIntersecting) { | |
| player.start(player.started ? 0 : (i % 3) * 450); | |
| player.started = true; | |
| } else { | |
| player.stop(); | |
| } | |
| }); | |
| }, { threshold: 0.25 }); | |
| cards.forEach((card) => observer.observe(card)); | |
| } | |
| const more = $('#tx-more'); | |
| const extras = cards.filter((card) => card.classList.contains('is-extra')); | |
| if (more) { | |
| if (!extras.length) more.hidden = true; | |
| more.textContent = `Show ${extras.length} more terminal tasks`; | |
| more.addEventListener('click', () => { | |
| grid.classList.add('show-all'); | |
| more.setAttribute('aria-expanded', 'true'); | |
| if (extras[0]) extras[0].focus({ preventScroll: true }); | |
| }); | |
| } | |
| return cards; | |
| } | |
| function initModal(tasks, cards) { | |
| const modal = $('#tx-modal'); | |
| if (!modal || !cards) return; | |
| const sheet = $('.ex-sheet', modal); | |
| const closeButton = $('.icon-btn[data-tx-close]', modal); | |
| const field = (id) => document.getElementById(id); | |
| const f = { | |
| area: field('txm-area'), | |
| title: field('txm-title'), | |
| tags: field('txm-tags'), | |
| instruction: field('txm-instruction'), | |
| base: field('txm-base'), | |
| termBase: field('txm-term-base'), | |
| lines: field('txm-lines'), | |
| replay: field('txm-replay'), | |
| code: field('txm-code'), | |
| cta: field('txm-cta'), | |
| }; | |
| let current = 0; | |
| let lastFocus = null; | |
| let player = null; | |
| const play = () => { | |
| if (player) player.stop(); | |
| player = new Replayer(f.replay, tasks[current].replay); | |
| if (reduceMotion) player.renderStatic(); | |
| else player.start(250); | |
| }; | |
| const render = (index) => { | |
| current = (index + tasks.length) % tasks.length; | |
| const t = tasks[current]; | |
| f.area.textContent = t.area; | |
| f.title.textContent = t.title; | |
| f.tags.replaceChildren(...t.tags.map((tag) => { | |
| const li = document.createElement('li'); | |
| li.className = 'trk trk-mod'; | |
| li.textContent = tag; | |
| return li; | |
| })); | |
| f.instruction.innerHTML = t.instruction; | |
| f.base.textContent = t.base; | |
| f.termBase.textContent = t.base; | |
| f.lines.textContent = `${t.lines}-line solve.sh`; | |
| f.code.textContent = t.code; | |
| f.code.scrollTop = 0; | |
| f.cta.href = contactHref(`${modal.dataset.subject || 'Terminal task sample'}: ${t.title}`); | |
| sheet.scrollTop = 0; | |
| if (!modal.hidden) play(); | |
| }; | |
| const open = (index) => { | |
| lastFocus = document.activeElement; | |
| modal.hidden = false; | |
| render(index); | |
| document.body.classList.add('modal-open'); | |
| closeButton.focus(); | |
| }; | |
| const close = () => { | |
| modal.hidden = true; | |
| if (player) player.stop(); | |
| document.body.classList.remove('modal-open'); | |
| if (lastFocus) lastFocus.focus({ preventScroll: true }); | |
| }; | |
| cards.forEach((card) => { | |
| const index = Number(card.dataset.index); | |
| card.addEventListener('click', () => open(index)); | |
| card.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| open(index); | |
| } | |
| }); | |
| }); | |
| $$('[data-tx-close]', modal).forEach((el) => el.addEventListener('click', close)); | |
| $('[data-tx-prev]', modal).addEventListener('click', () => render(current - 1)); | |
| $('[data-tx-next]', modal).addEventListener('click', () => render(current + 1)); | |
| document.addEventListener('keydown', (e) => { | |
| if (modal.hidden) return; | |
| if (e.key === 'Escape') { | |
| e.preventDefault(); | |
| close(); | |
| } else if (e.key === 'ArrowRight') { | |
| render(current + 1); | |
| } else if (e.key === 'ArrowLeft') { | |
| render(current - 1); | |
| } else if (e.key === 'Tab') { | |
| const focusables = $$('button, a[href]', sheet); | |
| const first = focusables[0]; | |
| const last = focusables[focusables.length - 1]; | |
| if (e.shiftKey && document.activeElement === first) { | |
| e.preventDefault(); | |
| last.focus(); | |
| } else if (!e.shiftKey && document.activeElement === last) { | |
| e.preventDefault(); | |
| first.focus(); | |
| } | |
| } | |
| }); | |
| } | |
| function initGrowth(growth) { | |
| const root = $('#growth'); | |
| if (!root || !growth || !growth.stages || !growth.stages.length) return; | |
| const stages = growth.stages; | |
| const steps = $('#growth-steps'); | |
| const instruction = $('#growth-instruction'); | |
| const linesEl = $('#growth-lines'); | |
| const wordsEl = $('#growth-words'); | |
| const linesBar = $('#growth-lines-bar'); | |
| const wordsBar = $('#growth-words-bar'); | |
| const spark = $('#growth-spark'); | |
| const maxLines = Math.max(...stages.map((s) => s.lines)); | |
| const maxWords = Math.max(...stages.map((s) => s.words)); | |
| const svgNS = 'http://www.w3.org/2000/svg'; | |
| const W = 240; | |
| const H = 72; | |
| const P = 6; | |
| const xs = stages.map((_, i) => P + (i * (W - 2 * P)) / Math.max(1, stages.length - 1)); | |
| const ys = stages.map((s) => H - P - ((H - 2 * P) * s.lines) / maxLines); | |
| const pts = xs.map((x, i) => `${x.toFixed(1)},${ys[i].toFixed(1)}`).join(' '); | |
| const make = (tag, attrs) => { | |
| const node = document.createElementNS(svgNS, tag); | |
| Object.entries(attrs).forEach(([k, v]) => node.setAttribute(k, v)); | |
| spark.append(node); | |
| return node; | |
| }; | |
| spark.replaceChildren(); | |
| make('polygon', { class: 'gs-area', points: `${P},${H - P} ${pts} ${W - P},${H - P}` }); | |
| make('polyline', { class: 'gs-line', points: pts }); | |
| const guide = make('line', { class: 'gs-guide', x1: 0, x2: 0, y1: 2, y2: H - 2 }); | |
| const dots = xs.map((x, i) => make('circle', { class: 'gs-dot', cx: x.toFixed(1), cy: ys[i].toFixed(1), r: 3 })); | |
| let index = 0; | |
| let shown = { lines: 0, words: 0 }; | |
| let tween = 0; | |
| const countTo = (target) => { | |
| cancelAnimationFrame(tween); | |
| const from = { ...shown }; | |
| const t0 = performance.now(); | |
| const step = (now) => { | |
| const k = reduceMotion ? 1 : Math.min(1, (now - t0) / 600); | |
| const e = 1 - (1 - k) ** 3; | |
| shown = { | |
| lines: Math.round(from.lines + (target.lines - from.lines) * e), | |
| words: Math.round(from.words + (target.words - from.words) * e), | |
| }; | |
| linesEl.textContent = `${shown.lines} lines`; | |
| wordsEl.textContent = `${shown.words} words`; | |
| if (k < 1) tween = requestAnimationFrame(step); | |
| }; | |
| tween = requestAnimationFrame(step); | |
| }; | |
| const buttons = stages.map((_, i) => { | |
| const b = document.createElement('button'); | |
| b.type = 'button'; | |
| b.className = 'gstep'; | |
| b.setAttribute('aria-label', `Stage ${i + 1} of ${stages.length}`); | |
| b.innerHTML = `<span>Stage</span><strong>${i + 1}</strong>`; | |
| b.addEventListener('click', () => { | |
| setAuto(false); | |
| show(i); | |
| }); | |
| return b; | |
| }); | |
| const toggle = document.createElement('button'); | |
| toggle.type = 'button'; | |
| toggle.className = 'gplay'; | |
| steps.replaceChildren(...buttons, toggle); | |
| const show = (i) => { | |
| index = i; | |
| const stage = stages[i]; | |
| buttons.forEach((b, k) => { | |
| b.setAttribute('aria-pressed', String(k === i)); | |
| b.classList.toggle('is-done', k < i); | |
| }); | |
| const text = document.createElement('p'); | |
| text.className = 'g-text'; | |
| text.innerHTML = stage.segments.map((s) => (s.new ? `<mark>${s.html}</mark>` : s.html)).join(' '); | |
| instruction.replaceChildren(text); | |
| linesBar.style.width = `${((100 * stage.lines) / maxLines).toFixed(1)}%`; | |
| wordsBar.style.width = `${((100 * stage.words) / maxWords).toFixed(1)}%`; | |
| countTo(stage); | |
| dots.forEach((d, k) => { | |
| d.classList.toggle('is-on', k <= i); | |
| d.setAttribute('r', k === i ? 5 : 3); | |
| }); | |
| guide.style.transform = `translateX(${xs[i].toFixed(1)}px)`; | |
| }; | |
| let auto = !reduceMotion; | |
| let visible = false; | |
| let timer = 0; | |
| const schedule = () => { | |
| clearTimeout(timer); | |
| if (!auto || !visible) return; | |
| timer = setTimeout(() => { | |
| show(index === stages.length - 1 ? 0 : index + 1); | |
| schedule(); | |
| }, index === stages.length - 1 ? 6000 : 4200); | |
| }; | |
| const setAuto = (on) => { | |
| auto = on; | |
| toggle.textContent = on ? 'Pause' : 'Play'; | |
| toggle.setAttribute('aria-label', on ? 'Pause stage autoplay' : 'Play stages automatically'); | |
| schedule(); | |
| }; | |
| toggle.addEventListener('click', () => setAuto(!auto)); | |
| show(0); | |
| setAuto(auto); | |
| if ('IntersectionObserver' in window) { | |
| new IntersectionObserver((entries) => { | |
| visible = entries[0].isIntersecting; | |
| schedule(); | |
| }, { threshold: 0.35 }).observe(root); | |
| } | |
| } | |
| const tasks = DATA.tasks || []; | |
| const cards = initCards(tasks); | |
| initModal(tasks, cards); | |
| initGrowth(DATA.growth); | |
| })(); | |