/** * WP Agentic Writer Settings V2 * Bootstrap-based settings page JavaScript */ (function($) { 'use strict'; // Global state const state = { models: {}, currentPage: 1, perPage: 25, filters: { post: '', model: '', type: '', dateFrom: '', dateTo: '' } }; // Preset configurations const presets = { budget: { chat: 'google/gemini-2.5-flash', clarity: 'google/gemini-2.5-flash', planning: 'google/gemini-2.5-flash', writing: 'mistralai/mistral-small-creative', refinement: 'google/gemini-2.5-flash', image: 'openai/gpt-4o' }, balanced: { chat: 'google/gemini-2.5-flash', clarity: 'google/gemini-2.5-flash', planning: 'google/gemini-2.5-flash', writing: 'anthropic/claude-3.5-sonnet', refinement: 'anthropic/claude-3.5-sonnet', image: 'openai/gpt-4o' }, premium: { chat: 'google/gemini-3-flash-preview', clarity: 'anthropic/claude-sonnet-4', planning: 'google/gemini-3-flash-preview', writing: 'openai/gpt-4.1', refinement: 'openai/gpt-4.1', image: 'openai/gpt-4o' } }; // Debug function to check models window.wpawDebugModels = function() { console.log('=== WPAW Models Debug ==='); console.log('Total model categories:', Object.keys(state.models).length); Object.keys(state.models).forEach(category => { const models = state.models[category]?.all || []; console.log(`\n${category.toUpperCase()}: ${models.length} models`); // Check for specific models const checkIds = ['deepseek/deepseek-chat-v3-0324', 'anthropic/claude-3.5-sonnet']; checkIds.forEach(id => { const found = models.find(m => m.id === id); if (found) { console.log(` ✓ FOUND: ${id} => ${found.name}`); } else { console.log(` ✗ NOT FOUND: ${id}`); } }); // Show models with raw is_free and pricing data if (category === 'image') { console.log(` ALL image models (raw data from PHP):`); models.forEach(m => { console.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing); }); } else { // Show first 10 models with is_free status console.log(` First 10 models (raw data from PHP):`); models.slice(0, 10).forEach(m => { console.log(` - ${m.id} | is_free=${m.is_free} | pricing=`, m.pricing); }); } }); // AJAX debug call console.log('\n=== Fetching from server ==='); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: { action: 'wpaw_debug_models', nonce: wpawSettingsV2.nonce }, success: function(response) { if (response.success) { console.log('Server response:', response.data); console.log('Total models from API:', response.data.total_models); console.log('Found models:', response.data.found_models); console.log('Missing models:', response.data.missing_models); console.log('Sample models:', response.data.sample_models); } else { console.error('Error:', response.data.message); } }, error: function(xhr, status, error) { console.error('AJAX error:', error); } }); }; // Initialize when document is ready $(document).ready(function() { initSelect2(); initApiKeyToggle(); initCustomLanguages(); initCostLog(); initPresets(); initRefreshModels(); initFormSave(); initCustomModels(); updateCostEstimate(); // Log debug info console.log('WPAW Settings V2 loaded. Run wpawDebugModels() to debug model issues.'); }); /** * Initialize Select2 for model dropdowns */ function initSelect2() { if (typeof wpawSettingsV2 === 'undefined') return; state.models = wpawSettingsV2.models || {}; $('.wpaw-select2-model').each(function() { const $select = $(this); const modelType = $select.data('model-type') || 'execution'; const models = getModelsForType(modelType); const currentValue = wpawSettingsV2.currentModels[modelType] || $select.val(); // Clear any pre-existing options $select.empty(); $select.select2({ theme: 'bootstrap-5', width: '100%', placeholder: wpawSettingsV2.i18n.searchPlaceholder || 'Search models...', allowClear: true, data: formatModelsForSelect2(models), templateResult: formatModelOption, templateSelection: formatModelSelection, language: { noResults: function() { return wpawSettingsV2.i18n.noResults || 'No models found'; } } }); // Set current value after Select2 is initialized if (currentValue) { const modelData = models.find(m => m.id === currentValue); if (modelData) { // Create a new option with the proper formatted name const newOption = new Option(modelData.name || currentValue, currentValue, true, true); $select.append(newOption).trigger('change'); } else { console.warn('Model not found in list:', currentValue); } } // Update cost estimate on change $select.on('change', function() { updateCostEstimate(); }); }); } /** * Get models for a specific type */ function getModelsForType(type) { if (type === 'clarity' || type === 'refinement' || type === 'chat') { return state.models.execution?.all || state.models.planning?.all || []; } // Merge 'all' and 'recommended' arrays for complete model list const allModels = state.models[type]?.all || []; const recommended = state.models[type]?.recommended || []; // Combine and deduplicate by id const combined = [...allModels]; recommended.forEach(model => { if (!combined.find(m => m.id === model.id)) { combined.push(model); } }); return combined; } /** * Format models for Select2 */ function formatModelsForSelect2(models) { if (!Array.isArray(models)) return []; return models.map(model => ({ id: model.id, text: model.name || model.id, is_free: model.is_free || false, is_custom: model.is_custom || false, pricing: model.pricing || {} })); } /** * Format model option in dropdown - trust OpenRouter's is_free and pricing */ function formatModelOption(model) { if (!model.id) return model.text; const $container = $('
'); const $name = $('').text(model.text); $container.append($name); // Custom models get "Custom" badge if (model.is_custom) { const $badge = $('Custom'); $container.append($badge); } else if (model.is_free) { const $badge = $('FREE'); $container.append($badge); } else { // Show price: prompt for text models, image for image models const promptPrice = parseFloat(model.pricing?.prompt) || 0; const imagePrice = parseFloat(model.pricing?.image) || 0; const price = imagePrice > 0 ? imagePrice : promptPrice; if (price > 0) { const cost = (price * 1000000).toFixed(2); const $cost = $('').text(`$${cost}/1M`); $container.append($cost); } } return $container; } /** * Format selected model - just show name */ function formatModelSelection(model) { if (!model.id) return model.text; return model.text; } /** * Initialize preset cards */ function initPresets() { $('.preset-card').on('click keypress', function(e) { if (e.type === 'keypress' && e.which !== 13) return; const preset = $(this).data('preset'); if (!preset || !presets[preset]) return; applyPreset(preset); // Update UI $('.preset-card').removeClass('border-primary'); $(this).addClass('border-primary'); showToast('Preset applied: ' + preset.charAt(0).toUpperCase() + preset.slice(1), 'success'); }); } /** * Apply a preset configuration */ function applyPreset(presetName) { const config = presets[presetName]; if (!config) return; const fieldMap = { chat: '#chat_model', clarity: '#clarity_model', planning: '#planning_model', writing: '#writing_model', refinement: '#refinement_model', image: '#image_model' }; Object.keys(config).forEach(type => { const $select = $(fieldMap[type]); if (!$select.length) return; const targetId = config[type]; // Add option if not exists if (!$select.find(`option[value="${targetId}"]`).length) { const displayName = targetId.split('/').pop().replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); $select.append(new Option(displayName, targetId, false, false)); } $select.val(targetId).trigger('change'); }); updateCostEstimate(); } /** * Initialize API key visibility toggle */ function initApiKeyToggle() { $('#wpaw-toggle-api-key').on('click', function() { const $input = $('#openrouter_api_key'); const type = $input.attr('type'); $input.attr('type', type === 'password' ? 'text' : 'password'); $(this).find('.bi') .toggleClass('bi-eye', type === 'text') .toggleClass('bi-eye-slash', type === 'password'); }); $('#wpaw-test-api-key').on('click', function() { const apiKey = $('#openrouter_api_key').val(); if (!apiKey) { showToast('Please enter an API key first', 'warning'); return; } const $btn = $(this); const originalText = $btn.html(); $btn.prop('disabled', true).html('Testing...'); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: { action: 'wpaw_test_api_connection', nonce: wpawSettingsV2.nonce }, success: function(response) { if (response.success) { showToast(response.data.message + ' (' + response.data.models_count + ' models available)', 'success'); } else { showToast(response.data.message || 'API test failed', 'danger'); } }, error: function() { showToast('Failed to test API connection', 'danger'); }, complete: function() { $btn.prop('disabled', false).html(originalText); } }); }); } /** * Initialize custom languages management */ function initCustomLanguages() { $('#wpaw-add-custom-language').on('click', function() { const html = `
`; $('#wpaw-custom-languages-list').append(html); }); $(document).on('click', '.wpaw-remove-language', function() { $(this).closest('.wpaw-custom-language-item').remove(); }); } /** * Initialize cost log functionality */ function initCostLog() { console.log('Initializing cost log...'); // Load on tab show $('#cost-log-tab').on('shown.bs.tab', function() { console.log('Cost log tab shown, loading data...'); loadCostLogData(); }); // Auto-load if cost-log tab is active on page load if ($('#cost-log-tab').hasClass('active')) { console.log('Cost log tab is active on load, loading data...'); loadCostLogData(); } // Filter controls $('#wpaw-apply-filters').on('click', function() { state.filters = { post: $('#wpaw-filter-post').val(), model: $('#wpaw-filter-model').val(), type: $('#wpaw-filter-type').val(), dateFrom: $('#wpaw-filter-date-from').val(), dateTo: $('#wpaw-filter-date-to').val() }; state.currentPage = 1; loadCostLogData(); }); $('#wpaw-clear-filters').on('click', function() { $('#wpaw-filter-post').val(''); $('#wpaw-filter-model').val(''); $('#wpaw-filter-type').val(''); $('#wpaw-filter-date-from').val(''); $('#wpaw-filter-date-to').val(''); state.filters = { post: '', model: '', type: '', dateFrom: '', dateTo: '' }; state.currentPage = 1; loadCostLogData(); }); $('#wpaw-per-page').on('change', function() { state.perPage = parseInt($(this).val()) || 25; state.currentPage = 1; loadCostLogData(); }); // Export CSV $('#wpaw-export-csv').on('click', exportCostLogCSV); // Pagination clicks $(document).on('click', '#wpaw-pagination .page-link', function(e) { e.preventDefault(); const page = $(this).data('page'); if (page && page !== state.currentPage) { state.currentPage = page; loadCostLogData(); } }); } /** * Load cost log data via AJAX */ function loadCostLogData() { console.log('loadCostLogData called'); console.log('wpawSettingsV2:', wpawSettingsV2); console.log('State:', state); const $tbody = $('#wpaw-cost-log-tbody'); console.log('Table tbody found:', $tbody.length); $tbody.html(`
Loading...
`); const ajaxData = { action: 'wpaw_get_cost_log_data', nonce: wpawSettingsV2.nonce, page: state.currentPage, per_page: state.perPage, filter_post: state.filters.post, filter_model: state.filters.model, filter_type: state.filters.type, filter_date_from: state.filters.dateFrom, filter_date_to: state.filters.dateTo }; console.log('AJAX request data:', ajaxData); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: ajaxData, success: function(response) { console.log('Cost log response:', response); if (response.success) { renderCostLogTable(response.data); updateCostLogStats(response.data.stats); updateFilterOptions(response.data.filters); renderPagination(response.data); } else { const errorMsg = response.data?.message || 'Error loading data'; console.error('Cost log error:', errorMsg); $tbody.html('' + escapeHtml(errorMsg) + ''); } }, error: function(xhr, status, error) { console.error('Cost log AJAX error:', status, error); console.error('XHR:', xhr); console.error('Response text:', xhr.responseText); $tbody.html('Failed to load cost log. Check browser console for details.'); } }); } /** * Render cost log table (grouped by post) */ function renderCostLogTable(data) { const $tbody = $('#wpaw-cost-log-tbody'); const records = data.records || []; if (records.length === 0) { $tbody.html('No cost records found.'); return; } let html = ''; records.forEach((group, index) => { const collapseId = `collapse-post-${group.post_id}-${index}`; const postCell = group.post_link ? `${escapeHtml(group.post_title)}` : `${escapeHtml(group.post_title)}`; // Main row (clickable to expand) html += ` ${postCell} ${group.call_count} ${group.call_count === 1 ? 'call' : 'calls'} $${group.total_cost} `; // Collapsible details row html += `
`; // Detail rows group.details.forEach(detail => { html += ` `; }); html += `
${escapeHtml(detail.created_at)} ${escapeHtml(detail.model)} ${escapeHtml(detail.action)} ${detail.input_tokens} ${detail.output_tokens} $${detail.cost}
`; }); $tbody.html(html); // Add collapse event listeners to rotate icon $('.wpaw-group-row').on('click', function() { const $icon = $(this).find('.wpaw-collapse-icon'); setTimeout(() => { const target = $(this).data('bs-target'); const isExpanded = $(target).hasClass('show'); $icon.toggleClass('dashicons-arrow-right-alt2', !isExpanded); $icon.toggleClass('dashicons-arrow-down-alt2', isExpanded); }, 10); }); // Update records info const start = (data.current_page - 1) * data.per_page + 1; const end = Math.min(data.current_page * data.per_page, data.total_items); $('#wpaw-records-info').text(`Showing ${start}-${end} of ${data.total_items} posts`); } /** * Update cost log stats */ function updateCostLogStats(stats) { if (!stats) return; $('#wpaw-stat-all-time').text('$' + stats.all_time); $('#wpaw-stat-monthly').text('$' + stats.monthly); $('#wpaw-stat-today').text('$' + stats.today); $('#wpaw-stat-avg').text('$' + stats.avg_per_post); } /** * Update filter dropdown options */ function updateFilterOptions(filters) { if (!filters) return; // Models const $modelSelect = $('#wpaw-filter-model'); const currentModel = $modelSelect.val(); $modelSelect.find('option:not(:first)').remove(); (filters.models || []).forEach(model => { $modelSelect.append(new Option(model, model, false, model === currentModel)); }); // Types const $typeSelect = $('#wpaw-filter-type'); const currentType = $typeSelect.val(); $typeSelect.find('option:not(:first)').remove(); (filters.types || []).forEach(type => { const label = type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); $typeSelect.append(new Option(label, type, false, type === currentType)); }); } /** * Render pagination */ function renderPagination(data) { const $pagination = $('#wpaw-pagination'); const totalPages = data.total_pages || 1; const currentPage = data.current_page || 1; if (totalPages <= 1) { $pagination.html(''); return; } let html = ''; // Previous html += `
  • «
  • `; // Page numbers const maxVisible = 5; let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2)); let endPage = Math.min(totalPages, startPage + maxVisible - 1); if (endPage - startPage < maxVisible - 1) { startPage = Math.max(1, endPage - maxVisible + 1); } if (startPage > 1) { html += `
  • 1
  • `; if (startPage > 2) { html += `
  • ...
  • `; } } for (let i = startPage; i <= endPage; i++) { html += `
  • ${i}
  • `; } if (endPage < totalPages) { if (endPage < totalPages - 1) { html += `
  • ...
  • `; } html += `
  • ${totalPages}
  • `; } // Next html += `
  • »
  • `; $pagination.html(html); } /** * Export cost log to CSV */ function exportCostLogCSV() { const table = $('#wpaw-cost-log-table'); const rows = []; // Headers const headers = []; table.find('thead th').each(function() { headers.push($(this).text().trim()); }); rows.push(headers.join(',')); // Data rows table.find('tbody tr').each(function() { const row = []; $(this).find('td').each(function() { let text = $(this).text().trim().replace(/"/g, '""'); row.push('"' + text + '"'); }); if (row.length > 0) { rows.push(row.join(',')); } }); // Download const csvContent = rows.join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'wp-agentic-writer-costs-' + new Date().toISOString().split('T')[0] + '.csv'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('CSV exported successfully', 'success'); } /** * Initialize refresh models button */ function initRefreshModels() { $('#wpaw-refresh-models').on('click', function() { const $btn = $(this); const $spinner = $('#wpaw-models-spinner'); const $message = $('#wpaw-models-message'); $btn.prop('disabled', true); $spinner.removeClass('d-none'); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: { action: 'wpaw_refresh_models', nonce: wpawSettingsV2.nonce }, success: function(response) { if (response.success) { // Update both state and wpawSettingsV2 with new models state.models = response.data.models; wpawSettingsV2.models = response.data.models; // Destroy all Select2 instances $('.wpaw-select2-model').each(function() { $(this).select2('destroy'); }); // Reinitialize Select2 with new models initSelect2(); showToast(response.data.message || 'Models refreshed!', 'success'); } else { showToast(response.data?.message || 'Failed to refresh models', 'danger'); } }, error: function() { showToast('Failed to refresh models', 'danger'); }, complete: function() { $btn.prop('disabled', false); $spinner.addClass('d-none'); } }); }); } /** * Initialize form save handling */ function initFormSave() { $('#wpaw-reset-settings').on('click', function() { if (confirm(wpawSettingsV2.i18n.confirmReset || 'Are you sure you want to reset all settings to defaults?')) { // Apply balanced preset as default applyPreset('balanced'); showToast('Settings reset to defaults. Click Save to apply.', 'info'); } }); } /** * Update cost estimate based on selected models */ function updateCostEstimate() { // Simple estimate calculation // Planning: ~2K tokens, Writing: ~4K tokens, 1 image const planningModel = $('#planning_model').val(); const writingModel = $('#writing_model').val(); let estimate = 0.10; // Default balanced estimate if (writingModel) { if (writingModel.includes('mistral') || writingModel.includes('gemini')) { estimate = 0.06; } else if (writingModel.includes('gpt-4.1') || writingModel.includes('opus')) { estimate = 0.31; } else if (writingModel.includes('claude') || writingModel.includes('sonnet')) { estimate = 0.14; } } $('#wpaw-cost-estimate').text('~$' + estimate.toFixed(2)); } /** * Show toast notification */ function showToast(message, type) { const $toast = $('#wpaw-toast'); const $body = $('#wpaw-toast-message'); $body.text(message); $toast.removeClass('bg-success bg-danger bg-warning bg-info text-white'); if (type === 'success') { $toast.addClass('bg-success text-white'); } else if (type === 'danger' || type === 'error') { $toast.addClass('bg-danger text-white'); } else if (type === 'warning') { $toast.addClass('bg-warning'); } else { $toast.addClass('bg-info text-white'); } const toast = new bootstrap.Toast($toast[0], { delay: 3000 }); toast.show(); } /** * Escape HTML entities */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Initialize custom models repeater with auto-save */ function initCustomModels() { let customModelIndex = $('#wpaw-custom-models-list .custom-model-row').length; // Add new custom model row $('#wpaw-add-custom-model').on('click', function() { const template = $('#wpaw-custom-model-template').html(); const newRow = template.replace(/__INDEX__/g, customModelIndex); $('#wpaw-custom-models-list').append(newRow); customModelIndex++; // Focus the new model ID input $('#wpaw-custom-models-list .custom-model-row:last input:first').focus(); }); // Auto-save on blur from model ID input $('#wpaw-custom-models-list').on('blur', '.wpaw-custom-model-id', function() { const $row = $(this).closest('.custom-model-row'); const modelId = $row.find('.wpaw-custom-model-id').val().trim(); if (modelId) { saveCustomModel($row); } }); // Auto-save on blur from model name input $('#wpaw-custom-models-list').on('blur', '.wpaw-custom-model-name', function() { const $row = $(this).closest('.custom-model-row'); const modelId = $row.find('.wpaw-custom-model-id').val().trim(); if (modelId) { saveCustomModel($row); } }); // Auto-save on type change $('#wpaw-custom-models-list').on('change', '.wpaw-custom-model-type', function() { const $row = $(this).closest('.custom-model-row'); const modelId = $row.find('.wpaw-custom-model-id').val().trim(); if (modelId) { saveCustomModel($row); } }); // Delete custom model $('#wpaw-custom-models-list').on('click', '.wpaw-remove-custom-model', function() { const $row = $(this).closest('.custom-model-row'); const modelId = $row.find('.wpaw-custom-model-id').val().trim(); if (modelId) { deleteCustomModel(modelId, $row); } else { $row.remove(); } }); } /** * Save custom model via AJAX */ function saveCustomModel($row) { const modelId = $row.find('.wpaw-custom-model-id').val().trim(); const modelName = $row.find('.wpaw-custom-model-name').val().trim(); const modelType = $row.find('.wpaw-custom-model-type').val(); if (!modelId) return; // Show saving indicator $row.css('opacity', '0.6'); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: { action: 'wpaw_save_custom_model', nonce: wpawSettingsV2.nonce, model_id: modelId, model_name: modelName, model_type: modelType }, success: function(response) { if (response.success) { // Mark row as saved $row.attr('data-saved', 'true'); // Update models and refresh Select2 state.models = response.data.models; wpawSettingsV2.models = response.data.models; refreshAllSelect2(); // Show toast only on first save if (!$row.data('first-save-done')) { showToast('Model saved!', 'success'); $row.data('first-save-done', true); } } else { showToast(response.data?.message || 'Failed to save', 'danger'); } }, error: function() { showToast('Failed to save model', 'danger'); }, complete: function() { $row.css('opacity', '1'); } }); } /** * Delete custom model via AJAX */ function deleteCustomModel(modelId, $row) { $row.css('opacity', '0.6'); $.ajax({ url: wpawSettingsV2.ajaxUrl, type: 'POST', data: { action: 'wpaw_delete_custom_model', nonce: wpawSettingsV2.nonce, model_id: modelId }, success: function(response) { if (response.success) { $row.remove(); state.models = response.data.models; wpawSettingsV2.models = response.data.models; refreshAllSelect2(); showToast('Model deleted!', 'success'); } else { showToast(response.data?.message || 'Failed to delete', 'danger'); $row.css('opacity', '1'); } }, error: function() { showToast('Failed to delete model', 'danger'); $row.css('opacity', '1'); } }); } /** * Refresh all Select2 dropdowns with current model data */ function refreshAllSelect2() { $('.wpaw-select2-model').each(function() { const $select = $(this); const currentValue = $select.val(); // Destroy and reinitialize if ($select.hasClass('select2-hidden-accessible')) { $select.select2('destroy'); } }); // Reinitialize all initSelect2(); } })(jQuery);