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/Core/WriteWithAI.php +162 -483 4.5.64.2.6 View file →
@@ -1,410 +1,172 @@
1 1 <?php
2 2
3 - namespace WPDeveloper\BetterDocs\Core;
3 +namespace WPDeveloper\BetterDocs\Core;
4 4
5 -if ( ! defined( 'ABSPATH' ) ) {
6 - exit;
7 -}
5 +use WPDeveloper\BetterDocs\Utils\Base;
6 +use WPDeveloper\BetterDocs\Core\Settings;
7 +use WPDeveloper\BetterDocs\Core\PostType;
8 8
9 +use WPDeveloper\BetterDocs\Utils\Helper;
9 10
10 - use WPDeveloper\BetterDocs\Utils\Base;
11 - use WPDeveloper\BetterDocs\Core\Settings;
12 - use WPDeveloper\BetterDocs\Core\PostType;
11 +class WriteWithAI extends Base {
13 12
14 - use WPDeveloper\BetterDocs\Utils\Helper;
15 - use WPDeveloper\BetterDocs\Utils\AIHelper;
16 - use WPDeveloper\BetterDocs\Utils\AIUsage;
13 + public $settings;
17 14
18 - class WriteWithAI extends Base {
15 + public function __construct( Settings $settings ) {
16 + $this->settings = $settings;
17 + // Get the post ID from the URL
18 + $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0; // phpcs:ignore
19 19
20 - public $settings;
20 + if (!empty($_GET['post_type'])) { // phpcs:ignore
21 + $post_type = $_GET['post_type']; // phpcs:ignore
22 + } elseif ( $post_id > 0 ) {
23 + $post_type = get_post_type( $post_id );
24 + } else {
25 + $post_type = '';
26 + }
21 27
22 - public function __construct( Settings $settings ) {
23 - $this->settings = $settings;
24 - // Get the post ID from the URL
25 - $post_id = isset( $_GET[ 'post' ] ) ? intval( $_GET[ 'post' ] ) : 0; // phpcs:ignore
28 + if ( ! empty( $this->isEnabledWriteWithAI() ) && $post_type == 'docs' ) {
29 + add_action( 'admin_footer', [ $this, 'ai_autowrite_button' ] );
30 + }
31 + add_action( 'wp_ajax_generate_openai_content', [ $this, 'generate_openai_content_callback' ] );
26 32
27 - if ( ! empty( $_GET[ 'post_type' ] ) ) { // phpcs:ignore
28 - $post_type = $_GET[ 'post_type' ]; // phpcs:ignore
29 - } elseif ( $post_id > 0 ) {
30 - $post_type = get_post_type( $post_id );
31 - } else {
32 - $post_type = '';
33 - }
33 + }
34 34
35 - if ( ! empty( $this->isEnabledWriteWithAI() ) && 'docs' == $post_type ) {
36 - add_action( 'admin_footer', array( $this, 'ai_autowrite_button' ) );
37 - add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_ai_edit_assets' ) );
38 - }
39 - add_action( 'wp_ajax_generate_openai_content', array( $this, 'generate_openai_content_callback' ) );
40 - }
35 + public function isEnabledWriteWithAI() {
36 + $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
37 + return $isEnableAutoWrite;
38 + }
41 39
42 - public function enqueue_ai_edit_assets( $hook ) {
43 - if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
44 - return;
45 - }
40 + public function isValidAPIKey( $apiKey ) {
41 + if ( empty( $apiKey ) ) {
42 + $api_response['valid'] = false;
43 + $api_response['message'] = 'Please Insert your <a href="/admin.php?page=betterdocs-settings">OpenAI API Key</a> to use this Write with AI feature.';
46 44
47 - global $post_type;
48 - if ( 'docs' !== $post_type ) {
49 - return;
50 - }
45 + return $api_response;
46 + }
51 47
52 - if ( empty( $this->get_api_key() ) ) {
53 - return;
54 - }
48 + $ch = curl_init( 'https://api.openai.com/v1/engines' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
49 + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
50 + curl_setopt(
51 + $ch,
52 + CURLOPT_HTTPHEADER,
53 + [ //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
54 + 'Content-Type: application/json',
55 + 'Authorization: Bearer ' . $apiKey,
56 + ]
57 + );
55 58
56 - betterdocs()->assets->enqueue( 'betterdocs-ai-edit', 'blocks/ai-edit.js' );
57 - betterdocs()->assets->enqueue( 'betterdocs-ai-edit-style', 'blocks/ai-edit-style.css' );
59 + $api_response = [];
58 60
59 - wp_localize_script(
60 - 'betterdocs-ai-edit',
61 - 'betterdocsAIEdit',
62 - array(
63 - 'rest_url' => esc_url_raw( rest_url( 'betterdocs/v1/ai-edit' ) ),
64 - 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
65 - 'post_id' => get_the_ID()
66 - )
67 - );
68 - }
61 + $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
62 + $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
69 63
70 - public function isEnabledWriteWithAI() {
71 - $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
72 - return $isEnableAutoWrite;
73 - }
64 + curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
74 65
75 - public function isValidAPIKey( $apiKey ) {
76 - if ( empty( $apiKey ) ) {
77 - $api_response[ 'valid' ] = false;
78 - $api_response[ 'message' ] = 'Please Insert your <a href="/admin.php?page=betterdocs-settings#betterdocs-ai">OpenAI API Key</a> to use this Write with AI feature.';
66 + if ( $httpCode == 200 ) {
67 + $api_response['valid'] = true;
68 + $api_response['message'] = 'Valid API Key';
69 + } else {
70 + $responseData = json_decode( $response, true );
71 + // Access the message data (replace 'data' with the actual key used in the response)
72 + $messageData = $responseData['error'] ? $responseData['error'] : '';
73 + $api_response['valid'] = false;
74 + $api_response['message'] = $messageData['message'] ? $messageData['message'] : 'Invalid API Key';
75 + }
79 76
80 - return $api_response;
81 - }
77 + // print_r($response);
82 78
83 - $api_response = array();
79 + return $api_response;
80 + }
84 81
85 - $response = wp_safe_remote_get(
86 - 'https://api.openai.com/v1/engines',
87 - array(
88 - 'headers' => array(
89 - 'Content-Type' => 'application/json',
90 - 'Authorization' => 'Bearer ' . $apiKey,
91 - ),
92 - 'timeout' => 15,
93 - )
94 - );
82 + public function get_api_key() {
83 + $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
84 + return $api_key;
85 + }
95 86
96 - if ( is_wp_error( $response ) ) {
97 - $api_response[ 'valid' ] = false;
98 - $api_response[ 'message' ] = $response->get_error_message();
99 - return $api_response;
100 - }
101 87
102 - $httpCode = (int) wp_remote_retrieve_response_code( $response );
103 - $body = wp_remote_retrieve_body( $response );
88 + public function generate_openai_response( $prompt, $keywords ) {
89 + try {
90 + $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
91 + $max_tokens = $this->settings->get( 'ai_autowrite_max_token', 1500 );
92 + $model = $this->settings->get( 'write_with_ai_model', 'gpt-4o-mini' );
104 93
105 - if ( 200 === $httpCode ) {
106 - $api_response[ 'valid' ] = true;
107 - $api_response[ 'message' ] = 'Valid API Key';
108 - } else {
109 - $responseData = json_decode( $body, true );
110 - $messageData = ! empty( $responseData[ 'error' ] ) ? $responseData[ 'error' ] : array();
111 - $api_response[ 'valid' ] = false;
112 - $api_response[ 'message' ] = ! empty( $messageData[ 'message' ] ) ? $messageData[ 'message' ] : 'Invalid API Key';
113 - }
94 + $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
114 95
115 - // print_r($response);
96 + $request_options = array(
97 + 'headers' => array(
98 + 'Content-Type' => 'application/json',
99 + 'Authorization' => 'Bearer ' . $api_key,
100 + ),
101 + 'body' => json_encode(
102 + array( //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
103 + 'model' => $model, // Add the model parameter here
104 + 'messages' => array(
105 + array(
106 + 'role' => 'system',
107 + 'content' => 'You are a helpful assistant who writes documentation for users.'
108 + ),
109 + array(
110 + 'role' => 'user',
111 + 'content' => $prompt
112 + ),
113 + ),
114 + 'max_tokens' => $max_tokens,
116 115
117 - return $api_response;
118 - }
116 + )
117 + ),
118 + 'timeout' => 50,
119 + );
119 120
120 - public function get_api_key() {
121 - $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
122 - return $api_key;
123 - }
121 + $response = wp_remote_post( $api_endpoint, $request_options );
124 122
125 - public function get_system_prompt() {
126 - $prompt = <<<'PROMPT'
127 -You are a Senior Technical Writer specializing in comprehensive, high-quality documentation. Your goal is to produce documentation that scores 100/100 on clarity, completeness, and structure.
123 + if ( is_wp_error( $response ) ) {
124 + return 'Error: ' . $response->get_error_message();
125 + } else {
126 + $body = wp_remote_retrieve_body( $response );
128 127
129 -## Output format
128 + $data = json_decode( $body, true );
130 129
131 -Return only HTML body content. Never wrap output in `<!doctype>`, `<html>`, `<head>`, `<body>`, or markdown code fences (no ```html ... ```).
130 + if ( ! empty( $data['error'] ) ) {
131 + return $data['error']['message'];
132 + }
132 133
133 -Use semantic, Gutenberg-friendly tags only:
134 -- Headings: `<h2>`, `<h3>`, `<h4>` (do not emit `<h1>` — it is reserved for the document title)
135 -- Paragraphs: `<p>` for prose
136 -- Lists: `<ul>`/`<ol>` with `<li>` for any list of items, steps, or bullet points — never fake a list with paragraphs or `<br>`
137 -- Links: `<a href="https://...">link text</a>` with absolute URLs
138 -- Inline emphasis: `<strong>`, `<em>`, `<code>`
139 -- Code blocks: `<pre><code>...</code></pre>` for multi-line code or commands
140 -- Quotes: `<blockquote>`
141 -- Tables: `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`
142 -- Images: `<img src="..." alt="...">`
134 + return $data['choices'][0]['message']['content']; // Update this line to get the assistant's message
135 + }
136 + } catch ( Exception $error ) {
137 + return 'Error: ' . $error->getMessage();
138 + }
139 + }
143 140
144 -Apply `<span class="highlight">key term</span>` to important topic terms inside headings and to the first occurrence of each keyword in body text. Use it sparingly — never wrap whole sentences or wrap text inside `href`, `src`, `alt`, or other attributes.
145 141
146 -Do not emit `<style>`, `<script>`, `<iframe>`, `<form>`, or inline `style=""` / `class=""` attributes (other than the `highlight` class above).
147 142
148 -If — and only if — the user's prompt explicitly asks for raw/custom HTML, embed code, or a non-standard structure, follow that request instead of these defaults.
149 -PROMPT;
143 + public function generate_openai_content_callback() {
144 + // Verify the nonce
145 + if (!isset($_POST['ai_nonce']) || !wp_verify_nonce($_POST['ai_nonce'], 'generate_openai_content_nonce')) { //phpcs:ignore
146 + wp_send_json_error( 'Invalid nonce' );
147 + wp_die();
148 + }
150 149
151 - return apply_filters( 'betterdocs_write_with_ai_system_prompt', $prompt );
152 - }
150 + $prompt = sanitize_text_field($_POST['prompt']); //phpcs:ignore
151 + // $num_of_sections = sanitize_text_field($_POST['numOfSections']);
152 + // $num_of_paragraphs = sanitize_text_field($_POST['numOfParagraphs']);
153 + $keywords = sanitize_text_field($_POST['keywords']); //phpcs:ignore
153 154
154 - public function generate_openai_response( $prompt, $keywords ) {
155 - try {
156 - $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
157 - $max_tokens = $this->settings->get( 'ai_autowrite_max_token', 2500 );
158 - $model = $this->settings->get( 'write_with_ai_model', 'gpt-4o-mini' );
155 + $ai_instance = new WriteWithAI( $this->settings );
159 156
160 - $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
157 + $generated_content = $ai_instance->generate_openai_response( $prompt, $keywords );
161 158
162 - $request_body = AIHelper::build_openai_payload(
163 - $model,
164 - array(
165 - array(
166 - 'role' => 'system',
167 - 'content' => $this->get_system_prompt()
168 - ),
169 - array(
170 - 'role' => 'user',
171 - 'content' => $prompt
172 - )
173 - ),
174 - $max_tokens,
175 - null,
176 - 'write_with_ai'
177 - );
159 + // Send the generated content as the AJAX response
160 + wp_send_json_success( $generated_content );
161 + wp_die();
162 + }
178 163
179 - $request_options = array(
180 - 'headers' => array(
181 - 'Content-Type' => 'application/json',
182 - 'Authorization' => 'Bearer ' . $api_key
183 - ),
184 - 'body' => json_encode( $request_body ),
185 - 'timeout' => 300
186 - );
187 164
188 - // GPT-5.5 reasoning can run well past the default limits; give PHP and
189 - // the HTTP call room to finish (still subject to server php-fpm/nginx limits).
190 - if ( function_exists( 'set_time_limit' ) ) {
191 - set_time_limit( 300 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- long-running AI generation needs an extended limit; still bounded by server fpm/nginx timeouts.
192 - }
193 165
194 - $response = wp_remote_post( $api_endpoint, $request_options );
166 + public function ai_autowrite_button() {
195 167
196 - if ( is_wp_error( $response ) ) {
197 - return 'Error: ' . $response->get_error_message();
198 - } else {
199 - $body = wp_remote_retrieve_body( $response );
200 -
201 - $data = json_decode( $body, true );
202 -
203 - if ( ! empty( $data[ 'error' ] ) ) {
204 - return $data[ 'error' ][ 'message' ];
205 - }
206 -
207 - return $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ]; // Update this line to get the assistant's message
208 - }
209 - } catch ( Exception $error ) {
210 - return 'Error: ' . $error->getMessage();
211 - }
212 - }
213 -
214 - public function generate_openai_response_ai_edit( $prompt ) {
215 - $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
216 - $max_tokens = $this->settings->get( 'ai_autowrite_max_token', 2500 );
217 - $model = $this->settings->get( 'write_with_ai_model', 'gpt-4o-mini' );
218 -
219 - $api_endpoint = 'https://api.openai.com/v1/chat/completions';
220 -
221 - $payload = AIHelper::build_openai_payload(
222 - $model,
223 - array(
224 - array(
225 - 'role' => 'system',
226 - 'content' => $this->get_system_prompt()
227 - ),
228 - array(
229 - 'role' => 'user',
230 - 'content' => $prompt
231 - )
232 - ),
233 - $max_tokens,
234 - null,
235 - 'write_with_ai'
236 - );
237 -
238 - $request_options = array(
239 - 'headers' => array(
240 - 'Content-Type' => 'application/json',
241 - 'Authorization' => 'Bearer ' . $api_key
242 - ),
243 - 'body' => wp_json_encode( $payload ),
244 - 'timeout' => 300
245 - );
246 -
247 - // GPT-5.5 reasoning can run well past the default limits; give PHP and
248 - // the HTTP call room to finish (still subject to server php-fpm/nginx limits).
249 - if ( function_exists( 'set_time_limit' ) ) {
250 - set_time_limit( 300 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- long-running AI generation needs an extended limit; still bounded by server fpm/nginx timeouts.
251 - }
252 -
253 - $response = wp_remote_post( $api_endpoint, $request_options );
254 -
255 - if ( is_wp_error( $response ) ) {
256 - return array(
257 - 'success' => false,
258 - 'error' => $response->get_error_message(),
259 - 'model' => $model
260 - );
261 - }
262 -
263 - $body = wp_remote_retrieve_body( $response );
264 - $data = json_decode( $body, true );
265 -
266 - if ( ! empty( $data[ 'error' ] ) ) {
267 - return array(
268 - 'success' => false,
269 - 'error' => isset( $data[ 'error' ][ 'message' ] ) ? $data[ 'error' ][ 'message' ] : 'OpenAI error',
270 - 'model' => $model,
271 - 'raw' => $data
272 - );
273 - }
274 -
275 - $content = isset( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ? $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] : '';
276 - $usage = isset( $data[ 'usage' ] ) && is_array( $data[ 'usage' ] ) ? $data[ 'usage' ] : array();
277 -
278 - return array(
279 - 'success' => true,
280 - 'content' => $content,
281 - 'model' => $model,
282 - 'prompt_tokens' => isset( $usage[ 'prompt_tokens' ] ) ? (int) $usage[ 'prompt_tokens' ] : null,
283 - 'completion_tokens' => isset( $usage[ 'completion_tokens' ] ) ? (int) $usage[ 'completion_tokens' ] : null,
284 - 'total_tokens' => isset( $usage[ 'total_tokens' ] ) ? (int) $usage[ 'total_tokens' ] : null,
285 - 'finish_reason' => isset( $data[ 'choices' ][ 0 ][ 'finish_reason' ] ) ? $data[ 'choices' ][ 0 ][ 'finish_reason' ] : null
286 - );
287 - }
288 -
289 - /**
290 - * Generic chat completion using the Write-with-AI model/token/key settings.
291 - *
292 - * Shared by lightweight, plain-text generators (glossary definitions, FAQ answers)
293 - * that need the same dynamic model as Write with AI / AI Edit but a caller-supplied
294 - * system prompt instead of the doc-authoring HTML prompt. Returns the same structured
295 - * array shape as {@see self::generate_openai_response_ai_edit()}.
296 - *
297 - * @param string $user_prompt The user message.
298 - * @param string $system_prompt Optional system message (omitted when empty).
299 - * @return array { success:bool, content?:string, error?:string, model:string, *_tokens?:int }
300 - */
301 - public function generate_text( $user_prompt, $system_prompt = '' ) {
302 - $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
303 - $max_tokens = $this->settings->get( 'ai_autowrite_max_token', 2500 );
304 - $model = $this->settings->get( 'write_with_ai_model', 'gpt-4o-mini' );
305 -
306 - $messages = array();
307 - if ( $system_prompt !== '' ) {
308 - $messages[] = array(
309 - 'role' => 'system',
310 - 'content' => $system_prompt
311 - );
312 - }
313 - $messages[] = array(
314 - 'role' => 'user',
315 - 'content' => $user_prompt
316 - );
317 -
318 - $api_endpoint = 'https://api.openai.com/v1/chat/completions';
319 -
320 - $payload = AIHelper::build_openai_payload( $model, $messages, $max_tokens, null, 'write_with_ai' );
321 -
322 - $request_options = array(
323 - 'headers' => array(
324 - 'Content-Type' => 'application/json',
325 - 'Authorization' => 'Bearer ' . $api_key
326 - ),
327 - 'body' => wp_json_encode( $payload ),
328 - 'timeout' => 300
329 - );
330 -
331 - // GPT-5.5 reasoning can run well past the default limits; give PHP and
332 - // the HTTP call room to finish (still subject to server php-fpm/nginx limits).
333 - if ( function_exists( 'set_time_limit' ) ) {
334 - set_time_limit( 300 );
335 - }
336 -
337 - $response = wp_remote_post( $api_endpoint, $request_options );
338 -
339 - if ( is_wp_error( $response ) ) {
340 - return array(
341 - 'success' => false,
342 - 'error' => $response->get_error_message(),
343 - 'model' => $model
344 - );
345 - }
346 -
347 - $body = wp_remote_retrieve_body( $response );
348 - $data = json_decode( $body, true );
349 -
350 - if ( ! empty( $data[ 'error' ] ) ) {
351 - return array(
352 - 'success' => false,
353 - 'error' => isset( $data[ 'error' ][ 'message' ] ) ? $data[ 'error' ][ 'message' ] : 'OpenAI error',
354 - 'model' => $model,
355 - 'raw' => $data
356 - );
357 - }
358 -
359 - $content = isset( $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] ) ? $data[ 'choices' ][ 0 ][ 'message' ][ 'content' ] : '';
360 - $usage = isset( $data[ 'usage' ] ) && is_array( $data[ 'usage' ] ) ? $data[ 'usage' ] : array();
361 -
362 - return array(
363 - 'success' => true,
364 - 'content' => $content,
365 - 'model' => $model,
366 - 'prompt_tokens' => isset( $usage[ 'prompt_tokens' ] ) ? (int) $usage[ 'prompt_tokens' ] : null,
367 - 'completion_tokens' => isset( $usage[ 'completion_tokens' ] ) ? (int) $usage[ 'completion_tokens' ] : null,
368 - 'total_tokens' => isset( $usage[ 'total_tokens' ] ) ? (int) $usage[ 'total_tokens' ] : null,
369 - 'finish_reason' => isset( $data[ 'choices' ][ 0 ][ 'finish_reason' ] ) ? $data[ 'choices' ][ 0 ][ 'finish_reason' ] : null
370 - );
371 - }
372 -
373 - public function generate_openai_content_callback() {
374 - // Verify the nonce
375 - $ai_nonce = isset( $_POST['ai_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ai_nonce'] ) ) : '';
376 - if ( ! wp_verify_nonce( $ai_nonce, 'generate_openai_content_nonce' ) ) {
377 - wp_send_json_error( 'Invalid nonce' );
378 - wp_die();
379 - }
380 -
381 - if ( ! current_user_can( 'edit_posts' ) ) {
382 - wp_send_json_error( 'Insufficient permissions' );
383 - wp_die();
384 - }
385 -
386 - $prompt = isset( $_POST['prompt'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt'] ) ) : '';
387 - $keywords = isset( $_POST['keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['keywords'] ) ) : '';
388 -
389 - $ai_instance = new WriteWithAI( $this->settings );
390 -
391 - $generated_content = $ai_instance->generate_openai_response( $prompt, $keywords );
392 -
393 - // Count a successful generation (skip obvious upstream errors).
394 - if ( is_string( $generated_content ) && $generated_content !== '' && strpos( $generated_content, 'Error:' ) !== 0 ) {
395 - $post_id = isset( $_POST[ 'post_id' ] ) ? intval( $_POST[ 'post_id' ] ) : 0; //phpcs:ignore
396 - AIUsage::record( 'write_with_ai', $post_id );
397 - }
398 -
399 - // Send the generated content as the AJAX response
400 - wp_send_json_success( $generated_content );
401 - wp_die();
402 - }
403 -
404 - public function ai_autowrite_button() {
405 -
406 - ?>
168 + ?>
407 169 <script>
408 170 var docsTitle;
409 171
410 172 function responseMessage(message) {
@@ -459,9 +221,8 @@
459 221 prompt: document.querySelector('#betterdocs-ai-content').value,
460 222 // numOfSections: numOfSections,
461 223 // numOfParagraphs: numOfParagraphs,
462 224 keywords: keywords,
463 - post_id: (document.getElementById('post_ID') ? document.getElementById('post_ID').value : 0),
464 225 ai_nonce: nonce, // Pass the nonce value
465 226 },
466 227 success: function(response) {
467 228 // Update the content in the textarea
@@ -471,8 +232,14 @@
471 232 setTimeout(() => {
472 233 insertContentToEditor(title, response.data, isOverwrite, keywords);
473 234 docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
474 235 jQuery('.generate-btn').removeAttr('disabled');
236 +
237 + // console.log(jQuery('.betterdocs-ai-autowrite-form-container').data('new-doc-page'));
238 +
239 + // if(jQuery('.betterdocs-ai-autowrite-form-container')?.data('new-doc-page')) {
240 + // docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
241 + // }
475 242 }, 2000);
476 243 } else {
477 244 // alert(response.data.message);
478 245 jQuery('#betterdocs-ai-error-message').parent().removeClass('hidden');
@@ -481,21 +248,10 @@
481 248 jQuery('.generate-btn').removeAttr('disabled');
482 249
483 250 }
484 251 },
485 - error: function(jqXHR, textStatus, errorThrown) {
486 - console.error(jqXHR, textStatus, errorThrown);
487 -
488 - // Reset the UI so a slow/failed request never leaves a permanent "Generating...".
489 - var timedOut = ( textStatus === 'timeout' ) || ( jqXHR && jqXHR.status === 504 ) || ( jqXHR && jqXHR.status === 0 );
490 - var errMessage = timedOut
491 - ? "<?php echo esc_html__( 'The request timed out before a response came back. The AI may still have generated content on the server — try again, or pick a faster model / lower the token limit.', 'betterdocs' ); ?>"
492 - : "<?php echo esc_html__( 'Something went wrong while generating. Please try again.', 'betterdocs' ); ?>";
493 -
494 - jQuery('#betterdocs-ai-error-message').parent().removeClass('hidden');
495 - jQuery('#betterdocs-ai-error-message').html(responseMessage(errMessage));
496 - docGenerateBtnTxt.innerHTML = generateDocLabel;
497 - jQuery('.generate-btn').removeAttr('disabled');
252 + error: function(error) {
253 + console.error(error);
498 254 },
499 255 });
500 256
501 257 }
@@ -519,9 +275,9 @@
519 275 if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
520 276 jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
521 277 }
522 278
523 - contentTextArea.value = `Write documentation for '${title}'. Cover these topics in depth: ${keywords}. Use headings, paragraphs, lists, links, code blocks, and tables wherever they help the reader. Follow the output-format rules in the system instructions.`;
279 + contentTextArea.value = `Generate a documentation using HTML heading tags for '${title}'. Include relevant details on ${keywords} in the documentation. Ensure all content is enclosed with <p> tags and apply a <span> tag with the class 'highlight' to headings and topic tags for emphasis.`;
524 280 }
525 281
526 282 function convertMarkdownToHTML(markdownContent) {
527 283 // Simple Markdown to HTML conversion (you may need to enhance this based on your needs)
@@ -551,107 +307,30 @@
551 307 return newDiv.innerHTML;
552 308 }
553 309
554 310
555 - function escapeHtml(str) {
556 - return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
557 - }
558 -
559 - function escapeRegex(str) {
560 - return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
561 - }
562 -
563 - function stripCodeFences(htmlString) {
564 - if (!htmlString) {
565 - return '';
566 - }
567 - return htmlString
568 - .replace(/^\s*```(?:html|HTML)?\s*\n?/, '')
569 - .replace(/\n?\s*```\s*$/, '');
570 - }
571 -
572 311 function wrapwithHeighlight(inputString, keywords) {
573 - if (!inputString) {
574 - return '';
575 - }
312 + let modifiedString = inputString.replace(/<(h\d)(.*?)>(.*?)<\/\1>/g, '<$1$2><span class="highlight">$3</span></$1>');
576 313
577 - const container = document.createElement('div');
578 - container.innerHTML = inputString;
314 + const keywordArray = keywords.split(',').map(keyword => keyword.trim());
579 315
580 - // Wrap each heading's visible text with the highlight span (skip if already present)
581 - container.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(function (heading) {
582 - if (heading.querySelector('.highlight')) {
583 - return;
584 - }
585 - const text = heading.textContent;
586 - if (!text || !text.trim()) {
587 - return;
588 - }
589 - heading.innerHTML = '<span class="highlight">' + escapeHtml(text) + '</span>';
316 + keywordArray.forEach(keyword => {
317 + modifiedString = modifiedString.replace(new RegExp(`\\b(${keyword})\\b`, 'gi'), '<span class="highlight">$1</span>');
590 318 });
591 319
592 - // Wrap keywords only inside text nodes — never inside attributes, code, or existing highlights
593 - const keywordList = (keywords || '')
594 - .split(',')
595 - .map(function (k) { return k.trim(); })
596 - .filter(Boolean);
597 -
598 - if (keywordList.length) {
599 - const pattern = new RegExp('\\b(' + keywordList.map(escapeRegex).join('|') + ')\\b', 'gi');
600 - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
601 - acceptNode: function (node) {
602 - let parent = node.parentNode;
603 - while (parent && parent !== container) {
604 - if (parent.nodeType === 1) {
605 - const tag = parent.tagName.toLowerCase();
606 - if (tag === 'code' || tag === 'pre') {
607 - return NodeFilter.FILTER_REJECT;
608 - }
609 - if (parent.classList && parent.classList.contains('highlight')) {
610 - return NodeFilter.FILTER_REJECT;
611 - }
612 - }
613 - parent = parent.parentNode;
614 - }
615 - return NodeFilter.FILTER_ACCEPT;
616 - }
617 - });
618 -
619 - const textNodes = [];
620 - let current;
621 - while ((current = walker.nextNode())) {
622 - textNodes.push(current);
623 - }
624 -
625 - textNodes.forEach(function (textNode) {
626 - pattern.lastIndex = 0;
627 - if (!pattern.test(textNode.nodeValue)) {
628 - return;
629 - }
630 - pattern.lastIndex = 0;
631 - const fragmentHost = document.createElement('span');
632 - fragmentHost.innerHTML = escapeHtml(textNode.nodeValue).replace(pattern, '<span class="highlight">$1</span>');
633 - const parent = textNode.parentNode;
634 - while (fragmentHost.firstChild) {
635 - parent.insertBefore(fragmentHost.firstChild, textNode);
636 - }
637 - parent.removeChild(textNode);
638 - });
639 - }
640 -
641 - return container.innerHTML;
320 + return modifiedString;
642 321 }
643 322
644 323 function getBodyContent(htmlString) {
645 - if (!htmlString) {
646 - return '';
647 - }
648 - const cleaned = stripCodeFences(htmlString);
649 - const match = cleaned.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
324 + var match = htmlString.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
325 +
326 + // Check if match is null before accessing match[1]
650 327 if (match) {
328 + console.log(match[1].replace(/<p>\s+/g, '<p>'));
651 329 return match[1].replace(/<p>\s+/g, '<p>');
330 + } else {
331 + return '';
652 332 }
653 - return cleaned;
654 333 }
655 334
656 335
657 336
@@ -664,9 +343,9 @@
664 343
665 344 htmlContent = getBodyContent(htmlContent);
666 345 htmlContent = removeFirstHeading(htmlContent);
667 346 htmlContent = wrapwithHeighlight(htmlContent, keywords);
668 - const isPostContent = `<?php echo isset( $_GET['post'] ) ? esc_html( get_the_content( absint( wp_unslash( $_GET['post'] ) ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only post id from URL. ?>`;
347 + const isPostContent = `<?php echo isset($_GET['post']) ? esc_html(get_the_content($_GET['post'])) : ''; // phpcs:ignore ?>`;
669 348
670 349 const blocks = wp.blocks.rawHandler({
671 350 HTML: htmlContent
672 351 });
@@ -719,9 +398,9 @@
719 398 let currentKeywords = jQuery('#betterdocs-ai-keyword').val();
720 399 if (!currentKeywords) {
721 400 currentKeywords = '{Documentation Keywords}';
722 401 }
723 - const currentPrompt = `Write documentation for '${docsTitle?docsTitle:'{Documentation title}'}'. Cover these topics in depth: ${currentKeywords}. Use headings, paragraphs, lists, links, code blocks, and tables wherever they help the reader. Follow the output-format rules in the system instructions.`;
402 + const currentPrompt = `Generate documentation using HTML heading tags for '${docsTitle?docsTitle:'{Documentation title}'}'. Include relevant details on ${currentKeywords} in the documentation. Ensure all content is enclosed with <p> tags and apply a <span> tag with the class 'highlight' to headings and topic tags for emphasis.`;
724 403 jQuery('#betterdocs-ai-content').val(currentPrompt);
725 404
726 405 jQuery('#betterdocs-ai-title').val(docsTitle);
727 406
@@ -738,14 +417,14 @@
738 417
739 418
740 419 function writeWithAIForm() {
741 420
742 - let title = `<?php echo isset( $_GET['post'] ) ? esc_html( get_the_title( absint( wp_unslash( $_GET['post'] ) ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only post id from URL. ?>`;
421 + let title = `<?php echo isset($_GET['post']) ? esc_html(get_the_title($_GET['post'])) : ''; // phpcs:ignore ?>`;
743 422 if (docsTitle) {
744 423 title = docsTitle;
745 424 }
746 425
747 - const promtTitle = `<?php echo isset( $_GET['post'] ) ? esc_html( get_the_title( absint( wp_unslash( $_GET['post'] ) ) ) ) : '{Documentation Title}'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only post id from URL. ?>`;
426 + const promtTitle = `<?php echo isset($_GET['post']) ? esc_html(get_the_title($_GET['post'])) : '{Documentation Title}'; // phpcs:ignore ?>`;
748 427 const titlePlaceholder = "<?php echo esc_attr__( 'Enter a descriptive title for your documentation.', 'betterdocs' ); ?>";
749 428 const keywords = "<?php echo esc_attr( '{Documentation Keywords}' ); ?>";
750 429 const keywordsPlaceholder = "<?php echo esc_attr__( 'Add keywords to generate precise & relevant documentation (comma-separated).', 'betterdocs' ); ?>";
751 430 const language = "<?php echo esc_attr( 'English' ); ?>";
@@ -760,18 +439,18 @@
760 439 const checkboxLabel = "<?php echo esc_html__( 'Overwrite your existing Doc:', 'betterdocs' ); ?>";
761 440 const nonce = "<?php echo esc_attr( wp_create_nonce( 'generate_openai_content_nonce' ) ); ?>";
762 441
763 442 <?php
764 - $is_valid = $this->get_api_key();
765 - $disable_field = 'disabled-input-field';
766 - if ( $this->get_api_key() ) {
767 - $disable_field = '';
768 - }
769 - ?>
443 + $is_valid = $this->get_api_key();
444 + $disable_field = 'disabled-input-field';
445 + if ( $this->get_api_key() ) {
446 + $disable_field = '';
447 + }
448 + ?>
770 449
771 450 let hiddenClass = 'hidden';
772 451 let newDocPageAtt = 'data-new-doc-page="true"';
773 - const isPostContent = `<?php echo isset( $_GET['post'] ) ? esc_html( get_the_content( absint( wp_unslash( $_GET['post'] ) ) ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only post id from URL. ?>`;
452 + const isPostContent = `<?php echo isset($_GET['post']) ? esc_html(get_the_content($_GET['post'])) : ''; // phpcs:ignore ?>`;
774 453
775 454 if (isPostContent !== '') {
776 455 hiddenClass = '';
777 456 newDocPageAtt = '';
@@ -781,9 +460,9 @@
781 460 // const prompt = `Make a knowledge base article with html heading tags about [${title}] in [${language}]. Here is some topic to describe the article: [${keywords}]. and wrap the content with p tag also add a span tag with a class named 'highlight' to all the heading and topic tags in the article.`;
782 461
783 462 // const prompt = `Create a details knowledge base article in [${language}] about [${title}] with html heading tags. Here is some topic to describe the article: [${keywords}]. And wrap the content with p tag also add a span tag with a class named 'highlight' to all the heading and topic tags in the article.`;
784 463
785 - const prompt = `Write documentation for '${promtTitle}'. Cover these topics in depth: ${keywords}. Use headings, paragraphs, lists, links, code blocks, and tables wherever they help the reader. Follow the output-format rules in the system instructions.`;
464 + const prompt = `Generate a documentation using HTML heading tags for '${promtTitle}'. Include relevant details on ${keywords} in the documentation. Ensure all content is enclosed with <p> tags and apply a <span> tag with the class 'highlight' to headings and topic tags for emphasis.`;
786 465
787 466 const aiIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
788 467 <g clip-path="url(#clip0_2460_1729)">
789 468 <path d="M10.3956 18.8608L6.10991 19.2322L6.48134 14.9465L15.3956 6.08936C15.5287 5.95329 15.6876 5.84518 15.863 5.77137C16.0384 5.69756 16.2268 5.65954 16.4171 5.65954C16.6074 5.65954 16.7957 5.69756 16.9711 5.77137C17.1466 5.84518 17.3054 5.95329 17.4385 6.08936L19.2528 7.91793C19.3865 8.05083 19.4926 8.20886 19.565 8.38293C19.6375 8.55701 19.6747 8.74368 19.6747 8.93221C19.6747 9.12075 19.6375 9.30742 19.565 9.48149C19.4926 9.65557 19.3865 9.8136 19.2528 9.9465L10.3956 18.8608ZM1.7042 5.67507C1.20277 5.58793 1.20277 4.86793 1.7042 4.78079C2.59203 4.62626 3.41374 4.21085 4.06456 3.58751C4.71538 2.96417 5.16583 2.16113 5.35848 1.28079L5.38848 1.14221C5.49705 0.647929 6.20277 0.643643 6.31705 1.13936L6.35277 1.29936C6.55248 2.17579 7.0067 2.97367 7.65837 3.59281C8.31005 4.21195 9.13013 4.62475 10.0156 4.77936C10.5199 4.86507 10.5199 5.58936 10.0156 5.67793C9.13005 5.83218 8.3098 6.24465 7.65787 6.86354C7.00594 7.48243 6.5514 8.28014 6.35134 9.1565L6.3142 9.31793C6.20134 9.81221 5.49563 9.80936 5.38705 9.31364L5.35848 9.17507C5.16564 8.29433 4.71476 7.49102 4.06339 6.86764C3.41202 6.24426 2.58969 5.82908 1.70134 5.67507H1.7042Z" stroke="white" stroke-width="1.42857" stroke-linecap="round" stroke-linejoin="round"/>
@@ -820,14 +499,14 @@
820 499 <div class="autowrite-subheadding">
821 500 <p><?php echo esc_html__( 'Generate documentation effortlessly with BetterDocs AI. Simply input your doc title, keywords, prompt and let the system automatically generate comprehensive documentation tailored to your needs.', 'betterdocs' ); ?></p>
822 501 </div>
823 502
824 - <?php if ( empty( $this->get_api_key() ) ): ?>
503 + <?php if ( empty( $this->get_api_key() ) ) : ?>
825 504 <div id="betterdocs-ai-message">
826 505 <div class="warning-message">
827 506 <span class="dashicons dashicons-warning"></span>
828 507 <div>
829 - <?php echo wp_kses_post( 'Please Insert your <a target="_blank" href="' . esc_url( admin_url( 'admin.php?page=betterdocs-settings#betterdocs-ai' ) ) . '">OpenAI API Key</a> to use this Write with AI feature.', 'betterdocs' ); ?>
508 + <?php echo wp_kses_post( 'Please Insert your <a target="_blank" href="' . esc_url( admin_url( 'admin.php?page=betterdocs-settings&tab=tab-ai-autowrite' ) ) . '">OpenAI API Key</a> to use this Write with AI feature.', 'betterdocs' ); ?>
830 509 </div>
831 510 </div>
832 511
833 512 </div>
@@ -1439,6 +1118,6 @@
1439 1118
1440 1119 }
1441 1120 </style>
1442 1121 <?php
1443 - }
1444 - }
1122 + }
1123 +}