PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/Admin.php +195 -91 3.0.43.2.1 View file →
@@ -9,16 +9,20 @@
9 9 defined('ABSPATH') || die('No direct access.');
10 10
11 11 use Extendify\Agent\Controllers\ChatHistoryController;
12 12 use Extendify\Agent\Controllers\TourController;
13 -use Extendify\Agent\Controllers\WorkflowHistoryController;
14 13 use Extendify\Config;
14 +use Extendify\Constants;
15 15 use Extendify\Shared\Services\Escaper;
16 +use Extendify\Shared\Services\HttpClient;
16 17 use Extendify\Agent\TagBlocks;
17 18 use Extendify\Agent\TagTemplateParts;
19 +use Extendify\Agent\WooProductImages;
20 +use Extendify\Agent\AbilitiesDiscovery;
18 21 use Extendify\Agent\Controllers\SiteNavigationController;
19 22 use Extendify\PartnerData;
20 23 use Extendify\Shared\DataProvider\ProductsData;
24 +use Extendify\SiteVisibility;
21 25
22 26 /**
23 27 * This class handles any file loading for the admin area.
24 28 */
@@ -33,9 +37,8 @@
33 37 {
34 38 \add_action('admin_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
35 39 \add_action('wp_enqueue_scripts', [$this, 'loadScriptsAndStyles']);
36 40 ChatHistoryController::init();
37 - WorkflowHistoryController::init();
38 41
39 42 // Tag blocks so we can identify them later
40 43 TagBlocks::init();
41 44 TagTemplateParts::init();
@@ -41,11 +44,31 @@
41 44 TagTemplateParts::init();
42 45
43 46 // Add the site navigation ids to the navigation blocks
44 47 SiteNavigationController::init();
48 +
49 + Skeleton::init();
50 +
51 + WooProductImages::init();
52 +
53 + \add_action('extendify_agent_suggestions_refresh', [$this, 'refreshSuggestions']);
45 54 }
46 55
47 56 /**
57 + * Where the agent panel mounts — docked-left only on the onboarding front end.
58 + *
59 + * @return string
60 + */
61 + public static function agentPosition()
62 + {
63 + $agentOnboarding = PartnerData::setting('useAgentOnboarding') ||
64 + Config::preview('agent-onboarding') ||
65 + constant('EXTENDIFY_DEVMODE');
66 +
67 + return $agentOnboarding && !is_admin() ? 'docked-left' : 'floating';
68 + }
69 +
70 + /**
48 71 * Adds various JS scripts and styles
49 72 *
50 73 * @return void
51 74 */
@@ -50,8 +73,14 @@
50 73 * @return void
51 74 */
52 75 public function loadScriptsAndStyles()
53 76 {
77 + // The Customizer preview iframe is a front-end render, so this fires
78 + // there too — but the Agent only belongs on the live, top-level page.
79 + if (is_customize_preview()) {
80 + return;
81 + }
82 +
54 83 $version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version;
55 84 $scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-agent.php'];
56 85 $fallback = [
57 86 'dependencies' => [],
@@ -90,14 +119,13 @@
90 119 (bool) use_block_editor_for_post($this->getCurrentPostId()) :
91 120 false,
92 121 'isOnEditorOrFSE' => $this->isGutenbergOrFse(),
93 122 'activePlugins' => array_values(\get_option('active_plugins', [])),
94 - // Whether the user is using the vibes experience or not.
95 - 'isUsingVibes' => (bool) file_exists(EXTENDIFY_PATH . 'src/Launch/_data/block-style-variations.json') &&
96 - version_compare(wp_get_theme("extendable")->get('Version'), '2.0.32', '>='),
123 + 'isUsingVibes' => version_compare((string) wp_get_theme('extendable')->get('Version'), '2.0.32', '>='),
97 124 'siteTitle' => \esc_attr(\get_bloginfo('name')),
98 125 'siteDescription' => \esc_attr(\get_bloginfo('description')),
99 126 'themePresets' => $this->getThemePresets(),
127 + 'presetSlugs' => $this->getPresetSlugs(),
100 128 ];
101 129 $recommendations = ProductsData::get() ?? [];
102 130 $pluginRecommendations = array_filter($recommendations, function ($item) {
103 131 return in_array('ai-agent', $item['slots'] ?? [], true) && $item['ctaType'] === 'plugin';
@@ -112,8 +140,10 @@
112 140 }, $pluginRecommendations));
113 141 $agentContext = [
114 142 'availableAdminPages' => get_option('_transient_extendify_admin_pages_menu', []),
115 143 'pluginRecommendations' => $mappedPluginRecommendations,
144 + 'sitePublished' => SiteVisibility::isPublished(),
145 + 'comingSoonEnabled' => (bool) PartnerData::setting('useComingSoon'),
116 146 ];
117 147 $abilities = [
118 148 'canEditPost' => (bool) \current_user_can('edit_post', \get_queried_object_id()),
119 149 // TODO: this may be true for a user, while they still can't edit every post
@@ -136,24 +166,33 @@
136 166 Config::$slug . '-agent-scripts',
137 167 'window.extAgentData = ' . \wp_json_encode([
138 168 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
139 169 'startOnboarding' => isset($_GET['extendify-launch-success']) && $agentOnboarding,
140 - 'agentPosition' => $agentOnboarding && !is_admin() ? 'docked-left' : 'floating',
170 + 'agentPosition' => self::agentPosition(),
141 171 // Add context about where they are
142 172 'context' => $context,
143 - // Context that the Agent might need when returning a response,
144 - // but not for handling the workflow.
173 + // Material a workflow may want; each declares the keys it reads.
145 174 'agentContext' => $agentContext,
146 175 // List of abilities the AI can perform for this user.
147 176 // For example, we could check whether their theme has variations.
148 177 'abilities' => $abilities,
178 + // Registered WordPress Abilities (6.9+) this user may run.
179 + 'wpAbilities' => AbilitiesDiscovery::discover(),
149 180 // List of suggestions the AI can make for this user.
150 181 // For example, we could check whether they need to set up a specific plugin.
151 182 'suggestions' => $this->getSuggestions(),
183 + 'domainsSuggestionSettings' => [
184 + 'showPrimary' => (bool) PartnerData::setting('showPrimaryDomainRecommendationAgent'),
185 + 'showSecondary' => (bool) PartnerData::setting('showSecondaryDomainRecommendationAgent'),
186 + 'stagingSites' => PartnerData::setting('stagingSites'),
187 + 'searchUrl' => PartnerData::setting('domainSearchURL'),
188 + ],
152 189 'chatHistory' => ChatHistoryController::getChatHistory(),
153 - 'workflowHistory' => WorkflowHistoryController::getWorkflowHistory(),
154 190 'userData' => [
155 191 'tourData' => \wp_json_encode(TourController::get()->get_data()),
192 + 'domainsRecommendationsActivities' => \wp_json_encode(
193 + \get_option('extendify_domains_recommendations_activities', null)
194 + ),
156 195 ],
157 196 ]),
158 197 'before'
159 198 );
@@ -258,8 +297,85 @@
258 297 'colorPairs' => $colorPairs,
259 298 ];
260 299 }
261 300
301 + /**
302 + * Preset slugs per design-token family across every origin. Feeds the Agent's
303 + * block-attribute schema so the model picks named tokens, not raw CSS.
304 + *
305 + * @return array<string,string[]>
306 + */
307 + private function getPresetSlugs()
308 + {
309 + if (!function_exists('wp_get_global_settings')) {
310 + return [];
311 + }
312 +
313 + $color = \wp_get_global_settings(['color']);
314 + $typography = \wp_get_global_settings(['typography']);
315 +
316 + return [
317 + 'color' => $this->originSlugs($color, 'palette', 'defaultPalette'),
318 + 'colorValues' => $this->originValues($color, 'palette', 'defaultPalette', 'color'),
319 + 'gradient' => $this->originSlugs($color, 'gradients', 'defaultGradients'),
320 + 'fontSize' => $this->originSlugs($typography, 'fontSizes', 'defaultFontSizes'),
321 + 'fontFamily' => $this->originSlugs($typography, 'fontFamilies', 'defaultFontFamilies'),
322 + ];
323 + }
324 +
325 + /**
326 + * Slug => value, so a colour written as its own hex finds the token naming it.
327 + *
328 + * @return array<string, string>
329 + */
330 + private function originValues($node, $listKey, $defaultKey, $valueKey)
331 + {
332 + $list = $node[$listKey] ?? [];
333 +
334 + $origins = ['custom', 'theme'];
335 + if (($node[$defaultKey] ?? true) !== false) {
336 + $origins[] = 'default';
337 + }
338 +
339 + $values = [];
340 + foreach ($origins as $origin) {
341 + foreach ($list[$origin] ?? [] as $item) {
342 + if (isset($item['slug'], $item[$valueKey])) {
343 + $values[(string) $item['slug']] = (string) $item[$valueKey];
344 + }
345 + }
346 + }
347 +
348 + return $values;
349 + }
350 +
351 + /**
352 + * Preset slugs from a settings node's origin buckets; drops the `default`
353 + * origin when the theme opted the family out.
354 + *
355 + * @return string[]
356 + */
357 + private function originSlugs($node, $listKey, $defaultKey)
358 + {
359 + $list = $node[$listKey] ?? [];
360 +
361 + $origins = ['custom', 'theme'];
362 + if (($node[$defaultKey] ?? true) !== false) {
363 + $origins[] = 'default';
364 + }
365 +
366 + $slugs = [];
367 + foreach ($origins as $origin) {
368 + foreach ($list[$origin] ?? [] as $item) {
369 + if (isset($item['slug'])) {
370 + $slugs[] = (string) $item['slug'];
371 + }
372 + }
373 + }
374 +
375 + return array_values(array_unique($slugs));
376 + }
377 +
262 378 private static function extractColorSlug(string $value)
263 379 {
264 380 if (preg_match('/var\(--wp--preset--color--([^)]+)\)/', $value, $m)) {
265 381 return $m[1];
@@ -366,90 +482,78 @@
366 482 * @return array
367 483 */
368 484 private function getSuggestions()
369 485 {
370 - return [
371 - [
372 - 'id' => 'publish-this-page',
373 - 'icon' => 'published',
374 - 'message' => __('Publish this page', 'extendify-local'),
375 - 'workflowId' => 'update-post-status',
376 - 'source' => 'plugin',
377 - 'available' => ['context' => ['postStatus' => 'draft']],
378 - ],
379 - [
380 - 'id' => 'unpublish-this-page',
381 - 'icon' => 'drafts',
382 - 'message' => __('Unpublish this page', 'extendify-local'),
383 - 'workflowId' => 'update-post-status',
384 - 'source' => 'plugin',
385 - 'available' => ['context' => ['postStatus' => 'publish', 'isFrontPage' => false]],
386 - ],
387 - [
388 - 'id' => 'change-site-title',
389 - 'icon' => 'edit',
390 - 'message' => __('Change website title', 'extendify-local'),
391 - 'workflowId' => 'edit-wp-setting',
392 - 'source' => 'plugin',
393 - 'available' => ['abilities' => ['canEditSettings']],
394 - ],
395 - [
396 - 'id' => 'change-theme-colors',
397 - 'icon' => 'styles',
398 - 'message' => __('Change website colors', 'extendify-local'),
399 - 'workflowId' => 'change-theme-variation',
400 - 'source' => 'plugin',
401 - 'available' => ['context' => ['hasThemeVariations']],
402 - ],
403 - [
404 - 'id' => 'change-theme-fonts',
405 - 'icon' => 'typography',
406 - 'message' => __('Change website fonts', 'extendify-local'),
407 - 'workflowId' => 'change-theme-fonts-variation',
408 - 'source' => 'plugin',
409 - 'available' => ['context' => ['hasThemeVariations']],
410 - ],
411 - [
412 - 'id' => 'change-site-style',
413 - 'icon' => 'styles',
414 - 'message' => __('Change website style', 'extendify-local'),
415 - 'workflowId' => 'change-site-vibes',
416 - 'source' => 'plugin',
417 - 'available' => ['abilities' => ['canEditThemes'], 'context' => ['isUsingVibes']],
418 - ],
419 - [
420 - 'id' => 'edit-text-on-this-page',
421 - 'icon' => 'edit',
422 - 'message' => __('Edit text on this page', 'extendify-local'),
423 - 'workflowId' => 'edit-post-strings',
424 - 'source' => 'plugin',
425 - 'available' => ['abilities' => ['canEditPost'], 'context' => ['postId', 'usingBlockEditor']],
426 - ],
427 - [
428 - 'id' => 'change-website-animation',
429 - 'icon' => 'swatch',
430 - 'message' => __('Change website animation', 'extendify-local'),
431 - 'workflowId' => 'change-animation',
432 - 'source' => 'plugin',
433 - 'available' => ['abilities' => ['canEditSettings']],
434 - ],
435 - [
436 - 'id' => 'change-website-logo',
437 - 'icon' => 'siteLogo',
438 - 'message' => __('Change website logo', 'extendify-local'),
439 - 'workflowId' => 'update-logo',
440 - 'source' => 'plugin',
441 - 'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
442 - ],
443 - [
444 - 'id' => 'change-website-browser-icon',
445 - 'icon' => 'siteLogo',
446 - 'message' => __('Change website browser icon', 'extendify-local'),
447 - 'workflowId' => 'update-site-icon',
448 - 'source' => 'plugin',
449 - 'available' => ['abilities' => ['canEditSettings', 'canUploadMedia']],
450 - ],
451 - ];
486 + $locale = \get_locale();
487 + $cached = \get_option('extendify_agent_suggestions_' . $locale);
488 +
489 + if (!is_array($cached) || !isset($cached['fetchedAt'])) {
490 + return $this->refreshSuggestions($locale) ?? [];
491 + }
492 +
493 + $age = time() - $cached['fetchedAt'];
494 + if ($age > DAY_IN_SECONDS) {
495 + if (!\wp_next_scheduled('extendify_agent_suggestions_refresh', [$locale])) {
496 + \wp_schedule_single_event(time(), 'extendify_agent_suggestions_refresh', [$locale]);
497 + if (\is_admin()) {
498 + \spawn_cron();
499 + }
500 + }
501 + }
502 +
503 + return $cached['data'] ?? [];
504 + }
505 +
506 + /**
507 + * Fetch suggestions from the API and persist them.
508 + * Called synchronously on cold start and via wp-cron when cache is stale.
509 + *
510 + * @param string $locale - Locale to fetch (cron may run in a different site locale).
511 + * @return array|null
512 + */
513 + public function refreshSuggestions($locale)
514 + {
515 + // When the refresh runs via wp-cron, the active locale may differ from the cached entry's locale.
516 + // Switch so HttpClient sends the matching wp_language and the response lands in the right cache key.
517 + $needSwitch = $locale !== \get_locale();
518 + if ($needSwitch) {
519 + \switch_to_locale($locale);
520 + }
521 +
522 + $response = HttpClient::post(
523 + Constants::AI_HOST . '/api/agent/suggestions',
524 + [],
525 + null,
526 + true
527 + );
528 +
529 + if ($needSwitch) {
530 + \restore_previous_locale();
531 + }
532 +
533 + $optionKey = 'extendify_agent_suggestions_' . $locale;
534 +
535 + if ($response['code'] !== 200) {
536 + // Back off: stamp the cache as fetched ~23h ago so we retry in ~1h instead of every request.
537 + $cached = \get_option($optionKey);
538 + \update_option(
539 + $optionKey,
540 + [
541 + 'data' => is_array($cached) ? ($cached['data'] ?? []) : [],
542 + 'fetchedAt' => time() - (DAY_IN_SECONDS - HOUR_IN_SECONDS),
543 + ],
544 + false
545 + );
546 + return null;
547 + }
548 +
549 + $suggestions = $response['response']['suggestions'] ?? [];
550 + \update_option(
551 + $optionKey,
552 + ['data' => $suggestions, 'fetchedAt' => time()],
553 + false
554 + );
555 + return $suggestions;
452 556 }
453 557
454 558 /**
455 559 * Check if the user is in the Gutenberg or FSE editor.