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(''); - + + // Check if data is nested (workflow steps have data nested) + $step_data = isset($step['data']) ? $step['data'] : $step; + + // Get the forward number(s) from the proper location + $forward_numbers = array(); + + // Check various possible locations for forward numbers + if (isset($step_data['forward_numbers']) && is_array($step_data['forward_numbers'])) { + $forward_numbers = $step_data['forward_numbers']; + error_log('TWP Workflow Forward: Found forward_numbers array in data: ' . print_r($forward_numbers, true)); + } elseif (isset($step_data['forward_number']) && !empty($step_data['forward_number'])) { + $forward_numbers = array($step_data['forward_number']); + error_log('TWP Workflow Forward: Found single forward_number in data: ' . $step_data['forward_number']); + } elseif (isset($step_data['number']) && !empty($step_data['number'])) { + $forward_numbers = array($step_data['number']); + error_log('TWP Workflow Forward: Found number in data: ' . $step_data['number']); + } elseif (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) { + $forward_numbers = $step['forward_numbers']; + error_log('TWP Workflow Forward: Found forward_numbers array: ' . print_r($forward_numbers, true)); + } elseif (isset($step['forward_number']) && !empty($step['forward_number'])) { + $forward_numbers = array($step['forward_number']); + error_log('TWP Workflow Forward: Found single forward_number: ' . $step['forward_number']); + } elseif (isset($step['number']) && !empty($step['number'])) { + $forward_numbers = array($step['number']); + error_log('TWP Workflow Forward: Found number: ' . $step['number']); + } + + // Filter out empty numbers + $forward_numbers = array_filter($forward_numbers, function($num) { + return !empty($num); + }); + + if (empty($forward_numbers)) { + error_log('TWP Workflow Forward: ERROR - No forward numbers found in step data'); + // Return error message instead of empty dial + $say = $twiml->addChild('Say', 'Sorry, the forwarding destination is not configured. Please try again later.'); + $say->addAttribute('voice', 'alice'); + $twiml->addChild('Hangup'); + return $twiml->asXML(); + } + + error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers)); + $dial = $twiml->addChild('Dial'); $dial->addAttribute('answerOnBridge', 'true'); - - if (isset($step['timeout'])) { - $dial->addAttribute('timeout', $step['timeout']); + + // Set timeout (default to 30 seconds if not specified) + $timeout = isset($step_data['timeout']) ? $step_data['timeout'] : + (isset($step['timeout']) ? $step['timeout'] : '30'); + $dial->addAttribute('timeout', $timeout); + error_log('TWP Workflow Forward: Using timeout: ' . $timeout); + + // Add all forward numbers + foreach ($forward_numbers as $number) { + error_log('TWP Workflow Forward: Adding number to Dial: ' . $number); + $dial->addChild('Number', $number); } - - if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) { - // Sequential forwarding - foreach ($step['forward_numbers'] as $number) { - $dial->addChild('Number', $number); - } - } elseif (isset($step['forward_number'])) { - $dial->addChild('Number', $step['forward_number']); - } - - return $twiml->asXML(); + + $result = $twiml->asXML(); + error_log('TWP Workflow Forward: Final Forward TwiML: ' . $result); + + return $result; } /**