botui/ui/suite/chat/chat.js

787 lines
22 KiB
JavaScript
Raw Normal View History

/* Chat module JavaScript - including projector component */
// Projector State
let projectorState = {
isOpen: false,
contentType: null,
source: null,
options: {},
currentSlide: 1,
totalSlides: 1,
currentImage: 0,
totalImages: 1,
zoom: 100,
rotation: 0,
isPlaying: false,
isLooping: false,
isMuted: false,
lineNumbers: true,
wordWrap: false
};
// Get media element
function getMediaElement() {
return document.querySelector('.projector-video, .projector-audio');
}
// Open Projector
function openProjector(data) {
const overlay = document.getElementById('projector-overlay');
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
const title = document.getElementById('projector-title');
const icon = document.getElementById('projector-icon');
// Reset state
projectorState = {
...projectorState,
isOpen: true,
contentType: data.content_type,
source: data.source_url,
options: data.options || {}
};
// Set title
title.textContent = data.title || 'Content Viewer';
// Set icon based on content type
const icons = {
'Video': '🎬',
'Audio': '🎵',
'Image': '🖼️',
'Pdf': '📄',
'Presentation': '📊',
'Code': '💻',
'Spreadsheet': '📈',
'Markdown': '📝',
'Html': '🌐',
'Document': '📃'
};
icon.textContent = icons[data.content_type] || '📁';
// Show loading
loading.classList.remove('hidden');
hideAllControls();
// Show overlay
overlay.classList.remove('hidden');
// Load content based on type
loadContent(data);
}
// Load Content
function loadContent(data) {
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
setTimeout(() => {
loading.classList.add('hidden');
switch (data.content_type) {
case 'Video':
loadVideo(content, data);
break;
case 'Audio':
loadAudio(content, data);
break;
case 'Image':
loadImage(content, data);
break;
case 'Pdf':
loadPdf(content, data);
break;
case 'Presentation':
loadPresentation(content, data);
break;
case 'Code':
loadCode(content, data);
break;
case 'Markdown':
loadMarkdown(content, data);
break;
case 'Iframe':
case 'Html':
loadIframe(content, data);
break;
default:
loadGeneric(content, data);
}
}, 300);
}
// Load Video
function loadVideo(container, data) {
const loading = document.getElementById('projector-loading');
const video = document.createElement('video');
video.className = 'projector-video';
video.src = data.source_url;
video.controls = false;
video.autoplay = data.options?.autoplay || false;
video.loop = data.options?.loop_content || false;
video.muted = data.options?.muted || false;
video.addEventListener('loadedmetadata', () => {
loading.classList.add('hidden');
updateTimeDisplay();
});
video.addEventListener('timeupdate', () => {
updateProgress();
updateTimeDisplay();
});
video.addEventListener('play', () => {
projectorState.isPlaying = true;
document.getElementById('play-pause-btn').textContent = '⏸️';
});
video.addEventListener('pause', () => {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
});
video.addEventListener('ended', () => {
if (!projectorState.isLooping) {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
}
});
// Clear and add video
clearContent(container);
container.appendChild(video);
// Show media controls
showControls('media');
}
// Load Audio
function loadAudio(container, data) {
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '40px';
// Visualizer placeholder
const visualizer = document.createElement('canvas');
visualizer.className = 'audio-visualizer';
visualizer.id = 'audio-visualizer';
wrapper.appendChild(visualizer);
const audio = document.createElement('audio');
audio.className = 'projector-audio';
audio.src = data.source_url;
audio.autoplay = data.options?.autoplay || false;
audio.loop = data.options?.loop_content || false;
audio.addEventListener('loadedmetadata', () => updateTimeDisplay());
audio.addEventListener('timeupdate', () => {
updateProgress();
updateTimeDisplay();
});
audio.addEventListener('play', () => {
projectorState.isPlaying = true;
document.getElementById('play-pause-btn').textContent = '⏸️';
});
audio.addEventListener('pause', () => {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
});
wrapper.appendChild(audio);
clearContent(container);
container.appendChild(wrapper);
showControls('media');
}
// Load Image
function loadImage(container, data) {
const img = document.createElement('img');
img.className = 'projector-image';
img.src = data.source_url;
img.alt = data.title || 'Image';
img.id = 'projector-img';
img.addEventListener('load', () => {
document.getElementById('projector-loading').classList.add('hidden');
});
img.addEventListener('error', () => {
showError('Failed to load image');
});
clearContent(container);
container.appendChild(img);
// Hide nav if single image
document.getElementById('prev-image-btn').style.display =
projectorState.totalImages > 1 ? 'block' : 'none';
document.getElementById('next-image-btn').style.display =
projectorState.totalImages > 1 ? 'block' : 'none';
showControls('image');
updateImageInfo();
}
// Load PDF
function loadPdf(container, data) {
const iframe = document.createElement('iframe');
iframe.className = 'projector-pdf';
iframe.src = `/static/pdfjs/web/viewer.html?file=${encodeURIComponent(data.source_url)}`;
clearContent(container);
container.appendChild(iframe);
showControls('slide');
}
// Load Presentation
function loadPresentation(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-presentation';
const slideContainer = document.createElement('div');
slideContainer.className = 'slide-container';
slideContainer.id = 'slide-container';
// For now, show as images (each slide converted to image)
const slideImg = document.createElement('img');
slideImg.className = 'slide-content';
slideImg.id = 'slide-content';
slideImg.src = `${data.source_url}?slide=1`;
slideContainer.appendChild(slideImg);
wrapper.appendChild(slideContainer);
clearContent(container);
container.appendChild(wrapper);
showControls('slide');
updateSlideInfo();
}
// Load Code
function loadCode(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-code';
wrapper.id = 'code-container';
if (projectorState.lineNumbers) {
wrapper.classList.add('line-numbers');
}
const pre = document.createElement('pre');
const code = document.createElement('code');
// Fetch code content
fetch(data.source_url)
.then(res => res.text())
.then(text => {
// Split into lines for line numbers
const lines = text.split('\n').map(line =>
`<span class="line">${escapeHtml(line)}</span>`
).join('\n');
code.innerHTML = lines;
// Apply syntax highlighting if Prism is available
if (window.Prism) {
Prism.highlightElement(code);
}
})
.catch(() => {
code.textContent = 'Failed to load code';
});
pre.appendChild(code);
wrapper.appendChild(pre);
clearContent(container);
container.appendChild(wrapper);
// Update code info
const filename = data.source_url.split('/').pop();
document.getElementById('code-info').textContent = filename;
showControls('code');
}
// Load Markdown
function loadMarkdown(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-markdown';
fetch(data.source_url)
.then(res => res.text())
.then(text => {
// Simple markdown parsing (use marked.js in production)
wrapper.innerHTML = parseMarkdown(text);
})
.catch(() => {
wrapper.innerHTML = '<p>Failed to load markdown</p>';
});
clearContent(container);
container.appendChild(wrapper);
hideAllControls();
}
// Load Iframe
function loadIframe(container, data) {
const iframe = document.createElement('iframe');
iframe.className = 'projector-iframe';
iframe.src = data.source_url;
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.allowFullscreen = true;
clearContent(container);
container.appendChild(iframe);
hideAllControls();
}
// Load Generic
function loadGeneric(container, data) {
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '40px';
wrapper.style.color = '#888';
wrapper.innerHTML = `
<div style="font-size: 64px; margin-bottom: 20px;">📁</div>
<div style="font-size: 18px; margin-bottom: 10px;">Cannot preview this file type</div>
<a href="${data.source_url}" download style="color: #667eea; text-decoration: none;">
Download File
</a>
`;
clearContent(container);
container.appendChild(wrapper);
hideAllControls();
}
// Show Error
function showError(message) {
const content = document.getElementById('projector-content');
content.innerHTML = `
<div class="projector-error">
<span class="projector-error-icon"></span>
<span class="projector-error-message">${message}</span>
</div>
`;
}
// Clear Content
function clearContent(container) {
const loading = document.getElementById('projector-loading');
container.innerHTML = '';
container.appendChild(loading);
}
// Show/Hide Controls
function showControls(type) {
hideAllControls();
const controls = document.getElementById(`${type}-controls`);
if (controls) {
controls.classList.remove('hidden');
}
}
function hideAllControls() {
document.getElementById('media-controls')?.classList.add('hidden');
document.getElementById('slide-controls')?.classList.add('hidden');
document.getElementById('image-controls')?.classList.add('hidden');
document.getElementById('code-controls')?.classList.add('hidden');
}
// Close Projector
function closeProjector() {
const overlay = document.getElementById('projector-overlay');
overlay.classList.add('hidden');
projectorState.isOpen = false;
// Stop any playing media
const media = getMediaElement();
if (media) {
media.pause();
media.src = '';
}
// Clear content
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
content.innerHTML = '';
content.appendChild(loading);
}
function closeProjectorOnOverlay(event) {
if (event.target.id === 'projector-overlay') {
closeProjector();
}
}
// Media Controls
function togglePlayPause() {
const media = getMediaElement();
if (media) {
if (media.paused) {
media.play();
} else {
media.pause();
}
}
}
function mediaSeekBack() {
const media = getMediaElement();
if (media) {
media.currentTime = Math.max(0, media.currentTime - 10);
}
}
function mediaSeekForward() {
const media = getMediaElement();
if (media) {
media.currentTime = Math.min(media.duration, media.currentTime + 10);
}
}
function seekTo(percent) {
const media = getMediaElement();
if (media && media.duration) {
media.currentTime = (percent / 100) * media.duration;
}
}
function setVolume(value) {
const media = getMediaElement();
if (media) {
media.volume = value / 100;
projectorState.isMuted = value === 0;
document.getElementById('mute-btn').textContent = value === 0 ? '🔇' : '🔊';
}
}
function toggleMute() {
const media = getMediaElement();
if (media) {
media.muted = !media.muted;
projectorState.isMuted = media.muted;
document.getElementById('mute-btn').textContent = media.muted ? '🔇' : '🔊';
}
}
function toggleLoop() {
const media = getMediaElement();
if (media) {
media.loop = !media.loop;
projectorState.isLooping = media.loop;
document.getElementById('loop-btn').classList.toggle('active', media.loop);
}
}
function setPlaybackSpeed(speed) {
const media = getMediaElement();
if (media) {
media.playbackRate = parseFloat(speed);
}
}
function updateProgress() {
const media = getMediaElement();
if (media && media.duration) {
const progress = (media.currentTime / media.duration) * 100;
document.getElementById('progress-bar').value = progress;
}
}
function updateTimeDisplay() {
const media = getMediaElement();
if (media) {
const current = formatTime(media.currentTime);
const duration = formatTime(media.duration || 0);
document.getElementById('time-display').textContent = `${current} / ${duration}`;
}
}
function formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
// Slide/Page Controls
function prevSlide() {
if (projectorState.currentSlide > 1) {
projectorState.currentSlide--;
updateSlide();
}
}
function nextSlide() {
if (projectorState.currentSlide < projectorState.totalSlides) {
projectorState.currentSlide++;
updateSlide();
}
}
function goToSlide(num) {
const slide = parseInt(num);
if (slide >= 1 && slide <= projectorState.totalSlides) {
projectorState.currentSlide = slide;
updateSlide();
}
}
function updateSlide() {
const slideContent = document.getElementById('slide-content');
if (slideContent) {
slideContent.src = `${projectorState.source}?slide=${projectorState.currentSlide}`;
}
updateSlideInfo();
}
function updateSlideInfo() {
document.getElementById('slide-info').textContent =
`Slide ${projectorState.currentSlide} of ${projectorState.totalSlides}`;
document.getElementById('slide-input').value = projectorState.currentSlide;
}
// Image Controls
function prevImage() {
if (projectorState.currentImage > 0) {
projectorState.currentImage--;
updateImage();
}
}
function nextImage() {
if (projectorState.currentImage < projectorState.totalImages - 1) {
projectorState.currentImage++;
updateImage();
}
}
function updateImage() {
// Implementation for image galleries
updateImageInfo();
}
function updateImageInfo() {
document.getElementById('image-info').textContent =
`${projectorState.currentImage + 1} of ${projectorState.totalImages}`;
}
function rotateImage() {
projectorState.rotation = (projectorState.rotation + 90) % 360;
const img = document.getElementById('projector-img');
if (img) {
img.style.transform = `rotate(${projectorState.rotation}deg) scale(${projectorState.zoom / 100})`;
}
}
function fitToScreen() {
projectorState.zoom = 100;
projectorState.rotation = 0;
const img = document.getElementById('projector-img');
if (img) {
img.style.transform = 'none';
}
document.getElementById('zoom-level').textContent = '100%';
}
// Zoom Controls
function zoomIn() {
projectorState.zoom = Math.min(300, projectorState.zoom + 25);
applyZoom();
}
function zoomOut() {
projectorState.zoom = Math.max(25, projectorState.zoom - 25);
applyZoom();
}
function applyZoom() {
const img = document.getElementById('projector-img');
const slideContainer = document.getElementById('slide-container');
if (img) {
img.style.transform = `rotate(${projectorState.rotation}deg) scale(${projectorState.zoom / 100})`;
}
if (slideContainer) {
slideContainer.style.transform = `scale(${projectorState.zoom / 100})`;
}
document.getElementById('zoom-level').textContent = `${projectorState.zoom}%`;
}
// Code Controls
function toggleLineNumbers() {
projectorState.lineNumbers = !projectorState.lineNumbers;
const container = document.getElementById('code-container');
if (container) {
container.classList.toggle('line-numbers', projectorState.lineNumbers);
}
}
function toggleWordWrap() {
projectorState.wordWrap = !projectorState.wordWrap;
const container = document.getElementById('code-container');
if (container) {
container.style.whiteSpace = projectorState.wordWrap ? 'pre-wrap' : 'pre';
}
}
function setCodeTheme(theme) {
const container = document.getElementById('code-container');
if (container) {
container.className = `projector-code ${projectorState.lineNumbers ? 'line-numbers' : ''} theme-${theme}`;
}
}
function copyCode() {
const code = document.querySelector('.projector-code code');
if (code) {
navigator.clipboard.writeText(code.textContent).then(() => {
// Show feedback
const btn = document.querySelector('.code-controls .control-btn:last-child');
const originalText = btn.textContent;
btn.textContent = '✅';
setTimeout(() => btn.textContent = originalText, 2000);
});
}
}
// Fullscreen
function toggleFullscreen() {
const container = document.querySelector('.projector-container');
const icon = document.getElementById('fullscreen-icon');
if (!document.fullscreenElement) {
container.requestFullscreen().then(() => {
container.classList.add('fullscreen');
icon.textContent = '⛶';
}).catch(() => {});
} else {
document.exitFullscreen().then(() => {
container.classList.remove('fullscreen');
icon.textContent = '⛶';
}).catch(() => {});
}
}
// Download
function downloadContent() {
const link = document.createElement('a');
link.href = projectorState.source;
link.download = '';
link.click();
}
// Share
function shareContent() {
if (navigator.share) {
navigator.share({
title: document.getElementById('projector-title').textContent,
url: projectorState.source
}).catch(() => {});
} else {
navigator.clipboard.writeText(window.location.origin + projectorState.source).then(() => {
alert('Link copied to clipboard!');
});
}
}
// Keyboard shortcuts for projector
document.addEventListener('keydown', (e) => {
if (!projectorState.isOpen) return;
switch (e.key) {
case 'Escape':
closeProjector();
break;
case ' ':
e.preventDefault();
togglePlayPause();
break;
case 'ArrowLeft':
if (projectorState.contentType === 'Video' || projectorState.contentType === 'Audio') {
mediaSeekBack();
} else {
prevSlide();
}
break;
case 'ArrowRight':
if (projectorState.contentType === 'Video' || projectorState.contentType === 'Audio') {
mediaSeekForward();
} else {
nextSlide();
}
break;
case 'f':
toggleFullscreen();
break;
case 'm':
toggleMute();
break;
case '+':
case '=':
zoomIn();
break;
case '-':
zoomOut();
break;
}
});
// Helper Functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function parseMarkdown(text) {
// Simple markdown parsing - use marked.js for full support
return text
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/`([^`]+)`/gim, '<code>$1</code>')
.replace(/\n/gim, '<br>');
}
// Listen for play messages from WebSocket
if (window.htmx) {
htmx.on('htmx:wsMessage', function(event) {
try {
const data = JSON.parse(event.detail.message);
if (data.type === 'play') {
openProjector(data.data);
} else if (data.type === 'player_command') {
switch (data.command) {
case 'stop':
closeProjector();
break;
case 'pause':
const media = getMediaElement();
if (media) media.pause();
break;
case 'resume':
const mediaR = getMediaElement();
if (mediaR) mediaR.play();
break;
}
}
} catch (e) {
// Not a projector message
}
});
}