// ============================================================ // APP.JS — Core utilities, tabs, model selector, refine, speak // ============================================================ // --- TAB NAVIGATION --- document.querySelectorAll('.tab-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); }); document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); }); btn.classList.add('active'); document.getElementById(btn.getAttribute('data-tab') + '-tab').classList.add('active'); }); }); // --- MODEL SELECTOR --- var modelSelect = document.getElementById('global-model-select'); var costBadge = document.getElementById('model-cost-badge'); fetch('/api/models').then(function(r) { return r.json(); }).then(function(data) { if (data.models && modelSelect) { modelSelect.innerHTML = ''; data.models.forEach(function(m) { var opt = document.createElement('option'); opt.value = m.id; var prefix = m.tag === 'FREE' ? '🆓' : m.tag === 'PREMIUM' ? '👑' : m.tag === 'REASONING' ? '🧠' : '⚡'; opt.textContent = prefix + ' ' + m.name + ' (' + m.cost + ')'; modelSelect.appendChild(opt); }); if (costBadge) costBadge.textContent = data.models[0].cost; } }).catch(function() {}); function getSelectedModel() { return modelSelect ? modelSelect.value : 'google/gemini-2.5-flash'; } if (modelSelect) { modelSelect.addEventListener('change', function() { fetch('/api/models').then(function(r) { return r.json(); }).then(function(data) { var m = data.models.find(function(x) { return x.id === modelSelect.value; }); if (m && costBadge) costBadge.textContent = m.cost; }); showToast('Model: ' + modelSelect.value.split('/').pop(), 'info'); }); } // --- LOADING --- function showLoading(text) { document.getElementById('loading-text').textContent = text || 'Processing...'; document.getElementById('loading-overlay').classList.remove('hidden'); } function hideLoading() { document.getElementById('loading-overlay').classList.add('hidden'); } // --- TOAST --- function showToast(message, type) { var container = document.getElementById('toast-container'); var toast = document.createElement('div'); toast.className = 'toast toast-' + (type || 'success'); var icon = type === 'error' ? 'exclamation-circle' : type === 'info' ? 'info-circle' : 'check-circle'; toast.innerHTML = ' ' + message; container.appendChild(toast); setTimeout(function() { toast.remove(); }, 3500); } // --- COPY --- function copyText(elementId) { var el = document.getElementById(elementId); var text = el.innerText || el.textContent; navigator.clipboard.writeText(text).then(function() { showToast('Copied!', 'success'); }).catch(function() { var range = document.createRange(); range.selectNode(el); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); document.execCommand('copy'); window.getSelection().removeAllRanges(); showToast('Copied!', 'success'); }); } // --- SPEAK / STOP --- var currentlyReadingId = null; var currentAudio = null; function speakText(elementId) { if (currentlyReadingId === elementId) { stopReading(); return; } stopReading(); var el = document.getElementById(elementId); var text = (el.innerText || el.textContent).trim(); if (!text) { showToast('Nothing to read', 'error'); return; } var readBtn = findReadButton(elementId); useBrowserTTS(text, elementId, readBtn); } function useBrowserTTS(text, elementId, readBtn) { if (!('speechSynthesis' in window)) { showToast('TTS not supported', 'error'); return; } window.speechSynthesis.cancel(); var utter = new SpeechSynthesisUtterance(text); utter.rate = 0.9; utter.onend = function() { stopReading(); }; utter.onerror = function() { stopReading(); }; currentlyReadingId = elementId; setReadButtonActive(readBtn, true); window.speechSynthesis.speak(utter); showToast('Reading — click Stop to end', 'info'); } function stopReading() { if ('speechSynthesis' in window) window.speechSynthesis.cancel(); if (currentAudio) { currentAudio.pause(); currentAudio = null; } if (currentlyReadingId) { setReadButtonActive(findReadButton(currentlyReadingId), false); currentlyReadingId = null; } } function findReadButton(elementId) { var el = document.getElementById(elementId); if (!el) return null; var card = el.closest('.output-card') || el.closest('.card'); if (!card) return null; var buttons = card.querySelectorAll('button'); for (var i = 0; i < buttons.length; i++) { var oc = buttons[i].getAttribute('onclick') || ''; if (oc.indexOf('speakText') !== -1 && oc.indexOf(elementId) !== -1) return buttons[i]; } return null; } function setReadButtonActive(btn, active) { if (!btn) return; if (active) { btn.classList.add('btn-reading'); btn.innerHTML = ' Stop'; } else { btn.classList.remove('btn-reading'); btn.innerHTML = ' Read'; } } // --- TIMER --- function createTimer(el) { var s = 0, iv = null; return { start: function() { s = 0; this.update(); var self = this; iv = setInterval(function() { s++; self.update(); }, 1000); }, stop: function() { if (iv) { clearInterval(iv); iv = null; } return s; }, update: function() { el.textContent = String(Math.floor(s/60)).padStart(2,'0') + ':' + String(s%60).padStart(2,'0'); } }; } // --- AUDIO RECORDER --- function AudioRecorder() { this.mediaRecorder = null; this.chunks = []; this.stream = null; } AudioRecorder.prototype.start = function() { var self = this; self.chunks = []; return navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true } }) .then(function(stream) { self.stream = stream; var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm'; self.mediaRecorder = new MediaRecorder(stream, { mimeType: mime }); self.mediaRecorder.ondataavailable = function(e) { if (e.data.size > 0) self.chunks.push(e.data); }; self.mediaRecorder.start(1000); }); }; AudioRecorder.prototype.stop = function() { var self = this; return new Promise(function(resolve) { if (!self.mediaRecorder || self.mediaRecorder.state === 'inactive') { resolve(null); return; } self.mediaRecorder.onstop = function() { var blob = new Blob(self.chunks, { type: self.mediaRecorder.mimeType }); if (self.stream) self.stream.getTracks().forEach(function(t) { t.stop(); }); resolve(blob); }; self.mediaRecorder.stop(); }); }; // --- TRANSCRIBE HELPER --- function transcribeAudio(blob) { var formData = new FormData(); formData.append('audio', blob, 'audio.webm'); return fetch('/api/transcribe', { method: 'POST', body: formData }) .then(function(r) { return r.json(); }); } // --- REFINE HELPER --- function refineDocument(outputElementId, inputElementId) { var doc = document.getElementById(outputElementId).innerText.trim(); var instructions = document.getElementById(inputElementId).value.trim(); if (!doc) { showToast('No document to refine', 'error'); return; } if (!instructions) { showToast('Enter instructions for AI', 'error'); return; } showLoading('Refining...'); fetch('/api/refine', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ currentDocument: doc, instructions: instructions, model: getSelectedModel() }) }) .then(function(r) { return r.json(); }) .then(function(data) { hideLoading(); if (data.success) { document.getElementById(outputElementId).textContent = data.refined; document.getElementById(inputElementId).value = ''; showToast('Refined!', 'success'); } else { showToast(data.error || 'Refine failed', 'error'); } }) .catch(function(err) { hideLoading(); showToast('Error: ' + err.message, 'error'); }); } // --- SHORTEN HELPER --- function shortenDocument(outputElementId) { var doc = document.getElementById(outputElementId).innerText.trim(); if (!doc) { showToast('No document to shorten', 'error'); return; } showLoading('Shortening...'); fetch('/api/shorten', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ document: doc, model: getSelectedModel() }) }) .then(function(r) { return r.json(); }) .then(function(data) { hideLoading(); if (data.success) { document.getElementById(outputElementId).textContent = data.shortened; showToast('Shortened!', 'success'); } else { showToast(data.error || 'Failed', 'error'); } }) .catch(function(err) { hideLoading(); showToast('Error: ' + err.message, 'error'); }); } // --- EXPORT NEXTCLOUD HELPER --- function exportToNextcloud(elementId, docType) { var text = document.getElementById(elementId).innerText.trim(); if (!text) { showToast('Nothing to export', 'error'); return; } var date = new Date().toISOString().split('T')[0]; var filename = docType + '-' + date + '-' + Date.now(); fetch('/api/nextcloud/export', { method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ content: text, filename: filename, type: docType }) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) showToast(data.message, 'success'); else showToast(data.error || 'Export failed', 'error'); }) .catch(function(err) { showToast('Nextcloud not connected. Go to Settings.', 'error'); }); } // --- WEB SPEECH RECOGNITION HELPER --- function createSpeechRecognition() { if (!(window.SpeechRecognition || window.webkitSpeechRecognition)) return null; var SR = window.SpeechRecognition || window.webkitSpeechRecognition; var rec = new SR(); rec.continuous = true; rec.interimResults = true; rec.lang = 'en-US'; return rec; } console.log('✅ App.js loaded'); // Register Service Worker for PWA if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function() { console.log('✅ PWA: Service Worker registered'); }).catch(function(err) { console.log('PWA: SW registration failed', err); }); }