// 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 = `
`;
}
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 = `
Select folder
Delete folder?
Are you sure you want to delete this domain?
`;
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 `
`;
}
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 = `
Loading...
Uploading Files...
Please wait for Doclink to process your files
This might take a moment depending on file size
`;
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 = `
`;
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 = `
`;
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, '');
// 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}${cellTag}>`;
});
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]}
`;
container.appendChild(item);
});
}
copyToClipboard(content) {
const cleanText = content.replace(/