' . esc_html__( 'Settings', 'helpo' ) . '' ); return $links; } /** * The single admin page. The React app takes over #kasp-admin-root. * * @return void */ public function render_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', 'helpo' ) ); } // Read-only status flags set by the connect redirects. They are escaped // on output and carry no privileged meaning. // phpcs:disable WordPress.Security.NonceVerification.Recommended $error_slug = isset( $_GET['kasp_error'] ) ? sanitize_key( wp_unslash( $_GET['kasp_error'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended echo '
'; if ( '' !== $error_slug ) { printf( '

%s

', esc_html( Kasp_Connect::error_message( $error_slug ) ) ); } if ( Kasp_Settings::is_connection_broken() ) { printf( '

%1$s %3$s

', esc_html__( 'Helpo rejected this site’s connection. Reconnect to restore your AI support agent.', 'helpo' ), esc_url( Kasp_Connect::connect_url() ), esc_html__( 'Reconnect', 'helpo' ) ); } if ( ! $this->has_build() ) { printf( '

%s

', esc_html__( 'The helpo admin interface has not been built yet. Run "npm install && npm run build" in the plugin folder.', 'helpo' ) ); } echo '
'; printf( '', esc_html__( 'Helpo needs JavaScript enabled in wp-admin.', 'helpo' ) ); echo '
'; } /** * Enqueue the built React app on our page, and the tiny dashboard-widget * script on the WordPress Dashboard. * * @param string $hook Current admin page hook. * @return void */ public function enqueue_assets( $hook ) { if ( 'index.php' === $hook ) { $this->enqueue_dashboard_widget_script(); return; } if ( 'toplevel_page_' . self::PAGE_SLUG !== $hook || ! current_user_can( 'manage_options' ) ) { return; } $asset = $this->asset_manifest(); wp_enqueue_script( self::SCRIPT_HANDLE, KASP_PLUGIN_URL . 'build/index.js', $asset['dependencies'], $asset['version'], true ); $style = KASP_PLUGIN_DIR . 'build/index.css'; if ( file_exists( $style ) ) { wp_enqueue_style( self::SCRIPT_HANDLE, KASP_PLUGIN_URL . 'build/index.css', array( 'wp-components' ), $asset['version'] ); // @wordpress/scripts emits build/index-rtl.css alongside the LTR // sheet; without this, right-to-left admins get the LTR layout. if ( file_exists( KASP_PLUGIN_DIR . 'build/index-rtl.css' ) ) { wp_style_add_data( self::SCRIPT_HANDLE, 'rtl', 'replace' ); } } if ( function_exists( 'wp_set_script_translations' ) ) { wp_set_script_translations( self::SCRIPT_HANDLE, 'helpo', KASP_PLUGIN_DIR . 'languages' ); } wp_localize_script( self::SCRIPT_HANDLE, 'kaspAdmin', $this->script_data() ); } /** * The data handed to the React app. * * Deliberately excludes the integration token — the app reaches helpo only * through the kasp/v1 REST namespace. * * @return array */ private function script_data() { $connection = Kasp_Settings::get_connection(); $connected = Kasp_Settings::is_connected(); // phpcs:disable WordPress.Security.NonceVerification.Recommended $just_connected = isset( $_GET['connected'] ) && '1' === $_GET['connected']; $disconnected = isset( $_GET['disconnected'] ) && '1' === $_GET['disconnected']; // phpcs:enable WordPress.Security.NonceVerification.Recommended return array( 'restUrl' => esc_url_raw( rest_url( Kasp_Rest::NAMESPACE_V1 . '/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'connected' => $connected, 'connectionBroken' => Kasp_Settings::is_connection_broken(), 'justConnected' => $just_connected, 'justDisconnected' => $disconnected, 'connectUrl' => Kasp_Connect::connect_url(), 'disconnectUrl' => Kasp_Connect::disconnect_url(), 'appUrl' => kasp_app_url(), 'widgetUrl' => kasp_widget_url(), 'siteUrl' => home_url( '/' ), 'siteName' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), 'adminUrl' => admin_url( 'admin.php?page=' . self::PAGE_SLUG ), 'pluginVersion' => KASP_VERSION, 'settings' => Kasp_Settings::get_settings(), 'woo' => Kasp_Woo::status(), 'projectId' => $connected ? $connection['project_id'] : '', 'projectName' => $connected ? $connection['project_name'] : '', // Public key: it is printed in the front-end embed snippet anyway. 'siteKey' => $connected ? $connection['site_key'] : '', ); } /** * Dependencies and cache-busting version for the built bundle. * * @return array */ private function asset_manifest() { $manifest = KASP_PLUGIN_DIR . 'build/index.asset.php'; if ( file_exists( $manifest ) ) { $asset = include $manifest; if ( is_array( $asset ) ) { return array( 'dependencies' => isset( $asset['dependencies'] ) ? (array) $asset['dependencies'] : array(), 'version' => isset( $asset['version'] ) ? (string) $asset['version'] : KASP_VERSION, ); } } return array( 'dependencies' => array( 'wp-element', 'wp-i18n', 'wp-api-fetch', 'wp-components' ), 'version' => KASP_VERSION, ); } /** * Has the admin app been built? * * @return bool */ private function has_build() { return file_exists( KASP_PLUGIN_DIR . 'build/index.js' ); } /* --------------------------------------------------------------------- */ /* WordPress Dashboard widget */ /* --------------------------------------------------------------------- */ /** * Register the "at a glance" card. * * @return void */ public function register_dashboard_widget() { if ( ! current_user_can( 'manage_options' ) ) { return; } wp_add_dashboard_widget( 'kasp_dashboard_widget', __( 'helpo — AI Support Chat', 'helpo' ), array( $this, 'render_dashboard_widget' ) ); } /** * Server-rendered card. When connected, the placeholders below are filled * in by one small fetch to kasp/v1/bootstrap. * * @return void */ public function render_dashboard_widget() { $page_url = admin_url( 'admin.php?page=' . self::PAGE_SLUG ); if ( ! Kasp_Settings::is_connected() ) { printf( '

%1$s

%3$s

', esc_html__( 'Connect this site to helpo to add an AI support agent trained on your own content.', 'helpo' ), esc_url( Kasp_Connect::connect_url() ), esc_html__( 'Connect to helpo', 'helpo' ) ); return; } $project = Kasp_Settings::get_project_name(); echo '
'; printf( '

%1$s %2$s

', esc_html( '' !== $project ? $project : __( 'Your helpo agent', 'helpo' ) ), esc_html__( 'Loading…', 'helpo' ) ); echo ''; printf( '%2$s', esc_url( $page_url ), esc_html__( 'Open helpo', 'helpo' ) ); echo '
'; } /** * Register + enqueue the dashboard widget's inline script. * * @return void */ private function enqueue_dashboard_widget_script() { if ( ! current_user_can( 'manage_options' ) || ! Kasp_Settings::is_connected() ) { return; } // Inline-only script: no src, the body is attached below. wp_register_script( self::WIDGET_HANDLE, false, array(), KASP_VERSION, true ); wp_enqueue_script( self::WIDGET_HANDLE ); wp_localize_script( self::WIDGET_HANDLE, 'kaspDashboard', array( 'restUrl' => esc_url_raw( rest_url( Kasp_Rest::NAMESPACE_V1 . '/bootstrap' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'offline' => __( 'Could not reach helpo.', 'helpo' ), 'live' => __( 'Live on this site', 'helpo' ), 'idle' => __( 'Widget not detected yet', 'helpo' ), /* translators: %s: plan name, e.g. "pro". */ 'trial' => __( '%s (trial)', 'helpo' ), ) ); wp_add_inline_script( self::WIDGET_HANDLE, $this->dashboard_widget_script() ); } /** * The card's data fetch. Plain fetch + textContent — no HTML is built in JS. * * @return string */ private function dashboard_widget_script() { return <<<'JS' ( function () { var cfg = window.kaspDashboard; if ( ! cfg ) { return; } var set = function ( id, value ) { var el = document.getElementById( id ); if ( el ) { el.textContent = String( value ); } }; fetch( cfg.restUrl, { credentials: 'same-origin', headers: { 'X-WP-Nonce': cfg.nonce } } ) .then( function ( res ) { return res.json(); } ) .then( function ( data ) { if ( ! data || ! data.remote ) { set( 'kasp-dw-status', cfg.offline ); return; } var remote = data.remote; var usage = remote.usage || {}; var limits = remote.limits || {}; var meter = function ( used, cap ) { if ( typeof used !== 'number' ) { return '—'; } return typeof cap === 'number' ? used + ' / ' + cap : String( used ); }; set( 'kasp-dw-status', remote.live ? cfg.live : cfg.idle ); set( 'kasp-dw-pages', meter( usage.kbPages, limits.kbPages ) ); set( 'kasp-dw-products', meter( usage.products, limits.products ) ); set( 'kasp-dw-plan', remote.plan && remote.trialEndsAt ? cfg.trial.replace( '%s', remote.plan ) : remote.plan || '—' ); } ) .catch( function () { set( 'kasp-dw-status', cfg.offline ); } ); } )(); JS; } }