PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Core/WriteWithAI.php +638 -963 3.4.04.9.2 View file →
@@ -1,1100 +1,775 @@
1 1 <?php
2 2
3 -namespace WPDeveloper\BetterDocs\Core;
3 + namespace WPDeveloper\BetterDocs\Core;
4 4
5 -use WPDeveloper\BetterDocs\Utils\Base;
6 -use WPDeveloper\BetterDocs\Core\Settings;
7 -use WPDeveloper\BetterDocs\Core\PostType;
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit;
7 +}
8 8
9 -use WPDeveloper\BetterDocs\Utils\Helper;
10 9
11 -class WriteWithAI extends Base
12 -{
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\AI\ProviderFactory;
17 + use WPDeveloper\BetterDocs\AI\ModelRegistry;
18 + use WPDeveloper\BetterDocs\Utils\AIUsage;
19 + use WPDeveloper\BetterDocs\REST\AIEdit;
20 +
21 + class WriteWithAI extends Base {
22 +
13 23 public $settings;
14 24
15 - public function __construct(Settings $settings)
16 - {
25 + public function __construct( Settings $settings ) {
17 26 $this->settings = $settings;
18 27 // Get the post ID from the URL
19 - $post_id = isset($_GET['post']) ? intval($_GET['post']) : 0;
28 + $post_id = isset( $_GET[ 'post' ] ) ? intval( $_GET[ 'post' ] ) : 0; // phpcs:ignore
20 29
21 - if (!empty($_GET['post_type'])) {
22 - $post_type = $_GET['post_type'];
23 - } else if ($post_id > 0) {
24 - $post_type = get_post_type($post_id);
30 + if ( ! empty( $_GET[ 'post_type' ] ) ) { // phpcs:ignore
31 + $post_type = $_GET[ 'post_type' ]; // phpcs:ignore
32 + } elseif ( $post_id > 0 ) {
33 + $post_type = get_post_type( $post_id );
25 34 } else {
26 35 $post_type = '';
27 36 }
28 37
29 - if (!empty($this->isEnabledWriteWithAI()) && $post_type == 'docs') {
30 - add_action('admin_footer', [$this, 'ai_autowrite_button']);
38 + if ( ! empty( $this->isEnabledWriteWithAI() ) && 'docs' == $post_type ) {
39 + add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_ai_edit_assets' ) );
31 40 }
32 - add_action('wp_ajax_generate_openai_content', [$this, 'generate_openai_content_callback']);
41 + // Legacy AJAX handler kept for back-compat; the redesigned modal uses the REST route.
42 + add_action( 'wp_ajax_generate_openai_content', array( $this, 'generate_openai_content_callback' ) );
33 43 }
34 44
35 - public function isEnabledWriteWithAI()
36 - {
37 - $isEnableAutoWrite = $this->settings->get('enable_write_with_ai', true);
38 - return $isEnableAutoWrite;
39 - }
45 + public function enqueue_ai_edit_assets( $hook ) {
46 + if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
47 + return;
48 + }
40 49
41 - public function isValidAPIKey($apiKey)
42 - {
43 - if (empty($apiKey)) {
44 - $api_response['valid'] = false;
45 - $api_response['message'] = 'Please Insert your <a href="/admin.php?page=betterdocs-settings">OpenAI API Key</a> to use this Write with AI feature.';
50 + global $post_type;
51 + if ( 'docs' !== $post_type ) {
52 + return;
53 + }
46 54
47 - return $api_response;
48 - }
55 + $factory = new ProviderFactory( $this->settings );
56 + $api_key = $factory->api_key_for( $factory->active_platform() );
57 + $has_key = ! empty( $api_key );
49 58
50 - $ch = curl_init('https://api.openai.com/v1/engines');
51 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
52 - curl_setopt($ch, CURLOPT_HTTPHEADER, [
53 - 'Content-Type: application/json',
54 - 'Authorization: Bearer ' . $apiKey,
55 - ]);
59 + // Resolve the *active* AI platform + model (multi-platform aware) so the
60 + // modal reflects the current Settings → AI selection instead of the legacy
61 + // OpenAI-only `write_with_ai_model` key.
62 + $active_platform = $factory->active_platform();
63 + $active_model = $factory->active_model();
64 + $platform_labels = ModelRegistry::platforms();
65 + $model_labels = ModelRegistry::models( $active_platform );
56 66
57 - $api_response = [];
67 + // Glossary suggestions are existing-terms-only, and the glossaries taxonomy is only
68 + // registered for Pro (see Core\PostType::register_glossaries_taxonomy). So the "Suggest
69 + // glossaries" control must require, on top of the two settings, that Pro is active AND at
70 + // least one glossary term exists — otherwise the modal advertises an offer that can never
71 + // return anything. Mirrors the Docs-AI-suite availability check in Core\DocsAISuite.
72 + $glossary_count = wp_count_terms( array( 'taxonomy' => 'glossaries', 'hide_empty' => false ) );
73 + $has_glossary_terms = ! is_wp_error( $glossary_count ) && (int) $glossary_count > 0;
74 + $glossary_suggestions_enabled = (bool) $this->settings->get( 'enable_glossaries', false )
75 + && (bool) $this->settings->get( 'show_glossary_suggestions', true )
76 + && betterdocs()->is_pro_active()
77 + && $has_glossary_terms;
58 78
59 - $response = curl_exec($ch);
60 - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
79 + // Write with AI — loads even without a key so the modal can show its
80 + // "add an API key" banner (matches the legacy inline form behavior).
81 + betterdocs()->assets->enqueue( 'betterdocs-write-with-ai', 'blocks/write-with-ai.js' );
82 + betterdocs()->assets->enqueue( 'betterdocs-write-with-ai-style', 'blocks/write-with-ai-style.css' );
61 83
62 - curl_close($ch);
84 + wp_localize_script(
85 + 'betterdocs-write-with-ai',
86 + 'betterdocsWriteWithAI',
87 + array(
88 + 'rest_url' => esc_url_raw( rest_url( 'betterdocs/v1/write-with-ai' ) ),
89 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
90 + 'post_id' => get_the_ID(),
91 + 'has_key' => $has_key,
92 + // Term-suggestion (Docs AI Suite) wiring reused by the Write-with-AI
93 + // preview step. Endpoint + gate mirror REST\DocsAISuite / Core\DocsAISuite.
94 + 'rest_suggest_url' => esc_url_raw( rest_url( 'betterdocs/v1/ai-suggest-terms' ) ),
95 + 'suggest_terms_enabled' => (bool) $this->settings->get( 'enable_docs_ai_suite', true ),
96 + // Glossary suggestions follow the glossary feature AND real availability
97 + // (Pro active + at least one glossary term); see the computation above.
98 + 'glossary_suggestions_enabled' => $glossary_suggestions_enabled,
99 + 'platform' => $active_platform,
100 + 'platform_label' => isset( $platform_labels[ $active_platform ] ) ? $platform_labels[ $active_platform ] : ucfirst( (string) $active_platform ),
101 + 'model' => $active_model,
102 + 'model_label' => isset( $model_labels[ $active_model ] ) ? $model_labels[ $active_model ] : $active_model,
103 + 'max_token' => (int) $this->settings->get( 'ai_autowrite_max_token', 2500 ),
104 + 'settings_url' => esc_url( admin_url( 'admin.php?page=betterdocs-settings#betterdocs-ai' ) ),
105 + 'woo_active' => class_exists( 'WooCommerce' ),
106 + 'is_multilingual_active' => Helper::is_multilingual_active(),
107 + 'language_options' => Helper::get_active_languages(),
108 + 'instructions' => $this->get_instruction_choices(),
109 + // From Git is a Pro feature. Pro is detected here; Git enabled/auth
110 + // status is filled in by Pro via the filter below (default: off).
111 + 'is_pro_active' => betterdocs()->is_pro_active(),
112 + 'git' => apply_filters(
113 + 'betterdocs_write_with_ai_git',
114 + array(
115 + 'enabled' => false,
116 + 'connected' => false,
117 + 'settings_url' => admin_url( 'admin.php?page=betterdocs-settings#git-sync' ),
118 + )
119 + ),
120 + )
121 + );
63 122
64 - if ($httpCode == 200) {
65 - $api_response['valid'] = true;
66 - $api_response['message'] = 'Valid API Key';
67 - } else {
68 - $responseData = json_decode($response, true);
69 - // Access the message data (replace 'data' with the actual key used in the response)
70 - $messageData = $responseData['error'] ? $responseData['error'] : '';
71 - $api_response['valid'] = false;
72 - $api_response['message'] = $messageData['message'] ? $messageData['message'] : 'Invalid API Key';
123 + // AI Edit — only meaningful with a key configured.
124 + if ( ! $has_key ) {
125 + return;
73 126 }
74 127
75 - // print_r($response);
128 + betterdocs()->assets->enqueue( 'betterdocs-ai-edit', 'blocks/ai-edit.js' );
129 + betterdocs()->assets->enqueue( 'betterdocs-ai-edit-style', 'blocks/ai-edit-style.css' );
76 130
77 - return $api_response;
131 + wp_localize_script(
132 + 'betterdocs-ai-edit',
133 + 'betterdocsAIEdit',
134 + array(
135 + 'rest_url' => esc_url_raw( rest_url( 'betterdocs/v1/ai-edit' ) ),
136 + 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
137 + 'post_id' => get_the_ID(),
138 + 'instructions' => $this->get_instruction_choices(),
139 + 'actions' => AIEdit::get_localized_actions()
140 + )
141 + );
78 142 }
79 143
80 - public function get_api_key()
81 - {
82 - $api_key = $this->settings->get('ai_autowrite_api_key', '');
83 - return $api_key;
144 + public function isEnabledWriteWithAI() {
145 + $isEnableAutoWrite = $this->settings->get( 'enable_write_with_ai', true );
146 + return $isEnableAutoWrite;
84 147 }
85 148
86 -
87 - public function generate_openai_response($prompt, $keywords)
88 - {
89 - try {
90 - $api_key = $this->settings->get('ai_autowrite_api_key', '');
91 - $max_tokens = $this->settings->get('ai_autowrite_max_token', 1500);
92 -
93 - $api_endpoint = 'https://api.openai.com/v1/chat/completions'; // Update the endpoint based on OpenAI API version
94 -
95 - $request_options = array(
96 - 'headers' => array(
97 - 'Content-Type' => 'application/json',
98 - 'Authorization' => 'Bearer ' . $api_key,
99 - ),
100 - 'body' => json_encode(array(
101 - 'model' => 'gpt-3.5-turbo', // Add the model parameter here
102 - 'messages' => array(
103 - array('role' => 'system', 'content' => 'You are a helpful assistant who writes documentation for users.'),
104 - array('role' => 'user', 'content' => $prompt),
105 - ),
106 - 'max_tokens' => $max_tokens,
107 -
108 - )),
109 - 'timeout' => 50,
149 + public function isValidAPIKey( $apiKey ) {
150 + if ( empty( $apiKey ) ) {
151 + return array(
152 + 'valid' => false,
153 + 'message' => 'Please Insert your <a href="/admin.php?page=betterdocs-settings#betterdocs-ai">API Key</a> to use this Write with AI feature.'
110 154 );
155 + }
111 156
112 - $response = wp_remote_post($api_endpoint, $request_options);
157 + $factory = new ProviderFactory( $this->settings );
158 + return $factory->validate( $factory->active_platform(), $apiKey );
159 + }
113 160
114 - if (is_wp_error($response)) {
115 - return 'Error: ' . $response->get_error_message();
116 - } else {
117 - $body = wp_remote_retrieve_body($response);
118 -
119 - $data = json_decode($body, true);
120 -
121 - if (!empty($data['error'])) {
122 - return $data['error']['message'];
123 - }
124 -
125 - return $data['choices'][0]['message']['content']; // Update this line to get the assistant's message
126 - }
127 - } catch (Exception $error) {
128 - return 'Error: ' . $error->getMessage();
129 - }
161 + public function get_api_key() {
162 + $factory = new ProviderFactory( $this->settings );
163 + return $factory->api_key_for( $factory->active_platform() );
130 164 }
131 165
166 + /**
167 + * The built-in "Default" instruction body.
168 + *
169 + * Single source of truth shared by {@see Settings::get_default()} (which seeds
170 + * the editable "Default" instruction set) and {@see self::get_system_prompt()}
171 + * (the fallback when no saved Default content exists).
172 + *
173 + * @return string
174 + */
175 + public static function default_instruction_content() {
176 + return <<<'PROMPT'
177 +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.
132 178
179 +## Output format
133 180
134 - public function generate_openai_content_callback()
135 - {
136 - // Verify the nonce
137 - if (!isset($_POST['ai_nonce']) || !wp_verify_nonce($_POST['ai_nonce'], 'generate_openai_content_nonce')) {
138 - wp_send_json_error('Invalid nonce');
139 - wp_die();
140 - }
181 +Return only HTML body content. Never wrap output in `<!doctype>`, `<html>`, `<head>`, `<body>`, or markdown code fences (no ```html ... ```).
141 182
142 - $prompt = sanitize_text_field($_POST['prompt']);
143 - // $num_of_sections = sanitize_text_field($_POST['numOfSections']);
144 - // $num_of_paragraphs = sanitize_text_field($_POST['numOfParagraphs']);
145 - $keywords = sanitize_text_field($_POST['keywords']);
183 +Use semantic, Gutenberg-friendly tags only:
184 +- Headings: `<h2>`, `<h3>`, `<h4>` (do not emit `<h1>` — it is reserved for the document title)
185 +- Paragraphs: `<p>` for prose
186 +- Lists: `<ul>`/`<ol>` with `<li>` for any list of items, steps, or bullet points — never fake a list with paragraphs or `<br>`
187 +- Links: `<a href="https://...">link text</a>` with absolute URLs
188 +- Inline emphasis: `<strong>`, `<em>`, `<code>`
189 +- Code blocks: `<pre><code>...</code></pre>` for multi-line code or commands
190 +- Quotes: `<blockquote>`
191 +- Tables: `<table>` with `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`
192 +- Images: `<img src="..." alt="...">`
146 193
147 - $ai_instance = new WriteWithAI($this->settings);
194 +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.
148 195
149 - $generated_content = $ai_instance->generate_openai_response($prompt, $keywords);
196 +Do not emit `<style>`, `<script>`, `<iframe>`, `<form>`, or inline `style=""` / `class=""` attributes (other than the `highlight` class above).
150 197
151 - // Send the generated content as the AJAX response
152 - wp_send_json_success($generated_content);
153 - wp_die();
198 +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.
199 +PROMPT;
154 200 }
155 201
156 - public function ai_autowrite_button()
157 - {
202 + /**
203 + * The saved instruction sets, normalized to a list of { id, title, content }.
204 + *
205 + * Stored in the `write_with_ai_instructions` setting. Always returns at least
206 + * the built-in "Default" set so callers never have to special-case an empty
207 + * option (e.g. a site whose stored value predates this feature).
208 + *
209 + * @return array<int,array{id:string,title:string,content:string}>
210 + */
211 + public function get_instructions() {
212 + $stored = $this->settings->get( 'write_with_ai_instructions', array() );
158 213
159 - ?>
160 - <script>
161 - var docsTitle;
162 -
163 - function responseMessage(message) {
164 - return `<div class="warning-message">
165 - <span class="dashicons dashicons-warning"></span>
166 - <div class="footer-message">
167 - ${message}
168 - </div>
169 - </div>`;
170 - }
171 -
172 - function generateArticle(e) {
173 -
174 -
175 - const title = document.getElementById('betterdocs-ai-title').value;
176 - const keywords = document.getElementById('betterdocs-ai-keyword').value;
177 - // const sectionOptions = document.getElementById('bd-autowrtite-content-section');
178 - // const numOfSections = sectionOptions.options[sectionOptions.selectedIndex].value;
179 -
180 - // const paragraphOptions = document.getElementById('bd-autowrtite-content-paragraph');
181 - // const numOfParagraphs = paragraphOptions.options[paragraphOptions.selectedIndex].value;
182 -
183 - const contentTextArea = document.getElementById('betterdocs-ai-content');
184 - const docGenerateBtnTxt = document.querySelector('.generate-btn span');
185 -
186 - // Add nonce value to the data being sent
187 - const nonce = document.querySelector('input[name="ai_nonce"]').value;
188 - const isOverwrite = document.getElementById('betterdocs-ai-checkbox').checked;
189 - const generateDocLabel = "<?php echo esc_html__('Generate Doc', 'betterdocs'); ?>";
190 - const reGenerateDocLabel = "<?php echo esc_html__('Regenerate Doc', 'betterdocs'); ?>";
191 -
192 -
193 - // 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.`;
194 -
195 -
196 - // Check if the title is not empty
197 - if (title.trim() !== '' && keywords.trim() !== '') {
198 - e.preventDefault();
199 -
200 - if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
201 - jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
202 - }
203 -
204 - docGenerateBtnTxt.innerHTML = "<?php echo esc_html__('Generating...', 'betterdocs'); ?>";
205 - jQuery('.generate-btn').prop('disabled', true);
206 -
207 - jQuery.post({
208 - url: ajaxurl, // Make sure ajaxurl is defined in your script
209 - type: 'POST',
210 - data: {
211 - action: 'generate_openai_content',
212 - prompt: document.querySelector('#betterdocs-ai-content').value,
213 - // numOfSections: numOfSections,
214 - // numOfParagraphs: numOfParagraphs,
215 - keywords: keywords,
216 - ai_nonce: nonce, // Pass the nonce value
217 - },
218 - success: function(response) {
219 - // Update the content in the textarea
220 -
221 - if (response.success && (typeof response.data === 'string')) {
222 - docGenerateBtnTxt.innerHTML = "<?php echo esc_html__('Generated', 'betterdocs'); ?>";
223 - setTimeout(() => {
224 - insertContentToEditor(title, response.data, isOverwrite, keywords);
225 - docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
226 - jQuery('.generate-btn').removeAttr('disabled');
227 -
228 - // console.log(jQuery('.betterdocs-ai-autowrite-form-container').data('new-doc-page'));
229 -
230 - // if(jQuery('.betterdocs-ai-autowrite-form-container')?.data('new-doc-page')) {
231 - // docGenerateBtnTxt.innerHTML = reGenerateDocLabel;
232 - // }
233 - }, 2000);
234 - } else {
235 - // alert(response.data.message);
236 - jQuery('#betterdocs-ai-error-message').parent().removeClass('hidden');
237 - jQuery('#betterdocs-ai-error-message').html(responseMessage(response.data.message));
238 - docGenerateBtnTxt.innerHTML = generateDocLabel;
239 - jQuery('.generate-btn').removeAttr('disabled');
240 -
241 - }
242 - },
243 - error: function(error) {
244 - console.error(error);
245 - },
246 - });
247 -
214 + $instructions = array();
215 + if ( is_array( $stored ) ) {
216 + foreach ( $stored as $item ) {
217 + if ( ! is_array( $item ) || empty( $item['id'] ) ) {
218 + continue;
248 219 }
220 + $instructions[] = array(
221 + 'id' => sanitize_key( (string) $item['id'] ),
222 + 'title' => isset( $item['title'] ) ? (string) $item['title'] : '',
223 + 'content' => isset( $item['content'] ) ? (string) $item['content'] : '',
224 + );
249 225 }
226 + }
250 227
251 - // Function to update the textarea
252 - function updateTextarea() {
253 - const title = document.getElementById('betterdocs-ai-title').value;
254 - const keywords = document.getElementById('betterdocs-ai-keyword').value;
255 - const sectionOptions = document.getElementById('bd-autowrtite-content-section');
256 - let language = 'English';
257 -
258 - if (language === '') {
259 - language = 'English';
260 - }
261 -
262 - let keywordsText = keywords !== '' ? ` and incorporate the keywords: ${keywords}` : '';
263 -
264 - const contentTextArea = document.getElementById('betterdocs-ai-content');
265 -
266 - if (!jQuery('#betterdocs-ai-error-message').parent().hasClass('hidden')) {
267 - jQuery('#betterdocs-ai-error-message').parent().addClass('hidden');
268 - }
269 -
270 - 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.`;
228 + // Guarantee a Default set is always present.
229 + $has_default = false;
230 + foreach ( $instructions as $item ) {
231 + if ( 'default' === $item['id'] ) {
232 + $has_default = true;
233 + break;
271 234 }
235 + }
236 + if ( ! $has_default ) {
237 + array_unshift(
238 + $instructions,
239 + array(
240 + 'id' => 'default',
241 + 'title' => __( 'Default/Core', 'betterdocs' ),
242 + 'content' => self::default_instruction_content(),
243 + )
244 + );
245 + }
272 246
273 - function convertMarkdownToHTML(markdownContent) {
274 - // Simple Markdown to HTML conversion (you may need to enhance this based on your needs)
275 - const htmlContent = markdownContent
276 - .replace(/^# (.+)$/gm, '<h1>$1</h1>')
277 - .replace(/^## (.+)$/gm, '<h2>$1</h2>')
278 - .replace(/^### (.+)$/gm, '<h3>$1</h3>')
279 - .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
280 - .replace(/\*(.+?)\*/g, '<em>$1</em>');
247 + return $instructions;
248 + }
281 249
282 - return htmlContent;
250 + /**
251 + * The selectable (non-default) instruction sets exposed to the editor UI.
252 + *
253 + * Only id + title are sent to the browser; the prompt body stays server-side
254 + * and is resolved at request time by {@see self::get_instruction_messages()}.
255 + *
256 + * @return array<int,array{id:string,title:string}>
257 + */
258 + public function get_instruction_choices() {
259 + $choices = array();
260 + foreach ( $this->get_instructions() as $item ) {
261 + if ( 'default' === $item['id'] ) {
262 + continue;
283 263 }
284 -
285 - function replaceHeadingTitle(content, title, overviewTitle) {
286 - let regex = new RegExp(title, 'g');
287 - let updateContent = content.replace(regex, overviewTitle);
288 - return updateContent;
264 + if ( '' === trim( $item['content'] ) ) {
265 + continue; // skip empty sets — they'd inject nothing.
289 266 }
267 + $choices[] = array(
268 + 'id' => $item['id'],
269 + 'title' => '' !== trim( $item['title'] ) ? $item['title'] : $item['id'],
270 + );
271 + }
272 + return $choices;
273 + }
290 274
291 - function removeFirstHeading(htmlString) {
292 - var newDiv = document.createElement('div');
293 - newDiv.innerHTML = htmlString;
294 - var firstHeading = newDiv.querySelector('h1');
295 - if (firstHeading) {
296 - firstHeading.parentNode.removeChild(firstHeading);
297 - }
298 - return newDiv.innerHTML;
299 - }
275 + /**
276 + * Resolve selected instruction ids into extra `system` messages.
277 + *
278 + * The "Default" set is intentionally excluded — it is always sent as the base
279 + * system prompt by {@see self::get_system_prompt()}. Unknown or empty ids are
280 + * silently ignored. Output order follows the requested $ids order.
281 + *
282 + * @param array $ids
283 + * @return array<int,array{role:string,content:string}>
284 + */
285 + public function get_instruction_messages( $ids ) {
286 + if ( empty( $ids ) || ! is_array( $ids ) ) {
287 + return array();
288 + }
300 289
290 + $by_id = array();
291 + foreach ( $this->get_instructions() as $item ) {
292 + $by_id[ $item['id'] ] = $item;
293 + }
301 294
302 - function wrapwithHeighlight(inputString, keywords) {
303 - let modifiedString = inputString.replace(/<(h\d)(.*?)>(.*?)<\/\1>/g, '<$1$2><span class="highlight">$3</span></$1>');
304 -
305 - const keywordArray = keywords.split(',').map(keyword => keyword.trim());
306 -
307 - keywordArray.forEach(keyword => {
308 - modifiedString = modifiedString.replace(new RegExp(`\\b(${keyword})\\b`, 'gi'), '<span class="highlight">$1</span>');
309 - });
310 -
311 - return modifiedString;
295 + $messages = array();
296 + $seen = array();
297 + foreach ( $ids as $id ) {
298 + $id = sanitize_key( (string) $id );
299 + if ( 'default' === $id || isset( $seen[ $id ] ) || ! isset( $by_id[ $id ] ) ) {
300 + continue;
312 301 }
313 -
314 - function getBodyContent(htmlString) {
315 - var match = htmlString.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
316 -
317 - console.log(match[1].replace(/<p>\s+/g, '<p>'));
318 -
319 - return match ? match[1].replace(/<p>\s+/g, '<p>') : '';
302 + $content = trim( $by_id[ $id ]['content'] );
303 + if ( '' === $content ) {
304 + continue;
320 305 }
306 + $seen[ $id ] = true;
307 + $messages[] = array(
308 + 'role' => 'system',
309 + 'content' => $content,
310 + );
311 + }
321 312
313 + return $messages;
314 + }
322 315
323 - function insertContentToEditor(title, htmlContent, isOverwrite, keywords) {
316 + /**
317 + * Coerce a list of extra messages into well-formed `system` chat messages.
318 + *
319 + * Accepts either pre-built [ 'role'=>'system', 'content'=>… ] entries (as
320 + * returned by {@see self::get_instruction_messages()}) or bare strings, and
321 + * drops anything empty. Keeps the OpenAI payload builders tolerant of either
322 + * shape so callers can pass instruction ids or ready-made messages.
323 + *
324 + * @param array $extra_system
325 + * @return array<int,array{role:string,content:string}>
326 + */
327 + protected function normalize_extra_system( $extra_system ) {
328 + if ( empty( $extra_system ) || ! is_array( $extra_system ) ) {
329 + return array();
330 + }
324 331
325 - const {
326 - dispatch,
327 - select
328 - } = wp.data;
329 -
330 - htmlContent = getBodyContent(htmlContent);
331 - htmlContent = removeFirstHeading(htmlContent);
332 - htmlContent = wrapwithHeighlight(htmlContent, keywords);
333 - const isPostContent = `<?php echo isset($_GET['post']) ? get_the_content($_GET['post']) : ''; ?>`;
334 -
335 - const blocks = wp.blocks.rawHandler({
336 - HTML: htmlContent
337 - });
338 -
339 - dispatch('core/editor').editPost({
340 - title: title,
341 - });
342 -
343 - const selectedBlockClientId = select('core/block-editor').getSelectedBlockClientId();
344 -
345 - const allBlocks = select('core/block-editor').getBlocks();
346 -
347 - if (isOverwrite || isPostContent === '') {
348 -
349 - allBlocks.forEach((block) => {
350 - dispatch('core/block-editor').removeBlock(block.clientId);
351 - });
352 - dispatch('core/block-editor').insertBlocks(blocks);
353 -
354 - } else if (selectedBlockClientId) {
355 - const selectedIndex = select('core/block-editor').getBlockIndex(selectedBlockClientId);
356 - dispatch('core/block-editor').insertBlocks(blocks, selectedIndex + 1);
357 - } else {
358 - dispatch('core/block-editor').insertBlocks(blocks);
359 - }
360 -
361 - closeWriteWithAIForm('afterInsertContent');
332 + $messages = array();
333 + foreach ( $extra_system as $entry ) {
334 + if ( is_string( $entry ) ) {
335 + $content = trim( $entry );
336 + $role = 'system';
337 + } elseif ( is_array( $entry ) && isset( $entry['content'] ) ) {
338 + $content = trim( (string) $entry['content'] );
339 + $role = isset( $entry['role'] ) ? (string) $entry['role'] : 'system';
340 + } else {
341 + continue;
362 342 }
363 343
364 - function closeWriteWithAIForm(after) {
365 - jQuery('.betterdocs-ai-autowrite-form-container').addClass('hidden');
366 - jQuery('#betterdocs-ai-autowrite-form-container-overlay').addClass('hidden');
367 - jQuery('.betterdocs-ai-button').addClass('regenerate-btn');
344 + if ( '' === $content ) {
345 + continue;
368 346 }
369 347
348 + $messages[] = array(
349 + 'role' => $role,
350 + 'content' => $content,
351 + );
352 + }
370 353
371 - document.addEventListener('DOMContentLoaded', function() {
354 + return $messages;
355 + }
372 356
373 - // Subscribe to changes in the editor state
374 - let previousDocsTitle = '';
375 -
376 - wp.data.subscribe(() => {
377 - // Get the updated post title
378 - // const docsTitle = wp.data.select('core/editor').getEditedPostAttribute('title');
379 -
380 - const docsTitle = wp.data.select('core/editor').getEditedPostAttribute('title');
381 -
382 - // Check if the title has changed
383 - if (docsTitle !== previousDocsTitle) {
384 - let currentKeywords = jQuery('#betterdocs-ai-keyword').val();
385 - if (!currentKeywords) {
386 - currentKeywords = '{Documentation Keywords}';
387 - }
388 - 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.`;
389 - jQuery('#betterdocs-ai-content').val(currentPrompt);
390 -
391 - jQuery('#betterdocs-ai-title').val(docsTitle);
392 -
393 - previousDocsTitle = docsTitle;
394 - }
395 -
396 -
397 - });
398 - });
399 -
400 -
401 - // This example assumes that this code is executed when your script is loaded.
402 - // You may want to place it within the appropriate context in your application.
403 -
404 -
405 - function writeWithAIForm() {
406 -
407 - let title = `<?php echo isset($_GET['post']) ? get_the_title($_GET['post']) : ''; ?>`;
408 - if (docsTitle) {
409 - title = docsTitle;
410 - }
411 -
412 - const promtTitle = `<?php echo isset($_GET['post']) ? get_the_title($_GET['post']) : '{Documentation Title}'; ?>`;
413 - const titlePlaceholder = "<?php echo esc_attr__('Enter a descriptive title for your documentation.', 'betterdocs'); ?>";
414 - const keywords = "<?php echo esc_attr('{Documentation Keywords}'); ?>";
415 - const keywordsPlaceholder = "<?php echo esc_attr__('Add keywords to generate precise & relevant documentation (comma-separated).', 'betterdocs'); ?>";
416 - const language = "<?php echo esc_attr('English'); ?>";
417 - const titleLabel = "<?php echo esc_html__('Documentation Title:', 'betterdocs'); ?>";
418 - const keywordLabel = "<?php echo esc_html__('Keywords:', 'betterdocs'); ?>";
419 - const secLabel = "<?php echo esc_html__('# of Sections:', 'betterdocs'); ?>";
420 - const paraLabel = "<?php echo esc_html__('# of Paragraph Per Section: ', 'betterdocs'); ?>";
421 - const langLabel = "<?php echo esc_html__('Language:', 'betterdocs'); ?>";
422 - const promtLabel = "<?php echo esc_html__('Prompt:', 'betterdocs'); ?>";
423 - const promtBtnLabel = "<?php echo esc_html__('Generate Prompt', 'betterdocs'); ?>";
424 - let promtArticleLabel = "<?php echo esc_html__('Generate Doc', 'betterdocs'); ?>";
425 - const checkboxLabel = "<?php echo esc_html__('Overwrite your existing Doc:', 'betterdocs'); ?>";
426 - const nonce = "<?php echo esc_attr(wp_create_nonce('generate_openai_content_nonce')); ?>";
427 -
428 - <?php $is_valid = $this->get_api_key();
429 - $disable_field = 'disabled-input-field';
430 - if ($this->get_api_key()) {
431 - $disable_field = '';
432 - }
433 - ?>
434 -
435 - let hiddenClass = 'hidden';
436 - let newDocPageAtt = 'data-new-doc-page="true"';
437 - const isPostContent = `<?php echo isset($_GET['post']) ? get_the_content($_GET['post']) : ''; ?>`;
438 -
439 - if (isPostContent !== '') {
440 - hiddenClass = '';
441 - newDocPageAtt = '';
442 - }
443 -
444 -
445 - // 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.`;
446 -
447 - // 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.`;
448 -
449 - 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.`;
450 -
451 - const aiIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
452 - <g clip-path="url(#clip0_2460_1729)">
453 - <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"/>
454 - </g>
455 - <defs>
456 - <clipPath id="clip0_2460_1729">
457 - <rect width="20" height="20" fill="white" transform="translate(0.5)"/>
458 - </clipPath>
459 - </defs>
460 - </svg>`;
461 -
462 -
463 -
464 - return `
465 - <div class="betterdocs-ai-autowrite-form-container hidden" ${newDocPageAtt}>
466 - <div class="betterdocs-ai-autowrite-form-content">
467 - <div class="betterdocs-ai-autowrite-top-part">
468 - <div class="autowrite-heading">
469 - <span class="autowrite-icon">
470 - <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
471 - <g clip-path="url(#clip0_2453_20882)">
472 - <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"/>
473 - </g>
474 - <defs>
475 - <clipPath id="clip0_2453_20882">
476 - <rect width="32" height="32" fill="white"/>
477 - </clipPath>
478 - </defs>
479 - </svg>
480 - </span>
481 - <h1><?php echo esc_html__('Write Documentation with BetterDocs AI', 'betterdocs'); ?></h1>
482 -
483 - </div>
484 - <div class="autowrite-subheadding">
485 - <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>
486 - </div>
487 -
488 - <?php if (empty($this->get_api_key())) : ?>
489 - <div id="betterdocs-ai-message">
490 - <div class="warning-message">
491 - <span class="dashicons dashicons-warning"></span>
492 - <div>
493 - <?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'); ?>
494 - </div>
495 - </div>
496 -
497 - </div>
498 - <?php endif; ?>
499 -
500 - <div class="form-group hidden">
501 - <div id="betterdocs-ai-error-message"></div>
502 - </div>
503 -
504 - </div>
505 - <form id="betterdocs-ai-form" class="<?php echo esc_attr($disable_field); ?>">
506 - <div class="form-inner-content">
507 - <div class="form-group">
508 - <label for="betterdocs-ai-title">${titleLabel}</label>
509 - <input type="text" placeholder="${titlePlaceholder}" id="betterdocs-ai-title" name="betterdocs-ai-title" required value="${title}">
510 - </div>
511 -
512 - <div class="form-group">
513 - <label for="betterdocs-ai-keyword">${keywordLabel}</label>
514 - <input type="text" placeholder="${keywordsPlaceholder}" id="betterdocs-ai-keyword" name="betterdocs-ai-keyword" required>
515 - </div>
516 -
517 - <div class="form-group prompt-field">
518 - <label for="betterdocs-ai-content">${promtLabel}</label>
519 - <textarea id="betterdocs-ai-content" name="betterdocs-ai-content" rows="6" required>${prompt}</textarea>
520 - <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>
521 - </div>
522 - <div class="betterdocs-ai-checkbox-field ${hiddenClass}">
523 - <div class="form-group">
524 - <div class="checkbox-field-label">
525 - <label for="betterdocs-ai-checkbox">${checkboxLabel}</label>
526 - <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>
527 - </div>
528 -
529 - <div class="checkbox-input-field">
530 - <input type="checkbox" id="betterdocs-ai-checkbox" name="betterdocs-ai-checkbox" data-overwrite="false">
531 - <label for="betterdocs-ai-checkbox" class="toggle-label"></label>
532 - </div>
533 - </div>
534 - </div>
535 -
536 - <input type="hidden" name="ai_nonce" value="${nonce}" />
537 - </div>
538 - <div class="generate-button-container">
539 - <button type="submit" class="generate-btn" onclick="generateArticle(event)">
540 - ${aiIcon} <span>${promtArticleLabel}</span>
541 - </button>
542 - </div>
543 - </form>
544 - </div>
545 -
546 - <div class="bd-close-button" onclick="closeWriteWithAIForm('afterClosedClicked')">✕</div>
547 - </div>
548 -
549 - <div id="betterdocs-ai-autowrite-form-container-overlay" class="hidden"></div>
550 - `;
357 + public function get_system_prompt() {
358 + // Prefer the saved (editable) "Default" instruction set; fall back to the
359 + // built-in body when it's missing or has been cleared.
360 + $prompt = self::default_instruction_content();
361 + foreach ( $this->get_instructions() as $item ) {
362 + if ( 'default' === $item['id'] && '' !== trim( $item['content'] ) ) {
363 + $prompt = $item['content'];
364 + break;
551 365 }
366 + }
552 367
553 - jQuery(document).on('click', '.regenerate-btn', function() {
554 - jQuery('.betterdocs-ai-autowrite-form-container').removeClass('hidden');
555 - jQuery('#betterdocs-ai-autowrite-form-container-overlay').removeClass('hidden');
556 - });
368 + return apply_filters( 'betterdocs_write_with_ai_system_prompt', $prompt );
369 + }
557 370
558 - jQuery(document).ready(function($) {
559 - // Check if we are in the Edit Post screen
560 - if ($('.block-editor-page').length > 0) {
561 - const bdAIButton = $(`<div class="betterdocs-ai-button-container">
562 - <button class="betterdocs-ai-button">
563 - <img src="<?php echo esc_url(BETTERDOCS_ABSURL . 'assets/admin/images/betterdocs-icon-white.png'); ?>" alt="<?php echo esc_attr__('betterdocs icon', 'embedpress'); ?>" />
564 - <span><?php echo esc_html__('Write with AI', 'embedpress'); ?></span>
565 - </button>
566 - </div>`);
567 - const closeBtn = $('bd-close-button');
371 + /**
372 + * Build the system + user messages and chat options shared by the
373 + * Write-with-AI generator and the in-editor AI-Edit endpoint. Routes through
374 + * the active AI platform (ProviderFactory), so it works for every provider.
375 + *
376 + * @param string $prompt
377 + * @param int|null $max_tokens Optional cap override (e.g. a "large" doc).
378 + * @param array $extra_system Optional extra system messages.
379 + * @return array array( array $messages, array $options )
380 + */
381 + private function ai_request_args( $prompt, $max_tokens = null, $extra_system = array() ) {
382 + $messages = array_merge(
383 + array( array( 'role' => 'system', 'content' => $this->get_system_prompt() ) ),
384 + $this->normalize_extra_system( $extra_system ),
385 + array( array( 'role' => 'user', 'content' => $prompt ) )
386 + );
568 387
569 - const formHtml = writeWithAIForm();
570 - $(formHtml).addClass('hidden');
388 + return array( $messages, $this->ai_chat_options( $max_tokens ) );
389 + }
571 390
572 - $(document).on('click', '.betterdocs-ai-button', function() {
573 - $('.betterdocs-ai-autowrite-form-container').removeClass('hidden');
574 - $('#betterdocs-ai-autowrite-form-container-overlay').removeClass('hidden');
575 - });
391 + /**
392 + * Assemble the chat options passed to the active provider. Document
393 + * generation is long-running on every platform: reasoning models (gpt-5*)
394 + * and larger non-OpenAI models (e.g. Claude Opus) routinely need well over
395 + * the old 50s default to return a full doc, which timed them out mid-
396 + * generation. Give every request a generous ceiling; fast models simply
397 + * finish early and are unaffected.
398 + *
399 + * @param int|null $max_tokens Optional token cap override.
400 + * @param float|null $temperature Optional sampling temperature.
401 + * @return array
402 + */
403 + private function ai_chat_options( $max_tokens = null, $temperature = null ) {
404 + $timeout = 300;
405 + if ( function_exists( 'set_time_limit' ) ) {
406 + set_time_limit( 300 );
407 + }
576 408
577 - // $(document).on('click', '.betterdocs-ai-button', function() {
578 - if ($('.betterdocs-ai-autowrite-form-container').length < 1) {
579 - $('body').append(formHtml);
409 + $options = array(
410 + 'max_tokens' => null !== $max_tokens ? (int) $max_tokens : (int) $this->settings->get( 'ai_autowrite_max_token', 2500 ),
411 + 'context' => 'write_with_ai',
412 + 'timeout' => $timeout,
413 + );
580 414
581 - // Add event listeners to input elements
582 - document.getElementById('betterdocs-ai-title').addEventListener('input', updateTextarea);
583 - document.getElementById('betterdocs-ai-keyword').addEventListener('input', updateTextarea);
584 - // document.getElementById('bd-autowrtite-content-section').addEventListener('change', updateTextarea);
585 - // document.getElementById('bd-autowrtite-content-paragraph').addEventListener('change', updateTextarea);
586 - // document.getElementById('betterdocs-ai-language').addEventListener('input', updateTextarea);
587 - }
588 - // });
589 - const btn = document.createElement('button');
590 - wp.data.subscribe(function() {
591 - setTimeout(() => {
592 - if ($('.edit-post-header__settings').length > 0) {
593 - $('.edit-post-header__settings').prepend(bdAIButton);
594 - }
595 - }, 1);
596 - });
597 - }
415 + if ( null !== $temperature ) {
416 + $options['temperature'] = $temperature;
417 + }
598 418
599 - $(document).on('click', '#betterdocs-ai-autowrite-form-container-overlay', function() {
600 - closeWriteWithAIForm();
601 - });
602 - });
603 - </script>
419 + return $options;
420 + }
604 421
605 - <style>
606 - .hidden {
607 - display: none !important;
608 - }
422 + public function generate_openai_response( $prompt, $keywords, $max_tokens = null, $extra_system = array() ) {
423 + try {
424 + list( $messages, $options ) = $this->ai_request_args( $prompt, $max_tokens, $extra_system );
609 425
610 - .disabled-input-field input,
611 - .disabled-input-field textarea,
612 - .disabled-input-field button,
613 - .disabled-input-field label {
614 - pointer-events: none;
615 - opacity: 0.6;
616 - }
426 + $result = ( new ProviderFactory( $this->settings ) )->make()->chat( $messages, $options );
617 427
618 - .disabled-input-field label {
619 - opacity: 1;
428 + if ( is_wp_error( $result ) ) {
429 + return $result->get_error_message();
620 430 }
621 431
622 - .betterdocs-ai-button-container {
623 - margin-left: 10px;
624 - }
432 + return $result['content'];
433 + } catch ( \Exception $error ) {
434 + return 'Error: ' . $error->getMessage();
435 + }
436 + }
625 437
626 - .autowrite-icon {
627 - display: flex;
628 - align-items: center;
629 - width: 55px;
630 - height: 55px;
631 - background: #FFE4E8;
632 - justify-content: center;
633 - border-radius: 50px;
634 - }
438 + /**
439 + * Generate documentation from an uploaded image using a vision-capable model.
440 + *
441 + * Mirrors generate_openai_response() but sends the picture alongside the text
442 + * prompt as an OpenAI-format multimodal user message
443 + * (`content: [ {type:text}, {type:image_url} ]`). The OpenAI-compatible
444 + * provider forwards that message array to the wire verbatim, so no provider
445 + * change is needed. Only OpenAI vision models are wired — Claude and Gemini
446 + * use a different image envelope, so they are refused with a clear error
447 + * instead of being sent a payload they would reject.
448 + *
449 + * @param string $prompt Composed instruction prompt.
450 + * @param array $image { data_uri:string, mime:string }.
451 + * @param int|null $max_tokens Optional token cap.
452 + * @param array $extra_system Extra system messages (instruction sets).
453 + * @return string|\WP_Error Generated content, or WP_Error on guard/failure.
454 + */
455 + public function generate_vision_response( $prompt, $image, $max_tokens = null, $extra_system = array() ) {
456 + if ( empty( $image['data_uri'] ) ) {
457 + return new \WP_Error( 'ai_vision_no_image', __( 'No image data to send to the AI.', 'betterdocs' ) );
458 + }
635 459
636 - .autowrite-icon svg {
637 - width: 28px;
638 - height: 28px;
639 - }
460 + $factory = new ProviderFactory( $this->settings );
461 + $platform = $factory->active_platform();
462 + $model = $factory->active_model( $platform );
640 463
641 - .autowrite-heading {
642 - display: flex;
643 - gap: 10px;
644 - align-items: center;
645 - }
464 + if ( ! $this->platform_supports_vision( $platform, $model ) ) {
465 + return new \WP_Error(
466 + 'ai_no_vision',
467 + sprintf(
468 + /* translators: 1: AI platform id, 2: model name. */
469 + __( 'The configured AI model (%1$s / %2$s) can\'t read images. Switch to an OpenAI vision model such as GPT-4o or GPT-4o mini in BetterDocs → Settings → AI Content Suite, or upload a PDF/DOCX/TXT instead.', 'betterdocs' ),
470 + $platform,
471 + '' !== (string) $model ? $model : 'default'
472 + )
473 + );
474 + }
646 475
647 - .autowrite-heading h1 {
648 - color: #1D2939;
649 - font-family: 'IBM Plex Sans', sans-serif;
650 - font-size: 24px;
651 - font-style: normal;
652 - font-weight: 500;
653 - line-height: normal;
654 - margin: 0;
655 - }
476 + try {
477 + $messages = array_merge(
478 + array( array( 'role' => 'system', 'content' => $this->get_system_prompt() ) ),
479 + $this->normalize_extra_system( $extra_system ),
480 + array(
481 + array(
482 + 'role' => 'user',
483 + 'content' => array(
484 + array( 'type' => 'text', 'text' => (string) $prompt ),
485 + array( 'type' => 'image_url', 'image_url' => array( 'url' => (string) $image['data_uri'] ) ),
486 + ),
487 + ),
488 + )
489 + );
656 490
657 - .autowrite-heading p,
658 - form#betterdocs-ai-form p {
659 - margin: 0;
660 - margin-bottom: 24px;
661 - }
491 + $result = $factory->make()->chat( $messages, $this->ai_chat_options( $max_tokens ) );
662 492
663 - .autowrite-subheadding p {
664 - font-family: 'IBM Plex Sans', sans-serif;
665 - font-size: 14px;
493 + if ( is_wp_error( $result ) ) {
494 + return $result;
666 495 }
667 496
668 - .prompt-field .input-description {
669 - margin: 0 !important;
670 - }
497 + return $result['content'];
498 + } catch ( \Exception $error ) {
499 + return new \WP_Error( 'ai_vision_failed', 'Error: ' . $error->getMessage() );
500 + }
501 + }
671 502
672 - .autowrite-heading p {
673 - color: #475467;
674 - font-family: 'IBM Plex Sans', sans-serif;
675 - font-size: 16px;
676 - font-style: normal;
677 - font-weight: 400;
678 - }
503 + /**
504 + * Whether the active platform + model can accept image input in the OpenAI
505 + * multimodal format. Deliberately conservative: only OpenAI vision model
506 + * families qualify, because Claude and Gemini require a different image
507 + * envelope this path does not build. gpt-3.5 (text-only) is excluded.
508 + *
509 + * @param string $platform
510 + * @param string $model
511 + * @return bool
512 + */
513 + protected function platform_supports_vision( $platform, $model ) {
514 + if ( 'openai' !== $platform ) {
515 + return false;
516 + }
679 517
680 - .betterdocs-ai-button {
681 - background: #00B884;
682 - border: 1px solid transparent;
683 - color: #fff;
684 - box-shadow: none;
685 - font-size: 12px;
686 - margin-right: 8px;
687 - padding: 0px 15px;
688 - cursor: pointer;
689 - border-radius: 3px;
690 - height: 38px;
691 - display: flex;
692 - align-items: center;
693 - justify-content: space-between;
694 - gap: 10px;
695 - }
518 + $model = strtolower( (string) $model );
696 519
697 - .betterdocs-ai-button img {
698 - width: 20px;
699 - height: 20px;
700 - }
520 + if ( '' === $model || false !== strpos( $model, 'gpt-3.5' ) ) {
521 + return false;
522 + }
701 523
702 - .betterdocs-ai-autowrite-form-container {
703 - position: fixed;
704 - top: 50%;
705 - left: 50%;
706 - transform: translate(-50%, -50%);
707 - z-index: 100001;
708 - width: 650px;
709 - margin: 0 auto;
710 - background-color: #fff;
711 - border-radius: 5px;
712 - font-family: 'IBM Plex Sans', sans-serif;
713 - /* max-height: 600px; */
714 - max-height: 750px;
715 - max-width: 100%;
524 + foreach ( array( 'gpt-4o', 'gpt-4.1', 'gpt-4-turbo', 'gpt-4-vision', 'chatgpt-4o', 'gpt-5', 'o1', 'o3', 'o4' ) as $family ) {
525 + if ( false !== strpos( $model, $family ) ) {
526 + return true;
716 527 }
528 + }
717 529
718 - .betterdocs-ai-autowrite-form-content {
719 - background-color: #fff;
720 - padding: 20px 0px;
721 - border-radius: 5px;
722 - padding-bottom: 0;
723 - overflow: auto;
724 - /* max-height: 700px; */
725 - height: 100%;
530 + return false;
531 + }
726 532
727 - }
533 + public function get_outline_system_prompt() {
534 + $prompt = <<<'PROMPT'
535 +You are a Senior Technical Writer. Produce a documentation OUTLINE only — not the full article.
728 536
729 - .betterdocs-ai-autowrite-top-part {
730 - /* padding-bottom: 20px; */
731 - border-bottom: 1px solid #ddd;
732 - /* margin-bottom: 20px; */
733 - padding: 0 40px;
734 - }
537 +Return ONLY a JSON array (no prose, no markdown, no code fences). Each element is an object:
538 + { "level": "h2" | "h3", "text": "Section heading" }
735 539
736 - #betterdocs-ai-form {
737 - display: flex;
738 - flex-direction: column;
739 - /* height: 100%; */
740 - overflow: hidden;
741 - }
540 +Rules:
541 +- 5 to 9 top-level "h2" sections that comprehensively cover the topic.
542 +- Add "h3" sub-sections only where they genuinely help; keep each one directly after its parent h2.
543 +- Headings are concise, descriptive, and free of numbering.
544 +- Do not include an h1/title and do not include any text outside the JSON array.
545 +PROMPT;
742 546
743 - .form-inner-content {
744 - overflow: auto;
745 - height: 100%;
746 - max-height: 435px;
747 - /* max-height: 330px; */
748 - padding: 0 40px;
749 - }
547 + return apply_filters( 'betterdocs_write_with_ai_outline_system_prompt', $prompt );
548 + }
750 549
751 - .form-inner-content>.form-group {
752 - margin-top: 20px;
753 - }
550 + /**
551 + * Generate a documentation outline as a structured array of sections.
552 + *
553 + * @param string $user_prompt The composed prompt (title/keywords/instructions).
554 + * @return array { success:bool, outline?:array<int,array{level:string,text:string}>, error?:string }
555 + */
556 + public function generate_outline_response( $user_prompt, $extra_system = array() ) {
557 + $result = $this->generate_text( $user_prompt, $this->get_outline_system_prompt(), $extra_system );
754 558
559 + if ( empty( $result['success'] ) ) {
560 + return array(
561 + 'success' => false,
562 + 'error' => isset( $result['error'] ) ? $result['error'] : 'AI error',
563 + );
564 + }
755 565
756 - #betterdocs-ai-autowrite-form-container-overlay {
757 - position: fixed;
758 - top: 0;
759 - right: 0;
760 - bottom: 0;
761 - left: 0;
762 - background-color: rgba(0, 0, 0, .7);
763 - z-index: 100000;
764 - }
566 + $outline = $this->parse_outline( (string) $result['content'] );
765 567
568 + if ( empty( $outline ) ) {
569 + return array(
570 + 'success' => false,
571 + 'error' => 'The AI returned an outline we could not read. Please try again.',
572 + );
573 + }
766 574
575 + return array(
576 + 'success' => true,
577 + 'outline' => $outline,
578 + );
579 + }
767 580
768 - #betterdocs-ai-form {
769 - display: flex;
770 - flex-direction: column;
771 - }
581 + /**
582 + * Parse a model outline response (ideally a JSON array) into a clean list of
583 + * { level, text } sections. Tolerates code fences and stray prose.
584 + *
585 + * @param string $content
586 + * @return array<int,array{level:string,text:string}>
587 + */
588 + protected function parse_outline( $content ) {
589 + $content = trim( $content );
590 + $content = preg_replace( '/^```(?:json)?\s*/i', '', $content );
591 + $content = preg_replace( '/```\s*$/', '', $content );
772 592
773 - #betterdocs-ai-form .form-group {
774 - margin-bottom: 24px;
775 - }
593 + // Grab the first JSON array if the model wrapped it in prose.
594 + if ( preg_match( '/\[[\s\S]*\]/', $content, $m ) ) {
595 + $content = $m[0];
596 + }
776 597
777 - #betterdocs-ai-form .bd-aiform-flex {
778 - display: flex;
779 - justify-content: space-between;
780 - }
598 + $decoded = json_decode( $content, true );
599 + if ( ! is_array( $decoded ) ) {
600 + return array();
601 + }
781 602
782 - #betterdocs-ai-form label {
783 - font-weight: 600;
784 - margin-bottom: 5px;
785 - display: block;
786 - font-size: 14px;
787 - line-height: 20px;
603 + $outline = array();
604 + foreach ( $decoded as $item ) {
605 + if ( ! is_array( $item ) || empty( $item['text'] ) ) {
606 + continue;
788 607 }
789 -
790 - select#bd-autowrtite-content-section,
791 - #bd-autowrtite-content-paragraph,
792 - #betterdocs-ai-language {
793 - width: 150px !important;
794 - height: 40px;
608 + $level = isset( $item['level'] ) && 'h3' === strtolower( (string) $item['level'] ) ? 'h3' : 'h2';
609 + $text = sanitize_text_field( (string) $item['text'] );
610 + if ( '' === $text ) {
611 + continue;
795 612 }
613 + $outline[] = array( 'level' => $level, 'text' => $text );
614 + }
796 615
797 - #betterdocs-ai-form input[type="text"],
798 - #betterdocs-ai-form textarea {
799 - width: 100%;
800 - padding: 8px;
801 - box-sizing: border-box;
802 - border: 1px solid #D0D5DD;
803 - border-radius: 4px;
804 - color: #667085;
805 - font-weight: 400;
806 - }
616 + return $outline;
617 + }
807 618
808 - /* Override autofill text color */
809 - #betterdocs-ai-form input:-webkit-autofill {
810 - -webkit-text-fill-color: #667085 !important;
811 - }
619 + public function generate_openai_response_ai_edit( $prompt, $extra_system = array() ) {
620 + $factory = new ProviderFactory( $this->settings );
621 + $model = $factory->active_model();
812 622
813 - #betterdocs-ai-form input::placeholder {
814 - color: #acb2bf;
815 - }
623 + $messages = array_merge(
624 + array( array( 'role' => 'system', 'content' => $this->get_system_prompt() ) ),
625 + $this->normalize_extra_system( $extra_system ),
626 + array( array( 'role' => 'user', 'content' => $prompt ) )
627 + );
816 628
817 - #betterdocs-ai-form textarea {
818 - color: #667085;
819 - }
629 + $result = $factory->make()->chat( $messages, $this->ai_chat_options() );
820 630
821 - #betterdocs-ai-form input:focus,
822 - #betterdocs-ai-form textarea:focus {
823 - box-shadow: none;
824 - }
631 + if ( is_wp_error( $result ) ) {
632 + return array(
633 + 'success' => false,
634 + 'error' => $result->get_error_message(),
635 + 'model' => $model
636 + );
637 + }
825 638
826 - #betterdocs-ai-form textarea:focus {
827 - color: inherit;
828 - box-shadow: none;
639 + $usage = isset( $result['usage'] ) && is_array( $result['usage'] ) ? $result['usage'] : array();
829 640
830 - }
641 + return array(
642 + 'success' => true,
643 + 'content' => $result['content'],
644 + 'model' => isset( $result['model'] ) ? $result['model'] : $model,
645 + 'prompt_tokens' => isset( $usage['prompt_tokens'] ) ? $usage['prompt_tokens'] : null,
646 + 'completion_tokens' => isset( $usage['completion_tokens'] ) ? $usage['completion_tokens'] : null,
647 + 'total_tokens' => isset( $usage['total_tokens'] ) ? $usage['total_tokens'] : null,
648 + 'finish_reason' => isset( $result['finish_reason'] ) ? $result['finish_reason'] : null
649 + );
650 + }
831 651
652 + /**
653 + * Generic chat completion using the active Write-with-AI platform/model/token
654 + * settings. Shared by lightweight, plain-text generators (glossary definitions,
655 + * FAQ answers, outlines) that need the same dynamic model as Write with AI /
656 + * AI Edit but a caller-supplied system prompt instead of the doc-authoring HTML
657 + * prompt. Routes through ProviderFactory so every provider is supported.
658 + *
659 + * @param string $user_prompt The user message.
660 + * @param string $system_prompt Optional system message (omitted when empty).
661 + * @param array $extra_system Optional extra system messages.
662 + * @return array { success:bool, content?:string, error?:string, model:string, *_tokens?:int }
663 + */
664 + public function generate_text( $user_prompt, $system_prompt = '', $extra_system = array() ) {
665 + $factory = new ProviderFactory( $this->settings );
666 + $model = $factory->active_model();
832 667
833 - .generate-button-container {
834 - position: sticky;
835 - bottom: 0px;
836 - width: 100%;
837 - margin-left: 0;
838 - z-index: 999999 !important;
839 - padding: 20px 0;
840 - display: flex;
841 - justify-content: end;
842 - border-top: 1px solid #ddd;
843 - background: white;
844 - border-bottom-right-radius: 5px;
845 - border-bottom-left-radius: 5px;
846 - }
668 + $messages = array();
669 + if ( $system_prompt !== '' ) {
670 + $messages[] = array( 'role' => 'system', 'content' => $system_prompt );
671 + }
672 + foreach ( $this->normalize_extra_system( $extra_system ) as $extra ) {
673 + $messages[] = $extra;
674 + }
675 + $messages[] = array( 'role' => 'user', 'content' => $user_prompt );
847 676
848 - .generate-button-container button {
849 - display: flex;
850 - gap: 8px;
851 - align-items: center;
852 - justify-content: center;
853 - opacity: 1 !important;
854 - margin-right: 40px;
855 - }
677 + $result = $factory->make()->chat( $messages, $this->ai_chat_options() );
856 678
857 - .input-description {
858 - font-size: 14px;
859 - color: #667085;
860 - }
679 + if ( is_wp_error( $result ) ) {
680 + return array(
681 + 'success' => false,
682 + 'error' => $result->get_error_message(),
683 + 'model' => $model
684 + );
685 + }
861 686
862 - .betterdocs-ai-checkbox-field .form-group {
863 - display: flex;
864 - align-items: start;
865 - /* gap: 200px; */
866 - justify-content: space-between;
867 - }
687 + $usage = isset( $result['usage'] ) && is_array( $result['usage'] ) ? $result['usage'] : array();
868 688
869 - .betterdocs-ai-checkbox-field input {
870 - width: 20px;
871 - height: 20px;
872 - }
689 + return array(
690 + 'success' => true,
691 + 'content' => $result['content'],
692 + 'model' => isset( $result['model'] ) ? $result['model'] : $model,
693 + 'prompt_tokens' => isset( $usage['prompt_tokens'] ) ? $usage['prompt_tokens'] : null,
694 + 'completion_tokens' => isset( $usage['completion_tokens'] ) ? $usage['completion_tokens'] : null,
695 + 'total_tokens' => isset( $usage['total_tokens'] ) ? $usage['total_tokens'] : null,
696 + 'finish_reason' => isset( $result['finish_reason'] ) ? $result['finish_reason'] : null
697 + );
698 + }
873 699
874 - /* Add this CSS to your existing stylesheet or create a new one */
700 + /**
701 + * Generate a chat completion with a caller-supplied system + user prompt.
702 + * Mirrors generate_openai_response_ai_edit() (same model/token floor/timeout
703 + * handling and return shape) but does NOT force the documentation-writer
704 + * system prompt, so callers such as the Docs AI Suite (taxonomy suggestions,
705 + * excerpts) can supply task-appropriate instructions. Routes through the active
706 + * provider via ProviderFactory.
707 + *
708 + * @param string $system_prompt System instruction for the model.
709 + * @param string $user_prompt User message / content payload.
710 + * @param float|null $temperature Optional sampling temperature (ignored for gpt-5*).
711 + * @return array{success:bool,content?:string,error?:string,model:string,...}
712 + */
713 + public function generate_openai_response_raw( $system_prompt, $user_prompt, $temperature = null ) {
714 + $factory = new ProviderFactory( $this->settings );
715 + $model = $factory->active_model();
875 716
876 - .betterdocs-ai-checkbox-field {
877 - position: relative;
878 - font-size: 16px;
879 - /* Adjust font size as needed */
880 - }
717 + $messages = array(
718 + array( 'role' => 'system', 'content' => $system_prompt ),
719 + array( 'role' => 'user', 'content' => $user_prompt )
720 + );
881 721
882 - .betterdocs-ai-checkbox-field input[type=checkbox]:checked::before {
883 - padding: 2px;
884 - }
722 + $result = $factory->make()->chat( $messages, $this->ai_chat_options( null, $temperature ) );
885 723
886 - .betterdocs-ai-checkbox-field input[type=checkbox] {
887 - border: 1px solid #00b884;
888 - }
724 + if ( is_wp_error( $result ) ) {
725 + return array(
726 + 'success' => false,
727 + 'error' => $result->get_error_message(),
728 + 'model' => $model
729 + );
730 + }
889 731
890 - .betterdocs-ai-checkbox-field input[type=checkbox]:checked::before {
891 - content: '\2713';
892 - color: #00b884;
893 - }
732 + $usage = isset( $result['usage'] ) && is_array( $result['usage'] ) ? $result['usage'] : array();
894 733
895 - /* Change the default checkbox color */
896 - .betterdocs-ai-checkbox-field input[type="checkbox"]:hover {
897 - -webkit-appearance: none;
898 - /* WebKit/Blink Browsers */
899 - -moz-appearance: none;
900 - /* Firefox */
901 - appearance: none;
902 - border: 1px solid #00b884;
903 - /* Set the height of the checkbox */
904 - /* Optional: Round the corners */
905 - outline: none;
906 - /* Remove the default outline */
907 - }
734 + return array(
735 + 'success' => true,
736 + 'content' => $result['content'],
737 + 'model' => isset( $result['model'] ) ? $result['model'] : $model,
738 + 'prompt_tokens' => isset( $usage['prompt_tokens'] ) ? $usage['prompt_tokens'] : null,
739 + 'completion_tokens' => isset( $usage['completion_tokens'] ) ? $usage['completion_tokens'] : null,
740 + 'total_tokens' => isset( $usage['total_tokens'] ) ? $usage['total_tokens'] : null,
741 + 'finish_reason' => isset( $result['finish_reason'] ) ? $result['finish_reason'] : null
742 + );
743 + }
908 744
909 - /* Style the checked state */
910 - .betterdocs-ai-checkbox-field input[type="checkbox"]:checked {
911 - /* Set the background color when checked */
912 - border: 1px solid #00b884;
913 - /* Set the border color when checked */
914 - }
745 + public function generate_openai_content_callback() {
746 + // Verify the nonce
747 + $ai_nonce = isset( $_POST['ai_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['ai_nonce'] ) ) : '';
748 + if ( ! wp_verify_nonce( $ai_nonce, 'generate_openai_content_nonce' ) ) {
749 + wp_send_json_error( 'Invalid nonce' );
750 + wp_die();
751 + }
915 752
916 - .checkbox-field-label {
917 - width: calc(100% - 150px);
918 - }
753 + if ( ! current_user_can( 'edit_posts' ) ) {
754 + wp_send_json_error( 'Insufficient permissions' );
755 + wp_die();
756 + }
919 757
920 - .checkbox-field-label p {
921 - margin: 0 !important;
922 - }
758 + $prompt = isset( $_POST['prompt'] ) ? sanitize_text_field( wp_unslash( $_POST['prompt'] ) ) : '';
759 + $keywords = isset( $_POST['keywords'] ) ? sanitize_text_field( wp_unslash( $_POST['keywords'] ) ) : '';
923 760
924 - .checkbox-input-field {
925 - width: 300px;
926 - text-align: right;
927 - }
761 + $ai_instance = new WriteWithAI( $this->settings );
928 762
929 - .checkbox-input-field {
930 - position: relative;
931 - display: inline-block;
932 - width: 60px;
933 - height: 34px;
934 - }
763 + $generated_content = $ai_instance->generate_openai_response( $prompt, $keywords );
935 764
936 - .checkbox-input-field input {
937 - display: none;
938 - }
765 + // Count a successful generation (skip obvious upstream errors).
766 + if ( is_string( $generated_content ) && $generated_content !== '' && strpos( $generated_content, 'Error:' ) !== 0 ) {
767 + $post_id = isset( $_POST[ 'post_id' ] ) ? intval( $_POST[ 'post_id' ] ) : 0; //phpcs:ignore
768 + AIUsage::record( 'write_with_ai', $post_id );
769 + }
939 770
940 - .toggle-label {
941 - position: absolute;
942 - cursor: pointer;
943 - top: 0;
944 - left: 0;
945 - right: 0;
946 - bottom: 0;
947 - background-color: #ccc;
948 - border-radius: 34px;
949 - transition: background-color 0.3s;
950 - scale: .9;
951 - width: 52px;
952 - height: 26px;
953 - }
954 -
955 -
956 - .checkbox-input-field input:checked+.toggle-label {
957 - background-color: #00b884;
958 - }
959 -
960 - .toggle-label:after {
961 - content: "";
962 - position: absolute;
963 - height: 22px;
964 - width: 22px;
965 - left: 2px;
966 - bottom: 2px;
967 - background-color: white;
968 - border-radius: 50%;
969 - transition: transform 0.3s;
970 - }
971 -
972 - .checkbox-input-field input:checked+.toggle-label:after {
973 - transform: translateX(26px);
974 - }
975 -
976 - .generate-btn,
977 - .prompt-btn,
978 - .keep-btn {
979 - background-color: #00b884;
980 - color: #fff;
981 - border: none;
982 - padding: 10px 15px;
983 - text-align: center;
984 - text-decoration: none;
985 - display: inline-block;
986 - font-size: 16px;
987 - cursor: pointer;
988 - border-radius: 4px;
989 - }
990 -
991 - .generate-btn[disabled] {
992 - cursor: unset;
993 - }
994 -
995 - .bd-close-button {
996 - cursor: pointer;
997 - font-size: 18px;
998 - font-weight: bold;
999 - float: right;
1000 - position: absolute;
1001 - right: 1px;
1002 - top: 1px;
1003 - padding: 10px;
1004 - background: #ffffff;
1005 - border-radius: 25px;
1006 - width: 20px;
1007 - height: 20px;
1008 - display: flex;
1009 - align-items: center;
1010 - justify-content: center;
1011 - color: #281617;
1012 - right: -60px;
1013 - }
1014 -
1015 - div#betterdocs-ai-error-message,
1016 - #betterdocs-ai-message {
1017 - font-size: 14px;
1018 - font-family: 'IBM Plex Sans', sans-serif;
1019 - color: #667085;
1020 - margin-bottom: 20px;
1021 - }
1022 -
1023 - div#betterdocs-ai-error-message a,
1024 - #betterdocs-ai-message a {
1025 - text-decoration: none;
1026 - font-weight: 600;
1027 - }
1028 -
1029 - div#betterdocs-ai-error-message a:focus,
1030 - #betterdocs-ai-message a:focus {
1031 - outline: none;
1032 - box-shadow: none;
1033 - color: #2271b1;
1034 - }
1035 -
1036 - #betterdocs-ai-message span {
1037 - color: #d63638;
1038 - }
1039 -
1040 - .warning-message {
1041 - display: flex;
1042 - align-items: center;
1043 - gap: 10px;
1044 - background: #fbebed;
1045 - padding: 12px;
1046 - border-radius: 5px;
1047 - font-size: 14px;
1048 - line-height: 1.4em;
1049 - border-left: 4px solid #d63638;
1050 - }
1051 -
1052 - .warning-message svg {
1053 - width: 20px;
1054 - height: 20px;
1055 - }
1056 -
1057 - .warning-message span {
1058 - color: #d63638;
1059 - }
1060 -
1061 - .warning-message .footer-message {
1062 - width: calc(100% - 20px);
1063 - }
1064 -
1065 - @media only screen and (max-width: 991px) {
1066 - .betterdocs-ai-autowrite-form-container {
1067 - left: 0;
1068 - right: 0;
1069 - transform: translate(0%, -50%);
1070 - }
1071 -
1072 - .betterdocs-ai-autowrite-form-content {
1073 - overflow-x: auto;
1074 - }
1075 - }
1076 -
1077 - @media only screen and (max-width: 740px) {
1078 -
1079 - /* .bd-close-button {
1080 - right: px;
1081 - top: 1px;
1082 - border-radius: 5px;
1083 - width: 12px;
1084 - height: 12px;
1085 - color: #ffffff;
1086 - background: #00b884;
1087 - } */
1088 - .bd-close-button {
1089 - right: 0px;
1090 - top: 0px;
1091 - width: 12px;
1092 - height: 12px;
1093 - font-size: 14px;
1094 - }
1095 -
1096 - }
1097 - </style>
1098 -<?php
771 + // Send the generated content as the AJAX response
772 + wp_send_json_success( $generated_content );
773 + wp_die();
1099 774 }
1100 775 }