PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.0
3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / labs / wpai-connectors.php
ai-engine / labs Last commit date
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