PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.8.9
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.8.9
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
← All changes | includes/Core/WriteWithAI.php +249 -630 4.6.03.8.9 View file →
@@ -1,410 +1,168 @@
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' ] );
32 + }
26 33
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 - }
34 + public function isEnabledWriteWithAI() {
35 + $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
36 + return $isEnableAutoWrite;
37 + }
34 38
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 - }
39 + public function isValidAPIKey( $apiKey ) {
40 + if ( empty( $apiKey ) ) {
41 + $api_response['valid'] = false;
42 + $api_response['message'] = 'Please Insert your <a href="/admin.php?page=betterdocs-settings">OpenAI API Key</a> to use this Write with AI feature.';
41 43
42 - public function enqueue_ai_edit_assets( $hook ) {
43 - if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
44 - return;
45 - }
44 + return $api_response;
45 + }
46 46
47 - global $post_type;
48 - if ( 'docs' !== $post_type ) {
49 - return;
50 - }
47 + $ch = curl_init( 'https://api.openai.com/v1/engines' ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_init
48 + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
49 + curl_setopt(
50 + $ch,
51 + CURLOPT_HTTPHEADER,
52 + [ //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
53 + 'Content-Type: application/json',
54 + 'Authorization: Bearer ' . $apiKey,
55 + ]
56 + );
51 57
52 - if ( empty( $this->get_api_key() ) ) {
53 - return;
54 - }
58 + $api_response = [];
55 59
56 - betterdocs()->assets->enqueue( 'betterdocs-ai-edit', 'blocks/ai-edit.js' );
57 - betterdocs()->assets->enqueue( 'betterdocs-ai-edit-style', 'blocks/ai-edit-style.css' );
60 + $response = curl_exec( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_exec
61 + $httpCode = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_getinfo
58 62
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 - }
63 + curl_close( $ch ); //phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_close
69 64
70 - public function isEnabledWriteWithAI() {
71 - $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
72 - return $isEnableAutoWrite;
73 - }
65 + if ( $httpCode == 200 ) {
66 + $api_response['valid'] = true;
67 + $api_response['message'] = 'Valid API Key';
68 + } else {
69 + $responseData = json_decode( $response, true );
70 + // Access the message data (replace 'data' with the actual key used in the response)
71 + $messageData = $responseData['error'] ? $responseData['error'] : '';
72 + $api_response['valid'] = false;
73 + $api_response['message'] = $messageData['message'] ? $messageData['message'] : 'Invalid API Key';
74 + }
74 75
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.';
76 + // print_r($response);
79 77
80 - return $api_response;
81 - }
78 + return $api_response;
79 + }
82 80
83 - $api_response = array();
81 + public function get_api_key() {
82 + $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
83 + return $api_key;
84 + }
84 85
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 - );
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 - }
87 + public function generate_openai_response( $prompt, $keywords ) {
88 + try {
89 + $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
90 + $max_tokens = $this->settings->get( 'ai_autowrite_max_token', 1500 );
101 91
102 - $httpCode = (int) wp_remote_retrieve_response_code( $response );
103 - $body = wp_remote_retrieve_body( $response );
92 + $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
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 + $request_options = array(
95 + 'headers' => array(
96 + 'Content-Type' => 'application/json',
97 + 'Authorization' => 'Bearer ' . $api_key,
98 + ),
99 + 'body' => json_encode(
100 + array( //phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
101 + 'model' => 'gpt-4o-mini', // Add the model parameter here
102 + 'messages' => array(
103 + array(
104 + 'role' => 'system',
105 + 'content' => 'You are a helpful assistant who writes documentation for users.'
106 + ),
107 + array(
108 + 'role' => 'user',
109 + 'content' => $prompt
110 + ),
111 + ),
112 + 'max_tokens' => $max_tokens,
114 113
115 - // print_r($response);
114 + )
115 + ),
116 + 'timeout' => 50,
117 + );
116 118
117 - return $api_response;
118 - }
119 + $response = wp_remote_post( $api_endpoint, $request_options );
119 120
120 - public function get_api_key() {
121 - $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
122 - return $api_key;
123 - }
121 + if ( is_wp_error( $response ) ) {
122 + return 'Error: ' . $response->get_error_message();
123 + } else {
124 + $body = wp_remote_retrieve_body( $response );
124 125
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.
126 + $data = json_decode( $body, true );
128 127
129 -## Output format
128 + if ( ! empty( $data['error'] ) ) {
129 + return $data['error']['message'];
130 + }
130 131
131 -Return only HTML body content. Never wrap output in `<!doctype>`, `<html>`, `<head>`, `<body>`, or markdown code fences (no ```html ... ```).
132 + return $data['choices'][0]['message']['content']; // Update this line to get the assistant's message
133 + }
134 + } catch ( Exception $error ) {
135 + return 'Error: ' . $error->getMessage();
136 + }
137 + }
132 138
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="...">`
143 139
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 140
146 -Do not emit `<style>`, `<script>`, `<iframe>`, `<form>`, or inline `style=""` / `class=""` attributes (other than the `highlight` class above).
141 + public function generate_openai_content_callback() {
142 + // Verify the nonce
143 + if (!isset($_POST['ai_nonce']) || !wp_verify_nonce($_POST['ai_nonce'], 'generate_openai_content_nonce')) { //phpcs:ignore
144 + wp_send_json_error( 'Invalid nonce' );
145 + wp_die();
146 + }
147 147
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;
148 + $prompt = sanitize_text_field($_POST['prompt']); //phpcs:ignore
149 + // $num_of_sections = sanitize_text_field($_POST['numOfSections']);
150 + // $num_of_paragraphs = sanitize_text_field($_POST['numOfParagraphs']);
151 + $keywords = sanitize_text_field($_POST['keywords']); //phpcs:ignore
150 152
151 - return apply_filters( 'betterdocs_write_with_ai_system_prompt', $prompt );
152 - }
153 + $ai_instance = new WriteWithAI( $this->settings );
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 + $generated_content = $ai_instance->generate_openai_response( $prompt, $keywords );
159 156
160 - $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
157 + // Send the generated content as the AJAX response
158 + wp_send_json_success( $generated_content );
159 + wp_die();
160 + }
161 161
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 - );
162 + public function ai_autowrite_button() {
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 -
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 -
194 - $response = wp_remote_post( $api_endpoint, $request_options );
195 -
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 - ?>
164 + ?>
407 165 <script>
408 166 var docsTitle;
409 167
410 168 function responseMessage(message) {
@@ -459,9 +217,8 @@
459 217 prompt: document.querySelector('#betterdocs-ai-content').value,
460 218 // numOfSections: numOfSections,
461 219 // numOfParagraphs: numOfParagraphs,
462 220 keywords: keywords,
463 - post_id: (document.getElementById('post_ID') ? document.getElementById('post_ID').value : 0),
464 221 ai_nonce: nonce, // Pass the nonce value
465 222 },
466 223 success: function(response) {
467 224 // Update the content in the textarea
@@ -471,8 +228,14 @@
471 228 setTimeout(() => {
472 229 insertContentToEditor(title, response.data, isOverwrite, keywords);
473 230 docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
474 231 jQuery('.generate-btn').removeAttr('disabled');
232 +
233 + // console.log(jQuery('.betterdocs-ai-autowrite-form-container').data('new-doc-page'));
234 +
235 + // if(jQuery('.betterdocs-ai-autowrite-form-container')?.data('new-doc-page')) {
236 + // docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
237 + // }
475 238 }, 2000);
476 239 } else {
477 240 // alert(response.data.message);
478 241 jQuery('#betterdocs-ai-error-message').parent().removeClass('hidden');
@@ -481,21 +244,10 @@
481 244 jQuery('.generate-btn').removeAttr('disabled');
482 245
483 246 }
484 247 },
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');
248 + error: function(error) {
249 + console.error(error);
498 250 },
499 251 });
500 252
501 253 }
@@ -519,9 +271,9 @@
519 271 if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
520 272 jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
521 273 }
522 274
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.`;
275 + 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 276 }
525 277
526 278 function convertMarkdownToHTML(markdownContent) {
527 279 // Simple Markdown to HTML conversion (you may need to enhance this based on your needs)
@@ -551,107 +303,30 @@
551 303 return newDiv.innerHTML;
552 304 }
553 305
554 306
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 307 function wrapwithHeighlight(inputString, keywords) {
573 - if (!inputString) {
574 - return '';
575 - }
308 + let modifiedString = inputString.replace(/<(h\d)(.*?)>(.*?)<\/\1>/g, '<$1$2><span class="highlight">$3</span></$1>');
576 309
577 - const container = document.createElement('div');
578 - container.innerHTML = inputString;
310 + const keywordArray = keywords.split(',').map(keyword => keyword.trim());
579 311
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>';
312 + keywordArray.forEach(keyword => {
313 + modifiedString = modifiedString.replace(new RegExp(`\\b(${keyword})\\b`, 'gi'), '<span class="highlight">$1</span>');
590 314 });
591 315
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;
316 + return modifiedString;
642 317 }
643 318
644 319 function getBodyContent(htmlString) {
645 - if (!htmlString) {
646 - return '';
647 - }
648 - const cleaned = stripCodeFences(htmlString);
649 - const match = cleaned.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
320 + var match = htmlString.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
321 +
322 + // Check if match is null before accessing match[1]
650 323 if (match) {
324 + console.log(match[1].replace(/<p>\s+/g, '<p>'));
651 325 return match[1].replace(/<p>\s+/g, '<p>');
326 + } else {
327 + return '';
652 328 }
653 - return cleaned;
654 329 }
655 330
656 331
657 332
@@ -664,9 +339,9 @@
664 339
665 340 htmlContent = getBodyContent(htmlContent);
666 341 htmlContent = removeFirstHeading(htmlContent);
667 342 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. ?>`;
343 + const isPostContent = `<?php echo isset($_GET['post']) ? esc_html(get_the_content($_GET['post'])) : ''; // phpcs:ignore ?>`;
669 344
670 345 const blocks = wp.blocks.rawHandler({
671 346 HTML: htmlContent
672 347 });
@@ -719,9 +394,9 @@
719 394 let currentKeywords = jQuery('#betterdocs-ai-keyword').val();
720 395 if (!currentKeywords) {
721 396 currentKeywords = '{Documentation Keywords}';
722 397 }
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.`;
398 + 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 399 jQuery('#betterdocs-ai-content').val(currentPrompt);
725 400
726 401 jQuery('#betterdocs-ai-title').val(docsTitle);
727 402
@@ -738,14 +413,14 @@
738 413
739 414
740 415 function writeWithAIForm() {
741 416
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. ?>`;
417 + let title = `<?php echo isset($_GET['post']) ? esc_html(get_the_title($_GET['post'])) : ''; // phpcs:ignore ?>`;
743 418 if (docsTitle) {
744 419 title = docsTitle;
745 420 }
746 421
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. ?>`;
422 + const promtTitle = `<?php echo isset($_GET['post']) ? esc_html(get_the_title($_GET['post'])) : '{Documentation Title}'; // phpcs:ignore ?>`;
748 423 const titlePlaceholder = "<?php echo esc_attr__( 'Enter a descriptive title for your documentation.', 'betterdocs' ); ?>";
749 424 const keywords = "<?php echo esc_attr( '{Documentation Keywords}' ); ?>";
750 425 const keywordsPlaceholder = "<?php echo esc_attr__( 'Add keywords to generate precise & relevant documentation (comma-separated).', 'betterdocs' ); ?>";
751 426 const language = "<?php echo esc_attr( 'English' ); ?>";
@@ -760,18 +435,18 @@
760 435 const checkboxLabel = "<?php echo esc_html__( 'Overwrite your existing Doc:', 'betterdocs' ); ?>";
761 436 const nonce = "<?php echo esc_attr( wp_create_nonce( 'generate_openai_content_nonce' ) ); ?>";
762 437
763 438 <?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 - ?>
439 + $is_valid = $this->get_api_key();
440 + $disable_field = 'disabled-input-field';
441 + if ( $this->get_api_key() ) {
442 + $disable_field = '';
443 + }
444 + ?>
770 445
771 446 let hiddenClass = 'hidden';
772 447 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. ?>`;
448 + const isPostContent = `<?php echo isset($_GET['post']) ? esc_html(get_the_content($_GET['post'])) : ''; // phpcs:ignore ?>`;
774 449
775 450 if (isPostContent !== '') {
776 451 hiddenClass = '';
777 452 newDocPageAtt = '';
@@ -781,11 +456,20 @@
781 456 // 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 457
783 458 // 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 459
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.`;
460 + 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 461
787 - const aiIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="-1.5 -1.5 23 23" fill="none"><path d="m9.895 18.861-4.286.371.371-4.286 8.915-8.857a1.429 1.429 0 0 1 2.043 0l1.814 1.829a1.431 1.431 0 0 1 0 2.029l-8.857 8.914ZM1.204 5.674c-.501-.088-.501-.807 0-.894a4.537 4.537 0 0 0 3.654-3.5l.03-.139c.109-.494.814-.499.929-.003l.036.16a4.561 4.561 0 0 0 3.663 3.48c.504.086.504.81 0 .899a4.561 4.561 0 0 0-3.664 3.479l-.037.161c-.112.494-.818.491-.926-.004l-.029-.139a4.537 4.537 0 0 0-3.658-3.5h.003Z" stroke="#fff" stroke-width="1.429" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
462 + const aiIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
463 + <g clip-path="url(#clip0_2460_1729)">
464 + <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"/>
465 + </g>
466 + <defs>
467 + <clipPath id="clip0_2460_1729">
468 + <rect width="20" height="20" fill="white" transform="translate(0.5)"/>
469 + </clipPath>
470 + </defs>
471 + </svg>`;
788 472
789 473
790 474
791 475 return `
@@ -792,24 +476,33 @@
792 476 <div class="betterdocs-ai-autowrite-form-container hidden" ${newDocPageAtt}>
793 477 <div class="betterdocs-ai-autowrite-form-content">
794 478 <div class="betterdocs-ai-autowrite-top-part">
795 479 <div class="autowrite-heading">
796 - <span class="autowrite-icon" style="--bd-icon-tint: rgb(127, 86, 217); --bd-icon-tint-rgb: 127, 86, 217; display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px; flex-shrink:0; border-radius:9px; color:rgb(127, 86, 217); background:rgba(127, 86, 217, 0.2);">
797 - <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" viewBox="-1.5 -1.5 23 23"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.429" d="m9.895 18.861-4.286.371.371-4.286 8.915-8.857a1.43 1.43 0 0 1 2.043 0l1.814 1.829a1.43 1.43 0 0 1 0 2.029zM1.204 5.674c-.501-.088-.501-.807 0-.894a4.54 4.54 0 0 0 3.654-3.5l.03-.139c.109-.494.814-.499.929-.003l.036.16a4.56 4.56 0 0 0 3.663 3.48c.504.086.504.81 0 .899a4.56 4.56 0 0 0-3.664 3.479l-.037.161c-.112.494-.818.491-.926-.004l-.029-.139a4.54 4.54 0 0 0-3.658-3.5h.003Z"></path></svg>
480 + <span class="autowrite-icon">
481 + <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
482 + <g clip-path="url(#clip0_2453_20882)">
483 + <path d="M15.8322 30.1765L8.97508 30.7708L9.56936 23.9136L23.8322 9.74219C24.0451 9.52448 24.2993 9.3515 24.58 9.23341C24.8606 9.11531 25.162 9.05447 25.4665 9.05447C25.771 9.05447 26.0724 9.11531 26.3531 9.23341C26.6337 9.3515 26.8879 9.52448 27.1008 9.74219L30.0037 12.6679C30.2176 12.8805 30.3874 13.1334 30.5033 13.4119C30.6192 13.6904 30.6788 13.9891 30.6788 14.2908C30.6788 14.5924 30.6192 14.8911 30.5033 15.1696C30.3874 15.4481 30.2176 15.701 30.0037 15.9136L15.8322 30.1765ZM1.92593 9.07933C1.12365 8.9399 1.12365 7.7879 1.92593 7.64848C3.34647 7.40124 4.6612 6.73658 5.70251 5.73923C6.74382 4.74188 7.46455 3.45703 7.77279 2.04848L7.82079 1.82676C7.99451 1.03591 9.12365 1.02905 9.30651 1.82219L9.36365 2.07819C9.68319 3.48048 10.4099 4.75709 11.4526 5.74772C12.4953 6.73834 13.8074 7.39881 15.2242 7.64619C16.0311 7.78333 16.0311 8.94219 15.2242 9.0839C13.8073 9.33071 12.4949 9.99066 11.4518 10.9809C10.4087 11.9711 9.68146 13.2474 9.36136 14.6496L9.30193 14.9079C9.12136 15.6988 7.99222 15.6942 7.81851 14.901L7.77279 14.6793C7.46424 13.2702 6.74284 11.9849 5.70064 10.9874C4.65845 9.99003 3.34273 9.32574 1.92136 9.07933H1.92593Z" stroke="#E31B54" stroke-width="2.28571" stroke-linecap="round" stroke-linejoin="round"/>
484 + </g>
485 + <defs>
486 + <clipPath id="clip0_2453_20882">
487 + <rect width="32" height="32" fill="white"/>
488 + </clipPath>
489 + </defs>
490 + </svg>
798 491 </span>
799 - <h1 style="margin:0; font-family:'IBM Plex Sans',sans-serif; font-size:18px; font-weight:600; line-height:1.3; color:#101828;"><?php echo esc_html__( 'Write Documentation with BetterDocs AI', 'betterdocs' ); ?></h1>
492 + <h1><?php echo esc_html__( 'Write Documentation with BetterDocs AI', 'betterdocs' ); ?></h1>
800 493
801 494 </div>
802 495 <div class="autowrite-subheadding">
803 - <p style="margin:8px 0 0; font-family:'IBM Plex Sans',sans-serif; font-size:14px; font-weight:400; line-height:1.4; color:#667085;"><?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>
496 + <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>
804 497 </div>
805 498
806 - <?php if ( empty( $this->get_api_key() ) ): ?>
499 + <?php if ( empty( $this->get_api_key() ) ) : ?>
807 500 <div id="betterdocs-ai-message">
808 501 <div class="warning-message">
809 502 <span class="dashicons dashicons-warning"></span>
810 503 <div>
811 - <?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' ); ?>
504 + <?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' ); ?>
812 505 </div>
813 506 </div>
814 507
815 508 </div>
@@ -860,9 +553,9 @@
860 553 </div>
861 554 </form>
862 555 </div>
863 556
864 - <div class="bd-close-button" onclick="closeWriteWithAIForm('afterClosedClicked')"><svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.69687 14L0.296875 12.6L5.89687 7L0.296875 1.4L1.69687 0L7.29688 5.6L12.8969 0L14.2969 1.4L8.69688 7L14.2969 12.6L12.8969 14L7.29688 8.4L1.69687 14Z" fill="currentColor"/></svg></div>
557 + <div class="bd-close-button" onclick="closeWriteWithAIForm('afterClosedClicked')">✕</div>
865 558 </div>
866 559
867 560 <div id="betterdocs-ai-autowrite-form-container-overlay" class="hidden"></div>
868 561 `;
@@ -945,28 +638,23 @@
945 638 }
946 639
947 640 .betterdocs-ai-button-container {
948 641 margin-left: 10px;
949 - white-space: nowrap;
950 - max-width: 145px;
951 642 }
952 643
953 644 .autowrite-icon {
954 645 display: flex;
955 646 align-items: center;
956 - width: 32px;
957 - height: 32px;
958 - flex-shrink: 0;
959 - /* Mint chip + green sparkle — matches the FAQ/Glossary modal
960 - header icons (rgba(#00B884, 0.12)). */
961 - background: rgba(0, 184, 132, 0.12);
647 + width: 55px;
648 + height: 55px;
649 + background: #FFE4E8;
962 650 justify-content: center;
963 - border-radius: 9px;
651 + border-radius: 50px;
964 652 }
965 653
966 654 .autowrite-icon svg {
967 - width: 17px;
968 - height: 17px;
655 + width: 28px;
656 + height: 28px;
969 657 }
970 658
971 659 .autowrite-heading {
972 660 display: flex;
@@ -971,19 +659,17 @@
971 659 .autowrite-heading {
972 660 display: flex;
973 661 gap: 10px;
974 662 align-items: center;
975 - /* Keep the title clear of the in-header close button. */
976 - padding-right: 64px;
977 663 }
978 664
979 665 .autowrite-heading h1 {
980 - color: #101828;
666 + color: #1D2939;
981 667 font-family: 'IBM Plex Sans', sans-serif;
982 - font-size: 18px;
668 + font-size: 24px;
983 669 font-style: normal;
984 - font-weight: 600;
985 - line-height: 1.3;
670 + font-weight: 500;
671 + line-height: normal;
986 672 margin: 0;
987 673 }
988 674
989 675 .autowrite-heading p,
@@ -993,10 +679,9 @@
993 679 }
994 680
995 681 .autowrite-subheadding p {
996 682 font-family: 'IBM Plex Sans', sans-serif;
997 - font-size: 13px;
998 - color: #667085;
683 + font-size: 14px;
999 684 }
1000 685
1001 686 .prompt-field .input-description {
1002 687 margin: 0 !important;
@@ -1024,9 +709,8 @@
1024 709 display: flex;
1025 710 align-items: center;
1026 711 justify-content: space-between;
1027 712 gap: 10px;
1028 - width: 135px;
1029 713 }
1030 714
1031 715 .betterdocs-ai-button img {
1032 716 width: 20px;
@@ -1041,9 +725,9 @@
1041 725 z-index: 100001;
1042 726 width: 650px;
1043 727 margin: 0 auto;
1044 728 background-color: #fff;
1045 - border-radius: 20px;
729 + border-radius: 5px;
1046 730 font-family: 'IBM Plex Sans', sans-serif;
1047 731 /* max-height: 600px; */
1048 732 max-height: 750px;
1049 733 max-width: 100%;
@@ -1050,10 +734,11 @@
1050 734 }
1051 735
1052 736 .betterdocs-ai-autowrite-form-content {
1053 737 background-color: #fff;
1054 - padding: 0;
1055 - border-radius: 20px;
738 + padding: 20px 0px;
739 + border-radius: 5px;
740 + padding-bottom: 0;
1056 741 overflow: auto;
1057 742 /* max-height: 700px; */
1058 743 height: 100%;
1059 744
@@ -1059,13 +744,12 @@
1059 744
1060 745 }
1061 746
1062 747 .betterdocs-ai-autowrite-top-part {
1063 - /* Soft-gray header bar — matches the FAQ/Glossary/Edit-with-AI modals. */
1064 - background: #f9fafb;
1065 - border-bottom: 1px solid #f2f4f7;
1066 - border-radius: 20px 20px 0 0;
1067 - padding: 24px 40px 20px;
748 + /* padding-bottom: 20px; */
749 + border-bottom: 1px solid #ddd;
750 + /* margin-bottom: 20px; */
751 + padding: 0 40px;
1068 752 }
1069 753
1070 754 #betterdocs-ai-form {
1071 755 display: flex;
@@ -1113,13 +797,12 @@
1113 797 justify-content: space-between;
1114 798 }
1115 799
1116 800 #betterdocs-ai-form label {
1117 - font-size: 14px;
1118 - font-weight: 500;
1119 - color: #667085;
801 + font-weight: 600;
1120 802 margin-bottom: 5px;
1121 803 display: block;
804 + font-size: 14px;
1122 805 line-height: 20px;
1123 806 }
1124 807
1125 808 select#bd-autowrtite-content-section,
@@ -1126,76 +809,59 @@
1126 809 #bd-autowrtite-content-paragraph,
1127 810 #betterdocs-ai-language {
1128 811 width: 150px !important;
1129 812 height: 40px;
1130 - border: 1px solid #f2f4f7;
1131 - background: #f2f4f7;
1132 - color: #101828;
1133 - font-size: 14px;
1134 - border-radius: 5px;
1135 - padding: 5px 12px;
1136 - outline: none;
1137 - box-shadow: none;
1138 813 }
1139 814
1140 815 #betterdocs-ai-form input[type="text"],
1141 816 #betterdocs-ai-form textarea {
1142 817 width: 100%;
818 + padding: 8px;
1143 819 box-sizing: border-box;
1144 - border: 1px solid #f2f4f7;
1145 - background: #fff;
1146 - border-radius: 5px;
1147 - color: #101828;
1148 - font-size: 14px;
820 + border: 1px solid #D0D5DD;
821 + border-radius: 4px;
822 + color: #667085;
1149 823 font-weight: 400;
1150 - outline: none;
1151 - box-shadow: none;
1152 824 }
1153 825
1154 - #betterdocs-ai-form input[type="text"] {
1155 - height: 40px;
1156 - padding: 5px 15px;
826 + /* Override autofill text color */
827 + #betterdocs-ai-form input:-webkit-autofill {
828 + -webkit-text-fill-color: #667085 !important;
1157 829 }
1158 830
1159 - #betterdocs-ai-form textarea {
1160 - padding: 10px 15px;
1161 - min-height: 100px;
831 + #betterdocs-ai-form input::placeholder {
832 + color: #acb2bf;
1162 833 }
1163 834
1164 - /* Override autofill text color */
1165 - #betterdocs-ai-form input:-webkit-autofill {
1166 - -webkit-text-fill-color: #101828 !important;
835 + #betterdocs-ai-form textarea {
836 + color: #667085;
1167 837 }
1168 838
1169 - #betterdocs-ai-form input::placeholder,
1170 - #betterdocs-ai-form textarea::placeholder {
1171 - color: #667085;
839 + #betterdocs-ai-form input:focus,
840 + #betterdocs-ai-form textarea:focus {
841 + box-shadow: none;
1172 842 }
1173 843
1174 - #betterdocs-ai-form input:focus,
1175 844 #betterdocs-ai-form textarea:focus {
1176 - border-color: #00b884;
1177 - box-shadow: 0 0 0 3px rgba(0, 184, 132, 0.15);
1178 - outline: none;
845 + color: inherit;
846 + box-shadow: none;
847 +
1179 848 }
1180 849
1181 850
1182 - /* Footer bar — soft gray surface; Cancel pinned left, Generate right. */
1183 851 .generate-button-container {
1184 852 position: sticky;
1185 853 bottom: 0px;
1186 854 width: 100%;
1187 - box-sizing: border-box;
1188 855 margin-left: 0;
1189 856 z-index: 999999 !important;
1190 - padding: 16px 22px;
857 + padding: 20px 0;
1191 858 display: flex;
1192 - align-items: center;
1193 - justify-content: flex-end;
1194 - border-top: 1px solid #f2f4f7;
1195 - background: #f9fafb;
1196 - border-bottom-right-radius: 20px;
1197 - border-bottom-left-radius: 20px;
859 + justify-content: end;
860 + border-top: 1px solid #ddd;
861 + background: white;
862 + border-bottom-right-radius: 5px;
863 + border-bottom-left-radius: 5px;
1198 864 }
1199 865
1200 866 .generate-button-container button {
1201 867 display: flex;
@@ -1202,35 +868,11 @@
1202 868 gap: 8px;
1203 869 align-items: center;
1204 870 justify-content: center;
1205 871 opacity: 1 !important;
872 + margin-right: 40px;
1206 873 }
1207 874
1208 - /* Cancel — white pill with neutral border. Stays clickable even when
1209 - the form is disabled (missing API key). */
1210 - .generate-button-container .bd-ai-cancel-btn {
1211 - pointer-events: auto;
1212 - display: inline-flex;
1213 - min-height: 36px;
1214 - padding: 7px 16px;
1215 - justify-content: center;
1216 - align-items: center;
1217 - background-color: #ffffff;
1218 - color: #344054;
1219 - border: 1px solid #d0d5dd;
1220 - border-radius: 9px;
1221 - font-size: 16px;
1222 - cursor: pointer;
1223 - text-decoration: none;
1224 - transition: all 0.3s ease 0s;
1225 - box-sizing: border-box;
1226 - }
1227 -
1228 - .generate-button-container .bd-ai-cancel-btn:hover {
1229 - background-color: #f2f4f7;
1230 - color: #101828;
1231 - }
1232 -
1233 875 .input-description {
1234 876 font-size: 14px;
1235 877 color: #667085;
1236 878 }
@@ -1351,67 +993,44 @@
1351 993
1352 994 .generate-btn,
1353 995 .prompt-btn,
1354 996 .keep-btn {
1355 - display: inline-flex;
1356 - min-height: 0;
1357 - padding: 9px 20px;
1358 - gap: 8px;
1359 - justify-content: center;
1360 - align-items: center;
1361 997 background-color: #00b884;
1362 998 color: #fff;
1363 - border: 1px solid transparent;
1364 - border-radius: 10px;
1365 - font-size: 13px;
1366 - font-weight: 600;
999 + border: none;
1000 + padding: 10px 15px;
1001 + text-align: center;
1002 + text-decoration: none;
1003 + display: inline-block;
1004 + font-size: 16px;
1367 1005 cursor: pointer;
1368 - text-decoration: none;
1369 - box-shadow: none;
1370 - transition: background 0.15s, transform 0.05s;
1371 - box-sizing: border-box;
1006 + border-radius: 4px;
1372 1007 }
1373 1008
1374 - .generate-btn:hover,
1375 - .prompt-btn:hover,
1376 - .keep-btn:hover {
1377 - background-color: #00956b;
1378 - }
1379 -
1380 1009 .generate-btn[disabled] {
1381 1010 cursor: unset;
1382 1011 }
1383 1012
1384 - /* Close — 44px gray rounded square INSIDE the modal, top-right of
1385 - the header, aligned with the header icon. */
1386 1013 .bd-close-button {
1387 1014 cursor: pointer;
1015 + font-size: 18px;
1016 + font-weight: bold;
1017 + float: right;
1388 1018 position: absolute;
1389 - top: 24px;
1390 - right: 24px;
1391 - z-index: 2;
1392 - width: 32px;
1393 - height: 32px;
1019 + right: 1px;
1020 + top: 1px;
1021 + padding: 10px;
1022 + background: #ffffff;
1023 + border-radius: 25px;
1024 + width: 20px;
1025 + height: 20px;
1394 1026 display: flex;
1395 1027 align-items: center;
1396 1028 justify-content: center;
1397 - border-radius: 9px;
1398 - background: #f2f4f7;
1399 - box-shadow: none;
1400 - color: #667085;
1401 - transition: all 0.2s ease-in-out;
1029 + color: #281617;
1030 + right: -60px;
1402 1031 }
1403 1032
1404 - .bd-close-button svg {
1405 - width: 16px;
1406 - height: 16px;
1407 - }
1408 -
1409 - .bd-close-button:hover {
1410 - background: #eaecf0;
1411 - color: #101828;
1412 - }
1413 -
1414 1033 div#betterdocs-ai-error-message,
1415 1034 #betterdocs-ai-message {
1416 1035 font-size: 14px;
1417 1036 font-family: 'IBM Plex Sans', sans-serif;
@@ -1494,6 +1113,6 @@
1494 1113
1495 1114 }
1496 1115 </style>
1497 1116 <?php
1498 - }
1499 - }
1117 + }
1118 +}