| 1 |
import { __ } from "@wordpress/i18n"; |
| 2 |
|
| 3 |
jQuery(document).ready(function($) { |
| 4 |
|
| 5 |
function getScoreColorClass(score) { |
| 6 |
if (score >= 80) return 'green'; |
| 7 |
if (score >= 50) return 'yellow'; |
| 8 |
if (score >= 30) return 'orange'; |
| 9 |
return 'red'; |
| 10 |
} |
| 11 |
|
| 12 |
function getScoreStatusText(score) { |
| 13 |
if (score >= 80) return __('Excellent', 'betterdocs'); |
| 14 |
if (score >= 60) return __('Good', 'betterdocs'); |
| 15 |
if (score >= 40) return __('Needs Improvement', 'betterdocs'); |
| 16 |
return __('Poor', 'betterdocs'); |
| 17 |
} |
| 18 |
|
| 19 |
function showResults(container, data) { |
| 20 |
const $container = $(container); |
| 21 |
const $results = $container.find('.quality-results'); |
| 22 |
const $empty = $container.find('.quality-empty'); |
| 23 |
|
| 24 |
// Hide empty state, show results |
| 25 |
$empty.hide(); |
| 26 |
$results.show(); |
| 27 |
|
| 28 |
// Handle nested data structure (response.data.data) |
| 29 |
const analysisData = data.data || data; |
| 30 |
|
| 31 |
// Build results HTML |
| 32 |
const score = analysisData.overall_score || 0; |
| 33 |
const scores = analysisData.scores || {}; |
| 34 |
const feedback = analysisData.feedback || {}; |
| 35 |
const strengths = feedback.strengths || []; |
| 36 |
const improvements = feedback.improvements || []; |
| 37 |
const suggestions = feedback.suggestions || []; |
| 38 |
|
| 39 |
let resultsHTML = ''; |
| 40 |
|
| 41 |
// Results header with regenerate button |
| 42 |
resultsHTML += '<div class="quality-results-header">'; |
| 43 |
resultsHTML += '<h4>' + __('Docs quality analysis results', 'betterdocs') + '</h4>'; |
| 44 |
resultsHTML += '<button type="button" class="betterdocs-regenerate-quality" id="betterdocs-regenerate-quality">'; |
| 45 |
resultsHTML += '<svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">'; |
| 46 |
resultsHTML += '<path d="M1.1 15.05C0.733333 14.4167 0.458333 13.7667 0.275 13.1C0.0916667 12.4333 0 11.75 0 11.05C0 8.81667 0.775 6.91667 2.325 5.35C3.875 3.78333 5.76667 3 8 3H8.175L6.575 1.4L7.975 0L11.975 4L7.975 8L6.575 6.6L8.175 5H8C6.33333 5 4.91667 5.5875 3.75 6.7625C2.58333 7.9375 2 9.36667 2 11.05C2 11.4833 2.05 11.9083 2.15 12.325C2.25 12.7417 2.4 13.15 2.6 13.55L1.1 15.05ZM8.025 22L4.025 18L8.025 14L9.425 15.4L7.825 17H8C9.66667 17 11.0833 16.4125 12.25 15.2375C13.4167 14.0625 14 12.6333 14 10.95C14 10.5167 13.95 10.0917 13.85 9.675C13.75 9.25833 13.6 8.85 13.4 8.45L14.9 6.95C15.2667 7.58333 15.5417 8.23333 15.725 8.9C15.9083 9.56667 16 10.25 16 10.95C16 13.1833 15.225 15.0833 13.675 16.65C12.125 18.2167 10.2333 19 8 19H7.825L9.425 20.6L8.025 22Z" fill="#475467"/>'; |
| 47 |
resultsHTML += '</svg>'; |
| 48 |
resultsHTML += '</button>'; |
| 49 |
resultsHTML += '</div>'; |
| 50 |
|
| 51 |
// Calculate circular progress |
| 52 |
const scoreColor = getScoreColorClass(score); |
| 53 |
const circumference = 2 * Math.PI * 54; // radius = 54 |
| 54 |
const offset = circumference - (score / 100) * circumference; |
| 55 |
|
| 56 |
// Start two-column layout |
| 57 |
resultsHTML += '<div class="quality-results-body">'; |
| 58 |
// Left Column: Circular Progress Score |
| 59 |
resultsHTML += '<div class="score-column">'; |
| 60 |
resultsHTML += '<h3 class="score-column-title">' + __('Overall Score', 'betterdocs') + '</h3>'; |
| 61 |
resultsHTML += '<div class="progress-ring-wrapper">'; |
| 62 |
resultsHTML += '<svg class="progress-ring" viewBox="0 0 120 120">'; |
| 63 |
resultsHTML += '<circle class="progress-ring-bg" cx="60" cy="60" r="54" fill="none" stroke-width="12"></circle>'; |
| 64 |
resultsHTML += '<circle class="progress-ring-circle ' + scoreColor + '" cx="60" cy="60" r="54" fill="none" stroke-width="12" stroke-linecap="round" style="stroke-dasharray: ' + circumference + '; stroke-dashoffset: ' + offset + '; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 0.5s ease-in-out;"></circle>'; |
| 65 |
resultsHTML += '</svg>'; |
| 66 |
resultsHTML += '<div class="score-text-wrapper">'; |
| 67 |
resultsHTML += '<span class="score-value ' + scoreColor + '">' + score + '</span>'; |
| 68 |
resultsHTML += '<span class="score-label">' + __('out of 100', 'betterdocs') + '</span>'; |
| 69 |
resultsHTML += '</div>'; |
| 70 |
resultsHTML += '</div>'; |
| 71 |
resultsHTML += '<div class="score-feedback ' + scoreColor + '">' + getScoreStatusText(score) + '</div>'; |
| 72 |
resultsHTML += '<p class="score-description">' + __('This score is a weighted average of clarity, relevance, structure, and other quality factors.', 'betterdocs') + '</p>'; |
| 73 |
resultsHTML += '</div>'; |
| 74 |
|
| 75 |
// Right Column: Details (Score Breakdown) |
| 76 |
resultsHTML += '<div class="details-column">'; |
| 77 |
if (Object.keys(scores).length > 0) { |
| 78 |
resultsHTML += '<div class="section">'; |
| 79 |
resultsHTML += '<h3 class="section-title">' + __('Score Breakdown', 'betterdocs') + '</h3>'; |
| 80 |
resultsHTML += '<div class="score-breakdown-list">'; |
| 81 |
Object.keys(scores).forEach(function(criterion) { |
| 82 |
const criterionScore = scores[criterion]; |
| 83 |
resultsHTML += '<div class="score-item">'; |
| 84 |
resultsHTML += '<div class="score-name-group">'; |
| 85 |
resultsHTML += '<span class="score-name">' + criterion.charAt(0).toUpperCase() + criterion.slice(1) + '</span>'; |
| 86 |
resultsHTML += '<span class="score-percentage">' + criterionScore + '%</span>'; |
| 87 |
resultsHTML += '</div>'; |
| 88 |
resultsHTML += '<div class="score-bar-group">'; |
| 89 |
resultsHTML += '<div class="score-bar-wrapper">'; |
| 90 |
resultsHTML += '<div class="score-bar-fill ' + getScoreColorClass(criterionScore) + '" style="width: ' + criterionScore + '%"></div>'; |
| 91 |
resultsHTML += '</div>'; |
| 92 |
resultsHTML += '</div>'; |
| 93 |
resultsHTML += '</div>'; |
| 94 |
}); |
| 95 |
resultsHTML += '</div>'; |
| 96 |
resultsHTML += '</div>'; |
| 97 |
} |
| 98 |
resultsHTML += '</div>'; // Close details-column |
| 99 |
resultsHTML += '</div>'; // Close quality-results-body |
| 100 |
|
| 101 |
// Gather all suggestions for "Actionable Insights" section |
| 102 |
const allSuggestions = []; |
| 103 |
if (strengths.length > 0) allSuggestions.push(...strengths); |
| 104 |
if (improvements.length > 0) allSuggestions.push(...improvements); |
| 105 |
if (suggestions.length > 0) allSuggestions.push(...suggestions); |
| 106 |
if (allSuggestions.length > 0) { |
| 107 |
resultsHTML += '<div class="improvement-section section">'; |
| 108 |
resultsHTML += '<h3 class="section-title">' + __('Actionable Insights', 'betterdocs') + '</h3>'; |
| 109 |
resultsHTML += '<div class="improvements-list">'; |
| 110 |
allSuggestions.forEach(function(suggestion) { |
| 111 |
resultsHTML += '<div class="improvement-item">'; |
| 112 |
resultsHTML += '<div class="improvement-icon">'; |
| 113 |
resultsHTML += '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">'; |
| 114 |
resultsHTML += '<rect width="16" height="16" rx="8" fill="#D0D5DD"></rect>'; |
| 115 |
resultsHTML += '<path d="M5 8.88L6.67619 10.8L10.8667 6" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"></path>'; |
| 116 |
resultsHTML += '</svg>'; |
| 117 |
resultsHTML += '</div>'; |
| 118 |
resultsHTML += '<span class="improvement-text">' + suggestion + '</span>'; |
| 119 |
resultsHTML += '</div>'; |
| 120 |
}); |
| 121 |
resultsHTML += '</div>'; |
| 122 |
resultsHTML += '</div>'; |
| 123 |
} |
| 124 |
|
| 125 |
$results.html(resultsHTML); |
| 126 |
} |
| 127 |
|
| 128 |
function showLoading(container) { |
| 129 |
const $container = $(container); |
| 130 |
$container.find('.quality-empty').hide(); |
| 131 |
$container.find('.quality-results').hide(); |
| 132 |
$container.find('.quality-error').hide(); |
| 133 |
$container.find('.quality-loading').show(); |
| 134 |
} |
| 135 |
|
| 136 |
function hideLoading(container) { |
| 137 |
const $container = $(container); |
| 138 |
$container.find('.quality-loading').hide(); |
| 139 |
} |
| 140 |
|
| 141 |
function showError(container, errorData) { |
| 142 |
const $container = $(container); |
| 143 |
hideLoading(container); |
| 144 |
|
| 145 |
// Extract error message from various response formats |
| 146 |
let message = ''; |
| 147 |
let isConfigError = false; |
| 148 |
|
| 149 |
if (typeof errorData === 'string') { |
| 150 |
message = errorData; |
| 151 |
} else if (errorData && typeof errorData === 'object') { |
| 152 |
// Handle WP_Error format or standard error response |
| 153 |
if (errorData.message) { |
| 154 |
message = errorData.message; |
| 155 |
} else if (errorData.data && errorData.data.message) { |
| 156 |
message = errorData.data.message; |
| 157 |
} else { |
| 158 |
message = __('An unexpected error occurred. Please try again.', 'betterdocs'); |
| 159 |
} |
| 160 |
} else { |
| 161 |
message = __('Analysis failed. Please try again.', 'betterdocs'); |
| 162 |
} |
| 163 |
|
| 164 |
// Check if this is an API key configuration error |
| 165 |
if (message.toLowerCase().includes('api key') || message.toLowerCase().includes('not configured')) { |
| 166 |
isConfigError = true; |
| 167 |
message = __('AI API key is not configured. Please configure your API key in the settings to use this feature.', 'betterdocs'); |
| 168 |
|
| 169 |
// Hide the analyze button and show error in empty state |
| 170 |
$container.find('.betterdocs-analyze-btn').hide(); |
| 171 |
$container.find('.quality-empty').show(); |
| 172 |
|
| 173 |
// Show configuration error UI elements |
| 174 |
$container.find('.quality-error').find('.error-icon').show(); |
| 175 |
$container.find('.quality-error').find('strong').show(); |
| 176 |
$container.find('.quality-error').find('.error-action-button').show(); |
| 177 |
|
| 178 |
// Add configuration error class, remove content error class |
| 179 |
$container.find('.quality-error').removeClass('error-content-validation').addClass('error-config'); |
| 180 |
} else { |
| 181 |
// For content errors (empty, too short, etc.) |
| 182 |
// Hide configuration-specific UI elements |
| 183 |
$container.find('.quality-error').find('.error-icon').hide(); |
| 184 |
$container.find('.quality-error').find('strong').hide(); |
| 185 |
$container.find('.quality-error').find('.error-action-button').hide(); |
| 186 |
|
| 187 |
// Keep the analyze button visible for content errors |
| 188 |
$container.find('.betterdocs-analyze-btn').show(); |
| 189 |
$container.find('.quality-empty').show(); |
| 190 |
|
| 191 |
// Add content error class, remove configuration error class |
| 192 |
$container.find('.quality-error').removeClass('error-config').addClass('error-content-validation'); |
| 193 |
} |
| 194 |
|
| 195 |
$container.find('.quality-error').find('.error-message').text(message); |
| 196 |
$container.find('.quality-error').show(); |
| 197 |
} |
| 198 |
|
| 199 |
// Analyze button click handler |
| 200 |
$(document).on('click', '#betterdocs-analyze-quality', function(e) { |
| 201 |
e.preventDefault(); |
| 202 |
|
| 203 |
const $button = $(this); |
| 204 |
const $container = $button.closest('.betterdocs-quality-analysis-content'); |
| 205 |
const postId = $container.data('post-id'); |
| 206 |
|
| 207 |
if (!postId) { |
| 208 |
alert(__('Please save the post first.', 'betterdocs')); |
| 209 |
return; |
| 210 |
} |
| 211 |
|
| 212 |
// Get current editor content (handles both Classic and Gutenberg editors) |
| 213 |
let currentContent = ''; |
| 214 |
let currentTitle = ''; |
| 215 |
|
| 216 |
// Try to get Gutenberg editor content |
| 217 |
if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) { |
| 218 |
const editor = wp.data.select('core/editor'); |
| 219 |
currentContent = editor.getEditedPostContent(); |
| 220 |
currentTitle = editor.getEditedPostAttribute('title'); |
| 221 |
} |
| 222 |
// Fallback to Classic Editor |
| 223 |
else if (typeof tinymce !== 'undefined' && tinymce.get('content')) { |
| 224 |
currentContent = tinymce.get('content').getContent(); |
| 225 |
currentTitle = $('#title').val() || ''; |
| 226 |
} |
| 227 |
// Fallback to textarea (if visual editor is disabled) |
| 228 |
else if ($('#content').length) { |
| 229 |
currentContent = $('#content').val(); |
| 230 |
currentTitle = $('#title').val() || ''; |
| 231 |
} |
| 232 |
|
| 233 |
// Show loading state |
| 234 |
$button.prop('disabled', true); |
| 235 |
showLoading($container); |
| 236 |
|
| 237 |
// Prepare AJAX data |
| 238 |
const ajaxData = { |
| 239 |
action: 'betterdocs_analyze_quality', |
| 240 |
post_id: postId, |
| 241 |
nonce: betterdocsQualityScore.nonce |
| 242 |
}; |
| 243 |
|
| 244 |
// Add current content if available |
| 245 |
if (currentContent) { |
| 246 |
ajaxData.current_content = currentContent; |
| 247 |
ajaxData.current_title = currentTitle; |
| 248 |
} |
| 249 |
|
| 250 |
// Run analysis |
| 251 |
$.ajax({ |
| 252 |
url: betterdocsQualityScore.ajax_url, |
| 253 |
type: 'POST', |
| 254 |
data: ajaxData, |
| 255 |
success: function(response) { |
| 256 |
if (response.success) { |
| 257 |
hideLoading($container); |
| 258 |
showResults($container, response.data); |
| 259 |
} else { |
| 260 |
showError($container, response.data || response); |
| 261 |
} |
| 262 |
}, |
| 263 |
error: function() { |
| 264 |
showError($container, __('Network error. Please try again.', 'betterdocs')); |
| 265 |
}, |
| 266 |
complete: function() { |
| 267 |
$button.prop('disabled', false); |
| 268 |
} |
| 269 |
}); |
| 270 |
}); |
| 271 |
|
| 272 |
// Regenerate button click handler |
| 273 |
$(document).on('click', '#betterdocs-regenerate-quality', function(e) { |
| 274 |
e.preventDefault(); |
| 275 |
|
| 276 |
const $button = $(this); |
| 277 |
const $container = $button.closest('.betterdocs-quality-analysis-content'); |
| 278 |
const postId = $container.data('post-id'); |
| 279 |
|
| 280 |
if (!postId) { |
| 281 |
alert(__('Please save the post first.', 'betterdocs')); |
| 282 |
return; |
| 283 |
} |
| 284 |
|
| 285 |
// Get current editor content (handles both Classic and Gutenberg editors) |
| 286 |
let currentContent = ''; |
| 287 |
let currentTitle = ''; |
| 288 |
|
| 289 |
// Try to get Gutenberg editor content |
| 290 |
if (typeof wp !== 'undefined' && wp.data && wp.data.select('core/editor')) { |
| 291 |
const editor = wp.data.select('core/editor'); |
| 292 |
currentContent = editor.getEditedPostContent(); |
| 293 |
currentTitle = editor.getEditedPostAttribute('title'); |
| 294 |
} |
| 295 |
// Fallback to Classic Editor |
| 296 |
else if (typeof tinymce !== 'undefined' && tinymce.get('content')) { |
| 297 |
currentContent = tinymce.get('content').getContent(); |
| 298 |
currentTitle = $('#title').val() || ''; |
| 299 |
} |
| 300 |
// Fallback to textarea (if visual editor is disabled) |
| 301 |
else if ($('#content').length) { |
| 302 |
currentContent = $('#content').val(); |
| 303 |
currentTitle = $('#title').val() || ''; |
| 304 |
} |
| 305 |
|
| 306 |
// Show loading state |
| 307 |
$button.prop('disabled', true); |
| 308 |
showLoading($container); |
| 309 |
|
| 310 |
// Prepare AJAX data |
| 311 |
const ajaxData = { |
| 312 |
action: 'betterdocs_analyze_quality', |
| 313 |
post_id: postId, |
| 314 |
nonce: betterdocsQualityScore.nonce |
| 315 |
}; |
| 316 |
|
| 317 |
// Add current content if available |
| 318 |
if (currentContent) { |
| 319 |
ajaxData.current_content = currentContent; |
| 320 |
ajaxData.current_title = currentTitle; |
| 321 |
} |
| 322 |
|
| 323 |
// Run analysis |
| 324 |
$.ajax({ |
| 325 |
url: betterdocsQualityScore.ajax_url, |
| 326 |
type: 'POST', |
| 327 |
data: ajaxData, |
| 328 |
success: function(response) { |
| 329 |
if (response.success) { |
| 330 |
hideLoading($container); |
| 331 |
showResults($container, response.data); |
| 332 |
} else { |
| 333 |
showError($container, response.data || response); |
| 334 |
} |
| 335 |
}, |
| 336 |
error: function() { |
| 337 |
showError($container, __('Network error. Please try again.', 'betterdocs')); |
| 338 |
}, |
| 339 |
complete: function() { |
| 340 |
$button.prop('disabled', false); |
| 341 |
} |
| 342 |
}); |
| 343 |
}); |
| 344 |
|
| 345 |
}); |
| 346 |
|