PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.6.1
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.6.1
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Core / WriteWithAI.php

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

1,500 lines 51.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Core;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9
10 use WPDeveloper\BetterDocs\Utils\Base;
11 use WPDeveloper\BetterDocs\Core\Settings;
12 use WPDeveloper\BetterDocs\Core\PostType;
13
14 use WPDeveloper\BetterDocs\Utils\Helper;
15 use WPDeveloper\BetterDocs\Utils\AIHelper;
16 use WPDeveloper\BetterDocs\Utils\AIUsage;
17
18 class WriteWithAI extends Base {
19
20 public $settings;
21
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
26
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
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 }
41
42 public function enqueue_ai_edit_assets( $hook ) {
43 if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
44 return;
45 }
46
47 global $post_type;
48 if ( 'docs' !== $post_type ) {
49 return;
50 }
51
52 if ( empty( $this->get_api_key() ) ) {
53 return;
54 }
55
56 betterdocs()->assets->enqueue( 'betterdocs-ai-edit', 'blocks/ai-edit.js' );
57 betterdocs()->assets->enqueue( 'betterdocs-ai-edit-style', 'blocks/ai-edit-style.css' );
58
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 }
69
70 public function isEnabledWriteWithAI() {
71 $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
72 return $isEnableAutoWrite;
73 }
74
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.';
79
80 return $api_response;
81 }
82
83 $api_response = array();
84
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
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
102 $httpCode = (int) wp_remote_retrieve_response_code( $response );
103 $body = wp_remote_retrieve_body( $response );
104
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 }
114
115 // print_r($response);
116
117 return $api_response;
118 }
119
120 public function get_api_key() {
121 $api_key = $this->settings->get( 'ai_autowrite_api_key', '' );
122 return $api_key;
123 }
124
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.
128
129 ## Output format
130
131 Return only HTML body content. Never wrap output in `<!doctype>`, `<html>`, `<head>`, `<body>`, or markdown code fences (no ```html ... ```).
132
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
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
146 Do not emit `<style>`, `<script>`, `<iframe>`, `<form>`, or inline `style=""` / `class=""` attributes (other than the `highlight` class above).
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;
150
151 return apply_filters( 'betterdocs_write_with_ai_system_prompt', $prompt );
152 }
153
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' );
159
160 $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
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 );
178
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 ?>
407 <script>
408 var docsTitle;
409
410 function responseMessage(message) {
411 return `<div class="warning-message">
412 <span class="dashicons dashicons-warning"></span>
413 <div class="footer-message">
414 ${message}
415 </div>
416 </div>`;
417 }
418
419 function generateArticle(e) {
420
421
422 const title = document.getElementById('betterdocs-ai-title').value;
423 const keywords = document.getElementById('betterdocs-ai-keyword').value;
424 // const sectionOptions = document.getElementById('bd-autowrtite-content-section');
425 // const numOfSections = sectionOptions.options[sectionOptions.selectedIndex].value;
426
427 // const paragraphOptions = document.getElementById('bd-autowrtite-content-paragraph');
428 // const numOfParagraphs = paragraphOptions.options[paragraphOptions.selectedIndex].value;
429
430 const contentTextArea = document.getElementById('betterdocs-ai-content');
431 const docGenerateBtnTxt = document.querySelector('.generate-btn span');
432
433 // Add nonce value to the data being sent
434 const nonce = document.querySelector('input[name="ai_nonce"]').value;
435 const isOverwrite = document.getElementById('betterdocs-ai-checkbox').checked;
436 const generateDocLabel = "<?php echo esc_html__( 'Generate Doc', 'betterdocs' ); ?>";
437 const reGenerateDocLabel = "<?php echo esc_html__( 'Regenerate Doc', 'betterdocs' ); ?>";
438
439
440 // contentTextArea.value = `Write an article about ${title} in English. The article is organized by the following exact ${numOfSections} headings. Write exact ${numOfParagraphs} number of paragraphs per heading.${keywords} Use Markdown for formatting. Add an introduction and a conclusion. Style: creative. Tone: cheerful.`;
441
442
443 // Check if the title is not empty
444 if (title.trim() !== '' && keywords.trim() !== '') {
445 e.preventDefault();
446
447 if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
448 jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
449 }
450
451 docGenerateBtnTxt.innerHTML = "<?php echo esc_html__( 'Generating...', 'betterdocs' ); ?>";
452 jQuery('.generate-btn').prop('disabled', true);
453
454 jQuery.post({
455 url: ajaxurl, // Make sure ajaxurl is defined in your script
456 type: 'POST',
457 data: {
458 action: 'generate_openai_content',
459 prompt: document.querySelector('#betterdocs-ai-content').value,
460 // numOfSections: numOfSections,
461 // numOfParagraphs: numOfParagraphs,
462 keywords: keywords,
463 post_id: (document.getElementById('post_ID') ? document.getElementById('post_ID').value : 0),
464 ai_nonce: nonce, // Pass the nonce value
465 },
466 success: function(response) {
467 // Update the content in the textarea
468
469 if (response.success && (typeof response.data === 'string')) {
470 docGenerateBtnTxt.innerHTML = "<?php echo esc_html__( 'Generated', 'betterdocs' ); ?>";
471 setTimeout(() => {
472 insertContentToEditor(title, response.data, isOverwrite, keywords);
473 docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
474 jQuery('.generate-btn').removeAttr('disabled');
475 }, 2000);
476 } else {
477 // alert(response.data.message);
478 jQuery('#betterdocs-ai-error-message').parent().removeClass('hidden');
479 jQuery('#betterdocs-ai-error-message').html(responseMessage(response.data.message));
480 docGenerateBtnTxt.innerHTML = generateDocLabel;
481 jQuery('.generate-btn').removeAttr('disabled');
482
483 }
484 },
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');
498 },
499 });
500
501 }
502 }
503
504 // Function to update the textarea
505 function updateTextarea() {
506 const title = document.getElementById('betterdocs-ai-title').value;
507 const keywords = document.getElementById('betterdocs-ai-keyword').value;
508 const sectionOptions = document.getElementById('bd-autowrtite-content-section');
509 let language = 'English';
510
511 if (language === '') {
512 language = 'English';
513 }
514
515 let keywordsText = keywords !== '' ? ` and incorporate the keywords: ${keywords}` : '';
516
517 const contentTextArea = document.getElementById('betterdocs-ai-content');
518
519 if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
520 jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
521 }
522
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.`;
524 }
525
526 function convertMarkdownToHTML(markdownContent) {
527 // Simple Markdown to HTML conversion (you may need to enhance this based on your needs)
528 const htmlContent = markdownContent
529 .replace(/^# (.+)$/gm, '<h1>$1</h1>')
530 .replace(/^## (.+)$/gm, '<h2>$1</h2>')
531 .replace(/^### (.+)$/gm, '<h3>$1</h3>')
532 .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
533 .replace(/\*(.+?)\*/g, '<em>$1</em>');
534
535 return htmlContent;
536 }
537
538 function replaceHeadingTitle(content, title, overviewTitle) {
539 let regex = new RegExp(title, 'g');
540 let updateContent = content.replace(regex, overviewTitle);
541 return updateContent;
542 }
543
544 function removeFirstHeading(htmlString) {
545 var newDiv = document.createElement('div');
546 newDiv.innerHTML = htmlString;
547 var firstHeading = newDiv.querySelector('h1');
548 if (firstHeading) {
549 firstHeading.parentNode.removeChild(firstHeading);
550 }
551 return newDiv.innerHTML;
552 }
553
554
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 function wrapwithHeighlight(inputString, keywords) {
573 if (!inputString) {
574 return '';
575 }
576
577 const container = document.createElement('div');
578 container.innerHTML = inputString;
579
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>';
590 });
591
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;
642 }
643
644 function getBodyContent(htmlString) {
645 if (!htmlString) {
646 return '';
647 }
648 const cleaned = stripCodeFences(htmlString);
649 const match = cleaned.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
650 if (match) {
651 return match[1].replace(/<p>\s+/g, '<p>');
652 }
653 return cleaned;
654 }
655
656
657
658 function insertContentToEditor(title, htmlContent, isOverwrite, keywords) {
659
660 const {
661 dispatch,
662 select
663 } = wp.data;
664
665 htmlContent = getBodyContent(htmlContent);
666 htmlContent = removeFirstHeading(htmlContent);
667 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. ?>`;
669
670 const blocks = wp.blocks.rawHandler({
671 HTML: htmlContent
672 });
673
674 dispatch('core/editor').editPost({
675 title: title,
676 });
677
678 const selectedBlockClientId = select('core/block-editor').getSelectedBlockClientId();
679
680 const allBlocks = select('core/block-editor').getBlocks();
681
682 if (isOverwrite || isPostContent === '') {
683
684 allBlocks.forEach((block) => {
685 dispatch('core/block-editor').removeBlock(block.clientId);
686 });
687 dispatch('core/block-editor').insertBlocks(blocks);
688
689 } else if (selectedBlockClientId) {
690 const selectedIndex = select('core/block-editor').getBlockIndex(selectedBlockClientId);
691 dispatch('core/block-editor').insertBlocks(blocks, selectedIndex + 1);
692 } else {
693 dispatch('core/block-editor').insertBlocks(blocks);
694 }
695
696 closeWriteWithAIForm('afterInsertContent');
697 }
698
699 function closeWriteWithAIForm(after) {
700 jQuery('.betterdocs-ai-autowrite-form-container').addClass('hidden');
701 jQuery('#betterdocs-ai-autowrite-form-container-overlay').addClass('hidden');
702 jQuery('.betterdocs-ai-button').addClass('regenerate-btn');
703 }
704
705
706 document.addEventListener('DOMContentLoaded', function() {
707
708 // Subscribe to changes in the editor state
709 let previousDocsTitle = '';
710
711 wp.data.subscribe(() => {
712 // Get the updated post title
713 // const docsTitle = wp.data.select('core/editor').getEditedPostAttribute('title');
714
715 const docsTitle = wp.data.select('core/editor').getEditedPostAttribute('title');
716
717 // Check if the title has changed
718 if (docsTitle !== previousDocsTitle) {
719 let currentKeywords = jQuery('#betterdocs-ai-keyword').val();
720 if (!currentKeywords) {
721 currentKeywords = '{Documentation Keywords}';
722 }
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.`;
724 jQuery('#betterdocs-ai-content').val(currentPrompt);
725
726 jQuery('#betterdocs-ai-title').val(docsTitle);
727
728 previousDocsTitle = docsTitle;
729 }
730
731
732 });
733 });
734
735
736 // This example assumes that this code is executed when your script is loaded.
737 // You may want to place it within the appropriate context in your application.
738
739
740 function writeWithAIForm() {
741
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. ?>`;
743 if (docsTitle) {
744 title = docsTitle;
745 }
746
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. ?>`;
748 const titlePlaceholder = "<?php echo esc_attr__( 'Enter a descriptive title for your documentation.', 'betterdocs' ); ?>";
749 const keywords = "<?php echo esc_attr( '{Documentation Keywords}' ); ?>";
750 const keywordsPlaceholder = "<?php echo esc_attr__( 'Add keywords to generate precise & relevant documentation (comma-separated).', 'betterdocs' ); ?>";
751 const language = "<?php echo esc_attr( 'English' ); ?>";
752 const titleLabel = "<?php echo esc_html__( 'Documentation Title:', 'betterdocs' ); ?>";
753 const keywordLabel = "<?php echo esc_html__( 'Keywords:', 'betterdocs' ); ?>";
754 const secLabel = "<?php echo esc_html__( '# of Sections:', 'betterdocs' ); ?>";
755 const paraLabel = "<?php echo esc_html__( '# of Paragraph Per Section: ', 'betterdocs' ); ?>";
756 const langLabel = "<?php echo esc_html__( 'Language:', 'betterdocs' ); ?>";
757 const promtLabel = "<?php echo esc_html__( 'Prompt:', 'betterdocs' ); ?>";
758 const promtBtnLabel = "<?php echo esc_html__( 'Generate Prompt', 'betterdocs' ); ?>";
759 let promtArticleLabel = "<?php echo esc_html__( 'Generate Doc', 'betterdocs' ); ?>";
760 const checkboxLabel = "<?php echo esc_html__( 'Overwrite your existing Doc:', 'betterdocs' ); ?>";
761 const nonce = "<?php echo esc_attr( wp_create_nonce( 'generate_openai_content_nonce' ) ); ?>";
762
763 <?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 ?>
770
771 let hiddenClass = 'hidden';
772 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. ?>`;
774
775 if (isPostContent !== '') {
776 hiddenClass = '';
777 newDocPageAtt = '';
778 }
779
780
781 // 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
783 // 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
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.`;
786
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>`;
788
789
790
791 return `
792 <div class="betterdocs-ai-autowrite-form-container hidden" ${newDocPageAtt}>
793 <div class="betterdocs-ai-autowrite-form-content">
794 <div class="betterdocs-ai-autowrite-top-part">
795 <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>
798 </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>
800
801 </div>
802 <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>
804 </div>
805
806 <?php if ( empty( $this->get_api_key() ) ): ?>
807 <div id="betterdocs-ai-message">
808 <div class="warning-message">
809 <span class="dashicons dashicons-warning"></span>
810 <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' ); ?>
812 </div>
813 </div>
814
815 </div>
816 <?php endif; ?>
817
818 <div class="form-group hidden">
819 <div id="betterdocs-ai-error-message"></div>
820 </div>
821
822 </div>
823 <form id="betterdocs-ai-form" class="<?php echo esc_attr( $disable_field ); ?>">
824 <div class="form-inner-content">
825 <div class="form-group">
826 <label for="betterdocs-ai-title">${titleLabel}</label>
827 <input type="text" placeholder="${titlePlaceholder}" id="betterdocs-ai-title" name="betterdocs-ai-title" required value="${title}">
828 </div>
829
830 <div class="form-group">
831 <label for="betterdocs-ai-keyword">${keywordLabel}</label>
832 <input type="text" placeholder="${keywordsPlaceholder}" id="betterdocs-ai-keyword" name="betterdocs-ai-keyword" required>
833 </div>
834
835 <div class="form-group prompt-field">
836 <label for="betterdocs-ai-content">${promtLabel}</label>
837 <textarea id="betterdocs-ai-content" name="betterdocs-ai-content" rows="6" required>${prompt}</textarea>
838 <p class="input-description"><?php echo esc_html__( 'Ensure you include a clear and detailed prompt to receive the desired output. Follow the guidelines provided for the best results.', 'betterdocs' ); ?></p>
839 </div>
840 <div class="betterdocs-ai-checkbox-field ${hiddenClass}">
841 <div class="form-group">
842 <div class="checkbox-field-label">
843 <label for="betterdocs-ai-checkbox">${checkboxLabel}</label>
844 <p class="input-description"><?php echo esc_html__( 'Overwrite the existing doc with the AI Generated Content. If Disabled, new AI Generated content will be added in the section you are currently editing.', 'betterdocs' ); ?></p>
845 </div>
846
847 <div class="checkbox-input-field">
848 <input type="checkbox" id="betterdocs-ai-checkbox" name="betterdocs-ai-checkbox" data-overwrite="false">
849 <label for="betterdocs-ai-checkbox" class="toggle-label"></label>
850 </div>
851 </div>
852 </div>
853
854 <input type="hidden" name="ai_nonce" value="${nonce}" />
855 </div>
856 <div class="generate-button-container">
857 <button type="submit" class="generate-btn" onclick="generateArticle(event)">
858 ${aiIcon} <span>${promtArticleLabel}</span>
859 </button>
860 </div>
861 </form>
862 </div>
863
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>
865 </div>
866
867 <div id="betterdocs-ai-autowrite-form-container-overlay" class="hidden"></div>
868 `;
869 }
870
871 jQuery(document).on('click', '.regenerate-btn', function() {
872 jQuery('.betterdocs-ai-autowrite-form-container').removeClass('hidden');
873 jQuery('#betterdocs-ai-autowrite-form-container-overlay').removeClass('hidden');
874 });
875
876 jQuery(document).ready(function($) {
877 // Check if we are in the Edit Post screen
878
879 if ($('.block-editor-page').length > 0) {
880
881 const bdAIButton = $(`<div class="betterdocs-ai-button-container">
882 <button class="betterdocs-ai-button">
883 <img src="<?php echo esc_url( BETTERDOCS_ABSURL . 'assets/admin/images/betterdocs-icon-white.png' ); ?>" alt="<?php echo esc_attr__( 'betterdocs icon', 'betterdocs' ); ?>" />
884 <span><?php echo esc_html__( 'Write with AI', 'betterdocs' ); ?></span>
885 </button>
886 </div>`);
887
888
889 const closeBtn = $('bd-close-button');
890
891 const formHtml = writeWithAIForm();
892 $(formHtml).addClass('hidden');
893
894 $(document).on('click', '.betterdocs-ai-button', function() {
895 $('.betterdocs-ai-autowrite-form-container').removeClass('hidden');
896 $('#betterdocs-ai-autowrite-form-container-overlay').removeClass('hidden');
897 });
898
899 // $(document).on('click', '.betterdocs-ai-button', function() {
900 if ($('.betterdocs-ai-autowrite-form-container').length < 1) {
901 $('body').append(formHtml);
902
903 // Add event listeners to input elements
904 document.getElementById('betterdocs-ai-title').addEventListener('input', updateTextarea);
905 document.getElementById('betterdocs-ai-keyword').addEventListener('input', updateTextarea);
906 // document.getElementById('bd-autowrtite-content-section').addEventListener('change', updateTextarea);
907 // document.getElementById('bd-autowrtite-content-paragraph').addEventListener('change', updateTextarea);
908 // document.getElementById('betterdocs-ai-language').addEventListener('input', updateTextarea);
909 }
910 // });
911 const btn = document.createElement('button');
912 wp.data.subscribe(function() {
913 setTimeout(() => {
914 if ($('.edit-post-header__settings').length > 0) {
915 $('.edit-post-header__settings').prepend(bdAIButton);
916 } else if ($('.editor-header__settings').length > 0) {
917 $('.editor-header__settings').prepend(bdAIButton);
918 }
919
920 }, 1);
921 });
922 }
923
924 $(document).on('click', '#betterdocs-ai-autowrite-form-container-overlay', function() {
925 closeWriteWithAIForm();
926 });
927 });
928 </script>
929
930 <style>
931 .hidden {
932 display: none !important;
933 }
934
935 .disabled-input-field input,
936 .disabled-input-field textarea,
937 .disabled-input-field button,
938 .disabled-input-field label {
939 pointer-events: none;
940 opacity: 0.6;
941 }
942
943 .disabled-input-field label {
944 opacity: 1;
945 }
946
947 .betterdocs-ai-button-container {
948 margin-left: 10px;
949 white-space: nowrap;
950 max-width: 145px;
951 }
952
953 .autowrite-icon {
954 display: flex;
955 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);
962 justify-content: center;
963 border-radius: 9px;
964 }
965
966 .autowrite-icon svg {
967 width: 17px;
968 height: 17px;
969 }
970
971 .autowrite-heading {
972 display: flex;
973 gap: 10px;
974 align-items: center;
975 /* Keep the title clear of the in-header close button. */
976 padding-right: 64px;
977 }
978
979 .autowrite-heading h1 {
980 color: #101828;
981 font-family: 'IBM Plex Sans', sans-serif;
982 font-size: 18px;
983 font-style: normal;
984 font-weight: 600;
985 line-height: 1.3;
986 margin: 0;
987 }
988
989 .autowrite-heading p,
990 form#betterdocs-ai-form p {
991 margin: 0;
992 margin-bottom: 24px;
993 }
994
995 .autowrite-subheadding p {
996 font-family: 'IBM Plex Sans', sans-serif;
997 font-size: 13px;
998 color: #667085;
999 }
1000
1001 .prompt-field .input-description {
1002 margin: 0 !important;
1003 }
1004
1005 .autowrite-heading p {
1006 color: #475467;
1007 font-family: 'IBM Plex Sans', sans-serif;
1008 font-size: 16px;
1009 font-style: normal;
1010 font-weight: 400;
1011 }
1012
1013 .betterdocs-ai-button {
1014 background: #00B884;
1015 border: 1px solid transparent;
1016 color: #fff;
1017 box-shadow: none;
1018 font-size: 12px;
1019 margin-right: 8px;
1020 padding: 0px 15px;
1021 cursor: pointer;
1022 border-radius: 3px;
1023 height: 38px;
1024 display: flex;
1025 align-items: center;
1026 justify-content: space-between;
1027 gap: 10px;
1028 width: 135px;
1029 }
1030
1031 .betterdocs-ai-button img {
1032 width: 20px;
1033 height: 20px;
1034 }
1035
1036 .betterdocs-ai-autowrite-form-container {
1037 position: fixed;
1038 top: 50%;
1039 left: 50%;
1040 transform: translate(-50%, -50%);
1041 z-index: 100001;
1042 width: 650px;
1043 margin: 0 auto;
1044 background-color: #fff;
1045 border-radius: 20px;
1046 font-family: 'IBM Plex Sans', sans-serif;
1047 /* max-height: 600px; */
1048 max-height: 750px;
1049 max-width: 100%;
1050 }
1051
1052 .betterdocs-ai-autowrite-form-content {
1053 background-color: #fff;
1054 padding: 0;
1055 border-radius: 20px;
1056 overflow: auto;
1057 /* max-height: 700px; */
1058 height: 100%;
1059
1060 }
1061
1062 .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;
1068 }
1069
1070 #betterdocs-ai-form {
1071 display: flex;
1072 flex-direction: column;
1073 /* height: 100%; */
1074 overflow: hidden;
1075 }
1076
1077 .form-inner-content {
1078 overflow: auto;
1079 height: 100%;
1080 max-height: 435px;
1081 /* max-height: 330px; */
1082 padding: 0 40px;
1083 }
1084
1085 .form-inner-content>.form-group {
1086 margin-top: 20px;
1087 }
1088
1089
1090 #betterdocs-ai-autowrite-form-container-overlay {
1091 position: fixed;
1092 top: 0;
1093 right: 0;
1094 bottom: 0;
1095 left: 0;
1096 background-color: rgba(0, 0, 0, .7);
1097 z-index: 100000;
1098 }
1099
1100
1101
1102 #betterdocs-ai-form {
1103 display: flex;
1104 flex-direction: column;
1105 }
1106
1107 #betterdocs-ai-form .form-group {
1108 margin-bottom: 24px;
1109 }
1110
1111 #betterdocs-ai-form .bd-aiform-flex {
1112 display: flex;
1113 justify-content: space-between;
1114 }
1115
1116 #betterdocs-ai-form label {
1117 font-size: 14px;
1118 font-weight: 500;
1119 color: #667085;
1120 margin-bottom: 5px;
1121 display: block;
1122 line-height: 20px;
1123 }
1124
1125 select#bd-autowrtite-content-section,
1126 #bd-autowrtite-content-paragraph,
1127 #betterdocs-ai-language {
1128 width: 150px !important;
1129 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 }
1139
1140 #betterdocs-ai-form input[type="text"],
1141 #betterdocs-ai-form textarea {
1142 width: 100%;
1143 box-sizing: border-box;
1144 border: 1px solid #f2f4f7;
1145 background: #fff;
1146 border-radius: 5px;
1147 color: #101828;
1148 font-size: 14px;
1149 font-weight: 400;
1150 outline: none;
1151 box-shadow: none;
1152 }
1153
1154 #betterdocs-ai-form input[type="text"] {
1155 height: 40px;
1156 padding: 5px 15px;
1157 }
1158
1159 #betterdocs-ai-form textarea {
1160 padding: 10px 15px;
1161 min-height: 100px;
1162 }
1163
1164 /* Override autofill text color */
1165 #betterdocs-ai-form input:-webkit-autofill {
1166 -webkit-text-fill-color: #101828 !important;
1167 }
1168
1169 #betterdocs-ai-form input::placeholder,
1170 #betterdocs-ai-form textarea::placeholder {
1171 color: #667085;
1172 }
1173
1174 #betterdocs-ai-form input:focus,
1175 #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;
1179 }
1180
1181
1182 /* Footer bar — soft gray surface; Cancel pinned left, Generate right. */
1183 .generate-button-container {
1184 position: sticky;
1185 bottom: 0px;
1186 width: 100%;
1187 box-sizing: border-box;
1188 margin-left: 0;
1189 z-index: 999999 !important;
1190 padding: 16px 22px;
1191 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;
1198 }
1199
1200 .generate-button-container button {
1201 display: flex;
1202 gap: 8px;
1203 align-items: center;
1204 justify-content: center;
1205 opacity: 1 !important;
1206 }
1207
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 .input-description {
1234 font-size: 14px;
1235 color: #667085;
1236 }
1237
1238 .betterdocs-ai-checkbox-field .form-group {
1239 display: flex;
1240 align-items: start;
1241 /* gap: 200px; */
1242 justify-content: space-between;
1243 }
1244
1245 .betterdocs-ai-checkbox-field input {
1246 width: 20px;
1247 height: 20px;
1248 }
1249
1250 /* Add this CSS to your existing stylesheet or create a new one */
1251
1252 .betterdocs-ai-checkbox-field {
1253 position: relative;
1254 font-size: 16px;
1255 /* Adjust font size as needed */
1256 }
1257
1258 .betterdocs-ai-checkbox-field input[type=checkbox]:checked::before {
1259 padding: 2px;
1260 }
1261
1262 .betterdocs-ai-checkbox-field input[type=checkbox] {
1263 border: 1px solid #00b884;
1264 }
1265
1266 .betterdocs-ai-checkbox-field input[type=checkbox]:checked::before {
1267 content: '\2713';
1268 color: #00b884;
1269 }
1270
1271 /* Change the default checkbox color */
1272 .betterdocs-ai-checkbox-field input[type="checkbox"]:hover {
1273 -webkit-appearance: none;
1274 /* WebKit/Blink Browsers */
1275 -moz-appearance: none;
1276 /* Firefox */
1277 appearance: none;
1278 border: 1px solid #00b884;
1279 /* Set the height of the checkbox */
1280 /* Optional: Round the corners */
1281 outline: none;
1282 /* Remove the default outline */
1283 }
1284
1285 /* Style the checked state */
1286 .betterdocs-ai-checkbox-field input[type="checkbox"]:checked {
1287 /* Set the background color when checked */
1288 border: 1px solid #00b884;
1289 /* Set the border color when checked */
1290 }
1291
1292 .checkbox-field-label {
1293 width: calc(100% - 150px);
1294 }
1295
1296 .checkbox-field-label p {
1297 margin: 0 !important;
1298 }
1299
1300 .checkbox-input-field {
1301 width: 300px;
1302 text-align: right;
1303 }
1304
1305 .checkbox-input-field {
1306 position: relative;
1307 display: inline-block;
1308 width: 60px;
1309 height: 34px;
1310 }
1311
1312 .checkbox-input-field input {
1313 display: none;
1314 }
1315
1316 .toggle-label {
1317 position: absolute;
1318 cursor: pointer;
1319 top: 0;
1320 left: 0;
1321 right: 0;
1322 bottom: 0;
1323 background-color: #ccc;
1324 border-radius: 34px;
1325 transition: background-color 0.3s;
1326 scale: .9;
1327 width: 52px;
1328 height: 26px;
1329 }
1330
1331
1332 .checkbox-input-field input:checked+.toggle-label {
1333 background-color: #00b884;
1334 }
1335
1336 .toggle-label:after {
1337 content: "";
1338 position: absolute;
1339 height: 22px;
1340 width: 22px;
1341 left: 2px;
1342 bottom: 2px;
1343 background-color: white;
1344 border-radius: 50%;
1345 transition: transform 0.3s;
1346 }
1347
1348 .checkbox-input-field input:checked+.toggle-label:after {
1349 transform: translateX(26px);
1350 }
1351
1352 .generate-btn,
1353 .prompt-btn,
1354 .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 background-color: #00b884;
1362 color: #fff;
1363 border: 1px solid transparent;
1364 border-radius: 10px;
1365 font-size: 13px;
1366 font-weight: 600;
1367 cursor: pointer;
1368 text-decoration: none;
1369 box-shadow: none;
1370 transition: background 0.15s, transform 0.05s;
1371 box-sizing: border-box;
1372 }
1373
1374 .generate-btn:hover,
1375 .prompt-btn:hover,
1376 .keep-btn:hover {
1377 background-color: #00956b;
1378 }
1379
1380 .generate-btn[disabled] {
1381 cursor: unset;
1382 }
1383
1384 /* Close — 44px gray rounded square INSIDE the modal, top-right of
1385 the header, aligned with the header icon. */
1386 .bd-close-button {
1387 cursor: pointer;
1388 position: absolute;
1389 top: 24px;
1390 right: 24px;
1391 z-index: 2;
1392 width: 32px;
1393 height: 32px;
1394 display: flex;
1395 align-items: center;
1396 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;
1402 }
1403
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 div#betterdocs-ai-error-message,
1415 #betterdocs-ai-message {
1416 font-size: 14px;
1417 font-family: 'IBM Plex Sans', sans-serif;
1418 color: #667085;
1419 margin-bottom: 20px;
1420 }
1421
1422 div#betterdocs-ai-error-message a,
1423 #betterdocs-ai-message a {
1424 text-decoration: none;
1425 font-weight: 600;
1426 }
1427
1428 div#betterdocs-ai-error-message a:focus,
1429 #betterdocs-ai-message a:focus {
1430 outline: none;
1431 box-shadow: none;
1432 color: #2271b1;
1433 }
1434
1435 #betterdocs-ai-message span {
1436 color: #d63638;
1437 }
1438
1439 .warning-message {
1440 display: flex;
1441 align-items: center;
1442 gap: 10px;
1443 background: #fbebed;
1444 padding: 12px;
1445 border-radius: 5px;
1446 font-size: 14px;
1447 line-height: 1.4em;
1448 border-left: 4px solid #d63638;
1449 }
1450
1451 .warning-message svg {
1452 width: 20px;
1453 height: 20px;
1454 }
1455
1456 .warning-message span {
1457 color: #d63638;
1458 }
1459
1460 .warning-message .footer-message {
1461 width: calc(100% - 20px);
1462 }
1463
1464 @media only screen and (max-width: 991px) {
1465 .betterdocs-ai-autowrite-form-container {
1466 left: 0;
1467 right: 0;
1468 transform: translate(0%, -50%);
1469 }
1470
1471 .betterdocs-ai-autowrite-form-content {
1472 overflow-x: auto;
1473 }
1474 }
1475
1476 @media only screen and (max-width: 740px) {
1477
1478 /* .bd-close-button {
1479 right: px;
1480 top: 1px;
1481 border-radius: 5px;
1482 width: 12px;
1483 height: 12px;
1484 color: #ffffff;
1485 background: #00b884;
1486 } */
1487 .bd-close-button {
1488 right: 0px;
1489 top: 0px;
1490 width: 12px;
1491 height: 12px;
1492 font-size: 14px;
1493 }
1494
1495 }
1496 </style>
1497 <?php
1498 }
1499 }
1500