PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.4
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.4
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / classes / react / react-manager.php

react-manager.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.4, at classes/react/react-manager.php

530 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * React Manager - Renders chat assistant directly in WordPress (no iframe)
4 *
5 * @package zip-ai
6 */
7
8 namespace ZipAI\Classes\React;
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 use ZipAI\Classes\Core\Helper;
16 use ZipAI\Classes\Traits\Enqueue;
17
18 /**
19 * The React_Manager Class.
20 * Handles rendering of the React chat assistant directly in WordPress.
21 */
22 class React_Manager {
23
24 use Enqueue;
25
26 /**
27 * Constructor of this class.
28 *
29 * @since 0.0.1
30 * @return void
31 */
32 public function __construct() {
33 $this->enqueue_scripts();
34 $this->enqueue_scripts_admin();
35 add_action( 'admin_menu', array( $this, 'register_admin_page' ) );
36 add_action( 'admin_footer', array( $this, 'render_container' ) );
37 add_action( 'wp_footer', array( $this, 'render_container' ) );
38
39 // Collapse WP sidebar and tag body on the dedicated full-page screen.
40 if ( is_admin() ) {
41 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
42 if ( 'zip-ai-assistant' === $page ) {
43 add_filter( 'admin_body_class', array( $this, 'add_fullpage_body_classes' ) );
44 // Remove all admin notices on the fullpage assistant screen.
45 add_action( 'in_admin_header', array( $this, 'remove_admin_notices' ) );
46 }
47 }
48 }
49
50 /**
51 * Add body classes for the dedicated full-page assistant screen.
52 * `folded` – collapses WP admin sidebar to icon-only mode.
53 * `zip-ai-fullpage-page` – lets CSS target this page precisely.
54 *
55 * @since 0.0.1
56 * @param string $classes Existing body classes.
57 * @return string
58 */
59 public function add_fullpage_body_classes( $classes ) {
60 return $classes . ' folded zip-ai-fullpage-page';
61 }
62
63 /**
64 * Remove all admin notices on the fullpage assistant screen.
65 *
66 * @since 0.0.1
67 * @return void
68 */
69 public function remove_admin_notices() {
70 remove_all_actions( 'admin_notices' );
71 remove_all_actions( 'all_admin_notices' );
72 }
73
74 /**
75 * Register dedicated full-page assistant screen in WP admin.
76 *
77 * @since 0.0.1
78 * @return void
79 */
80 public function register_admin_page() {
81 if ( ! current_user_can( 'manage_options' ) ) {
82 return;
83 }
84
85 add_options_page(
86 __( 'Zip AI', 'zip-ai' ),
87 __( 'Zip AI', 'zip-ai' ),
88 'manage_options',
89 'zip-ai-assistant',
90 array( $this, 'render_fullpage_screen' )
91 );
92 }
93
94 /**
95 * Check if we should use source (non-minified) scripts.
96 *
97 * @since 0.0.1
98 * @return bool
99 */
100 private function use_source_scripts() {
101 return ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ||
102 ( defined( 'ZIP_AI_DEBUG' ) && ZIP_AI_DEBUG );
103 }
104
105 /**
106 * Frontend enqueue callback (registered by trait).
107 *
108 * @since 0.0.1
109 * @return void
110 */
111 public function wp_enqueue_scripts() {
112 $this->enqueue_all_assets();
113 }
114
115 /**
116 * Admin enqueue callback (registered by trait).
117 *
118 * @since 0.0.1
119 * @return void
120 */
121 public function admin_enqueue_scripts() {
122 $this->enqueue_all_assets();
123 }
124
125 /**
126 * Enqueue all scripts and styles for the React chat assistant.
127 *
128 * @since 0.0.1
129 * @return void
130 */
131 private function enqueue_all_assets() {
132 if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
133 return;
134 }
135
136 $use_source = $this->use_source_scripts();
137 $auth_url = $this->get_auth_url();
138
139 // ── Bridge scripts (tool hooks, context, bridge host) ──
140 if ( $use_source ) {
141 $this->enqueue_source_scripts();
142 } else {
143 $this->enqueue_minified_scripts();
144 }
145
146 // ── React app bundle (read .asset.php for React dependencies) ──
147 $asset_file = $this->build_path . 'js/dist/chat-assistant.asset.php';
148 $asset = file_exists( $asset_file ) ? require $asset_file : array(
149 'dependencies' => array(),
150 'version' => ZIP_AI_VERSION,
151 );
152 $react_deps = array_merge( $asset['dependencies'], array( $this->enqueue_prefix . '-bridge-host' ) );
153
154 $this->script_operations(
155 'chat-assistant',
156 $this->build_url . 'js/dist/chat-assistant.js',
157 $react_deps,
158 array(),
159 $asset['version']
160 );
161
162 // ── React app styles ──
163 $this->style_operations(
164 'chat-assistant',
165 $this->build_url . 'css/dist/chat-assistant.css'
166 );
167
168 // ── Gutenberg editor plugin (block editor only) ──
169 if ( is_admin() ) {
170 $screen = get_current_screen();
171 if ( $screen && $screen->is_block_editor() ) {
172 $editor_file = $use_source ? 'js/editor/editor-plugin.js' : 'js/dist/zip-ai-editor.min.js';
173 $this->script_operations(
174 'editor-plugin',
175 $this->build_url . $editor_file,
176 array( 'wp-plugins', 'wp-element', 'wp-i18n', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-editor' )
177 );
178 }
179 }
180
181 // ── Localize bridge config ──
182 $this->localize_script(
183 'tool-hooks',
184 'zipAiIframeConfig',
185 array(
186 'nonce' => wp_create_nonce( 'zip_ai_iframe' ),
187 'restNonce' => wp_create_nonce( 'wp_rest' ),
188 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
189 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar',
190 'adminHomeUrl' => admin_url(),
191 'userId' => get_current_user_id(),
192 'authUrl' => $auth_url,
193 'websiteContext' => array(
194 'site_url' => get_site_url(),
195 'admin_url' => admin_url(),
196 'site_title' => get_bloginfo( 'name' ),
197 'site_tagline' => get_bloginfo( 'description' ),
198 'language' => get_bloginfo( 'language' ),
199 'timezone' => wp_timezone_string(),
200 'date_format' => get_option( 'date_format' ),
201 'time_format' => get_option( 'time_format' ),
202 'is_multisite' => is_multisite(),
203 ),
204 'pageContext' => $this->get_current_page_context(),
205 'themeContext' => array(
206 'color_palette' => $this->get_theme_color_palette(),
207 ),
208 'installedPlugins' => $this->get_installed_plugins_versions(),
209 'blockScannerTextLimit' => apply_filters( 'zip_ai_block_scanner_text_limit', 100 ),
210 )
211 );
212
213 // ── Localize React app config ──
214 $this->localize_script(
215 'chat-assistant',
216 'ZIP_AI_CONFIG',
217 array(
218 'apiUrl' => rtrim( ZIP_AI_CREDIT_SERVER_API, '/' ),
219 'token' => Helper::get_decrypted_auth_token(),
220 'isAuthenticated' => Helper::is_authorized(),
221 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar',
222 'fullPageUrl' => admin_url( 'options-general.php?page=zip-ai-assistant' ),
223 'adminHomeUrl' => admin_url(),
224 'userId' => get_current_user_id(),
225 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ),
226 'user' => array(
227 'id' => get_current_user_id(),
228 'email' => Helper::get_setting( 'user_email', '' ),
229 'name' => Helper::get_setting( 'user_name', '' ),
230 ),
231 'isFreshSite' => (bool) get_option( 'fresh_site', false ),
232 'nonce' => wp_create_nonce( 'zip_ai_iframe' ),
233 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
234 'authUrl' => $auth_url,
235 )
236 );
237 }
238
239 /**
240 * Enqueue individual source scripts for development/debugging.
241 *
242 * @since 0.0.1
243 * @return void
244 */
245 private function enqueue_source_scripts() {
246 $this->script_operations(
247 'tool-hooks',
248 $this->build_url . 'js/core/tool-hooks-registry.js',
249 array()
250 );
251
252 // tool-context-provider-registry.js was removed in the page-delivery refactor.
253 // Only enqueue if it still exists on disk; bridge-host's dependency on it
254 // is stripped below when missing.
255 $context_registry_path = $this->build_path . 'js/core/tool-context-provider-registry.js';
256 $has_context_registry = file_exists( $context_registry_path );
257 if ( $has_context_registry ) {
258 $this->script_operations(
259 'tool-context-registry',
260 $this->build_url . 'js/core/tool-context-provider-registry.js',
261 array()
262 );
263 }
264
265 $this->script_operations(
266 'create-page-handler',
267 $this->build_url . 'js/tools/zip-ai/create-page/handler.js',
268 array( $this->enqueue_prefix . '-tool-hooks' )
269 );
270
271 $this->script_operations(
272 'block-context-picker',
273 $this->build_url . 'js/core/block-context-picker.js',
274 array( $this->enqueue_prefix . '-tool-hooks' )
275 );
276
277 $bridge_deps = array(
278 $this->enqueue_prefix . '-tool-hooks',
279 $this->enqueue_prefix . '-block-context-picker',
280 );
281 if ( $has_context_registry ) {
282 $bridge_deps[] = $this->enqueue_prefix . '-tool-context-registry';
283 }
284 $this->script_operations(
285 'popover-drag',
286 $this->build_url . 'js/core/popover-drag.js',
287 array()
288 );
289
290 $bridge_deps[] = $this->enqueue_prefix . '-popover-drag';
291 $this->script_operations(
292 'bridge-host',
293 $this->build_url . 'js/core/wp-bridge-host.js',
294 $bridge_deps
295 );
296
297 // Block scanner and intent classifier — core orchestrator utilities.
298 $this->script_operations(
299 'block-scanner',
300 $this->build_url . 'js/core/block-scanner.js',
301 array( $this->enqueue_prefix . '-bridge-host' )
302 );
303
304 $this->script_operations(
305 'intent-classifier',
306 $this->build_url . 'js/core/intent-classifier.js',
307 array( $this->enqueue_prefix . '-block-scanner' )
308 );
309 }
310
311 /**
312 * Enqueue combined minified script for production.
313 *
314 * @since 0.0.1
315 * @return void
316 */
317 private function enqueue_minified_scripts() {
318 $this->script_operations(
319 'tool-hooks',
320 $this->build_url . 'js/dist/zip-ai.min.js',
321 array()
322 );
323
324 // Tool context registry is bundled in the same file — virtual handle.
325 $this->register_script( 'tool-context-registry', false, array( $this->enqueue_prefix . '-tool-hooks' ) );
326 $this->enqueue_script( 'tool-context-registry' );
327
328 // Bridge host is also bundled — virtual handle for dependency chain.
329 $this->register_script( 'bridge-host', false, array( $this->enqueue_prefix . '-tool-hooks' ) );
330 $this->enqueue_script( 'bridge-host' );
331 }
332
333 /**
334 * Get current page/post context from PHP.
335 *
336 * @since 0.0.1
337 * @return array
338 */
339 private function get_current_page_context() {
340 global $post;
341
342 $context = array(
343 'post_id' => null,
344 'post_type' => null,
345 'post_title' => null,
346 'post_status' => null,
347 );
348
349 if ( $post instanceof \WP_Post ) {
350 $context['post_id'] = $post->ID;
351 $context['post_type'] = $post->post_type;
352 $context['post_title'] = $post->post_title;
353 $context['post_status'] = $post->post_status;
354 return $context;
355 }
356
357 if ( is_admin() ) {
358 $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
359 if ( $post_id ) {
360 $admin_post = get_post( $post_id );
361 if ( $admin_post instanceof \WP_Post ) {
362 $context['post_id'] = $admin_post->ID;
363 $context['post_type'] = $admin_post->post_type;
364 $context['post_title'] = $admin_post->post_title;
365 $context['post_status'] = $admin_post->post_status;
366 }
367 }
368 }
369
370 return $context;
371 }
372
373 /**
374 * Get authentication URL with CSRF protection for ZipWP OAuth callback.
375 *
376 * @since 0.0.1
377 * @return string
378 */
379 private function get_auth_url() {
380 $transient_key = 'zip_ai_oauth_state_' . get_current_user_id();
381
382 // Reuse existing state if still valid — prevents overwriting during auth popup flow.
383 $state = get_transient( $transient_key );
384 if ( empty( $state ) ) {
385 $state = wp_generate_password( 32, false );
386 set_transient( $transient_key, $state, 10 * MINUTE_IN_SECONDS );
387 }
388
389 $redirect_url = add_query_arg(
390 array(
391 'nonce' => wp_create_nonce( 'zip_ai_auth_nonce' ),
392 'state' => $state,
393 'zip-ai-auth' => 'true',
394 ),
395 admin_url( 'options-general.php?page=zip-ai-assistant' )
396 );
397
398 $auth_middleware = ZIP_AI_MIDDLEWARE;
399
400 $auth_url = add_query_arg(
401 array(
402 'type' => 'token',
403 'redirect_url' => rawurlencode( $redirect_url ),
404 'state' => $state,
405 ),
406 $auth_middleware
407 );
408
409 return $auth_url;
410 }
411
412 /**
413 * Get theme color palette formatted as CSS variables.
414 *
415 * @since 0.0.1
416 * @return array
417 */
418 private function get_theme_color_palette() {
419 if ( ! function_exists( 'astra_get_palette_colors' ) ) {
420 return array();
421 }
422
423 $palette_data = astra_get_palette_colors();
424 $current_palette = $palette_data['currentPalette'] ?? '';
425 $palettes = $palette_data['palettes'] ?? array();
426
427 if ( empty( $current_palette ) || empty( $palettes[ $current_palette ] ) ) {
428 return array();
429 }
430
431 $colors = $palettes[ $current_palette ];
432 $formatted_palette = array();
433
434 foreach ( $colors as $index => $color ) {
435 $formatted_palette[ '--ast-global-color-' . $index ] = $color;
436 }
437
438 return $formatted_palette;
439 }
440
441 /**
442 * Get installed plugins with their versions.
443 *
444 * @since 0.0.1
445 * @return array
446 */
447 private function get_installed_plugins_versions() {
448 if ( ! function_exists( 'get_plugins' ) ) {
449 require_once ABSPATH . 'wp-admin/includes/plugin.php';
450 }
451
452 $all_plugins = get_plugins();
453 $plugin_versions = array();
454
455 foreach ( $all_plugins as $plugin_file => $plugin_data ) {
456 $slug = dirname( $plugin_file );
457 if ( '.' === $slug ) {
458 $slug = basename( $plugin_file, '.php' );
459 }
460 $plugin_versions[ $slug ] = $plugin_data['Version'] ?? '0.0.0';
461 }
462
463 return $plugin_versions;
464 }
465
466 /**
467 * Render the assistant container HTML.
468 *
469 * @since 0.0.1
470 * @return void
471 */
472 public function render_container() {
473 if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
474 return;
475 }
476
477 // On dedicated full-page screen we render a different container.
478 if ( is_admin() && $this->is_fullpage_screen() ) {
479 return;
480 }
481
482 ?>
483 <div id="zip-ai-assistant-container" class="zip-ai-panel-container" inert>
484 <!-- React app mounts here (trigger + resize handle rendered by React via portals) -->
485 <div id="chat-assistant-root"></div>
486 </div>
487 <?php
488 }
489
490 /**
491 * Render dedicated full-page assistant admin screen.
492 *
493 * @since 0.0.1
494 * @return void
495 */
496 public function render_fullpage_screen() {
497 if ( ! current_user_can( 'manage_options' ) ) {
498 return;
499 }
500 // Note: no .wrap class — we bypass WP's default margin/padding for a true full-bleed layout.
501 ?>
502 <div class="zip-ai-fullpage-screen">
503 <div id="zip-ai-fullpage-container" class="zip-ai-fullpage-container">
504 <div id="chat-assistant-root"></div>
505 </div>
506 </div>
507 <?php
508 }
509
510 /**
511 * Check if current admin page is dedicated full-page assistant.
512 *
513 * @since 0.0.1
514 * @return bool
515 */
516 private function is_fullpage_screen() {
517 if ( ! is_admin() ) {
518 return false;
519 }
520
521 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
522 if ( $screen && 'settings_page_zip-ai-assistant' === $screen->id ) {
523 return true;
524 }
525
526 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
527 return 'zip-ai-assistant' === $page;
528 }
529 }
530