enqueue_scripts(); $this->enqueue_scripts_admin(); add_action( 'admin_menu', array( $this, 'register_admin_page' ) ); add_action( 'admin_footer', array( $this, 'render_container' ) ); add_action( 'wp_footer', array( $this, 'render_container' ) ); // Collapse WP sidebar and tag body on the dedicated full-page screen. if ( is_admin() ) { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'zip-ai-assistant' === $page ) { add_filter( 'admin_body_class', array( $this, 'add_fullpage_body_classes' ) ); // Remove all admin notices on the fullpage assistant screen. add_action( 'in_admin_header', array( $this, 'remove_admin_notices' ) ); } } } /** * Add body classes for the dedicated full-page assistant screen. * `folded` – collapses WP admin sidebar to icon-only mode. * `zip-ai-fullpage-page` – lets CSS target this page precisely. * * @since 0.0.1 * @param string $classes Existing body classes. * @return string */ public function add_fullpage_body_classes( $classes ) { return $classes . ' folded zip-ai-fullpage-page'; } /** * Remove all admin notices on the fullpage assistant screen. * * @since 0.0.1 * @return void */ public function remove_admin_notices() { remove_all_actions( 'admin_notices' ); remove_all_actions( 'all_admin_notices' ); } /** * Register dedicated full-page assistant screen in WP admin. * * @since 0.0.1 * @return void */ public function register_admin_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } add_options_page( __( 'Zip AI', 'zip-ai' ), __( 'Zip AI', 'zip-ai' ), 'manage_options', 'zip-ai-assistant', array( $this, 'render_fullpage_screen' ) ); } /** * Check if we should use source (non-minified) scripts. * * @since 0.0.1 * @return bool */ private function use_source_scripts() { return ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ( defined( 'ZIP_AI_DEBUG' ) && ZIP_AI_DEBUG ); } /** * Frontend enqueue callback (registered by trait). * * @since 0.0.1 * @return void */ public function wp_enqueue_scripts() { $this->enqueue_all_assets(); } /** * Admin enqueue callback (registered by trait). * * @since 0.0.1 * @return void */ public function admin_enqueue_scripts() { $this->enqueue_all_assets(); } /** * Enqueue all scripts and styles for the React chat assistant. * * @since 0.0.1 * @return void */ private function enqueue_all_assets() { if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { return; } $use_source = $this->use_source_scripts(); $auth_url = $this->get_auth_url(); // ── Bridge scripts (tool hooks, context, bridge host) ── if ( $use_source ) { $this->enqueue_source_scripts(); } else { $this->enqueue_minified_scripts(); } // ── React app bundle (read .asset.php for React dependencies) ── $asset_file = $this->build_path . 'js/dist/chat-assistant.asset.php'; $asset = file_exists( $asset_file ) ? require $asset_file : array( 'dependencies' => array(), 'version' => ZIP_AI_VERSION, ); $react_deps = array_merge( $asset['dependencies'], array( $this->enqueue_prefix . '-bridge-host' ) ); $this->script_operations( 'chat-assistant', $this->build_url . 'js/dist/chat-assistant.js', $react_deps, array(), $asset['version'] ); // ── React app styles ── $this->style_operations( 'chat-assistant', $this->build_url . 'css/dist/chat-assistant.css' ); // ── Gutenberg editor plugin (block editor only) ── if ( is_admin() ) { $screen = get_current_screen(); if ( $screen && $screen->is_block_editor() ) { $editor_file = $use_source ? 'js/editor/editor-plugin.js' : 'js/dist/zip-ai-editor.min.js'; $this->script_operations( 'editor-plugin', $this->build_url . $editor_file, array( 'wp-plugins', 'wp-element', 'wp-i18n', 'wp-components', 'wp-data', 'wp-edit-post', 'wp-editor' ) ); } } // ── Localize bridge config ── $this->localize_script( 'tool-hooks', 'zipAiIframeConfig', array( 'nonce' => wp_create_nonce( 'zip_ai_iframe' ), 'restNonce' => wp_create_nonce( 'wp_rest' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar', 'adminHomeUrl' => admin_url(), 'userId' => get_current_user_id(), 'authUrl' => $auth_url, 'websiteContext' => array( 'site_url' => get_site_url(), 'admin_url' => admin_url(), 'site_title' => get_bloginfo( 'name' ), 'site_tagline' => get_bloginfo( 'description' ), 'language' => get_bloginfo( 'language' ), 'timezone' => wp_timezone_string(), 'date_format' => get_option( 'date_format' ), 'time_format' => get_option( 'time_format' ), 'is_multisite' => is_multisite(), ), 'pageContext' => $this->get_current_page_context(), 'themeContext' => array( 'color_palette' => $this->get_theme_color_palette(), ), 'installedPlugins' => $this->get_installed_plugins_versions(), 'blockScannerTextLimit' => apply_filters( 'zip_ai_block_scanner_text_limit', 100 ), ) ); // ── Localize React app config ── $this->localize_script( 'chat-assistant', 'ZIP_AI_CONFIG', array( 'apiUrl' => rtrim( ZIP_AI_CREDIT_SERVER_API, '/' ), 'token' => Helper::get_decrypted_auth_token(), 'isAuthenticated' => Helper::is_authorized(), 'displayMode' => $this->is_fullpage_screen() ? 'fullpage' : 'sidebar', 'fullPageUrl' => admin_url( 'options-general.php?page=zip-ai-assistant' ), 'adminHomeUrl' => admin_url(), 'userId' => get_current_user_id(), 'domain' => wp_parse_url( home_url(), PHP_URL_HOST ), 'user' => array( 'id' => get_current_user_id(), 'email' => Helper::get_setting( 'user_email', '' ), 'name' => Helper::get_setting( 'user_name', '' ), ), 'isFreshSite' => (bool) get_option( 'fresh_site', false ), 'nonce' => wp_create_nonce( 'zip_ai_iframe' ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'authUrl' => $auth_url, ) ); } /** * Enqueue individual source scripts for development/debugging. * * @since 0.0.1 * @return void */ private function enqueue_source_scripts() { $this->script_operations( 'tool-hooks', $this->build_url . 'js/core/tool-hooks-registry.js', array() ); // tool-context-provider-registry.js was removed in the page-delivery refactor. // Only enqueue if it still exists on disk; bridge-host's dependency on it // is stripped below when missing. $context_registry_path = $this->build_path . 'js/core/tool-context-provider-registry.js'; $has_context_registry = file_exists( $context_registry_path ); if ( $has_context_registry ) { $this->script_operations( 'tool-context-registry', $this->build_url . 'js/core/tool-context-provider-registry.js', array() ); } $this->script_operations( 'create-page-handler', $this->build_url . 'js/tools/zip-ai/create-page/handler.js', array( $this->enqueue_prefix . '-tool-hooks' ) ); $this->script_operations( 'block-context-picker', $this->build_url . 'js/core/block-context-picker.js', array( $this->enqueue_prefix . '-tool-hooks' ) ); $bridge_deps = array( $this->enqueue_prefix . '-tool-hooks', $this->enqueue_prefix . '-block-context-picker', ); if ( $has_context_registry ) { $bridge_deps[] = $this->enqueue_prefix . '-tool-context-registry'; } $this->script_operations( 'popover-drag', $this->build_url . 'js/core/popover-drag.js', array() ); $bridge_deps[] = $this->enqueue_prefix . '-popover-drag'; $this->script_operations( 'bridge-host', $this->build_url . 'js/core/wp-bridge-host.js', $bridge_deps ); // Block scanner and intent classifier — core orchestrator utilities. $this->script_operations( 'block-scanner', $this->build_url . 'js/core/block-scanner.js', array( $this->enqueue_prefix . '-bridge-host' ) ); $this->script_operations( 'intent-classifier', $this->build_url . 'js/core/intent-classifier.js', array( $this->enqueue_prefix . '-block-scanner' ) ); } /** * Enqueue combined minified script for production. * * @since 0.0.1 * @return void */ private function enqueue_minified_scripts() { $this->script_operations( 'tool-hooks', $this->build_url . 'js/dist/zip-ai.min.js', array() ); // Tool context registry is bundled in the same file — virtual handle. $this->register_script( 'tool-context-registry', false, array( $this->enqueue_prefix . '-tool-hooks' ) ); $this->enqueue_script( 'tool-context-registry' ); // Bridge host is also bundled — virtual handle for dependency chain. $this->register_script( 'bridge-host', false, array( $this->enqueue_prefix . '-tool-hooks' ) ); $this->enqueue_script( 'bridge-host' ); } /** * Get current page/post context from PHP. * * @since 0.0.1 * @return array */ private function get_current_page_context() { global $post; $context = array( 'post_id' => null, 'post_type' => null, 'post_title' => null, 'post_status' => null, ); if ( $post instanceof \WP_Post ) { $context['post_id'] = $post->ID; $context['post_type'] = $post->post_type; $context['post_title'] = $post->post_title; $context['post_status'] = $post->post_status; return $context; } if ( is_admin() ) { $post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $post_id ) { $admin_post = get_post( $post_id ); if ( $admin_post instanceof \WP_Post ) { $context['post_id'] = $admin_post->ID; $context['post_type'] = $admin_post->post_type; $context['post_title'] = $admin_post->post_title; $context['post_status'] = $admin_post->post_status; } } } return $context; } /** * Get authentication URL with CSRF protection for ZipWP OAuth callback. * * @since 0.0.1 * @return string */ private function get_auth_url() { $transient_key = 'zip_ai_oauth_state_' . get_current_user_id(); // Reuse existing state if still valid — prevents overwriting during auth popup flow. $state = get_transient( $transient_key ); if ( empty( $state ) ) { $state = wp_generate_password( 32, false ); set_transient( $transient_key, $state, 10 * MINUTE_IN_SECONDS ); } $redirect_url = add_query_arg( array( 'nonce' => wp_create_nonce( 'zip_ai_auth_nonce' ), 'state' => $state, 'zip-ai-auth' => 'true', ), admin_url( 'options-general.php?page=zip-ai-assistant' ) ); $auth_middleware = ZIP_AI_MIDDLEWARE; $auth_url = add_query_arg( array( 'type' => 'token', 'redirect_url' => rawurlencode( $redirect_url ), 'state' => $state, ), $auth_middleware ); return $auth_url; } /** * Get theme color palette formatted as CSS variables. * * @since 0.0.1 * @return array */ private function get_theme_color_palette() { if ( ! function_exists( 'astra_get_palette_colors' ) ) { return array(); } $palette_data = astra_get_palette_colors(); $current_palette = $palette_data['currentPalette'] ?? ''; $palettes = $palette_data['palettes'] ?? array(); if ( empty( $current_palette ) || empty( $palettes[ $current_palette ] ) ) { return array(); } $colors = $palettes[ $current_palette ]; $formatted_palette = array(); foreach ( $colors as $index => $color ) { $formatted_palette[ '--ast-global-color-' . $index ] = $color; } return $formatted_palette; } /** * Get installed plugins with their versions. * * @since 0.0.1 * @return array */ private function get_installed_plugins_versions() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all_plugins = get_plugins(); $plugin_versions = array(); foreach ( $all_plugins as $plugin_file => $plugin_data ) { $slug = dirname( $plugin_file ); if ( '.' === $slug ) { $slug = basename( $plugin_file, '.php' ); } $plugin_versions[ $slug ] = $plugin_data['Version'] ?? '0.0.0'; } return $plugin_versions; } /** * Render the assistant container HTML. * * @since 0.0.1 * @return void */ public function render_container() { if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { return; } // On dedicated full-page screen we render a different container. if ( is_admin() && $this->is_fullpage_screen() ) { return; } ?>