Spaces:
Running
Running
Download app.js from IntelligenceLab/AI-for-Science: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/spaces/IntelligenceLab/AI-for-Science/resolve/main/app.js
- Command line
-
hf download hf://spaces/IntelligenceLab/AI-for-Science/app.js
-
curl -L -o app.js https://huggingface.co/spaces/IntelligenceLab/AI-for-Science/resolve/main/app.js
19.1 kB
| (() => { | |
| 'use strict'; | |
| // Every message from the interest form and email links goes to all of these addresses. | |
| const CONTACT_EMAILS = ['zli12321@terpmail.umd.edu', 'zl22754@uga.edu']; | |
| const CONTACT_TEXT = CONTACT_EMAILS.join(' and '); | |
| 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 mailto = (subject, body) => { | |
| const params = [`subject=${encodeURIComponent(subject)}`]; | |
| if (body) params.push(`body=${encodeURIComponent(body)}`); | |
| return `mailto:${CONTACT_EMAILS.join(',')}?${params.join('&')}`; | |
| }; | |
| function initContact() { | |
| $$('[data-contact]').forEach((link) => { | |
| link.href = mailto(link.dataset.subject || 'AI for Science collaboration'); | |
| if (link.hasAttribute('data-show-address')) link.textContent = CONTACT_TEXT; | |
| }); | |
| $$('[data-year]').forEach((el) => { | |
| el.textContent = String(new Date().getFullYear()); | |
| }); | |
| } | |
| function initNav() { | |
| const nav = $('.nav'); | |
| const onScroll = () => nav.classList.toggle('is-scrolled', window.scrollY > 8); | |
| onScroll(); | |
| window.addEventListener('scroll', onScroll, { passive: true }); | |
| const toggle = $('.nav-toggle'); | |
| const menu = $('#mobile-menu'); | |
| const setOpen = (open) => { | |
| toggle.setAttribute('aria-expanded', String(open)); | |
| toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); | |
| menu.hidden = !open; | |
| document.body.classList.toggle('menu-open', open); | |
| }; | |
| toggle.addEventListener('click', () => setOpen(toggle.getAttribute('aria-expanded') !== 'true')); | |
| $$('a', menu).forEach((a) => a.addEventListener('click', () => setOpen(false))); | |
| window.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') setOpen(false); | |
| }); | |
| window.matchMedia('(min-width: 1101px)').addEventListener('change', (e) => { | |
| if (e.matches) setOpen(false); | |
| }); | |
| if (!('IntersectionObserver' in window)) return; | |
| const links = new Map($$('.nav-links a').map((a) => [a.getAttribute('href').slice(1), a])); | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach((entry) => { | |
| if (!entry.isIntersecting) return; | |
| links.forEach((a) => a.classList.remove('is-active')); | |
| const active = links.get(entry.target.dataset.nav || entry.target.id); | |
| if (active) active.classList.add('is-active'); | |
| }); | |
| }, { rootMargin: '-45% 0px -50% 0px' }); | |
| $$('main > section[id]').forEach((section) => observer.observe(section)); | |
| } | |
| function initReveal() { | |
| const items = $$('.reveal'); | |
| if (reduceMotion || !('IntersectionObserver' in window)) { | |
| items.forEach((el) => el.classList.add('is-visible')); | |
| return; | |
| } | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach((entry) => { | |
| if (!entry.isIntersecting) return; | |
| entry.target.classList.add('is-visible'); | |
| observer.unobserve(entry.target); | |
| }); | |
| }, { rootMargin: '0px 0px -8% 0px', threshold: 0.12 }); | |
| items.forEach((el) => observer.observe(el)); | |
| } | |
| function initExplorer() { | |
| const root = $('#explorer'); | |
| if (!root) return; | |
| root.classList.add('is-enhanced'); | |
| const panels = $$('.explorer-panel', root); | |
| panels.forEach((panel) => { | |
| const stages = $$('.stage', panel); | |
| if (!stages.length) return; | |
| const tabs = document.createElement('div'); | |
| tabs.className = 'stage-tabs'; | |
| tabs.setAttribute('role', 'group'); | |
| tabs.setAttribute('aria-label', 'Project stages'); | |
| const buttons = stages.map((stage, i) => { | |
| const button = document.createElement('button'); | |
| button.type = 'button'; | |
| button.className = 'stage-btn'; | |
| const num = document.createElement('span'); | |
| num.className = 'sb-num'; | |
| num.textContent = String(i + 1); | |
| const name = document.createElement('span'); | |
| name.textContent = $('.stage-title', stage).textContent; | |
| button.append(num, name); | |
| return button; | |
| }); | |
| const select = (index) => { | |
| buttons.forEach((b, i) => b.setAttribute('aria-pressed', String(i === index))); | |
| stages.forEach((s, i) => { | |
| s.hidden = i !== index; | |
| }); | |
| }; | |
| buttons.forEach((b, i) => b.addEventListener('click', () => select(i))); | |
| tabs.append(...buttons); | |
| panel.insertBefore(tabs, stages[0]); | |
| select(0); | |
| }); | |
| const toggles = $$('.ft-btn', root); | |
| const setField = (field) => { | |
| root.dataset.field = field; | |
| toggles.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.field === field))); | |
| panels.forEach((p) => { | |
| p.hidden = p.dataset.field !== field; | |
| }); | |
| }; | |
| toggles.forEach((b) => b.addEventListener('click', () => setField(b.dataset.field))); | |
| setField('bio'); | |
| } | |
| function initExamples() { | |
| const grid = $('#ex-grid'); | |
| const modal = $('#ex-modal'); | |
| const source = $('#examples-data'); | |
| if (!grid || !modal || !source) return; | |
| let examples; | |
| try { | |
| examples = JSON.parse(source.textContent); | |
| } catch (err) { | |
| return; | |
| } | |
| const cards = $$('.ex-card', grid); | |
| const more = $('#ex-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 demos`; | |
| more.addEventListener('click', () => { | |
| grid.classList.add('show-all'); | |
| more.setAttribute('aria-expanded', 'true'); | |
| if (extras[0]) extras[0].focus({ preventScroll: true }); | |
| }); | |
| } | |
| const sheet = $('.ex-sheet', modal); | |
| const closeButton = $('.icon-btn[data-ex-close]', modal); | |
| const field = (id) => document.getElementById(id); | |
| const fields = { | |
| domain: field('exm-domain'), | |
| title: field('exm-title'), | |
| tracks: field('exm-tracks'), | |
| img: field('exm-img'), | |
| desc: field('exm-desc'), | |
| why: field('exm-why'), | |
| software: field('exm-software'), | |
| artifacts: field('exm-artifacts'), | |
| axes: field('exm-axes'), | |
| outputs: field('exm-outputs'), | |
| cta: field('exm-cta'), | |
| live: field('exm-live'), | |
| }; | |
| let current = 0; | |
| let lastFocus = null; | |
| let liveView = null; | |
| const mountLive = (ex) => { | |
| if (liveView) { | |
| liveView.dispose(); | |
| liveView = null; | |
| } | |
| const live = window.ILLive; | |
| if (!fields.live || !live || !live.ready) { | |
| if (fields.live) fields.live.hidden = true; | |
| return; | |
| } | |
| fields.live.hidden = false; | |
| fields.live.style.setProperty('--dc', ex.color); | |
| liveView = live.mount(fields.live, ex.kind, { color: ex.color, hero: true }); | |
| if (!liveView) fields.live.hidden = true; | |
| }; | |
| const listItems = (ul, items, cls) => { | |
| ul.replaceChildren(...items.map((text) => { | |
| const li = document.createElement('li'); | |
| li.textContent = text; | |
| if (cls) li.className = cls; | |
| return li; | |
| })); | |
| }; | |
| const render = (index) => { | |
| current = (index + examples.length) % examples.length; | |
| const ex = examples[current]; | |
| sheet.style.setProperty('--dc', ex.color); | |
| fields.domain.textContent = ex.domain; | |
| fields.title.textContent = ex.title; | |
| listItems(fields.tracks, ex.tags, 'trk trk-rel'); | |
| fields.img.src = ex.img; | |
| fields.img.alt = ex.alt; | |
| fields.desc.textContent = ex.description; | |
| const lead = document.createElement('strong'); | |
| lead.textContent = 'In your research: '; | |
| fields.why.replaceChildren(lead, document.createTextNode(ex.why)); | |
| listItems(fields.software, ex.software); | |
| listItems(fields.artifacts, ex.artifacts); | |
| listItems(fields.axes, ex.axes); | |
| listItems(fields.outputs, ex.outputs); | |
| sheet.scrollTop = 0; | |
| if (!modal.hidden) mountLive(ex); | |
| }; | |
| const open = (index) => { | |
| lastFocus = document.activeElement; | |
| render(index); | |
| modal.hidden = false; | |
| mountLive(examples[current]); | |
| document.body.classList.add('modal-open'); | |
| closeButton.focus(); | |
| }; | |
| const close = () => { | |
| modal.hidden = true; | |
| if (liveView) { | |
| liveView.dispose(); | |
| liveView = null; | |
| } | |
| document.body.classList.remove('modal-open'); | |
| if (lastFocus) lastFocus.focus({ preventScroll: true }); | |
| }; | |
| document.addEventListener('il-live-ready', () => { | |
| if (!modal.hidden) mountLive(examples[current]); | |
| }); | |
| fields.cta.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const form = $('#join-form'); | |
| if (form) form.elements.namedItem('interest').value = examples[current].title; | |
| close(); | |
| $('#join').scrollIntoView({ behavior: reduceMotion ? 'auto' : 'smooth' }); | |
| const name = form && form.elements.namedItem('name'); | |
| if (name) setTimeout(() => name.focus({ preventScroll: true }), reduceMotion ? 0 : 600); | |
| }); | |
| 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-ex-close]', modal).forEach((el) => el.addEventListener('click', close)); | |
| $('[data-ex-prev]', modal).addEventListener('click', () => render(current - 1)); | |
| $('[data-ex-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 initForm() { | |
| const form = $('#join-form'); | |
| if (!form) return; | |
| const note = $('#form-note'); | |
| const nameInput = form.elements.namedItem('name'); | |
| nameInput.addEventListener('input', () => nameInput.removeAttribute('aria-invalid')); | |
| form.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const data = new FormData(form); | |
| const value = (key) => String(data.get(key) || '').trim(); | |
| const name = value('name'); | |
| if (!name) { | |
| nameInput.setAttribute('aria-invalid', 'true'); | |
| note.textContent = 'Please add your name so we know who to reply to.'; | |
| nameInput.focus(); | |
| return; | |
| } | |
| const field = value('field') || 'Not specified'; | |
| const lines = [ | |
| `Name: ${name}`, | |
| `University & program: ${value('program') || '-'}`, | |
| `Field: ${field}`, | |
| `Research: ${value('topic') || '-'}`, | |
| '', | |
| 'How I use AI in my research today:', | |
| value('ai_use') || '-', | |
| '', | |
| `How I'd like to take part: ${data.getAll('involve').join(', ') || 'Not sure yet'}`, | |
| ]; | |
| if (value('interest')) lines.push(`Demo that caught my eye: ${value('interest')}`); | |
| const link = document.createElement('a'); | |
| link.href = mailto(`AI for Science collaboration: ${name} (${field})`, lines.join('\r\n')); | |
| link.hidden = true; | |
| document.body.append(link); | |
| link.click(); | |
| link.remove(); | |
| note.textContent = `Your email app should open with your message ready to send. If it doesn't, write to ${CONTACT_TEXT}.`; | |
| }); | |
| } | |
| function initMorph() { | |
| const canvas = $('#morph'); | |
| if (!canvas || !canvas.getContext) return; | |
| const ctx = canvas.getContext('2d'); | |
| const labels = $$('.morph-labels .ml'); | |
| // Rock-salt lattice: 5 x 5 x 5 sites, two alternating species. | |
| const SPACING = 0.42; | |
| const lattice = []; | |
| for (let i = 0; i < 5; i += 1) { | |
| for (let j = 0; j < 5; j += 1) { | |
| for (let k = 0; k < 5; k += 1) { | |
| lattice.push({ x: (i - 2) * SPACING, y: (j - 2) * SPACING, z: (k - 2) * SPACING, c: (i + j + k) % 2, r: 3.3 }); | |
| } | |
| } | |
| } | |
| const latticeEdges = []; | |
| for (let a = 0; a < lattice.length; a += 1) { | |
| for (let b = a + 1; b < lattice.length; b += 1) { | |
| const p = lattice[a]; | |
| const q = lattice[b]; | |
| const d = Math.abs(p.x - q.x) + Math.abs(p.y - q.y) + Math.abs(p.z - q.z); | |
| if (Math.abs(d - SPACING) < 1e-6) latticeEdges.push([a, b]); | |
| } | |
| } | |
| // Double helix: two backbones of 50 points plus 25 base pairs on the axis. | |
| const PER_STRAND = 50; | |
| const helix = []; | |
| const helixEdges = []; | |
| for (let s = 0; s < 2; s += 1) { | |
| for (let i = 0; i < PER_STRAND; i += 1) { | |
| const t = i / (PER_STRAND - 1); | |
| const angle = t * 2.1 * Math.PI * 2 + s * Math.PI; | |
| helix.push({ x: 0.6 * Math.cos(angle), y: (t - 0.5) * 2.1, z: 0.6 * Math.sin(angle), c: 2 + s, r: 3.4 }); | |
| if (i > 0) helixEdges.push([s * PER_STRAND + i - 1, s * PER_STRAND + i]); | |
| } | |
| } | |
| for (let i = 0; i < PER_STRAND; i += 2) { | |
| const a = helix[i]; | |
| const b = helix[PER_STRAND + i]; | |
| const mid = helix.length; | |
| helix.push({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, z: (a.z + b.z) / 2, c: 4, r: 2.3 }); | |
| helixEdges.push([i, mid], [mid, PER_STRAND + i]); | |
| } | |
| // Pair points by height so the morph flows within horizontal bands. | |
| const byHeight = (pts) => pts.map((_, i) => i).sort((a, b) => | |
| (pts[a].y - pts[b].y) || (Math.atan2(pts[a].z, pts[a].x) - Math.atan2(pts[b].z, pts[b].x))); | |
| const orderA = byHeight(lattice); | |
| const orderB = byHeight(helix); | |
| const slotA = []; | |
| const slotB = []; | |
| orderA.forEach((orig, k) => { slotA[orig] = k; }); | |
| orderB.forEach((orig, k) => { slotB[orig] = k; }); | |
| const points = orderA.map((orig, k) => ({ a: lattice[orig], b: helix[orderB[k]] })); | |
| const edgesA = latticeEdges.map(([p, q]) => [slotA[p], slotA[q]]); | |
| const edgesB = helixEdges.map(([p, q]) => [slotB[p], slotB[q]]); | |
| const RGB = [[15, 159, 117], [79, 70, 229], [15, 159, 117], [20, 184, 166], [245, 158, 11]]; | |
| const projected = points.map(() => ({ sx: 0, sy: 0, z: 0, p: 1 })); | |
| const depthOrder = points.map((_, i) => i); | |
| const smooth = (x) => x * x * (3 - 2 * x); | |
| const morphAt = (ms) => { | |
| const s = (ms / 1000) % 12; | |
| if (s < 3.6) return 0; | |
| if (s < 6) return smooth((s - 3.6) / 2.4); | |
| if (s < 9.6) return 1; | |
| return 1 - smooth((s - 9.6) / 2.4); | |
| }; | |
| let w = 0; | |
| let h = 0; | |
| let dpr = 1; | |
| let shown = -1; | |
| const resize = () => { | |
| const rect = canvas.getBoundingClientRect(); | |
| dpr = Math.min(window.devicePixelRatio || 1, 2); | |
| w = rect.width; | |
| h = rect.height; | |
| canvas.width = Math.max(1, Math.round(w * dpr)); | |
| canvas.height = Math.max(1, Math.round(h * dpr)); | |
| }; | |
| const draw = (now) => { | |
| if (w < 8 || h < 8) return; | |
| const m = reduceMotion ? 0 : morphAt(now); | |
| const rot = reduceMotion ? 0.6 : now * 0.00022; | |
| const tilt = 0.32; | |
| const cosR = Math.cos(rot); | |
| const sinR = Math.sin(rot); | |
| const cosT = Math.cos(tilt); | |
| const sinT = Math.sin(tilt); | |
| const size = Math.min(w, h); | |
| const scale = size * 0.34; | |
| const cx = w / 2; | |
| const cy = h * 0.47; | |
| const camera = 3.6; | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| ctx.clearRect(0, 0, w, h); | |
| for (let k = 0; k < points.length; k += 1) { | |
| const { a, b } = points[k]; | |
| const x = a.x + (b.x - a.x) * m; | |
| const y = a.y + (b.y - a.y) * m; | |
| const z = a.z + (b.z - a.z) * m; | |
| const x1 = x * cosR + z * sinR; | |
| const z1 = -x * sinR + z * cosR; | |
| const y2 = y * cosT - z1 * sinT; | |
| const z2 = y * sinT + z1 * cosT; | |
| const persp = camera / (camera - z2); | |
| const pr = projected[k]; | |
| pr.sx = cx + x1 * scale * persp; | |
| pr.sy = cy - y2 * scale * persp; | |
| pr.z = z2; | |
| pr.p = persp; | |
| } | |
| const drawEdges = (edges, alpha, rgb) => { | |
| if (alpha < 0.02) return; | |
| ctx.lineWidth = 1.2; | |
| for (const [i, j] of edges) { | |
| const p = projected[i]; | |
| const q = projected[j]; | |
| const depth = Math.min(1, Math.max(0, ((p.z + q.z) / 2 + 1.2) / 2.4)); | |
| ctx.strokeStyle = `rgba(${rgb}, ${(alpha * (0.1 + 0.4 * depth)).toFixed(3)})`; | |
| ctx.beginPath(); | |
| ctx.moveTo(p.sx, p.sy); | |
| ctx.lineTo(q.sx, q.sy); | |
| ctx.stroke(); | |
| } | |
| }; | |
| drawEdges(edgesA, 1 - m, '79, 70, 229'); | |
| drawEdges(edgesB, m, '15, 159, 117'); | |
| depthOrder.sort((i, j) => projected[i].z - projected[j].z); | |
| const unit = size / 440; | |
| for (const k of depthOrder) { | |
| const pr = projected[k]; | |
| const { a, b } = points[k]; | |
| const ca = RGB[a.c]; | |
| const cb = RGB[b.c]; | |
| const depth = Math.min(1, Math.max(0, (pr.z + 1.2) / 2.4)); | |
| const radius = (a.r + (b.r - a.r) * m) * pr.p * unit; | |
| ctx.fillStyle = `rgba(${Math.round(ca[0] + (cb[0] - ca[0]) * m)}, ${Math.round(ca[1] + (cb[1] - ca[1]) * m)}, ${Math.round(ca[2] + (cb[2] - ca[2]) * m)}, ${(0.42 + 0.58 * depth).toFixed(3)})`; | |
| ctx.beginPath(); | |
| ctx.arc(pr.sx, pr.sy, radius, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| const shape = m >= 0.5 ? 1 : 0; | |
| if (shape !== shown) { | |
| shown = shape; | |
| labels.forEach((label) => label.classList.toggle('is-on', Number(label.dataset.shape) === shape)); | |
| } | |
| }; | |
| let raf = 0; | |
| let inView = true; | |
| const loop = (now) => { | |
| draw(now); | |
| raf = requestAnimationFrame(loop); | |
| }; | |
| const start = () => { | |
| if (!raf && inView && !document.hidden && !reduceMotion) raf = requestAnimationFrame(loop); | |
| }; | |
| const stop = () => { | |
| cancelAnimationFrame(raf); | |
| raf = 0; | |
| }; | |
| resize(); | |
| draw(performance.now()); | |
| if ('ResizeObserver' in window) { | |
| new ResizeObserver(() => { | |
| resize(); | |
| if (!raf) draw(performance.now()); | |
| }).observe(canvas); | |
| } | |
| if (reduceMotion) return; | |
| if ('IntersectionObserver' in window) { | |
| new IntersectionObserver((entries) => { | |
| inView = entries[0].isIntersecting; | |
| if (inView) start(); | |
| else stop(); | |
| }).observe(canvas); | |
| } | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) stop(); | |
| else start(); | |
| }); | |
| start(); | |
| } | |
| initContact(); | |
| initNav(); | |
| initReveal(); | |
| initExplorer(); | |
| initExamples(); | |
| initForm(); | |
| initMorph(); | |
| })(); | |