| 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 |
|