// Split Screen Document Viewer for Chainlit (function() { 'use strict'; let documentViewer = null; let documents = []; let activeDocIndex = 0; let viewerActive = false; // Create the document viewer panel (hidden by default) function createDocumentViewer() { if (document.getElementById('document-viewer')) { return document.getElementById('document-viewer'); } const viewer = document.createElement('div'); viewer.id = 'document-viewer'; viewer.className = ''; // Start without 'active' class // Create header const header = document.createElement('div'); header.className = 'document-header'; header.innerHTML = `

Document Viewer

`; viewer.appendChild(header); // Create tabs container const tabsContainer = document.createElement('div'); tabsContainer.className = 'document-tabs'; viewer.appendChild(tabsContainer); // Create content area const contentArea = document.createElement('div'); contentArea.id = 'document-content'; contentArea.innerHTML = `

No Document Selected

Click on any document, image, or file in the chat to view it here

`; viewer.appendChild(contentArea); // Add close functionality header.querySelector('.document-close-btn').addEventListener('click', () => { hideDocumentViewer(); }); // Add pin functionality const pinBtn = header.querySelector('.document-pin'); pinBtn.addEventListener('click', () => { pinBtn.classList.toggle('pinned'); const isPinned = pinBtn.classList.contains('pinned'); pinBtn.textContent = isPinned ? '📍 Pinned' : '📌 Pin'; }); return viewer; } // Show document viewer function showDocumentViewer() { if (!documentViewer) { documentViewer = createDocumentViewer(); document.body.appendChild(documentViewer); } // Add active classes documentViewer.classList.add('active'); document.body.classList.add('document-viewer-active'); // Adjust main container const mainContainer = document.querySelector('#root > div'); if (mainContainer) { mainContainer.classList.add('split-active'); } viewerActive = true; console.log('Document viewer shown'); } // Hide document viewer function hideDocumentViewer() { if (documentViewer) { documentViewer.classList.remove('active'); document.body.classList.remove('document-viewer-active'); const mainContainer = document.querySelector('#root > div'); if (mainContainer) { mainContainer.classList.remove('split-active'); } } viewerActive = false; console.log('Document viewer hidden'); } // Display document in viewer function displayDocument(content, title = 'Document', type = 'text') { console.log('Displaying document:', title, type); // Show viewer if not visible if (!viewerActive) { showDocumentViewer(); } const contentArea = document.getElementById('document-content'); const tabsContainer = documentViewer.querySelector('.document-tabs'); // Check if document already exists const existingIndex = documents.findIndex(doc => doc.title === title); if (existingIndex !== -1) { // Switch to existing tab activeDocIndex = existingIndex; updateTabs(); displayContent(documents[existingIndex]); return; } // Add to documents array const docIndex = documents.length; documents.push({ content, title, type }); // Create tab const tab = document.createElement('button'); tab.className = 'document-tab'; tab.textContent = title.length > 20 ? title.substring(0, 20) + '...' : title; tab.title = title; tab.dataset.docIndex = docIndex; tab.addEventListener('click', () => { activeDocIndex = parseInt(tab.dataset.docIndex); updateTabs(); displayContent(documents[activeDocIndex]); }); // Add close button to tab const closeTab = document.createElement('span'); closeTab.innerHTML = ' ×'; closeTab.style.cssText = 'margin-left: 8px; font-weight: bold; color: #888;'; closeTab.onclick = (e) => { e.stopPropagation(); removeDocument(docIndex); }; tab.appendChild(closeTab); tabsContainer.appendChild(tab); // Display content activeDocIndex = docIndex; updateTabs(); displayContent({ content, title, type }); } // Update active tab styling function updateTabs() { const tabs = documentViewer.querySelectorAll('.document-tab'); tabs.forEach((tab, index) => { if (parseInt(tab.dataset.docIndex) === activeDocIndex) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); } // Remove document and tab function removeDocument(index) { documents.splice(index, 1); // Rebuild tabs const tabsContainer = documentViewer.querySelector('.document-tabs'); tabsContainer.innerHTML = ''; documents.forEach((doc, i) => { const tab = document.createElement('button'); tab.className = 'document-tab'; tab.textContent = doc.title.length > 20 ? doc.title.substring(0, 20) + '...' : doc.title; tab.title = doc.title; tab.dataset.docIndex = i; if (i === activeDocIndex) { tab.classList.add('active'); } tab.addEventListener('click', () => { activeDocIndex = i; updateTabs(); displayContent(documents[i]); }); const closeTab = document.createElement('span'); closeTab.innerHTML = ' ×'; closeTab.style.cssText = 'margin-left: 8px; font-weight: bold; color: #888;'; closeTab.onclick = (e) => { e.stopPropagation(); removeDocument(i); }; tab.appendChild(closeTab); tabsContainer.appendChild(tab); }); // If no documents left, show empty state if (documents.length === 0) { const contentArea = document.getElementById('document-content'); contentArea.innerHTML = `

No Document Selected

Click on any document, image, or file in the chat to view it here

`; // Hide viewer if not pinned const pinBtn = documentViewer.querySelector('.document-pin'); if (!pinBtn.classList.contains('pinned')) { hideDocumentViewer(); } } else { // Adjust active index if needed if (activeDocIndex >= documents.length) { activeDocIndex = documents.length - 1; } displayContent(documents[activeDocIndex]); } } // Display content based on type function displayContent(doc) { const contentArea = document.getElementById('document-content'); switch (doc.type) { case 'image': contentArea.innerHTML = `
${doc.title}
`; break; case 'pdf': contentArea.innerHTML = ` `; break; case 'code': contentArea.innerHTML = `
${escapeHtml(doc.content)}
`; break; case 'html': contentArea.innerHTML = `
${doc.content}
`; break; default: contentArea.innerHTML = `
${escapeHtml(doc.content)}
`; } } // Escape HTML for safe display function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Add "View in Document Viewer" buttons to file elements function addViewerButtons() { console.log('Scanning for file elements to add viewer buttons...'); // Enhanced file element selectors const fileSelectors = [ '[data-testid*="element"]', '[data-testid*="file"]', '[data-testid*="pdf"]', '.cl-file', '.cl-pdf', '[class*="file"]', '[class*="pdf"]', 'a[href$=".pdf"]', 'a[download]', 'div[class*="MuiPaper-root"]:has(a[download])', '.message-content a', '[title*=".pdf"]', '[title*="PDF"]' ]; let foundElements = 0; fileSelectors.forEach(selector => { try { const elements = document.querySelectorAll(selector); elements.forEach(element => { if (element.querySelector('.viewer-button') || element.closest('#document-viewer')) { return; // Already has button or is in viewer } // Skip if element is too small or hidden const rect = element.getBoundingClientRect(); if (rect.width < 10 || rect.height < 10) { return; } // Check if element is actually visible const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return; } foundElements++; console.log(`Adding viewer button to element:`, element, `(selector: ${selector})`); // Create viewer button const viewerBtn = document.createElement('button'); viewerBtn.className = 'viewer-button'; viewerBtn.innerHTML = '👁️ View'; viewerBtn.title = 'Open in Document Viewer'; // Force button visibility with inline styles viewerBtn.style.cssText = ` position: absolute !important; top: 5px !important; right: 5px !important; padding: 6px 12px !important; background: #ff4757 !important; color: white !important; border: none !important; border-radius: 6px !important; font-size: 12px !important; font-weight: 500 !important; cursor: pointer !important; z-index: 9999 !important; display: block !important; visibility: visible !important; opacity: 1 !important; min-width: 60px !important; text-align: center !important; box-shadow: 0 2px 8px rgba(255, 71, 87, 0.3) !important; `; // Position the parent element if needed const computedStyle = window.getComputedStyle(element); if (computedStyle.position === 'static') { element.style.position = 'relative'; } viewerBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); console.log('Viewer button clicked for element:', element); // Find download link or href let link = element.href || element.getAttribute('href') || element.querySelector('a[href]')?.href || element.querySelector('[href]')?.getAttribute('href'); let fileName = element.textContent?.trim() || element.getAttribute('title') || element.querySelector('[title]')?.title || element.querySelector('a')?.textContent?.trim() || `Document ${documents.length + 1}`; console.log('Found link:', link, 'fileName:', fileName); if (link) { if (link.endsWith('.pdf') || link.includes('pdf') || fileName.toLowerCase().includes('pdf')) { displayDocument(link, fileName, 'pdf'); } else if (link.match(/\.(jpg|jpeg|png|gif|webp)$/i)) { displayDocument(link, fileName, 'image'); } else { // Try to fetch content fetch(link) .then(res => res.text()) .then(content => { const type = link.endsWith('.html') ? 'html' : 'text'; displayDocument(content, fileName, type); }) .catch(err => { console.error('Error loading document:', err); displayDocument('Error loading document content', fileName, 'text'); }); } } else { // If no link, display the element's content const content = element.textContent || element.innerHTML; displayDocument(content, fileName, 'html'); } }); element.appendChild(viewerBtn); }); } catch (error) { console.warn(`Error processing selector ${selector}:`, error); } }); console.log(`Added viewer buttons to ${foundElements} elements`); } // Attach click handlers to documents function attachDocumentHandlers() { document.addEventListener('click', (e) => { const target = e.target; // Don't process clicks on viewer buttons if (target.classList.contains('viewer-button')) { return; } console.log('Click detected on:', target.tagName, target.className, target); // Enhanced detection for Chainlit file elements const fileSelectors = [ '[data-testid*="element"]', '[data-testid*="file"]', '[data-testid*="pdf"]', '.cl-file', '.cl-pdf', '[class*="file"]', '[class*="pdf"]', 'a[href$=".pdf"]', 'a[download]' ]; let fileElement = null; for (const selector of fileSelectors) { fileElement = target.closest(selector); if (fileElement && !fileElement.closest('#document-viewer')) { break; } } if (fileElement) { console.log('File element detected:', fileElement); e.preventDefault(); e.stopPropagation(); // Try to find the download link within the element const downloadLink = fileElement.href || fileElement.querySelector('a[href]')?.href || fileElement.querySelector('[href]')?.getAttribute('href'); if (downloadLink) { const fileName = fileElement.textContent || fileElement.querySelector('[title]')?.title || fileElement.getAttribute('title') || 'Document ' + (documents.length + 1); console.log('Opening document:', fileName, downloadLink); if (downloadLink.endsWith('.pdf')) { displayDocument(downloadLink, fileName, 'pdf'); } else { // Try to fetch and display content fetch(downloadLink) .then(res => res.text()) .then(content => { const type = downloadLink.endsWith('.html') ? 'html' : 'text'; displayDocument(content, fileName, type); }) .catch(err => { console.error('Error loading document:', err); displayDocument('Error loading document content', fileName, 'text'); }); } } return; } // Check for images if (target.tagName === 'IMG' && target.src && !target.closest('#document-viewer')) { e.preventDefault(); e.stopPropagation(); const title = target.alt || 'Image ' + (documents.length + 1); displayDocument(target.src, title, 'image'); return; } // Check for code blocks const codeBlock = target.closest('pre'); if (codeBlock && !codeBlock.closest('#document-viewer')) { e.preventDefault(); e.stopPropagation(); const content = codeBlock.textContent; const title = 'Code Block ' + (documents.length + 1); displayDocument(content, title, 'code'); return; } }, true); } // Periodically scan for new file elements and add viewer buttons function scanForNewElements() { addViewerButtons(); } // Initialize when DOM is ready function initialize() { console.log('Initializing Chainlit Split Screen Document Viewer'); // Add a global helper function to manually open documents window.openInDocumentViewer = function(url, title, type = 'pdf') { console.log('Manual document viewer open called:', url, title, type); displayDocument(url, title, type); }; // Create viewer but keep it hidden documentViewer = createDocumentViewer(); document.body.appendChild(documentViewer); // Attach handlers attachDocumentHandlers(); // Add initial viewer buttons addViewerButtons(); // Scan for new elements periodically (more frequently for better responsiveness) setInterval(scanForNewElements, 1000); // Also scan when new messages are added const observer = new MutationObserver((mutations) => { let shouldScan = false; mutations.forEach((mutation) => { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if new element contains file-like content if (node.querySelector && ( node.querySelector('a[download]') || node.querySelector('[class*="file"]') || node.querySelector('[data-testid*="file"]') || node.textContent?.toLowerCase().includes('pdf') )) { shouldScan = true; } } }); } }); if (shouldScan) { console.log('New content detected, scanning for file elements...'); setTimeout(addViewerButtons, 500); // Small delay to let DOM settle } }); observer.observe(document.body, { childList: true, subtree: true }); // Add mobile toggle button if needed if (window.innerWidth <= 768) { const toggleBtn = document.createElement('button'); toggleBtn.className = 'mobile-doc-toggle'; toggleBtn.innerHTML = '📄'; toggleBtn.style.display = viewerActive ? 'flex' : 'none'; toggleBtn.addEventListener('click', () => { if (viewerActive) { hideDocumentViewer(); toggleBtn.style.display = 'none'; } else { showDocumentViewer(); } }); document.body.appendChild(toggleBtn); } console.log('Document Viewer initialized (hidden by default)'); } // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { // Delay initialization to ensure Chainlit is fully loaded setTimeout(initialize, 1000); // Additional initialization after a longer delay setTimeout(() => { console.log('Additional document viewer initialization...'); addViewerButtons(); // Force scan every few seconds in case elements are missed setInterval(() => { console.log('Force scanning for new elements...'); addViewerButtons(); }, 3000); }, 3000); } // Export functions for external use window.ChainlitDocumentViewer = { displayDocument, showDocumentViewer, hideDocumentViewer, addViewerButtons, openInDocumentViewer: function(url, title, type = 'pdf') { displayDocument(url, title, type); } }; })();