PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / article-quality-score.js

article-quality-score.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/article-quality-score.js

346 lines 15.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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