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

Update static/js/script.js

Browse files
Files changed (1) hide show
  1. static/js/script.js +225 -269
static/js/script.js CHANGED
@@ -1,23 +1,26 @@
1
- let chatHistory = [];
 
 
 
 
 
 
2
  let currentSettings = null;
3
  let modelsOR = [];
4
  let modelsNV = [];
5
  let pdfText = "";
6
  let pdfFilename = "";
7
- let savedSessions = JSON.parse(localStorage.getItem('orbit_sessions')) || [];
8
 
9
- // 1. Inisialisasi Aplikasi (Load User & Settings)
10
  async function initApp() {
11
  try {
12
  const userRes = await fetch('/api/me');
13
  if (!userRes.ok) throw new Error('Not logged in');
14
  const userData = await userRes.json();
15
 
16
- // Render Profil
17
  document.getElementById('user-name').textContent = userData.name || userData.email;
18
  if (userData.picture) document.getElementById('user-avatar').src = userData.picture;
19
 
20
- // Render Settings
21
  const setRes = await fetch('/api/settings');
22
  if (setRes.ok) {
23
  currentSettings = await setRes.json();
@@ -25,14 +28,26 @@ async function initApp() {
25
  modelsNV = currentSettings.models_nvidia || [];
26
  populateModelDropdown(currentSettings);
27
  }
 
28
  renderHistoryList();
 
 
 
 
 
 
 
 
 
29
  } catch (error) {
 
30
  window.location.href = '/login';
31
  }
32
  }
33
 
34
  function populateModelDropdown(settings) {
35
  const select = document.getElementById('model-select');
 
36
  select.innerHTML = '';
37
  let models = [];
38
  if (settings.provider === 'Google Gemini') models = ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest'];
@@ -49,309 +64,250 @@ function populateModelDropdown(settings) {
49
  });
50
  }
51
 
52
- // 2. Sidebar & Clear Chat Logic
53
- const sidebar = document.getElementById('sidebar');
54
- const overlay = document.getElementById('sidebar-overlay');
55
-
56
- function toggleSidebar() {
57
- sidebar.classList.toggle('-translate-x-full');
58
- overlay.classList.toggle('hidden');
59
  }
60
 
61
- // Pasang event listener secara aman
62
- const btnHamburger = document.getElementById('btn-hamburger');
63
- if(btnHamburger) btnHamburger.addEventListener('click', toggleSidebar);
64
-
65
- const btnCloseSidebar = document.getElementById('btn-close-sidebar');
66
- if(btnCloseSidebar) btnCloseSidebar.addEventListener('click', toggleSidebar);
 
 
 
 
 
67
 
68
- if(overlay) overlay.addEventListener('click', toggleSidebar);
 
 
 
 
 
 
69
 
70
- function clearChat() {
71
- chatHistory = [];
72
- removeAttach();
73
  const chatBox = document.getElementById('chat-messages');
74
- if(chatBox) {
 
 
 
 
 
75
  chatBox.innerHTML = `
76
  <div id="welcome-msg" class="flex flex-col items-center justify-center h-full text-center px-4">
77
  <img src="/static/icon.png" class="w-16 h-16 mb-4 shadow-sm rounded-2xl" onerror="this.style.display='none'">
78
  <h2 class="text-2xl font-bold text-gray-800 mb-2">Welcome to ORBIT</h2>
79
  <p class="text-gray-500 text-sm max-w-md">Your AI-powered Educational Research Assistant. Upload a document or type a prompt below to begin.</p>
80
- </div>
81
- `;
82
- }
83
- if(window.innerWidth < 768 && !sidebar.classList.contains('-translate-x-full')) {
84
- toggleSidebar();
 
 
 
 
85
  }
 
86
  }
87
 
88
- const btnNewChat = document.getElementById('btn-new-chat');
89
- if(btnNewChat) btnNewChat.addEventListener('click', clearChat);
90
-
91
- const btnClearTop = document.getElementById('btn-clear-chat-top');
92
- if(btnClearTop) btnClearTop.addEventListener('click', clearChat);
93
-
94
- // 3. LocalStorage History
95
  function renderHistoryList() {
96
  const list = document.getElementById('history-list');
97
  if(!list) return;
98
- if(savedSessions.length === 0) {
 
 
99
  list.innerHTML = `<p class="text-xs text-gray-400 px-3 py-2 italic">No recent chats.</p>`;
100
  return;
101
  }
102
- list.innerHTML = savedSessions.map(s => `
103
- <button class="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-gray-600 hover:bg-white hover:text-accent hover:shadow-sm transition-all text-left truncate">
104
- <svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" 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>
105
- <span class="truncate">${s.title}</span>
106
- </button>
107
- `).join('');
108
- }
109
 
110
- function saveSessionTitle(prompt) {
111
- if(chatHistory.length === 1) {
112
- const title = prompt.length > 25 ? prompt.substring(0, 25) + "..." : prompt;
113
- savedSessions.unshift({ id: Date.now(), title: title });
114
- if(savedSessions.length > 10) savedSessions.pop(); // Max 10 histori
115
- localStorage.setItem('orbit_sessions', JSON.stringify(savedSessions));
116
- renderHistoryList();
117
- }
118
  }
119
 
120
- // 4. Modal Settings
121
- const setModal = document.getElementById('settings-modal');
122
-
123
- function renderModelLists() {
124
- const listOR = document.getElementById('models-list-openrouter');
125
- const listNV = document.getElementById('models-list-nvidia');
126
- if(listOR) {
127
- listOR.innerHTML = modelsOR.map((m, i) => `<div class="flex justify-between items-center p-1.5 border-b border-gray-100 text-sm text-gray-600"><span>${m}</span><button onclick="modelsOR.splice(${i},1);renderModelLists()" class="text-red-400 hover:text-red-600 hover:bg-red-50 rounded px-2 font-bold transition-colors">×</button></div>`).join('');
128
- }
129
- if(listNV) {
130
- listNV.innerHTML = modelsNV.map((m, i) => `<div class="flex justify-between items-center p-1.5 border-b border-gray-100 text-sm text-gray-600"><span>${m}</span><button onclick="modelsNV.splice(${i},1);renderModelLists()" class="text-red-400 hover:text-red-600 hover:bg-red-50 rounded px-2 font-bold transition-colors">×</button></div>`).join('');
131
- }
132
- }
133
-
134
- const btnAddOR = document.getElementById('btn-add-model-or');
135
- if(btnAddOR) {
136
- btnAddOR.addEventListener('click', () => {
137
- const val = document.getElementById('new-model-or').value.trim();
138
- if(val && !modelsOR.includes(val)) { modelsOR.push(val); document.getElementById('new-model-or').value=''; renderModelLists(); }
139
- });
140
- }
141
 
142
- const btnAddNV = document.getElementById('btn-add-model-nv');
143
- if(btnAddNV) {
144
- btnAddNV.addEventListener('click', () => {
145
- const val = document.getElementById('new-model-nv').value.trim();
146
- if(val && !modelsNV.includes(val)) { modelsNV.push(val); document.getElementById('new-model-nv').value=''; renderModelLists(); }
147
- });
148
  }
149
 
150
- const btnOpenSettings = document.getElementById('btn-open-settings');
151
- if(btnOpenSettings) {
152
- btnOpenSettings.addEventListener('click', () => {
153
- if(currentSettings) {
154
- document.getElementById('settings-provider').value = currentSettings.provider;
155
- document.getElementById('settings-url').value = currentSettings.base_url;
156
- document.getElementById('settings-apikey').value = currentSettings.api_key;
157
- modelsOR = [...currentSettings.models_openrouter];
158
- modelsNV = [...currentSettings.models_nvidia];
159
- renderModelLists();
160
- }
161
- if(setModal) setModal.classList.remove('hidden');
162
- if(window.innerWidth < 768) toggleSidebar();
163
- });
164
- }
165
 
166
- if(document.getElementById('btn-close-settings')) document.getElementById('btn-close-settings').addEventListener('click', () => setModal.classList.add('hidden'));
167
- if(document.getElementById('btn-cancel-settings')) document.getElementById('btn-cancel-settings').addEventListener('click', () => setModal.classList.add('hidden'));
168
-
169
- const btnSaveSettings = document.getElementById('btn-save-settings');
170
- if(btnSaveSettings) {
171
- btnSaveSettings.addEventListener('click', async () => {
172
- const payload = {
173
- provider: document.getElementById('settings-provider').value,
174
- base_url: document.getElementById('settings-url').value,
175
- api_key: document.getElementById('settings-apikey').value,
176
- models_openrouter: modelsOR,
177
- models_nvidia: modelsNV,
178
- current_model: document.getElementById('model-select').value
179
- };
180
- await fetch('/api/settings', { method: 'POST', body: JSON.stringify(payload) });
181
- if(setModal) setModal.classList.add('hidden');
182
- initApp(); // Refresh settingan dropdown
183
- });
184
- }
185
 
186
- // 5. Modal DOI
187
- const doiModal = document.getElementById('doi-modal');
188
- const btnOpenDoi = document.getElementById('btn-open-doi');
189
- if(btnOpenDoi) {
190
- btnOpenDoi.addEventListener('click', () => {
191
- if(doiModal) doiModal.classList.remove('hidden');
192
- const resDiv = document.getElementById('doi-result');
193
- if(resDiv) resDiv.classList.add('hidden');
194
- if(window.innerWidth < 768) toggleSidebar();
195
- });
196
  }
197
 
198
- if(document.getElementById('btn-close-doi')) document.getElementById('btn-close-doi').addEventListener('click', () => doiModal.classList.add('hidden'));
199
-
200
- const btnValDoi = document.getElementById('btn-validate-doi-submit');
201
- if(btnValDoi) {
202
- btnValDoi.addEventListener('click', async () => {
203
- const doi = document.getElementById('doi-input').value;
204
- const resDiv = document.getElementById('doi-result');
205
- if(!doi) return;
206
-
207
- resDiv.classList.remove('hidden');
208
- resDiv.innerHTML = `<div class="flex items-center gap-2 text-blue-600"><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> Validating...</div>`;
209
-
210
- const res = await fetch('/api/validate_doi', { method: 'POST', body: JSON.stringify({doi: doi}) });
211
- const data = await res.json();
212
-
213
- if(data.error) {
214
- resDiv.innerHTML = `<span class="text-red-500 font-medium">Error: ${data.error}</span>`;
215
- } else {
216
- resDiv.innerHTML = `<strong class="text-gray-800">${data.title}</strong><br><span class="text-xs text-gray-500 block mt-1">${data.authors} (${data.year})</span><span class="inline-block mt-2 px-2 py-1 bg-blue-50 text-blue-600 text-[10px] rounded border border-blue-100 uppercase tracking-wide font-bold">${data.type}</span>`;
217
- }
218
- });
219
  }
220
 
221
- // 6. PDF Attachment
222
- const btnAttach = document.getElementById('btn-attach');
223
- const pdfInput = document.getElementById('pdf-input');
224
-
225
- if(btnAttach && pdfInput) {
226
- btnAttach.addEventListener('click', () => pdfInput.click());
227
 
228
- pdfInput.addEventListener('change', async (e) => {
229
- const file = e.target.files[0];
230
- if(!file) return;
231
- const formData = new FormData();
232
- formData.append('file', file);
233
-
234
- document.getElementById('attach-name').textContent = "Extracting...";
235
- document.getElementById('attach-badge').classList.remove('hidden');
236
-
237
- try {
238
- const res = await fetch('/api/upload_pdf', { method: 'POST', body: formData });
239
- const data = await res.json();
240
- if(res.ok) {
241
- pdfText = data.text;
242
- pdfFilename = data.filename;
243
- document.getElementById('attach-name').textContent = pdfFilename;
244
- } else {
245
- throw new Error(data.error);
246
- }
247
- } catch (err) {
248
- alert("Upload failed: " + err.message);
249
- removeAttach();
250
- }
251
- });
252
- }
253
 
254
- function removeAttach() {
255
- pdfText = ""; pdfFilename = "";
256
- if(document.getElementById('pdf-input')) document.getElementById('pdf-input').value = "";
257
- if(document.getElementById('attach-badge')) document.getElementById('attach-badge').classList.add('hidden');
258
- }
259
-
260
- const btnRemoveAttach = document.getElementById('btn-remove-attach');
261
- if(btnRemoveAttach) btnRemoveAttach.addEventListener('click', removeAttach);
 
 
 
 
262
 
263
- // 7. Chat Logic
264
- const tx = document.getElementById('chat-textarea');
265
- const btnSend = document.getElementById('btn-send');
 
266
 
267
- if(tx) {
268
- tx.addEventListener("input", function() {
269
- this.style.height = 'auto';
270
- this.style.height = (this.scrollHeight) + 'px';
271
- });
272
 
273
- tx.addEventListener('keydown', function(e) {
274
- if (e.key === 'Enter' && !e.shiftKey) {
275
- e.preventDefault();
276
- if(btnSend) btnSend.click();
277
- }
278
- });
279
- }
280
 
281
- if(btnSend) {
282
- btnSend.addEventListener('click', async () => {
283
- let prompt = tx.value.trim();
284
- if(!prompt && !pdfText) return;
285
-
286
- const chatBox = document.getElementById('chat-messages');
287
- const welcomeMsg = document.getElementById('welcome-msg');
288
- if(welcomeMsg) welcomeMsg.remove();
289
-
290
- tx.value = '';
291
- tx.style.height = 'auto';
292
- btnSend.disabled = true;
293
-
294
- let displayPrompt = prompt;
295
- let systemPrompt = prompt;
296
 
297
- // Inject PDF context
298
- if (pdfText) {
299
- 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}`;
300
- systemPrompt = `[Attached Document: ${pdfFilename}]\n${pdfText}\n\nUser Question: ${prompt}`;
301
- removeAttach();
302
- }
303
-
304
- // Tampilkan pesan User
305
- chatHistory.push({ role: "user", content: systemPrompt });
306
- saveSessionTitle(prompt || pdfFilename);
307
 
308
- 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">${displayPrompt.replace(/\n/g, '<br>')}</div></div>`;
309
- chatBox.scrollTop = chatBox.scrollHeight;
310
-
311
- // Tampilkan loading AI
312
- const loadId = 'load-' + Date.now();
313
- chatBox.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" onerror="this.style.display='none'"><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>`;
314
- chatBox.scrollTop = chatBox.scrollHeight;
315
-
316
- try {
317
- // Tembak API
318
- const res = await fetch('/api/chat', {
319
- method: 'POST',
320
- headers: { 'Content-Type': 'application/json' },
321
- body: JSON.stringify({
322
- prompt: systemPrompt,
323
- messages: chatHistory.slice(0, -1),
324
- model: document.getElementById('model-select').value
325
- })
326
- });
327
- const data = await res.json();
328
-
329
- document.getElementById(loadId).remove();
330
-
331
- if(!res.ok) throw new Error(data.error || 'Server error');
332
-
333
- chatHistory.push({ role: "assistant", content: data.reply });
334
-
335
- // Render format Markdown pakai library Marked.js
336
- const renderedMarkdown = marked.parse(data.reply);
337
- 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>`;
338
-
339
- } catch (e) {
340
- if(document.getElementById(loadId)) document.getElementById(loadId).remove();
341
- chatBox.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-[15px] shadow-sm"><strong class="block mb-1">Error processing request:</strong>${e.message}</div></div>`;
342
- }
343
 
344
- btnSend.disabled = false;
345
- chatBox.scrollTop = chatBox.scrollHeight;
346
- });
347
- }
 
 
 
 
348
 
349
- // 8. Boot & Service Worker
350
- initApp();
 
351
 
352
- if ('serviceWorker' in navigator) {
353
- window.addEventListener('load', () => {
354
- navigator.serviceWorker.register('/sw.js', { scope: '/' })
355
- .catch(err => console.error('PWA SW failed:', err));
356
- });
357
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ORBIT – Educational Research Assistant
3
+ * Logic with Multi-Session History Support
4
+ */
5
+
6
+ let currentSid = null;
7
+ let sessions = JSON.parse(localStorage.getItem('orbit_sessions')) || {};
8
  let currentSettings = null;
9
  let modelsOR = [];
10
  let modelsNV = [];
11
  let pdfText = "";
12
  let pdfFilename = "";
 
13
 
14
+ // 1. Boot Application
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;
22
  if (userData.picture) document.getElementById('user-avatar').src = userData.picture;
23
 
 
24
  const setRes = await fetch('/api/settings');
25
  if (setRes.ok) {
26
  currentSettings = await setRes.json();
 
28
  modelsNV = currentSettings.models_nvidia || [];
29
  populateModelDropdown(currentSettings);
30
  }
31
+
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 {
39
+ newSession();
40
+ }
41
+
42
  } catch (error) {
43
+ console.error("Init failed:", error);
44
  window.location.href = '/login';
45
  }
46
  }
47
 
48
  function populateModelDropdown(settings) {
49
  const select = document.getElementById('model-select');
50
+ if(!select) return;
51
  select.innerHTML = '';
52
  let models = [];
53
  if (settings.provider === 'Google Gemini') models = ['gemini-1.5-pro-latest', 'gemini-1.5-flash-latest'];
 
64
  });
65
  }
66
 
67
+ // 2. Session Management
68
+ function saveToLocalStorage() {
69
+ localStorage.setItem('orbit_sessions', JSON.stringify(sessions));
 
 
 
 
70
  }
71
 
72
+ function newSession() {
73
+ const id = Date.now().toString();
74
+ sessions[id] = {
75
+ title: "New Chat",
76
+ messages: []
77
+ };
78
+ currentSid = id;
79
+ saveToLocalStorage();
80
+ renderHistoryList();
81
+ displayMessages();
82
+ }
83
 
84
+ function switchSession(id) {
85
+ if (!sessions[id]) return;
86
+ currentSid = id;
87
+ renderHistoryList();
88
+ displayMessages();
89
+ if(window.innerWidth < 768) toggleSidebar();
90
+ }
91
 
92
+ function displayMessages() {
 
 
93
  const chatBox = document.getElementById('chat-messages');
94
+ if(!chatBox) return;
95
+ chatBox.innerHTML = '';
96
+
97
+ const msgs = sessions[currentSid].messages;
98
+
99
+ if (msgs.length === 0) {
100
  chatBox.innerHTML = `
101
  <div id="welcome-msg" class="flex flex-col items-center justify-center h-full text-center px-4">
102
  <img src="/static/icon.png" class="w-16 h-16 mb-4 shadow-sm rounded-2xl" onerror="this.style.display='none'">
103
  <h2 class="text-2xl font-bold text-gray-800 mb-2">Welcome to ORBIT</h2>
104
  <p class="text-gray-500 text-sm max-w-md">Your AI-powered Educational Research Assistant. Upload a document or type a prompt below to begin.</p>
105
+ </div>`;
106
+ } else {
107
+ msgs.forEach(m => {
108
+ if (m.role === 'user') {
109
+ renderUserBubble(m.displayContent || m.content);
110
+ } else {
111
+ renderAiBubble(m.content);
112
+ }
113
+ });
114
  }
115
+ chatBox.scrollTop = chatBox.scrollHeight;
116
  }
117
 
 
 
 
 
 
 
 
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>`;
125
  return;
126
  }
 
 
 
 
 
 
 
127
 
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>`;
135
+ }).join('');
136
  }
137
 
138
+ // 3. UI Helpers
139
+ 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');
164
+ 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();