Spaces:
Sleeping
Sleeping
Update static/app.js
Browse files- 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**
|
| 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();
|
| 94 |
});
|
| 95 |
this.elements.chatInput.addEventListener('input', () => this.autoResizeTextarea(this.elements.chatInput));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
// API Key listeners
|
| 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();
|
| 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
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 384 |
-
|
|
|
|
| 385 |
return;
|
| 386 |
}
|
| 387 |
|
| 388 |
this.state.isIndexing = true;
|
| 389 |
this.updateUI();
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
const result = await response.json();
|
|
|
|
|
|
|
| 414 |
this.state.isIndexed = true;
|
| 415 |
-
this.showStatus(
|
|
|
|
| 416 |
} catch (error) {
|
| 417 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
}
|