Files
intelaide/doclink/app/static/js/app.js
2026-01-20 04:54:10 +00:00

3659 lines
135 KiB
JavaScript

// 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 = `
<div class="domain-content">
<div class="checkbox-wrapper">
<input type="checkbox" id="${this.data.id}" class="domain-checkbox">
<label for="${this.data.id}" class="checkbox-label"></label>
</div>
<div class="domain-info">
<h6 title="${this.data.name}">${this.data.name}</h6>
<span class="file-count">${this.data.fileCount || 0} files</span>
</div>
</div>
`;
}
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="domain-modal-wrapper">
<div class="domain-header">
<h5>Select folder</h5>
<button type="button" class="close-button" data-bs-dismiss="modal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="limit-indicator mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<small class="text-secondary">Folder Limit</small>
<small class="text-secondary domains-count">0/3</small>
</div>
<div class="progress" style="height: 6px; background: rgba(255, 255, 255, 0.1);">
<div class="progress-bar bg-primary-green" style="width: 0%"></div>
</div>
</div>
<div class="domain-search">
<i class="bi bi-search"></i>
<input type="text" placeholder="Search..." class="domain-search-input" id="domainSearchInput">
</div>
<div class="domains-container" id="domainsContainer">
<!-- Domains will be populated here -->
</div>
<button class="new-domain-button" id="newDomainBtn">
<i class="bi bi-plus-circle"></i>
Create New
</button>
<button class="select-button">
Select
</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="domain-modal-wrapper text-center">
<h6 class="mb-3">Delete folder?</h6>
<p class="text-secondary mb-4">Are you sure you want to delete this domain?</p>
<div class="d-flex gap-3">
<button class="btn btn-outline-secondary flex-grow-1" data-bs-dismiss="modal">Cancel</button>
<button class="btn btn-danger flex-grow-1" id="confirmDeleteBtn">Delete</button>
</div>
</div>
</div>
</div>
</div>
<!-- Template for new domain input -->
<template id="newDomainInputTemplate">
<div class="domain-card new-domain-input-card">
<input type="text" class="new-domain-input" placeholder="Enter name" autofocus>
<div class="new-domain-actions">
<button class="confirm-button"><i class="bi bi-check"></i></button>
<button class="cancel-button"><i class="bi bi-x"></i></button>
</div>
</div>
</template>
`;
this.element.innerHTML += `
<div class="modal fade" id="defaultDomainInfoModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="domain-modal-wrapper text-center">
<h6 class="mb-3">I can't do it...</h6>
<p class="text-secondary mb-4" id="domainInfoMessage"></p>
<div class="d-flex justify-content-center">
<button class="btn" style="background-color: #4169E1;; color: #fff;" data-bs-dismiss="modal">Got it</button>
</div>
</div>
</div>
</div>
</div>
`;
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 `
<div class="domain-card" data-domain-id="${domain.id}">
<div class="domain-content">
<div class="checkbox-wrapper">
<input type="checkbox" id="domain-${domain.id}" class="domain-checkbox">
<label for="domain-${domain.id}" class="checkbox-label"></label>
</div>
<div class="domain-info">
<h6 title="${domain.name}">${domain.name}</h6>
<span class="file-count">${domain.fileCount} files</span>
</div>
</div>
<div class="domain-actions">
<button class="edit-button">
<i class="bi bi-pencil"></i>
</button>
<button class="delete-button">
<i class="bi bi-trash3"></i>
</button>
</div>
</div>
`;
}
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 = `
<div class="alert-content">
<h5 class="alert-title">I can't do it...</h5>
<p class="alert-message">Folder name must be 20 characters or less. Please try again with a shorter name!</p>
<button class="alert-button" style="background-color: #4169E1;">Got it</button>
</div>
`;
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 = `
<div class="alert-content">
<div class="alert-icon">
<i class="bi bi-exclamation-circle text-primary-green"></i>
</div>
<h5 class="alert-title">Folder Limit Reached</h5>
<p class="alert-message">${result.message}</p>
<div class="domain-count mt-3 text-secondary">
<small>Domains Used: ${this.domainManager.getAllDomains().length}/3</small>
</div>
<button class="alert-button">Got it</button>
</div>
`;
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 = `
<input type="text" class="domain-name-input" value="${currentName}" maxlength="20">
<div class="domain-edit-actions">
<button class="edit-confirm-button"><i class="bi bi-check"></i></button>
<button class="edit-cancel-button"><i class="bi bi-x"></i></button>
</div>
`;
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="domain-modal-wrapper">
<div class="modal-header border-0 d-flex align-items-center">
<div>
<h6 class="mb-0">Selected Folder: <span class="domain-name text-primary-green text-truncate"></span></h6>
</div>
<button type="button" class="close-button" data-bs-dismiss="modal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="limit-indicator mt-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<small class="text-secondary">Total Sources</small>
<small class="text-secondary sources-count">0/20</small>
</div>
<div class="progress" style="height: 6px; background: rgba(255, 255, 255, 0.1);">
<div class="progress-bar bg-primary-green" style="width: 0%"></div>
</div>
</div>
</div>
<div class="upload-container">
<div id="fileList" class="file-list mb-3"></div>
<div class="upload-area" id="dropZone">
<div class="upload-content text-center">
<div class="upload-icon-wrapper">
<div class="upload-icon">
<i class="bi bi-cloud-upload text-primary-green"></i>
</div>
</div>
<h5 class="mb-2">Upload Files</h5>
<p class="mb-3">Drag & drop or <span class="text-primary-green choose-text">choose files</span> to upload</p>
<small class="text-secondary">Supported file types: PDF, DOCX, XLSX, PPTX, UDF and TXT</small>
<input type="file" id="fileInput" multiple accept=".pdf,.docx,.xlsx,.pptx,,.udf,.txt" class="d-none">
</div>
</div>
<button class="url-input-btn w-100 mb-2 d-flex align-items-center justify-content-center gap-2">
<i class="bi bi-link-45deg"></i>
Add from URL
</button>
<button class="upload-btn mt-3" id="uploadBtn" disabled>
Upload
<div class="upload-progress">
<div class="progress-bar"></div>
</div>
</button>
<div class="upload-loading-overlay" style="display: none">
<div class="loading-content">
<div class="spinner-border text-primary-green mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-2">Uploading Files...</h5>
<p class="text-center mb-0">Please wait for Doclink to process your files</p>
<p class="text-center text-secondary">This might take a moment depending on file size</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="file-icon">
<i class="bi ${icon} text-primary-green"></i>
</div>
<div class="file-info">
<div class="file-name">${fileName}</div>
<div class="file-progress">
<div class="progress-bar"></div>
</div>
</div>
<div class="file-remove">
<i class="bi bi-trash"></i>
</div>
`;
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 = `
<div class="alert-content">
<div class="alert-icon">
<i class="bi bi-exclamation-circle text-primary-green"></i>
</div>
<h5 class="alert-title">File Limit Reached</h5>
<p class="alert-message">${uploadResult.error}</p>
<button class="alert-button">Got it</button>
</div>
`;
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 = `
<div class="alert-content">
<div class="alert-icon">
<i class="bi bi-exclamation-circle text-primary-green"></i>
</div>
<h5 class="alert-title">Drive Access Required</h5>
<p class="alert-message">To access your Google Drive files:
<br>1. Sign out
<br>2. Sign in with Google
<br>3. Allow Drive access when prompted
</p>
<button class="alert-button">Got it!</button>
</div>
`;
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 = `
<i class="bi bi-plus-circle"></i>
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 = `
<textarea
class="message-input"
placeholder="Please select your folder from settings ⚙️ to start chat!"
rows="1"
disabled
></textarea>
<button class="export-button" disabled title="Export Selected Messages">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4h14a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2z" stroke="currentColor" stroke-width="2"/>
<path d="M8 8h8M8 12h8M8 16h5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16 12l3 3m0 0l3-3m-3 3V4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
`;
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 = `
<div class="alert-content">
<div class="alert-icon">
<i class="bi bi-exclamation-circle text-primary-green"></i>
</div>
<h5 class="alert-title">Daily Limit Reached</h5>
<p class="alert-message">${response.message}</p>
<div class="usage-count mt-3">
<small>Questions Used Today: 25/25</small>
</div>
<button class="alert-button">Got it</button>
</div>
`;
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 = '<i class="bi bi-check-circle"></i>';
const copyButton = document.createElement('button');
copyButton.className = 'copy-button';
copyButton.innerHTML = `
<i class="bi bi-clipboard"></i>
<span class="action-text">Copy</span>`;
copyButton.addEventListener('click', () => {
const messageContent = text.innerHTML;
this.copyToClipboard(messageContent);
copyButton.innerHTML = `
<i class="bi bi-check2"></i>
<span class="action-text">Copied!</span>`;
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.innerHTML = `
<i class="bi bi-clipboard"></i>
<span class="action-text">Copy</span>`;
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 = `<div class="spinner-border spinner-border-sm" role="status"></div>`;
exportButton.disabled = true;
const result = await window.exportResponse(selectedContents);
if (result === true) {
// Success state
exportButton.innerHTML = '<i class="bi bi-check2"></i>';
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 = '<i class="bi bi-x-circle"></i>';
setTimeout(() => {
exportButton.innerHTML = originalHTML;
exportButton.disabled = false;
}, 2000);
}
} catch (error) {
console.error('Export failed:', error);
exportButton.innerHTML = '<i class="bi bi-x-circle"></i>';
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 <span style="color: var(--primary-dark); font-size: 1.1em;">${domainName}</span>`;
} else {
headerTitle.textContent = 'Chat';
}
}
addLoadingMessage() {
const message = document.createElement('div');
message.className = 'chat-message ai';
message.innerHTML = `
<div class="message-bubble ai-bubble">
<div class="message-text">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
`;
this.messageContainer.appendChild(message);
this.scrollToBottom();
return message;
}
showDefaultMessage() {
this.messageContainer.innerHTML = `
<div class="chat-message ai">
<div class="message-bubble ai-bubble">
<div class="message-text">
Please select a folder to start chatting with your documents.
Click the settings icon <i class="bi bi-gear"></i> to select a folder.
</div>
</div>
</div>
`;
}
formatMessage(text) {
// First process headers
let formattedText = text.replace(/\[header\](.*?)\[\/header\]/g, '<div class="message-header">$1</div>');
// Handle nested lists with proper indentation
formattedText = formattedText.replace(/^-\s*(.*?)$/gm, '<div class="message-item">$1</div>');
formattedText = formattedText.replace(/^\s{2}-\s*(.*?)$/gm, '<div class="message-item nested-1">$1</div>');
formattedText = formattedText.replace(/^\s{4}-\s*(.*?)$/gm, '<div class="message-item nested-2">$1</div>');
// Process bold terms
formattedText = formattedText.replace(/\*\*(.*?)\*\*/g, '<strong class="message-bold">$1</strong>');
formattedText = formattedText.replace(/\[bold\](.*?)\[\/bold\]/g, '<strong class="message-bold">$1</strong>');
return `<div class="message-content">${formattedText}</div>`;
}
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(`<div class="description-content">${textContent}</div>`);
}
}
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(`<div class="description-content">${remainingText}</div>`);
}
}
}
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 = '<div class="table-wrapper"><table class="resource-table">';
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 += '<tr>';
cells.forEach(cell => {
const isHeader = (rowIndex === 0 && !hasSeparatorRow) ||
(rowIndex === 0 && hasSeparatorRow);
const cellTag = isHeader ? 'th' : 'td';
htmlTable += `<${cellTag} class="align-left">${cell}</${cellTag}>`;
});
htmlTable += '</tr>';
});
htmlTable += '</table></div>';
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 = `
<div class="source-info">
<span class="document-name">${resources.file_names[index]}</span>
<span class="page-number">
<i class="bi bi-file-text"></i>
Page ${resources.page_numbers[index]}
</span>
</div>
<div class="content-wrapper">
<div class="bullet-indicator">
<div class="bullet-line"></div>
<div class="bullet-number">${index + 1}</div>
</div>
<div class="description">
${content}
</div>
</div>
`;
container.appendChild(item);
});
}
copyToClipboard(content) {
const cleanText = content.replace(/<div class="message-header">(.*?)<\/div>/g, '$1\n')
.replace(/<div class="message-item.*?">(.*?)<\/div>/g, '- $1')
.replace(/<div class="message-item nested-1">(.*?)<\/div>/g, ' - $1')
.replace(/<div class="message-item nested-2">(.*?)<\/div>/g, ' - $1')
.replace(/<strong class="message-bold">(.*?)<\/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 = `
<div class="sidebar d-flex flex-column flex-shrink-0 h-100">
<div class="top-header py-3 px-4">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="logo-container">
<img src="/static/favicon/apple-touch-icon.png" alt="Doclink" class="logo-image">
<h1 class="logo-text">Doclink</h1>
</div>
</div>
</div>
</div>
<div class="px-4 py-3">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-folder empty-folder"></i>
<span class="d-xl-block selected-domain-text">Unselected</span>
</div>
<div class="settings-icon" title="Select Domain">
<i class="bi bi-folder2-open"></i>
</div>
</div>
<div class="file-list-container">
<div id="sidebarFileList" class="sidebar-files">
</div>
</div>
<div class="file-add">
<button class="open-file-btn">
Add Sources
</button>
<p class="helper-text text-center" style="color: var(--primary-dark)">
Select a folder from 📁 to start chatting
</p>
</div>
</div>
<div class="bottom-section mt-auto">
<div class="text-center mb-3">
<span class="plan-badge d-xl-block">Free Plan</span>
</div>
<div class="user-section d-flex align-items-center gap-3 mb-3" role="button" id="userProfileMenu">
<div class="user-avatar">i</div>
<div class="user-info d-xl-block">
<div class="user-email">ibrahimyasing@gmail.com</div>
<div class="user-status">
<span class="status-dot"></span>
Online
</div>
</div>
<div class="user-menu">
<div class="menu-item">
<i class="bi bi-person-circle"></i>
Usage Limits
</div>
<div class="menu-divider"></div>
<div class="menu-item logout-item">
<i class="bi bi-box-arrow-right"></i>
Logout
</div>
</div>
</div>
<div class="bottom-links justify-content-center">
<a href="#" class="premium-link">Go Premium!</a>
<a href="#">Feedback</a>
</div>
</div>
<div id="sidebar-seperator"></div>
</div>
`;
}
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 = `
<div class="d-flex align-items-center w-100">
<div class="icon-container">
<i class="bi ${icon} file-icon sidebar-file-list-icon" style="color: var(--primary-dark)"></i>
<button class="delete-file-btn">
<i class="bi bi-trash"></i>
</button>
<div class="delete-confirm-actions">
<button class="confirm-delete-btn" title="Confirm delete">
<i class="bi bi-check"></i>
</button>
<button class="cancel-delete-btn" title="Cancel delete">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<span class="file-name" title="${fileName}">${truncatedName}</span>
<div class="checkbox-wrapper">
<input type="checkbox" class="file-checkbox" id="file-${fileID}" data-file-id="${fileID}">
<label class="checkbox-label" for="file-${fileID}"></label>
</div>
</div>
`;
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 = `
<div class="feedback-modal-content">
<div class="feedback-modal-header">
<h3>Send Feedback</h3>
<button class="close-modal">
<i class="fas fa-times"></i>
</button>
</div>
<div class="feedback-modal-description">
<p>Your feedback really helps us get better!</p>
<p>Please follow these steps:</p>
<ol>
<li>Select the type of your feedback</li>
<li>Add your description</li>
<li>If it helps explain better, attach a screenshot</li>
</ol>
</div>
<form id="feedback-form" enctype="multipart/form-data">
<div class="form-group">
<label for="feedback-type">Type</label>
<select id="feedback-type" name="feedback_type" class="form-control">
<option value="general">General Feedback</option>
<option value="bug">Bug Report</option>
<option value="feature">Feature Request</option>
</select>
</div>
<div class="form-group">
<label for="feedback-description">Description</label>
<textarea
id="feedback-description"
name="feedback_description"
class="form-control"
rows="4"
placeholder="Please describe your feedback or issue..."
required
></textarea>
</div>
<div class="form-group">
<label for="feedback-screenshot">Screenshot (Optional)</label>
<input
type="file"
id="feedback-screenshot"
name="feedback_screenshot"
class="form-control"
accept="image/*"
>
<small class="form-text">Max size: 2MB</small>
</div>
<div class="feedback-modal-footer">
<button type="button" class="btn-cancel">Cancel</button>
<button type="submit" class="btn-submit">Submit Feedback</button>
</div>
</form>
</div>
`;
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="domain-modal-wrapper">
<div class="modal-header border-0">
<h5 class="modal-title">How would you rate Doclink?</h5>
<button type="button" class="close-button" data-bs-dismiss="modal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="modal-body text-center">
<div class="stars-container">
<div class="stars">
<i class="bi bi-star" data-rating="1"></i>
<i class="bi bi-star" data-rating="2"></i>
<i class="bi bi-star" data-rating="3"></i>
<i class="bi bi-star" data-rating="4"></i>
<i class="bi bi-star" data-rating="5"></i>
</div>
</div>
<div class="feedback-container">
<textarea class="feedback-input" placeholder="Share your thoughts..."></textarea>
</div>
<div class="text-center">
<button class="submit-button">Submit</button>
</div>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="domain-modal-wrapper">
<div class="modal-header border-0 d-flex align-items-center">
<h6 class="mb-0">Add content from URL</h6>
<button type="button" class="close-button" data-bs-dismiss="modal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="upload-container">
<div class="url-input-container">
<input
type="url"
class="form-control url-input"
placeholder="Enter website URL..."
required
>
<small class="text-secondary mt-2">
Enter the URL of the webpage you want to add to your folder
</small>
</div>
<button class="add-url-btn mt-3" disabled>
Add Content
<div class="url-progress">
<div class="progress-bar"></div>
</div>
</button>
<div class="upload-loading-overlay" style="display: none">
<div class="loading-content">
<div class="spinner-border text-primary-green mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5 class="mb-2">Processing URL...</h5>
<p class="text-center mb-0">Please wait while we extract the content</p>
<p class="text-center text-secondary">This might take a moment</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
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 = `
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="domain-modal-wrapper">
<div class="modal-header border-0">
<h5 class="modal-title">Usage Limits</h5>
<button type="button" class="close-button" data-bs-dismiss="modal">
<i class="bi bi-x"></i>
</button>
</div>
<div class="limits-container">
<!-- Sources Limit -->
<div class="limit-indicator mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<small class="text-secondary d-block">Total Sources</small>
<small class="text-white-50">Source limit across all folders</small>
</div>
<small class="text-secondary sources-count">0/20</small>
</div>
<div class="progress" style="height: 6px; background: rgba(255, 255, 255, 0.1);">
<div class="progress-bar bg-primary-blue" style="width: 0%"></div>
</div>
</div>
<!-- Domains Limit -->
<div class="limit-indicator mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<small class="text-secondary d-block">Folders</small>
<small class="text-white-50">Number of folders you can create</small>
</div>
<small class="text-secondary domains-count">0/3</small>
</div>
<div class="progress" style="height: 6px; background: rgba(255, 255, 255, 0.1);">
<div class="progress-bar bg-primary-green" style="width: 0%"></div>
</div>
</div>
<!-- Daily Questions Limit -->
<div class="limit-indicator">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<small class="text-secondary d-block">Daily Questions</small>
<small class="text-white-50">Resets daily at midnight UTC</small>
</div>
<small class="text-secondary questions-count">0/10</small>
</div>
<div class="progress" style="height: 6px; background: rgba(255, 255, 255, 0.1);">
<div class="progress-bar bg-primary-green" style="width: 0%"></div>
</div>
</div>
<div class="upgrade-section mt-4 text-center">
<button class="upgrade-button">
<i class="bi bi-gem me-2"></i>
Upgrade to Premium
</button>
</div>
</div>
</div>
</div>
</div>
`;
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 = `<img src="${this.userData.user_info.user_picture_url}" alt="${this.userData.user_info.user_name}" class="user-avatar-img">`;
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 = '';
}
});
}
});