diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php index 910f5ed..dfac4bb 100644 --- a/admin/class-twp-admin.php +++ b/admin/class-twp-admin.php @@ -390,7 +390,8 @@ class TWP_Admin { -
Default voice for text-to-speech. Click "Load Voices" after entering your API key.
+ +Default voice for text-to-speech. Click "Load Voices" after entering your API key, or "Refresh" to get updated voices.
Debug: Current saved voice ID = ""
@@ -1066,6 +1067,70 @@ class TWP_Admin { xhr.send('action=twp_get_elevenlabs_voices&nonce=' + ''); } + function refreshElevenLabsVoices() { + var select = document.getElementById('elevenlabs-voice-select'); + var button = event.target; + var currentValue = select.getAttribute('data-current') || select.value; + + console.log('Refreshing voices, current value:', currentValue); + + button.textContent = 'Refreshing...'; + button.disabled = true; + + var xhr = new XMLHttpRequest(); + xhr.open('POST', ajaxurl); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + xhr.onload = function() { + button.textContent = '🔄 Refresh'; + button.disabled = false; + + try { + var response = JSON.parse(xhr.responseText); + + if (response.success) { + var options = ''; + + if (Array.isArray(response.data)) { + response.data.forEach(function(voice) { + var selected = voice.voice_id === currentValue ? ' selected' : ''; + var category = voice.category === 'cloned' ? ' (Cloned)' : (voice.category === 'premade' ? ' (Premade)' : ''); + options += ''; + }); + } + + select.innerHTML = options; + select.setAttribute('data-current', currentValue); + + // Re-add preview buttons + addVoicePreviewButtons(select, response.data); + + // Show success message + var statusMsg = document.createElement('div'); + statusMsg.style.color = 'green'; + statusMsg.style.fontSize = '12px'; + statusMsg.style.marginTop = '5px'; + statusMsg.textContent = 'Voices refreshed successfully! Found ' + response.data.length + ' voices.'; + button.parentNode.appendChild(statusMsg); + + setTimeout(function() { + if (statusMsg.parentNode) { + statusMsg.parentNode.removeChild(statusMsg); + } + }, 3000); + + } else { + alert('Error refreshing voices: ' + (response.data || 'Unknown error')); + } + } catch (e) { + console.error('Refresh voices error:', e); + alert('Failed to refresh voices. Please try again.'); + } + }; + + xhr.send('action=twp_refresh_elevenlabs_voices&nonce=' + ''); + } + function addVoicePreviewButtons(select, voices) { // Remove existing preview container var existingPreview = document.getElementById('voice-preview-container'); @@ -4786,6 +4851,36 @@ class TWP_Admin { } } + /** + * AJAX handler for refreshing ElevenLabs voices (clears cache) + */ + public function ajax_refresh_elevenlabs_voices() { + check_ajax_referer('twp_ajax_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + // Clear the cached voices + delete_transient('twp_elevenlabs_voices'); + + // Now fetch fresh voices + $elevenlabs = new TWP_ElevenLabs_API(); + $result = $elevenlabs->get_voices(); // This will fetch from API and re-cache + + if ($result['success']) { + wp_send_json_success($result['data']['voices']); + } else { + $error_message = 'Failed to refresh voices'; + if (is_string($result['error'])) { + $error_message = $result['error']; + } elseif (is_array($result['error']) && isset($result['error']['detail'])) { + $error_message = $result['error']['detail']; + } + wp_send_json_error($error_message); + } + } + /** * AJAX handler for getting ElevenLabs models */ diff --git a/assets/js/admin.js b/assets/js/admin.js index 41fea52..f857e6d 100644 --- a/assets/js/admin.js +++ b/assets/js/admin.js @@ -413,6 +413,7 @@ jQuery(document).ready(function($) { html += ''; html += ''; html += ''; + html += ''; html += ''; // Audio File Options @@ -454,6 +455,7 @@ jQuery(document).ready(function($) { html += ''; html += ''; html += ''; + html += ''; html += ''; // Audio File Options @@ -520,6 +522,7 @@ jQuery(document).ready(function($) { html += ''; html += ''; html += ''; + html += ''; html += ''; // Audio File Options @@ -559,6 +562,7 @@ jQuery(document).ready(function($) { html += ''; html += ''; html += ''; + html += ''; html += ''; // Audio File Options @@ -1774,6 +1778,97 @@ jQuery(document).ready(function($) { }); }; + // Refresh voices function for workflow + window.refreshWorkflowVoices = function(buttonOrSelect) { + var $button, $select; + + // Check if we were passed a button or a select element + if ($(buttonOrSelect).is('button')) { + $button = $(buttonOrSelect); + $select = $button.prev('select.voice-select'); + } else if ($(buttonOrSelect).is('select')) { + $select = $(buttonOrSelect); + $button = $select.next('button'); + } else { + // Fallback - assume it's a button + $button = $(buttonOrSelect); + $select = $button.prev('select.voice-select'); + } + + // Read the current value + var currentValue = ''; + var selectedOption = $select.find('option:selected'); + if (selectedOption.length && selectedOption.val()) { + currentValue = selectedOption.val(); + } else { + currentValue = $select.attr('data-current') || ''; + } + + if ($button.length) { + $button.text('Refreshing...').prop('disabled', true); + } + + $.post(twp_ajax.ajax_url, { + action: 'twp_refresh_elevenlabs_voices', + nonce: twp_ajax.nonce + }, function(response) { + if ($button.length) { + $button.text('🔄').prop('disabled', false); + } + + if (response.success) { + var options = ''; + + response.data.forEach(function(voice) { + var selected = (voice.voice_id === currentValue) ? ' selected' : ''; + var description = voice.labels ? Object.values(voice.labels).join(', ') : ''; + var optionText = voice.name + (description ? ' (' + description + ')' : ''); + options += ''; + }); + + $select.html(options); + + // If we had a current value, make sure it's selected + if (currentValue) { + $select.val(currentValue); + // Update the voice name field with the selected voice's name + var $voiceNameInput = $select.siblings('input[name="voice_name"]'); + if ($voiceNameInput.length) { + var selectedVoice = $select.find('option:selected'); + var voiceName = selectedVoice.data('voice-name') || selectedVoice.text() || ''; + if (selectedVoice.val() === '') { + voiceName = ''; + } + $voiceNameInput.val(voiceName); + } + } + + // Show success message + var $statusMsg = $('Refreshed!'); + $button.after($statusMsg); + setTimeout(function() { + $statusMsg.remove(); + }, 3000); + + } else { + var errorMessage = 'Error refreshing voices: '; + if (typeof response.data === 'string') { + errorMessage += response.data; + } else if (response.data && response.data.message) { + errorMessage += response.data.message; + } else { + errorMessage += 'Unknown error'; + } + alert(errorMessage); + } + }).fail(function() { + if ($button.length) { + $button.text('🔄').prop('disabled', false); + } + alert('Failed to refresh voices. Please check your API key.'); + }); + }; + // Toggle audio type options visibility $(document).on('change', 'input[name="audio_type"]', function() { var $container = $(this).closest('.step-config-section'); diff --git a/includes/class-twp-core.php b/includes/class-twp-core.php index b9ad211..e542e5f 100644 --- a/includes/class-twp-core.php +++ b/includes/class-twp-core.php @@ -161,6 +161,7 @@ class TWP_Core { // Eleven Labs AJAX $this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices'); + $this->loader->add_action('wp_ajax_twp_refresh_elevenlabs_voices', $plugin_admin, 'ajax_refresh_elevenlabs_voices'); $this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models'); $this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice'); diff --git a/includes/class-twp-workflow.php b/includes/class-twp-workflow.php index 0e2b292..a740bcb 100644 --- a/includes/class-twp-workflow.php +++ b/includes/class-twp-workflow.php @@ -79,7 +79,9 @@ class TWP_Workflow { break; case 'forward': + error_log('TWP Workflow: Processing forward step: ' . json_encode($step)); $step_twiml = self::create_forward_twiml($step); + error_log('TWP Workflow: Forward step TwiML generated: ' . $step_twiml); $stop_after_step = true; // Forward ends the workflow break; @@ -135,6 +137,9 @@ class TWP_Workflow { // Add step TwiML to combined response if ($step_twiml) { + error_log('TWP Workflow: Appending step TwiML to combined response'); + error_log('TWP Workflow: Step TwiML before append: ' . $step_twiml); + // Parse the step TwiML and append to combined response $step_xml = simplexml_load_string($step_twiml); if ($step_xml) { @@ -142,10 +147,15 @@ class TWP_Workflow { self::append_twiml_element($response, $element); } $has_response = true; + + error_log('TWP Workflow: Combined response after append: ' . $response->asXML()); + } else { + error_log('TWP Workflow: ERROR - Failed to parse step TwiML: ' . $step_twiml); } - + // Stop processing if this step type should end the workflow if ($stop_after_step) { + error_log('TWP Workflow: Stopping after this step (stop_after_step = true)'); break; } } @@ -153,10 +163,13 @@ class TWP_Workflow { // Return combined response or default if ($has_response) { - return $response->asXML(); + $final_twiml = $response->asXML(); + error_log('TWP Workflow: Final workflow TwiML response: ' . $final_twiml); + return $final_twiml; } - + // Default response + error_log('TWP Workflow: No response generated, returning default response'); return self::create_default_response(); } @@ -196,7 +209,23 @@ class TWP_Workflow { $response->record($attributes); break; case 'Dial': - $response->dial((string) $element, $attributes); + // Create dial instance + $dial = $response->dial('', $attributes); + // Add child Number elements + foreach ($element->children() as $child) { + $child_name = $child->getName(); + if ($child_name === 'Number') { + $dial->number((string) $child, self::get_attributes($child)); + } elseif ($child_name === 'Client') { + $dial->client((string) $child, self::get_attributes($child)); + } elseif ($child_name === 'Queue') { + $dial->queue((string) $child, self::get_attributes($child)); + } elseif ($child_name === 'Conference') { + $dial->conference((string) $child, self::get_attributes($child)); + } elseif ($child_name === 'Sip') { + $dial->sip((string) $child, self::get_attributes($child)); + } + } break; case 'Queue': $response->queue((string) $element, $attributes); @@ -485,25 +514,72 @@ class TWP_Workflow { * Create forward TwiML */ private static function create_forward_twiml($step) { + error_log('TWP Workflow Forward: Creating forward TwiML with step data: ' . print_r($step, true)); + $twiml = new SimpleXMLElement('