// Core Event System class EventEmitter { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } emit(event, data) { if (!this.events[event]) return; this.events[event].forEach(callback => callback(data)); } } // Base Component class Component { constructor(element) { this.element = element; this.events = new EventEmitter(); } createElement(tag, className = '') { const element = document.createElement(tag); if (className) element.className = className; return element; } destroy() { this.element.remove(); } } class FileBasket { constructor() { this.files = new Map(); this.drivefiles = new Map(); this.uploadQueue = []; this.totalSize = 0; this.maxBatchSize = 20 * 1024 * 1024; // 20MB this.maxConcurrent = 10; } addFiles(fileList) { let duplicates = 0; Array.from(fileList).forEach(file => { if (!this.drivefiles.has(file.name) && !this.files.has(file.name)) { this.files.set(file.name, { file: file, lastModified: file.lastModified, status: 'pending' }); this.uploadQueue.push(file.name); this.totalSize += file.size; } else { duplicates++; } }); return { fileNames: this.getFileNames(), duplicates: duplicates }; } addDriveFiles(driveFiles) { let duplicates = 0; Array.from(driveFiles).forEach(file => { if (!this.drivefiles.has(file.name) && !this.files.has(file.name)) { this.drivefiles.set(file.name, { fileId: file.id, name: file.name, mimeType: file.mimeType, status: 'pending' }); this.uploadQueue.push(file.name); this.totalSize += file.size; } else { duplicates++; } }); return { fileNames: this.getFileNames(), duplicates: duplicates }; } getBatch() { let currentBatchSize = 0; const batch = []; while (this.uploadQueue.length > 0 && batch.length < this.maxConcurrent) { const fileName = this.uploadQueue[0]; const fileInfo = this.files.get(fileName) || this.drivefiles.get(fileName); if (!fileInfo) { this.uploadQueue.shift(); // Remove invalid file from queue continue; } if (this.drivefiles.has(fileName)) { batch.push(this.uploadQueue.shift()); continue; } if (currentBatchSize + fileInfo.file.size > this.maxBatchSize) { break; } batch.push(this.uploadQueue.shift()); currentBatchSize += fileInfo.file.size; } return batch; } getFileFormData(fileName) { const localFile = this.files.get(fileName); if (localFile) { const formData = new FormData(); formData.append('file', localFile.file); formData.append('lastModified', localFile.lastModified); return formData; } // Check drive files const driveFile = this.drivefiles.get(fileName); if (driveFile) { const formData = new FormData(); const accessToken = document.cookie .split('; ') .find(row => row.startsWith('drive_access_token=')) ?.split('=')[1]; formData.append('driveFileId', driveFile.fileId); formData.append('driveFileName', driveFile.name); formData.append('lastModified', String(Date.now())); formData.append('accessToken', accessToken); return formData; } return null; } removeFile(fileName) { const fileInfo = this.files.get(fileName); if (fileInfo) { this.totalSize -= fileInfo.file.size; this.files.delete(fileName); const queueIndex = this.uploadQueue.indexOf(fileName); if (queueIndex > -1) { this.uploadQueue.splice(queueIndex, 1); } this.updateSourceCount(); return true; } return false; } getFileNames() { const regularFiles = Array.from(this.files.keys()); const driveFiles = Array.from(this.drivefiles.keys()); return [...regularFiles, ...driveFiles]; } hasFilesToUpload() { return this.uploadQueue.length > 0; } getFileStatus(fileName) { return this.files.get(fileName)?.status || null; } updateFileStatus(fileName, status) { const fileInfo = this.files.get(fileName); if (fileInfo) { fileInfo.status = status; return true; } return false; } clear() { this.files.clear(); this.uploadQueue = []; this.totalSize = 0; } } // Domain Manager (Storage) class DomainManager { constructor() { this.domains = new Map(); this.selectedDomainId = null; this.events = new EventEmitter(); } getDomain(domainId) { return this.domains.get(domainId); } async addDomain(domain) { const domainData = { id: domain.id, name: domain.name, fileCount: domain.files?.length || 0, files: domain.files || [], fileIDS: domain.fileIDS || [] }; const domainCard = new DomainCard(domainData); this.domains.set(domain.id, { data: domainData, component: domainCard }); return domainCard; } getAllDomains() { return Array.from(this.domains.values()).map(entry => ({ id: entry.data.id, name: entry.data.name, fileCount: entry.data.fileCount, files: entry.data.files, fileIDS: entry.data.fileIDS })); } updateDomainFileCount(domainId) { const domain = this.domains.get(domainId); if (domain) { // Update fileCount based on current files array length domain.data.fileCount = domain.data.files.length; // Update the domain card display if (domain.component) { const fileCountElement = domain.component.element.querySelector('.file-count'); if (fileCountElement) { fileCountElement.textContent = `${domain.data.fileCount} files`; } } // Emit an event for other components that might need this update this.events.emit('domainFileCountUpdated', { domainId: domainId, newCount: domain.data.fileCount }); } } // Single method to handle selection state selectDomain(domainId) { // Deselect previous if (this.selectedDomainId) { const previous = this.domains.get(this.selectedDomainId); if (previous) { previous.component.setSelected(false); } } // Select new const domain = this.domains.get(domainId); if (domain) { domain.component.setSelected(true); this.selectedDomainId = domainId; } } getSelectedDomain() { if (!this.selectedDomainId) return null; return this.domains.get(this.selectedDomainId); } clearSelection() { if (this.selectedDomainId) { const previous = this.domains.get(this.selectedDomainId); if (previous) { previous.component.setSelected(false); } this.selectedDomainId = null; } } renameDomain(domainId, newName) { const domain = this.domains.get(domainId); if (domain) { domain.data.name = newName; return true; } return false; } deleteDomain(domainId) { const wasSelected = this.selectedDomainId === domainId; const success = this.domains.delete(domainId); if (success && wasSelected) { this.selectedDomainId = null; } return success; } } // Domain Card Component class DomainCard extends Component { constructor(domainData) { const element = document.createElement('div'); element.className = 'domain-card'; super(element); this.data = domainData; this.render(); this.attachEventListeners(); } render() { this.element.innerHTML = `
${this.data.name}
${this.data.fileCount || 0} files
`; } attachEventListeners() { const checkbox = this.element.querySelector('.domain-checkbox'); checkbox.addEventListener('change', () => { this.events.emit('selected', { id: this.data.id, selected: checkbox.checked }); }); } setSelected(selected) { const checkbox = this.element.querySelector('.domain-checkbox'); checkbox.checked = selected; } } class DomainSettingsModal extends Component { constructor(domainManager) { const element = document.createElement('div'); element.id = 'domainSelectModal'; element.className = 'modal fade'; element.setAttribute('tabindex', '-1'); element.setAttribute('aria-hidden', 'true'); super(element); this.domainManager = domainManager; this.domainToDelete = null; this.deleteModal = null; this.temporarySelectedId = null; this.render(); this.initializeDeleteModal(); this.setupEventListeners(); } render() { this.element.innerHTML = ` `; this.element.innerHTML += ` `; document.body.appendChild(this.element); } setupEventListeners() { // Search functionality const searchInput = this.element.querySelector('#domainSearchInput'); searchInput?.addEventListener('input', (e) => { this.events.emit('domainSearch', e.target.value); }); // New domain button const newDomainBtn = this.element.querySelector('#newDomainBtn'); newDomainBtn?.addEventListener('click', () => { this.handleNewDomain(); }); // Domain deletion this.domainManager.events.on('domainFileCountUpdated', ({ domainId, newCount }) => { const domainCard = this.element.querySelector(`[data-domain-id="${domainId}"]`); if (domainCard) { const fileCountElement = domainCard.querySelector('.file-count'); if (fileCountElement) { fileCountElement.textContent = `${newCount} files`; } } }); // Select button const selectButton = this.element.querySelector('.select-button'); selectButton?.addEventListener('click', () => { if (this.temporarySelectedId) { this.events.emit('domainSelected', this.temporarySelectedId); this.hide(); } }); // Close button const closeButton = this.element.querySelector('.close-button'); closeButton?.addEventListener('click', () => { this.resetTemporarySelection(); this.hide(); }); // Handle modal hidden event this.element.addEventListener('hidden.bs.modal', () => { this.resetTemporarySelection(); }); } createDomainCard(domain) { return `
${domain.name}
${domain.fileCount} files
`; } setupDomainCardListeners() { this.element.querySelectorAll('.domain-card').forEach(card => { if (card.classList.contains('new-domain-input-card')) return; const domainId = card.dataset.domainId; const checkbox = card.querySelector('.domain-checkbox'); // Handle entire card click for selection card.addEventListener('click', (e) => { if (!e.target.closest('.domain-actions') && !e.target.closest('.checkbox-wrapper')) { checkbox.checked = !checkbox.checked; this.handleDomainSelection(checkbox, domainId); } }); // Handle checkbox click checkbox?.addEventListener('change', (e) => { e.stopPropagation(); this.handleDomainSelection(checkbox, domainId); }); // Delete button card.querySelector('.delete-button')?.addEventListener('click', (e) => { e.stopPropagation(); this.domainToDelete = domainId; this.showDomainDeleteModal(); }); // Edit button card.querySelector('.edit-button')?.addEventListener('click', (e) => { e.stopPropagation(); this.enableDomainEditing(card); }); }); } handleDomainSelection(checkbox, domainId) { // Uncheck all other checkboxes this.element.querySelectorAll('.domain-checkbox').forEach(cb => { if (cb !== checkbox) { cb.checked = false; } }); // Update temporary selection this.temporarySelectedId = checkbox.checked ? domainId : null; } resetTemporarySelection() { this.temporarySelectedId = null; this.element.querySelectorAll('.domain-checkbox').forEach(cb => { cb.checked = false; }); } handleNewDomain() { const template = document.getElementById('newDomainInputTemplate'); const domainsContainer = this.element.querySelector('#domainsContainer'); if (template && domainsContainer) { const clone = template.content.cloneNode(true); domainsContainer.appendChild(clone); const inputCard = domainsContainer.querySelector('.new-domain-input-card'); const input = inputCard.querySelector('.new-domain-input'); this.setupNewDomainHandlers(inputCard, input); input.focus(); } } setupNewDomainHandlers(inputCard, input) { const confirmBtn = inputCard.querySelector('.confirm-button'); const cancelBtn = inputCard.querySelector('.cancel-button'); const handleConfirm = async () => { const name = input.value.trim(); if (name) { if (name.length > 20) { const alertElement = document.createElement('div'); alertElement.className = 'alert-modal'; alertElement.innerHTML = `
I can't do it...

Folder name must be 20 characters or less. Please try again with a shorter name!

`; document.body.appendChild(alertElement); const closeButton = alertElement.querySelector('.alert-button'); closeButton.addEventListener('click', () => { alertElement.classList.remove('show'); document.body.style.overflow = ''; setTimeout(() => alertElement.remove(), 300); }); requestAnimationFrame(() => { alertElement.classList.add('show'); document.body.style.overflow = 'hidden'; }); return; } const result = await window.createDomain(window.serverData.userId, name); if (result.success) { this.events.emit('domainCreate', { id: result.id, name: name }); this.updateDomainCount(); inputCard.remove(); } else { if (result.message && result.message.includes('up to 3 domains')) { const alertElement = document.createElement('div'); alertElement.className = 'alert-modal'; alertElement.innerHTML = `
Folder Limit Reached

${result.message}

Domains Used: ${this.domainManager.getAllDomains().length}/3
`; document.body.appendChild(alertElement); const closeButton = alertElement.querySelector('.alert-button'); closeButton.addEventListener('click', () => { alertElement.classList.remove('show'); document.body.style.overflow = ''; setTimeout(() => alertElement.remove(), 100); }); requestAnimationFrame(() => { alertElement.classList.add('show'); document.body.style.overflow = 'hidden'; }); } else { this.events.emit('warning', 'Failed to create folder. Please try again.'); } inputCard.remove(); } } }; confirmBtn.addEventListener('click', handleConfirm); cancelBtn.addEventListener('click', () => inputCard.remove()); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleConfirm(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') inputCard.remove(); }); } async enableDomainEditing(card) { const domainInfo = card.querySelector('.domain-info'); const domainNameElement = domainInfo.querySelector('h6'); const currentName = domainNameElement.getAttribute('title') || domainNameElement.textContent; const domainId = card.dataset.domainId; const wrapper = document.createElement('div'); wrapper.className = 'domain-name-input-wrapper'; wrapper.innerHTML = `
`; const input = wrapper.querySelector('.domain-name-input'); const confirmBtn = wrapper.querySelector('.edit-confirm-button'); const cancelBtn = wrapper.querySelector('.edit-cancel-button'); const handleConfirm = async () => { const newName = input.value.trim(); if (newName && newName !== currentName) { if (newName.length > 20) { this.events.emit('warning', 'Folder name must be less than 20 characters'); return; } const success = await window.renameDomain(domainId, newName); if (success) { this.events.emit('domainEdit', { id: domainId, newName: newName }); wrapper.replaceWith(domainNameElement); domainNameElement.textContent = newName; domainNameElement.setAttribute('title', newName); } else { this.events.emit('warning', 'Failed to rename domain'); } } else { wrapper.replaceWith(domainNameElement); } }; confirmBtn.addEventListener('click', handleConfirm); cancelBtn.addEventListener('click', () => wrapper.replaceWith(domainNameElement)); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleConfirm(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Escape') wrapper.replaceWith(domainNameElement); }); domainNameElement.replaceWith(wrapper); input.focus(); input.select(); } updateDomainsList(domains) { const container = this.element.querySelector('#domainsContainer'); if (container) { container.innerHTML = domains.map(domain => this.createDomainCard(domain)).join(''); this.setupDomainCardListeners(); } } updateDomainCount() { const domains = this.domainManager.getAllDomains(); const count = domains.length; const percentage = (count / 3) * 100; const countElement = this.element.querySelector('.domains-count'); const progressBar = this.element.querySelector('.progress-bar'); if (countElement && progressBar) { countElement.textContent = `${count}/3`; progressBar.style.width = `${percentage}%`; } } show() { const modal = new bootstrap.Modal(this.element); this.resetTemporarySelection(); this.updateDomainCount(); modal.show(); } hide() { const modal = bootstrap.Modal.getInstance(this.element); if (modal) { modal.hide(); } } initializeDeleteModal() { const deleteModalElement = document.getElementById('deleteConfirmModal'); if (deleteModalElement) { this.deleteModal = new bootstrap.Modal(deleteModalElement, { backdrop: 'static', keyboard: false }); deleteModalElement.addEventListener('show.bs.modal', () => { document.getElementById('domainSelectModal').classList.add('delete-confirmation-open'); }); deleteModalElement.addEventListener('hidden.bs.modal', () => { document.getElementById('domainSelectModal').classList.remove('delete-confirmation-open'); this.domainToDelete = null; // Clean up on hide }); const confirmBtn = deleteModalElement.querySelector('#confirmDeleteBtn'); confirmBtn?.addEventListener('click', async () => { if (this.domainToDelete) { await this.handleDomainDelete(this.domainToDelete); this.domainToDelete = null; this.deleteModal.hide(); } }); const cancelBtn = deleteModalElement.querySelector('.btn-outline-secondary'); cancelBtn?.addEventListener('click', () => { this.domainToDelete = null; this.deleteModal.hide(); }); } } showDomainDeleteModal() { if (this.deleteModal) { this.deleteModal.show(); } } hideDomainDeleteModal() { if (this.deleteModal) { this.deleteModal.hide(); } } async handleDomainDelete(domainId) { const result = await window.deleteDomain(domainId); if (result.success) { this.events.emit('domainDelete', domainId); this.hideDomainDeleteModal(); this.updateDomainCount(); this.events.emit('message', { text: 'Knowledege Base deleted!', type: 'success' }); } else { this.hideDomainDeleteModal(); const messageElement = document.getElementById('domainInfoMessage'); if (messageElement) { messageElement.textContent = result.message; } const infoModal = new bootstrap.Modal(document.getElementById('defaultDomainInfoModal')); infoModal.show(); } } } class FileUploadModal extends Component { constructor(DomainManager) { const element = document.createElement('div'); element.id = 'fileUploadModal'; element.className = 'modal fade'; element.setAttribute('tabindex', '-1'); element.setAttribute('aria-hidden', 'true'); super(element); this.isUploading = false; this.fileBasket = new FileBasket(); this.urlInputModal = new URLInputModal() this.domainManager = DomainManager; this.render(); this.setupEventListeners(); this.setupCloseButton(); this.currentpicker = null; } render() { this.element.innerHTML = ` `; document.body.appendChild(this.element); } setupEventListeners() { const dropZone = this.element.querySelector('#dropZone'); const fileInput = this.element.querySelector('#fileInput'); const uploadBtn = this.element.querySelector('#uploadBtn'); const chooseText = this.element.querySelector('.choose-text'); const uploadIcon = this.element.querySelector('.upload-icon-wrapper'); const urlButton = this.element.querySelector('.url-input-btn'); // Drag and drop handlers ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }); }); ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, () => { if (!this.isUploading) { dropZone.classList.add('dragover'); } }); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, () => { dropZone.classList.remove('dragover'); }); }); // File drop handler dropZone.addEventListener('drop', (e) => { if (!this.isUploading) { const files = e.dataTransfer.files; this.handleFiles(files); } }); // Icon click handler uploadIcon.addEventListener('click', () => { if (!this.isUploading) { fileInput.click(); } }); // File input handler chooseText.addEventListener('click', () => { if (!this.isUploading) { fileInput.click(); } }); fileInput.addEventListener('change', () => { this.handleFiles(fileInput.files); }); // Upload button handler uploadBtn.addEventListener('click', () => { this.startUpload(); }); urlButton.addEventListener('click', () => { if (!this.isUploading) { this.urlInputModal.show(); } }); this.urlInputModal.events.on('urlProcessed', (result) => { if (result.files) { this.handleFiles(result.files); } this.events.emit('message', result); }); this.element.addEventListener('hidden.bs.modal', () => { this.events.emit('modalClose'); }); } handleFiles(newFiles) { if (this.isUploading) return; const fileList = this.element.querySelector('#fileList'); const uploadBtn = this.element.querySelector('#uploadBtn'); const uploadArea = this.element.querySelector('#dropZone'); let addFilesResult; if (newFiles[0]?.mimeType) { addFilesResult = this.fileBasket.addDriveFiles(newFiles); } else { addFilesResult = this.fileBasket.addFiles(newFiles); } if (addFilesResult.duplicates > 0) { this.events.emit('warning', `${addFilesResult.duplicates} files were skipped as they were already added`); } // Update UI fileList.innerHTML = ''; this.fileBasket.getFileNames().forEach(fileName => { const fileItem = this.createFileItem(fileName); fileList.appendChild(fileItem); }); this.updateUploadUI(fileList, uploadBtn, uploadArea); this.updateSourceCount(); } createFileItem(fileName) { const fileItem = document.createElement('div'); fileItem.className = 'file-item pending-upload'; fileItem.dataset.fileName = fileName; const driveFile = this.fileBasket.drivefiles.get(fileName); const icon = this.getFileIcon(fileName,driveFile?.mimeType); fileItem.innerHTML = `
${fileName}
`; const removeButton = fileItem.querySelector('.file-remove'); removeButton.addEventListener('click', () => { if (!this.isUploading) { this.fileBasket.removeFile(fileName); fileItem.remove(); this.updateUploadUI( this.element.querySelector('#fileList'), this.element.querySelector('#uploadBtn'), this.element.querySelector('#dropZone') ); } }); return fileItem; } setupCloseButton() { const closeButton = this.element.querySelector('.close-button'); closeButton.addEventListener('click', () => { console.log('Close button clicked'); this.hide(); }); } setLoadingState(isLoading) { const loadingOverlay = this.element.querySelector('.upload-loading-overlay'); const closeButton = this.element.querySelector('.close-button'); const uploadBtn = this.element.querySelector('#uploadBtn'); const modal = bootstrap.Modal.getInstance(this.element); if (isLoading) { loadingOverlay.style.display = 'flex'; closeButton.style.display = 'none'; uploadBtn.disabled = true; modal._config.backdrop = 'static'; modal._config.keyboard = false; } else { loadingOverlay.style.display = 'none'; closeButton.style.display = 'block'; uploadBtn.disabled = false; modal._config.backdrop = true; modal._config.keyboard = true; } } async startUpload() { if (!this.fileBasket.hasFilesToUpload() || this.isUploading) return; this.isUploading = true; const uploadBtn = this.element.querySelector('#uploadBtn'); uploadBtn.disabled = true; this.setLoadingState(true); let successCount = 0; try { while (this.fileBasket.hasFilesToUpload()) { const batch = this.fileBasket.getBatch(); const uploadPromises = batch.map(async (fileName) => { try { const result = await this.uploadFile(fileName); if (result.success) successCount++; } catch (error) { console.error(`Failed to upload ${fileName}:`, error); } }); await Promise.all(uploadPromises); } if (successCount > 0) { const uploadResult = await window.uploadFiles(window.serverData.userId); if (uploadResult.success) { this.events.emit('filesUploaded', uploadResult.data); this.resetUploadUI(); this.updateSourceCount(); this.events.emit('message', { text: `Successfully uploaded ${successCount} files`, type: 'success' }); setTimeout(() => { this.hide(); this.events.emit('modalClose'); }, 500); } else if (uploadResult.error && uploadResult.error.includes('Upgrade')) { console.log('first') console.log(uploadResult.error) const alertElement = document.createElement('div'); alertElement.className = 'alert-modal'; alertElement.innerHTML = `
File Limit Reached

${uploadResult.error}

`; document.body.appendChild(alertElement); const closeButton = alertElement.querySelector('.alert-button'); closeButton.addEventListener('click', () => { alertElement.classList.remove('show'); document.body.style.overflow = ''; setTimeout(() => alertElement.remove(), 300); }); requestAnimationFrame(() => { alertElement.classList.add('show'); document.body.style.overflow = 'hidden'; }); } else { console.log('second') console.log(uploadResult.error) throw new Error(uploadResult.error); } } } catch (error) { console.error('Upload error:', error); this.events.emit('error', error.message); } finally { this.isUploading = false; this.fileBasket.clear(); uploadBtn.disabled = false; this.setLoadingState(false); } } resetUploadUI() { const fileList = this.element.querySelector('#fileList'); const uploadBtn = this.element.querySelector('#uploadBtn'); const uploadArea = this.element.querySelector('#dropZone'); // Clear file list fileList.innerHTML = ''; // Reset upload area uploadArea.style.display = 'flex'; uploadBtn.disabled = true; // Remove "Add More Files" button this.removeAddMoreFilesButton(); // Clear FileBasket this.fileBasket.clear(); } async uploadFile(fileName) { const fileItem = this.element.querySelector(`[data-file-name="${fileName}"]`); const progressBar = fileItem.querySelector('.progress-bar'); try { const formData = this.fileBasket.getFileFormData(fileName); if (!formData) throw new Error('File not found'); fileItem.classList.remove('pending-upload'); fileItem.classList.add('uploading'); let success; if (formData.has('driveFileId')) { success = await window.storedriveFile(window.serverData.userId, formData); } else { success = await window.storeFile(window.serverData.userId, formData); } if (success) { progressBar.style.width = '100%'; fileItem.classList.remove('uploading'); fileItem.classList.add('uploaded'); return { success: true }; } else { throw new Error(result.error); } } catch (error) { fileItem.classList.remove('uploading'); fileItem.classList.add('upload-error'); return { success: false, error: error.message }; } } loadDrivePicker() { if (typeof google === 'undefined') { const script = document.createElement('script'); script.src = 'https://apis.google.com/js/api.js'; script.onload = () => { window.gapi.load('picker', () => { this.createPicker(); }); }; document.body.appendChild(script); } else { this.createPicker(); } } createPicker() { if (this.currentPicker) { this.currentPicker.dispose(); this.currentPicker = null; } const accessToken = document.cookie .split('; ') .find(row => row.startsWith('drive_access_token=')) ?.split('=')[1]; if (!accessToken) { const alertModal = document.createElement('div'); alertModal.className = 'alert-modal'; alertModal.innerHTML = `
Drive Access Required

To access your Google Drive files:
1. Sign out
2. Sign in with Google
3. Allow Drive access when prompted

`; document.body.appendChild(alertModal); requestAnimationFrame(() => { alertModal.classList.add('show'); document.body.style.overflow = 'hidden'; }); const closeButton = alertModal.querySelector('.alert-button'); closeButton.addEventListener('click', () => { alertModal.classList.remove('show'); document.body.style.overflow = ''; setTimeout(() => alertModal.remove(), 300); }); return; } const GOOGLE_API_KEY = document.cookie .split('; ') .find(row => row.startsWith('google_api_key=')) ?.split('=')[1]; const picker = new google.picker.PickerBuilder() .addView(google.picker.ViewId.DOCS) .setOAuthToken(accessToken) .setDeveloperKey(GOOGLE_API_KEY) .enableFeature(google.picker.Feature.SUPPORT_DRIVES) .enableFeature(google.picker.Feature.MULTISELECT_ENABLED) .setCallback((data) => { if (data[google.picker.Response.ACTION] === google.picker.Action.PICKED) { const docs = data[google.picker.Response.DOCUMENTS]; this.handleDriveSelection(docs); // Pass token to handler } }) .build(); picker.setVisible(true); this.currentPicker = picker; setTimeout(() => { const pickerFrame = document.querySelector('.picker-dialog-bg'); const pickerDialog = document.querySelector('.picker-dialog'); if (pickerFrame && pickerDialog) { document.querySelectorAll('.picker-dialog-bg, .picker-dialog').forEach(el => { if (el !== pickerFrame && el !== pickerDialog) { el.remove(); } }); pickerFrame.style.zIndex = '10000'; pickerDialog.style.zIndex = '10001'; } }, 0); } handleDriveSelection(files) { const supportedTypes = [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.google-apps.document', 'application/vnd.google-apps.document', 'application/vnd.google-apps.spreadsheet', 'application/vnd.google-apps.presentation', 'application/vnd.google-apps.script', ]; const filteredFiles = files.filter(file => { return supportedTypes.includes(file.mimeType); }); if (filteredFiles.length === 0) { this.events.emit('warning', 'No supported files selected. Please select PDF, DOCX, or TXT files.'); return; } if (filteredFiles.length < files.length) { this.events.emit('warning', `${files.length - filteredFiles.length} files were skipped due to unsupported file types`); } const fileList = this.element.querySelector('#fileList'); this.fileBasket.files.clear(); this.fileBasket.drivefiles.clear(); this.fileBasket.uploadQueue = []; fileList.innerHTML = ''; filteredFiles.forEach(file => { const fileItem = this.createFileItem(file.name); fileList.appendChild(fileItem); }); this.updateUploadUI( fileList, this.element.querySelector('#uploadBtn'), this.element.querySelector('#dropZone') ); this.handleFiles(filteredFiles); } getFileIcon(fileName, mimeType) { const extension = fileName.split('.').pop().toLowerCase(); if (mimeType) { switch (mimeType) { case 'application/vnd.google-apps.document': return 'bi-file-word'; case 'application/vnd.google-apps.spreadsheet': return 'bi-file-excel'; case 'application/vnd.google-apps.presentation': return 'bi-file-ppt'; case 'application/vnd.google-apps.script': return 'bi-file-text'; } } const iconMap = { pdf: 'bi-file-pdf', docx: 'bi-file-word', doc: 'bi-file-word', txt: 'bi-file-text', pptx: 'bi-file-ppt', xlsx: 'bi-file-excel', udf: 'bi-file-post', html: 'bi-file-code', }; return iconMap[extension] || 'bi-file'; } updateUploadUI(fileList, uploadBtn, uploadArea) { if (this.fileBasket.getFileNames().length > 0 || this.fileBasket.drivefiles.size > 0) { uploadArea.style.display = 'none'; uploadBtn.disabled = false; this.ensureAddMoreFilesButton(fileList); } else { uploadArea.style.display = 'flex'; uploadBtn.disabled = true; this.removeAddMoreFilesButton(); } } ensureAddMoreFilesButton(fileList) { let addFileBtn = this.element.querySelector('.add-file-btn'); if (!addFileBtn) { addFileBtn = document.createElement('button'); addFileBtn.className = 'add-file-btn'; addFileBtn.innerHTML = ` Add More Files `; addFileBtn.addEventListener('click', () => { if (!this.isUploading) { this.element.querySelector('#fileInput').click(); } }); fileList.after(addFileBtn); } addFileBtn.disabled = this.isUploading; addFileBtn.style.opacity = this.isUploading ? '0.5' : '1'; } removeAddMoreFilesButton() { const addFileBtn = this.element.querySelector('.add-file-btn'); if (addFileBtn) { addFileBtn.remove(); } } updateSourceCount() { const domains = this.domainManager.getAllDomains(); let totalSources = 0; domains.forEach(domain => { if (domain.fileCount) { totalSources += domain.fileCount; } }); const percentage = (totalSources / 20) * 100; const countElement = this.element.querySelector('.sources-count'); const progressBar = this.element.querySelector('.progress-bar'); if (countElement && progressBar) { countElement.textContent = `${totalSources}/20`; progressBar.style.width = `${percentage}%`; } } show(domainName = '') { const domainNameElement = this.element.querySelector('.domain-name'); if (domainNameElement) { domainNameElement.textContent = domainName; } this.updateSourceCount(); const modal = new bootstrap.Modal(this.element); modal.show(); } hide() { const modal = bootstrap.Modal.getInstance(this.element); if (modal) { modal.hide(); this.events.emit('modalClose'); this.fileBasket.clear(); this.resetUploadUI(); } } } class ChatManager extends Component { constructor() { const element = document.querySelector('.chat-content'); super(element); this.messageContainer = this.element.querySelector('.chat-messages'); this.setupMessageInput(); this.setupExportButton(); } setupMessageInput() { const container = document.querySelector('.message-input-container'); container.innerHTML = ` `; const input = container.querySelector('.message-input'); input.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.handleSendMessage(input); } }); } async handleSendMessage(input) { const message = input.value.trim(); if (!message) return; // Add user message this.addMessage(message, 'user'); input.value = ''; // Add loading message const loadingMessage = this.addLoadingMessage(); // Disable chat this.disableChat(); try { const selectedFileIds = window.app.sidebar.getSelectedFileIds(); const response = await window.sendMessage( message, window.serverData.userId, window.serverData.sessionId, selectedFileIds ); // Remove loading message loadingMessage.remove(); if (response.status === 400) { if (response.message.includes('Daily question limit')) { // Show limit reached modal const alertElement = document.createElement('div'); alertElement.className = 'alert-modal'; alertElement.innerHTML = `
Daily Limit Reached

${response.message}

Questions Used Today: 25/25
`; document.body.appendChild(alertElement); const closeButton = alertElement.querySelector('.alert-button'); closeButton.addEventListener('click', () => { alertElement.classList.remove('show'); document.body.style.overflow = ''; setTimeout(() => alertElement.remove(), 300); }); requestAnimationFrame(() => { alertElement.classList.add('show'); document.body.style.overflow = 'hidden'; }); } else { this.addMessage(response.message, 'ai'); } return; } if (response.answer && response.question_count == 10) { this.addMessage(response.answer, 'ai'); this.updateResources(response.resources, response.resource_sentences); this.events.emit('ratingModalOpen'); window.app.profileLimitsModal.updateDailyCount(response.daily_count); } else if (response.answer) { this.addMessage(response.answer, 'ai'); this.updateResources(response.resources, response.resource_sentences); window.app.profileLimitsModal.updateDailyCount(response.daily_count); } else { this.addMessage(response.message, 'ai'); window.app.profileLimitsModal.updateDailyCount(response.daily_count); } } catch (error) { loadingMessage.remove(); this.addMessage('Error generating message!', 'ai'); console.error('Error:', error); } finally { this.enableChat(); } } addMessage(content, type) { const message = document.createElement('div'); message.className = `chat-message ${type}`; const bubble = document.createElement('div'); bubble.className = `message-bubble ${type}-bubble`; const text = document.createElement('div'); text.className = 'message-text'; if (type === 'ai') { text.innerHTML = this.formatMessage(content); bubble.appendChild(text); if (!content.includes('what can I find for you?')) { message.setAttribute('data-exportable', 'true'); const actionBar = document.createElement('div'); actionBar.className = 'message-actions'; const actionContainer = document.createElement('div'); actionContainer.className = 'action-container'; const selectionMark = document.createElement('div'); selectionMark.className = 'selection-mark'; selectionMark.innerHTML = ''; const copyButton = document.createElement('button'); copyButton.className = 'copy-button'; copyButton.innerHTML = ` Copy`; copyButton.addEventListener('click', () => { const messageContent = text.innerHTML; this.copyToClipboard(messageContent); copyButton.innerHTML = ` Copied!`; copyButton.classList.add('copied'); setTimeout(() => { copyButton.innerHTML = ` Copy`; copyButton.classList.remove('copied'); }, 2000); }); selectionMark.addEventListener('click', () => { message.classList.toggle('selected'); this.updateExportButton(); }); actionContainer.appendChild(copyButton); actionBar.appendChild(copyButton); bubble.appendChild(selectionMark); bubble.appendChild(actionBar); message.appendChild(bubble); } else { message.appendChild(bubble); bubble.appendChild(text); message.appendChild(bubble); } } else { text.textContent = content; bubble.appendChild(text); message.appendChild(bubble) } bubble.appendChild(text); message.appendChild(bubble); this.messageContainer.appendChild(message); this.scrollToBottom(); return message; } setupExportButton() { const exportButton = document.querySelector('.export-button'); if (exportButton) { exportButton.addEventListener('click', () => this.handleExportSelected()); exportButton.disabled = true; } } updateExportButton() { const exportButton = document.querySelector('.export-button'); const selectedMessages = document.querySelectorAll('.chat-message.ai.selected'); const count = selectedMessages.length; let counter = document.querySelector('.export-counter'); if (!counter) { counter = document.createElement('div'); counter.className = 'export-counter'; exportButton.parentElement.appendChild(counter); } counter.textContent = `${count}/10`; counter.style.color = count === 10 ? '#10B981' : 'white'; exportButton.disabled = count === 0; if (count > 10) { const lastSelected = selectedMessages[selectedMessages.length - 1]; lastSelected.classList.remove('selected'); this.updateExportButton(); } } getSelectedMessages() { const selectedMessages = document.querySelectorAll('.chat-message.ai.selected'); return Array.from(selectedMessages).map(message => { return message.querySelector('.message-text').innerHTML; }); } async handleExportSelected() { const selectedContents = this.getSelectedMessages(); if (selectedContents.length === 0 || selectedContents.length > 10 ) return; const exportButton = document.querySelector('.export-button'); const originalHTML = exportButton.innerHTML; try { // Show loading state exportButton.innerHTML = `
`; exportButton.disabled = true; const result = await window.exportResponse(selectedContents); if (result === true) { // Success state exportButton.innerHTML = ''; setTimeout(() => { // Reset state exportButton.innerHTML = originalHTML; exportButton.disabled = false; // Deselect all messages document.querySelectorAll('.chat-message.ai.selected').forEach(msg => { msg.classList.remove('selected'); }); this.updateExportButton(); }, 2000); } else { // Error state exportButton.innerHTML = ''; setTimeout(() => { exportButton.innerHTML = originalHTML; exportButton.disabled = false; }, 2000); } } catch (error) { console.error('Export failed:', error); exportButton.innerHTML = ''; setTimeout(() => { exportButton.innerHTML = originalHTML; exportButton.disabled = false; }, 2000); } } updateHeader(domainName = null) { const headerTitle = document.querySelector('.header-title'); if (!headerTitle) return; if (domainName) { headerTitle.innerHTML = `Chat with ${domainName}`; } else { headerTitle.textContent = 'Chat'; } } addLoadingMessage() { const message = document.createElement('div'); message.className = 'chat-message ai'; message.innerHTML = `
Loading...
`; this.messageContainer.appendChild(message); this.scrollToBottom(); return message; } showDefaultMessage() { this.messageContainer.innerHTML = `
Please select a folder to start chatting with your documents. Click the settings icon to select a folder.
`; } formatMessage(text) { // First process headers let formattedText = text.replace(/\[header\](.*?)\[\/header\]/g, '
$1
'); // Handle nested lists with proper indentation formattedText = formattedText.replace(/^-\s*(.*?)$/gm, '
$1
'); formattedText = formattedText.replace(/^\s{2}-\s*(.*?)$/gm, '
$1
'); formattedText = formattedText.replace(/^\s{4}-\s*(.*?)$/gm, '
$1
'); // Process bold terms formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '$1'); formattedText = formattedText.replace(/\[bold\](.*?)\[\/bold\]/g, '$1'); return `
${formattedText}
`; } convertMarkdownToHtmlTable(content) { if (!content.includes('|')) { return content; } let segments = []; const startsWithTable = content.trimStart().startsWith('|'); if (startsWithTable) { const tableEndIndex = findTableEndIndex(content); if (tableEndIndex > 0) { const tableContent = content.substring(0, tableEndIndex).trim(); segments.push(processTableContent(tableContent)); if (tableEndIndex < content.length) { const remainingText = content.substring(tableEndIndex).trim(); if (remainingText) { segments.push(convertMarkdownToHtmlTable(remainingText)); } } } else { segments.push(processTableContent(content)); } } else { const tableRegex = /(\|[^\n]+\|(?:\r?\n\|[^\n]+\|)*)/g; let lastIndex = 0; let match; while ((match = tableRegex.exec(content)) !== null) { if (match.index > lastIndex) { const textContent = content.substring(lastIndex, match.index).trim(); if (textContent) { segments.push(`
${textContent}
`); } } segments.push(processTableContent(match[0])); lastIndex = match.index + match[0].length; } if (lastIndex < content.length) { const remainingText = content.substring(lastIndex).trim(); if (remainingText) { segments.push(`
${remainingText}
`); } } } return segments.join(''); function findTableEndIndex(text) { const lines = text.split('\n'); let lineIndex = 0; for (let i = 0; i < lines.length; i++) { lineIndex += lines[i].length + 1; if (!lines[i].trimStart().startsWith('|')) { return lineIndex - 1; } } return -1; } function processTableContent(tableContent) { const rows = tableContent.split(/\r?\n/).filter(row => row.trim() && row.includes('|')); let htmlTable = '
'; let hasSeparatorRow = rows.some(row => row.replace(/[\|\-:\s]/g, '').length === 0 ); rows.forEach((row, rowIndex) => { if (row.replace(/[\|\-:\s]/g, '').length === 0) return; const cells = []; let cellMatch; const cellRegex = /\|(.*?)(?=\||$)/g; while ((cellMatch = cellRegex.exec(row + '|')) !== null) { if (cellMatch[1] !== undefined) { cells.push(cellMatch[1].trim()); } } if (cells.length === 0) return; htmlTable += ''; cells.forEach(cell => { const isHeader = (rowIndex === 0 && !hasSeparatorRow) || (rowIndex === 0 && hasSeparatorRow); const cellTag = isHeader ? 'th' : 'td'; htmlTable += `<${cellTag} class="align-left">${cell}`; }); htmlTable += ''; }); htmlTable += '
'; return htmlTable; } } updateResources(resources, sentences) { const container = document.querySelector('.resources-list'); container.innerHTML = ''; if (!resources || !sentences || !resources.file_names?.length) { return; } sentences.forEach((sentence, index) => { const item = document.createElement('div'); item.className = 'resource-item'; const content = this.convertMarkdownToHtmlTable(sentence); item.innerHTML = `
${resources.file_names[index]} Page ${resources.page_numbers[index]}
${index + 1}
${content}
`; container.appendChild(item); }); } copyToClipboard(content) { const cleanText = content.replace(/
(.*?)<\/div>/g, '$1\n') .replace(/
(.*?)<\/div>/g, '- $1') .replace(/
(.*?)<\/div>/g, ' - $1') .replace(/
(.*?)<\/div>/g, ' - $1') .replace(/(.*?)<\/strong>/g, '$1') .replace(/<[^>]+>/g, '') .replace(/\n\s*\n/g, '\n\n') .trim(); navigator.clipboard.writeText(cleanText) .catch(err => console.error('Failed to copy text:', err)); } scrollToBottom() { this.element.scrollTop = this.element.scrollHeight; } enableChat() { this.element.classList.remove('chat-disabled'); const input = document.querySelector('.message-input'); input.disabled = false; input.placeholder = "Send message"; } disableChat() { this.element.classList.add('chat-disabled'); const input = document.querySelector('.message-input'); input.disabled = true; input.placeholder = "Select your folder to start chat..."; } clearDefaultMessage() { this.messageContainer.innerHTML = ''; } } // Sidebar Component class Sidebar extends Component { constructor(domainManager) { const element = document.createElement('div'); element.className = 'sidebar-container open'; super(element); this.domainManager = domainManager; this.isOpen = true; this.timeout = null; this.selectedFiles = new Set(); this.render(); this.setupEventListeners(); this.isModalOpen = false; } render() { this.element.innerHTML = ` `; } setupEventListeners() { // Existing event listeners const settingsIcon = this.element.querySelector('.settings-icon'); if (settingsIcon) { settingsIcon.addEventListener('click', () => { this.events.emit('settingsClick'); }); } const fileMenuBtn = this.element.querySelector('.open-file-btn'); fileMenuBtn.addEventListener('click', () => { this.events.emit('fileMenuClick'); }); // Add hover handlers for desktop const menuTrigger = document.querySelector('.menu-trigger'); if (menuTrigger) { menuTrigger.addEventListener('click', () => { this.toggle(); }); } this.events.on('modalOpen', () => { this.isModalOpen = true; }); this.events.on('modalClose', () => { this.isModalOpen = false; setTimeout(() => { this.toggle(false); // Force close the sidebar }, 200); }); // Mobile menu trigger handler this.events.on('menuTrigger', () => { if (window.innerWidth < 992) { const menuIcon = document.querySelector('.menu-trigger .bi-list'); if (menuIcon) { menuIcon.style.transform = this.isOpen ? 'rotate(0)' : 'rotate(45deg)'; } this.toggle(); } }); // Handle window resize window.addEventListener('resize', () => { if (window.innerWidth >= 992) { document.body.style.overflow = ''; const menuIcon = document.querySelector('.menu-trigger .bi-list'); if (menuIcon) { menuIcon.style.transform = 'rotate(0)'; } } }); // User Profile Menu const userSection = this.element.querySelector('#userProfileMenu'); if (userSection) { userSection.addEventListener('click', (e) => { e.stopPropagation(); userSection.classList.toggle('active'); }); // Close menu when clicking outside document.addEventListener('click', (e) => { if (!userSection.contains(e.target)) { userSection.classList.remove('active'); } }); // Handle menu items userSection.querySelectorAll('.menu-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); if (item.classList.contains('logout-item')) { // Handle logout logic here console.log('Logging out...'); } userSection.classList.remove('active'); }); }); } // Premium and Feedback links const premiumLink = this.element.querySelector('.premium-link'); if (premiumLink) { premiumLink.addEventListener('click', (e) => { e.preventDefault(); this.events.emit('premiumClick'); }); } const feedbackLink = this.element.querySelector('.bottom-links a:not(.premium-link)'); if (feedbackLink) { feedbackLink.addEventListener('click', (e) => { e.preventDefault(); this.events.emit('feedbackClick'); }); } const profileMenuItem = userSection.querySelector('.menu-item:first-child'); if (profileMenuItem) { profileMenuItem.addEventListener('click', (e) => { e.stopPropagation(); userSection.classList.remove('active'); this.events.emit('showProfileLimits'); }); } } toggle() { this.isOpen = !this.isOpen; this.element.classList.toggle('open', this.isOpen); // Toggle chat container margin const chatContainer = document.querySelector('.chat-container'); if (chatContainer) { chatContainer.classList.toggle('sidebar-closed', !this.isOpen); const messageContainer = document.querySelector('.message-container'); if (messageContainer) { messageContainer.style.left = this.isOpen ? '294px' : '0'; messageContainer.style.width = this.isOpen ? 'calc(100% - 600px - 294px)' : 'calc(100% - 600px)'; } } } updateDomainSelection(domain) { const domainText = this.element.querySelector('.selected-domain-text'); const folderIcon = this.element.querySelector('.bi-folder'); const helperText = this.element.querySelector('.helper-text'); if (domain) { domainText.textContent = domain.name; domainText.title = domain.name; folderIcon.className = 'bi bi-folder empty-folder'; helperText.style.display = 'none'; } else { domainText.textContent = 'No Domain Selected'; domainText.removeAttribute('title'); folderIcon.className = 'bi bi-folder empty-folder'; helperText.style.display = 'block'; } } updateFileList(files, fileIDS) { const fileList = this.element.querySelector('#sidebarFileList'); if (!fileList) return; fileList.innerHTML = ''; if (files.length > 0 && fileIDS.length > 0) { files.forEach((file, index) => { const fileItem = this.createFileListItem(file, fileIDS[index]); // Check the checkbox by default const checkbox = fileItem.querySelector('.file-checkbox'); if (checkbox) { checkbox.checked = true; } fileList.appendChild(fileItem); }); } this.updateFileMenuVisibility(); } createFileListItem(fileName, fileID) { const fileItem = document.createElement('li'); let extension; if (fileName.includes('http') || fileName.includes('www.')) { extension = 'html'; } else { extension = fileName.split('.').pop().toLowerCase(); } const icon = this.getFileIcon(extension); const truncatedName = this.truncateFileName(fileName); fileItem.innerHTML = `
${truncatedName}
`; this.selectedFiles.add(fileID); const checkbox = fileItem.querySelector('.file-checkbox'); checkbox.checked = true; // Handle checkbox changes checkbox.addEventListener('change', () => { if (checkbox.checked) { this.selectedFiles.add(fileID); } else { this.selectedFiles.delete(fileID); } // Update sources count window.app.updateSourcesCount(this.selectedFiles.size); }); const deleteBtn = fileItem.querySelector('.delete-file-btn'); const confirmActions = fileItem.querySelector('.delete-confirm-actions'); deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); // Show confirmation actions confirmActions.classList.add('show'); deleteBtn.style.display = 'none'; }); // Add confirm/cancel handlers const confirmBtn = fileItem.querySelector('.confirm-delete-btn'); const cancelBtn = fileItem.querySelector('.cancel-delete-btn'); confirmBtn.addEventListener('click', async (e) => { e.stopPropagation(); const selectedDomain = this.domainManager.getSelectedDomain(); if (!selectedDomain) return; const success = await window.removeFile(fileID, selectedDomain.data.id, window.serverData.userId); if (success) { // Remove file from UI fileItem.remove(); // Update domain file count selectedDomain.data.files = selectedDomain.data.files.filter(f => f !== fileName); selectedDomain.data.fileIDS = selectedDomain.data.fileIDS.filter(id => id !== fileID); this.domainManager.updateDomainFileCount(selectedDomain.data.id); // Update sources count const sourcesCount = selectedDomain.data.files.length; window.app.updateSourcesCount(sourcesCount); } }); cancelBtn.addEventListener('click', (e) => { e.stopPropagation(); confirmActions.classList.remove('show'); deleteBtn.style.display = 'flex'; }); return fileItem; } truncateFileName(fileName, maxLength = 25) { if (fileName.length <= maxLength) return fileName; let extension; if (fileName.includes('http') || fileName.includes('www.')) { extension = 'html'; } else { extension = fileName.split('.').pop().toLowerCase(); } const nameWithoutExt = fileName.slice(0, fileName.lastIndexOf('.')); // Leave room for ellipsis and extension const truncatedLength = maxLength - 3 - extension.length - 1; return `${nameWithoutExt.slice(0, truncatedLength)}...${extension}`; } getSelectedFileIds() { return Array.from(this.selectedFiles); } updateFileList(files, fileIDS) { const fileList = this.element.querySelector('#sidebarFileList'); if (!fileList) return; fileList.innerHTML = ''; this.selectedFiles.clear(); // Clear existing selections if (files.length > 0 && fileIDS.length > 0) { files.forEach((file, index) => { const fileItem = this.createFileListItem(file, fileIDS[index]); fileList.appendChild(fileItem); }); } this.updateFileMenuVisibility(); // Update initial sources count window.app.updateSourcesCount(this.selectedFiles.size); } updatePlanBadge(userType) { const planBadge = this.element.querySelector('.plan-badge'); if (planBadge) { if (userType === 'premium') { planBadge.textContent = 'Premium Plan'; } else { planBadge.textContent = 'Free Plan'; } } } hideDeleteConfirmations() { this.element.querySelectorAll('.delete-confirm-actions').forEach(actions => { actions.classList.remove('show'); }); this.element.querySelectorAll('.delete-file-btn').forEach(btn => { btn.style.display = 'flex'; }); } clearFileSelections() { this.selectedFiles.clear(); window.app.updateSourcesCount(0); } getFileIcon(extension) { const iconMap = { pdf: 'bi-file-pdf', docx: 'bi-file-word', doc: 'bi-file-word', txt: 'bi-file-text', pptx: 'bi-file-ppt', xlsx: 'bi-file-excel', udf: 'bi-file-post', html: 'bi-file-earmark-code', }; return iconMap[extension] || 'bi-file'; } updateFileMenuVisibility() { const fileList = this.element.querySelector('#sidebarFileList'); const helperText = this.element.querySelector('.helper-text'); const fileMenuBtn = this.element.querySelector('.open-file-btn'); const fileListContainer = this.element.querySelector('.file-list-container'); if (fileList.children.length > 0) { helperText.style.display = 'none'; helperText.style.height = '0'; helperText.style.margin = '0'; helperText.style.padding = '0'; } else { fileListContainer.style.height = 'auto'; fileMenuBtn.style.position = 'static'; fileMenuBtn.style.width = '100%'; } } } class PremiumModal extends Component { constructor() { const element = document.getElementById('premiumAlert'); super(element); this.setupEventListeners(); } setupEventListeners() { const closeButton = this.element.querySelector('.alert-button'); closeButton?.addEventListener('click', () => this.hide()); } show() { this.element.classList.add('show'); document.body.style.overflow = 'hidden'; } hide() { this.element.classList.remove('show'); document.body.style.overflow = ''; } } // Feedback Modal Component class FeedbackModal extends Component { constructor() { const element = document.createElement('div'); element.className = 'feedback-modal'; super(element); this.render(); this.setupEventListeners(); } render() { this.element.innerHTML = ` `; document.body.appendChild(this.element); } setupEventListeners() { // Close button handlers const closeButtons = this.element.querySelectorAll('.close-modal, .btn-cancel'); closeButtons.forEach(button => { button.addEventListener('click', () => this.hide()); }); // Click outside to close this.element.addEventListener('click', (e) => { if (e.target === this.element) { this.hide(); } }); // Form submission const form = this.element.querySelector('#feedback-form'); form.addEventListener('submit', async (e) => { e.preventDefault(); await this.handleSubmit(e); }); // File size validation const fileInput = this.element.querySelector('#feedback-screenshot'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file && file.size > 2 * 1024 * 1024) { alert('File size must be less than 2MB'); e.target.value = ''; } }); } async handleSubmit(e) { const form = e.target; const submitButton = form.querySelector('.btn-submit'); submitButton.disabled = true; try { const formData = new FormData(form); const result = await window.sendFeedback(formData, window.serverData.userId); if (result.success) { this.hide(); this.events.emit('success', result.message); } else { this.events.emit('error', result.message); } } catch (error) { console.error('Error in feedback submission:', error); this.events.emit('error', 'An unexpected error occurred'); } finally { submitButton.disabled = false; form.reset(); } } show() { this.element.classList.add('show'); document.body.style.overflow = 'hidden'; } hide() { this.element.classList.remove('show'); document.body.style.overflow = ''; this.element.querySelector('#feedback-form').reset(); } } class SuccessAlert extends Component { constructor() { const element = document.getElementById('feedbackSuccessAlert'); super(element); this.setupEventListeners(); } setupEventListeners() { const closeButton = this.element.querySelector('.alert-button'); closeButton?.addEventListener('click', () => this.hide()); } show() { this.element.classList.add('show'); document.body.style.overflow = 'hidden'; } hide() { this.element.classList.remove('show'); document.body.style.overflow = ''; } } // Logout class LogoutModal extends Component { constructor() { const element = document.getElementById('logoutConfirmModal'); super(element); this.setupEventListeners(); // Set URLs based on environment from serverData this.WEB_URL = window.serverData.environment === 'dev' ? 'http://localhost:3000' : 'https://doclink.io'; } setupEventListeners() { const logoutButton = this.element.querySelector('.alert-button'); const cancelButton = this.element.querySelector('.btn-cancel'); logoutButton?.addEventListener('click', () => { this.handleLogout(); }); cancelButton?.addEventListener('click', () => { this.hide(); }); } handleLogout() { try { // 1. Clear client-side app state localStorage.clear(); sessionStorage.clear(); // 2. Call FastAPI logout endpoint window.handleLogoutRequest(window.serverData.userId, window.serverData.sessionId) .finally(() => { // 3. Clear cookies manually as backup this.clearCookies(); // 4. Redirect to signout window.location.href = `${this.WEB_URL}/api/auth/signout?callbackUrl=/`; }); } catch (error) { console.error('Logout error:', error); window.location.href = this.WEB_URL; } } clearCookies() { const cookies = document.cookie.split(';'); const domain = window.location.hostname; for (let cookie of cookies) { const name = cookie.split('=')[0].trim(); document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=${domain}`; document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`; } } show() { this.element.classList.add('show'); document.body.style.overflow = 'hidden'; } hide() { this.element.classList.remove('show'); document.body.style.overflow = ''; } } // Rating Modal class RatingModal extends Component { constructor() { const element = document.createElement('div'); element.className = 'modal fade'; element.id = 'ratingModal'; element.setAttribute('tabindex', '-1'); element.setAttribute('aria-hidden', 'true'); super(element); this.rating = 0; this.render(); this.setupEventListeners(); } render() { this.element.innerHTML = ` `; document.body.appendChild(this.element); this.modal = new bootstrap.Modal(this.element); } setupEventListeners() { // Star rating handlers const stars = this.element.querySelectorAll('.stars i'); stars.forEach((star, index) => { star.addEventListener('click', () => this.handleStarClick(index)); star.addEventListener('mouseover', () => this.highlightStars(index)); star.addEventListener('mouseout', () => this.updateStars()); }); // Close button handler const closeButton = this.element.querySelector('.close-button'); closeButton.addEventListener('click', () => this.hide()); // Submit button handler const submitButton = this.element.querySelector('.submit-button'); submitButton.addEventListener('click', () => { const feedbackInput = this.element.querySelector('.feedback-input'); this.sendRating(this.rating,feedbackInput.value) setTimeout(() => { this.hide(); }, 1000); }); // Click outside to close this.element.addEventListener('click', (e) => { if (e.target === this.element) { this.hide(); } }); } handleStarClick(index) { this.rating = index + 1; this.updateStars(); } async sendRating(rating,user_note) { try { const result = await window.sendRating(rating, user_note, window.serverData.userId); if (result.success) { this.hide() this.events.emit('success', result.message); } else { this.events.emit('error', result.message); } } catch (error) { console.error('Error in rating submission:', error); this.events.emit('error', 'An unexpected error occurred'); } finally { this.reset(); } } highlightStars(index) { const stars = this.element.querySelectorAll('.stars i'); stars.forEach((star, i) => { star.classList.remove('bi-star-fill', 'bi-star'); if (i <= index) { star.classList.add('bi-star-fill'); star.classList.add('active'); } else { star.classList.add('bi-star'); star.classList.remove('active'); } }); } updateStars() { const stars = this.element.querySelectorAll('.stars i'); stars.forEach((star, i) => { star.classList.remove('bi-star-fill', 'bi-star', 'active'); if (i < this.rating) { star.classList.add('bi-star-fill'); star.classList.add('active'); } else { star.classList.add('bi-star'); } }); } show() { this.modal.show(); document.body.style.overflow = 'hidden'; } hide() { this.modal.hide(); document.body.style.overflow = ''; } reset() { this.rating = 0; this.updateStars(); const feedbackInput = this.element.querySelector('.feedback-input'); if (feedbackInput) { feedbackInput.value = ''; } } } // URLuploadModal class URLInputModal extends Component { constructor() { const element = document.createElement('div'); element.className = 'modal fade'; element.id = 'urlInputModal'; element.setAttribute('tabindex', '-1'); element.setAttribute('aria-hidden', 'true'); super(element); this.render(); this.setupEventListeners(); this.modal = null; } render() { this.element.innerHTML = ` `; document.body.appendChild(this.element); this.modal = new bootstrap.Modal(this.element); } setupEventListeners() { const urlInput = this.element.querySelector('.url-input'); const addButton = this.element.querySelector('.add-url-btn'); const closeButton = this.element.querySelector('.close-button'); // Enable/disable add button based on URL input urlInput.addEventListener('input', () => { addButton.disabled = !urlInput.value.trim(); }); // URL processing addButton.addEventListener('click', () => { const url = urlInput.value; this.startProcessing(url); urlInput.value = ''; }); // Close button handler closeButton.addEventListener('click', () => { this.hide(); }); // Click outside to close this.element.addEventListener('click', (e) => { if (e.target === this.element) { this.hide(); } }); } async startProcessing(url) { const clean_url = url.trim(); if (!clean_url) return; this.setLoadingState(true); try { const success = await window.storeURL(window.serverData.userId, clean_url); if (success === 1) { this.handleFileBasketAddition(clean_url) this.events.emit('urlProcessed', { message: 'Successfully processed URL', type: 'success' }); this.hide(); } else { throw new Error('Failed to process URL'); } } catch (error) { this.events.emit('error', error.message); } finally { this.setLoadingState(false); } } handleFileBasketAddition(url) { try { // Create a clean filename from the URL const urlObj = new URL(url); const fileName = `${urlObj.hostname}.html`; // Create URL file object similar to drive file object const urlFile = { name: fileName, mimeType: 'text/html', lastModified: Date.now() }; // Emit event for FileUploadModal to handle this.events.emit('urlProcessed', { files: [urlFile], message: 'Successfully processed URL', type: 'success' }); this.hide(); return true; } catch (error) { console.error('Error preparing URL file:', error); return false; } } setLoadingState(isLoading) { const loadingOverlay = this.element.querySelector('.upload-loading-overlay'); const closeButton = this.element.querySelector('.close-button'); const addButton = this.element.querySelector('.add-url-btn'); if (isLoading) { loadingOverlay.style.display = 'flex'; closeButton.style.display = 'none'; addButton.disabled = true; this.modal._config.backdrop = 'static'; this.modal._config.keyboard = false; } else { loadingOverlay.style.display = 'none'; closeButton.style.display = 'block'; addButton.disabled = false; this.modal._config.backdrop = true; this.modal._config.keyboard = true; } } show() { if (this.modal) { this.modal.dispose(); } this.modal = new bootstrap.Modal(this.element); this.element.style.zIndex = '9999'; this.modal.show(); setTimeout(() => { const backdrop = document.querySelector('.modal-backdrop:last-child'); if (backdrop) { backdrop.style.zIndex = '9998'; } }, 0); } hide() { this.modal.hide(); const urlInput = this.element.querySelector('.url-input'); urlInput.value = ''; } } // Add this class after other modal classes class ProfileLimitsModal extends Component { constructor(domainManager) { const element = document.createElement('div'); element.className = 'modal fade'; element.id = 'profileLimitsModal'; element.setAttribute('tabindex', '-1'); element.setAttribute('aria-hidden', 'true'); super(element); this.domainManager = domainManager; this.render(); this.setupEventListeners(); this.dailyQuestionsCount = 0; } render() { this.element.innerHTML = ` `; document.body.appendChild(this.element); } updateLimits() { const domains = this.domainManager.getAllDomains(); let totalSources = 0; const userType = window.app?.userData?.user_info?.user_type || 'free'; const upgradeButton = this.element.querySelector('.upgrade-section'); const limitsContainer = this.element.querySelector('.limits-container'); if (limitsContainer) { const limitIndicators = limitsContainer.querySelectorAll('.limit-indicator'); const dailyQuestionBar = limitIndicators.length >= 3 ? limitIndicators[2] : null; if (upgradeButton) { upgradeButton.style.display = userType === 'premium' ? 'none' : 'block'; } if (dailyQuestionBar) { dailyQuestionBar.style.display = userType === 'premium' ? 'none' : 'block'; } } domains.forEach(domain => { if (domain.fileCount) { totalSources += domain.fileCount; } }); if (userType === 'free') { this.updateProgressBar('sources', totalSources, 10); this.updateProgressBar('domains', domains.length, 3); this.updateProgressBar('questions', this.dailyQuestionsCount, 25); } else if (userType === 'premium') { this.updateProgressBar('sources', totalSources, 100); this.updateProgressBar('domains', domains.length, 20); } } updateDailyCount(count) { this.dailyQuestionsCount = count; if (this.element.classList.contains('show')) { this.updateProgressBar('questions', count, 25); } } updateProgressBar(type, current, max) { const countElement = this.element.querySelector(`.${type}-count`); const progressBar = countElement?.closest('.limit-indicator').querySelector('.progress-bar'); if (countElement && progressBar) { const percentage = (current / max) * 100; countElement.textContent = `${current}/${max}`; progressBar.style.width = `${percentage}%`; } } setupEventListeners() { const upgradeButton = this.element.querySelector('.upgrade-button'); upgradeButton?.addEventListener('click', () => { this.events.emit('upgradeClick'); }); } show() { this.updateLimits(); const modal = new bootstrap.Modal(this.element); modal.show(); } hide() { const modal = bootstrap.Modal.getInstance(this.element); if (modal) { modal.hide(); } } } // Application class App { constructor() { this.domainManager = new DomainManager(); this.sidebar = new Sidebar(this.domainManager); this.feedbackModal = new FeedbackModal(); this.domainSettingsModal = new DomainSettingsModal(this.domainManager); this.fileUploadModal = new FileUploadModal(this.domainManager); this.events = new EventEmitter(); this.userData = null; this.sourcesCount = 0; this.sourcesBox = document.querySelector('.sources-box'); this.sourcesNumber = document.querySelector('.sources-number'); this.chatManager = new ChatManager(); this.premiumModal = new PremiumModal(); this.successAlert = new SuccessAlert(); this.logoutModal = new LogoutModal(); this.ratingModal = new RatingModal(); this.profileLimitsModal = new ProfileLimitsModal(this.domainManager); this.chatManager.disableChat(); this.setupEventListeners(); } updateUserInterface() { // Update user section in sidebar const userEmail = this.sidebar.element.querySelector('.user-email'); const userAvatar = this.sidebar.element.querySelector('.user-avatar'); userEmail.textContent = this.userData.user_info.user_email; if (this.userData.user_info.user_picture_url && this.userData.user_info.user_picture_url !== "null") { userAvatar.innerHTML = `${this.userData.user_info.user_name}`; userAvatar.classList.add('has-image'); } else { userAvatar.textContent = this.userData.user_info.user_name[0].toUpperCase(); userAvatar.classList.remove('has-image'); } this.sidebar.updatePlanBadge(this.userData.user_info.user_type); } updateSourcesCount(count) { this.sourcesCount = count; if (this.sourcesNumber) { this.sourcesNumber.textContent = count; this.sourcesBox.setAttribute('count', count); } } updateDomainCount() { this.domainSettingsModal.updateDomainCount(); } setupEventListeners() { // Sidebar events this.sidebar.events.on('settingsClick', () => { this.domainSettingsModal.show(); }); this.sidebar.events.on('fileMenuClick', () => { const selectedDomain = this.domainManager.getSelectedDomain(); if (!selectedDomain) { this.events.emit('warning', 'Please select a domain first'); return; } this.fileUploadModal.show(selectedDomain.data.name); this.sidebar.events.emit('modalOpen'); }); this.sidebar.events.on('feedbackClick', () => { this.feedbackModal.show(); }); // Domain Settings Modal events this.domainSettingsModal.events.on('domainCreate', async (domainData) => { const domainCard = this.domainManager.addDomain({ id: domainData.id, name: domainData.name }); // Update the domains list in the modal this.domainSettingsModal.updateDomainsList(this.domainManager.getAllDomains()); this.updateDomainCount(); this.events.emit('message', { text: `Successfully created folder ${domainData.name}`, type: 'success' }); }); this.domainSettingsModal.events.on('domainSearch', (searchTerm) => { const filteredDomains = this.domainManager.searchDomains(searchTerm); this.domainSettingsModal.updateDomainsList(filteredDomains); }); this.domainSettingsModal.events.on('domainSelected', async (domainId) => { try { const success = await window.selectDomain(domainId, window.serverData.userId); if (success) { const domain = this.domainManager.getDomain(domainId); if (!domain) return; // Update domain manager state and UI this.domainManager.selectDomain(domainId); this.sidebar.updateDomainSelection(domain.data); // Update header with domain name this.chatManager.updateHeader(domain.data.name); const files = domain.data.files || []; const fileIDS = domain.data.fileIDS || []; this.sidebar.updateFileList(files, fileIDS); // Update sources count this.updateSourcesCount(files.length); // Enable chat this.chatManager.enableChat(); this.events.emit('message', { text: `Successfully switched to folder ${domain.data.name}`, type: 'success' }); } } catch (error) { this.events.emit('message', { text: 'Failed to select folder', type: 'error' }); } }); const selectButton = this.domainSettingsModal.element.querySelector('.select-button'); selectButton?.addEventListener('click', () => { const selectedCheckbox = this.domainSettingsModal.element.querySelector('.domain-checkbox:checked'); if (selectedCheckbox) { const domainCard = selectedCheckbox.closest('.domain-card'); const domainId = domainCard.dataset.domainId; this.domainSettingsModal.events.emit('domainSelected', domainId); } }); this.domainSettingsModal.events.on('domainEdit', async ({ id, newName }) => { const success = this.domainManager.renameDomain(id, newName); if (success) { // If this is the currently selected domain, update the sidebar const selectedDomain = this.domainManager.getSelectedDomain(); if (selectedDomain && selectedDomain.data.id === id) { this.sidebar.updateDomainSelection(selectedDomain.data); } // Update the domains list in the modal this.domainSettingsModal.updateDomainsList(this.domainManager.getAllDomains()); this.events.emit('message', { text: `Successfully renamed folder to ${newName}`, type: 'success' }); } }); this.domainSettingsModal.events.on('warning', (message) => { this.events.emit('message', { text: message, type: 'warning' }); }); this.domainSettingsModal.events.on('domainDelete', async (domainId) => { const wasSelected = this.domainManager.getSelectedDomain()?.data.id === domainId; if (this.domainManager.deleteDomain(domainId)) { if (wasSelected) { // Reset sidebar to default state this.sidebar.updateDomainSelection(null); this.sidebar.updateFileList([], []); // Reset sources count this.updateSourcesCount(0); // Disable chat this.chatManager.disableChat(); } this.domainSettingsModal.updateDomainsList(this.domainManager.getAllDomains()); this.updateDomainCount(); this.events.emit('message', { text: 'Folder successfully deleted', type: 'success' }); } }); this.chatManager.events.on('ratingModalOpen', () => { setTimeout(() => { this.ratingModal.show(); }, 500); }); // File Upload Modal events this.fileUploadModal.events.on('filesUploaded', (data) => { const selectedDomain = this.domainManager.getSelectedDomain(); if (selectedDomain) { // Access the nested data object selectedDomain.data.files = data.file_names; selectedDomain.data.fileIDS = data.file_ids; this.sidebar.updateFileList(data.file_names, data.file_ids); this.updateSourcesCount(data.file_names.length); this.domainManager.updateDomainFileCount(selectedDomain.data.id); } }); this.fileUploadModal.events.on('warning', (message) => { this.events.emit('message', { text: message, type: 'warning' }); }); this.fileUploadModal.events.on('error', (message) => { this.events.emit('message', { text: message, type: 'error' }); }); this.fileUploadModal.events.on('modalClose', () => { this.sidebar.events.emit('modalClose'); }); // Feedback Modal events this.feedbackModal.events.on('feedbackSubmitted', (message) => { console.log(message); }); this.feedbackModal.events.on('feedbackError', (error) => { console.error(error); }); this.feedbackModal.events.on('success', (message) => { this.successAlert.show(); }); // Premium Modal Events const premiumLink = this.sidebar.element.querySelector('.premium-link'); premiumLink?.addEventListener('click', (e) => { e.preventDefault(); this.initiateCheckout(); }); this.profileLimitsModal.events.on('upgradeClick', () => { this.initiateCheckout(); }); // Logout event const logoutItem = this.sidebar.element.querySelector('.logout-item'); logoutItem?.addEventListener('click', (e) => { e.preventDefault(); this.logoutModal.show(); }); this.sidebar.events.on('showProfileLimits', () => { this.profileLimitsModal.show() }); } // In App class initialization async init() { // Initialize this.userData = await window.fetchUserInfo(window.serverData.userId); if (!this.userData) { throw new Error('Failed to load user data'); } // Update user interface with user data this.updateUserInterface() // Store domain data Object.keys(this.userData.domain_info).forEach(key => { const domainData = this.userData.domain_info[key]; const domain = { id: key, name: domainData.domain_name, fileCount: domainData.file_names.length, files: domainData.file_names, fileIDS: domainData.file_ids }; this.domainManager.addDomain(domain); }); // Update UI with domain data this.domainSettingsModal.updateDomainsList( this.domainManager.getAllDomains() ); window.user_type = this.userData.user_type // Add sidebar to DOM document.body.appendChild(this.sidebar.element); // Setup menu trigger const menuTrigger = document.querySelector('.menu-trigger'); if (menuTrigger) { menuTrigger.addEventListener('click', () => { this.sidebar.events.emit('menuTrigger'); }); } window.app.profileLimitsModal.updateDailyCount(this.userData.user_info.user_daily_count); // Welcome operations const isFirstTime = window.serverData.isFirstTime === 'True'; if (1 === 1) { localStorage.setItem('firstTime', 0); const firstTimeMsg = `[header]Welcome to Doclink${this.userData.user_info.user_name ? `, ${this.userData.user_info.user_name}` : ''}👋[/header]\nYour first folder with helpful guide settled up. You can always use this file to get information about Doclink!\n[header]To get started[/header]\n- Select your folder on navigation bar \n- Upload your documents or insert a link\n- Ask any question to get information\n- All answers will include sources on references\n\n[header]Quick Tips[/header]\n- Doclink is specialized to answer only from your files\n- Specialized questions can help Doclink to find information better\n- Doclink supports PDF, DOCX, Excel, PowerPoint, UDF and TXT file formats\n- You can create different folders for different topics and interact with them\n- You can also ask just selected files to get isolated information\n- You can select answers on the upper right of the message box and create report with clicking report icon on the chat`; this.chatManager.addMessage(firstTimeMsg, 'ai'); const domains = this.domainManager.getAllDomains(); if (domains.length > 0) { this.domainSettingsModal.events.emit('domainSelected', domains[0].id); } } else { // Regular welcome message for returning users this.chatManager.addMessage( `Welcome ${this.userData.user_info.user_name}, what can I find for you?`, 'ai' ); } } // Initial Checkout initiateCheckout() { const checkoutUrl = 'https://doclinkio.lemonsqueezy.com/buy/0c0294bb-1cbe-4411-a9bc-800053d1580c'; window.location.href = checkoutUrl; } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.app = new App(); window.app.init(); const resourcesTrigger = document.querySelector('.resources-trigger'); const resourcesContainer = document.querySelector('.resources-container'); const mainContent = document.querySelector('.chat-container'); if (resourcesTrigger && resourcesContainer) { resourcesTrigger.addEventListener('click', () => { resourcesContainer.classList.toggle('show'); mainContent.classList.toggle('blur-content'); if (resourcesContainer.classList.contains('show')) { backdrop.classList.add('show'); document.body.style.overflow = 'hidden'; } else { backdrop.classList.remove('show'); document.body.style.overflow = ''; } }); // Escape tuşu ile kapatma document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && resourcesContainer.classList.contains('show')) { resourcesContainer.classList.remove('show'); mainContent.classList.remove('blur-content'); backdrop.classList.remove('show'); document.body.style.overflow = ''; } }); } });