| |
| |
| |
| |
|
|
| 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; |
| |
| |
| 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" |
| ]; |
|
|
| |
| 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; |
|
|
| |
| 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}`; |
| 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; |
| |
| |
| 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 |
| }) |
| }); |
| 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); |
| } |
| }); |
|
|
| |
| 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); |
|
|
| |
| 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; |
| |
| |
| 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 = ''; |
| return; |
| } |
|
|
| const fd = new FormData(); fd.append('file', f); |
| $('attach-badge').classList.remove('hidden'); |
| $('attach-name').textContent = "Memproses file..."; |
| |
| try { |
| |
| const res = await fetch('/api/upload_file', { method: 'POST', body: fd }); |
| const d = await res.json(); |
| |
| if(res.ok) { |
| attachedFile = d; |
| $('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(); |
| }); |