xenux4u commited on
Commit
c7129d3
·
verified ·
1 Parent(s): f8fbf35

Update static/js/script.js

Browse files
Files changed (1) hide show
  1. static/js/script.js +169 -137
static/js/script.js CHANGED
@@ -1,20 +1,42 @@
 
 
 
 
 
1
  document.addEventListener('DOMContentLoaded', () => {
2
  const $ = id => document.getElementById(id);
3
  const addEvt = (id, event, handler) => { if($(id)) $(id).addEventListener(event, handler); };
4
 
5
  let currentSid = null;
6
- let sessions = JSON.parse(localStorage.getItem('orbit_sessions_v10')) || {};
7
- if (typeof sessions !== 'object' || Array.isArray(sessions)) sessions = {};
8
-
9
  let appSettings = null;
10
  let isBusy = false;
11
- let pdfText = ""; let pdfFilename = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- // PWA INSTALL LOGIC (Fixed line breaks alert)
14
  let deferredPrompt;
15
  if ($('btn-install-pwa') && !window.matchMedia('(display-mode: standalone)').matches) {
16
  $('btn-install-pwa').classList.remove('hidden');
17
  }
 
18
  window.addEventListener('beforeinstallprompt', (e) => {
19
  e.preventDefault();
20
  deferredPrompt = e;
@@ -27,30 +49,24 @@ document.addEventListener('DOMContentLoaded', () => {
27
  if(outcome === 'accepted' && $('btn-install-pwa')) $('btn-install-pwa').classList.add('hidden');
28
  deferredPrompt = null;
29
  } else {
30
- // Fix: \n\n diganti agar berfungsi sebagai line break betulan di javascript
31
  alert("Chrome memblokir pop-up otomatis.\n\nUntuk Install: Klik ikon Titik-Tiga (⋮) di pojok kanan atas browser Anda, lalu pilih 'Tambahkan ke Layar Utama' (Add to Home screen).");
32
  }
33
  });
34
 
35
- // BOOT & INIT
36
  async function init() {
37
  try {
38
- const me = await fetch('/api/me');
39
- if(me.status === 401) { window.location.href = '/login'; return; }
40
- const user = await me.json();
 
41
  if($('user-name')) $('user-name').textContent = user.name || user.email;
42
  if($('user-avatar') && user.picture) $('user-avatar').src = user.picture;
43
 
44
  const setRes = await fetch('/api/settings');
45
- if(setRes.ok) {
46
- appSettings = await setRes.json();
47
- // Ensure arrays exist
48
- if(!appSettings.models_openrouter) appSettings.models_openrouter = [];
49
- if(!appSettings.models_nvidia) appSettings.models_nvidia = [];
50
- if(!appSettings.models_gemini) appSettings.models_gemini = ["gemini-1.5-pro-latest", "gemini-1.5-flash-latest"];
51
- if(!appSettings.models_agentrouter) appSettings.models_agentrouter = [];
52
- if(!appSettings.models_openai) appSettings.models_openai = [];
53
- }
54
  } finally {
55
  populateModelSelect();
56
  const ids = Object.keys(sessions).sort((a,b) => b-a);
@@ -58,138 +74,168 @@ document.addEventListener('DOMContentLoaded', () => {
58
  }
59
  }
60
 
61
- function save() { localStorage.setItem('orbit_sessions_v10', JSON.stringify(sessions)); }
62
- function newSession() { const id = Date.now().toString(); sessions[id] = { title: "New Chat", messages: [] }; loadSession(id); }
 
 
 
 
 
 
63
  addEvt('btn-new-chat', 'click', newSession);
64
 
65
  function loadSession(id) {
 
66
  currentSid = id;
67
- const cm = $('chat-messages'); if(!cm) return;
 
 
 
68
  cm.innerHTML = '';
69
- if(sessions[id].messages && sessions[id].messages.length) {
70
- if($('welcome-msg')) $('welcome-msg').style.display = 'none';
 
71
  sessions[id].messages.forEach(m => renderBubble(m.role, m.displayContent || m.content));
72
  } else {
73
- if($('welcome-msg')) $('welcome-msg').style.display = 'flex';
 
74
  }
75
- renderHistory(); cm.scrollTop = cm.scrollHeight;
 
 
76
  }
77
 
78
  function renderHistory() {
79
  const list = $('history-list'); if(!list) return;
80
  const ids = Object.keys(sessions).sort((a,b) => b-a);
 
 
 
 
81
  list.innerHTML = ids.map(id => {
82
- const active = (id === currentSid) ? 'bg-accent-light text-accent shadow-sm' : 'text-gray-600 hover:bg-white';
83
- 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>`;
84
  }).join('');
85
  }
86
  window.ls = id => { loadSession(id); if(window.innerWidth < 768) toggleSidebar(); };
87
 
 
88
  function renderBubble(role, content) {
89
  const isUser = (role === 'user');
90
  const wrap = document.createElement('div');
91
  wrap.className = `flex mb-6 ${isUser ? 'justify-end' : 'justify-start'}`;
 
92
  if(isUser) {
93
- wrap.innerHTML = `<div class="bg-accent text-white p-4 rounded-2xl rounded-tr-none max-w-[85%] text-[15px] leading-relaxed">${content}</div>`;
94
  } else {
95
- let html = content; try { html = marked.parse(content); } catch(e) { html = content.replace(/\n/g, '<br>'); }
96
- wrap.innerHTML = `<div class="flex gap-4 items-start w-full"><img src="/static/icon.png" class="w-8 h-8 rounded-full 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">${html}</div></div>`;
 
 
 
 
 
 
 
97
  }
98
- if($('chat-messages')) { $('chat-messages').appendChild(wrap); $('chat-messages').scrollTop = $('chat-messages').scrollHeight; }
 
99
  }
100
 
 
101
  async function sendChat() {
102
  if(isBusy) return;
103
- const raw = $('chat-textarea').value.trim();
 
104
  if(!raw && !pdfText) return;
105
 
106
- $('chat-textarea').value = ''; $('chat-textarea').style.height = 'auto';
107
- if($('welcome-msg')) $('welcome-msg').style.display = 'none';
108
 
109
- let full = raw; let display = raw.replace(/\n/g, '<br>');
110
- if(pdfText) { full = `[PDF: ${pdfFilename}]\n${pdfText}\n\nUser: ${raw}`; display = `<div class="bg-blue-500 text-white text-[10px] px-2 py-1 rounded w-fit mb-1 font-bold">📄 ${pdfFilename}</div>${display}`; pdfText = ""; $('attach-badge').classList.add('hidden'); }
 
 
 
 
 
 
 
 
 
 
111
 
112
- if(!sessions[currentSid].messages || !sessions[currentSid].messages.length) sessions[currentSid].title = raw.slice(0, 20) || "New Chat";
113
- sessions[currentSid].messages.push({ role: 'user', content: full, displayContent: display });
114
- renderBubble('user', display);
 
 
 
 
115
 
116
  isBusy = true; $('btn-send').disabled = true;
117
  const loadId = 'load-' + Date.now();
118
- if($('chat-messages')) { $('chat-messages').insertAdjacentHTML('beforeend', `<div id="${loadId}" class="flex mb-6 gap-4 items-start animate-pulse"><img src="/static/icon.png" class="w-8 h-8 rounded-full shrink-0"><div class="bg-slate-50 p-4 rounded-2xl rounded-tl-none text-gray-400 text-sm">Analyzing...</div></div>`); $('chat-messages').scrollTop = $('chat-messages').scrollHeight; }
 
119
 
120
  try {
121
  const res = await fetch('/api/chat', {
122
  method: 'POST',
123
  headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify({ prompt: full, model: $('model-select').value, messages: sessions[currentSid].messages.slice(0,-1) })
 
 
 
 
125
  });
126
  const data = await res.json();
127
  if($(loadId)) $(loadId).remove();
 
128
  if(!res.ok) throw new Error(data.error || "Server error");
 
129
  sessions[currentSid].messages.push({ role: 'assistant', content: data.reply });
130
  renderBubble('assistant', data.reply);
131
  save(); renderHistory();
132
  } catch(e) {
133
  if($(loadId)) $(loadId).remove();
134
  renderBubble('assistant', `**Error:** ${e.message}`);
135
- } finally { isBusy = false; $('btn-send').disabled = false; }
 
 
136
  }
137
 
138
  addEvt('btn-send', 'click', sendChat);
139
  addEvt('chat-textarea', 'keydown', e => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
140
- if($('chat-textarea')) $('chat-textarea').addEventListener('input', function() { this.style.height = 'auto'; this.style.height = Math.min(this.scrollHeight, 160) + 'px'; });
141
-
142
- // SETTINGS MODAL & DYNAMIC UI
143
- const provMap = { "OpenRouter": "or", "Nvidia NIM": "nv", "Google Gemini": "gem", "AgentRouter": "ar", "Custom OpenAI": "oai" };
144
-
145
- function syncProviderUI() {
146
- const prov = $('settings-provider').value;
147
- // Hide all sections first
148
- document.querySelectorAll('.prov-sec').forEach(el => el.classList.add('hidden'));
149
- // Show matching section
150
- const secId = provMap[prov];
151
- if(secId && $(`sec-${secId}`)) {
152
- $(`sec-${secId}`).classList.remove('hidden');
153
- }
154
  }
155
 
 
156
  function populateModelSelect() {
157
  const ms = $('model-select'); if(!ms) return;
158
  ms.innerHTML = "";
159
- let list = [];
160
- if(appSettings) {
161
- const mapKey = { "OpenRouter": "models_openrouter", "Nvidia NIM": "models_nvidia", "Google Gemini": "models_gemini", "AgentRouter": "models_agentrouter", "Custom OpenAI": "models_openai" };
162
- const k = mapKey[appSettings.provider];
163
- list = appSettings[k] || [];
164
- }
165
- if(list.length === 0) list = ["default-model"];
166
- 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); });
167
- }
168
-
169
- function renderDynamicLists() {
170
- const draw = (arr, listId, k) => {
171
- const lst = $(listId); if(!lst) return;
172
- lst.innerHTML = (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('');
173
- lst.querySelectorAll('.btn-del').forEach(b => {
174
- b.addEventListener('click', function() { appSettings[this.dataset.k].splice(Number(this.dataset.i), 1); renderDynamicLists(); });
175
- });
176
- };
177
  if(appSettings) {
178
- draw(appSettings.models_openrouter, 'list-or', 'models_openrouter');
179
- draw(appSettings.models_nvidia, 'list-nv', 'models_nvidia');
180
- draw(appSettings.models_gemini, 'list-gem', 'models_gemini');
181
- draw(appSettings.models_agentrouter, 'list-ar', 'models_agentrouter');
182
- draw(appSettings.models_openai, 'list-oai', 'models_openai');
183
  }
 
 
 
 
 
 
 
184
  }
185
 
186
  addEvt('btn-settings', 'click', () => {
187
  if(appSettings) {
188
  $('settings-provider').value = appSettings.provider;
189
  $('settings-apikey').value = appSettings.api_key || "";
190
- $('settings-url').value = appSettings.base_url || "";
191
- renderDynamicLists();
192
- syncProviderUI();
193
  }
194
  $('settings-modal').classList.remove('hidden');
195
  if(window.innerWidth < 768) toggleSidebar();
@@ -197,76 +243,62 @@ document.addEventListener('DOMContentLoaded', () => {
197
  addEvt('btn-close-settings', 'click', () => $('settings-modal').classList.add('hidden'));
198
  addEvt('btn-cancel-settings', 'click', () => $('settings-modal').classList.add('hidden'));
199
 
200
- addEvt('settings-provider', 'change', () => {
201
- const prov = $('settings-provider').value;
202
- 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" };
203
- if (urls[prov] && $('settings-url')) $('settings-url').value = urls[prov];
204
- syncProviderUI();
205
- });
206
-
207
- addEvt('btn-toggle-key', 'click', () => { const inp = $('settings-apikey'); if(inp) inp.type = inp.type === 'password' ? 'text' : 'password'; });
208
-
209
- const bindAdd = (btnId, inpId, listKey) => {
210
- const f = () => {
211
- const val = $(inpId)?.value.trim();
212
- if(!val || !appSettings) return;
213
- if(!appSettings[listKey].includes(val)) { appSettings[listKey].push(val); $(inpId).value = ""; renderDynamicLists(); }
214
- };
215
- addEvt(btnId, 'click', f);
216
- if($(inpId)) $(inpId).addEventListener('keydown', e => { if(e.key==='Enter') f(); });
217
- };
218
- bindAdd('btn-add-or', 'inp-or', 'models_openrouter');
219
- bindAdd('btn-add-nv', 'inp-nv', 'models_nvidia');
220
- bindAdd('btn-add-gem', 'inp-gem', 'models_gemini');
221
- bindAdd('btn-add-ar', 'inp-ar', 'models_agentrouter');
222
- bindAdd('btn-add-oai', 'inp-oai', 'models_openai');
223
-
224
  addEvt('btn-save-settings', async () => {
225
  const payload = {
226
  provider: $('settings-provider').value,
227
- base_url: $('settings-url').value,
228
  api_key: $('settings-apikey').value,
229
- models_openrouter: appSettings.models_openrouter,
230
- models_nvidia: appSettings.models_nvidia,
231
- models_gemini: appSettings.models_gemini,
232
- models_agentrouter: appSettings.models_agentrouter,
233
- models_openai: appSettings.models_openai,
234
- current_model: $('model-select').value // prevent override if empty
235
  };
236
  const res = await fetch('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
237
- if(res.ok) { appSettings = await res.json(); populateModelSelect(); $('settings-modal').classList.add('hidden'); }
 
 
 
 
238
  });
239
 
240
- // MISC (Sidebar, PDF, DOI)
241
- function toggleSidebar() { $('sidebar').classList.toggle('-translate-x-full'); $('sidebar-overlay').classList.toggle('hidden'); }
242
- addEvt('btn-hamburger', 'click', toggleSidebar); addEvt('btn-close-sidebar', 'click', toggleSidebar); addEvt('sidebar-overlay', 'click', toggleSidebar);
243
-
244
- function clr() {
245
- if(!currentSid) return; sessions[currentSid].messages = []; sessions[currentSid].title = "New Chat"; save(); loadSession(currentSid);
246
- }
247
- addEvt('btn-clear-chat-top', 'click', clr); addEvt('btn-clear-chat-mobile', 'click', clr);
248
-
249
  addEvt('btn-attach', 'click', () => $('pdf-input').click());
250
- addEvt('btn-remove-attach', 'click', () => { pdfText = ""; $('attach-badge').classList.add('hidden'); });
251
  if($('pdf-input')) {
252
  $('pdf-input').addEventListener('change', async e => {
253
- const f = e.target.files[0]; if(!f) return; const fd = new FormData(); fd.append('file', f);
 
254
  $('attach-badge').classList.remove('hidden'); $('attach-name').textContent = "Extracting...";
255
- const res = await fetch('/api/upload_pdf', { method: 'POST', body: fd });
256
- const d = await res.json();
257
- if(res.ok) { pdfText = d.text; pdfFilename = d.filename; $('attach-name').textContent = d.filename; } else { alert(d.error); $('attach-badge').classList.add('hidden'); }
 
 
 
 
 
 
 
258
  });
259
  }
 
 
 
 
 
 
 
 
 
 
260
 
261
- addEvt('btn-doi', 'click', () => { $('doi-modal').classList.remove('hidden'); if(window.innerWidth < 768) toggleSidebar(); });
262
- addEvt('btn-close-doi', 'click', () => $('doi-modal').classList.add('hidden'));
263
- addEvt('btn-validate-doi', 'click', async () => {
264
- const doi = $('doi-input').value.trim(); if(!doi) return;
265
- $('doi-result').classList.remove('hidden'); $('doi-result').innerHTML = "Validating...";
266
- const res = await fetch('/api/validate_doi', { method: 'POST', body: JSON.stringify({doi}) });
267
- const d = await res.json();
268
- if(!res.ok || d.error) $('doi-result').innerHTML = d.error || "Gagal"; else $('doi-result').innerHTML = `<b>${d.title}</b><br>${d.authors} (${d.year})`;
269
  });
 
270
 
 
271
  init();
 
 
 
272
  });
 
1
+ /**
2
+ * ORBIT – Educational Research Assistant
3
+ * SCRIPT FINAL: Welcome Screen Injector + PWA Fixed
4
+ */
5
+
6
  document.addEventListener('DOMContentLoaded', () => {
7
  const $ = id => document.getElementById(id);
8
  const addEvt = (id, event, handler) => { if($(id)) $(id).addEventListener(event, handler); };
9
 
10
  let currentSid = null;
11
+ let sessions = {};
 
 
12
  let appSettings = null;
13
  let isBusy = false;
14
+ let pdfText = "";
15
+ let pdfFilename = "";
16
+
17
+ // Template Welcome Screen (Disuntik manual tiap buka chat baru)
18
+ const welcomeHTML = `
19
+ <div id="welcome-msg" class="flex flex-col items-center justify-center h-full text-center py-20 px-4">
20
+ <img src="/static/icon.png" class="w-16 h-16 mb-4 shadow-sm rounded-2xl" onerror="this.style.display='none'">
21
+ <h2 class="text-2xl font-bold text-gray-800 mb-2">Welcome to ORBIT</h2>
22
+ <p class="text-gray-500 text-sm max-w-md mt-2">Your AI-powered Educational Research Assistant. Upload a document or type a prompt below to begin.</p>
23
+ </div>`;
24
+
25
+ // 1. DATA RECOVERY
26
+ try {
27
+ const stored = localStorage.getItem('orbit_sessions_v8');
28
+ sessions = stored ? JSON.parse(stored) : {};
29
+ if (typeof sessions !== 'object' || Array.isArray(sessions)) sessions = {};
30
+ } catch(e) {
31
+ sessions = {};
32
+ }
33
 
34
+ // 2. PWA INSTALL LOGIC (PAKSA MUNCUL)
35
  let deferredPrompt;
36
  if ($('btn-install-pwa') && !window.matchMedia('(display-mode: standalone)').matches) {
37
  $('btn-install-pwa').classList.remove('hidden');
38
  }
39
+
40
  window.addEventListener('beforeinstallprompt', (e) => {
41
  e.preventDefault();
42
  deferredPrompt = e;
 
49
  if(outcome === 'accepted' && $('btn-install-pwa')) $('btn-install-pwa').classList.add('hidden');
50
  deferredPrompt = null;
51
  } else {
 
52
  alert("Chrome memblokir pop-up otomatis.\n\nUntuk Install: Klik ikon Titik-Tiga (⋮) di pojok kanan atas browser Anda, lalu pilih 'Tambahkan ke Layar Utama' (Add to Home screen).");
53
  }
54
  });
55
 
56
+ // 3. BOOT & INIT
57
  async function init() {
58
  try {
59
+ const meRes = await fetch('/api/me');
60
+ if(meRes.status === 401) { window.location.href = '/login'; return; }
61
+ const user = await meRes.json();
62
+
63
  if($('user-name')) $('user-name').textContent = user.name || user.email;
64
  if($('user-avatar') && user.picture) $('user-avatar').src = user.picture;
65
 
66
  const setRes = await fetch('/api/settings');
67
+ if(setRes.ok) appSettings = await setRes.json();
68
+ } catch(e) {
69
+ console.error("Init failed:", e);
 
 
 
 
 
 
70
  } finally {
71
  populateModelSelect();
72
  const ids = Object.keys(sessions).sort((a,b) => b-a);
 
74
  }
75
  }
76
 
77
+ // 4. SESSION MANAGEMENT
78
+ function save() { localStorage.setItem('orbit_sessions_v8', JSON.stringify(sessions)); }
79
+
80
+ function newSession() {
81
+ const id = Date.now().toString();
82
+ sessions[id] = { title: "New Chat", messages: [] };
83
+ loadSession(id);
84
+ }
85
  addEvt('btn-new-chat', 'click', newSession);
86
 
87
  function loadSession(id) {
88
+ if(!sessions[id]) return;
89
  currentSid = id;
90
+ const cm = $('chat-messages');
91
+ if(!cm) return;
92
+
93
+ // Bersihkan area chat
94
  cm.innerHTML = '';
95
+
96
+ if(sessions[id].messages && sessions[id].messages.length > 0) {
97
+ // Kalau ada chat, render semua pesannya
98
  sessions[id].messages.forEach(m => renderBubble(m.role, m.displayContent || m.content));
99
  } else {
100
+ // Kalau chatnya kosong, SUNTIK ULANG WELCOME SCREEN!
101
+ cm.innerHTML = welcomeHTML;
102
  }
103
+
104
+ renderHistory();
105
+ cm.scrollTop = cm.scrollHeight;
106
  }
107
 
108
  function renderHistory() {
109
  const list = $('history-list'); if(!list) return;
110
  const ids = Object.keys(sessions).sort((a,b) => b-a);
111
+ if(ids.length === 0) {
112
+ list.innerHTML = '<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>';
113
+ return;
114
+ }
115
  list.innerHTML = ids.map(id => {
116
+ const active = (id === currentSid) ? 'bg-accent-light text-accent font-semibold shadow-sm' : 'text-gray-600 hover:bg-white';
117
+ return `<button onclick="window.ls('${id}')" class="w-full text-left px-3 py-2.5 rounded-xl text-xs truncate font-medium transition-all ${active}">${sessions[id].title || "New Chat"}</button>`;
118
  }).join('');
119
  }
120
  window.ls = id => { loadSession(id); if(window.innerWidth < 768) toggleSidebar(); };
121
 
122
+ // 5. CHAT UI RENDERING
123
  function renderBubble(role, content) {
124
  const isUser = (role === 'user');
125
  const wrap = document.createElement('div');
126
  wrap.className = `flex mb-6 ${isUser ? 'justify-end' : 'justify-start'}`;
127
+
128
  if(isUser) {
129
+ wrap.innerHTML = `<div class="bg-accent text-white p-4 rounded-2xl rounded-tr-none max-w-[85%] text-[15px] shadow-sm leading-relaxed">${content}</div>`;
130
  } else {
131
+ let html = content;
132
+ try {
133
+ html = (typeof marked !== 'undefined') ? marked.parse(content) : content.replace(/\n/g, '<br>');
134
+ } catch(e) { html = content; }
135
+ wrap.innerHTML = `
136
+ <div class="flex gap-4 items-start w-full">
137
+ <img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 shadow-sm shrink-0" onerror="this.style.display='none'">
138
+ <div class="bg-[#f8f9fa] border border-slate-200 p-5 rounded-2xl rounded-tl-none max-w-[90%] md:max-w-[85%] prose-orbit shadow-sm w-full">${html}</div>
139
+ </div>`;
140
  }
141
+ $('chat-messages').appendChild(wrap);
142
+ $('chat-messages').scrollTop = $('chat-messages').scrollHeight;
143
  }
144
 
145
+ // 6. API INTERACTION (Send Chat)
146
  async function sendChat() {
147
  if(isBusy) return;
148
+ const inputEl = $('chat-textarea');
149
+ const raw = inputEl.value.trim();
150
  if(!raw && !pdfText) return;
151
 
152
+ inputEl.value = '';
153
+ inputEl.style.height = 'auto';
154
 
155
+ // Hapus Welcome Screen pas mulai ngetik chat
156
+ if($('welcome-msg')) $('welcome-msg').remove();
157
+
158
+ let fullPrompt = raw;
159
+ let displayPrompt = raw.replace(/\n/g, '<br>');
160
+
161
+ if(pdfText) {
162
+ fullPrompt = `[Attached PDF: ${pdfFilename}]\n${pdfText}\n\nUser Question: ${raw}`;
163
+ displayPrompt = `<div class="bg-blue-500 text-white text-[10px] px-2 py-1 rounded w-fit mb-1 font-bold">📄 ${pdfFilename}</div>${displayPrompt}`;
164
+ pdfText = ""; pdfFilename = "";
165
+ $('attach-badge').classList.add('hidden');
166
+ }
167
 
168
+ // Set title dari pesan pertama
169
+ if(!sessions[currentSid].messages || sessions[currentSid].messages.length === 0) {
170
+ sessions[currentSid].title = raw.substring(0, 25) || "New Chat";
171
+ }
172
+
173
+ sessions[currentSid].messages.push({ role: 'user', content: fullPrompt, displayContent: displayPrompt });
174
+ renderBubble('user', displayPrompt);
175
 
176
  isBusy = true; $('btn-send').disabled = true;
177
  const loadId = 'load-' + Date.now();
178
+ $('chat-messages').insertAdjacentHTML('beforeend', `<div id="${loadId}" class="flex mb-6 gap-4 items-start animate-pulse"><img src="/static/icon.png" class="w-8 h-8 rounded-full shrink-0"><div class="bg-slate-50 p-4 rounded-2xl rounded-tl-none text-gray-400 text-sm">Analyzing...</div></div>`);
179
+ $('chat-messages').scrollTop = $('chat-messages').scrollHeight;
180
 
181
  try {
182
  const res = await fetch('/api/chat', {
183
  method: 'POST',
184
  headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify({
186
+ prompt: fullPrompt,
187
+ model: $('model-select').value,
188
+ messages: sessions[currentSid].messages.slice(0,-1)
189
+ })
190
  });
191
  const data = await res.json();
192
  if($(loadId)) $(loadId).remove();
193
+
194
  if(!res.ok) throw new Error(data.error || "Server error");
195
+
196
  sessions[currentSid].messages.push({ role: 'assistant', content: data.reply });
197
  renderBubble('assistant', data.reply);
198
  save(); renderHistory();
199
  } catch(e) {
200
  if($(loadId)) $(loadId).remove();
201
  renderBubble('assistant', `**Error:** ${e.message}`);
202
+ } finally {
203
+ isBusy = false; $('btn-send').disabled = false;
204
+ }
205
  }
206
 
207
  addEvt('btn-send', 'click', sendChat);
208
  addEvt('chat-textarea', 'keydown', e => { if(e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChat(); } });
209
+ if($('chat-textarea')) {
210
+ $('chat-textarea').addEventListener('input', function() {
211
+ this.style.height = 'auto';
212
+ this.style.height = Math.min(this.scrollHeight, 160) + 'px';
213
+ });
 
 
 
 
 
 
 
 
 
214
  }
215
 
216
+ // 7. MODALS LOGIC
217
  function populateModelSelect() {
218
  const ms = $('model-select'); if(!ms) return;
219
  ms.innerHTML = "";
220
+ let list = ["gemini-1.5-pro-latest", "gemini-1.5-flash-latest"];
221
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  if(appSettings) {
223
+ if(appSettings.provider === "OpenRouter") list = appSettings.models_openrouter || list;
224
+ else if(appSettings.provider === "Nvidia NIM") list = appSettings.models_nvidia || list;
 
 
 
225
  }
226
+
227
+ list.forEach(m => {
228
+ const opt = document.createElement('option');
229
+ opt.value = m; opt.textContent = m;
230
+ if(appSettings && m === appSettings.current_model) opt.selected = true;
231
+ ms.appendChild(opt);
232
+ });
233
  }
234
 
235
  addEvt('btn-settings', 'click', () => {
236
  if(appSettings) {
237
  $('settings-provider').value = appSettings.provider;
238
  $('settings-apikey').value = appSettings.api_key || "";
 
 
 
239
  }
240
  $('settings-modal').classList.remove('hidden');
241
  if(window.innerWidth < 768) toggleSidebar();
 
243
  addEvt('btn-close-settings', 'click', () => $('settings-modal').classList.add('hidden'));
244
  addEvt('btn-cancel-settings', 'click', () => $('settings-modal').classList.add('hidden'));
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  addEvt('btn-save-settings', async () => {
247
  const payload = {
248
  provider: $('settings-provider').value,
 
249
  api_key: $('settings-apikey').value,
250
+ current_model: $('model-select').value
 
 
 
 
 
251
  };
252
  const res = await fetch('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
253
+ if(res.ok) {
254
+ appSettings = await res.json();
255
+ populateModelSelect();
256
+ $('settings-modal').classList.add('hidden');
257
+ }
258
  });
259
 
260
+ // 8. PDF UPLOAD
 
 
 
 
 
 
 
 
261
  addEvt('btn-attach', 'click', () => $('pdf-input').click());
 
262
  if($('pdf-input')) {
263
  $('pdf-input').addEventListener('change', async e => {
264
+ const f = e.target.files[0]; if(!f) return;
265
+ const fd = new FormData(); fd.append('file', f);
266
  $('attach-badge').classList.remove('hidden'); $('attach-name').textContent = "Extracting...";
267
+ try {
268
+ const res = await fetch('/api/upload_pdf', { method: 'POST', body: fd });
269
+ const d = await res.json();
270
+ if(res.ok) {
271
+ pdfText = d.text; pdfFilename = d.filename; $('attach-name').textContent = d.filename;
272
+ } else throw new Error(d.error);
273
+ } catch(err) {
274
+ alert(err.message);
275
+ $('attach-badge').classList.add('hidden');
276
+ }
277
  });
278
  }
279
+ addEvt('btn-remove-attach', 'click', () => { pdfText = ""; $('attach-badge').classList.add('hidden'); });
280
+
281
+ // 9. UI TOGGLES
282
+ function toggleSidebar() {
283
+ $('sidebar').classList.toggle('-translate-x-full');
284
+ $('sidebar-overlay').classList.toggle('hidden');
285
+ }
286
+ addEvt('btn-hamburger', 'click', toggleSidebar);
287
+ addEvt('btn-close-sidebar', 'click', toggleSidebar);
288
+ addEvt('sidebar-overlay', 'click', toggleSidebar);
289
 
290
+ addEvt('btn-clear-chat-top', 'click', () => {
291
+ if(confirm("Hapus obrolan ini?")) {
292
+ sessions[currentSid].messages = [];
293
+ sessions[currentSid].title = "New Chat";
294
+ save(); loadSession(currentSid); // Ini bakal langsung nge-load ulang welcome screen
295
+ }
 
 
296
  });
297
+ addEvt('btn-clear-chat-mobile', 'click', () => $('btn-clear-chat-top').click());
298
 
299
+ // 10. START
300
  init();
301
+ if ('serviceWorker' in navigator) {
302
+ window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js', { scope: '/' }));
303
+ }
304
  });