Update static/js/script.js
Browse files- static/js/script.js +65 -8
static/js/script.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/**
|
| 2 |
* ORBIT – Educational Research Assistant
|
| 3 |
-
* FULL V-MASTER SCRIPT -
|
| 4 |
*/
|
| 5 |
|
| 6 |
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -14,6 +14,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 14 |
let isBusy = false;
|
| 15 |
let pdfText = ""; let pdfFilename = "";
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
// 1. DATA RECOVERY
|
| 18 |
try {
|
| 19 |
const stored = localStorage.getItem('orbit_sessions_v12');
|
|
@@ -21,7 +53,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 21 |
if (typeof sessions !== 'object' || Array.isArray(sessions)) sessions = {};
|
| 22 |
} catch(e) { sessions = {}; }
|
| 23 |
|
| 24 |
-
// 2. PWA INSTALL LOGIC
|
| 25 |
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 26 |
let deferredPrompt;
|
| 27 |
|
|
@@ -61,7 +93,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 61 |
const setRes = await fetch('/api/settings');
|
| 62 |
if(setRes.ok) {
|
| 63 |
appSettings = await setRes.json();
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
appSettings.models_openrouter = safeArr(appSettings.models_openrouter);
|
|
|
|
|
|
|
| 65 |
appSettings.models_nvidia = safeArr(appSettings.models_nvidia);
|
| 66 |
appSettings.models_gemini = safeArr(appSettings.models_gemini);
|
| 67 |
appSettings.models_agentrouter = safeArr(appSettings.models_agentrouter);
|
|
@@ -88,7 +126,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 88 |
const cm = $('chat-messages');
|
| 89 |
const ws = $('welcome-msg');
|
| 90 |
|
| 91 |
-
if(cm) cm.innerHTML = '';
|
| 92 |
|
| 93 |
if(sessions[id].messages && sessions[id].messages.length > 0) {
|
| 94 |
if(ws) ws.classList.add('hidden');
|
|
@@ -174,7 +212,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 174 |
addEvt('chat-textarea', 'keydown', e => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
|
| 175 |
if($('chat-textarea')) $('chat-textarea').addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 160) + 'px'; });
|
| 176 |
|
| 177 |
-
// 6. SETTINGS MODAL
|
| 178 |
const provMap = { "OpenRouter": "or", "Nvidia NIM": "nv", "Google Gemini": "gem", "AgentRouter": "ar", "Custom OpenAI": "oai" };
|
| 179 |
|
| 180 |
function syncProviderUI() {
|
|
@@ -251,7 +289,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 251 |
bindAdd('btn-add-ar', 'inp-ar', 'models_agentrouter');
|
| 252 |
bindAdd('btn-add-oai', 'inp-oai', 'models_openai');
|
| 253 |
|
|
|
|
| 254 |
addEvt('btn-save-settings', async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
const payload = {
|
| 256 |
provider: $('settings-provider').value,
|
| 257 |
base_url: $('settings-url').value,
|
|
@@ -265,11 +309,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 265 |
};
|
| 266 |
try {
|
| 267 |
const res = await fetch('/api/settings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
|
| 268 |
-
if(res.ok) {
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
});
|
| 271 |
|
| 272 |
-
// 7. DOI MODAL
|
| 273 |
addEvt('btn-doi', 'click', () => {
|
| 274 |
$('doi-modal').classList.remove('hidden');
|
| 275 |
if($('doi-input')) { $('doi-input').value = ""; $('doi-input').focus(); }
|
|
@@ -278,7 +336,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 278 |
});
|
| 279 |
addEvt('btn-close-doi', 'click', () => $('doi-modal').classList.add('hidden'));
|
| 280 |
|
| 281 |
-
// Bind enter key on input to the submit button
|
| 282 |
if($('doi-input')) {
|
| 283 |
$('doi-input').addEventListener('keydown', e => {
|
| 284 |
if(e.key === 'Enter') {
|
|
|
|
| 1 |
/**
|
| 2 |
* ORBIT – Educational Research Assistant
|
| 3 |
+
* FULL V-MASTER SCRIPT - WITH TOAST NOTIFICATION & AUTO CLOSE
|
| 4 |
*/
|
| 5 |
|
| 6 |
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
| 14 |
let isBusy = false;
|
| 15 |
let pdfText = ""; let pdfFilename = "";
|
| 16 |
|
| 17 |
+
// --- FITUR BARU: TOAST NOTIFICATION ---
|
| 18 |
+
function showToast(message, isError = false) {
|
| 19 |
+
let toast = $('orbit-toast');
|
| 20 |
+
if (!toast) {
|
| 21 |
+
toast = document.createElement('div');
|
| 22 |
+
toast.id = 'orbit-toast';
|
| 23 |
+
document.body.appendChild(toast);
|
| 24 |
+
}
|
| 25 |
+
// Styling Toast dengan Tailwind
|
| 26 |
+
const bgClass = isError ? 'bg-red-500' : 'bg-emerald-500';
|
| 27 |
+
const icon = isError
|
| 28 |
+
? `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>`
|
| 29 |
+
: `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>`;
|
| 30 |
+
|
| 31 |
+
toast.className = `fixed top-6 left-1/2 transform -translate-x-1/2 px-5 py-2.5 rounded-full shadow-2xl text-sm font-semibold z-[9999] transition-all duration-300 flex items-center gap-2 text-white ${bgClass}`;
|
| 32 |
+
toast.style.opacity = '0';
|
| 33 |
+
toast.style.transform = 'translate(-50%, -20px)';
|
| 34 |
+
toast.innerHTML = `${icon} <span>${message}</span>`;
|
| 35 |
+
|
| 36 |
+
// Animasi Masuk
|
| 37 |
+
requestAnimationFrame(() => {
|
| 38 |
+
toast.style.opacity = '1';
|
| 39 |
+
toast.style.transform = 'translate(-50%, 0)';
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Animasi Keluar setelah 3 detik
|
| 43 |
+
setTimeout(() => {
|
| 44 |
+
toast.style.opacity = '0';
|
| 45 |
+
toast.style.transform = 'translate(-50%, -20px)';
|
| 46 |
+
}, 3000);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
// 1. DATA RECOVERY
|
| 50 |
try {
|
| 51 |
const stored = localStorage.getItem('orbit_sessions_v12');
|
|
|
|
| 53 |
if (typeof sessions !== 'object' || Array.isArray(sessions)) sessions = {};
|
| 54 |
} catch(e) { sessions = {}; }
|
| 55 |
|
| 56 |
+
// 2. PWA INSTALL LOGIC
|
| 57 |
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
| 58 |
let deferredPrompt;
|
| 59 |
|
|
|
|
| 93 |
const setRes = await fetch('/api/settings');
|
| 94 |
if(setRes.ok) {
|
| 95 |
appSettings = await setRes.json();
|
| 96 |
+
|
| 97 |
+
// Inject Default Free Models untuk OpenRouter jika kosong
|
| 98 |
+
const DEFAULT_OR_MODELS = ["nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", "baidu/cobuddy:free", "inclusionai/ring-2.6-1t:free", "poolside/laguna-xs.2:free", "google/gemma-4-26b-a4b-it:free", "google/gemma-4-31b-it:free", "nvidia/llama-nemotron-embed-vl-1b-v2:free", "minimax/minimax-m2.5:free", "openai/gpt-oss-120b:free", "liquid/lfm-2.5-1.2b-thinking:free", "liquid/lfm-2.5-1.2b-instruct:free", "qwen/qwen3-next-80b-a3b-instruct:free", "z-ai/glm-4.5-air:free", "meta-llama/llama-3.3-70b-instruct:free"];
|
| 99 |
+
|
| 100 |
appSettings.models_openrouter = safeArr(appSettings.models_openrouter);
|
| 101 |
+
if(appSettings.models_openrouter.length === 0) appSettings.models_openrouter = [...DEFAULT_OR_MODELS];
|
| 102 |
+
|
| 103 |
appSettings.models_nvidia = safeArr(appSettings.models_nvidia);
|
| 104 |
appSettings.models_gemini = safeArr(appSettings.models_gemini);
|
| 105 |
appSettings.models_agentrouter = safeArr(appSettings.models_agentrouter);
|
|
|
|
| 126 |
const cm = $('chat-messages');
|
| 127 |
const ws = $('welcome-msg');
|
| 128 |
|
| 129 |
+
if(cm) cm.innerHTML = '';
|
| 130 |
|
| 131 |
if(sessions[id].messages && sessions[id].messages.length > 0) {
|
| 132 |
if(ws) ws.classList.add('hidden');
|
|
|
|
| 212 |
addEvt('chat-textarea', 'keydown', e => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
|
| 213 |
if($('chat-textarea')) $('chat-textarea').addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 160) + 'px'; });
|
| 214 |
|
| 215 |
+
// 6. SETTINGS MODAL & DYNAMIC UI
|
| 216 |
const provMap = { "OpenRouter": "or", "Nvidia NIM": "nv", "Google Gemini": "gem", "AgentRouter": "ar", "Custom OpenAI": "oai" };
|
| 217 |
|
| 218 |
function syncProviderUI() {
|
|
|
|
| 289 |
bindAdd('btn-add-ar', 'inp-ar', 'models_agentrouter');
|
| 290 |
bindAdd('btn-add-oai', 'inp-oai', 'models_openai');
|
| 291 |
|
| 292 |
+
// --- SAVE SETTINGS DENGAN TOAST & AUTO CLOSE ---
|
| 293 |
addEvt('btn-save-settings', async () => {
|
| 294 |
+
const btn = $('btn-save-settings');
|
| 295 |
+
const originalText = btn.textContent;
|
| 296 |
+
btn.textContent = "Saving...";
|
| 297 |
+
btn.disabled = true;
|
| 298 |
+
|
| 299 |
const payload = {
|
| 300 |
provider: $('settings-provider').value,
|
| 301 |
base_url: $('settings-url').value,
|
|
|
|
| 309 |
};
|
| 310 |
try {
|
| 311 |
const res = await fetch('/api/settings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) });
|
| 312 |
+
if(res.ok) {
|
| 313 |
+
appSettings = await res.json();
|
| 314 |
+
populateModelSelect();
|
| 315 |
+
$('settings-modal').classList.add('hidden'); // Auto close
|
| 316 |
+
showToast("Settings saved successfully!"); // Notifikasi Toast hijau
|
| 317 |
+
} else {
|
| 318 |
+
const errData = await res.json();
|
| 319 |
+
throw new Error(errData.error || "Gagal menyimpan");
|
| 320 |
+
}
|
| 321 |
+
} catch(e) {
|
| 322 |
+
console.error(e);
|
| 323 |
+
showToast(`Error: ${e.message}`, true); // Notifikasi Toast merah
|
| 324 |
+
} finally {
|
| 325 |
+
btn.textContent = originalText;
|
| 326 |
+
btn.disabled = false;
|
| 327 |
+
}
|
| 328 |
});
|
| 329 |
|
| 330 |
+
// 7. DOI MODAL
|
| 331 |
addEvt('btn-doi', 'click', () => {
|
| 332 |
$('doi-modal').classList.remove('hidden');
|
| 333 |
if($('doi-input')) { $('doi-input').value = ""; $('doi-input').focus(); }
|
|
|
|
| 336 |
});
|
| 337 |
addEvt('btn-close-doi', 'click', () => $('doi-modal').classList.add('hidden'));
|
| 338 |
|
|
|
|
| 339 |
if($('doi-input')) {
|
| 340 |
$('doi-input').addEventListener('keydown', e => {
|
| 341 |
if(e.key === 'Enter') {
|