PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.3
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 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Core / ArticleQualityScore.php

ArticleQualityScore.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.3, at includes/Core/ArticleQualityScore.php

514 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Core;
4
5 use WPDeveloper\BetterDocs\Utils\Base;
6 use WPDeveloper\BetterDocs\Utils\AIHelper;
7 use WPDeveloper\BetterDocs\Core\Settings;
8
9 class ArticleQualityScore extends Base {
10
11 /**
12 * Settings instance
13 *
14 * @var Settings
15 */
16 private $settings;
17
18 /**
19 * AIHelper instance
20 *
21 * @var AIHelper
22 */
23 private $ai_helper;
24
25 public function __construct( Settings $settings ) {
26 $this->settings = $settings;
27 $this->ai_helper = new AIHelper( $settings );
28
29 // Only initialize for docs post type in admin
30 if ( is_admin() ) {
31 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
32 add_action( 'wp_ajax_betterdocs_analyze_quality', [ $this, 'ajax_analyze_article_quality' ] );
33 add_action( 'wp_ajax_betterdocs_save_quality_analysis', [ $this, 'ajax_save_quality_analysis' ] );
34 add_action( 'wp_ajax_betterdocs_check_cached_analysis', [ $this, 'ajax_check_cached_analysis' ] );
35
36 // Hook into unified metabox
37 add_action( 'betterdocs_quality_analysis_tab_content', [ $this, 'render_quality_analysis_content' ] );
38 }
39 }
40
41 /**
42 * Enqueue admin scripts for quality score functionality
43 */
44 public function enqueue_admin_scripts( $hook ) {
45 global $post_type;
46
47 // Only load on docs edit screens
48 if ( ( $hook === 'post.php' || $hook === 'post-new.php' ) && $post_type === 'docs' ) {
49 betterdocs()->assets->enqueue( 'betterdocs-article-quality-score', 'admin/js/article-quality-score.js' );
50
51 // Localize script with AJAX data
52 wp_localize_script(
53 'betterdocs-article-quality-score',
54 'betterdocsQualityScore',
55 [
56 'ajax_url' => admin_url( 'admin-ajax.php' ),
57 'nonce' => wp_create_nonce( 'betterdocs_quality_score_nonce' ),
58 'post_id' => get_the_ID(),
59 'strings' => [
60 'analysing' => __( 'Analysing your docs with BetterDocs AI...', 'betterdocs' ),
61 'error' => __( 'Error', 'betterdocs' ),
62 'regenerate' => __( 'Regenerate', 'betterdocs' ),
63 'analyze' => __( 'Analyze with BetterDocs AI', 'betterdocs' ),
64 ]
65 ]
66 );
67 }
68 }
69
70 /**
71 * Render quality analysis content for the unified metabox tab
72 *
73 * @param \WP_Post $post Current post object
74 */
75 public function render_quality_analysis_content( $post ) {
76 $nonce = wp_create_nonce( 'betterdocs_quality_score_nonce' );
77 $cached_analysis = get_post_meta( $post->ID, '_betterdocs_article_quality_analysis', true );
78 $has_results = ! empty( $cached_analysis ) && isset( $cached_analysis['overall_score'] );
79
80 ?>
81 <div class="betterdocs-quality-analysis-content" data-post-id="<?php echo esc_attr( $post->ID ); ?>">
82 <!-- Loading State -->
83 <div class="quality-loading" style="display: none;">
84 <div class="quality-loading-spinner">
85 <div class="quality-spinner is-active">
86 <div class="quality-spinner-color"></div>
87 <div class="quality-spinner-mask"></div>
88 </div>
89 </div>
90 <p><?php esc_html_e( 'Analysing your docs with BetterDocs AI...', 'betterdocs' ); ?></p>
91 </div>
92
93 <!-- Results State (always rendered, hidden if no results) -->
94 <div class="quality-results" style="<?php echo $has_results ? '' : 'display: none;'; ?>">
95 <?php if ( $has_results ) : ?>
96 <?php $this->render_quality_results( $cached_analysis ); ?>
97 <?php endif; ?>
98 </div>
99
100 <!-- Empty State -->
101 <div class="quality-empty" style="<?php echo $has_results ? 'display: none;' : ''; ?>">
102 <h4><?php esc_html_e( 'Docs quality analysis', 'betterdocs' ); ?></h4>
103 <p><?php esc_html_e( 'Analyze your docs quality with BetterDocs AI to get actionable insights and improvements', 'betterdocs' ); ?></p>
104 <span class="betterdocs-analyze-btn" id="betterdocs-analyze-quality">
105 <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
106 <g clip-path="url(#clip0_13783_29413)">
107 <g filter="url(#filter0_d_13783_29413)">
108 <path d="M3.32812 10.2207C3.32812 9.93383 3.56132 9.70404 3.84675 9.67574C6.30094 9.43242 8.2527 7.48064 8.49602 5.02643C8.52431 4.741 8.75411 4.50781 9.04093 4.50781C9.32776 4.50781 9.55755 4.741 9.58585 5.02643C9.82917 7.48064 11.7809 9.43242 14.2351 9.67574C14.5205 9.70404 14.7537 9.93383 14.7537 10.2207C14.7537 10.5075 14.5205 10.7373 14.2351 10.7656C11.7809 11.0089 9.82917 12.9607 9.58585 15.4149C9.55755 15.7003 9.32776 15.9335 9.04093 15.9335C8.75411 15.9335 8.52431 15.7003 8.49602 15.4149C8.2527 12.9607 6.30094 11.0089 3.84675 10.7656C3.56132 10.7373 3.32812 10.5075 3.32812 10.2207Z" fill="url(#paint0_linear_13783_29413)"></path>
109 </g>
110 <g filter="url(#filter1_d_13783_29413)">
111 <path d="M13.6133 3.93261C13.6133 3.84656 13.6832 3.77762 13.7689 3.76913C14.5051 3.69613 15.0907 3.1106 15.1636 2.37434C15.1721 2.28871 15.2411 2.21875 15.3271 2.21875C15.4132 2.21875 15.4821 2.28871 15.4906 2.37434C15.5636 3.1106 16.1491 3.69613 16.8854 3.76913C16.971 3.77762 17.041 3.84656 17.041 3.93261C17.041 4.01865 16.971 4.08759 16.8854 4.09608C16.1491 4.16908 15.5636 4.75462 15.4906 5.49088C15.4821 5.5765 15.4132 5.64646 15.3271 5.64646C15.2411 5.64646 15.1721 5.5765 15.1636 5.49088C15.0907 4.75462 14.5051 4.16908 13.7689 4.09608C13.6832 4.08759 13.6133 4.01865 13.6133 3.93261Z" fill="url(#paint1_linear_13783_29413)"></path>
112 </g>
113 <g filter="url(#filter2_d_13783_29413)">
114 <path d="M12.4727 15.9336C12.4727 15.8188 12.5659 15.7269 12.6801 15.7156C13.6618 15.6183 14.4425 14.8376 14.5398 13.8559C14.5511 13.7417 14.643 13.6484 14.7578 13.6484C14.8725 13.6484 14.9644 13.7417 14.9757 13.8559C15.0731 14.8376 15.8538 15.6183 16.8355 15.7156C16.9496 15.7269 17.0429 15.8188 17.0429 15.9336C17.0429 16.0483 16.9496 16.1402 16.8355 16.1515C15.8538 16.2489 15.0731 17.0296 14.9757 18.0113C14.9644 18.1254 14.8725 18.2187 14.7578 18.2187C14.643 18.2187 14.5511 18.1254 14.5398 18.0113C14.4425 17.0296 13.6618 16.2489 12.6801 16.1515C12.5659 16.1402 12.4727 16.0483 12.4727 15.9336Z" fill="url(#paint2_linear_13783_29413)"></path>
115 </g>
116 </g>
117 <defs>
118 <filter id="filter0_d_13783_29413" x="-1.24214" y="3.36525" width="20.5663" height="20.5624" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
119 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
120 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
121 <feOffset dy="3.4277"></feOffset>
122 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
123 <feComposite in2="hardAlpha" operator="out"></feComposite>
124 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
125 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
126 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
127 </filter>
128 <filter id="filter1_d_13783_29413" x="9.04302" y="1.07618" width="12.5663" height="12.5702" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
129 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
130 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
131 <feOffset dy="3.4277"></feOffset>
132 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
133 <feComposite in2="hardAlpha" operator="out"></feComposite>
134 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
135 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
136 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
137 </filter>
138 <filter id="filter2_d_13783_29413" x="7.90239" y="12.5059" width="13.7108" height="13.7108" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
139 <feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood>
140 <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix>
141 <feOffset dy="3.4277"></feOffset>
142 <feGaussianBlur stdDeviation="2.28513"></feGaussianBlur>
143 <feComposite in2="hardAlpha" operator="out"></feComposite>
144 <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.00392157 0 0 0 0 0.137255 0 0 0 0.13 0"></feColorMatrix>
145 <feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_13783_29413"></feBlend>
146 <feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_13783_29413" result="shape"></feBlend>
147 </filter>
148 <linearGradient id="paint0_linear_13783_29413" x1="9.04093" y1="4.50781" x2="9.04093" y2="15.9335" gradientUnits="userSpaceOnUse">
149 <stop stop-color="#CD93FF"></stop>
150 <stop offset="1" stop-color="#A741FF"></stop>
151 </linearGradient>
152 <linearGradient id="paint1_linear_13783_29413" x1="15.3271" y1="2.21875" x2="15.3271" y2="5.64646" gradientUnits="userSpaceOnUse">
153 <stop stop-color="#D099FF"></stop>
154 <stop offset="1" stop-color="#CE96FF"></stop>
155 </linearGradient>
156 <linearGradient id="paint2_linear_13783_29413" x1="14.7578" y1="13.6484" x2="14.7578" y2="18.2187" gradientUnits="userSpaceOnUse">
157 <stop stop-color="#D099FF"></stop>
158 <stop offset="1" stop-color="#CE96FF"></stop>
159 </linearGradient>
160 <clipPath id="clip0_13783_29413">
161 <rect width="20" height="20" fill="white"></rect>
162 </clipPath>
163 </defs>
164 </svg>
165 <?php esc_html_e( 'Analyze with BetterDocs AI', 'betterdocs' ); ?>
166 </span>
167 </div>
168
169 <!-- Error State -->
170 <div class="quality-error" style="display: none;">
171 <div class="error-content">
172 <div class="error-icon">
173 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings w-6 h-6" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
174 </div>
175 <div class="error-text">
176 <strong><?php esc_html_e( 'Configuration Required', 'betterdocs' ); ?></strong>
177 <div class="error-message"></div>
178 <a href="<?php echo esc_url( admin_url( 'admin.php?page=betterdocs-settings#betterdocs-ai' ) ); ?>" class="error-action-button" style="display: none;">
179 <?php esc_html_e( 'Go to Settings', 'betterdocs' ); ?>
180 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right" aria-hidden="true"><path d="m9 18 6-6-6-6"></path></svg>
181 </a>
182 </div>
183 </div>
184 </div>
185 </div>
186 <?php
187 }
188
189 /**
190 * Render quality results
191 *
192 * @param array $analysis Analysis data
193 */
194 private function render_quality_results( $analysis ) {
195 $overall_score = $analysis['overall_score'] ?? 0;
196 $detailed_scores = $analysis['detailed_scores'] ?? $analysis['scores'] ?? [];
197 $suggestions = $analysis['suggestions'] ?? [];
198
199 // Extract suggestions from feedback if available
200 if ( empty( $suggestions ) && isset( $analysis['feedback'] ) ) {
201 $feedback = $analysis['feedback'];
202 if ( isset( $feedback['strengths'] ) && is_array( $feedback['strengths'] ) ) {
203 $suggestions = array_merge( $suggestions, $feedback['strengths'] );
204 }
205 if ( isset( $feedback['improvements'] ) && is_array( $feedback['improvements'] ) ) {
206 $suggestions = array_merge( $suggestions, $feedback['improvements'] );
207 }
208 if ( isset( $feedback['suggestions'] ) && is_array( $feedback['suggestions'] ) ) {
209 $suggestions = array_merge( $suggestions, $feedback['suggestions'] );
210 }
211 }
212
213 $score_color = $this->get_score_status_class( $overall_score );
214 $circumference = 2 * M_PI * 54; // radius = 54
215 $offset = $circumference - ( $overall_score / 100 ) * $circumference;
216 ?>
217
218 <div class="quality-results-header">
219 <h4><?php esc_html_e( 'Docs quality analysis results', 'betterdocs' ); ?></h4>
220 <button type="button" class="betterdocs-regenerate-quality" id="betterdocs-regenerate-quality">
221 <svg
222 width="16"
223 height="22"
224 viewBox="0 0 16 22"
225 fill="none"
226 xmlns="http://www.w3.org/2000/svg"
227 >
228 <path
229 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"
230 fill="#475467"
231 />
232 </svg>
233 </button>
234 </div>
235
236 <div class="quality-results-body">
237 <!-- Left Column: Circular Progress Score -->
238 <div class="score-column">
239 <h3 class="score-column-title"><?php esc_html_e( 'Overall Score', 'betterdocs' ); ?></h3>
240 <div class="progress-ring-wrapper">
241 <svg class="progress-ring" viewBox="0 0 120 120">
242 <circle class="progress-ring-bg" cx="60" cy="60" r="54" fill="none" stroke-width="12"></circle>
243 <circle class="progress-ring-circle <?php echo esc_attr( $score_color ); ?>" cx="60" cy="60" r="54" fill="none" stroke-width="12" stroke-linecap="round" style="stroke-dasharray: <?php echo esc_attr( $circumference ); ?>; stroke-dashoffset: <?php echo esc_attr( $offset ); ?>; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 0.5s ease-in-out;"></circle>
244 </svg>
245 <div class="score-text-wrapper">
246 <span class="score-value <?php echo esc_attr( $score_color ); ?>"><?php echo esc_html( $overall_score ); ?></span>
247 <span class="score-label"><?php esc_html_e( 'out of 100', 'betterdocs' ); ?></span>
248 </div>
249 </div>
250 <div class="score-feedback <?php echo esc_attr( $score_color ); ?>">
251 <?php echo esc_html( $this->get_score_status_text( $overall_score ) ); ?>
252 </div>
253 <p class="score-description">
254 <?php esc_html_e( 'This score is a weighted average of clarity, relevance, structure, and other quality factors.', 'betterdocs' ); ?>
255 </p>
256 </div>
257
258 <!-- Right Column: Score Breakdown & Suggestions -->
259 <div class="details-column">
260 <?php if ( ! empty( $detailed_scores ) ) : ?>
261 <div class="section">
262 <h3 class="section-title"><?php esc_html_e( 'Score Breakdown', 'betterdocs' ); ?></h3>
263 <div class="score-breakdown-list">
264 <?php foreach ( $detailed_scores as $criterion => $score ) : ?>
265 <div class="score-item">
266 <div class="score-name-group">
267 <span class="score-name"><?php echo esc_html( ucfirst( $criterion ) ); ?></span>
268 <span class="score-percentage"><?php echo esc_html( $score ); ?>%</span>
269 </div>
270 <div class="score-bar-group">
271 <div class="score-bar-wrapper">
272 <div class="score-bar-fill <?php echo esc_attr( $this->get_score_status_class( $score ) ); ?>" style="width: <?php echo esc_attr( $score ); ?>%"></div>
273 </div>
274
275 </div>
276 </div>
277 <?php endforeach; ?>
278 </div>
279 </div>
280 <?php endif; ?>
281 </div>
282 </div>
283 <?php if ( ! empty( $suggestions ) ) : ?>
284 <div class="improvement-section section">
285 <h3 class="section-title"><?php esc_html_e( 'Actionable Insights', 'betterdocs' ); ?></h3>
286 <div class="improvements-list">
287 <?php foreach ( $suggestions as $suggestion ) : ?>
288 <div class="improvement-item">
289 <div class="improvement-icon">
290 <svg
291 width="16"
292 height="16"
293 viewBox="0 0 16 16"
294 fill="none"
295 xmlns="http://www.w3.org/2000/svg"
296 >
297 <rect width="16" height="16" rx="8" fill="#D0D5DD" />
298 <path
299 d="M5 8.88L6.67619 10.8L10.8667 6"
300 stroke="white"
301 strokeWidth="1.2"
302 strokeLinecap="round"
303 strokeLinejoin="round"
304 />
305 </svg>
306 </div>
307 <span class="improvement-text"><?php echo esc_html( $suggestion ); ?></span>
308 </div>
309 <?php endforeach; ?>
310 </div>
311 </div>
312 <?php endif; ?>
313 <?php
314 }
315
316
317
318
319
320 /**
321 * Get CSS class for score status
322 *
323 * @param int $score Overall score
324 * @return string CSS class
325 */
326 private function get_score_status_class( $score ) {
327 if ( $score >= 80 ) return 'green';
328 if ( $score >= 50 ) return 'yellow';
329 if ( $score >= 30 ) return 'orange';
330 return 'red';
331 }
332
333 /**
334 * Get text for score status
335 *
336 * @param int $score Overall score
337 * @return string Status text
338 */
339 private function get_score_status_text( $score ) {
340 if ( $score >= 80 ) return __( 'Excellent', 'betterdocs' );
341 if ( $score >= 60 ) return __( 'Good', 'betterdocs' );
342 if ( $score >= 40 ) return __( 'Needs Improvement', 'betterdocs' );
343 return __( 'Poor', 'betterdocs' );
344 }
345
346 /**
347 * AJAX handler for docs quality analysis
348 */
349 public function ajax_analyze_article_quality() {
350 // Verify nonce
351 if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'betterdocs_quality_score_nonce' ) ) {
352 wp_send_json_error( [ 'message' => __( 'Invalid nonce', 'betterdocs' ) ] );
353 }
354
355 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
356
357 if ( empty( $post_id ) ) {
358 wp_send_json_error( [ 'message' => __( 'Invalid post ID', 'betterdocs' ) ] );
359 }
360
361 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
362 if ( ! current_user_can( 'edit_post', $post_id ) ) {
363 wp_send_json_error( [ 'message' => __( 'Insufficient permissions', 'betterdocs' ) ] );
364 }
365
366 $post = get_post( $post_id );
367
368 if ( ! $post || $post->post_type !== 'docs' ) {
369 wp_send_json_error( [ 'message' => __( 'Invalid post or post type', 'betterdocs' ) ] );
370 }
371
372 // Check if current content is provided (from editor, may be unsaved)
373 $current_content = isset( $_POST['current_content'] ) ? wp_kses_post( $_POST['current_content'] ) : '';
374 $current_title = isset( $_POST['current_title'] ) ? sanitize_text_field( $_POST['current_title'] ) : '';
375
376 // Use current content if provided, otherwise use saved post content
377 if ( ! empty( $current_content ) ) {
378 $content = $current_content;
379 $title = ! empty( $current_title ) ? $current_title : $post->post_title;
380 } else {
381 $content = $post->post_content;
382 $title = $post->post_title;
383 }
384
385 if ( empty( $content ) ) {
386 wp_send_json_error( [ 'message' => __( 'Post content is empty. Please add content to your post and try again.', 'betterdocs' ) ] );
387 }
388
389 // Strip HTML tags and shortcodes for analysis
390 $clean_content = wp_strip_all_tags( do_shortcode( $content ) );
391 $clean_content = preg_replace( '/\s+/', ' ', trim( $clean_content ) );
392
393 if ( strlen( $clean_content ) < 50 ) {
394 wp_send_json_error( [ 'message' => __( 'Content is too short for meaningful analysis. Please add more content (at least 50 characters) and try again.', 'betterdocs' ) ] );
395 }
396
397 // Perform AI analysis
398 $analysis_result = $this->ai_helper->analyze_article_quality( $clean_content, $title );
399
400 if ( is_wp_error( $analysis_result ) ) {
401 wp_send_json_error( [ 'message' => $analysis_result->get_error_message() ] );
402 }
403
404 // Save the analysis result
405 $saved = $this->ai_helper->save_article_quality_score( $post_id, $analysis_result );
406
407 if ( ! $saved ) {
408 wp_send_json_error( [ 'message' => __( 'Failed to save analysis result', 'betterdocs' ) ] );
409 }
410
411 // Return success with the analysis data
412 wp_send_json_success( [
413 'message' => __( 'Docs analyzed successfully', 'betterdocs' ),
414 'data' => $analysis_result
415 ] );
416 }
417
418 /**
419 * AJAX handler for saving quality analysis results
420 */
421 public function ajax_save_quality_analysis() {
422 // Verify nonce
423 if ( ! wp_verify_nonce( $_POST['nonce'], 'betterdocs_quality_score_nonce' ) ) {
424 wp_send_json_error( __( 'Security check failed', 'betterdocs' ) );
425 }
426
427 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
428 $analysis_data = isset( $_POST['analysis_data'] ) ? $_POST['analysis_data'] : '';
429
430 if ( ! $post_id || ! $analysis_data ) {
431 wp_send_json_error( __( 'Invalid data provided', 'betterdocs' ) );
432 }
433
434 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
435 if ( ! current_user_can( 'edit_post', $post_id ) ) {
436 wp_send_json_error( __( 'Insufficient permissions', 'betterdocs' ) );
437 }
438
439 // Decode the analysis data
440 $decoded_data = json_decode( stripslashes( $analysis_data ), true );
441 if ( ! $decoded_data ) {
442 wp_send_json_error( __( 'Invalid analysis data format', 'betterdocs' ) );
443 }
444
445 // Add timestamp and post modified date to the analysis data
446 $post = get_post( $post_id );
447 $decoded_data['analysis_timestamp'] = current_time( 'timestamp' );
448 $decoded_data['post_modified'] = $post->post_modified;
449
450 // Save the analysis data as post meta
451 $meta_key = '_betterdocs_article_quality_analysis';
452 $saved = update_post_meta( $post_id, $meta_key, $decoded_data );
453
454 if ( $saved !== false ) {
455 wp_send_json_success( [
456 'message' => __( 'Analysis results saved successfully', 'betterdocs' ),
457 'data' => $decoded_data
458 ] );
459 } else {
460 wp_send_json_error( __( 'Failed to save analysis results', 'betterdocs' ) );
461 }
462 }
463
464 /**
465 * AJAX handler for checking cached analysis results
466 */
467 public function ajax_check_cached_analysis() {
468 // Verify nonce
469 if ( ! wp_verify_nonce( $_POST['nonce'], 'betterdocs_quality_score_nonce' ) ) {
470 wp_send_json_error( __( 'Security check failed', 'betterdocs' ) );
471 }
472
473 $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
474
475 if ( ! $post_id ) {
476 wp_send_json_error( __( 'Invalid post ID', 'betterdocs' ) );
477 }
478
479 // SECURITY: Check if user can edit THIS SPECIFIC post (prevents IDOR)
480 if ( ! current_user_can( 'edit_post', $post_id ) ) {
481 wp_send_json_error( __( 'Insufficient permissions', 'betterdocs' ) );
482 }
483
484 // Get the post and its last modified time
485 $post = get_post( $post_id );
486 if ( ! $post ) {
487 wp_send_json_error( __( 'Post not found', 'betterdocs' ) );
488 }
489
490 // Get cached analysis data
491 $cached_analysis = get_post_meta( $post_id, '_betterdocs_article_quality_analysis', true );
492
493 // Check if we have cached data and if the post hasn't been modified since analysis
494 if ( $cached_analysis &&
495 isset( $cached_analysis['post_modified'] ) &&
496 $cached_analysis['post_modified'] === $post->post_modified ) {
497
498 // Return cached data
499 wp_send_json_success( [
500 'has_cache' => true,
501 'message' => __( 'Using cached analysis results', 'betterdocs' ),
502 'data' => $cached_analysis
503 ] );
504 } else {
505 // No cache or post has been updated
506 wp_send_json_success( [
507 'has_cache' => false,
508 'message' => __( 'No valid cache found, new analysis required', 'betterdocs' )
509 ] );
510 }
511 }
512
513 }
514