PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.6.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.6.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 3.5.2 All 199 releases
betterdocs / includes / REST / AIEdit.php

AIEdit.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.6.2, at includes/REST/AIEdit.php

229 lines 9.4 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\REST;
4
5 use WP_REST_Request;
6 use WPDeveloper\BetterDocs\Core\BaseAPI;
7 use WPDeveloper\BetterDocs\Utils\AIUsage;
8
9 class AIEdit extends BaseAPI {
10
11 const MAX_SELECTION_LENGTH = 12000;
12 const MAX_INSTRUCTION_LENGTH = 1000;
13
14 public function register() {
15 $this->post(
16 '/ai-edit',
17 array( $this, 'generate' ),
18 array(
19 'post_id' => array(
20 'type' => 'integer',
21 'required' => true
22 ),
23 'action' => array(
24 'type' => 'string',
25 'required' => true
26 ),
27 'selection' => array(
28 'type' => 'string',
29 'required' => false,
30 'default' => ''
31 ),
32 'selection_type' => array(
33 'type' => 'string',
34 'required' => false,
35 'default' => 'block'
36 ),
37 'instruction' => array(
38 'type' => 'string',
39 'required' => false,
40 'default' => ''
41 ),
42 'option' => array(
43 'type' => 'string',
44 'required' => false,
45 'default' => ''
46 )
47 )
48 );
49 }
50
51 public function permission_check() {
52 $post_id = isset( $_REQUEST[ 'post_id' ] ) ? intval( $_REQUEST[ 'post_id' ] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST permission_check; read-only id, request auth handled by the route's permission_callback.
53
54 if ( $post_id <= 0 ) {
55 return current_user_can( 'edit_posts' );
56 }
57
58 return current_user_can( 'edit_post', $post_id );
59 }
60
61 public function generate( WP_REST_Request $request ) {
62 $write_ai = betterdocs()->ai_autowrtie;
63
64 if ( empty( $write_ai ) || ! $write_ai->isEnabledWriteWithAI() ) {
65 return $this->error(
66 'ai_disabled',
67 __( 'Write with AI is disabled. Enable it from BetterDocs settings.', 'betterdocs' ),
68 400
69 );
70 }
71
72 $api_key = $write_ai->get_api_key();
73 if ( empty( $api_key ) ) {
74 return $this->error(
75 'ai_no_key',
76 __( 'OpenAI API key is missing. Add one in BetterDocs settings.', 'betterdocs' ),
77 400
78 );
79 }
80
81 $action = sanitize_key( (string) $request->get_param( 'action' ) );
82 $selection_type = sanitize_key( (string) $request->get_param( 'selection_type' ) );
83 $selection = (string) $request->get_param( 'selection' );
84 $instruction = (string) $request->get_param( 'instruction' );
85 $option = sanitize_text_field( (string) $request->get_param( 'option' ) );
86
87 if ( strlen( $selection ) > self::MAX_SELECTION_LENGTH ) {
88 $selection = substr( $selection, 0, self::MAX_SELECTION_LENGTH );
89 }
90 if ( strlen( $instruction ) > self::MAX_INSTRUCTION_LENGTH ) {
91 $instruction = substr( $instruction, 0, self::MAX_INSTRUCTION_LENGTH );
92 }
93
94 $instruction = wp_kses_post( $instruction );
95 $selection = wp_kses_post( $selection );
96
97 if ( 'inline' !== $selection_type ) {
98 $selection_type = 'block';
99 }
100
101 $presets = self::get_presets();
102
103 if ( 'custom' !== $action && ! isset( $presets[ $action ] ) ) {
104 return $this->error(
105 'ai_bad_action',
106 __( 'Unknown AI action.', 'betterdocs' ),
107 400
108 );
109 }
110
111 if ( trim( wp_strip_all_tags( $instruction ) ) === '' ) {
112 return $this->error(
113 'ai_empty_instruction',
114 __( 'Please provide a prompt for the AI.', 'betterdocs' ),
115 400
116 );
117 }
118
119 if ( trim( wp_strip_all_tags( $selection ) ) === '' && 'continue' !== $action ) {
120 return $this->error(
121 'ai_empty_selection',
122 __( 'No content selected for the AI to work on.', 'betterdocs' ),
123 400
124 );
125 }
126
127 $prompt = $this->build_prompt( $action, $presets, $selection, $selection_type, $instruction, $option );
128
129 $result = $write_ai->generate_openai_response_ai_edit( $prompt );
130
131 if ( empty( $result[ 'success' ] ) ) {
132 $message = isset( $result[ 'error' ] ) ? (string) $result[ 'error' ] : __( 'Unknown AI error.', 'betterdocs' );
133 return $this->error( 'ai_upstream', $message, 502 );
134 }
135
136 $content = $this->clean_output( (string) $result[ 'content' ] );
137
138 if ( '' === $content ) {
139 return $this->error(
140 'ai_empty_response',
141 __( 'The AI returned no content. Try again or rephrase your instruction.', 'betterdocs' ),
142 502
143 );
144 }
145
146 AIUsage::record( 'ai_edit', (int) $request->get_param( 'post_id' ), $action );
147
148 return $this->success(
149 array(
150 'content' => $content,
151 'action' => $action,
152 'usage' => array(
153 'prompt_tokens' => isset( $result[ 'prompt_tokens' ] ) ? $result[ 'prompt_tokens' ] : null,
154 'completion_tokens' => isset( $result[ 'completion_tokens' ] ) ? $result[ 'completion_tokens' ] : null,
155 'total_tokens' => isset( $result[ 'total_tokens' ] ) ? $result[ 'total_tokens' ] : null
156 ),
157 'model' => isset( $result[ 'model' ] ) ? $result[ 'model' ] : null
158 )
159 );
160 }
161
162 protected function build_prompt( $action, $presets, $selection, $selection_type, $instruction, $option ) {
163 $core = trim( $instruction );
164
165 $preset = 'custom' !== $action && isset( $presets[ $action ] ) ? $presets[ $action ] : null;
166 $format_hint = isset( $preset[ 'format' ] ) && $preset[ 'format' ]
167 ? $preset[ 'format' ]
168 : ( 'inline' === $selection_type ? __( 'Return only plain text (no block markup, no quotes, no explanations).', 'betterdocs' )
169 : __( 'Return only valid Gutenberg-compatible HTML using standard tags such as <p>, <h2>, <ul>, <ol>, <li>, <strong>, <em>, <a>, <code>. Do not include explanations, preambles, code fences, or markdown.', 'betterdocs' ) );
170
171 $prompt = $core . "\n\n";
172 $prompt .= __( 'Formatting requirements:', 'betterdocs' ) . ' ' . $format_hint . "\n\n";
173 $prompt .= __( 'Content to work on:', 'betterdocs' ) . "\n";
174 $prompt .= "---\n" . $selection . "\n---";
175
176 return $prompt;
177 }
178
179 protected function clean_output( $content ) {
180 $content = trim( $content );
181
182 $content = preg_replace( '/^```(?:html|HTML)?\s*\n?/', '', $content );
183 $content = preg_replace( '/\n?```\s*$/', '', $content );
184
185 return trim( $content );
186 }
187
188 public static function get_presets() {
189 return array(
190 'improve' => array(
191 'prompt' => 'Improve the writing quality, clarity, grammar, and structure of the content below while preserving its original meaning and intent.'
192 ),
193 'rewrite' => array(
194 'prompt' => 'Rewrite the content below with different wording while keeping the same meaning, tone, and level of detail.'
195 ),
196 'shorten' => array(
197 'prompt' => 'Make the content below more concise. Preserve all essential information; remove redundancy and filler.'
198 ),
199 'expand' => array(
200 'prompt' => 'Expand the content below with more detail, examples, and clear explanations. Keep the original voice.'
201 ),
202 'simplify' => array(
203 'prompt' => 'Rewrite the content below in simpler language so it is easy to understand for a non-technical reader.'
204 ),
205 'fix_grammar' => array(
206 'prompt' => 'Fix only the grammar, spelling, and punctuation in the content below. Do not rewrite or change the meaning or style.'
207 ),
208 'change_tone' => array(
209 'prompt' => 'Rewrite the content below in a {option} tone while preserving its meaning.',
210 'needs_option' => true
211 ),
212 'translate' => array(
213 'prompt' => 'Translate the content below into {option}. Preserve meaning, tone, and any HTML structure.',
214 'needs_option' => true
215 ),
216 'summarize' => array(
217 'prompt' => 'Summarize the content below into 2 to 3 clear sentences.'
218 ),
219 'continue' => array(
220 'prompt' => 'Continue writing naturally from where the content below ends. Match its tone, style, and level of detail.'
221 ),
222 'create_table' => array(
223 'prompt' => 'Extract the statistics, figures, or comparable data from the content below and represent them as a single HTML table. Infer clear column headers. Include every distinct data point. If no tabular data can be reasonably inferred, return the best possible structured representation.',
224 'format' => 'Return only one complete <table> element with <thead> and <tbody>. Use <th scope="col"> for header cells. Do not include <style>, <script>, inline CSS classes, comments, explanations, preambles, code fences, or markdown.'
225 )
226 );
227 }
228 }
229