PluginProbe
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs / 5.0.1
BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs v5.0.1
5.0.1 4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 All 39 releases
blockspare / inc / ai / class-blockspare-ai-rest-api.php

class-blockspare-ai-rest-api.php in BlockSpare – Gutenberg Blocks, AI Content Generator & Site Builder for News, Magazine & Blogs 5.0.1, at inc/ai/class-blockspare-ai-rest-api.php

535 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (! defined('ABSPATH')) {
4 exit;
5 }
6 //using_model_preference
7 /**
8 * Handles the block editor assets and the REST API endpoint that
9 * powers the AI content generation, summarization, and Q&A tools.
10 */
11 class Blockspare_AI_REST_API
12 {
13
14 /**
15 * @var Blockspare_AI_Settings
16 */
17 private $settings;
18
19
20 /**
21 * @param Blockspare_AI_Settings $settings Settings instance, used to
22 * read the saved/default prompts.
23 */
24 public function __construct(Blockspare_AI_Settings $settings)
25 {
26 $this->settings = $settings;
27
28 add_action('init', array($this, 'blockspare_register_assets'));
29 add_action('enqueue_block_editor_assets', array($this, 'blockspare_enqueue_editor_assets'));
30 add_action('rest_api_init', array($this, 'register_rest_routes'));
31 }
32
33
34 /**
35 * Register the compiled JS build asset.
36 */
37 public function blockspare_register_assets()
38 {
39 $asset_file_path = plugin_dir_path(__FILE__) . 'build/index.asset.php';
40
41 // Fallback default asset structure.
42 $asset_file = file_exists($asset_file_path)
43 ? include $asset_file_path
44 : array(
45 'dependencies' => array(),
46 'version' => '1.0',
47 );
48
49 // Define your manual dependencies.
50 $manual_deps = array(
51 'wp-plugins',
52 'wp-editor',
53 'wp-edit-post',
54 'wp-element',
55 'wp-components',
56 'wp-data',
57 'wp-blocks',
58 'wp-block-editor',
59 'wp-api-fetch',
60 'wp-i18n',
61 );
62
63 // Merge and deduplicate dependencies.
64 $merged_deps = array_unique(
65 array_merge($asset_file['dependencies'], $manual_deps)
66 );
67
68 wp_register_script(
69 'blockspare-ai-generator-editor',
70 BLOCKSPARE_PLUGIN_URL . 'dist/aigen.js',
71 $merged_deps,
72 '1.0.0'
73 );
74 }
75
76
77 /**
78 * Enqueue assets only in editor screens where AI is supported.
79 */
80 public function blockspare_enqueue_editor_assets()
81 {
82
83 wp_enqueue_script('blockspare-ai-generator-editor');
84
85 // Check if function exists AND if any active provider/connector is available.
86 $ai_active = function_exists('wp_ai_client_prompt');
87 $has_credentials = false;
88 $missing_reason = '';
89
90 $settings = $this->settings->get_ai_settings();
91 $ai_settings_class = new Blockspare_AI_Settings();
92
93 // echo ":sss";
94 // var_dump($settings);
95 $aiProvider = isset($settings['ai_provider']) ? $settings['ai_provider'] : '';
96 $aiModel = isset($settings['ai_model']) ? $settings['ai_model'] : '';
97 if ($ai_active) {
98
99 // Query builder to verify if text generation model/credentials exist.
100 $builder = wp_ai_client_prompt();
101
102 $has_credentials = $builder->is_supported_for_text_generation();
103
104 if (! $has_credentials) {
105 $missing_reason = __(
106 'AI features are currently unavailable. Please ensure the WordPress AI plugin is installed and active API keys/connectors are set up.',
107 'blockspare'
108 );
109 }
110 } else {
111
112 $missing_reason = __(
113 'The WordPress AI Client API (wp_ai_client_prompt) is not active.',
114 'blockspare'
115 );
116 }
117
118 wp_localize_script(
119 'blockspare-ai-generator-editor',
120 'blockspareAiPluginData',
121 array(
122 'isAiSupported' => $ai_active && $has_credentials,
123 'missingReason' => $missing_reason,
124 'havePermission' => current_user_can('manage_options'),
125 'nonce' => wp_create_nonce('wp_rest'),
126 'aiProvider' => $aiProvider,
127 'aiModel' => $aiModel,
128 'language' => $ai_settings_class->blockspare_get_default_language(),
129 'connectorLink' => admin_url('options-connectors.php'),
130 'connectorText' => __('Configure Connectors', 'blockspare'),
131 'bsAiLink' => admin_url('admin.php?page=blockspare-ai'),
132 'bsAiText' => __('Configure AI Settings', 'blockspare'),
133
134
135 )
136 );
137
138 // Feature Detection: Ensure WP AI Client API exists and supports text generation.
139 if (! function_exists('wp_ai_client_prompt')) {
140 return;
141 }
142
143 $builder = wp_ai_client_prompt('test');
144
145 if (! $builder->is_supported_for_text_generation()) {
146 return;
147 }
148 }
149
150
151 /**
152 * Register custom REST API Endpoint wrapper for wp_ai_client_prompt.
153 */
154 public function register_rest_routes()
155 {
156 register_rest_route(
157 'blockspare/v1',
158 '/generate-content',
159 array(
160 'methods' => 'POST',
161 'callback' => array($this, 'handle_generation_request'),
162 'permission_callback' => array($this, 'check_permissions'),
163 'args' => array(
164
165 'action' => array(
166 'required' => true,
167 'type' => 'string',
168 'sanitize_callback' => 'sanitize_text_field',
169 'enum' => array(
170 'generate',
171 'summarize',
172 'highlight_qa',
173 ),
174 ),
175
176 'prompt' => array(
177 'required' => false,
178 'type' => 'string',
179 'sanitize_callback' => 'sanitize_textarea_field',
180 ),
181
182 'post_id' => array(
183 'required' => true,
184 'type' => 'integer',
185 'sanitize_callback' => 'absint',
186 ),
187 ),
188 )
189 );
190 }
191
192
193 /**
194 * Permission check ensuring user can edit the requested post.
195 */
196 public function check_permissions(WP_REST_Request $request)
197 {
198 // 1. Verify user capability to manage options
199 if (! current_user_can('manage_options')) {
200 return new WP_Error(
201 'rest_forbidden',
202 __('You do not have permission to access this endpoint.', 'blockspare'),
203 array('status' => 403)
204 );
205 }
206
207 // 2. Ensure post ID exists and user can edit this post
208 $post_id = $request->get_param('post_id');
209 if (! current_user_can('edit_post', $post_id)) {
210 return new WP_Error(
211 'rest_cannot_edit',
212 __('You are not allowed to edit this post.', 'blockspare'),
213 array('status' => 403)
214 );
215 }
216 return true;
217 }
218
219 private function blockspare_words_to_max_tokens($word_limit, $language = 'English')
220 {
221 $word_limit = absint($word_limit);
222
223 if ($word_limit < 1) {
224 $word_limit = 1;
225 }
226
227 // Non-Latin / token-heavy scripts get a much larger multiplier and
228 // floor. This list covers the languages most likely to need it;
229 // extend it if you add other non-Latin languages to the settings.
230 $heavy_script_languages = array(
231 'hindi',
232 'nepali',
233 'chinese',
234 'japanese',
235 'korean',
236 'arabic',
237 'persian',
238 'hebrew',
239 'thai',
240 'russian',
241 'ukrainian',
242 'bulgarian',
243 'greek',
244 );
245
246 $is_heavy_script = in_array(strtolower($language), $heavy_script_languages, true);
247
248 $multiplier = $is_heavy_script ? 5 : 2.5;
249 $floor = $is_heavy_script ? 1536 : 512;
250
251 $tokens = (int) ceil($word_limit * $multiplier);
252
253 return max($floor, min($tokens, 8192));
254 }
255
256
257 /**
258 * Starts a prompt builder and applies the admin's saved
259 * provider/model preference, if one was configured.
260 *
261 * Falls back to the site default provider/model when the setting
262 * is empty (i.e. the "Default (System Default)" option was chosen).
263 *
264 * @param string $prompt The initial prompt text.
265 * @param array $settings The already-loaded AI settings array.
266 * @return WP_AI_Client_Prompt_Builder
267 */
268 private function get_ai_prompt_builder($prompt, $settings)
269 {
270 $builder = wp_ai_client_prompt($prompt);
271
272 $provider = ! empty($settings['ai_provider']) ? $settings['ai_provider'] : '';
273 $model = ! empty($settings['ai_model']) ? $settings['ai_model'] : '';
274
275 if (! empty($provider)) {
276 $builder = $builder->using_provider($provider);
277
278 if (! empty($model)) {
279 $builder = $builder->using_model_preference($model);
280 }
281 }
282
283
284 return $builder;
285 }
286
287
288 /**
289 * Execute AI Generation using WordPress Native API.
290 */
291 public function handle_generation_request(WP_REST_Request $request)
292 {
293 $action = $request->get_param('action');
294 $prompt = $request->get_param('prompt');
295 $post_id = absint($request->get_param('post_id'));
296
297 /**
298 * Get saved AI settings.
299 *
300 * If settings do not exist, get_ai_settings()
301 * automatically returns the original default prompts.
302 */
303 $settings = $this->settings->get_ai_settings();
304
305 $ai_settings_class = new Blockspare_AI_Settings();
306 $language = $ai_settings_class->blockspare_get_default_language();
307
308 /**
309 * Word/answer limits configured on the Settings screen, with safe
310 * fallbacks in case the option is missing or was tampered with.
311 */
312 $generate_word_limit = ! empty($settings['generate_word_limit'])
313 ? absint($settings['generate_word_limit'])
314 : 75;
315
316 $summarize_word_limit = ! empty($settings['summarize_word_limit'])
317 ? absint($settings['summarize_word_limit'])
318 : 25;
319
320 $qa_count = ! empty($settings['qa_count'])
321 ? absint($settings['qa_count'])
322 : 2;
323
324 // Defensive clamp -- the settings sanitizer already restricts this to
325 // 1-5, but re-checking here means a bad/edited option value can never
326 // ask the AI for an unreasonable number of Q&A pairs.
327 if ($qa_count < 1) {
328 $qa_count = 1;
329 } elseif ($qa_count > 5) {
330 $qa_count = 5;
331 }
332
333
334 /**
335 * Make sure the post exists.
336 */
337 $post = get_post($post_id);
338
339 if (! $post) {
340 return new WP_Error(
341 'post_not_found',
342 __('Post not found.', 'blockspare'),
343 array('status' => 404)
344 );
345 }
346
347
348 /**
349 * =========================
350 * GENERATE CONTENT
351 * =========================
352 */
353 if ($action === 'generate') {
354
355 if (empty(trim($prompt))) {
356 return new WP_Error(
357 'missing_prompt',
358 __('Please provide a prompt.', 'blockspare'),
359 array('status' => 400)
360 );
361 }
362
363
364 /**
365 * Get custom/default generation prompt.
366 */
367 $generate_prompt = $settings['generate_prompt'];
368
369
370 /**
371 * Replace dynamic placeholders.
372 */
373 $generate_prompt = str_replace(
374 array(
375 '{prompt}',
376 '{language}',
377 '{word_limit}',
378 ),
379 array(
380 $prompt,
381 $language,
382 $generate_word_limit,
383 ),
384 $generate_prompt
385 );
386
387
388 /**
389 * Generate AI content.
390 */
391 $response = $this->get_ai_prompt_builder($generate_prompt, $settings)
392 ->using_system_instruction(
393 'You are a professional blog post copywriter.
394 Output clean HTML paragraphs without markdown code blocks.
395 Write the response in ' . $language . '.
396 Target approximately ' . $generate_word_limit . ' words, but prioritize
397 writing complete, coherent, well-formed content over hitting that number
398 exactly -- never cut a sentence short just to stay under it.
399 Return ONLY the article content itself.
400 Do not include any commentary, notes, word counts, or quality remarks
401 about the article -- for example, never write things like "Word count:"
402 or "Quality: Professional".'
403 )
404 ->using_max_tokens(
405 $this->blockspare_words_to_max_tokens($generate_word_limit, $language)
406 )
407 ->generate_text();
408 }
409
410
411 /**
412 * =========================
413 * SUMMARIZE CONTENT
414 * =========================
415 */
416 elseif ($action === 'summarize') {
417
418 $post_content = $post->post_content;
419
420
421 /**
422 * Check whether the post actually has content.
423 */
424 if (empty(trim(wp_strip_all_tags($post_content)))) {
425 return new WP_Error(
426 'empty_post_content',
427 __('There is no content available to summarize.', 'blockspare'),
428 array('status' => 400)
429 );
430 }
431
432
433 /**
434 * Get custom/default summarization prompt.
435 */
436 $summary_prompt = $settings['summarize_prompt'];
437
438
439 /**
440 * Replace dynamic placeholders.
441 */
442 $summary_prompt = str_replace(
443 array(
444 '{language}',
445 '{post_content}',
446 '{word_limit}',
447 ),
448 array(
449 $language,
450 $post_content,
451 $summarize_word_limit,
452 ),
453 $summary_prompt
454 );
455
456
457 /**
458 * Generate summary.
459 */
460 $response = $this->get_ai_prompt_builder($summary_prompt, $settings)
461 ->using_system_instruction(
462 'You are a professional content summarization assistant.
463 Your job is to accurately summarize the provided content
464 without adding information that does not exist in the original content.
465 Always respond in ' . $language . '.
466 Target approximately ' . $summarize_word_limit . ' words, but prioritize
467 writing a complete, coherent summary over hitting that number exactly --
468 never cut a sentence short just to stay under it.
469 Return ONLY the summary content itself.
470 Do not include any commentary, notes, word counts, or quality remarks
471 about the summary.'
472 )
473 ->using_max_tokens(
474 $this->blockspare_words_to_max_tokens($summarize_word_limit, $language)
475 )
476 ->generate_text();
477 }
478
479
480 /**
481 * =========================
482 * HELPFUL Q&A
483 * =========================
484 */
485 elseif ($action === 'highlight_qa') {
486
487 return new WP_Error(
488 'rest_forbidden_action',
489 __('Highlight Q&A is a Pro feature and is disabled in the free version.', 'blockspare'),
490 array('status' => 403)
491 );
492 }
493
494
495 /**
496 * =========================
497 * INVALID ACTION
498 * =========================
499 */
500 else {
501
502 return new WP_Error(
503 'invalid_action',
504 __('Invalid AI action.', 'blockspare'),
505 array('status' => 400)
506 );
507 }
508
509
510 /**
511 * Handle AI error.
512 */
513 if (is_wp_error($response)) {
514 return new WP_Error(
515 'ai_generation_failed',
516 $response->get_error_message(),
517 array('status' => 500)
518 );
519 }
520
521
522 /**
523 * Return AI response.
524 */
525 return rest_ensure_response(
526 array(
527 'success' => true,
528 'action' => $action,
529 'post_id' => $post_id,
530 'content' => $response,
531 )
532 );
533 }
534 }
535