Adive01's picture
Upload frontend/app.js with huggingface_hub
a833b9d verified
/* ════════════════════════════════════════════════════════════════════
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)';
});