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

Update static/js/script.js

Browse files
Files changed (1) hide show
  1. static/js/script.js +137 -169
static/js/script.js CHANGED
@@ -1,44 +1,20 @@
1
- /**
2
- * ORBIT – Educational Research Assistant
3
- * V-FINAL SCRIPT: Welcome Screen Fix & Forced PWA Button
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 HTML untuk Welcome Screen yang re-usable
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 (DIPAKSA MUNCUL)
35
  let deferredPrompt;
36
-
37
- // Paksa tombol muncul, KECUALI webnya udah di-install (jalan sbg aplikasi)
38
  if ($('btn-install-pwa') && !window.matchMedia('(display-mode: standalone)').matches) {
39
  $('btn-install-pwa').classList.remove('hidden');
40
  }
41
-
42
  window.addEventListener('beforeinstallprompt', (e) => {
43
  e.preventDefault();
44
  deferredPrompt = e;
@@ -51,25 +27,30 @@ document.addEventListener('DOMContentLoaded', () => {
51
  if(outcome === 'accepted' && $('btn-install-pwa')) $('btn-install-pwa').classList.add('hidden');
52
  deferredPrompt = null;
53
  } else {
54
- // Fallback kalau Chrome memblokir pop-up PWA
55
- 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).");
56
  }
57
  });
58
 
59
- // 3. BOOT & INIT
60
  async function init() {
61
  try {
62
- const meRes = await fetch('/api/me');
63
- if(meRes.status === 401) { window.location.href = '/login'; return; }
64
- const user = await meRes.json();
65
-
66
  if($('user-name')) $('user-name').textContent = user.name || user.email;
67
  if($('user-avatar') && user.picture) $('user-avatar').src = user.picture;
68
 
69
  const setRes = await fetch('/api/settings');
70
- if(setRes.ok) appSettings = await setRes.json();
71
- } catch(e) {
72
- console.error("Init failed:", e);
 
 
 
 
 
 
73
  } finally {
74
  populateModelSelect();
75
  const ids = Object.keys(sessions).sort((a,b) => b-a);
@@ -77,165 +58,138 @@ document.addEventListener('DOMContentLoaded', () => {
77
  }
78
  }
79
 
80
- // 4. SESSION MANAGEMENT
81
- function save() { localStorage.setItem('orbit_sessions_v8', JSON.stringify(sessions)); }
82
-
83
- function newSession() {
84
- const id = Date.now().toString();
85
- sessions[id] = { title: "New Chat", messages: [] };
86
- loadSession(id);
87
- }
88
  addEvt('btn-new-chat', 'click', newSession);
89
 
90
  function loadSession(id) {
91
- if(!sessions[id]) return;
92
  currentSid = id;
93
- const cm = $('chat-messages');
94
- if(!cm) return;
95
-
96
- // Bersihkan area chat
97
  cm.innerHTML = '';
98
-
99
  if(sessions[id].messages && sessions[id].messages.length) {
100
- // Render ulang semua bubble chat
101
  sessions[id].messages.forEach(m => renderBubble(m.role, m.displayContent || m.content));
102
  } else {
103
- // Inject ulang Welcome Screen jika kosong (ANTI BLANK PUTIH)
104
- cm.innerHTML = welcomeHTML;
105
  }
106
-
107
- renderHistory();
108
- cm.scrollTop = cm.scrollHeight;
109
  }
110
 
111
  function renderHistory() {
112
  const list = $('history-list'); if(!list) return;
113
  const ids = Object.keys(sessions).sort((a,b) => b-a);
114
- if(ids.length === 0) {
115
- list.innerHTML = '<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>';
116
- return;
117
- }
118
  list.innerHTML = ids.map(id => {
119
- const active = (id === currentSid) ? 'bg-accent-light text-accent font-semibold shadow-sm' : 'text-gray-600 hover:bg-white';
120
- 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>`;
121
  }).join('');
122
  }
123
  window.ls = id => { loadSession(id); if(window.innerWidth < 768) toggleSidebar(); };
124
 
125
- // 5. CHAT UI RENDERING
126
  function renderBubble(role, content) {
127
  const isUser = (role === 'user');
128
  const wrap = document.createElement('div');
129
  wrap.className = `flex mb-6 ${isUser ? 'justify-end' : 'justify-start'}`;
130
-
131
  if(isUser) {
132
- 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>`;
133
  } else {
134
- let html = content;
135
- try {
136
- html = (typeof marked !== 'undefined') ? marked.parse(content) : content.replace(/\n/g, '<br>');
137
- } catch(e) { html = content; }
138
- wrap.innerHTML = `
139
- <div class="flex gap-4 items-start w-full">
140
- <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'">
141
- <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>
142
- </div>`;
143
  }
144
- $('chat-messages').appendChild(wrap);
145
- $('chat-messages').scrollTop = $('chat-messages').scrollHeight;
146
  }
147
 
148
- // 6. API INTERACTION (Send Chat)
149
  async function sendChat() {
150
  if(isBusy) return;
151
- const inputEl = $('chat-textarea');
152
- const raw = inputEl.value.trim();
153
  if(!raw && !pdfText) return;
154
 
155
- inputEl.value = '';
156
- inputEl.style.height = 'auto';
157
- if($('welcome-msg')) $('welcome-msg').remove();
158
 
159
- let fullPrompt = raw;
160
- let displayPrompt = raw.replace(/\n/g, '<br>');
161
-
162
- if(pdfText) {
163
- fullPrompt = `[Attached PDF: ${pdfFilename}]\n${pdfText}\n\nUser Question: ${raw}`;
164
- displayPrompt = `<div class="bg-blue-500 text-white text-[10px] px-2 py-1 rounded w-fit mb-1 font-bold">📄 ${pdfFilename}</div>${displayPrompt}`;
165
- pdfText = ""; pdfFilename = "";
166
- $('attach-badge').classList.add('hidden');
167
- }
168
 
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,62 +197,76 @@ document.addEventListener('DOMContentLoaded', () => {
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);
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
  });
 
 
 
 
 
 
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
  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
  }
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
  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
  });