Ab-Romia commited on
Commit
4e173cb
·
verified ·
1 Parent(s): 5c05e42

Update static/app.js

Browse files
Files changed (1) hide show
  1. static/app.js +570 -53
static/app.js CHANGED
@@ -10,6 +10,9 @@ class ContextAwareApp {
10
  statusIndicator: document.getElementById('status-indicator'),
11
  clearContextBtn: document.getElementById('clear-context-btn'),
12
  indexContextBtn: document.getElementById('index-context-btn'),
 
 
 
13
 
14
  // API Key elements
15
  apiKeyInput: document.getElementById('api-key-input'),
@@ -29,10 +32,6 @@ class ContextAwareApp {
29
  assistantHeader: document.getElementById('assistant-header'),
30
  assistantContent: document.getElementById('assistant-content'),
31
  assistantToggleIcon: document.getElementById('assistant-toggle-icon'),
32
-
33
- // Context stats
34
- charCount: document.getElementById('char-count'),
35
- wordCount: document.getElementById('word-count')
36
  };
37
 
38
  // Application state
@@ -43,7 +42,8 @@ class ContextAwareApp {
43
  apiKeyValidated: false,
44
  isTestingApiKey: false,
45
  userApiKey: '',
46
- apiSectionCollapsed: true,
 
47
  kbSectionCollapsed: false,
48
  assistantSectionCollapsed: false,
49
  };
@@ -63,13 +63,14 @@ class ContextAwareApp {
63
  this.addMessageToChat(
64
  "👋 **Welcome to ContextIQ!**\n\n" +
65
  "To get started:\n" +
66
- "1. **Enter your OpenRouter API key** and save it.\n" +
67
  "2. **Add your context** in the Knowledge Base on the left.\n" +
68
  "3. **Index the context** and start asking questions!\n\n" +
69
  "🆓 You can get a free API key from [openrouter.ai](https://openrouter.ai) - no credit card required!",
70
  'system'
71
  );
72
 
 
73
  this.updateUI();
74
  this.updateContextStats();
75
  }
@@ -89,97 +90,613 @@ class ContextAwareApp {
89
  });
90
  this.elements.contextInput.addEventListener('input', () => {
91
  this.updateContextStats();
92
- this.updateUI();
93
  });
94
  this.elements.chatInput.addEventListener('input', () => this.autoResizeTextarea(this.elements.chatInput));
95
 
 
96
  this.elements.testApiKeyBtn.addEventListener('click', (e) => {
97
  e.preventDefault();
98
- this.handleTestApiKey();
99
  });
100
  this.elements.saveApiKeyBtn.addEventListener('click', (e) => {
101
  e.preventDefault();
102
- this.handleSaveApiKey();
 
 
 
 
 
 
 
 
 
 
103
  });
104
- this.elements.apiKeyInput.addEventListener('input', () => this.updateApiKeyUI());
105
 
 
106
  this.elements.toggleApiSection.addEventListener('click', () => this.toggleSection('api'));
107
  this.elements.kbHeader.addEventListener('click', () => this.toggleSection('kb'));
108
  this.elements.assistantHeader.addEventListener('click', () => this.toggleSection('assistant'));
109
 
 
110
  window.addEventListener('resize', () => this.setupResponsiveUI());
111
  }
112
 
113
  /**
114
- * Updates the UI state of the application.
115
  */
116
- updateUI() {
117
- const canIndex = this.elements.contextInput.value.trim().length > 0;
118
- this.elements.indexContextBtn.disabled = this.state.isIndexing || !canIndex;
119
- this.elements.clearContextBtn.disabled = this.state.isIndexing || !canIndex;
120
 
121
- const canChat = this.elements.chatInput.value.trim().length > 0 && this.state.isIndexed && this.state.apiKeyValidated;
122
- this.elements.sendButton.disabled = this.state.isGenerating || !canChat;
 
 
123
 
124
- this.elements.statusIndicator.style.display = (this.state.isIndexing || this.state.isGenerating) ? 'flex' : 'none';
125
-
126
- this.updateApiKeyUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
128
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  /**
130
- * Updates the UI specifically for the API key section.
131
  */
132
- updateApiKeyUI() {
133
  const apiKey = this.elements.apiKeyInput.value.trim();
134
- const hasKey = !!apiKey;
135
- const isValidFormat = hasKey && apiKey.startsWith('sk-or-') && apiKey.length >= 40;
136
 
137
- this.elements.testApiKeyBtn.disabled = this.state.isTestingApiKey || !hasKey;
138
- this.elements.saveApiKeyBtn.disabled = this.state.isTestingApiKey || !hasKey;
139
-
140
- if (this.state.isTestingApiKey) {
141
- this.updateApiKeyStatus('testing', 'Testing API key...');
142
- } else if (this.state.apiKeyValidated) {
143
- this.updateApiKeyStatus('success', 'API key is valid!');
144
- } else if (!hasKey) {
145
  this.updateApiKeyStatus('pending', 'Enter API key and click Test');
146
- } else if (!isValidFormat) {
147
- this.updateApiKeyStatus('error', 'Key must start with "sk-or-" and be at least 40 chars.');
 
 
148
  } else {
149
  this.updateApiKeyStatus('pending', 'Click "Test Key" to validate');
150
  }
151
  }
152
 
153
  /**
154
- * Updates the API key status display.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  */
156
- updateApiKeyStatus(status, text) {
157
- const icon = this.elements.apiStatusIcon;
158
- const statusText = this.elements.apiStatusText;
159
- icon.innerHTML = '';
160
- statusText.textContent = text;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
  switch (status) {
163
  case 'testing':
164
- icon.innerHTML = `<div class="w-3 h-3 bg-blue-400 rounded-full animate-pulse"></div>`;
165
- statusText.className = 'text-sm text-blue-400';
166
  break;
167
  case 'success':
168
- icon.innerHTML = `<svg class="w-4 h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-10.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>`;
169
- statusText.className = 'text-sm text-green-500';
170
  break;
171
  case 'error':
172
- icon.innerHTML = `<svg class="w-4 h-4 text-red-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>`;
173
- statusText.className = 'text-sm text-red-500';
174
  break;
175
- default: // pending
176
- icon.innerHTML = `<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-1-12a1 1 0 102 0v4a1 1 0 10-2 0v-4zm1 10a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd"></path></svg>`;
177
- statusText.className = 'text-sm text-yellow-500';
178
  break;
179
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
-
182
- // ... other methods follow a similar refactoring logic
183
  }
184
 
185
- document.addEventListener('DOMContentLoaded', () => new ContextAwareApp());
 
 
 
 
10
  statusIndicator: document.getElementById('status-indicator'),
11
  clearContextBtn: document.getElementById('clear-context-btn'),
12
  indexContextBtn: document.getElementById('index-context-btn'),
13
+ taskSelect: document.getElementById('task-select'),
14
+ charCount: document.getElementById('char-count'),
15
+ wordCount: document.getElementById('word-count'),
16
 
17
  // API Key elements
18
  apiKeyInput: document.getElementById('api-key-input'),
 
32
  assistantHeader: document.getElementById('assistant-header'),
33
  assistantContent: document.getElementById('assistant-content'),
34
  assistantToggleIcon: document.getElementById('assistant-toggle-icon'),
 
 
 
 
35
  };
36
 
37
  // Application state
 
42
  apiKeyValidated: false,
43
  isTestingApiKey: false,
44
  userApiKey: '',
45
+ // Collapse states for mobile view
46
+ apiSectionCollapsed: false,
47
  kbSectionCollapsed: false,
48
  assistantSectionCollapsed: false,
49
  };
 
63
  this.addMessageToChat(
64
  "👋 **Welcome to ContextIQ!**\n\n" +
65
  "To get started:\n" +
66
+ "1. **Enter your OpenRouter API key** in the configuration section above.\n" +
67
  "2. **Add your context** in the Knowledge Base on the left.\n" +
68
  "3. **Index the context** and start asking questions!\n\n" +
69
  "🆓 You can get a free API key from [openrouter.ai](https://openrouter.ai) - no credit card required!",
70
  'system'
71
  );
72
 
73
+ // Initial UI update
74
  this.updateUI();
75
  this.updateContextStats();
76
  }
 
90
  });
91
  this.elements.contextInput.addEventListener('input', () => {
92
  this.updateContextStats();
93
+ this.updateUI(); // Add this to ensure UI updates when context changes
94
  });
95
  this.elements.chatInput.addEventListener('input', () => this.autoResizeTextarea(this.elements.chatInput));
96
 
97
+ // API Key listeners - Fixed event handling
98
  this.elements.testApiKeyBtn.addEventListener('click', (e) => {
99
  e.preventDefault();
100
+ this.testApiKey();
101
  });
102
  this.elements.saveApiKeyBtn.addEventListener('click', (e) => {
103
  e.preventDefault();
104
+ this.saveApiKey();
105
+ });
106
+ this.elements.apiKeyInput.addEventListener('input', () => {
107
+ this.onApiKeyInputChange();
108
+ this.updateUI(); // Ensure UI updates immediately on input change
109
+ });
110
+ this.elements.apiKeyInput.addEventListener('keydown', e => {
111
+ if (e.key === 'Enter') {
112
+ e.preventDefault();
113
+ this.testApiKey();
114
+ }
115
  });
 
116
 
117
+ // Toggle listeners for collapsible sections
118
  this.elements.toggleApiSection.addEventListener('click', () => this.toggleSection('api'));
119
  this.elements.kbHeader.addEventListener('click', () => this.toggleSection('kb'));
120
  this.elements.assistantHeader.addEventListener('click', () => this.toggleSection('assistant'));
121
 
122
+ // Listen for window resize to adjust UI
123
  window.addEventListener('resize', () => this.setupResponsiveUI());
124
  }
125
 
126
  /**
127
+ * Sets up the initial state of collapsible sections based on screen size.
128
  */
129
+ setupResponsiveUI() {
130
+ const isMobile = window.innerWidth < 1024;
 
 
131
 
132
+ // On mobile, collapse the knowledge base by default to show the chat first.
133
+ // On desktop, ensure everything is expanded.
134
+ this.state.kbSectionCollapsed = isMobile;
135
+ this.state.assistantSectionCollapsed = false; // Always show assistant on load
136
 
137
+ // Hide API section by default if a valid key is already loaded
138
+ if (this.state.apiKeyValidated) {
139
+ this.state.apiSectionCollapsed = true;
140
+ }
141
+
142
+ this.updateSectionVisibility('api');
143
+ this.updateSectionVisibility('kb');
144
+ this.updateSectionVisibility('assistant');
145
+ }
146
+
147
+ /**
148
+ * Toggles a specific collapsible section.
149
+ * @param {'api' | 'kb' | 'assistant'} sectionName - The name of the section to toggle.
150
+ */
151
+ toggleSection(sectionName) {
152
+ const stateKey = `${sectionName}SectionCollapsed`;
153
+ this.state[stateKey] = !this.state[stateKey];
154
+ this.updateSectionVisibility(sectionName);
155
+ }
156
+
157
+ /**
158
+ * Updates the visibility of a collapsible section based on its state.
159
+ * @param {'api' | 'kb' | 'assistant'} sectionName - The name of the section to update.
160
+ */
161
+ updateSectionVisibility(sectionName) {
162
+ const contentEl = this.elements[`${sectionName}Content`];
163
+ const toggleIconEl = this.elements[`${sectionName}ToggleIcon`];
164
+ const isCollapsed = this.state[`${sectionName}SectionCollapsed`];
165
+
166
+ if (contentEl) {
167
+ contentEl.style.display = isCollapsed ? 'none' : 'block';
168
+ if(sectionName !== 'api' && contentEl.classList.contains('lg:flex')){
169
+ contentEl.style.display = isCollapsed ? 'none' : 'flex';
170
+ }
171
+ }
172
+ if (toggleIconEl) {
173
+ toggleIconEl.style.transform = isCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)';
174
+ }
175
  }
176
+
177
+ /**
178
+ * Load stored API key from localStorage if available
179
+ */
180
+ loadStoredApiKey() {
181
+ try {
182
+ const storedKey = localStorage.getItem('openrouter_api_key');
183
+ if (storedKey) {
184
+ this.elements.apiKeyInput.value = storedKey;
185
+ this.state.userApiKey = storedKey;
186
+ // Don't auto-test on load, just update the UI
187
+ this.onApiKeyInputChange();
188
+ }
189
+ } catch (error) {
190
+ console.warn('Could not load stored API key:', error);
191
+ }
192
+ }
193
+
194
  /**
195
+ * Handle API key input changes
196
  */
197
+ onApiKeyInputChange() {
198
  const apiKey = this.elements.apiKeyInput.value.trim();
 
 
199
 
200
+ // Reset validation state when input changes
201
+ this.state.apiKeyValidated = false;
202
+ this.state.userApiKey = '';
203
+
204
+ if (!apiKey) {
 
 
 
205
  this.updateApiKeyStatus('pending', 'Enter API key and click Test');
206
+ } else if (!apiKey.startsWith('sk-or-')) {
207
+ this.updateApiKeyStatus('error', 'Key should start with "sk-or-"');
208
+ } else if (apiKey.length < 40) {
209
+ this.updateApiKeyStatus('error', 'API key appears too short');
210
  } else {
211
  this.updateApiKeyStatus('pending', 'Click "Test Key" to validate');
212
  }
213
  }
214
 
215
  /**
216
+ * Test the API key validity - Fixed with better error handling and timeout
217
+ */
218
+ async testApiKey(silent = false) {
219
+ const apiKey = this.elements.apiKeyInput.value.trim();
220
+ if (!apiKey) {
221
+ if (!silent) this.updateApiKeyStatus('error', 'Please enter an API key');
222
+ return;
223
+ }
224
+
225
+ this.state.isTestingApiKey = true;
226
+ this.updateUI();
227
+ if (!silent) {
228
+ this.updateApiKeyStatus('testing', 'Testing API key...');
229
+ }
230
+
231
+ try {
232
+ // Create an AbortController for timeout handling
233
+ const controller = new AbortController();
234
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
235
+
236
+ const response = await fetch('/api/v1/test-api-key', {
237
+ method: 'POST',
238
+ headers: { 'Content-Type': 'application/json' },
239
+ body: JSON.stringify({ api_key: apiKey }),
240
+ signal: controller.signal
241
+ });
242
+
243
+ clearTimeout(timeoutId);
244
+
245
+ if (!response.ok) {
246
+ const errorText = await response.text();
247
+ throw new Error(`Server error (${response.status}): ${errorText}`);
248
+ }
249
+
250
+ const result = await response.json();
251
+
252
+ if (result.valid) {
253
+ this.state.apiKeyValidated = true;
254
+ this.state.userApiKey = apiKey;
255
+ this.updateApiKeyStatus('success', result.message || 'API key is valid');
256
+ if (!silent) {
257
+ this.addMessageToChat("✅ **API Key Validated!** You can now use the assistant.", 'system');
258
+ this.state.apiSectionCollapsed = true;
259
+ this.updateSectionVisibility('api');
260
+ }
261
+ } else {
262
+ this.state.apiKeyValidated = false;
263
+ this.state.userApiKey = '';
264
+ this.updateApiKeyStatus('error', result.message || 'API key is invalid');
265
+ if (!silent) {
266
+ this.addMessageToChat(`❌ **API Key Invalid**: ${result.message || 'Unknown error'}`, 'system');
267
+ }
268
+ }
269
+ } catch (error) {
270
+ console.error('API key test error:', error);
271
+ this.state.apiKeyValidated = false;
272
+ this.state.userApiKey = '';
273
+
274
+ let errorMessage = 'Error testing API key';
275
+ if (error.name === 'AbortError') {
276
+ errorMessage = 'Request timed out. Please check your connection and try again.';
277
+ } else if (error.message) {
278
+ errorMessage = error.message;
279
+ }
280
+
281
+ this.updateApiKeyStatus('error', errorMessage);
282
+ if (!silent) {
283
+ this.addMessageToChat(`❌ **Connection Error**: ${errorMessage}`, 'system');
284
+ }
285
+ } finally {
286
+ this.state.isTestingApiKey = false;
287
+ this.updateUI();
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Save the API key to localStorage
293
  */
294
+ saveApiKey() {
295
+ const apiKey = this.elements.apiKeyInput.value.trim();
296
+ if (!this.state.apiKeyValidated) {
297
+ this.addMessageToChat("⚠️ **Please test the API key first** before saving.", 'system');
298
+ return;
299
+ }
300
+ try {
301
+ localStorage.setItem('openrouter_api_key', apiKey);
302
+ this.updateApiKeyStatus('success', 'API key saved locally!');
303
+ this.addMessageToChat("💾 **API Key Saved!** It will be remembered for future sessions.", 'system');
304
+ } catch (error) {
305
+ console.error('Save error:', error);
306
+ this.addMessageToChat("❌ **Save Failed**: Could not save API key to local storage.", 'system');
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Update API key status display
312
+ */
313
+ updateApiKeyStatus(status, message) {
314
+ const statusEl = this.elements.apiStatusText;
315
+ const iconEl = this.elements.apiStatusIcon;
316
 
317
  switch (status) {
318
  case 'testing':
319
+ iconEl.className = 'w-3 h-3 bg-blue-500 rounded-full animate-pulse flex-shrink-0';
320
+ statusEl.textContent = 'Testing...';
321
  break;
322
  case 'success':
323
+ iconEl.className = 'w-3 h-3 bg-green-500 rounded-full flex-shrink-0';
324
+ statusEl.textContent = 'API Key Valid';
325
  break;
326
  case 'error':
327
+ iconEl.className = 'w-3 h-3 bg-red-500 rounded-full flex-shrink-0';
328
+ statusEl.textContent = 'API Key Invalid';
329
  break;
330
+ case 'pending':
331
+ iconEl.className = 'w-3 h-3 bg-yellow-500 rounded-full flex-shrink-0';
332
+ statusEl.textContent = 'API Key Pending';
333
  break;
334
  }
335
+
336
+ // Also update the detailed status message box
337
+ const detailedStatusEl = this.elements.apiKeyStatus;
338
+ if (detailedStatusEl) {
339
+ detailedStatusEl.textContent = message;
340
+ detailedStatusEl.classList.remove('hidden');
341
+ detailedStatusEl.className = 'p-3 rounded-lg text-sm ';
342
+
343
+ const colors = {
344
+ testing: 'bg-blue-500/20 text-blue-300 border border-blue-500/30',
345
+ success: 'bg-green-500/20 text-green-300 border border-green-500/30',
346
+ error: 'bg-red-500/20 text-red-300 border border-red-500/30',
347
+ pending: 'bg-yellow-500/20 text-yellow-300 border border-yellow-500/30',
348
+ };
349
+ detailedStatusEl.classList.add(...colors[status].split(' '));
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Main handler for the send button. Directs to the correct function based on the selected task.
355
+ */
356
+ handleSubmit() {
357
+ if (!this.state.apiKeyValidated) {
358
+ this.addMessageToChat("🔑 **API Key Required**: Please enter and test your API key first.", 'system');
359
+ this.state.apiSectionCollapsed = false;
360
+ this.updateSectionVisibility('api');
361
+ this.elements.apiKeyInput.focus();
362
+ return;
363
+ }
364
+
365
+ const selectedTask = this.elements.taskSelect.value;
366
+ if (selectedTask === 'q_and_a') {
367
+ this.handleSendPrompt();
368
+ } else {
369
+ this.handleExecuteTask();
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Handles the logic for indexing the provided context.
375
+ */
376
+ async handleIndexContext() {
377
+ if (!this.state.apiKeyValidated) {
378
+ this.addMessageToChat("🔑 **API Key Required**: Please validate your API key before indexing.", 'system');
379
+ return;
380
+ }
381
+
382
+ const context = this.elements.contextInput.value.trim();
383
+ if (context.length < 20) {
384
+ this.showStatus('Context is too short. Please provide at least 20 characters.', 'error');
385
+ return;
386
+ }
387
+
388
+ this.state.isIndexing = true;
389
+ this.updateUI();
390
+ this.showStatus('Indexing context... This may take a moment.', 'loading');
391
+
392
+ try {
393
+ const controller = new AbortController();
394
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout for indexing
395
+
396
+ const response = await fetch('/api/v1/index', {
397
+ method: 'POST',
398
+ headers: {
399
+ 'Content-Type': 'application/json',
400
+ 'X-API-Key': this.state.userApiKey
401
+ },
402
+ body: JSON.stringify({ context }),
403
+ signal: controller.signal
404
+ });
405
+
406
+ clearTimeout(timeoutId);
407
+
408
+ if (!response.ok) {
409
+ const errorText = await response.text();
410
+ throw new Error(`Server error (${response.status}): ${errorText}`);
411
+ }
412
+
413
+ const result = await response.json();
414
+ this.state.isIndexed = true;
415
+ this.showStatus(`Successfully indexed ${result.documents_added || '1'} document chunks.`, 'success');
416
+ } catch (error) {
417
+ console.error('Indexing error:', error);
418
+ let errorMessage = 'Error indexing context';
419
+ if (error.name === 'AbortError') {
420
+ errorMessage = 'Indexing timed out. Please try with smaller content or check your connection.';
421
+ } else if (error.message) {
422
+ errorMessage = error.message;
423
+ }
424
+ this.showStatus(errorMessage, 'error');
425
+ this.state.isIndexed = false;
426
+ } finally {
427
+ this.state.isIndexing = false;
428
+ this.updateUI();
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Handles sending a user's prompt to the backend for a response.
434
+ */
435
+ async handleSendPrompt() {
436
+ const prompt = this.elements.chatInput.value.trim();
437
+ if (prompt.length < 2 || this.state.isGenerating) return;
438
+
439
+ if (!this.state.isIndexed) {
440
+ this.showStatus('Please index your context before asking questions.', 'error');
441
+ return;
442
+ }
443
+
444
+ this.addMessageToChat(prompt, 'user');
445
+ this.elements.chatInput.value = '';
446
+ this.autoResizeTextarea(this.elements.chatInput);
447
+
448
+ this.state.isGenerating = true;
449
+ this.updateUI();
450
+ this.showStatus('AI is thinking...', 'loading');
451
+
452
+ try {
453
+ const controller = new AbortController();
454
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
455
+
456
+ const response = await fetch('/api/v1/generate', {
457
+ method: 'POST',
458
+ headers: {
459
+ 'Content-Type': 'application/json',
460
+ 'X-API-Key': this.state.userApiKey
461
+ },
462
+ body: JSON.stringify({ prompt }),
463
+ signal: controller.signal
464
+ });
465
+
466
+ clearTimeout(timeoutId);
467
+
468
+ if (!response.ok) {
469
+ const errorBody = await response.json().catch(() => ({ detail: 'Unknown error occurred' }));
470
+ throw new Error(errorBody.detail || 'An unknown error occurred.');
471
+ }
472
+
473
+ const result = await response.json();
474
+ this.addMessageToChat(result.response, 'ai');
475
+ this.showStatus('Ready for your next question.', 'success');
476
+ } catch (error) {
477
+ console.error('Generation error:', error);
478
+ let errorMessage = error.message;
479
+ if (error.name === 'AbortError') {
480
+ errorMessage = 'Request timed out. Please try again.';
481
+ }
482
+ this.addMessageToChat(`An error occurred: ${errorMessage}`, 'system');
483
+ this.showStatus(`Error: ${errorMessage}`, 'error');
484
+ } finally {
485
+ this.state.isGenerating = false;
486
+ this.updateUI();
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Handles executing a non-Q&A task like summarization or planning.
492
+ */
493
+ async handleExecuteTask() {
494
+ const context = this.elements.contextInput.value.trim();
495
+ const task_type = this.elements.taskSelect.value;
496
+ const prompt = this.elements.chatInput.value.trim();
497
+
498
+ if (context.length < 20) {
499
+ this.showStatus('Please provide at least 20 characters of context for this task.', 'error');
500
+ return;
501
+ }
502
+
503
+ let userMessage = `Task: ${task_type.charAt(0).toUpperCase() + task_type.slice(1)}`;
504
+ if (prompt) {
505
+ userMessage += `\nPrompt: ${prompt}`;
506
+ }
507
+ this.addMessageToChat(userMessage, 'user');
508
+
509
+ this.elements.chatInput.value = '';
510
+ this.autoResizeTextarea(this.elements.chatInput);
511
+
512
+ this.state.isGenerating = true;
513
+ this.updateUI();
514
+ this.showStatus(`AI is performing task: ${task_type}...`, 'loading');
515
+
516
+ try {
517
+ const controller = new AbortController();
518
+ const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 second timeout
519
+
520
+ const response = await fetch('/api/v1/task', {
521
+ method: 'POST',
522
+ headers: {
523
+ 'Content-Type': 'application/json',
524
+ 'X-API-Key': this.state.userApiKey
525
+ },
526
+ body: JSON.stringify({ context, task_type, prompt }),
527
+ signal: controller.signal
528
+ });
529
+
530
+ clearTimeout(timeoutId);
531
+
532
+ if (!response.ok) {
533
+ const errorBody = await response.json().catch(() => ({ detail: 'Unknown error occurred' }));
534
+ throw new Error(errorBody.detail || 'An unknown error occurred.');
535
+ }
536
+
537
+ const result = await response.json();
538
+ this.addMessageToChat(result.result, 'ai');
539
+ this.showStatus('Task completed successfully.', 'success');
540
+ } catch (error) {
541
+ console.error('Task execution error:', error);
542
+ let errorMessage = error.message;
543
+ if (error.name === 'AbortError') {
544
+ errorMessage = 'Task timed out. Please try again.';
545
+ }
546
+ this.addMessageToChat(`An error occurred: ${errorMessage}`, 'system');
547
+ this.showStatus(`Error: ${errorMessage}`, 'error');
548
+ } finally {
549
+ this.state.isGenerating = false;
550
+ this.updateUI();
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Clears the context input and the indexed data on the backend.
556
+ */
557
+ async handleClearContext() {
558
+ this.elements.contextInput.value = '';
559
+ this.updateContextStats();
560
+ this.state.isIndexed = false;
561
+ this.updateUI();
562
+ this.showStatus('Clearing knowledge base...', 'loading');
563
+
564
+ try {
565
+ await fetch('/api/v1/clear_index', {
566
+ method: 'POST',
567
+ headers: { 'X-API-Key': this.state.userApiKey }
568
+ });
569
+ this.showStatus('Knowledge base cleared. Ready for new context.', 'success');
570
+ } catch (error) {
571
+ console.error('Clear index error:', error);
572
+ this.showStatus(`Error clearing index: ${error.message}`, 'error');
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Updates all UI elements based on the current application state.
578
+ */
579
+ updateUI() {
580
+ const hasContext = this.elements.contextInput.value.trim().length > 10;
581
+ const isQandA = this.elements.taskSelect.value === 'q_and_a';
582
+ const hasValidApiKey = this.state.apiKeyValidated;
583
+ const isBusy = this.state.isIndexing || this.state.isGenerating || this.state.isTestingApiKey;
584
+
585
+ // Update API key related buttons
586
+ const apiKeyEntered = this.elements.apiKeyInput.value.trim().length > 0;
587
+ this.elements.testApiKeyBtn.disabled = this.state.isTestingApiKey || !apiKeyEntered;
588
+ this.elements.saveApiKeyBtn.disabled = isBusy || !this.state.apiKeyValidated;
589
+
590
+ // Update button text based on state
591
+ if (this.state.isTestingApiKey) {
592
+ this.elements.testApiKeyBtn.textContent = 'Testing...';
593
+ } else {
594
+ this.elements.testApiKeyBtn.textContent = 'Test Key';
595
+ }
596
+
597
+ // Update context and chat related buttons
598
+ this.elements.indexContextBtn.disabled = isBusy || !hasContext || !hasValidApiKey;
599
+
600
+ if (isQandA) {
601
+ this.elements.sendButton.disabled = isBusy || !this.state.isIndexed || !hasValidApiKey;
602
+ } else {
603
+ this.elements.sendButton.disabled = isBusy || !hasContext || !hasValidApiKey;
604
+ }
605
+
606
+ // Update visual states
607
+ const buttonStates = [
608
+ this.elements.testApiKeyBtn,
609
+ this.elements.saveApiKeyBtn,
610
+ this.elements.indexContextBtn,
611
+ this.elements.sendButton
612
+ ];
613
+
614
+ buttonStates.forEach(button => {
615
+ if (button && button.disabled) {
616
+ button.style.opacity = '0.5';
617
+ button.style.cursor = 'not-allowed';
618
+ } else if (button) {
619
+ button.style.opacity = '1';
620
+ button.style.cursor = 'pointer';
621
+ }
622
+ });
623
+ }
624
+
625
+ /**
626
+ * Displays a status message to the user.
627
+ */
628
+ showStatus(message, type) {
629
+ const indicator = this.elements.statusIndicator;
630
+ indicator.classList.remove('hidden');
631
+ let colorClass = 'text-slate-400';
632
+ if (type === 'success') colorClass = 'text-green-400';
633
+ if (type === 'error') colorClass = 'text-red-400';
634
+
635
+ indicator.innerHTML = `<span class="${colorClass}">${message}</span>`;
636
+
637
+ if (type !== 'loading') {
638
+ setTimeout(() => indicator.classList.add('hidden'), 5000);
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Adds a new message to the chat display.
644
+ */
645
+ addMessageToChat(message, sender) {
646
+ const messageDiv = document.createElement('div');
647
+ messageDiv.className = 'chat-message flex items-start space-x-3 animate-fade-in';
648
+ const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
649
+
650
+ const icons = {
651
+ user: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
652
+ ai: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>`,
653
+ system: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`
654
+ };
655
+ const bubbleClasses = {
656
+ user: 'bg-gradient-to-br from-emerald-500 to-teal-600',
657
+ ai: 'bg-gradient-to-br from-indigo-500 to-purple-600',
658
+ system: 'bg-gradient-to-br from-blue-500 to-cyan-600'
659
+ };
660
+
661
+ const formattedMessage = sender === 'user' ?
662
+ this.escapeHtml(message).replace(/\n/g, '<br>') :
663
+ marked.parse(message);
664
+
665
+ messageDiv.innerHTML = `
666
+ <div class="w-8 h-8 ${bubbleClasses[sender]} rounded-full flex items-center justify-center flex-shrink-0 text-white">${icons[sender]}</div>
667
+ <div class="flex-1">
668
+ <div class="bg-slate-800/50 rounded-xl p-4 border border-slate-600/30">
669
+ <div class="text-slate-200 leading-relaxed markdown-content">${formattedMessage}</div>
670
+ </div>
671
+ <div class="text-xs text-slate-500 mt-2">${timestamp}</div>
672
+ </div>`;
673
+
674
+ this.elements.chatContainer.appendChild(messageDiv);
675
+ this.elements.chatContainer.scrollTop = this.elements.chatContainer.scrollHeight;
676
+ }
677
+
678
+ /**
679
+ * Updates character and word counts for the context input.
680
+ */
681
+ updateContextStats() {
682
+ const text = this.elements.contextInput.value;
683
+ this.elements.charCount.textContent = text.length.toLocaleString();
684
+ this.elements.wordCount.textContent = (text.trim().split(/\s+/).filter(Boolean).length).toLocaleString();
685
+ }
686
+
687
+ autoResizeTextarea(element) {
688
+ element.style.height = 'auto';
689
+ element.style.height = `${Math.min(element.scrollHeight, 120)}px`;
690
+ }
691
+
692
+ escapeHtml(text) {
693
+ const div = document.createElement('div');
694
+ div.textContent = text;
695
+ return div.innerHTML;
696
  }
 
 
697
  }
698
 
699
+ // Initialize the app when DOM is fully loaded
700
+ document.addEventListener('DOMContentLoaded', () => {
701
+ new ContextAwareApp();
702
+ });