xenux4u commited on
Commit
4e18ba0
·
verified ·
1 Parent(s): 0709b0b

Update static/js/script.js

Browse files
Files changed (1) hide show
  1. static/js/script.js +172 -141
static/js/script.js CHANGED
@@ -1,6 +1,6 @@
1
  /**
2
  * ORBIT – Educational Research Assistant
3
- * Logic with Multi-Session History Support
4
  */
5
 
6
  let currentSid = null;
@@ -15,7 +15,13 @@ let pdfFilename = "";
15
  async function initApp() {
16
  try {
17
  const userRes = await fetch('/api/me');
18
- if (!userRes.ok) throw new Error('Not logged in');
 
 
 
 
 
 
19
  const userData = await userRes.json();
20
 
21
  document.getElementById('user-name').textContent = userData.name || userData.email;
@@ -32,7 +38,7 @@ async function initApp() {
32
  renderHistoryList();
33
 
34
  // Load sesi terakhir jika ada, kalau tak buat sesi baru
35
- const ids = Object.keys(sessions).sort((a, b) => b - a);
36
  if (ids.length > 0) {
37
  switchSession(ids[0]);
38
  } else {
@@ -40,8 +46,8 @@ async function initApp() {
40
  }
41
 
42
  } catch (error) {
43
- console.error("Init failed:", error);
44
- window.location.href = '/login';
45
  }
46
  }
47
 
@@ -81,7 +87,8 @@ function newSession() {
81
  displayMessages();
82
  }
83
 
84
- function switchSession(id) {
 
85
  if (!sessions[id]) return;
86
  currentSid = id;
87
  renderHistoryList();
@@ -94,7 +101,7 @@ function displayMessages() {
94
  if(!chatBox) return;
95
  chatBox.innerHTML = '';
96
 
97
- const msgs = sessions[currentSid].messages;
98
 
99
  if (msgs.length === 0) {
100
  chatBox.innerHTML = `
@@ -118,7 +125,7 @@ function displayMessages() {
118
  function renderHistoryList() {
119
  const list = document.getElementById('history-list');
120
  if(!list) return;
121
- const ids = Object.keys(sessions).sort((a, b) => b - a);
122
 
123
  if(ids.length === 0) {
124
  list.innerHTML = `<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>`;
@@ -128,7 +135,7 @@ function renderHistoryList() {
128
  list.innerHTML = ids.map(id => {
129
  const activeClass = (id === currentSid) ? 'bg-white text-accent shadow-sm ring-1 ring-slate-200' : 'text-gray-600 hover:bg-white hover:text-accent';
130
  return `
131
- <button onclick="switchSession('${id}')" class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all text-left truncate ${activeClass}">
132
  <svg class="w-4 h-4 shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
133
  <span class="truncate">${sessions[id].title}</span>
134
  </button>`;
@@ -140,24 +147,26 @@ const sidebar = document.getElementById('sidebar');
140
  const overlay = document.getElementById('sidebar-overlay');
141
 
142
  function toggleSidebar() {
143
- sidebar.classList.toggle('-translate-x-full');
144
- overlay.classList.toggle('hidden');
145
  }
146
 
147
- document.getElementById('btn-hamburger').addEventListener('click', toggleSidebar);
148
  if(document.getElementById('btn-close-sidebar')) document.getElementById('btn-close-sidebar').addEventListener('click', toggleSidebar);
149
- overlay.addEventListener('click', toggleSidebar);
150
-
151
- document.getElementById('btn-new-chat').addEventListener('click', newSession);
152
- document.getElementById('btn-clear-chat-top').addEventListener('click', () => {
153
- if(confirm("Clear this conversation?")) {
154
- sessions[currentSid].messages = [];
155
- sessions[currentSid].title = "New Chat";
156
- saveToLocalStorage();
157
- displayMessages();
158
- renderHistoryList();
159
- }
160
- });
 
 
161
 
162
  // 4. Chat Logic
163
  const tx = document.getElementById('chat-textarea');
@@ -165,149 +174,171 @@ const btnSend = document.getElementById('btn-send');
165
 
166
  function renderUserBubble(content) {
167
  const chatBox = document.getElementById('chat-messages');
168
- chatBox.innerHTML += `<div class="flex justify-end mb-6"><div class="bg-accent text-white p-4 rounded-2xl rounded-tr-none max-w-[85%] shadow-sm text-[15px] leading-relaxed">${content.replace(/\n/g, '<br>')}</div></div>`;
 
169
  }
170
 
171
  function renderAiBubble(content) {
172
  const chatBox = document.getElementById('chat-messages');
173
- const renderedMarkdown = marked.parse(content);
174
- chatBox.innerHTML += `<div class="flex mb-6 gap-4 items-start"><img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 mt-1 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 shadow-sm">${renderedMarkdown}</div></div>`;
175
- }
176
-
177
- btnSend.addEventListener('click', async () => {
178
- let prompt = tx.value.trim();
179
- if(!prompt && !pdfText) return;
180
-
181
- if(document.getElementById('welcome-msg')) document.getElementById('welcome-msg').remove();
182
-
183
- tx.value = '';
184
- tx.style.height = 'auto';
185
- btnSend.disabled = true;
186
-
187
- let displayPrompt = prompt;
188
- let fullSystemPrompt = prompt;
189
-
190
- if (pdfText) {
191
- displayPrompt = `<div class="flex items-center gap-1.5 bg-blue-500/30 text-white text-xs px-2.5 py-1.5 rounded-lg mb-2 font-medium w-fit"><svg class="w-3.5 h-3.5" 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> ${pdfFilename}</div>${prompt}`;
192
- fullSystemPrompt = `[Attached Document: ${pdfFilename}]\n${pdfText}\n\nUser Question: ${prompt}`;
193
- removeAttach();
194
- }
195
-
196
- // Kemaskini History tajuk jika ini mesej pertama
197
- if(sessions[currentSid].messages.length === 0) {
198
- sessions[currentSid].title = prompt.substring(0, 25) || pdfFilename.substring(0, 25);
199
  }
 
200
 
201
- sessions[currentSid].messages.push({ role: "user", content: fullSystemPrompt, displayContent: displayPrompt });
202
- renderUserBubble(displayPrompt);
203
-
204
- const loadId = 'load-' + Date.now();
205
- document.getElementById('chat-messages').innerHTML += `<div id="${loadId}" class="flex mb-6 gap-4 items-start"><img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 mt-1 shadow-sm shrink-0"><div class="bg-[#f8f9fa] border border-slate-200 p-4 rounded-2xl rounded-tl-none text-gray-500 flex items-center gap-2"><svg class="w-4 h-4 animate-spin text-accent" 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> Analyzing...</div></div>`;
206
- document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
207
 
208
- try {
209
- const res = await fetch('/api/chat', {
210
- method: 'POST',
211
- headers: { 'Content-Type': 'application/json' },
212
- body: JSON.stringify({
213
- prompt: fullSystemPrompt,
214
- messages: sessions[currentSid].messages.slice(0, -1),
215
- model: document.getElementById('model-select').value
216
- })
217
- });
218
- const data = await res.json();
219
- document.getElementById(loadId).remove();
220
 
221
- if(!res.ok) throw new Error(data.error || 'Server error');
 
222
 
223
- sessions[currentSid].messages.push({ role: "assistant", content: data.reply });
 
 
 
 
 
 
 
 
 
 
 
224
  saveToLocalStorage();
225
  renderHistoryList();
226
- renderAiBubble(data.reply);
227
 
228
- } catch (e) {
229
- if(document.getElementById(loadId)) document.getElementById(loadId).remove();
230
- document.getElementById('chat-messages').innerHTML += `<div class="flex mb-6 gap-4 items-start"><div class="bg-red-50 text-red-600 p-4 rounded-2xl rounded-tl-none border border-red-100 max-w-[85%] text-sm shadow-sm"><strong>Error:</strong> ${e.message}</div></div>`;
231
- }
232
-
233
- btnSend.disabled = false;
234
- document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
235
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
  // 5. Tool Modals
238
  const setModal = document.getElementById('settings-modal');
239
  const doiModal = document.getElementById('doi-modal');
240
 
241
- document.getElementById('btn-open-settings').addEventListener('click', () => {
242
- if(currentSettings) {
243
- document.getElementById('settings-provider').value = currentSettings.provider;
244
- document.getElementById('settings-url').value = currentSettings.base_url;
245
- document.getElementById('settings-apikey').value = currentSettings.api_key;
246
- renderModelLists();
247
- }
248
- setModal.classList.remove('hidden');
249
- if(window.innerWidth < 768) toggleSidebar();
250
- });
251
-
252
- document.getElementById('btn-save-settings').addEventListener('click', async () => {
253
- const payload = {
254
- provider: document.getElementById('settings-provider').value,
255
- base_url: document.getElementById('settings-url').value,
256
- api_key: document.getElementById('settings-apikey').value,
257
- models_openrouter: modelsOR,
258
- models_nvidia: modelsNV,
259
- current_model: document.getElementById('model-select').value
260
- };
261
- await fetch('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
262
- setModal.classList.add('hidden');
263
- initApp();
264
- });
265
 
266
- document.getElementById('btn-open-doi').addEventListener('click', () => {
267
- doiModal.classList.remove('hidden');
268
- if(window.innerWidth < 768) toggleSidebar();
269
- });
270
-
271
- document.getElementById('btn-validate-doi-submit').addEventListener('click', async () => {
272
- const doi = document.getElementById('doi-input').value;
273
- const resDiv = document.getElementById('doi-result');
274
- if(!doi) return;
275
- resDiv.classList.remove('hidden');
276
- resDiv.innerHTML = `Validating...`;
277
- const res = await fetch('/api/validate_doi', { method: 'POST', body: JSON.stringify({doi: doi}) });
278
- const data = await res.json();
279
- if(data.error) resDiv.innerHTML = `<span class="text-red-500">${data.error}</span>`;
280
- else resDiv.innerHTML = `<strong>${data.title}</strong><br><span class="text-xs">${data.authors} (${data.year})</span>`;
281
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  // 6. PDF Logic
284
  function removeAttach() {
285
  pdfText = ""; pdfFilename = "";
286
- document.getElementById('pdf-input').value = "";
287
- document.getElementById('attach-badge').classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
- document.getElementById('btn-attach').addEventListener('click', () => document.getElementById('pdf-input').click());
290
- document.getElementById('pdf-input').addEventListener('change', async (e) => {
291
- const file = e.target.files[0];
292
- if(!file) return;
293
- const formData = new FormData();
294
- formData.append('file', file);
295
- document.getElementById('attach-name').textContent = "Extracting...";
296
- document.getElementById('attach-badge').classList.remove('hidden');
297
- const res = await fetch('/api/upload_pdf', { method: 'POST', body: formData });
298
- const data = await res.json();
299
- if(res.ok) { pdfText = data.text; pdfFilename = data.filename; document.getElementById('attach-name').textContent = pdfFilename; }
300
- else { alert(data.error); removeAttach(); }
301
- });
302
- document.getElementById('btn-remove-attach').addEventListener('click', removeAttach);
303
 
304
  // Close Modals
305
- document.getElementById('btn-close-settings').onclick = () => setModal.classList.add('hidden');
306
- document.getElementById('btn-cancel-settings').onclick = () => setModal.classList.add('hidden');
307
- document.getElementById('btn-close-doi').onclick = () => doiModal.classList.add('hidden');
308
 
309
- tx.addEventListener("input", function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; });
310
- tx.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); btnSend.click(); }});
 
 
311
 
312
  // Boot
313
  initApp();
 
1
  /**
2
  * ORBIT – Educational Research Assistant
3
+ * Logic with Multi-Session History Support & Crash Protection
4
  */
5
 
6
  let currentSid = null;
 
15
  async function initApp() {
16
  try {
17
  const userRes = await fetch('/api/me');
18
+ // HANYA lempar ke login kalau benar-benar statusnya 401 (Unauthorized)
19
+ if (userRes.status === 401) {
20
+ window.location.href = '/login';
21
+ return;
22
+ }
23
+
24
+ if (!userRes.ok) throw new Error('Gagal memuat data user');
25
  const userData = await userRes.json();
26
 
27
  document.getElementById('user-name').textContent = userData.name || userData.email;
 
38
  renderHistoryList();
39
 
40
  // Load sesi terakhir jika ada, kalau tak buat sesi baru
41
+ const ids = Object.keys(sessions).sort((a, b) => Number(b) - Number(a));
42
  if (ids.length > 0) {
43
  switchSession(ids[0]);
44
  } else {
 
46
  }
47
 
48
  } catch (error) {
49
+ // Skrip berhenti di sini kalau ada error ringan, tapi TIDAK AKAN infinite loop
50
+ console.error("System Init Error:", error);
51
  }
52
  }
53
 
 
87
  displayMessages();
88
  }
89
 
90
+ // Jadikan fungsi ini global agar bisa dipanggil dari onclick HTML
91
+ window.switchSession = function(id) {
92
  if (!sessions[id]) return;
93
  currentSid = id;
94
  renderHistoryList();
 
101
  if(!chatBox) return;
102
  chatBox.innerHTML = '';
103
 
104
+ const msgs = sessions[currentSid]?.messages || [];
105
 
106
  if (msgs.length === 0) {
107
  chatBox.innerHTML = `
 
125
  function renderHistoryList() {
126
  const list = document.getElementById('history-list');
127
  if(!list) return;
128
+ const ids = Object.keys(sessions).sort((a, b) => Number(b) - Number(a));
129
 
130
  if(ids.length === 0) {
131
  list.innerHTML = `<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>`;
 
135
  list.innerHTML = ids.map(id => {
136
  const activeClass = (id === currentSid) ? 'bg-white text-accent shadow-sm ring-1 ring-slate-200' : 'text-gray-600 hover:bg-white hover:text-accent';
137
  return `
138
+ <button onclick="window.switchSession('${id}')" class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-all text-left truncate ${activeClass}">
139
  <svg class="w-4 h-4 shrink-0 opacity-60" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
140
  <span class="truncate">${sessions[id].title}</span>
141
  </button>`;
 
147
  const overlay = document.getElementById('sidebar-overlay');
148
 
149
  function toggleSidebar() {
150
+ if(sidebar) sidebar.classList.toggle('-translate-x-full');
151
+ if(overlay) overlay.classList.toggle('hidden');
152
  }
153
 
154
+ if(document.getElementById('btn-hamburger')) document.getElementById('btn-hamburger').addEventListener('click', toggleSidebar);
155
  if(document.getElementById('btn-close-sidebar')) document.getElementById('btn-close-sidebar').addEventListener('click', toggleSidebar);
156
+ if(overlay) overlay.addEventListener('click', toggleSidebar);
157
+
158
+ if(document.getElementById('btn-new-chat')) document.getElementById('btn-new-chat').addEventListener('click', newSession);
159
+ if(document.getElementById('btn-clear-chat-top')) {
160
+ document.getElementById('btn-clear-chat-top').addEventListener('click', () => {
161
+ if(confirm("Clear this conversation?")) {
162
+ sessions[currentSid].messages = [];
163
+ sessions[currentSid].title = "New Chat";
164
+ saveToLocalStorage();
165
+ displayMessages();
166
+ renderHistoryList();
167
+ }
168
+ });
169
+ }
170
 
171
  // 4. Chat Logic
172
  const tx = document.getElementById('chat-textarea');
 
174
 
175
  function renderUserBubble(content) {
176
  const chatBox = document.getElementById('chat-messages');
177
+ const safeContent = String(content || "");
178
+ chatBox.innerHTML += `<div class="flex justify-end mb-6"><div class="bg-accent text-white p-4 rounded-2xl rounded-tr-none max-w-[85%] shadow-sm text-[15px] leading-relaxed">${safeContent.replace(/\n/g, '<br>')}</div></div>`;
179
  }
180
 
181
  function renderAiBubble(content) {
182
  const chatBox = document.getElementById('chat-messages');
183
+ try {
184
+ const safeContent = String(content || "");
185
+ const renderedMarkdown = marked.parse(safeContent);
186
+ chatBox.innerHTML += `<div class="flex mb-6 gap-4 items-start"><img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 mt-1 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 shadow-sm">${renderedMarkdown}</div></div>`;
187
+ } catch(e) {
188
+ console.error("Markdown parse error:", e);
189
+ chatBox.innerHTML += `<div class="flex mb-6 gap-4 items-start"><img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 mt-1 shadow-sm shrink-0" onerror="this.style.display='none'"><div class="bg-[#f8f9fa] border border-red-200 text-red-500 p-5 rounded-2xl rounded-tl-none max-w-[90%] md:max-w-[85%] prose-orbit shadow-sm">Pesan tidak dapat ditampilkan.</div></div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
191
+ }
192
 
193
+ if(btnSend) {
194
+ btnSend.addEventListener('click', async () => {
195
+ let prompt = tx.value.trim();
196
+ if(!prompt && !pdfText) return;
197
+
198
+ if(document.getElementById('welcome-msg')) document.getElementById('welcome-msg').remove();
199
 
200
+ tx.value = '';
201
+ tx.style.height = 'auto';
202
+ btnSend.disabled = true;
 
 
 
 
 
 
 
 
 
203
 
204
+ let displayPrompt = prompt;
205
+ let fullSystemPrompt = prompt;
206
 
207
+ if (pdfText) {
208
+ displayPrompt = `<div class="flex items-center gap-1.5 bg-blue-500/30 text-white text-xs px-2.5 py-1.5 rounded-lg mb-2 font-medium w-fit"><svg class="w-3.5 h-3.5" 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> ${pdfFilename}</div>${prompt}`;
209
+ fullSystemPrompt = `[Attached Document: ${pdfFilename}]\n${pdfText}\n\nUser Question: ${prompt}`;
210
+ removeAttach();
211
+ }
212
+
213
+ if(sessions[currentSid].messages.length === 0) {
214
+ sessions[currentSid].title = prompt.substring(0, 25) || pdfFilename.substring(0, 25);
215
+ }
216
+
217
+ sessions[currentSid].messages.push({ role: "user", content: fullSystemPrompt, displayContent: displayPrompt });
218
+ renderUserBubble(displayPrompt);
219
  saveToLocalStorage();
220
  renderHistoryList();
 
221
 
222
+ const loadId = 'load-' + Date.now();
223
+ document.getElementById('chat-messages').innerHTML += `<div id="${loadId}" class="flex mb-6 gap-4 items-start"><img src="/static/icon.png" class="w-8 h-8 rounded-full border border-slate-200 mt-1 shadow-sm shrink-0"><div class="bg-[#f8f9fa] border border-slate-200 p-4 rounded-2xl rounded-tl-none text-gray-500 flex items-center gap-2"><svg class="w-4 h-4 animate-spin text-accent" 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> Analyzing...</div></div>`;
224
+ document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
225
+
226
+ try {
227
+ const res = await fetch('/api/chat', {
228
+ method: 'POST',
229
+ headers: { 'Content-Type': 'application/json' },
230
+ body: JSON.stringify({
231
+ prompt: fullSystemPrompt,
232
+ messages: sessions[currentSid].messages.slice(0, -1),
233
+ model: document.getElementById('model-select').value
234
+ })
235
+ });
236
+ const data = await res.json();
237
+ document.getElementById(loadId).remove();
238
+
239
+ if(!res.ok) throw new Error(data.error || 'Server error');
240
+
241
+ sessions[currentSid].messages.push({ role: "assistant", content: data.reply });
242
+ saveToLocalStorage();
243
+ renderAiBubble(data.reply);
244
+
245
+ } catch (e) {
246
+ if(document.getElementById(loadId)) document.getElementById(loadId).remove();
247
+ document.getElementById('chat-messages').innerHTML += `<div class="flex mb-6 gap-4 items-start"><div class="bg-red-50 text-red-600 p-4 rounded-2xl rounded-tl-none border border-red-100 max-w-[85%] text-sm shadow-sm"><strong>Error:</strong> ${e.message}</div></div>`;
248
+ }
249
+
250
+ btnSend.disabled = false;
251
+ document.getElementById('chat-messages').scrollTop = document.getElementById('chat-messages').scrollHeight;
252
+ });
253
+ }
254
 
255
  // 5. Tool Modals
256
  const setModal = document.getElementById('settings-modal');
257
  const doiModal = document.getElementById('doi-modal');
258
 
259
+ if(document.getElementById('btn-open-settings')) {
260
+ document.getElementById('btn-open-settings').addEventListener('click', () => {
261
+ if(currentSettings) {
262
+ document.getElementById('settings-provider').value = currentSettings.provider;
263
+ document.getElementById('settings-url').value = currentSettings.base_url;
264
+ document.getElementById('settings-apikey').value = currentSettings.api_key;
265
+ renderModelLists();
266
+ }
267
+ setModal.classList.remove('hidden');
268
+ if(window.innerWidth < 768) toggleSidebar();
269
+ });
270
+ }
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ if(document.getElementById('btn-save-settings')) {
273
+ document.getElementById('btn-save-settings').addEventListener('click', async () => {
274
+ const payload = {
275
+ provider: document.getElementById('settings-provider').value,
276
+ base_url: document.getElementById('settings-url').value,
277
+ api_key: document.getElementById('settings-apikey').value,
278
+ models_openrouter: modelsOR,
279
+ models_nvidia: modelsNV,
280
+ current_model: document.getElementById('model-select').value
281
+ };
282
+ await fetch('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
283
+ setModal.classList.add('hidden');
284
+ initApp();
285
+ });
286
+ }
287
+
288
+ if(document.getElementById('btn-open-doi')) {
289
+ document.getElementById('btn-open-doi').addEventListener('click', () => {
290
+ doiModal.classList.remove('hidden');
291
+ if(window.innerWidth < 768) toggleSidebar();
292
+ });
293
+ }
294
+
295
+ if(document.getElementById('btn-validate-doi-submit')) {
296
+ document.getElementById('btn-validate-doi-submit').addEventListener('click', async () => {
297
+ const doi = document.getElementById('doi-input').value;
298
+ const resDiv = document.getElementById('doi-result');
299
+ if(!doi) return;
300
+ resDiv.classList.remove('hidden');
301
+ resDiv.innerHTML = `Validating...`;
302
+ const res = await fetch('/api/validate_doi', { method: 'POST', body: JSON.stringify({doi: doi}) });
303
+ const data = await res.json();
304
+ if(data.error) resDiv.innerHTML = `<span class="text-red-500">${data.error}</span>`;
305
+ else resDiv.innerHTML = `<strong>${data.title}</strong><br><span class="text-xs">${data.authors} (${data.year})</span>`;
306
+ });
307
+ }
308
 
309
  // 6. PDF Logic
310
  function removeAttach() {
311
  pdfText = ""; pdfFilename = "";
312
+ if(document.getElementById('pdf-input')) document.getElementById('pdf-input').value = "";
313
+ if(document.getElementById('attach-badge')) document.getElementById('attach-badge').classList.add('hidden');
314
+ }
315
+
316
+ if(document.getElementById('btn-attach')) document.getElementById('btn-attach').addEventListener('click', () => document.getElementById('pdf-input').click());
317
+ if(document.getElementById('pdf-input')) {
318
+ document.getElementById('pdf-input').addEventListener('change', async (e) => {
319
+ const file = e.target.files[0];
320
+ if(!file) return;
321
+ const formData = new FormData();
322
+ formData.append('file', file);
323
+ document.getElementById('attach-name').textContent = "Extracting...";
324
+ document.getElementById('attach-badge').classList.remove('hidden');
325
+ const res = await fetch('/api/upload_pdf', { method: 'POST', body: formData });
326
+ const data = await res.json();
327
+ if(res.ok) { pdfText = data.text; pdfFilename = data.filename; document.getElementById('attach-name').textContent = pdfFilename; }
328
+ else { alert(data.error); removeAttach(); }
329
+ });
330
  }
331
+ if(document.getElementById('btn-remove-attach')) document.getElementById('btn-remove-attach').addEventListener('click', removeAttach);
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
  // Close Modals
334
+ if(document.getElementById('btn-close-settings')) document.getElementById('btn-close-settings').onclick = () => setModal.classList.add('hidden');
335
+ if(document.getElementById('btn-cancel-settings')) document.getElementById('btn-cancel-settings').onclick = () => setModal.classList.add('hidden');
336
+ if(document.getElementById('btn-close-doi')) document.getElementById('btn-close-doi').onclick = () => doiModal.classList.add('hidden');
337
 
338
+ if(tx) {
339
+ tx.addEventListener("input", function() { this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'; });
340
+ tx.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); btnSend.click(); }});
341
+ }
342
 
343
  // Boot
344
  initApp();