Spaces:
Build error
Build error
| // 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 = ` | |
| <h3>Document Viewer</h3> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="document-pin" title="Pin document">π Pin</button> | |
| <button class="document-close-btn">Close</button> | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="document-empty-state"> | |
| <h4>No Document Selected</h4> | |
| <p>Click on any document, image, or file in the chat to view it here</p> | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="document-empty-state"> | |
| <h4>No Document Selected</h4> | |
| <p>Click on any document, image, or file in the chat to view it here</p> | |
| </div> | |
| `; | |
| // 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 = ` | |
| <div class="document-content"> | |
| <img src="${doc.content}" alt="${doc.title}" class="document-image"> | |
| </div> | |
| `; | |
| break; | |
| case 'pdf': | |
| contentArea.innerHTML = ` | |
| <iframe src="${doc.content}" class="pdf-viewer"></iframe> | |
| `; | |
| break; | |
| case 'code': | |
| contentArea.innerHTML = ` | |
| <div class="document-code"> | |
| <pre><code>${escapeHtml(doc.content)}</code></pre> | |
| </div> | |
| `; | |
| break; | |
| case 'html': | |
| contentArea.innerHTML = ` | |
| <div class="document-content"> | |
| ${doc.content} | |
| </div> | |
| `; | |
| break; | |
| default: | |
| contentArea.innerHTML = ` | |
| <div class="document-content"> | |
| ${escapeHtml(doc.content)} | |
| </div> | |
| `; | |
| } | |
| } | |
| // 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); | |
| } | |
| }; | |
| })(); |