Ab-Romia commited on
Commit
71306f3
·
verified ·
1 Parent(s): ba74fcb

Update static/app.js

Browse files
Files changed (1) hide show
  1. static/app.js +111 -38
static/app.js CHANGED
@@ -25,6 +25,10 @@ class ContextAwareApp {
25
  apiKeyContent: document.getElementById('api-key-content'),
26
  toggleIcon: document.getElementById('toggle-icon'),
27
 
 
 
 
 
28
  // Responsive collapsible section elements
29
  kbHeader: document.getElementById('kb-header'),
30
  kbContent: document.getElementById('kb-content'),
@@ -64,7 +68,7 @@ class ContextAwareApp {
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'
@@ -89,12 +93,21 @@ class ContextAwareApp {
89
  }
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();
@@ -105,7 +118,7 @@ class ContextAwareApp {
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') {
@@ -123,6 +136,23 @@ class ContextAwareApp {
123
  window.addEventListener('resize', () => this.setupResponsiveUI());
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  /**
127
  * Sets up the initial state of collapsible sections based on screen size.
128
  */
@@ -213,7 +243,7 @@ class ContextAwareApp {
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();
@@ -229,7 +259,6 @@ class ContextAwareApp {
229
  }
230
 
231
  try {
232
- // Create an AbortController for timeout handling
233
  const controller = new AbortController();
234
  const timeoutId = setTimeout(() => controller.abort(), 120000); // 120 second timeout
235
 
@@ -242,13 +271,12 @@ class ContextAwareApp {
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;
@@ -371,7 +399,7 @@ class ContextAwareApp {
371
  }
372
 
373
  /**
374
- * Handles the logic for indexing the provided context.
375
  */
376
  async handleIndexContext() {
377
  if (!this.state.apiKeyValidated) {
@@ -379,20 +407,35 @@ class ContextAwareApp {
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: {
@@ -400,35 +443,57 @@ class ContextAwareApp {
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
  */
@@ -556,6 +621,8 @@ class ContextAwareApp {
556
  */
557
  async handleClearContext() {
558
  this.elements.contextInput.value = '';
 
 
559
  this.updateContextStats();
560
  this.state.isIndexed = false;
561
  this.updateUI();
@@ -577,7 +644,10 @@ class ContextAwareApp {
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;
@@ -600,7 +670,8 @@ class ContextAwareApp {
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
@@ -631,10 +702,12 @@ class ContextAwareApp {
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
  }
 
25
  apiKeyContent: document.getElementById('api-key-content'),
26
  toggleIcon: document.getElementById('toggle-icon'),
27
 
28
+ // ✨ NEW: File Input elements
29
+ fileInput: document.getElementById('file-input'),
30
+ fileName: document.getElementById('file-name'),
31
+
32
  // Responsive collapsible section elements
33
  kbHeader: document.getElementById('kb-header'),
34
  kbContent: document.getElementById('kb-content'),
 
68
  "👋 **Welcome to ContextIQ!**\n\n" +
69
  "To get started:\n" +
70
  "1. **Enter your OpenRouter API key** in the configuration section above.\n" +
71
+ "2. **Add your context** by uploading a file or pasting text in the Knowledge Base.\n" +
72
  "3. **Index the context** and start asking questions!\n\n" +
73
  "🆓 You can get a free API key from [openrouter.ai](https://openrouter.ai) - no credit card required!",
74
  'system'
 
93
  }
94
  });
95
  this.elements.contextInput.addEventListener('input', () => {
96
+ // If user types in textarea, clear the file input
97
+ if (this.elements.fileInput.value) {
98
+ this.elements.fileInput.value = '';
99
+ this.elements.fileName.textContent = 'Choose a file...';
100
+ }
101
  this.updateContextStats();
102
+ this.updateUI();
103
  });
104
  this.elements.chatInput.addEventListener('input', () => this.autoResizeTextarea(this.elements.chatInput));
105
+
106
+ // ✨ NEW: File input listener
107
+ this.elements.fileInput.addEventListener('change', () => this.handleFileSelection());
108
+
109
 
110
+ // API Key listeners
111
  this.elements.testApiKeyBtn.addEventListener('click', (e) => {
112
  e.preventDefault();
113
  this.testApiKey();
 
118
  });
119
  this.elements.apiKeyInput.addEventListener('input', () => {
120
  this.onApiKeyInputChange();
121
+ this.updateUI();
122
  });
123
  this.elements.apiKeyInput.addEventListener('keydown', e => {
124
  if (e.key === 'Enter') {
 
136
  window.addEventListener('resize', () => this.setupResponsiveUI());
137
  }
138
 
139
+ /**
140
+ * ✨ NEW: Handles file selection, updates UI, and clears textarea.
141
+ */
142
+ handleFileSelection() {
143
+ const file = this.elements.fileInput.files[0];
144
+ if (file) {
145
+ this.elements.fileName.textContent = file.name;
146
+ // Clear textarea and its stats when a file is selected
147
+ this.elements.contextInput.value = '';
148
+ this.updateContextStats();
149
+ this.updateUI();
150
+ } else {
151
+ this.elements.fileName.textContent = 'Choose a file...';
152
+ }
153
+ }
154
+
155
+
156
  /**
157
  * Sets up the initial state of collapsible sections based on screen size.
158
  */
 
243
  }
244
 
245
  /**
246
+ * Test the API key validity
247
  */
248
  async testApiKey(silent = false) {
249
  const apiKey = this.elements.apiKeyInput.value.trim();
 
259
  }
260
 
261
  try {
 
262
  const controller = new AbortController();
263
  const timeoutId = setTimeout(() => controller.abort(), 120000); // 120 second timeout
264
 
 
271
 
272
  clearTimeout(timeoutId);
273
 
274
+ const result = await response.json();
275
+
276
  if (!response.ok) {
277
+ throw new Error(result.detail || `Server error (${response.status})`);
 
278
  }
279
 
 
 
280
  if (result.valid) {
281
  this.state.apiKeyValidated = true;
282
  this.state.userApiKey = apiKey;
 
399
  }
400
 
401
  /**
402
+ * ✨ UPDATED: Handles indexing from either a file or the textarea.
403
  */
404
  async handleIndexContext() {
405
  if (!this.state.apiKeyValidated) {
 
407
  return;
408
  }
409
 
410
+ const file = this.elements.fileInput.files[0];
411
  const context = this.elements.contextInput.value.trim();
412
+
413
+ if (!file && context.length < 20) {
414
+ this.showStatus('Context is too short. Please provide at least 20 characters or upload a file.', 'error');
415
  return;
416
  }
417
 
418
  this.state.isIndexing = true;
419
  this.updateUI();
420
+
421
+ // Decide which endpoint to use
422
+ if (file) {
423
+ this.showStatus(`Uploading and indexing ${file.name}...`, 'loading');
424
+ await this.handleIndexFile(file);
425
+ } else {
426
+ this.showStatus('Indexing context from text area...', 'loading');
427
+ await this.handleIndexText(context);
428
+ }
429
 
430
+ this.state.isIndexing = false;
431
+ this.updateUI();
432
+ }
433
 
434
+ /**
435
+ * Handles indexing from the text area.
436
+ */
437
+ async handleIndexText(context) {
438
+ try {
439
  const response = await fetch('/api/v1/index', {
440
  method: 'POST',
441
  headers: {
 
443
  'X-API-Key': this.state.userApiKey
444
  },
445
  body: JSON.stringify({ context }),
 
446
  });
447
 
448
+ const result = await response.json();
449
+ if (!response.ok) throw new Error(result.detail);
450
+
451
+ this.state.isIndexed = true;
452
+ this.showStatus(`Successfully indexed ${result.documents_added || '1'} document chunks.`, 'success');
453
 
454
+ } catch (error) {
455
+ this.handleIndexingError(error);
456
+ }
457
+ }
458
+
459
+ /**
460
+ * ✨ NEW: Handles indexing from a file upload.
461
+ */
462
+ async handleIndexFile(file) {
463
+ const formData = new FormData();
464
+ formData.append('file', file);
465
+
466
+ try {
467
+ const response = await fetch('/api/v1/index-file', {
468
+ method: 'POST',
469
+ headers: {
470
+ 'X-API-Key': this.state.userApiKey
471
+ // No 'Content-Type' header needed, browser sets it for FormData
472
+ },
473
+ body: formData,
474
+ });
475
 
476
  const result = await response.json();
477
+ if (!response.ok) throw new Error(result.detail);
478
+
479
  this.state.isIndexed = true;
480
+ this.showStatus(result.message, 'success');
481
+
482
  } catch (error) {
483
+ this.handleIndexingError(error);
 
 
 
 
 
 
 
 
 
 
 
484
  }
485
  }
486
 
487
+ /**
488
+ * Centralized error handler for all indexing methods.
489
+ */
490
+ handleIndexingError(error) {
491
+ console.error('Indexing error:', error);
492
+ this.showStatus(`Error indexing context: ${error.message}`, 'error');
493
+ this.state.isIndexed = false;
494
+ }
495
+
496
+
497
  /**
498
  * Handles sending a user's prompt to the backend for a response.
499
  */
 
621
  */
622
  async handleClearContext() {
623
  this.elements.contextInput.value = '';
624
+ this.elements.fileInput.value = ''; // Also clear the file input
625
+ this.elements.fileName.textContent = 'Choose a file...';
626
  this.updateContextStats();
627
  this.state.isIndexed = false;
628
  this.updateUI();
 
644
  * Updates all UI elements based on the current application state.
645
  */
646
  updateUI() {
647
+ const hasTextContext = this.elements.contextInput.value.trim().length > 10;
648
+ const hasFileContext = this.elements.fileInput.files.length > 0;
649
+ const hasContext = hasTextContext || hasFileContext;
650
+
651
  const isQandA = this.elements.taskSelect.value === 'q_and_a';
652
  const hasValidApiKey = this.state.apiKeyValidated;
653
  const isBusy = this.state.isIndexing || this.state.isGenerating || this.state.isTestingApiKey;
 
670
  if (isQandA) {
671
  this.elements.sendButton.disabled = isBusy || !this.state.isIndexed || !hasValidApiKey;
672
  } else {
673
+ // For other tasks, context comes from the text area, not the index
674
+ this.elements.sendButton.disabled = isBusy || !hasTextContext || !hasValidApiKey;
675
  }
676
 
677
  // Update visual states
 
702
  let colorClass = 'text-slate-400';
703
  if (type === 'success') colorClass = 'text-green-400';
704
  if (type === 'error') colorClass = 'text-red-400';
705
+ if (type === 'loading') colorClass = 'text-blue-400 animate-pulse';
706
 
707
  indicator.innerHTML = `<span class="${colorClass}">${message}</span>`;
708
 
709
  if (type !== 'loading') {
710
+ indicator.querySelector('span').classList.remove('animate-pulse');
711
  setTimeout(() => indicator.classList.add('hidden'), 5000);
712
  }
713
  }