Spaces:
Running
Running
| /* ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PRISM STUDIO β APP.JS | |
| GSAP Β· ScrollTrigger Β· Lenis Β· SplitType Β· Magnetic Cursor | |
| ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 1. LENIS β smooth scroll init, synced to GSAP ticker | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const lenis = new Lenis({ | |
| duration: 1.4, | |
| easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)), | |
| orientation: 'vertical', | |
| smoothWheel: true, | |
| wheelMultiplier: 0.9, | |
| touchMultiplier: 1.5, | |
| }); | |
| gsap.registerPlugin(ScrollTrigger); | |
| lenis.on('scroll', ScrollTrigger.update); | |
| gsap.ticker.add(t => lenis.raf(t * 1000)); | |
| gsap.ticker.lagSmoothing(0); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 2. CUSTOM CURSOR β dot + trailing ring + magnetic elements | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const cursorDot = document.getElementById('cursor-dot'); | |
| const cursorRing = document.getElementById('cursor-ring'); | |
| let mouse = { x: 0, y: 0 }; | |
| let ring = { x: 0, y: 0 }; | |
| window.addEventListener('mousemove', e => { | |
| mouse.x = e.clientX; | |
| mouse.y = e.clientY; | |
| gsap.set(cursorDot, { x: mouse.x, y: mouse.y }); | |
| }); | |
| gsap.ticker.add(() => { | |
| const lerp = (a, b, n) => a + (b - a) * n; | |
| ring.x = lerp(ring.x, mouse.x, 0.12); | |
| ring.y = lerp(ring.y, mouse.y, 0.12); | |
| gsap.set(cursorRing, { x: ring.x, y: ring.y }); | |
| }); | |
| // Hover states | |
| document.querySelectorAll('button, a, select').forEach(el => { | |
| el.addEventListener('mouseenter', () => cursorRing.classList.add('hovered')); | |
| el.addEventListener('mouseleave', () => cursorRing.classList.remove('hovered')); | |
| }); | |
| document.querySelectorAll('textarea').forEach(el => { | |
| el.addEventListener('mouseenter', () => cursorRing.classList.add('text-hovered')); | |
| el.addEventListener('mouseleave', () => cursorRing.classList.remove('text-hovered')); | |
| }); | |
| // Magnetic physics | |
| function applyMagnetic(selector, strength = 0.35, restEase = 0.7) { | |
| document.querySelectorAll(selector).forEach(el => { | |
| el.addEventListener('mousemove', e => { | |
| const r = el.getBoundingClientRect(); | |
| const dx = e.clientX - (r.left + r.width / 2); | |
| const dy = e.clientY - (r.top + r.height / 2); | |
| gsap.to(el, { x: dx * strength, y: dy * strength, duration: 0.4, ease: 'power2.out' }); | |
| }); | |
| el.addEventListener('mouseleave', () => { | |
| gsap.to(el, { x: 0, y: 0, duration: restEase, ease: 'elastic.out(1, 0.4)' }); | |
| }); | |
| }); | |
| } | |
| applyMagnetic('.magnetic', 0.35, 0.75); | |
| applyMagnetic('.magnetic-sm', 0.2, 0.6); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 3. THEME TOGGLE | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const toggle = document.getElementById('theme-toggle'); | |
| let currentTheme = 'dark'; | |
| if (toggle) { | |
| toggle.addEventListener('click', e => { | |
| const opt = e.target.closest('.toggle-option'); | |
| if (!opt) return; | |
| const val = opt.dataset.val; | |
| currentTheme = val; | |
| document.documentElement.setAttribute('data-theme', val); | |
| toggle.setAttribute('data-active', val); | |
| toggle.querySelectorAll('.toggle-option').forEach(o => o.classList.toggle('active', o.dataset.val === val)); | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 4. HERO TEXT REVEAL β Line by line with SplitType | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // The hero lines are already wrapped in .line-mask / .line-inner in HTML. | |
| const lineInners = document.querySelectorAll('#hero-title .line-inner'); | |
| const tlHero = gsap.timeline({ delay: 0.3 }); | |
| tlHero | |
| .to(lineInners, { | |
| y: 0, | |
| duration: 1.1, | |
| stagger: 0.12, | |
| ease: 'expo.out', | |
| }) | |
| .to('#hero-sub', { | |
| opacity: 1, | |
| y: 0, | |
| duration: 1, | |
| ease: 'expo.out', | |
| }, '-=0.6') | |
| .from('.badge, .scroll-indicator, #btn-scroll-down', { | |
| opacity: 0, | |
| y: 15, | |
| duration: 0.8, | |
| stagger: 0.1, | |
| ease: 'expo.out', | |
| }, '-=0.9'); | |
| gsap.set('#hero-sub', { y: 30 }); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 5. MARQUEE β gsap infinite loop | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const marqueeTrack = document.getElementById('marquee-track'); | |
| if (marqueeTrack) { | |
| gsap.to(marqueeTrack, { | |
| x: '-=33.33%', | |
| ease: 'none', | |
| duration: 16, | |
| repeat: -1, | |
| modifiers: { | |
| x: gsap.utils.unitize(x => parseFloat(x) % (marqueeTrack.scrollWidth / 3)), | |
| }, | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 6. HORIZONTAL SCROLL β GSAP ScrollTrigger pinned section | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const studioTrack = document.getElementById('studio-track'); | |
| const panels = gsap.utils.toArray('.studio-panel'); | |
| if (studioTrack && panels.length) { | |
| // Calculate total horizontal scroll distance | |
| const getTotalWidth = () => { | |
| return -(studioTrack.scrollWidth - window.innerWidth); | |
| }; | |
| const horizontalTween = gsap.to(studioTrack, { | |
| x: getTotalWidth, | |
| ease: 'none', | |
| }); | |
| ScrollTrigger.create({ | |
| trigger: '#studio', | |
| start: 'top top', | |
| end: () => `+=${studioTrack.scrollWidth - window.innerWidth}`, | |
| pin: '#studio-pin-wrapper', | |
| animation: horizontalTween, | |
| scrub: 1.2, | |
| invalidateOnRefresh: true, | |
| onUpdate: self => { | |
| // Parallax the background text slightly | |
| const bgText = document.querySelector('.hero-bg-text'); | |
| if (bgText) { | |
| gsap.set(bgText, { x: -self.progress * 80 }); | |
| } | |
| } | |
| }); | |
| // Panel title reveals as they scroll into horizontal view | |
| panels.forEach((panel, i) => { | |
| const lineInners = panel.querySelectorAll('.line-inner'); | |
| gsap.set(lineInners, { y: '110%' }); | |
| ScrollTrigger.create({ | |
| trigger: panel, | |
| containerAnimation: horizontalTween, | |
| start: 'left 80%', | |
| onEnter: () => { | |
| gsap.to(lineInners, { | |
| y: 0, | |
| duration: 1, | |
| stagger: 0.1, | |
| ease: 'expo.out', | |
| }); | |
| }, | |
| }); | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 7. FOOTER TITLE REVEAL | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const footerTitle = document.getElementById('footer-title'); | |
| if (footerTitle) { | |
| const splitFooter = new SplitType(footerTitle, { types: 'chars' }); | |
| gsap.set(splitFooter.chars, { yPercent: 120, opacity: 0 }); | |
| ScrollTrigger.create({ | |
| trigger: footerTitle, | |
| start: 'top 85%', | |
| onEnter: () => { | |
| gsap.to(splitFooter.chars, { | |
| yPercent: 0, | |
| opacity: 1, | |
| duration: 0.9, | |
| stagger: 0.018, | |
| ease: 'back.out(1.8)', | |
| }); | |
| }, | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 8. SCROLL BUTTON β smooth scroll to studio | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const btnScrollDown = document.getElementById('btn-scroll-down'); | |
| if (btnScrollDown) { | |
| btnScrollDown.addEventListener('click', () => { | |
| lenis.scrollTo('#studio', { duration: 1.8, easing: t => Math.min(1, 1.001 - Math.pow(2, -10 * t)) }); | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 9. TOKEN COUNTER & ENGINE SELECTOR | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const sourceInput = document.getElementById('source-input'); | |
| const tokenCounter = document.getElementById('token-counter'); | |
| const engineSelect = document.getElementById('engine-select'); | |
| const presetRow = document.getElementById('preset-row'); | |
| const LIMITS = { bart: Infinity, gemini: 1_000_000 }; | |
| let MAX_TOKENS = LIMITS.bart; | |
| function formatLimit(n) { | |
| if (n === Infinity) return 'β'; | |
| return n >= 1000 ? n.toLocaleString() : n.toString(); | |
| } | |
| function updateCounter() { | |
| const text = sourceInput ? sourceInput.value.trim() : ''; | |
| const words = text ? text.split(/\s+/).filter(Boolean).length : 0; | |
| const est = Math.floor(words * 1.3); | |
| if (tokenCounter) { | |
| const isUnlimited = MAX_TOKENS === Infinity; | |
| if (isUnlimited) { | |
| // BART chunked β show count, no warning possible | |
| const chunks = Math.max(1, Math.ceil(est / 900)); | |
| tokenCounter.textContent = est === 0 | |
| ? '000 TOKENS' | |
| : chunks > 1 | |
| ? `${est.toLocaleString()} TOKENS Β· ${chunks} CHUNKS` | |
| : `${String(est).padStart(3, '0')} TOKENS`; | |
| tokenCounter.classList.remove('warn'); | |
| } else { | |
| tokenCounter.textContent = est > MAX_TOKENS | |
| ? `β ${est.toLocaleString()} / ${formatLimit(MAX_TOKENS)} β WILL TRUNCATE` | |
| : `${String(est).padStart(3, '0')} / ${formatLimit(MAX_TOKENS)} TOKENS`; | |
| tokenCounter.classList.toggle('warn', est > MAX_TOKENS); | |
| } | |
| } | |
| } | |
| if (engineSelect) { | |
| engineSelect.addEventListener('change', () => { | |
| const isGemini = engineSelect.value === 'gemini'; | |
| MAX_TOKENS = isGemini ? LIMITS.gemini : LIMITS.bart; | |
| if (presetRow) presetRow.style.display = isGemini ? 'none' : ''; | |
| updateCounter(); | |
| }); | |
| } | |
| if (sourceInput) sourceInput.addEventListener('input', updateCounter); | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 10. SUMMARIZATION API CALL | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const outputDisplay = document.getElementById('output-display'); | |
| const btnSummarize = document.getElementById('btn-summarize'); | |
| const btnClear = document.getElementById('btn-clear'); | |
| const btnPolish = document.getElementById('btn-polish'); | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| const presetSelect = document.getElementById('preset-select'); | |
| // Polish toggle state | |
| let polishEnabled = false; | |
| if (btnPolish) { | |
| btnPolish.addEventListener('click', () => { | |
| polishEnabled = !polishEnabled; | |
| btnPolish.textContent = polishEnabled ? 'β¦ GEMINI POLISH ON' : 'β¦ GEMINI POLISH OFF'; | |
| btnPolish.style.borderColor = polishEnabled ? 'var(--lime)' : ''; | |
| btnPolish.style.color = polishEnabled ? 'var(--lime)' : ''; | |
| }); | |
| } | |
| const PRESETS = { | |
| quick: { max_new_tokens: 64, min_new_tokens: 15, num_beams: 4, length_penalty: 0.6 }, | |
| notes: { max_new_tokens: 100, min_new_tokens: 25, num_beams: 5, length_penalty: 0.8 }, | |
| deep: { max_new_tokens: 160, min_new_tokens: 40, num_beams: 6, length_penalty: 1.0 }, | |
| }; | |
| if (btnClear) { | |
| btnClear.addEventListener('click', () => { | |
| if (sourceInput) sourceInput.value = ''; | |
| if (outputDisplay) outputDisplay.value = ''; | |
| if (tokenCounter) { tokenCounter.textContent = '000 TOKENS'; tokenCounter.classList.remove('warn'); } | |
| }); | |
| } | |
| if (btnSummarize) { | |
| btnSummarize.addEventListener('click', async () => { | |
| const text = sourceInput ? sourceInput.value.trim() : ''; | |
| if (!text) { alert('PLEASE PROVIDE A SOURCE DOCUMENT.'); return; } | |
| const engine = engineSelect ? engineSelect.value : 'bart'; | |
| const preset = PRESETS[presetSelect ? presetSelect.value : 'notes']; | |
| if (loadingOverlay) loadingOverlay.classList.add('active'); | |
| try { | |
| const body = engine === 'gemini' | |
| ? { text, engine: 'gemini', gemini_model: 'gemini-2.5-flash' } | |
| : { | |
| text, | |
| engine: 'bart', | |
| max_new_tokens: preset.max_new_tokens, | |
| min_new_tokens: preset.min_new_tokens, | |
| num_beams: preset.num_beams, | |
| length_penalty: preset.length_penalty, | |
| polish: polishEnabled, | |
| gemini_model: 'gemini-2.5-flash', | |
| }; | |
| const res = await fetch('/api/summarize', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(body), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json(); | |
| throw new Error(err.detail || 'API error'); | |
| } | |
| const data = await res.json(); | |
| if (outputDisplay) outputDisplay.value = data.summary; | |
| if (data.engine_used && data.engine_used.includes('polish failed')) { | |
| alert('Gemini Polish failed (likely due to API rate limits or quota). Displaying raw BART summary instead.'); | |
| } | |
| } catch (err) { | |
| console.error(err); | |
| alert('INFERENCE FAILURE: ' + err.message); | |
| } finally { | |
| if (loadingOverlay) loadingOverlay.classList.remove('active'); | |
| } | |
| }); | |
| } | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // 11. NAV HIDE ON SCROLL | |
| // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const nav = document.getElementById('nav'); | |
| let lastY = 0; | |
| lenis.on('scroll', ({ scroll }) => { | |
| if (!nav) return; | |
| nav.style.transform = scroll > lastY && scroll > 80 | |
| ? 'translateY(-100%)' | |
| : 'translateY(0)'; | |
| lastY = scroll; | |
| nav.style.transition = 'transform 0.5s cubic-bezier(0.19,1,0.22,1)'; | |
| }); | |