PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.4.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.4.7
3.6.2 3.6.1 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 2 months ago mcp-rest.php 3 months ago mcp.conf 1 year ago mcp.js 8 months ago mcp.md 8 months ago mcp.php 3 months 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
526 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 }
75
76 public function mark_bridged_as_connected( array $data ): array {
77 if ( empty( $data['connectors'] ) || ! is_array( $data['connectors'] ) ) {
78 return $data;
79 }
80 $env_by_type = $this->envs_indexed_by_type();
81 foreach ( $data['connectors'] as $id => &$c ) {
82 $env = $env_by_type[ $id ] ?? null;
83 if ( $env && ! empty( $env['apikey'] ) && isset( $c['authentication'] ) ) {
84 $c['authentication']['isConnected'] = true;
85 if ( empty( $c['authentication']['keySource'] ) || $c['authentication']['keySource'] === 'none' ) {
86 $c['authentication']['keySource'] = 'database';
87 }
88 }
89 }
90 return $data;
91 }
92
93 /** Reentrance guard for cross-bridging. */
94 private static $bridging = false;
95
96 // ───────────────────────────────────────────────────────────────────────
97 // Mode helpers.
98 // ───────────────────────────────────────────────────────────────────────
99
100 private function get_mode(): string {
101 $mode = get_option( self::OPTION_MODE, null );
102 if ( $mode === null ) {
103 // One-shot migration from the pre-rename key (was 'mwai_wp7_...').
104 $legacy = get_option( 'mwai_wp7_connectors_mode', null );
105 if ( $legacy !== null ) {
106 update_option( self::OPTION_MODE, $legacy );
107 delete_option( 'mwai_wp7_connectors_mode' );
108 $mode = $legacy;
109 }
110 }
111 if ( $mode === null ) {
112 $mode = 'observe';
113 }
114 return in_array( $mode, [ 'observe', 'managed', 'off' ], true ) ? $mode : 'observe';
115 }
116
117 public function ajax_set_mode(): void {
118 check_ajax_referer( 'mwai_wpai_connectors', 'nonce' );
119 if ( ! current_user_can( 'manage_options' ) ) {
120 wp_send_json_error( [ 'message' => 'forbidden' ], 403 );
121 }
122 $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : '';
123 if ( ! in_array( $mode, [ 'observe', 'managed', 'off' ], true ) ) {
124 wp_send_json_error( [ 'message' => 'invalid mode' ], 400 );
125 }
126 update_option( self::OPTION_MODE, $mode );
127 if ( $mode === 'managed' ) {
128 $this->initial_sync();
129 }
130 wp_send_json_success( [ 'mode' => $mode ] );
131 }
132
133 // ───────────────────────────────────────────────────────────────────────
134 // Registry customization (managed mode).
135 // ───────────────────────────────────────────────────────────────────────
136
137 public function customize_registry( WP_Connector_Registry $registry ): void {
138 $env_by_type = $this->envs_indexed_by_type();
139 $engine_names = $this->engine_names_by_type();
140
141 // Build the set of provider ids AI Engine has configured.
142 $ours = array_keys( $env_by_type );
143
144 // Remove any existing connector that AI Engine doesn't manage. Keeps the
145 // page tidy: only cards for providers the user actually has envs for.
146 foreach ( array_keys( $registry->get_all_registered() ) as $id ) {
147 if ( ! in_array( $id, $ours, true ) ) {
148 $registry->unregister( $id );
149 }
150 }
151
152 // Register / re-register each AI Engine-owned provider.
153 foreach ( $env_by_type as $type => $env ) {
154 $name = $engine_names[ $type ] ?? ucwords( $type );
155 $description = $this->describe_bridge( $type, $env_by_type );
156 $auth = [ 'method' => $type === 'ollama' ? 'none' : 'api_key' ];
157 if ( isset( self::CREDENTIALS_URLS[ $type ] ) ) {
158 $auth['credentials_url'] = self::CREDENTIALS_URLS[ $type ];
159 }
160 $args = [
161 'name' => $name,
162 'description' => $description,
163 'type' => 'ai_provider',
164 'authentication' => $auth,
165 ];
166 if ( $registry->is_registered( $type ) ) {
167 $registry->unregister( $type );
168 }
169 $registry->register( $type, $args );
170 }
171 }
172
173 /** @return array<string,string> type → human name, e.g. ['openai' => 'OpenAI']. */
174 private function engine_names_by_type(): array {
175 $options = $this->core->get_all_options();
176 $map = [];
177 foreach ( $options['ai_engines'] ?? [] as $engine ) {
178 if ( ! empty( $engine['type'] ) ) {
179 $map[ $engine['type'] ] = $engine['name'] ?? $engine['type'];
180 }
181 }
182 return $map;
183 }
184
185 private function envs_indexed_by_type(): array {
186 $options = $this->core->get_all_options();
187 $by_type = [];
188 if ( ! empty( $options['ai_envs'] ) ) {
189 foreach ( $options['ai_envs'] as $env ) {
190 $type = $env['type'] ?? '';
191 if ( $type && ! isset( $by_type[ $type ] ) ) {
192 $by_type[ $type ] = $env;
193 }
194 }
195 }
196 return $by_type;
197 }
198
199 private function describe_bridge( string $connector_id, array $env_by_type ): string {
200 $env = $env_by_type[ $connector_id ] ?? null;
201 if ( $env && ! empty( $env['name'] ) ) {
202 /* translators: %s: AI Engine environment name. */
203 return sprintf( __( 'Using the "%s" environment from AI Engine.', 'ai-engine' ), $env['name'] );
204 }
205 return __( 'Add an environment in AI Engine to enable this.', 'ai-engine' );
206 }
207
208
209 // ───────────────────────────────────────────────────────────────────────
210 // Key bridge.
211 // ───────────────────────────────────────────────────────────────────────
212
213 public function maybe_bridge_key_to_env_added( $option, $value ): void {
214 $this->maybe_bridge_key_to_env( $option, '', $value );
215 }
216
217 /**
218 * When the user saves a key on the WP Connectors page, mirror it to the
219 * first AI Engine environment of that provider type.
220 */
221 public function maybe_bridge_key_to_env( $option, $old_value, $new_value ): void {
222 if ( self::$bridging ) {
223 return;
224 }
225 if ( strpos( $option, 'connectors_ai_' ) !== 0 || substr( $option, -8 ) !== '_api_key' ) {
226 return;
227 }
228 if ( $old_value === $new_value ) {
229 return;
230 }
231 $provider_id = substr( $option, strlen( 'connectors_ai_' ), -strlen( '_api_key' ) );
232 $provider_id = str_replace( '_', '-', $provider_id );
233 $options = $this->core->get_all_options();
234 if ( empty( $options['ai_envs'] ) ) {
235 return;
236 }
237 foreach ( $options['ai_envs'] as $env ) {
238 if ( isset( $env['type'] ) && $env['type'] === $provider_id ) {
239 self::$bridging = true;
240 try {
241 $this->core->update_ai_env( $env['id'], 'apikey', (string) $new_value );
242 } finally {
243 self::$bridging = false;
244 }
245 return;
246 }
247 }
248 }
249
250 /**
251 * When AI Engine options are saved, mirror each bridged env's key to the
252 * matching WP Connectors option so the `/options-connectors.php` page shows
253 * the provider as connected.
254 */
255 public function mirror_envs_to_wp_connectors( $old_value, $new_value ): void {
256 if ( self::$bridging ) {
257 return;
258 }
259 if ( ! is_array( $new_value ) || empty( $new_value['ai_envs'] ) ) {
260 return;
261 }
262 self::$bridging = true;
263 try {
264 $seen = [];
265 foreach ( $new_value['ai_envs'] as $env ) {
266 $type = $env['type'] ?? '';
267 $key = $env['apikey'] ?? '';
268 if ( ! $type || isset( $seen[ $type ] ) ) {
269 continue;
270 }
271 $seen[ $type ] = true;
272 $option_name = 'connectors_ai_' . str_replace( '-', '_', $type ) . '_api_key';
273 $current = get_option( $option_name, '' );
274 if ( $current !== $key ) {
275 update_option( $option_name, $key );
276 }
277 }
278 } finally {
279 self::$bridging = false;
280 }
281 }
282
283 /** One-shot initial sync from AI Engine envs → WP Connectors options. */
284 public function initial_sync(): void {
285 $options = $this->core->get_all_options();
286 if ( ! empty( $options ) ) {
287 $this->mirror_envs_to_wp_connectors( [], $options );
288 }
289 }
290
291 // ───────────────────────────────────────────────────────────────────────
292 // Banner on the Connectors page.
293 // ───────────────────────────────────────────────────────────────────────
294
295 public function maybe_render_banner(): void {
296 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
297 $id = $screen ? $screen->id : ( $GLOBALS['hook_suffix'] ?? '' );
298 $targets = [ 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' ];
299 if ( ! in_array( $id, $targets, true ) ) {
300 return;
301 }
302 $this->render_banner();
303 }
304
305 public function render_banner(): void {
306 $mode = $this->get_mode();
307 $nonce = wp_create_nonce( 'mwai_wpai_connectors' );
308 $ajaxUrl = admin_url( 'admin-ajax.php' );
309 $settingsUrl = admin_url( 'admin.php?page=mwai_settings&nekoTab=settings' );
310 $isManaged = $mode === 'managed';
311
312 // Copy — short, calm, informative. No marketing shouting.
313 $title = $isManaged
314 ? __( 'Managed by AI Engine.', 'ai-engine' )
315 : __( 'AI Engine can manage your connectors.', 'ai-engine' );
316
317 if ( $isManaged ) {
318 $sub = __( 'All WordPress AI requests run through AI Engine.', 'ai-engine' );
319 }
320 else {
321 $sub = __( 'Add Insights, cost tracking, MCP tools, and six more providers in one click.', 'ai-engine' );
322 }
323
324 $payload = [
325 'nonce' => $nonce,
326 'ajaxUrl' => $ajaxUrl,
327 'settings' => $settingsUrl,
328 'managed' => $isManaged,
329 'title' => $title,
330 'sub' => $sub,
331 'enable' => __( 'Enable', 'ai-engine' ),
332 'dismiss' => __( 'Dismiss', 'ai-engine' ),
333 'configure' => __( 'Configure', 'ai-engine' ),
334 'stop' => __( 'Stop', 'ai-engine' ),
335 'learnMore' => __( 'Learn more', 'ai-engine' ),
336 'learnUrl' => 'https://meowapps.com/wordpress-7-ai-engine-gateway/',
337 'manageLead' => __( 'Need another provider? Add or edit environments in', 'ai-engine' ),
338 'manageLink' => __( 'AI Engine settings', 'ai-engine' ),
339 ];
340 ?>
341 <style>
342 /* Injected below the Connectors page header. Uses a quiet, native look
343 so it sits inside the SPA as if it belonged there. */
344 .mwai-wpai-banner {
345 margin: 0 0 16px;
346 padding: 12px 16px;
347 display: flex; align-items: center; gap: 12px;
348 background: #f0f4ff;
349 border: 1px solid #d6deff;
350 border-radius: 4px;
351 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
352 font-size: 13px; line-height: 1.4;
353 color: #1e1e1e;
354 }
355 .mwai-wpai-banner.is-managed { background: #e8f8ef; border-color: #c3ecd3; }
356 .mwai-wpai-banner-icon {
357 width: 32px; height: 32px; border-radius: 50%;
358 background: #2f5fff; color: #fff;
359 display: flex; align-items: center; justify-content: center;
360 flex-shrink: 0;
361 }
362 .mwai-wpai-banner.is-managed .mwai-wpai-banner-icon { background: #10b981; }
363 .mwai-wpai-banner-icon svg { width: 18px; height: 18px; display: block; }
364 .mwai-wpai-banner-text { flex: 1; min-width: 0; }
365 .mwai-wpai-banner-text strong { font-weight: 600; }
366 .mwai-wpai-banner-text span { color: #50575e; }
367 .mwai-wpai-banner-actions { display: flex; gap: 6px; flex-shrink: 0; }
368 .mwai-wpai-btn {
369 appearance: none;
370 border: 1px solid transparent;
371 border-radius: 3px;
372 padding: 4px 12px;
373 font: inherit; font-size: 12px; font-weight: 500;
374 cursor: pointer;
375 text-decoration: none;
376 transition: background 0.12s ease, border-color 0.12s ease;
377 }
378 .mwai-wpai-btn-primary { background: #2f5fff; color: #fff; }
379 .mwai-wpai-btn-primary:hover { background: #2448cc; color: #fff; }
380 .mwai-wpai-banner.is-managed .mwai-wpai-btn-primary { background: #10b981; }
381 .mwai-wpai-banner.is-managed .mwai-wpai-btn-primary:hover { background: #0d9b6d; }
382 .mwai-wpai-btn-ghost { background: transparent; color: #50575e; border-color: transparent; }
383 .mwai-wpai-btn-ghost:hover { background: rgba(0,0,0,0.04); color: #1e1e1e; }
384 .mwai-wpai-btn[disabled] { opacity: 0.5; cursor: default; }
385 </style>
386 <script>
387 (function () {
388 var D = <?php echo wp_json_encode( $payload ); ?>;
389
390 function build() {
391 var host = document.createElement('div');
392 host.className = 'mwai-wpai-banner' + (D.managed ? ' is-managed' : '');
393 host.setAttribute('role', 'status');
394 // Checkmark when managed (high-confidence success). Spark icon
395 // when we're just inviting the user to turn the takeover on.
396 var iconSvg = D.managed
397 ? '<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>'
398 : '<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>';
399 host.innerHTML = [
400 '<span class="mwai-wpai-banner-icon">', iconSvg, '</span>',
401 '<div class="mwai-wpai-banner-text">',
402 '<strong></strong> <span class="mwai-wpai-banner-sub"></span>',
403 '</div>',
404 '<div class="mwai-wpai-banner-actions"></div>',
405 ].join('');
406 host.querySelector('strong').textContent = D.title;
407 host.querySelector('.mwai-wpai-banner-sub').textContent = D.sub;
408
409 var actions = host.querySelector('.mwai-wpai-banner-actions');
410 function btn(label, cls, onClick, href) {
411 var b = document.createElement((onClick === 'link' || href) ? 'a' : 'button');
412 b.className = 'mwai-wpai-btn ' + cls;
413 b.textContent = label;
414 if (href) {
415 b.href = href;
416 b.target = '_blank';
417 b.rel = 'noopener noreferrer';
418 } else if (onClick === 'link') {
419 b.href = D.settings;
420 } else {
421 b.addEventListener('click', onClick);
422 }
423 actions.appendChild(b);
424 }
425 function toggle(mode) {
426 return function (e) {
427 var t = e.currentTarget; t.disabled = true;
428 var f = new FormData();
429 f.append('action', 'mwai_wpai_connectors_set_mode');
430 f.append('nonce', D.nonce);
431 f.append('mode', mode);
432 fetch(D.ajaxUrl, { method: 'POST', credentials: 'same-origin', body: f })
433 .then(function () { window.location.reload(); })
434 .catch(function () { t.disabled = false; });
435 };
436 }
437 if (D.managed) {
438 btn(D.configure, 'mwai-wpai-btn-primary', 'link');
439 btn(D.stop, 'mwai-wpai-btn-ghost', toggle('observe'));
440 } else {
441 btn(D.enable, 'mwai-wpai-btn-primary', toggle('managed'));
442 btn(D.learnMore, 'mwai-wpai-btn-ghost', null, D.learnUrl);
443 btn(D.dismiss, 'mwai-wpai-btn-ghost', toggle('off'));
444 }
445 return host;
446 }
447
448 // Insert as the first child of .connectors-page so it inherits the
449 // same column width and padding as the provider cards. Fall back to
450 // placing it after the page header if .connectors-page isn't found.
451 function ensureBanner() {
452 if (document.querySelector('.mwai-wpai-banner')) return;
453 var page = document.querySelector('.connectors-page');
454 if (page) {
455 page.insertBefore(build(), page.firstChild);
456 return;
457 }
458 var header = document.querySelector('.boot-layout__stage header');
459 if (header && header.parentNode) {
460 header.parentNode.insertBefore(build(), header.nextSibling);
461 }
462 }
463
464 // When managed, rewrite the trailing "search the plugin directory" line
465 // and redirect any "Edit" button clicks to AI Engine's settings, since
466 // editing a connector here would modify WP AI Client state — not AI
467 // Engine state — and that divergence would be confusing.
468 function tweakManagedPage() {
469 if (!D.managed) return;
470 var page = document.querySelector('.connectors-page');
471 if (!page) return;
472
473 // Replace the "search the plugin directory" hint with a pointer to AI
474 // Engine. We flag the <p> with data-mwai so we don't keep re-writing.
475 var ps = page.querySelectorAll('p');
476 for (var i = 0; i < ps.length; i++) {
477 var p = ps[i];
478 if (p.dataset.mwaiReplaced) continue;
479 var t = (p.textContent || '').toLowerCase();
480 if (t.indexOf('plugin directory') !== -1 || t.indexOf('search') !== -1) {
481 p.innerHTML = '';
482 var a = document.createElement('a');
483 a.href = D.settings;
484 a.textContent = D.manageLink;
485 p.appendChild(document.createTextNode(D.manageLead + ' '));
486 p.appendChild(a);
487 p.appendChild(document.createTextNode('.'));
488 p.dataset.mwaiReplaced = '1';
489 }
490 }
491
492 // Redirect Edit clicks.
493 var buttons = page.querySelectorAll('button');
494 for (var j = 0; j < buttons.length; j++) {
495 var b = buttons[j];
496 if (b.dataset.mwaiIntercepted) continue;
497 if ((b.textContent || '').trim() === 'Edit') {
498 b.dataset.mwaiIntercepted = '1';
499 b.addEventListener('click', function (e) {
500 e.preventDefault();
501 e.stopPropagation();
502 window.location.href = D.settings;
503 }, true);
504 }
505 }
506 }
507
508 function run() { ensureBanner(); tweakManagedPage(); }
509
510 var tries = 0;
511 var iv = setInterval(function () {
512 run();
513 if (document.querySelector('.mwai-wpai-banner') && ++tries > 40) clearInterval(iv);
514 }, 120);
515
516 var app = document.getElementById('options-connectors-wp-admin-app')
517 || document.getElementById('options-connectors-app');
518 if (app && 'MutationObserver' in window) {
519 new MutationObserver(run).observe(app, { childList: true, subtree: true });
520 }
521 })();
522 </script>
523 <?php
524 }
525 }
526