mcp-core.php
15 hours ago
mcp-oauth.php
1 month ago
mcp-rest.php
3 weeks ago
mcp.conf
1 year ago
mcp.js
8 months ago
mcp.md
8 months ago
mcp.php
1 week ago
wpai-connectors.php
2 months ago
wpai-gateway-availability.php
2 months ago
wpai-gateway-directory.php
2 months ago
wpai-gateway-image-model.php
2 months ago
wpai-gateway-model.php
2 months ago
wpai-gateway-providers.php
2 months ago
wpai-gateway.php
2 months ago
wpai-connectors.php
584 lines
| 1 | <?php |
| 2 | /** |
| 3 | * WP AI Client: Connectors integration. |
| 4 | * |
| 5 | * WordPress 7+ ships an `/wp-admin/options-connectors.php` page backed by |
| 6 | * `WP_Connector_Registry`. The page is a sealed React SPA: no HTML filter, only |
| 7 | * the registry. What we do: |
| 8 | * |
| 9 | * 1. Show a banner on the page offering to let AI Engine manage the |
| 10 | * connectors (opt-in — the user most likely landed there by accident). |
| 11 | * 2. When opted in ("managed"): on `wp_connectors_init`, register one |
| 12 | * connector per AI Engine environment and remove anything else. The page |
| 13 | * then mirrors AI Engine's own configuration, nothing more. |
| 14 | * 3. Bridge `connectors_ai_{id}_api_key` ↔ AI Engine env apikey so either |
| 15 | * screen stays in sync. |
| 16 | * |
| 17 | * @package MeowApps |
| 18 | */ |
| 19 | |
| 20 | class Meow_MWAI_Labs_WPAI_Connectors { |
| 21 | const OPTION_MODE = 'mwai_wpai_connectors_mode'; // 'observe' | 'managed' | 'off' |
| 22 | |
| 23 | /** Optional credentials URLs per engine type. Only for the UX nicety of |
| 24 | * "Get an API key →" on cards; AI Engine itself doesn't need these. */ |
| 25 | const CREDENTIALS_URLS = [ |
| 26 | 'openai' => 'https://platform.openai.com/api-keys', |
| 27 | 'anthropic' => 'https://console.anthropic.com/settings/keys', |
| 28 | 'google' => 'https://aistudio.google.com/api-keys', |
| 29 | 'mistral' => 'https://console.mistral.ai/api-keys/', |
| 30 | 'openrouter' => 'https://openrouter.ai/keys', |
| 31 | 'perplexity' => 'https://www.perplexity.ai/settings/api', |
| 32 | 'replicate' => 'https://replicate.com/account/api-tokens', |
| 33 | ]; |
| 34 | |
| 35 | private $core = null; |
| 36 | |
| 37 | public function __construct( $core ) { |
| 38 | $this->core = $core; |
| 39 | |
| 40 | // Gate on WP AI Client availability. |
| 41 | if ( ! class_exists( 'WP_Connector_Registry' ) ) { |
| 42 | return; |
| 43 | } |
| 44 | |
| 45 | // The banner is shown unless the user explicitly dismissed with 'off'. |
| 46 | // We hook the global admin_footer and gate on the screen id, because the |
| 47 | // hook suffix differs between `/wp-admin/options-connectors.php` (loaded |
| 48 | // as `options-connectors.php`) and the menu-page variant. |
| 49 | if ( $this->get_mode() !== 'off' ) { |
| 50 | add_action( 'admin_footer', [ $this, 'maybe_render_banner' ] ); |
| 51 | } |
| 52 | |
| 53 | // AJAX: flip the mode. |
| 54 | add_action( 'wp_ajax_mwai_wpai_connectors_set_mode', [ $this, 'ajax_set_mode' ] ); |
| 55 | |
| 56 | // When managed: own the registry and bridge the keys both ways. |
| 57 | if ( $this->get_mode() === 'managed' ) { |
| 58 | add_action( 'wp_connectors_init', [ $this, 'customize_registry' ], 20 ); |
| 59 | add_action( 'updated_option', [ $this, 'maybe_bridge_key_to_env' ], 10, 3 ); |
| 60 | add_action( 'added_option', [ $this, 'maybe_bridge_key_to_env_added' ], 10, 2 ); |
| 61 | |
| 62 | // Reverse: AI Engine env key → WP Connectors option. |
| 63 | add_action( 'update_option_mwai_options', [ $this, 'mirror_envs_to_wp_connectors' ], 10, 2 ); |
| 64 | |
| 65 | // On admin page loads, reconcile keys so the WP Connectors page reflects |
| 66 | // the current AI Engine state even if it was edited elsewhere. |
| 67 | add_action( 'admin_init', [ $this, 'initial_sync' ], 99 ); |
| 68 | |
| 69 | // Flip `isConnected` for any bridged env that has a key, so the |
| 70 | // Connectors page shows providers as connected even when the AiClient |
| 71 | // registry doesn't know about them (e.g. OpenRouter, Mistral). |
| 72 | add_filter( 'script_module_data_options-connectors-wp-admin', [ $this, 'mark_bridged_as_connected' ], 20 ); |
| 73 | |
| 74 | // Fully take over the WP AI plugin's credentials gate. Once the user |
| 75 | // chose "managed", AI Engine is the source of truth for which providers |
| 76 | // are usable: the WP AI plugin shouldn't be vetoing on its own (its |
| 77 | // checks ignore Ollama, OpenRouter, Mistral, etc., and its live |
| 78 | // wp_ai_client_prompt('Test') round-trip can fail for providers WP core |
| 79 | // doesn't ship an AiClient adapter for). We declare credentials present |
| 80 | // when at least one AI Engine env is usable, and skip the live test. |
| 81 | add_filter( 'wpai_has_ai_credentials', [ $this, 'declare_credentials_present' ], 10, 1 ); |
| 82 | add_filter( 'wpai_pre_has_valid_credentials_check', [ $this, 'declare_credentials_valid' ], 10, 1 ); |
| 83 | } |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Filter for `wpai_has_ai_credentials`. Returns true if AI Engine has at |
| 88 | * least one usable env (api-key env with a key, or any keyless type like |
| 89 | * Ollama). Lets the WP AI plugin proceed past its own narrow check, which |
| 90 | * only counts api_key-method connectors with the matching option set. |
| 91 | */ |
| 92 | public function declare_credentials_present( $has_credentials ): bool { |
| 93 | if ( $has_credentials ) { |
| 94 | return true; |
| 95 | } |
| 96 | foreach ( $this->core->get_all_options()['ai_envs'] ?? [] as $env ) { |
| 97 | $type = $env['type'] ?? ''; |
| 98 | if ( ! $type ) { |
| 99 | continue; |
| 100 | } |
| 101 | if ( $type === 'ollama' ) { |
| 102 | return true; |
| 103 | } |
| 104 | if ( ! empty( $env['apikey'] ) ) { |
| 105 | return true; |
| 106 | } |
| 107 | } |
| 108 | return false; |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Filter for `wpai_pre_has_valid_credentials_check`. Skip the WP AI |
| 113 | * plugin's live `wp_ai_client_prompt('Test')` validation: AI Engine handles |
| 114 | * its own provider routing, and the AiClient registry doesn't know about |
| 115 | * providers like OpenRouter or Mistral, so the live test would falsely |
| 116 | * report them as invalid. |
| 117 | */ |
| 118 | public function declare_credentials_valid( $valid ): bool { |
| 119 | return true; |
| 120 | } |
| 121 | |
| 122 | public function mark_bridged_as_connected( array $data ): array { |
| 123 | if ( empty( $data['connectors'] ) || ! is_array( $data['connectors'] ) ) { |
| 124 | return $data; |
| 125 | } |
| 126 | $env_by_type = $this->envs_indexed_by_type(); |
| 127 | foreach ( $data['connectors'] as $id => &$c ) { |
| 128 | $env = $env_by_type[ $id ] ?? null; |
| 129 | if ( $env && ! empty( $env['apikey'] ) && isset( $c['authentication'] ) ) { |
| 130 | $c['authentication']['isConnected'] = true; |
| 131 | if ( empty( $c['authentication']['keySource'] ) || $c['authentication']['keySource'] === 'none' ) { |
| 132 | $c['authentication']['keySource'] = 'database'; |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | return $data; |
| 137 | } |
| 138 | |
| 139 | /** Reentrance guard for cross-bridging. */ |
| 140 | private static $bridging = false; |
| 141 | |
| 142 | // ─────────────────────────────────────────────────────────────────────── |
| 143 | // Mode helpers. |
| 144 | // ─────────────────────────────────────────────────────────────────────── |
| 145 | |
| 146 | private function get_mode(): string { |
| 147 | $mode = get_option( self::OPTION_MODE, null ); |
| 148 | if ( $mode === null ) { |
| 149 | // One-shot migration from the pre-rename key (was 'mwai_wp7_...'). |
| 150 | $legacy = get_option( 'mwai_wp7_connectors_mode', null ); |
| 151 | if ( $legacy !== null ) { |
| 152 | update_option( self::OPTION_MODE, $legacy ); |
| 153 | delete_option( 'mwai_wp7_connectors_mode' ); |
| 154 | $mode = $legacy; |
| 155 | } |
| 156 | } |
| 157 | if ( $mode === null ) { |
| 158 | $mode = 'observe'; |
| 159 | } |
| 160 | return in_array( $mode, [ 'observe', 'managed', 'off' ], true ) ? $mode : 'observe'; |
| 161 | } |
| 162 | |
| 163 | public function ajax_set_mode(): void { |
| 164 | check_ajax_referer( 'mwai_wpai_connectors', 'nonce' ); |
| 165 | if ( ! current_user_can( 'manage_options' ) ) { |
| 166 | wp_send_json_error( [ 'message' => 'forbidden' ], 403 ); |
| 167 | } |
| 168 | $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; |
| 169 | if ( ! in_array( $mode, [ 'observe', 'managed', 'off' ], true ) ) { |
| 170 | wp_send_json_error( [ 'message' => 'invalid mode' ], 400 ); |
| 171 | } |
| 172 | update_option( self::OPTION_MODE, $mode ); |
| 173 | if ( $mode === 'managed' ) { |
| 174 | $this->initial_sync(); |
| 175 | } |
| 176 | wp_send_json_success( [ 'mode' => $mode ] ); |
| 177 | } |
| 178 | |
| 179 | // ─────────────────────────────────────────────────────────────────────── |
| 180 | // Registry customization (managed mode). |
| 181 | // ─────────────────────────────────────────────────────────────────────── |
| 182 | |
| 183 | public function customize_registry( WP_Connector_Registry $registry ): void { |
| 184 | $env_by_type = $this->envs_indexed_by_type(); |
| 185 | $engine_names = $this->engine_names_by_type(); |
| 186 | |
| 187 | // Build the set of provider ids AI Engine has configured. |
| 188 | $ours = array_keys( $env_by_type ); |
| 189 | |
| 190 | // Remove any existing connector that AI Engine doesn't manage. Keeps the |
| 191 | // page tidy: only cards for providers the user actually has envs for. |
| 192 | foreach ( array_keys( $registry->get_all_registered() ) as $id ) { |
| 193 | if ( ! in_array( $id, $ours, true ) ) { |
| 194 | $registry->unregister( $id ); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | // Register / re-register each AI Engine-owned provider. |
| 199 | foreach ( $env_by_type as $type => $env ) { |
| 200 | $name = $engine_names[ $type ] ?? ucwords( $type ); |
| 201 | $description = $this->describe_bridge( $type, $env_by_type ); |
| 202 | $auth = [ 'method' => $type === 'ollama' ? 'none' : 'api_key' ]; |
| 203 | if ( isset( self::CREDENTIALS_URLS[ $type ] ) ) { |
| 204 | $auth['credentials_url'] = self::CREDENTIALS_URLS[ $type ]; |
| 205 | } |
| 206 | // WP core's _wp_register_default_connectors() auto-fills these for its own |
| 207 | // defaults; third-party register() calls don't get that treatment, and the |
| 208 | // WP AI plugin's has_ai_credentials() skips any connector with an empty |
| 209 | // setting_name. Mirror what WP core would have set so our connectors are |
| 210 | // recognised as having credentials. |
| 211 | if ( $auth['method'] === 'api_key' ) { |
| 212 | $sanitized_id = str_replace( '-', '_', $type ); |
| 213 | $auth['setting_name'] = "connectors_ai_{$sanitized_id}_api_key"; |
| 214 | $constant_case = strtoupper( preg_replace( '/([a-z])([A-Z])/', '$1_$2', $sanitized_id ) ) . '_API_KEY'; |
| 215 | $auth['constant_name'] = $constant_case; |
| 216 | $auth['env_var_name'] = $constant_case; |
| 217 | } |
| 218 | $args = [ |
| 219 | 'name' => $name, |
| 220 | 'description' => $description, |
| 221 | 'type' => 'ai_provider', |
| 222 | 'authentication' => $auth, |
| 223 | ]; |
| 224 | if ( $registry->is_registered( $type ) ) { |
| 225 | $registry->unregister( $type ); |
| 226 | } |
| 227 | $registry->register( $type, $args ); |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | /** @return array<string,string> type → human name, e.g. ['openai' => 'OpenAI']. */ |
| 232 | private function engine_names_by_type(): array { |
| 233 | $options = $this->core->get_all_options(); |
| 234 | $map = []; |
| 235 | foreach ( $options['ai_engines'] ?? [] as $engine ) { |
| 236 | if ( ! empty( $engine['type'] ) ) { |
| 237 | $map[ $engine['type'] ] = $engine['name'] ?? $engine['type']; |
| 238 | } |
| 239 | } |
| 240 | return $map; |
| 241 | } |
| 242 | |
| 243 | private function envs_indexed_by_type(): array { |
| 244 | $options = $this->core->get_all_options(); |
| 245 | $by_type = []; |
| 246 | if ( ! empty( $options['ai_envs'] ) ) { |
| 247 | foreach ( $options['ai_envs'] as $env ) { |
| 248 | $type = $env['type'] ?? ''; |
| 249 | if ( $type && ! isset( $by_type[ $type ] ) ) { |
| 250 | $by_type[ $type ] = $env; |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | return $by_type; |
| 255 | } |
| 256 | |
| 257 | private function describe_bridge( string $connector_id, array $env_by_type ): string { |
| 258 | $env = $env_by_type[ $connector_id ] ?? null; |
| 259 | if ( $env && ! empty( $env['name'] ) ) { |
| 260 | /* translators: %s: AI Engine environment name. */ |
| 261 | return sprintf( __( 'Using the "%s" environment from AI Engine.', 'ai-engine' ), $env['name'] ); |
| 262 | } |
| 263 | return __( 'Add an environment in AI Engine to enable this.', 'ai-engine' ); |
| 264 | } |
| 265 | |
| 266 | |
| 267 | // ─────────────────────────────────────────────────────────────────────── |
| 268 | // Key bridge. |
| 269 | // ─────────────────────────────────────────────────────────────────────── |
| 270 | |
| 271 | public function maybe_bridge_key_to_env_added( $option, $value ): void { |
| 272 | $this->maybe_bridge_key_to_env( $option, '', $value ); |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * When the user saves a key on the WP Connectors page, mirror it to the |
| 277 | * first AI Engine environment of that provider type. |
| 278 | */ |
| 279 | public function maybe_bridge_key_to_env( $option, $old_value, $new_value ): void { |
| 280 | if ( self::$bridging ) { |
| 281 | return; |
| 282 | } |
| 283 | if ( strpos( $option, 'connectors_ai_' ) !== 0 || substr( $option, -8 ) !== '_api_key' ) { |
| 284 | return; |
| 285 | } |
| 286 | if ( $old_value === $new_value ) { |
| 287 | return; |
| 288 | } |
| 289 | $provider_id = substr( $option, strlen( 'connectors_ai_' ), -strlen( '_api_key' ) ); |
| 290 | $provider_id = str_replace( '_', '-', $provider_id ); |
| 291 | $options = $this->core->get_all_options(); |
| 292 | if ( empty( $options['ai_envs'] ) ) { |
| 293 | return; |
| 294 | } |
| 295 | foreach ( $options['ai_envs'] as $env ) { |
| 296 | if ( isset( $env['type'] ) && $env['type'] === $provider_id ) { |
| 297 | self::$bridging = true; |
| 298 | try { |
| 299 | $this->core->update_ai_env( $env['id'], 'apikey', (string) $new_value ); |
| 300 | } finally { |
| 301 | self::$bridging = false; |
| 302 | } |
| 303 | return; |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * When AI Engine options are saved, mirror each bridged env's key to the |
| 310 | * matching WP Connectors option so the `/options-connectors.php` page shows |
| 311 | * the provider as connected. |
| 312 | */ |
| 313 | public function mirror_envs_to_wp_connectors( $old_value, $new_value ): void { |
| 314 | if ( self::$bridging ) { |
| 315 | return; |
| 316 | } |
| 317 | if ( ! is_array( $new_value ) || empty( $new_value['ai_envs'] ) ) { |
| 318 | return; |
| 319 | } |
| 320 | self::$bridging = true; |
| 321 | try { |
| 322 | $seen = []; |
| 323 | foreach ( $new_value['ai_envs'] as $env ) { |
| 324 | $type = $env['type'] ?? ''; |
| 325 | $key = $env['apikey'] ?? ''; |
| 326 | if ( ! $type || isset( $seen[ $type ] ) ) { |
| 327 | continue; |
| 328 | } |
| 329 | $seen[ $type ] = true; |
| 330 | $option_name = 'connectors_ai_' . str_replace( '-', '_', $type ) . '_api_key'; |
| 331 | $current = get_option( $option_name, '' ); |
| 332 | if ( $current !== $key ) { |
| 333 | update_option( $option_name, $key ); |
| 334 | } |
| 335 | } |
| 336 | } finally { |
| 337 | self::$bridging = false; |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | /** One-shot initial sync from AI Engine envs → WP Connectors options. */ |
| 342 | public function initial_sync(): void { |
| 343 | $options = $this->core->get_all_options(); |
| 344 | if ( ! empty( $options ) ) { |
| 345 | $this->mirror_envs_to_wp_connectors( [], $options ); |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | // ─────────────────────────────────────────────────────────────────────── |
| 350 | // Banner on the Connectors page. |
| 351 | // ─────────────────────────────────────────────────────────────────────── |
| 352 | |
| 353 | public function maybe_render_banner(): void { |
| 354 | $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; |
| 355 | $id = $screen ? $screen->id : ( $GLOBALS['hook_suffix'] ?? '' ); |
| 356 | $targets = [ 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' ]; |
| 357 | if ( ! in_array( $id, $targets, true ) ) { |
| 358 | return; |
| 359 | } |
| 360 | $this->render_banner(); |
| 361 | } |
| 362 | |
| 363 | public function render_banner(): void { |
| 364 | $mode = $this->get_mode(); |
| 365 | $nonce = wp_create_nonce( 'mwai_wpai_connectors' ); |
| 366 | $ajaxUrl = admin_url( 'admin-ajax.php' ); |
| 367 | $settingsUrl = admin_url( 'admin.php?page=mwai_settings&nekoTab=settings' ); |
| 368 | $isManaged = $mode === 'managed'; |
| 369 | |
| 370 | // Copy — short, calm, informative. No marketing shouting. |
| 371 | $title = $isManaged |
| 372 | ? __( 'Managed by AI Engine.', 'ai-engine' ) |
| 373 | : __( 'AI Engine can manage your connectors.', 'ai-engine' ); |
| 374 | |
| 375 | if ( $isManaged ) { |
| 376 | $sub = __( 'All WordPress AI requests run through AI Engine.', 'ai-engine' ); |
| 377 | } |
| 378 | else { |
| 379 | $sub = __( 'Add Insights, cost tracking, MCP tools, and six more providers in one click.', 'ai-engine' ); |
| 380 | } |
| 381 | |
| 382 | $payload = [ |
| 383 | 'nonce' => $nonce, |
| 384 | 'ajaxUrl' => $ajaxUrl, |
| 385 | 'settings' => $settingsUrl, |
| 386 | 'managed' => $isManaged, |
| 387 | 'title' => $title, |
| 388 | 'sub' => $sub, |
| 389 | 'enable' => __( 'Enable', 'ai-engine' ), |
| 390 | 'dismiss' => __( 'Dismiss', 'ai-engine' ), |
| 391 | 'configure' => __( 'Configure', 'ai-engine' ), |
| 392 | 'stop' => __( 'Stop', 'ai-engine' ), |
| 393 | 'learnMore' => __( 'Learn more', 'ai-engine' ), |
| 394 | 'learnUrl' => 'https://meowapps.com/wordpress-7-ai-engine-gateway/', |
| 395 | 'manageLead' => __( 'Need another provider? Add or edit environments in', 'ai-engine' ), |
| 396 | 'manageLink' => __( 'AI Engine settings', 'ai-engine' ), |
| 397 | ]; |
| 398 | ?> |
| 399 | <style> |
| 400 | /* Injected below the Connectors page header. Uses a quiet, native look |
| 401 | so it sits inside the SPA as if it belonged there. */ |
| 402 | .mwai-wpai-banner { |
| 403 | margin: 0 0 16px; |
| 404 | padding: 12px 16px; |
| 405 | display: flex; align-items: center; gap: 12px; |
| 406 | background: #f0f4ff; |
| 407 | border: 1px solid #d6deff; |
| 408 | border-radius: 4px; |
| 409 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 410 | font-size: 13px; line-height: 1.4; |
| 411 | color: #1e1e1e; |
| 412 | } |
| 413 | .mwai-wpai-banner.is-managed { background: #e8f8ef; border-color: #c3ecd3; } |
| 414 | .mwai-wpai-banner-icon { |
| 415 | width: 32px; height: 32px; border-radius: 50%; |
| 416 | background: #2f5fff; color: #fff; |
| 417 | display: flex; align-items: center; justify-content: center; |
| 418 | flex-shrink: 0; |
| 419 | } |
| 420 | .mwai-wpai-banner.is-managed .mwai-wpai-banner-icon { background: #10b981; } |
| 421 | .mwai-wpai-banner-icon svg { width: 18px; height: 18px; display: block; } |
| 422 | .mwai-wpai-banner-text { flex: 1; min-width: 0; } |
| 423 | .mwai-wpai-banner-text strong { font-weight: 600; } |
| 424 | .mwai-wpai-banner-text span { color: #50575e; } |
| 425 | .mwai-wpai-banner-actions { display: flex; gap: 6px; flex-shrink: 0; } |
| 426 | .mwai-wpai-btn { |
| 427 | appearance: none; |
| 428 | border: 1px solid transparent; |
| 429 | border-radius: 3px; |
| 430 | padding: 4px 12px; |
| 431 | font: inherit; font-size: 12px; font-weight: 500; |
| 432 | cursor: pointer; |
| 433 | text-decoration: none; |
| 434 | transition: background 0.12s ease, border-color 0.12s ease; |
| 435 | } |
| 436 | .mwai-wpai-btn-primary { background: #2f5fff; color: #fff; } |
| 437 | .mwai-wpai-btn-primary:hover { background: #2448cc; color: #fff; } |
| 438 | .mwai-wpai-banner.is-managed .mwai-wpai-btn-primary { background: #10b981; } |
| 439 | .mwai-wpai-banner.is-managed .mwai-wpai-btn-primary:hover { background: #0d9b6d; } |
| 440 | .mwai-wpai-btn-ghost { background: transparent; color: #50575e; border-color: transparent; } |
| 441 | .mwai-wpai-btn-ghost:hover { background: rgba(0,0,0,0.04); color: #1e1e1e; } |
| 442 | .mwai-wpai-btn[disabled] { opacity: 0.5; cursor: default; } |
| 443 | </style> |
| 444 | <script> |
| 445 | (function () { |
| 446 | var D = <?php echo wp_json_encode( $payload ); ?>; |
| 447 | |
| 448 | function build() { |
| 449 | var host = document.createElement('div'); |
| 450 | host.className = 'mwai-wpai-banner' + (D.managed ? ' is-managed' : ''); |
| 451 | host.setAttribute('role', 'status'); |
| 452 | // Checkmark when managed (high-confidence success). Spark icon |
| 453 | // when we're just inviting the user to turn the takeover on. |
| 454 | var iconSvg = D.managed |
| 455 | ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="5 12 10 17 19 7"/></svg>' |
| 456 | : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>'; |
| 457 | host.innerHTML = [ |
| 458 | '<span class="mwai-wpai-banner-icon">', iconSvg, '</span>', |
| 459 | '<div class="mwai-wpai-banner-text">', |
| 460 | '<strong></strong> <span class="mwai-wpai-banner-sub"></span>', |
| 461 | '</div>', |
| 462 | '<div class="mwai-wpai-banner-actions"></div>', |
| 463 | ].join(''); |
| 464 | host.querySelector('strong').textContent = D.title; |
| 465 | host.querySelector('.mwai-wpai-banner-sub').textContent = D.sub; |
| 466 | |
| 467 | var actions = host.querySelector('.mwai-wpai-banner-actions'); |
| 468 | function btn(label, cls, onClick, href) { |
| 469 | var b = document.createElement((onClick === 'link' || href) ? 'a' : 'button'); |
| 470 | b.className = 'mwai-wpai-btn ' + cls; |
| 471 | b.textContent = label; |
| 472 | if (href) { |
| 473 | b.href = href; |
| 474 | b.target = '_blank'; |
| 475 | b.rel = 'noopener noreferrer'; |
| 476 | } else if (onClick === 'link') { |
| 477 | b.href = D.settings; |
| 478 | } else { |
| 479 | b.addEventListener('click', onClick); |
| 480 | } |
| 481 | actions.appendChild(b); |
| 482 | } |
| 483 | function toggle(mode) { |
| 484 | return function (e) { |
| 485 | var t = e.currentTarget; t.disabled = true; |
| 486 | var f = new FormData(); |
| 487 | f.append('action', 'mwai_wpai_connectors_set_mode'); |
| 488 | f.append('nonce', D.nonce); |
| 489 | f.append('mode', mode); |
| 490 | fetch(D.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: f }) |
| 491 | .then(function () { window.location.reload(); }) |
| 492 | .catch(function () { t.disabled = false; }); |
| 493 | }; |
| 494 | } |
| 495 | if (D.managed) { |
| 496 | btn(D.configure, 'mwai-wpai-btn-primary', 'link'); |
| 497 | btn(D.stop, 'mwai-wpai-btn-ghost', toggle('observe')); |
| 498 | } else { |
| 499 | btn(D.enable, 'mwai-wpai-btn-primary', toggle('managed')); |
| 500 | btn(D.learnMore, 'mwai-wpai-btn-ghost', null, D.learnUrl); |
| 501 | btn(D.dismiss, 'mwai-wpai-btn-ghost', toggle('off')); |
| 502 | } |
| 503 | return host; |
| 504 | } |
| 505 | |
| 506 | // Insert as the first child of .connectors-page so it inherits the |
| 507 | // same column width and padding as the provider cards. Fall back to |
| 508 | // placing it after the page header if .connectors-page isn't found. |
| 509 | function ensureBanner() { |
| 510 | if (document.querySelector('.mwai-wpai-banner')) return; |
| 511 | var page = document.querySelector('.connectors-page'); |
| 512 | if (page) { |
| 513 | page.insertBefore(build(), page.firstChild); |
| 514 | return; |
| 515 | } |
| 516 | var header = document.querySelector('.boot-layout__stage header'); |
| 517 | if (header && header.parentNode) { |
| 518 | header.parentNode.insertBefore(build(), header.nextSibling); |
| 519 | } |
| 520 | } |
| 521 | |
| 522 | // When managed, rewrite the trailing "search the plugin directory" line |
| 523 | // and redirect any "Edit" button clicks to AI Engine's settings, since |
| 524 | // editing a connector here would modify WP AI Client state — not AI |
| 525 | // Engine state — and that divergence would be confusing. |
| 526 | function tweakManagedPage() { |
| 527 | if (!D.managed) return; |
| 528 | var page = document.querySelector('.connectors-page'); |
| 529 | if (!page) return; |
| 530 | |
| 531 | // Replace the "search the plugin directory" hint with a pointer to AI |
| 532 | // Engine. We flag the <p> with data-mwai so we don't keep re-writing. |
| 533 | var ps = page.querySelectorAll('p'); |
| 534 | for (var i = 0; i < ps.length; i++) { |
| 535 | var p = ps[i]; |
| 536 | if (p.dataset.mwaiReplaced) continue; |
| 537 | var t = (p.textContent || '').toLowerCase(); |
| 538 | if (t.indexOf('plugin directory') !== -1 || t.indexOf('search') !== -1) { |
| 539 | p.innerHTML = ''; |
| 540 | var a = document.createElement('a'); |
| 541 | a.href = D.settings; |
| 542 | a.textContent = D.manageLink; |
| 543 | p.appendChild(document.createTextNode(D.manageLead + ' ')); |
| 544 | p.appendChild(a); |
| 545 | p.appendChild(document.createTextNode('.')); |
| 546 | p.dataset.mwaiReplaced = '1'; |
| 547 | } |
| 548 | } |
| 549 | |
| 550 | // Redirect Edit clicks. |
| 551 | var buttons = page.querySelectorAll('button'); |
| 552 | for (var j = 0; j < buttons.length; j++) { |
| 553 | var b = buttons[j]; |
| 554 | if (b.dataset.mwaiIntercepted) continue; |
| 555 | if ((b.textContent || '').trim() === 'Edit') { |
| 556 | b.dataset.mwaiIntercepted = '1'; |
| 557 | b.addEventListener('click', function (e) { |
| 558 | e.preventDefault(); |
| 559 | e.stopPropagation(); |
| 560 | window.location.href = D.settings; |
| 561 | }, true); |
| 562 | } |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | function run() { ensureBanner(); tweakManagedPage(); } |
| 567 | |
| 568 | var tries = 0; |
| 569 | var iv = setInterval(function () { |
| 570 | run(); |
| 571 | if (document.querySelector('.mwai-wpai-banner') && ++tries > 40) clearInterval(iv); |
| 572 | }, 120); |
| 573 | |
| 574 | var app = document.getElementById('options-connectors-wp-admin-app') |
| 575 | || document.getElementById('options-connectors-app'); |
| 576 | if (app && 'MutationObserver' in window) { |
| 577 | new MutationObserver(run).observe(app, { childList: true, subtree: true }); |
| 578 | } |
| 579 | })(); |
| 580 | </script> |
| 581 | <?php |
| 582 | } |
| 583 | } |
| 584 |