Navada25's picture
Update public/split_screen.js for document viewer
bd43db3 verified
// 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);
}
};
})();