Skip to main content
Glama
dashboard.js20 kB
class PuppetProductionDashboard { constructor() { this.selectedTraits = new Set(); this.uploadedImage = null; this.initializeEventListeners(); this.updateProductionButton(); } initializeEventListeners() { // Image upload const uploadArea = document.getElementById('upload-area'); const imageInput = document.getElementById('image-input'); const removeImageBtn = document.getElementById('remove-image'); uploadArea.addEventListener('click', () => imageInput.click()); uploadArea.addEventListener('dragover', this.handleDragOver.bind(this)); uploadArea.addEventListener('drop', this.handleDrop.bind(this)); imageInput.addEventListener('change', this.handleImageSelect.bind(this)); removeImageBtn.addEventListener('click', this.removeImage.bind(this)); // Character form document.getElementById('character-name').addEventListener('input', this.updateProductionButton.bind(this)); document.getElementById('character-name').addEventListener('input', this.validateCharacterName.bind(this)); // Trait selection document.querySelectorAll('.trait-pill').forEach(pill => { pill.addEventListener('click', () => this.toggleTrait(pill.dataset.trait)); }); document.getElementById('custom-trait').addEventListener('keypress', (e) => { if (e.key === 'Enter' && e.target.value.trim()) { this.addCustomTrait(e.target.value.trim()); e.target.value = ''; } }); // Backstory enhancement document.getElementById('enhance-backstory').addEventListener('click', this.enhanceBackstory.bind(this)); // Voice system document.getElementById('voice-selection').addEventListener('change', this.updateVoiceInfo.bind(this)); document.getElementById('sample-voice').addEventListener('click', this.sampleVoice.bind(this)); // Production document.getElementById('start-production').addEventListener('click', this.startProduction.bind(this)); // Initialize voice system and character validation this.initializeVoices(); } // Image Upload Handlers handleDragOver(e) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.add('dragover'); } handleDrop(e) { e.preventDefault(); e.stopPropagation(); e.currentTarget.classList.remove('dragover'); const files = e.dataTransfer.files; if (files.length > 0) { this.processImageFile(files[0]); } } handleImageSelect(e) { const file = e.target.files[0]; if (file) { this.processImageFile(file); } } processImageFile(file) { if (!file.type.startsWith('image/')) { alert('Please upload a valid image file.'); return; } if (file.size > 10 * 1024 * 1024) { alert('Image size must be less than 10MB.'); return; } const reader = new FileReader(); reader.onload = (e) => { this.uploadedImage = { file: file, dataUrl: e.target.result }; this.showImagePreview(e.target.result); this.updateProductionButton(); }; reader.readAsDataURL(file); } showImagePreview(dataUrl) { const preview = document.getElementById('image-preview'); const img = document.getElementById('preview-img'); const uploadArea = document.getElementById('upload-area'); img.src = dataUrl; preview.classList.remove('hidden'); uploadArea.style.display = 'none'; } removeImage() { this.uploadedImage = null; const preview = document.getElementById('image-preview'); const uploadArea = document.getElementById('upload-area'); preview.classList.add('hidden'); uploadArea.style.display = 'block'; this.updateProductionButton(); } // Trait Management toggleTrait(trait) { const pill = document.querySelector(`[data-trait="${trait}"]`); if (this.selectedTraits.has(trait)) { this.selectedTraits.delete(trait); pill.classList.remove('selected'); } else { this.selectedTraits.add(trait); pill.classList.add('selected'); } this.updateSelectedTraits(); } addCustomTrait(trait) { if (!this.selectedTraits.has(trait)) { this.selectedTraits.add(trait); this.updateSelectedTraits(); } } updateSelectedTraits() { const container = document.getElementById('selected-traits'); container.innerHTML = ''; this.selectedTraits.forEach(trait => { const span = document.createElement('span'); span.className = 'selected-trait'; span.innerHTML = ` ${trait} <button class="remove-trait" onclick="dashboard.removeTrait('${trait}')">×</button> `; container.appendChild(span); }); } removeTrait(trait) { this.selectedTraits.delete(trait); const pill = document.querySelector(`[data-trait="${trait}"]`); if (pill) { pill.classList.remove('selected'); } this.updateSelectedTraits(); } // Backstory Enhancement async enhanceBackstory() { const basicBackstory = document.getElementById('basic-backstory').value; const characterName = document.getElementById('character-name').value; const selectedTraits = Array.from(this.selectedTraits); if (!basicBackstory.trim()) { alert('Please write a basic backstory first.'); return; } const enhanceBtn = document.getElementById('enhance-backstory'); const originalText = enhanceBtn.textContent; enhanceBtn.textContent = '✨ Enhancing...'; enhanceBtn.disabled = true; try { const response = await fetch('/api/enhance-backstory', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ characterName: characterName || 'Character', basicBackstory, personalityTraits: selectedTraits }) }); const data = await response.json(); if (data.success) { document.getElementById('enhanced-backstory').value = data.enhancedBackstory; } else { alert('Failed to enhance backstory: ' + data.error); } } catch (error) { alert('Error enhancing backstory: ' + error.message); } finally { enhanceBtn.textContent = originalText; enhanceBtn.disabled = false; } } // Production Control updateProductionButton() { const startBtn = document.getElementById('start-production'); const hasImage = this.uploadedImage !== null; const hasName = document.getElementById('character-name').value.trim() !== ''; startBtn.disabled = !hasImage || !hasName; } async startProduction() { const resultsSection = document.getElementById('results-section'); const productionLog = document.getElementById('production-log'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); // Show results section resultsSection.style.display = 'block'; resultsSection.scrollIntoView({ behavior: 'smooth' }); // Reset progress progressFill.style.width = '0%'; progressText.textContent = 'Starting production...'; productionLog.innerHTML = ''; // Collect form data const formData = new FormData(); formData.append('image', this.uploadedImage.file); formData.append('characterName', document.getElementById('character-name').value); formData.append('characterType', document.getElementById('character-type').value); formData.append('personalityTraits', JSON.stringify(Array.from(this.selectedTraits))); formData.append('basicBackstory', document.getElementById('basic-backstory').value); formData.append('enhancedBackstory', document.getElementById('enhanced-backstory').value); formData.append('voiceDescription', document.getElementById('voice-description').value); formData.append('voiceId', document.getElementById('voice-selection').value); formData.append('countryOrigin', document.getElementById('country-origin').value); formData.append('accent', document.getElementById('accent').value); formData.append('generateImages', document.getElementById('generate-images').checked); formData.append('storeDatabase', document.getElementById('store-database').checked); formData.append('prepareVoice', document.getElementById('prepare-voice').checked); try { this.logMessage('🚀 Starting character production pipeline...', 'info'); this.updateProgress(10, 'Uploading image and analyzing character...'); const response = await fetch('/api/create-character', { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Handle streaming response const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.trim().startsWith('data: ')) { try { const data = JSON.parse(line.slice(6)); this.handleProgressUpdate(data); } catch (e) { // Ignore parsing errors for incomplete chunks } } } } } catch (error) { this.logMessage(`❌ Production failed: ${error.message}`, 'error'); this.updateProgress(0, 'Production failed'); } } handleProgressUpdate(data) { const { type, message, progress, results } = data; switch (type) { case 'progress': this.updateProgress(progress, message); break; case 'log': this.logMessage(message, data.level || 'info'); break; case 'complete': this.updateProgress(100, 'Production complete!'); this.displayResults(results); break; case 'error': this.logMessage(`❌ ${message}`, 'error'); break; } } updateProgress(percentage, message) { const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); progressFill.style.width = `${percentage}%`; progressText.textContent = message; } logMessage(message, level = 'info') { const productionLog = document.getElementById('production-log'); const entry = document.createElement('div'); entry.className = `log-entry ${level}`; entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; productionLog.appendChild(entry); productionLog.scrollTop = productionLog.scrollHeight; } displayResults(results) { const resultsContainer = document.getElementById('character-results'); if (results.success) { resultsContainer.innerHTML = ` <div class="result-item"> <h4>✅ Character Created Successfully</h4> <p><strong>Name:</strong> ${results.character_name}</p> <p><strong>Affogato Character ID:</strong> ${results.character?.affogatoId || 'N/A'}</p> <p><strong>Notion Database ID:</strong> ${results.character?.notionId || 'N/A'}</p> <p><strong>Voice Profile:</strong> ${results.voice_profile}</p> </div> <div class="result-item"> <h4>🎨 Character Images</h4> <div class="character-images" id="character-images"> ${this.renderCharacterImages(results.character?.assets || [])} </div> </div> <div class="result-item"> <h4>🎭 Puppet Specifications</h4> <pre style="background: #f7fafc; padding: 12px; border-radius: 4px; overflow-x: auto;"> ${JSON.stringify(results.puppet_traits, null, 2)} </pre> </div> `; } else { resultsContainer.innerHTML = ` <div class="result-item"> <h4>❌ Production Failed</h4> <p>Error: ${results.error}</p> </div> `; } } renderCharacterImages(assets) { if (!assets || assets.length === 0) { return '<p>No character images generated.</p>'; } return assets.map(asset => ` <div class="character-image"> <img src="${asset.url || asset.filepath}" alt="${asset.emotion || asset.view} ${asset.type}" /> <p>${asset.type === 'emotion' ? `${asset.emotion} emotion` : `${asset.view} view`}</p> </div> `).join(''); } } // Voice System Functions PuppetProductionDashboard.prototype.initializeVoices = async function() { console.log('🎤 Loading voices...'); const voiceSelect = document.getElementById('voice-selection'); try { const response = await fetch('/api/voices'); const data = await response.json(); if (data.success && data.voices.length > 0) { voiceSelect.innerHTML = '<option value="">Select a voice...</option>'; data.voices.forEach(voice => { const option = document.createElement('option'); option.value = voice.voice_id; option.textContent = `${voice.name} (${voice.accent}, ${voice.gender})`; option.dataset.accent = voice.accent; option.dataset.country = voice.country; option.dataset.gender = voice.gender; option.dataset.age = voice.age; voiceSelect.appendChild(option); }); console.log(`✅ Loaded ${data.voices.length} voices`); } else { voiceSelect.innerHTML = '<option value="">No voices available</option>'; console.log('⚠️ No voices available'); } } catch (error) { console.error('❌ Failed to load voices:', error); voiceSelect.innerHTML = '<option value="">Voice loading failed</option>'; } }; PuppetProductionDashboard.prototype.updateVoiceInfo = function() { const voiceSelect = document.getElementById('voice-selection'); const voiceInfo = document.getElementById('voice-info'); const sampleBtn = document.getElementById('sample-voice'); const countrySelect = document.getElementById('country-origin'); const accentInput = document.getElementById('accent'); if (voiceSelect.value) { const selectedOption = voiceSelect.options[voiceSelect.selectedIndex]; const info = `${selectedOption.dataset.accent} accent, ${selectedOption.dataset.gender}, ${selectedOption.dataset.age}`; voiceInfo.innerHTML = `<small>🎵 ${info}</small>`; sampleBtn.disabled = false; // Auto-fill country and accent if (selectedOption.dataset.country) { countrySelect.value = selectedOption.dataset.country; } if (selectedOption.dataset.accent) { accentInput.value = selectedOption.dataset.accent; } } else { voiceInfo.innerHTML = ''; sampleBtn.disabled = true; } }; PuppetProductionDashboard.prototype.sampleVoice = async function() { const voiceSelect = document.getElementById('voice-selection'); const sampleBtn = document.getElementById('sample-voice'); const voiceAudio = document.getElementById('voice-audio'); if (!voiceSelect.value) return; sampleBtn.disabled = true; sampleBtn.textContent = '🔄 Generating...'; try { const response = await fetch('/api/voice-sample', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ voiceId: voiceSelect.value, text: "Hello! This is how I would sound as your character. I'm excited to be part of your puppet show!" }) }); const data = await response.json(); if (data.success) { const audioBlob = new Blob( [Uint8Array.from(atob(data.audio_base64), c => c.charCodeAt(0))], { type: data.content_type } ); const audioUrl = URL.createObjectURL(audioBlob); voiceAudio.src = audioUrl; voiceAudio.style.display = 'block'; voiceAudio.play(); console.log('✅ Voice sample generated successfully'); } else { throw new Error(data.error || 'Voice sampling failed'); } } catch (error) { console.error('❌ Voice sampling failed:', error); alert('Voice sampling failed. Please try again or select a different voice.'); } finally { sampleBtn.disabled = false; sampleBtn.textContent = '🎵 Sample Voice'; } }; // Character Validation Functions PuppetProductionDashboard.prototype.validateCharacterName = async function() { const nameInput = document.getElementById('character-name'); const validation = document.getElementById('name-validation'); const characterName = nameInput.value.trim(); if (!characterName) { validation.innerHTML = ''; return; } if (characterName.length < 2) { validation.innerHTML = '<small class="error">⚠️ Name must be at least 2 characters</small>'; return; } validation.innerHTML = '<small class="checking">🔄 Checking availability...</small>'; try { const response = await fetch('/api/validate-character', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ characterName }) }); const data = await response.json(); if (data.success) { if (data.exists) { validation.innerHTML = '<small class="error">❌ Character name already exists</small>'; } else { validation.innerHTML = '<small class="success">✅ Character name is available</small>'; } } else { validation.innerHTML = '<small class="warning">⚠️ Validation unavailable</small>'; } } catch (error) { console.error('❌ Character validation failed:', error); validation.innerHTML = '<small class="warning">⚠️ Validation unavailable</small>'; } }; // Initialize dashboard const dashboard = new PuppetProductionDashboard();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bermingham85/mcp-puppet-pipeline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server