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();