/** * jev8ball // Neural Decision Oracle Client * Pure vanilla ES6+, HTML5 Canvas, Web Audio API */ (function () { 'use strict'; // ─── AMBIENT PARTICLES (katdrop style) ─── const canvas = document.getElementById('particles'); const ctx = canvas.getContext('2d'); let particles = []; function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); class Particle { constructor() { this.reset(); } reset() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.size = Math.random() * 1.5 + 0.5; this.speedX = (Math.random() - 0.5) * 0.25; this.speedY = (Math.random() - 0.5) * 0.25; this.alpha = Math.random() * 0.35 + 0.1; } update() { this.x += this.speedX; this.y += this.speedY; if (this.x < 0 || this.x > canvas.width || this.y < 0 || this.y > canvas.height) { this.reset(); } } draw() { ctx.fillStyle = `rgba(255, 255, 255, ${this.alpha})`; ctx.fillRect(this.x, this.y, this.size, this.size); } } function initParticles(count = 45) { particles = Array.from({ length: count }, () => new Particle()); } initParticles(); function animateParticles() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (const p of particles) { p.update(); p.draw(); } requestAnimationFrame(animateParticles); } requestAnimationFrame(animateParticles); // ─── AUDIO SYNTHESIZER ─── class OracleAudio { constructor() { this.ctx = null; this.muted = localStorage.getItem('jev_muted') === 'true'; } init() { if (!this.ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (AC) this.ctx = new AC(); } if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); } toggleMute() { this.muted = !this.muted; localStorage.setItem('jev_muted', this.muted); return this.muted; } playTone(freq, type = 'sine', duration = 0.1, vol = 0.1) { if (this.muted) return; try { this.init(); if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime); gain.gain.setValueAtTime(vol, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + duration); } catch (e) {} } shakeSound() { if (this.muted) return; try { this.init(); if (!this.ctx) return; // Fluid swish pulse const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.setValueAtTime(90, this.ctx.currentTime); osc.frequency.linearRampToValueAtTime(160, this.ctx.currentTime + 0.2); osc.frequency.linearRampToValueAtTime(80, this.ctx.currentTime + 0.4); gain.gain.setValueAtTime(0.12, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.45); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.45); } catch (e) {} } revealChime(sentiment = 'affirmative') { if (this.muted) return; if (sentiment === 'affirmative') { this.playTone(440, 'triangle', 0.12, 0.15); setTimeout(() => this.playTone(554.37, 'triangle', 0.15, 0.15), 80); setTimeout(() => this.playTone(659.25, 'triangle', 0.25, 0.18), 160); } else if (sentiment === 'negative') { this.playTone(329.63, 'sawtooth', 0.14, 0.12); setTimeout(() => this.playTone(277.18, 'sawtooth', 0.22, 0.14), 100); } else { this.playTone(392.00, 'sine', 0.18, 0.14); setTimeout(() => this.playTone(440.00, 'sine', 0.22, 0.14), 110); } } } const audio = new OracleAudio(); // ─── DOM ELEMENTS ─── const ballWrapper = document.getElementById('ballWrapper'); const ballSphere = document.getElementById('ballSphere'); const floatingDie = document.getElementById('floatingDie'); const dieText = document.getElementById('dieText'); const askForm = document.getElementById('askForm'); const questionInput = document.getElementById('questionInput'); const submitBtn = document.getElementById('submitBtn'); const btnText = document.getElementById('btnText'); const btnSpinner = document.getElementById('btnSpinner'); const soundToggle = document.getElementById('soundToggle'); const backendStatus = document.getElementById('backendStatus'); const statusText = document.getElementById('statusText'); const telemetryCard = document.getElementById('telemetryCard'); const telemetryBadge = document.getElementById('telemetryBadge'); const tVerdict = document.getElementById('tVerdict'); const tNoul = document.getElementById('tNoul'); const noulFill = document.getElementById('noulFill'); const tConfidence = document.getElementById('tConfidence'); const tLatency = document.getElementById('tLatency'); const tCost = document.getElementById('tCost'); const jsonDump = document.getElementById('jsonDump'); const historyList = document.getElementById('historyList'); const historyCount = document.getElementById('historyCount'); const toast = document.getElementById('toast'); let isSubmitting = false; let historyData = []; // Load history from localStorage try { const saved = localStorage.getItem('jev_history'); if (saved) historyData = JSON.parse(saved); } catch (e) {} // ─── SOUND TOGGLE ─── function updateSoundUI() { soundToggle.textContent = audio.muted ? 'sound: off' : 'sound: on'; } updateSoundUI(); soundToggle.addEventListener('click', () => { audio.toggleMute(); updateSoundUI(); showToast(audio.muted ? 'Audio muted' : 'Audio enabled'); }); // ─── TOAST NOTIFICATION ─── let toastTimer = null; function showToast(msg) { clearTimeout(toastTimer); toast.textContent = msg; toast.classList.add('active'); toastTimer = setTimeout(() => { toast.classList.remove('active'); }, 2200); } // ─── 3D PARALLAX TILT ON HOVER ─── window.addEventListener('pointermove', (e) => { if (isSubmitting) return; const rect = ballWrapper.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const deltaX = (e.clientX - centerX) / (window.innerWidth / 2); const deltaY = (e.clientY - centerY) / (window.innerHeight / 2); const tiltX = Math.max(-14, Math.min(14, -deltaY * 16)); const tiltY = Math.max(-14, Math.min(14, deltaX * 16)); ballSphere.style.transform = `rotateX(${tiltX}deg) rotateY(${tiltY}deg)`; }); ballWrapper.addEventListener('mouseleave', () => { if (!isSubmitting) { ballSphere.style.transform = 'rotateX(0deg) rotateY(0deg)'; } }); // Clicking ball shakes it if input is non-empty, or prompts user ballWrapper.addEventListener('click', () => { if (isSubmitting) return; if (questionInput.value.trim()) { handleAsk(questionInput.value.trim()); } else { questionInput.focus(); showToast('Type a question to consult Jev'); } }); // ─── STATUS CHECK ─── async function checkStatus() { try { const res = await fetch('/api/status'); if (res.ok) { const data = await res.json(); backendStatus.classList.add('online'); statusText.textContent = data.model; } else { backendStatus.classList.remove('online'); statusText.textContent = 'status error'; } } catch (e) { backendStatus.classList.remove('online'); statusText.textContent = 'offline'; } } checkStatus(); backendStatus.addEventListener('click', () => { showToast('Checking Jev backend status...'); checkStatus(); }); // ─── QUICK PROMPTS ─── document.querySelectorAll('.quick-pill').forEach(pill => { pill.addEventListener('click', () => { if (isSubmitting) return; const q = pill.getAttribute('data-q'); questionInput.value = q; handleAsk(q); }); }); // ─── FORM SUBMISSION ─── askForm.addEventListener('submit', (e) => { e.preventDefault(); const q = questionInput.value.trim(); if (q) handleAsk(q); }); async function handleAsk(question) { if (isSubmitting) return; isSubmitting = true; // UI Loading state submitBtn.disabled = true; questionInput.disabled = true; btnText.style.display = 'none'; btnSpinner.style.display = 'inline-block'; // 1. Shake Ball & Submerge Die ballSphere.classList.add('shaking'); floatingDie.className = 'floating-die submerged'; audio.shakeSound(); const minShakeTime = new Promise(resolve => setTimeout(resolve, 750)); try { // 2. Call /api/ask const apiCall = fetch('/api/ask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question }) }).then(r => r.json()); const [_, result] = await Promise.all([minShakeTime, apiCall]); // Remove shaking ballSphere.classList.remove('shaking'); if (result.error && !result.fallback) { showToast(result.error || 'Decision failed'); dieText.innerHTML = 'CANNOT
PREDICT'; floatingDie.className = 'floating-die surfacing'; return; } const data = result.fallback ? { ...result.fallback, question } : result; displayOracleAnswer(data); } catch (err) { console.error('Request failed:', err); ballSphere.classList.remove('shaking'); dieText.innerHTML = 'REPLY HAZY
TRY AGAIN'; floatingDie.className = 'floating-die surfacing'; showToast('Network timeout. Please retry.'); } finally { isSubmitting = false; submitBtn.disabled = false; questionInput.disabled = false; btnText.style.display = 'inline'; btnSpinner.style.display = 'none'; questionInput.focus(); } } function displayOracleAnswer(data) { const formattedText = data.answer.replace(/\s+/g, ' ').toUpperCase(); const words = formattedText.split(' '); let htmlAnswer = formattedText; if (words.length > 2) { const mid = Math.ceil(words.length / 2); htmlAnswer = words.slice(0, mid).join(' ') + '
' + words.slice(mid).join(' '); } dieText.innerHTML = htmlAnswer; floatingDie.className = 'floating-die surfacing'; // Audio chime audio.revealChime(data.sentiment); // Update Telemetry telemetryBadge.className = `telemetry-badge ${data.sentiment}`; telemetryBadge.textContent = data.sentiment.toUpperCase(); tVerdict.textContent = data.answer; tNoul.textContent = (data.noul !== undefined ? data.noul.toFixed(2) : '0.50'); noulFill.className = `meter-fill ${data.sentiment}`; noulFill.style.width = `${Math.round((data.noul || 0.5) * 100)}%`; tConfidence.textContent = `${Math.round((data.confidence || 0.8) * 100)}%`; tLatency.textContent = `${data.latency || 320}ms`; tCost.textContent = data.cost ? `$${data.cost.toFixed(6)}` : '< $0.0001'; jsonDump.textContent = JSON.stringify(data.raw || data, null, 2); // Append to History addHistoryItem({ question: data.question, answer: data.answer, sentiment: data.sentiment, timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }); } function addHistoryItem(item) { historyData.unshift(item); if (historyData.length > 25) historyData.pop(); try { localStorage.setItem('jev_history', JSON.stringify(historyData)); } catch (e) {} renderHistory(); } function renderHistory() { historyList.innerHTML = ''; historyCount.textContent = `${historyData.length} ${historyData.length === 1 ? 'query' : 'queries'}`; historyData.forEach((item, idx) => { const li = document.createElement('li'); li.className = 'history-item'; li.innerHTML = `
${escapeHtml(item.question)}
${item.timestamp}
${escapeHtml(item.answer)}
`; li.style.cursor = 'pointer'; li.addEventListener('click', () => { navigator.clipboard.writeText(`Q: ${item.question}\nA: ${item.answer}`).then(() => { showToast('Copied fortune to clipboard'); }); }); historyList.appendChild(li); }); } function escapeHtml(str) { return str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } renderHistory(); })();