PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.6
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.6
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 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Utils/AIHelper.php +220 -571 4.5.6 → 4.2.6 View file →
@@ -5,612 +5,261 @@
5 5 use WPDeveloper\BetterDocs\Core\Settings;
6 6
7 7 class AIHelper {
8 8
9 - /**
10 - * Settings instance
11 - *
12 - * @var Settings
13 - */
14 - private $settings;
9 + /**
10 + * Settings instance
11 + *
12 + * @var Settings
13 + */
14 + private $settings;
15 15
16 - public function __construct( Settings $settings ) {
17 - $this->settings = $settings;
18 - }
16 + public function __construct( Settings $settings ) {
17 + $this->settings = $settings;
18 + }
19 19
20 - /**
21 - * Get OpenAI API key from settings
22 - *
23 - * @return string
24 - */
25 - public function get_api_key() {
26 - return $this->settings->get( 'ai_autowrite_api_key', '' );
27 - }
20 + /**
21 + * Get OpenAI API key from settings
22 + *
23 + * @return string
24 + */
25 + public function get_api_key() {
26 + return $this->settings->get( 'ai_autowrite_api_key', '' );
27 + }
28 28
29 - /**
30 - * Check if OpenAI API key is configured
31 - *
32 - * @return bool
33 - */
34 - public function has_api_key() {
35 - $api_key = $this->get_api_key();
36 - return ! empty( $api_key );
37 - }
29 + /**
30 + * Check if OpenAI API key is configured
31 + *
32 + * @return bool
33 + */
34 + public function has_api_key() {
35 + $api_key = $this->get_api_key();
36 + return ! empty( $api_key );
37 + }
38 38
39 - /**
40 - * Validate OpenAI API key
41 - *
42 - * @param string $api_key Optional API key to validate, uses stored key if not provided
43 - * @return array Array with 'valid' boolean and 'message' string
44 - */
45 - public function validate_api_key( $api_key = '' ) {
46 - if ( empty( $api_key ) ) {
47 - $api_key = $this->get_api_key();
48 - }
39 + /**
40 + * Validate OpenAI API key
41 + *
42 + * @param string $api_key Optional API key to validate, uses stored key if not provided
43 + * @return array Array with 'valid' boolean and 'message' string
44 + */
45 + public function validate_api_key( $api_key = '' ) {
46 + if ( empty( $api_key ) ) {
47 + $api_key = $this->get_api_key();
48 + }
49 49
50 - if ( empty( $api_key ) ) {
51 - return array(
52 - 'valid' => false,
53 - 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings#betterdocs-ai">OpenAI API Key</a> to use AI features.'
54 - );
55 - }
50 + if ( empty( $api_key ) ) {
51 + return [
52 + 'valid' => false,
53 + 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings">OpenAI API Key</a> to use AI features.'
54 + ];
55 + }
56 56
57 - $ch = curl_init( 'https://api.openai.com/v1/models' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
58 - curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
59 - curl_setopt( //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
60 - $ch,
61 - CURLOPT_HTTPHEADER,
62 - array(
63 - 'Content-Type: application/json',
64 - 'Authorization: Bearer ' . $api_key
65 - )
66 - );
57 + $ch = curl_init( 'https://api.openai.com/v1/models' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
58 + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
59 + curl_setopt( //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
60 + $ch,
61 + CURLOPT_HTTPHEADER,
62 + [
63 + 'Content-Type: application/json',
64 + 'Authorization: Bearer ' . $api_key,
65 + ]
66 + );
67 67
68 - $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
69 - $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
70 - curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
68 + $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
69 + $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
70 + curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
71 71
72 - if ( 200 == $httpCode ) {
73 - return array(
74 - 'valid' => true,
75 - 'message' => 'Valid API Key'
76 - );
77 - } else {
78 - $responseData = json_decode( $response, true );
79 - $messageData = $responseData[ 'error' ] ?? '';
80 - return array(
81 - 'valid' => false,
82 - 'message' => $messageData[ 'message' ] ?? 'Invalid API Key'
83 - );
84 - }
85 - }
72 + if ( $httpCode == 200 ) {
73 + return [
74 + 'valid' => true,
75 + 'message' => 'Valid API Key'
76 + ];
77 + } else {
78 + $responseData = json_decode( $response, true );
79 + $messageData = $responseData['error'] ?? '';
80 + return [
81 + 'valid' => false,
82 + 'message' => $messageData['message'] ?? 'Invalid API Key'
83 + ];
84 + }
85 + }
86 86
87 - /**
88 - * Minimum token policy by (feature context, model family). Used as the
89 - * single source of truth for:
90 - * - server-side save validation (Core/Settings.php)
91 - * - field UI props sent to the React notice (Core/Settings.php)
92 - * - runtime payload floor in build_openai_payload()
93 - *
94 - * Override the whole map (or any cell) via the `betterdocs_ai_min_tokens`
95 - * filter. Returns 0 when no minimum applies (unknown context or model).
96 - *
97 - * @param string $context Feature key, e.g. 'write_with_ai' or 'article_summary'.
98 - * @param string $model OpenAI model identifier.
99 - * @return int Minimum recommended max_tokens for that pair.
100 - */
101 - public static function get_min_tokens( $context, $model ) {
102 - $family = self::token_family( $model );
103 - $map = apply_filters( 'betterdocs_ai_min_tokens', array(
104 - 'write_with_ai' => array( 'gpt-4' => 2500, 'gpt-5' => 4500, 'gpt-5.5' => 10000 ),
105 - 'article_summary' => array( 'gpt-4' => 1500, 'gpt-5' => 2500, 'gpt-5.5' => 10000 ),
106 - ) );
107 - return isset( $map[ $context ][ $family ] ) ? (int) $map[ $context ][ $family ] : 0;
108 - }
87 + /**
88 + * Make a request to OpenAI API
89 + *
90 + * @param array $messages Array of messages for the chat completion
91 + * @param array $options Optional parameters (model, max_tokens, temperature, etc.)
92 + * @return string|\WP_Error API response content or error
93 + */
94 + public function make_openai_request( $messages, $options = [] ) {
95 + $api_key = $this->get_api_key();
96 + $max_tokens = $this->settings->get( 'article_summary_max_token', 1500 );
97 + $model = $this->settings->get( 'article_summary_model', 'gpt-4o-mini' );
109 98
110 - /**
111 - * Map a model id to its token-floor family key.
112 - *
113 - * gpt-5.x point releases (gpt-5.5, gpt-5.1, ...) generate much larger, slower
114 - * responses and need a heavier floor than the base gpt-5 family, so they get
115 - * their own 'gpt-5.5' key. Plain gpt-5* stays 'gpt-5'; everything else 'gpt-4'.
116 - *
117 - * @param string $model OpenAI model identifier.
118 - * @return string Family key used in the min-token map.
119 - */
120 - private static function token_family( $model ) {
121 - if ( self::is_gpt5_point_release( $model ) ) {
122 - return 'gpt-5.5';
123 - }
124 - return ( 0 === strpos( (string) $model, 'gpt-5' ) ) ? 'gpt-5' : 'gpt-4';
125 - }
99 + if ( empty( $api_key ) ) {
100 + return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured.' );
101 + }
126 102
127 - /**
128 - * Whether a model is a gpt-5.x point release (gpt-5.5, gpt-5.1, ...), which
129 - * use the newer reasoning_effort vocabulary and need a heavier token floor.
130 - *
131 - * @param string $model OpenAI model identifier.
132 - * @return bool
133 - */
134 - private static function is_gpt5_point_release( $model ) {
135 - return (bool) preg_match( '/^gpt-5\.\d/', (string) $model );
136 - }
103 + // Default options
104 + $defaults = [
105 + 'model' => $model,
106 + 'max_tokens' => $max_tokens,
107 + 'temperature' => 0.7,
108 + 'timeout' => 50
109 + ];
137 110
138 - /**
139 - * Return the threshold map for one context, in {family => min} shape, so
140 - * Settings.php can serialize it onto a field for the React notice to read.
141 - *
142 - * @param string $context Feature key.
143 - * @return array<string,int>
144 - */
145 - public static function get_min_tokens_map( $context ) {
146 - return array(
147 - 'gpt-4' => self::get_min_tokens( $context, 'gpt-4o' ),
148 - 'gpt-5' => self::get_min_tokens( $context, 'gpt-5' ),
149 - 'gpt-5.5' => self::get_min_tokens( $context, 'gpt-5.5' ),
150 - );
151 - }
111 + $options = wp_parse_args( $options, $defaults );
152 112
153 - /**
154 - * Build an OpenAI Chat Completions request body, switching parameter shape
155 - * for model families that reject the legacy max_tokens / custom temperature.
156 - *
157 - * GPT-5 family requires max_completion_tokens and rejects any non-default
158 - * temperature, so we omit both. It is also a reasoning model: internal
159 - * reasoning tokens are billed against max_completion_tokens before any
160 - * visible output is produced, so we send a low reasoning_effort by default
161 - * (see default_reasoning_effort()). Without that the model can spend the
162 - * entire budget on reasoning and return empty content with
163 - * finish_reason=length.
164 - *
165 - * When `$context` is provided we also raise `$max_tokens` to the per-family
166 - * minimum from get_min_tokens(), so the request never goes out below the
167 - * policy floor regardless of what's stored in settings.
168 - *
169 - * @param string $model OpenAI model identifier (e.g. 'gpt-4o', 'gpt-5-mini').
170 - * @param array $messages Chat messages array.
171 - * @param int $max_tokens Token cap (will be raised to feature minimum if $context is set).
172 - * @param float|null $temperature Optional sampling temperature; ignored for gpt-5*.
173 - * @param string|null $context Feature key for runtime min-token enforcement. Pass null for back-compat.
174 - * @return array Request body ready to JSON-encode.
175 - */
176 - public static function build_openai_payload( $model, $messages, $max_tokens, $temperature = null, $context = null ) {
177 - if ( null !== $context ) {
178 - $min = self::get_min_tokens( $context, $model );
179 - if ( $min > 0 && (int) $max_tokens < $min ) {
180 - $max_tokens = $min;
181 - }
182 - }
113 + $api_endpoint = 'https://api.openai.com/v1/chat/completions';
183 114
184 - $payload = array(
185 - 'model' => $model,
186 - 'messages' => $messages,
187 - );
115 + $request_body = [
116 + 'model' => $options['model'],
117 + 'messages' => $messages,
118 + 'max_tokens' => $options['max_tokens'],
119 + 'temperature' => $options['temperature']
120 + ];
188 121
189 - if ( 0 === strpos( $model, 'gpt-5' ) ) {
190 - $payload['max_completion_tokens'] = $max_tokens;
191 - $payload['reasoning_effort'] = apply_filters( 'betterdocs_openai_gpt5_reasoning_effort', self::default_reasoning_effort( $model ), $model, $max_tokens );
192 - return $payload;
193 - }
122 + $request_options = [
123 + 'headers' => [
124 + 'Content-Type' => 'application/json',
125 + 'Authorization' => 'Bearer ' . $api_key,
126 + ],
127 + 'body' => json_encode( $request_body ), //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
128 + 'timeout' => $options['timeout'],
129 + ];
194 130
195 - $payload['max_tokens'] = $max_tokens;
196 - if ( null !== $temperature ) {
197 - $payload['temperature'] = $temperature;
198 - }
199 - return $payload;
200 - }
131 + $response = wp_remote_post( $api_endpoint, $request_options );
201 132
202 - /**
203 - * Default reasoning_effort for a gpt-5* model.
204 - *
205 - * The original GPT-5 generation (gpt-5, gpt-5-mini, gpt-5-nano) accepts
206 - * 'minimal'. The gpt-5.x point releases (e.g. gpt-5.5) dropped 'minimal'
207 - * from the API and only accept none|low|medium|high|xhigh; sending
208 - * 'minimal' returns a 400 "Unsupported value: 'reasoning_effort'". For
209 - * those we default to 'none' — no reasoning tokens, which is the fastest
210 - * option and leaves the whole token budget for visible output (the closest
211 - * equivalent to the gpt-5 'minimal' behaviour). Override per model via the
212 - * betterdocs_openai_gpt5_reasoning_effort filter.
213 - *
214 - * @param string $model OpenAI model identifier.
215 - * @return string reasoning_effort value.
216 - */
217 - private static function default_reasoning_effort( $model ) {
218 - // Point releases like gpt-5.5 use the new vocabulary; plain gpt-5* keep 'minimal'.
219 - if ( self::is_gpt5_point_release( $model ) ) {
220 - return 'none';
221 - }
222 - return 'minimal';
223 - }
133 + if ( is_wp_error( $response ) ) {
134 + return new \WP_Error( 'api_error', 'Failed to connect to OpenAI API: ' . $response->get_error_message() );
135 + }
224 136
225 - /**
226 - * Make a request to OpenAI API
227 - *
228 - * @param array $messages Array of messages for the chat completion
229 - * @param array $options Optional parameters (model, max_tokens, temperature, etc.)
230 - * @return string|\WP_Error API response content or error
231 - */
232 - public function make_openai_request( $messages, $options = array() ) {
233 - $api_key = $this->get_api_key();
234 - $max_tokens = $this->settings->get( 'article_summary_max_token', 1500 );
235 - $model = $this->settings->get( 'article_summary_model', 'gpt-4o-mini' );
137 + $body = wp_remote_retrieve_body( $response );
138 + $data = json_decode( $body, true );
236 139
237 - if ( empty( $api_key ) ) {
238 - return new \WP_Error( 'no_api_key', 'OpenAI API key is not configured.' );
239 - }
140 + if ( ! empty( $data['error'] ) ) {
141 + return new \WP_Error( 'openai_error', $data['error']['message'] );
142 + }
240 143
241 - // Default options
242 - $defaults = array(
243 - 'model' => $model,
244 - 'max_tokens' => $max_tokens,
245 - 'temperature' => 0.7,
246 - 'timeout' => 50
247 - );
144 + if ( empty( $data['choices'][0]['message']['content'] ) ) {
145 + return new \WP_Error( 'no_content', 'No content received from OpenAI.' );
146 + }
248 147
249 - $options = wp_parse_args( $options, $defaults );
148 + return $data['choices'][0]['message']['content'];
149 + }
250 150
251 - $api_endpoint = 'https://api.openai.com/v1/chat/completions';
151 + /**
152 + * Create a system message for OpenAI
153 + *
154 + * @param string $content System message content
155 + * @return array Message array
156 + */
157 + public function create_system_message( $content ) {
158 + return [
159 + 'role' => 'system',
160 + 'content' => $content
161 + ];
162 + }
252 163
253 - $request_body = self::build_openai_payload(
254 - $options[ 'model' ],
255 - $messages,
256 - $options[ 'max_tokens' ],
257 - $options[ 'temperature' ],
258 - 'article_summary'
259 - );
164 + /**
165 + * Create a user message for OpenAI
166 + *
167 + * @param string $content User message content
168 + * @return array Message array
169 + */
170 + public function create_user_message( $content ) {
171 + return [
172 + 'role' => 'user',
173 + 'content' => $content
174 + ];
175 + }
260 176
261 - $request_options = array(
262 - 'headers' => array(
263 - 'Content-Type' => 'application/json',
264 - 'Authorization' => 'Bearer ' . $api_key
265 - ),
266 - 'body' => json_encode( $request_body ), //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
267 - 'timeout' => $options[ 'timeout' ]
268 - );
177 + /**
178 + * Create messages array for article summarization
179 + *
180 + * @param string $title Article title
181 + * @param string $content Article content
182 + * @return array Messages array
183 + */
184 + public function create_summary_messages( $title, $content ) {
185 + $system_message = $this->create_system_message(
186 + 'You are a helpful assistant that creates concise, informative summaries of documentation articles. Always format your response in clean HTML with paragraph tags. Do not use markdown formatting, code blocks, or backticks. Return only the HTML content without any wrapper formatting.'
187 + );
269 188
270 - $response = wp_remote_post( $api_endpoint, $request_options );
189 + $user_prompt = "Please provide a concise summary of the following article titled '{$title}'. The summary should be 2-3 paragraphs long, highlighting the main points and key takeaways. Format the response in HTML with proper paragraph tags. Do not wrap the response in markdown code blocks or use any markdown formatting.\n\nArticle content:\n{$content}";
271 190
272 - if ( is_wp_error( $response ) ) {
273 - return new \WP_Error( 'api_error', 'Failed to connect to OpenAI API: ' . $response->get_error_message() );
274 - }
191 + $user_message = $this->create_user_message( $user_prompt );
275 192
276 - $body = wp_remote_retrieve_body( $response );
277 - $data = json_decode( $body, true );
193 + return [ $system_message, $user_message ];
194 + }
278 195
279 - if ( ! empty( $data[ 'error' ] ) ) {
280 - return new \WP_Error( 'openai_error', $data[ 'error' ][ 'message' ] );
281 - }
196 + /**
197 + * Create messages array for content generation
198 + *
199 + * @param string $prompt User prompt
200 + * @param string $keywords Optional keywords
201 + * @return array Messages array
202 + */
203 + public function create_content_messages( $prompt, $keywords = '' ) {
204 + $system_message = $this->create_system_message(
205 + 'You are a helpful assistant who writes documentation for users.'
206 + );
282 207
283 - if ( empty( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ) {
284 - return new \WP_Error( 'no_content', 'No content received from OpenAI.' );
285 - }
208 + $user_message = $this->create_user_message( $prompt );
286 209
287 - return $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ];
288 - }
210 + return [ $system_message, $user_message ];
211 + }
289 212
290 - /**
291 - * Analyze article quality using OpenAI
292 - *
293 - * @param string $content Article content to analyze
294 - * @param string $title Article title (optional)
295 - * @return array|\WP_Error Analysis result with score and feedback
296 - */
297 - public function analyze_article_quality( $content, $title = '' ) {
298 - if ( empty( $content ) ) {
299 - return new \WP_Error( 'empty_content', 'Article content cannot be empty.' );
300 - }
213 + /**
214 + * Sanitize and prepare content for AI processing
215 + *
216 + * @param string $content Raw content
217 + * @param int $max_length Maximum length to keep
218 + * @return string Sanitized content
219 + */
220 + public function prepare_content_for_ai( $content, $max_length = 4000 ) {
221 + // Strip HTML tags and decode entities
222 + $content = wp_strip_all_tags( $content );
223 + $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
301 224
302 - // Create analysis prompt
303 - $prompt = $this->build_quality_analysis_prompt( $content, $title );
225 + // Remove extra whitespace
226 + $content = preg_replace( '/\s+/', ' ', $content );
227 + $content = trim( $content );
304 228
305 - $messages = array(
306 - array(
307 - 'role' => 'system',
308 - 'content' => 'You are an expert content analyst specializing in documentation quality assessment. Provide detailed, actionable feedback to help improve article quality.'
309 - ),
310 - array(
311 - 'role' => 'user',
312 - 'content' => $prompt
313 - )
314 - );
229 + // Limit length
230 + if ( strlen( $content ) > $max_length ) {
231 + $content = substr( $content, 0, $max_length );
232 + // Try to cut at a word boundary
233 + $last_space = strrpos( $content, ' ' );
234 + if ( $last_space !== false && $last_space > $max_length * 0.8 ) {
235 + $content = substr( $content, 0, $last_space );
236 + }
237 + $content .= '...';
238 + }
315 239
316 - $options = array(
317 - 'max_tokens' => 2000,
318 - 'temperature' => 0.3 // Lower temperature for more consistent analysis
319 - );
240 + return $content;
241 + }
320 242
321 - $response = $this->make_openai_request( $messages, $options );
243 + /**
244 + * Check if AI features are enabled
245 + *
246 + * @return bool
247 + */
248 + public function is_ai_enabled() {
249 + return $this->settings->get( 'enable_write_with_ai', true ) && $this->has_api_key();
250 + }
322 251
323 - if ( is_wp_error( $response ) ) {
324 - return $response;
325 - }
326 -
327 - // Parse the AI response into structured data
328 - return $this->parse_quality_analysis_response( $response );
329 - }
330 -
331 - /**
332 - * Build the prompt for article quality analysis
333 - *
334 - * @param string $content Article content
335 - * @param string $title Article title
336 - * @return string Formatted prompt
337 - */
338 - private function build_quality_analysis_prompt( $content, $title = '' ) {
339 - $title_section = ! empty( $title ) ? "Title: {$title}\n\n" : '';
340 -
341 - $prompt = "Please analyze the following documentation article for quality and provide a comprehensive assessment:\n\n";
342 - $prompt .= $title_section;
343 - $prompt .= "Content:\n{$content}\n\n";
344 - $prompt .= "Please evaluate the article based on these criteria and provide your response in the following JSON format:\n\n";
345 - $prompt .= "{\n";
346 - $prompt .= ' "overall_score": 91,';
347 - $prompt .= ' "scores": {';
348 - $prompt .= ' "clarity": 90,';
349 - $prompt .= ' "completeness": 92,';
350 - $prompt .= ' "relevance": 95,';
351 - $prompt .= ' "structure": 87,';
352 - $prompt .= ' "readability": 88';
353 - $prompt .= ' },';
354 - $prompt .= ' "feedback": {';
355 - $prompt .= ' "strengths": ["Clear headings", "Good use of examples"],';
356 - $prompt .= ' "improvements": ["Add more detailed explanations in section 2", "Include troubleshooting steps"],';
357 - $prompt .= ' "suggestions": ["Consider adding screenshots", "Break down complex paragraphs"]';
358 - $prompt .= ' },';
359 - $prompt .= ' "priority": "medium"';
360 - $prompt .= "}\n\n";
361 - $prompt .= "Scoring criteria (0-100):\n";
362 - $prompt .= "- Clarity: How clear and understandable is the content?\n";
363 - $prompt .= "- Completeness: Does it cover the topic thoroughly?\n";
364 - $prompt .= "- Relevance: Is the content relevant to the stated purpose?\n";
365 - $prompt .= "- Structure: Is the content well-organized with proper headings?\n";
366 - $prompt .= "- Readability: Is it easy to read and follow?\n\n";
367 - $prompt .= "Priority levels: low (80+), medium (60-79), high (below 60)\n";
368 - $prompt .= "Provide specific, actionable feedback that content creators can implement.";
369 -
370 - return $prompt;
371 - }
372 -
373 - /**
374 - * Parse AI response into structured quality analysis data
375 - *
376 - * @param string $response Raw AI response
377 - * @return array|\WP_Error Parsed analysis data
378 - */
379 - private function parse_quality_analysis_response( $response ) {
380 - // Try to extract JSON from the response
381 - $json_start = strpos( $response, '{' );
382 - $json_end = strrpos( $response, '}' );
383 -
384 - if ( false === $json_start || false === $json_end ) {
385 - return new \WP_Error( 'parse_error', 'Could not find valid JSON in AI response.' );
386 - }
387 -
388 - $json_string = substr( $response, $json_start, $json_end - $json_start + 1 );
389 - $data = json_decode( $json_string, true );
390 -
391 - if ( json_last_error() !== JSON_ERROR_NONE ) {
392 - return new \WP_Error( 'json_error', 'Invalid JSON in AI response: ' . json_last_error_msg() );
393 - }
394 -
395 - // Validate required fields
396 - $required_fields = array( 'overall_score', 'scores', 'feedback' );
397 - foreach ( $required_fields as $field ) {
398 - if ( ! isset( $data[ $field ] ) ) {
399 - return new \WP_Error( 'missing_field', "Missing required field: {$field}" );
400 - }
401 - }
402 -
403 - // Ensure scores are within valid range
404 - $data[ 'overall_score' ] = max( 0, min( 100, intval( $data[ 'overall_score' ] ) ) );
405 -
406 - if ( isset( $data[ 'scores' ] ) && is_array( $data[ 'scores' ] ) ) {
407 - foreach ( $data[ 'scores' ] as $key => $score ) {
408 - $data[ 'scores' ][ $key ] = max( 0, min( 100, intval( $score ) ) );
409 - }
410 - }
411 -
412 - // Set default priority if not provided
413 - if ( ! isset( $data[ 'priority' ] ) ) {
414 - $overall_score = $data[ 'overall_score' ];
415 - if ( $overall_score >= 80 ) {
416 - $data[ 'priority' ] = 'low';
417 - } elseif ( $overall_score >= 60 ) {
418 - $data[ 'priority' ] = 'medium';
419 - } else {
420 - $data[ 'priority' ] = 'high';
421 - }
422 - }
423 -
424 - return $data;
425 - }
426 -
427 - /**
428 - * Save article quality score as post meta
429 - *
430 - * @param int $post_id Post ID
431 - * @param array $quality_data Quality analysis data
432 - * @return bool Success status
433 - */
434 - public function save_article_quality_score( $post_id, $quality_data ) {
435 - if ( empty( $post_id ) || ! is_array( $quality_data ) ) {
436 - return false;
437 - }
438 -
439 - // Save the complete analysis data
440 - $saved = update_post_meta( $post_id, '_betterdocs_article_quality_analysis', $quality_data );
441 -
442 - // Save just the overall score for easy querying
443 - update_post_meta( $post_id, '_betterdocs_article_quality_score', $quality_data[ 'overall_score' ] );
444 -
445 - // Save analysis timestamp
446 - update_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', current_time( 'mysql' ) );
447 -
448 - return false !== $saved;
449 - }
450 -
451 - /**
452 - * Get article quality score from post meta
453 - *
454 - * @param int $post_id Post ID
455 - * @return array|false Quality analysis data or false if not found
456 - */
457 - public function get_article_quality_score( $post_id ) {
458 - if ( empty( $post_id ) ) {
459 - return false;
460 - }
461 -
462 - $quality_data = get_post_meta( $post_id, '_betterdocs_article_quality_analysis', true );
463 -
464 - if ( empty( $quality_data ) ) {
465 - return false;
466 - }
467 -
468 - // Add timestamp if available
469 - $analyzed_at = get_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', true );
470 - if ( $analyzed_at ) {
471 - $quality_data[ 'analyzed_at' ] = $analyzed_at;
472 - }
473 -
474 - return $quality_data;
475 - }
476 -
477 - /**
478 - * Check if article needs re-analysis based on last modified date
479 - *
480 - * @param int $post_id Post ID
481 - * @return bool True if re-analysis is needed
482 - */
483 - public function needs_reanalysis( $post_id ) {
484 - $analyzed_at = get_post_meta( $post_id, '_betterdocs_article_quality_analyzed_at', true );
485 -
486 - if ( empty( $analyzed_at ) ) {
487 - return true; // Never analyzed
488 - }
489 -
490 - $post = get_post( $post_id );
491 - if ( ! $post ) {
492 - return false;
493 - }
494 -
495 - // Check if post was modified after last analysis
496 - $post_modified = strtotime( $post->post_modified );
497 - $analyzed_timestamp = strtotime( $analyzed_at );
498 -
499 - return $post_modified > $analyzed_timestamp;
500 - }
501 -
502 - /**
503 - * Create a system message for OpenAI
504 - *
505 - * @param string $content System message content
506 - * @return array Message array
507 - */
508 - public function create_system_message( $content ) {
509 - return array(
510 - 'role' => 'system',
511 - 'content' => $content
512 - );
513 - }
514 -
515 - /**
516 - * Create a user message for OpenAI
517 - *
518 - * @param string $content User message content
519 - * @return array Message array
520 - */
521 - public function create_user_message( $content ) {
522 - return array(
523 - 'role' => 'user',
524 - 'content' => $content
525 - );
526 - }
527 -
528 - /**
529 - * Create messages array for article summarization
530 - *
531 - * @param string $title Article title
532 - * @param string $content Article content
533 - * @return array Messages array
534 - */
535 - public function create_summary_messages( $title, $content ) {
536 - $system_message = $this->create_system_message(
537 - 'You are a helpful assistant that creates concise, informative summaries of documentation articles. Always format your response in clean HTML with paragraph tags. Do not use markdown formatting, code blocks, or backticks. Return only the HTML content without any wrapper formatting.'
538 - );
539 -
540 - $user_prompt = "Please provide a concise summary of the following article titled '{$title}'. The summary should be 2-3 paragraphs long, highlighting the main points and key takeaways. Format the response in HTML with proper paragraph tags. Do not wrap the response in markdown code blocks or use any markdown formatting.\n\nArticle content:\n{$content}";
541 -
542 - $user_message = $this->create_user_message( $user_prompt );
543 -
544 - return array( $system_message, $user_message );
545 - }
546 -
547 - /**
548 - * Create messages array for content generation
549 - *
550 - * @param string $prompt User prompt
551 - * @param string $keywords Optional keywords
552 - * @return array Messages array
553 - */
554 - public function create_content_messages( $prompt, $keywords = '' ) {
555 - $system_message = $this->create_system_message(
556 - 'You are a helpful assistant who writes documentation for users.'
557 - );
558 -
559 - $user_message = $this->create_user_message( $prompt );
560 -
561 - return array( $system_message, $user_message );
562 - }
563 -
564 - /**
565 - * Sanitize and prepare content for AI processing
566 - *
567 - * @param string $content Raw content
568 - * @param int $max_length Maximum length to keep
569 - * @return string Sanitized content
570 - */
571 - public function prepare_content_for_ai( $content, $max_length = 4000 ) {
572 - // Strip HTML tags and decode entities
573 - $content = wp_strip_all_tags( $content );
574 - $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
575 -
576 - // Remove extra whitespace
577 - $content = preg_replace( '/\s+/', ' ', $content );
578 - $content = trim( $content );
579 -
580 - // Limit length
581 - if ( strlen( $content ) > $max_length ) {
582 - $content = substr( $content, 0, $max_length );
583 - // Try to cut at a word boundary
584 - $last_space = strrpos( $content, ' ' );
585 - if ( false !== $last_space && $last_space > $max_length * 0.8 ) {
586 - $content = substr( $content, 0, $last_space );
587 - }
588 - $content .= '...';
589 - }
590 -
591 - return $content;
592 - }
593 -
594 - /**
595 - * Check if AI features are enabled
596 - *
597 - * @return bool
598 - */
599 - public function is_ai_enabled() {
600 - return $this->settings->get( 'enable_write_with_ai', true ) && $this->has_api_key();
601 - }
602 -
603 - /**
604 - * Get AI usage statistics (placeholder for future implementation)
605 - *
606 - * @return array Usage statistics
607 - */
608 - public function get_usage_stats() {
609 - // This could be implemented to track API usage, costs, etc.
610 - return array(
611 - 'requests_today' => 0,
612 - 'tokens_used' => 0,
613 - 'cost_estimate' => 0
614 - );
615 - }
252 + /**
253 + * Get AI usage statistics (placeholder for future implementation)
254 + *
255 + * @return array Usage statistics
256 + */
257 + public function get_usage_stats() {
258 + // This could be implemented to track API usage, costs, etc.
259 + return [
260 + 'requests_today' => 0,
261 + 'tokens_used' => 0,
262 + 'cost_estimate' => 0
263 + ];
264 + }
616 265 }