File size: 25,014 Bytes
9c62054 e67e6d1 9c62054 5673e08 44fc334 9604a43 5673e08 21234f3 c7129d3 44fc334 e67e6d1 c7129d3 9b7d4c5 244c441 9b7d4c5 244c441 9b7d4c5 e67e6d1 66bdef6 eef5391 66bdef6 eef5391 ffdbd18 eef5391 e67e6d1 66bdef6 c7129d3 9b7d4c5 c7129d3 9604a43 44fc334 9604a43 5673e08 9604a43 9c62054 c7129d3 805bbe8 20a2068 44fc334 9604a43 5673e08 9c62054 5673e08 21234f3 9604a43 c397617 5673e08 44fc334 5673e08 9b7d4c5 9604a43 9c62054 5673e08 9b7d4c5 9604a43 66bdef6 9604a43 9b7d4c5 ffdbd18 9b7d4c5 ffdbd18 e67e6d1 9b7d4c5 9604a43 5673e08 40bd302 44fc334 c397617 7088e35 20a2068 9b7d4c5 9604a43 44fc334 c397617 5673e08 c7129d3 40bd302 9c62054 e67e6d1 66bdef6 9c62054 c7129d3 9c62054 44fc334 40bd302 9c62054 40bd302 9c62054 e67e6d1 1dfaeb1 21234f3 44fc334 9604a43 44fc334 9604a43 44fc334 21234f3 44fc334 21234f3 44fc334 9604a43 40bd302 9604a43 40bd302 9c62054 5673e08 c397617 5673e08 44fc334 9604a43 e67e6d1 44fc334 9604a43 9c62054 c7129d3 9604a43 e67e6d1 c7129d3 9604a43 e67e6d1 9604a43 44fc334 b559549 e67e6d1 5673e08 b559549 e67e6d1 b559549 e67e6d1 b559549 9c62054 b559549 e67e6d1 b559549 9c62054 bb775b3 b559549 5673e08 44fc334 e67e6d1 5673e08 b559549 5673e08 b559549 44fc334 b559549 5673e08 44fc334 e67e6d1 bb775b3 36075bf 44fc334 9604a43 c397617 5673e08 44fc334 40bd302 9604a43 f8fbf35 9604a43 40bd302 9604a43 5673e08 44fc334 9604a43 44fc334 9604a43 c397617 44fc334 5673e08 44fc334 5673e08 9604a43 ffdbd18 66bdef6 eef5391 66bdef6 5673e08 44fc334 9604a43 44fc334 9604a43 c7129d3 5673e08 eef5391 9c62054 9b7d4c5 eef5391 66bdef6 eef5391 66bdef6 eef5391 66bdef6 805bbe8 66bdef6 e67e6d1 66bdef6 4e18ba0 1dfaeb1 66bdef6 9604a43 9c62054 9604a43 9c62054 9604a43 e67e6d1 44fc334 e67e6d1 44fc334 e67e6d1 9c62054 e67e6d1 9c62054 e67e6d1 44fc334 21234f3 c7129d3 44fc334 5673e08 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 | /**
* ORBIT – Educational Research Assistant
* FULL V-MASTER SCRIPT - WITH VISION (IMAGE), DOCX SUPPORT & THINKING ILLUSION
*/
document.addEventListener('DOMContentLoaded', () => {
const $ = id => document.getElementById(id);
const addEvt = (id, event, handler) => { if($(id)) $(id).addEventListener(event, handler); };
const safeArr = arr => Array.isArray(arr) ? arr : [];
let currentSid = null;
let sessions = {};
let appSettings = null;
let isBusy = false;
// Penampung File Universal
let attachedFile = null;
const DEFAULT_OR_MODELS = [
"baidu/cobuddy:free",
"poolside/laguna-xs.2:free",
"inclusionai/ring-2.6-1t:free",
"z-ai/glm-4.5-air:free",
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning: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",
"nousresearch/hermes-3-llama-3.1-405b:free",
"qwen/qwen3-next-80b-a3b-instruct:free",
"meta-llama/llama-3.3-70b-instruct:free"
];
// TOAST NOTIFICATION
function showToast(message, isError = false) {
let toast = $('orbit-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'orbit-toast';
toast.style.cssText = `
position: fixed; top: 24px; left: 50%; transform: translate(-50%, -20px);
padding: 12px 24px; border-radius: 50px; box-shadow: 0 10px 25px rgba(0,0,0,0.2);
font-size: 14px; font-weight: 600; color: white; z-index: 99999;
opacity: 0; transition: all 0.3s ease-in-out; display: flex; align-items: center; gap: 8px;
pointer-events: none;
`;
document.body.appendChild(toast);
}
toast.style.backgroundColor = isError ? '#ef4444' : '#10b981';
toast.innerHTML = isError ? `<span>❌</span> <span>${message}</span>` : `<span>✅</span> <span>${message}</span>`;
setTimeout(() => { toast.style.opacity = '1'; toast.style.transform = 'translate(-50%, 0)'; }, 10);
setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translate(-50%, -20px)'; }, 4000);
}
try {
const stored = localStorage.getItem('orbit_sessions_v14');
sessions = stored ? JSON.parse(stored) : {};
if (typeof sessions !== 'object' || Array.isArray(sessions)) sessions = {};
} catch(e) { sessions = {}; }
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
let deferredPrompt;
if ($('btn-install-pwa') && !window.matchMedia('(display-mode: standalone)').matches) {
$('btn-install-pwa').classList.remove('hidden');
}
window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; });
addEvt('btn-install-pwa', 'click', async () => {
if (isIOS) {
alert("Apple iOS memblokir install otomatis.\n\nCara Install PWA di iPhone/iPad:\n1. Tekan ikon 'Share' (kotak dengan panah ke atas) di menu bawah Safari.\n2. Geser ke bawah dan pilih 'Add to Home Screen' (Tambahkan ke Layar Utama).");
} else if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if(outcome === 'accepted' && $('btn-install-pwa')) $('btn-install-pwa').classList.add('hidden');
deferredPrompt = null;
} else {
alert("Chrome memblokir pop-up otomatis.\n\nCara Install:\nKlik ikon Titik-Tiga (⋮) di pojok kanan atas browser, lalu pilih 'Tambahkan ke Layar Utama' (Add to Home screen).");
}
});
async function init() {
try {
const me = await fetch('/api/me', { cache: 'no-store' });
if(me.status === 401) { window.location.href = '/login'; return; }
if(me.ok) {
const user = await me.json();
if($('user-name')) $('user-name').textContent = user.name || user.email;
if($('user-avatar') && user.picture) $('user-avatar').src = user.picture;
}
const setRes = await fetch('/api/settings', { cache: 'no-store' });
if(setRes.ok) {
appSettings = await setRes.json();
appSettings.models_nvidia = safeArr(appSettings.models_nvidia);
appSettings.models_gemini = safeArr(appSettings.models_gemini);
appSettings.models_agentrouter = safeArr(appSettings.models_agentrouter);
appSettings.models_openai = safeArr(appSettings.models_openai);
if (!localStorage.getItem('orbit_force_free_v6')) {
appSettings.models_openrouter = [...DEFAULT_OR_MODELS];
localStorage.setItem('orbit_force_free_v6', 'true');
fetch('/api/settings', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(appSettings) }).catch(err => console.error("Auto-save failed", err));
} else {
appSettings.models_openrouter = safeArr(appSettings.models_openrouter);
}
}
} catch (e) {
console.error("Init err:", e);
} finally {
populateModelSelect();
const ids = Object.keys(sessions).sort((a,b) => b-a);
if(ids.length) loadSession(ids[0]); else newSession();
}
}
function save() { try { localStorage.setItem('orbit_sessions_v14', JSON.stringify(sessions)); } catch(e){} }
function newSession() { const id = Date.now().toString(); sessions[id] = { title: "New Chat", messages: [] }; loadSession(id); }
addEvt('btn-new-chat', 'click', newSession);
function loadSession(id) {
if(!sessions[id]) return;
currentSid = id;
const cm = $('chat-messages'); const ws = $('welcome-msg');
if(cm) cm.innerHTML = '';
if(sessions[id].messages && sessions[id].messages.length > 0) {
if(ws) ws.classList.add('hidden');
sessions[id].messages.forEach(m => renderBubble(m.role, m.displayContent || m.content));
} else {
if(ws) ws.classList.remove('hidden');
}
renderHistory();
const ca = $('chat-area'); if(ca) ca.scrollTop = ca.scrollHeight;
}
function renderHistory() {
const list = $('history-list'); if(!list) return;
const ids = Object.keys(sessions).sort((a,b) => b-a);
if(!ids.length) { list.innerHTML = '<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>'; return; }
list.innerHTML = ids.map(id => {
const active = (id === currentSid) ? 'bg-accent-light text-accent shadow-sm' : 'text-gray-600 hover:bg-white';
return `<button onclick="window.ls('${id}')" class="w-full text-left px-3 py-2.5 rounded-xl text-xs truncate font-medium ${active}">${sessions[id].title || "New Chat"}</button>`;
}).join('');
}
window.ls = id => { loadSession(id); if(window.innerWidth < 768) toggleSidebar(); };
function renderBubble(role, content) {
const isUser = (role === 'user');
const wrap = document.createElement('div');
wrap.className = `flex mb-6 ${isUser ? 'justify-end' : 'justify-start'}`;
if(isUser) {
wrap.innerHTML = `<div class="bg-accent text-white p-4 rounded-2xl rounded-tr-none max-w-[85%] text-[15px] leading-relaxed shadow-sm">${content}</div>`;
} else {
let html = content; try { html = marked.parse(content); } catch(e) { html = content.replace(/\n/g, '<br>'); }
wrap.innerHTML = `<div class="flex gap-4 items-start w-full"><img src="/static/icon.png" class="w-8 h-8 rounded-full shadow-sm shrink-0" onerror="this.style.display='none'"><div class="bg-[#f8f9fa] border border-slate-200 p-5 rounded-2xl rounded-tl-none max-w-[90%] md:max-w-[85%] prose-orbit w-full shadow-sm">${html}</div></div>`;
}
if($('chat-messages')) {
$('chat-messages').appendChild(wrap);
const ca = $('chat-area'); if(ca) ca.scrollTop = ca.scrollHeight;
}
}
async function sendChat() {
if(isBusy) return;
const raw = $('chat-textarea').value.trim();
if(!raw && !attachedFile) return;
$('chat-textarea').value = ''; $('chat-textarea').style.height = 'auto';
if($('welcome-msg')) $('welcome-msg').classList.add('hidden');
let full = raw; let display = raw.replace(/\n/g, '<br>');
let payloadImage = null;
let historyContent = raw; // Khusus buat disimpen di memori lokal biar ga overload
// JIKA ADA FILE ATTACHED
if (attachedFile) {
if (attachedFile.type === 'document') {
full = `[Document: ${attachedFile.filename}]\n${attachedFile.text}\n\nUser: ${raw}`;
historyContent = full;
display = `<div class="bg-emerald-500 text-white text-[10px] px-2 py-1 rounded w-fit mb-2 font-bold flex items-center gap-1"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>${attachedFile.filename}</div>${display}`;
} else if (attachedFile.type === 'image') {
payloadImage = { base64: attachedFile.base64, mime: attachedFile.mime };
historyContent = `[Gambar Diunggah: ${attachedFile.filename}]\n\n${raw}`; // Base64 ga kita simpen ke localstorage biar ga crash HP user
display = `<div class="mb-2"><img src="data:${attachedFile.mime};base64,${attachedFile.base64}" class="max-h-[200px] rounded-xl border border-gray-200 shadow-sm bg-white" /></div>${display}`;
}
attachedFile = null;
$('attach-badge').classList.add('hidden');
$('pdf-input').value = '';
}
if(!sessions[currentSid].messages || !sessions[currentSid].messages.length) sessions[currentSid].title = raw.slice(0, 20) || "New Chat";
sessions[currentSid].messages.push({ role: 'user', content: historyContent, displayContent: display });
renderBubble('user', display);
isBusy = true; $('btn-send').disabled = true;
// --- ANIMASI KERANGKA BERPIKIR ---
const loadId = 'load-' + Date.now();
const textId = 'text-' + Date.now();
const thinkingSteps = [
"Memahami konteks pertanyaan...",
"Memindai literatur dan referensi yang relevan...",
"Mengekstrak poin-poin penting...",
"Menyusun kerangka sintesis...",
"Menyempurnakan tata bahasa..."
];
if($('chat-messages')) {
$('chat-messages').insertAdjacentHTML('beforeend', `
<div id="${loadId}" class="flex mb-8 gap-4 items-start">
<img src="/static/icon.png" class="w-8 h-8 rounded-full shadow-sm shrink-0">
<div class="bg-white border border-gray-200 px-5 py-4 rounded-[24px] rounded-tl-[8px] shadow-sm flex flex-col gap-2 min-w-[220px]">
<div class="flex items-center gap-3 text-accent text-sm font-bold">
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path></svg>
Thinking Process
</div>
<div id="${textId}" class="text-xs font-semibold text-gray-500 animate-pulse pl-7 border-l-2 border-gray-200 ml-1.5 mt-1">Memulai inisialisasi...</div>
</div>
</div>
`);
const ca = $('chat-area'); if(ca) ca.scrollTop = ca.scrollHeight;
}
let stepIndex = 0;
const thinkingInterval = setInterval(() => {
const txtEl = $(textId);
if(txtEl) {
txtEl.textContent = thinkingSteps[stepIndex % thinkingSteps.length];
stepIndex++;
}
}, 2200);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: full,
model: $('model-select').value,
messages: sessions[currentSid].messages.slice(0,-1),
image: payloadImage // Ngirim payload ke backend
})
});
const data = await res.json();
clearInterval(thinkingInterval);
if($(loadId)) $(loadId).remove();
if(!res.ok) throw new Error(data.error || "Server error");
sessions[currentSid].messages.push({ role: 'assistant', content: data.reply });
renderBubble('assistant', data.reply);
save(); renderHistory();
} catch(e) {
clearInterval(thinkingInterval);
if($(loadId)) $(loadId).remove();
renderBubble('assistant', `**Error:** ${e.message}`);
} finally { isBusy = false; $('btn-send').disabled = false; }
}
addEvt('btn-send', 'click', sendChat);
addEvt('chat-textarea', 'keydown', e => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
if($('chat-textarea')) $('chat-textarea').addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 160) + 'px'; });
const provMap = { "OpenRouter": "or", "Nvidia NIM": "nv", "Google Gemini": "gem", "AgentRouter": "ar", "Custom OpenAI": "oai" };
function syncProviderUI() {
const prov = $('settings-provider').value;
document.querySelectorAll('.prov-sec').forEach(el => el.classList.add('hidden'));
const secId = provMap[prov];
if(secId && $(`sec-${secId}`)) $(`sec-${secId}`).classList.remove('hidden');
}
function populateModelSelect() {
const ms = $('model-select'); if(!ms) return;
ms.innerHTML = "";
let list = ["gemini-1.5-pro-latest", "gemini-1.5-flash-latest"];
if(appSettings) {
const mapKey = { "OpenRouter": "models_openrouter", "Nvidia NIM": "models_nvidia", "Google Gemini": "models_gemini", "AgentRouter": "models_agentrouter", "Custom OpenAI": "models_openai" };
const k = mapKey[appSettings.provider];
if(appSettings[k] && appSettings[k].length > 0) list = appSettings[k];
}
list.forEach(m => { const opt = document.createElement('option'); opt.value = m; opt.textContent = m; if(appSettings && m === appSettings.current_model) opt.selected = true; ms.appendChild(opt); });
}
function renderDynamicLists() {
if(!appSettings) return;
const draw = (arr, listId, k) => {
const lst = $(listId); if(!lst) return;
lst.innerHTML = safeArr(arr).map((m,i) => `<div class="flex items-center justify-between px-2 py-1.5 rounded hover:bg-white group"><span class="text-xs truncate flex-1">${m}</span><button data-i="${i}" data-k="${k}" class="btn-del text-red-400 font-bold ml-2">✕</button></div>`).join('');
lst.querySelectorAll('.btn-del').forEach(b => {
b.addEventListener('click', function() { appSettings[this.dataset.k].splice(Number(this.dataset.i), 1); renderDynamicLists(); });
});
};
draw(appSettings.models_openrouter, 'list-or', 'models_openrouter');
draw(appSettings.models_nvidia, 'list-nv', 'models_nvidia');
draw(appSettings.models_gemini, 'list-gem', 'models_gemini');
draw(appSettings.models_agentrouter, 'list-ar', 'models_agentrouter');
draw(appSettings.models_openai, 'list-oai', 'models_openai');
}
addEvt('btn-settings', 'click', () => {
if(appSettings) {
$('settings-provider').value = appSettings.provider || "OpenRouter";
$('settings-apikey').value = appSettings.api_key || "";
$('settings-url').value = appSettings.base_url || "";
renderDynamicLists();
syncProviderUI();
}
$('settings-modal').classList.remove('hidden');
if(window.innerWidth < 768) toggleSidebar();
});
addEvt('btn-close-settings', 'click', () => $('settings-modal').classList.add('hidden'));
addEvt('btn-cancel-settings', 'click', () => $('settings-modal').classList.add('hidden'));
addEvt('settings-provider', 'change', () => {
const prov = $('settings-provider').value;
const urls = { "OpenRouter": "https://openrouter.ai/api/v1/chat/completions", "Nvidia NIM": "https://integrate.api.nvidia.com/v1/chat/completions", "Google Gemini": "https://generativelanguage.googleapis.com/v1beta/models/", "AgentRouter": "https://agentrouter.org/v1/chat/completions" };
if (urls[prov] && $('settings-url')) $('settings-url').value = urls[prov];
syncProviderUI();
});
addEvt('btn-toggle-key', 'click', () => { const inp = $('settings-apikey'); if(inp) inp.type = inp.type === 'password' ? 'text' : 'password'; });
const bindAdd = (btnId, inpId, listKey) => {
const f = () => {
const val = $(inpId)?.value.trim();
if(!val || !appSettings) return;
if(!Array.isArray(appSettings[listKey])) appSettings[listKey] = [];
if(!appSettings[listKey].includes(val)) { appSettings[listKey].push(val); $(inpId).value = ""; renderDynamicLists(); }
};
addEvt(btnId, 'click', f);
if($(inpId)) $(inpId).addEventListener('keydown', e => { if(e.key==='Enter') f(); });
};
bindAdd('btn-add-or', 'inp-or', 'models_openrouter');
bindAdd('btn-add-nv', 'inp-nv', 'models_nvidia');
bindAdd('btn-add-gem', 'inp-gem', 'models_gemini');
bindAdd('btn-add-ar', 'inp-ar', 'models_agentrouter');
bindAdd('btn-add-oai', 'inp-oai', 'models_openai');
addEvt('btn-save-settings', 'click', async () => {
const btn = $('btn-save-settings');
const originalText = btn.textContent;
btn.innerHTML = `<svg class="w-4 h-4 animate-spin inline mr-2" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path></svg>Saving...`;
btn.disabled = true;
const payload = {
provider: $('settings-provider').value,
base_url: $('settings-url').value,
api_key: $('settings-apikey').value,
models_openrouter: safeArr(appSettings.models_openrouter),
models_nvidia: safeArr(appSettings.models_nvidia),
models_gemini: safeArr(appSettings.models_gemini),
models_agentrouter: safeArr(appSettings.models_agentrouter),
models_openai: safeArr(appSettings.models_openai),
current_model: $('model-select').value
};
try {
const res = await fetch('/api/settings', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if(res.ok) {
appSettings = await res.json();
populateModelSelect();
setTimeout(() => {
$('settings-modal').classList.add('hidden');
showToast("Settings saved successfully!");
}, 300);
} else {
throw new Error("Gagal terhubung ke database server");
}
} catch(e) {
console.error(e);
showToast(`Error: ${e.message}`, true);
} finally {
setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 300);
}
});
// 7. DOI MODAL
addEvt('btn-doi', 'click', () => {
$('doi-modal').classList.remove('hidden');
if($('doi-input')) { $('doi-input').value = ""; $('doi-input').focus(); }
if($('doi-result')) $('doi-result').classList.add('hidden');
if(window.innerWidth < 768) toggleSidebar();
});
addEvt('btn-close-doi', 'click', () => $('doi-modal').classList.add('hidden'));
if($('doi-input')) {
$('doi-input').addEventListener('keydown', e => {
if(e.key === 'Enter') {
e.preventDefault();
if($('btn-validate-doi-submit')) $('btn-validate-doi-submit').click();
}
});
}
addEvt('btn-validate-doi-submit', 'click', async () => {
const doi = $('doi-input').value.trim(); if(!doi) return;
$('doi-result').classList.remove('hidden'); $('doi-result').innerHTML = "Validating...";
try {
const res = await fetch('/api/validate_doi', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({doi}) });
const d = await res.json();
if(!res.ok || d.error) $('doi-result').innerHTML = `<p class="text-red-500 font-medium">Error: ${d.error || "Gagal"}</p>`;
else $('doi-result').innerHTML = `<div class="space-y-2"><div><p class="text-[10px] font-semibold text-gray-400 uppercase">Title</p><p class="font-medium text-gray-800 text-sm">${d.title}</p></div><div><p class="text-[10px] font-semibold text-gray-400 uppercase">Authors</p><p class="text-sm text-gray-700">${d.authors}</p></div><div class="flex gap-6"><div><p class="text-[10px] font-semibold text-gray-400 uppercase">Year</p><p class="text-sm text-gray-700">${d.year}</p></div><div><p class="text-[10px] font-semibold text-gray-400 uppercase">Type</p><p class="text-sm text-gray-700">${d.type}</p></div></div><div><p class="text-[10px] font-semibold text-gray-400 uppercase">Source</p><p class="text-sm text-gray-700">${d.journal}</p></div></div>`;
} catch(e) {
$('doi-result').innerHTML = `<p class="text-red-500 font-medium">Error: ${e.message}</p>`;
}
});
function toggleSidebar() { $('sidebar').classList.toggle('-translate-x-full'); $('sidebar-overlay').classList.toggle('hidden'); }
addEvt('btn-hamburger', 'click', toggleSidebar); addEvt('btn-close-sidebar', 'click', toggleSidebar); addEvt('sidebar-overlay', 'click', toggleSidebar);
function clr() {
if(!currentSid) return; sessions[currentSid].messages = []; sessions[currentSid].title = "New Chat"; save(); loadSession(currentSid);
}
addEvt('btn-clear-chat-top', 'click', clr); addEvt('btn-clear-chat-mobile', 'click', clr);
// 8. LOGIKA UPLOAD MULTI-FILE
addEvt('btn-attach', 'click', () => $('pdf-input').click());
addEvt('btn-remove-attach', 'click', () => { attachedFile = null; $('attach-badge').classList.add('hidden'); $('pdf-input').value = ''; });
if($('pdf-input')) {
$('pdf-input').addEventListener('change', async e => {
const f = e.target.files[0]; if(!f) return;
// VALIDASI EKSTENSI (GUE TAMBAHIN DISINI SESUAI MINTA LU)
const allowed = ['pdf', 'doc', 'docx', 'jpg', 'jpeg', 'png'];
const ext = f.name.split('.').pop().toLowerCase();
if(!allowed.includes(ext)) {
showToast("Error: Ekstensi file tidak valid atau tidak diterima!", true);
$('pdf-input').value = ''; // Reset inputnya
return;
}
const fd = new FormData(); fd.append('file', f);
$('attach-badge').classList.remove('hidden');
$('attach-name').textContent = "Memproses file...";
try {
// Tembak ke endpoint yang udah gue update di app.py
const res = await fetch('/api/upload_file', { method: 'POST', body: fd });
const d = await res.json();
if(res.ok) {
attachedFile = d; // Nampung data (text atau gambar base64)
$('attach-name').textContent = d.filename;
} else {
showToast(d.error || "Gagal mengunggah file.", true);
$('attach-badge').classList.add('hidden');
$('pdf-input').value = '';
}
} catch(e) {
showToast(e.message, true);
$('attach-badge').classList.add('hidden');
$('pdf-input').value = '';
}
});
}
init();
}); |