| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* Admin. |
| 5 |
*/ |
| 6 |
|
| 7 |
namespace Extendify\Agent; |
| 8 |
|
| 9 |
defined('ABSPATH') || die('No direct access.'); |
| 10 |
|
| 11 |
use Extendify\Agent\Controllers\ChatHistoryController; |
| 12 |
use Extendify\Agent\Controllers\TourController; |
| 13 |
use Extendify\Agent\Controllers\WorkflowHistoryController; |
| 14 |
use Extendify\Config; |
| 15 |
use Extendify\Shared\Services\Escaper; |
| 16 |
use Extendify\Agent\TagBlocks; |
| 17 |
use Extendify\Agent\TagTemplateParts; |
| 18 |
use Extendify\Agent\Controllers\SiteNavigationController; |
| 19 |
use Extendify\Shared\DataProvider\ProductsData; |
| 20 |
|
| 21 |
/** |
| 22 |
* This class handles any file loading for the admin area. |
| 23 |
*/ |
| 24 |
class Admin |
| 25 |
{ |
| 26 |
/** |
| 27 |
* Adds various actions to set up the page |
| 28 |
* |
| 29 |
* @return void |
| 30 |
*/ |
| 31 |
public function __construct() |
| 32 |
{ |
| 33 |
\add_action('admin_enqueue_scripts', [$this, 'loadScriptsAndStyles']); |
| 34 |
\add_action('wp_enqueue_scripts', [$this, 'loadScriptsAndStyles']); |
| 35 |
ChatHistoryController::init(); |
| 36 |
WorkflowHistoryController::init(); |
| 37 |
|
| 38 |
// Tag blocks so we can identify them later |
| 39 |
TagBlocks::init(); |
| 40 |
TagTemplateParts::init(); |
| 41 |
|
| 42 |
// Add the site navigation ids to the navigation blocks |
| 43 |
SiteNavigationController::init(); |
| 44 |
} |
| 45 |
|
| 46 |
/** |
| 47 |
* Adds various JS scripts and styles |
| 48 |
* |
| 49 |
* @return void |
| 50 |
*/ |
| 51 |
public function loadScriptsAndStyles() |
| 52 |
{ |
| 53 |
$version = constant('EXTENDIFY_DEVMODE') ? uniqid() : Config::$version; |
| 54 |
$scriptAssetPath = EXTENDIFY_PATH . 'public/build/' . Config::$assetManifest['extendify-agent.php']; |
| 55 |
$fallback = [ |
| 56 |
'dependencies' => [], |
| 57 |
'version' => $version, |
| 58 |
]; |
| 59 |
$scriptAsset = file_exists($scriptAssetPath) ? require $scriptAssetPath : $fallback; |
| 60 |
|
| 61 |
foreach ($scriptAsset['dependencies'] as $style) { |
| 62 |
\wp_enqueue_style($style); |
| 63 |
} |
| 64 |
|
| 65 |
\wp_enqueue_script( |
| 66 |
Config::$slug . '-agent-scripts', |
| 67 |
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.js'], |
| 68 |
array_merge([Config::$slug . '-shared-scripts'], $scriptAsset['dependencies']), |
| 69 |
$scriptAsset['version'], |
| 70 |
true |
| 71 |
); |
| 72 |
|
| 73 |
$context = [ |
| 74 |
'adminPage' => function_exists('get_current_screen') && ($screen = get_current_screen()) |
| 75 |
? \esc_attr($screen->id) |
| 76 |
: null, |
| 77 |
'postId' => (int) $this->getCurrentPostId(), |
| 78 |
'postTitle' => \esc_attr(\get_the_title($this->getCurrentPostId())), |
| 79 |
'postType' => \esc_attr(\get_post_type($this->getCurrentPostId())), |
| 80 |
'postUrl' => \esc_url(\get_permalink($this->getCurrentPostId())), |
| 81 |
'isFrontPage' => (bool) \is_front_page(), |
| 82 |
'postStatus' => \esc_attr(\get_post_status((int) $this->getCurrentPostId())), |
| 83 |
'isBlogPage' => (bool) \is_home(), |
| 84 |
'themeSlug' => \esc_attr(\wp_get_theme()->get_stylesheet()), |
| 85 |
'hasThemeVariations' => (bool) $this->hasThemeVariations(), |
| 86 |
'isBlockTheme' => function_exists('wp_is_block_theme') ? (bool) wp_is_block_theme() : false, |
| 87 |
'wordPressVersion' => \esc_attr(\get_bloginfo('version')), |
| 88 |
'usingBlockEditor' => function_exists('use_block_editor_for_post') ? |
| 89 |
(bool) use_block_editor_for_post($this->getCurrentPostId()) : |
| 90 |
false, |
| 91 |
'activePlugins' => array_values(\get_option('active_plugins', [])), |
| 92 |
]; |
| 93 |
$recommendations = ProductsData::get() ?? []; |
| 94 |
$pluginRecommendations = array_filter($recommendations, function ($item) { |
| 95 |
return in_array('ai-agent', $item['slots'] ?? [], true) && $item['ctaType'] === 'plugin'; |
| 96 |
}); |
| 97 |
$mappedPluginRecommendations = array_values(array_map(function ($item) { |
| 98 |
return [ |
| 99 |
'title' => $item['title'] ?? '', |
| 100 |
'slug' => $item['ctaPluginSlug'] ?? $item['slug'] ?? '', |
| 101 |
'description' => $item['aiDescription'] ?? $item['description'] ?? '', |
| 102 |
]; |
| 103 |
}, $pluginRecommendations)); |
| 104 |
$agentContext = [ |
| 105 |
'availableAdminPages' => get_option('_transient_extendify_admin_pages_menu', []), |
| 106 |
'pluginRecommendations' => $mappedPluginRecommendations, |
| 107 |
]; |
| 108 |
$abilities = [ |
| 109 |
'canEditPost' => (bool) \current_user_can('edit_post', \get_queried_object_id()), |
| 110 |
// TODO: this may be true for a user, while they still can't edit every post |
| 111 |
// So we would need to clarify this in the instructions, and |
| 112 |
// include a step that fetches the page they want to edit |
| 113 |
'canEditPosts' => (bool) \current_user_can('edit_posts'), |
| 114 |
'canEditThemes' => (bool) \current_user_can('edit_theme_options'), |
| 115 |
'canActivatePlugins' => (bool) \current_user_can('activate_plugins'), |
| 116 |
'canInstallPlugins' => (bool) \current_user_can('install_plugins'), |
| 117 |
'canEditUsers' => (bool) \current_user_can('edit_users'), |
| 118 |
'canEditSettings' => (bool) \current_user_can('manage_options'), |
| 119 |
'canUploadMedia' => (bool) \current_user_can('upload_files'), |
| 120 |
]; |
| 121 |
|
| 122 |
\wp_add_inline_script( |
| 123 |
Config::$slug . '-agent-scripts', |
| 124 |
'window.extAgentData = ' . \wp_json_encode([ |
| 125 |
// Add context about where they are |
| 126 |
'context' => $context, |
| 127 |
// Context that the Agent might need when returning a response, |
| 128 |
// but not for handling the workflow. |
| 129 |
'agentContext' => $agentContext, |
| 130 |
// List of abilities the AI can perform for this user. |
| 131 |
// For example, we could check whether their theme has variations. |
| 132 |
'abilities' => $abilities, |
| 133 |
// List of suggestions the AI can make for this user. |
| 134 |
// For example, we could check whether they need to set up a specific plugin. |
| 135 |
'suggestions' => $this->getSuggestions($context, $abilities), |
| 136 |
'chatHistory' => Escaper::recursiveEscAttr(ChatHistoryController::getChatHistory()), |
| 137 |
'workflowHistory' => Escaper::recursiveEscAttr(WorkflowHistoryController::getWorkflowHistory()), |
| 138 |
'userData' => [ |
| 139 |
'tourData' => \wp_json_encode(TourController::get()->get_data()), |
| 140 |
], |
| 141 |
]), |
| 142 |
'before' |
| 143 |
); |
| 144 |
|
| 145 |
\wp_set_script_translations( |
| 146 |
Config::$slug . '-agent-scripts', |
| 147 |
'extendify-local', |
| 148 |
EXTENDIFY_PATH . 'languages/js' |
| 149 |
); |
| 150 |
|
| 151 |
\wp_enqueue_style( |
| 152 |
Config::$slug . '-agent-styles', |
| 153 |
EXTENDIFY_BASE_URL . 'public/build/' . Config::$assetManifest['extendify-agent.css'], |
| 154 |
[], |
| 155 |
Config::$version, |
| 156 |
'all' |
| 157 |
); |
| 158 |
} |
| 159 |
/** |
| 160 |
* Get the current post ID based on the context. |
| 161 |
* |
| 162 |
* @return int |
| 163 |
*/ |
| 164 |
private function getCurrentPostId() |
| 165 |
{ |
| 166 |
if (is_admin() && function_exists('get_current_screen')) { |
| 167 |
$screen = get_current_screen(); |
| 168 |
if ($screen && $screen->base === 'post') { |
| 169 |
global $post; |
| 170 |
if ($post) { |
| 171 |
return (int) $post->ID; |
| 172 |
} |
| 173 |
} |
| 174 |
} |
| 175 |
if (\is_front_page()) { |
| 176 |
return (\get_option('show_on_front') === 'page') ? (int) \get_option('page_on_front') : 0; |
| 177 |
} |
| 178 |
if (\is_home()) { |
| 179 |
return (int) \get_option('page_for_posts'); |
| 180 |
} |
| 181 |
return (int) \get_queried_object_id(); |
| 182 |
} |
| 183 |
|
| 184 |
/** |
| 185 |
* Scan the style dirs to locate if they have variations. |
| 186 |
* Ported from here: |
| 187 |
* https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/class-wp-theme-json-resolver.php#L810 |
| 188 |
* |
| 189 |
* @return bool |
| 190 |
*/ |
| 191 |
private function hasThemeVariations() |
| 192 |
{ |
| 193 |
$base_directory = get_stylesheet_directory() . '/styles'; |
| 194 |
$template_directory = get_template_directory() . '/styles'; |
| 195 |
|
| 196 |
if (is_dir($base_directory) && glob($base_directory . '/*.json', GLOB_NOSORT)) { |
| 197 |
return true; |
| 198 |
} |
| 199 |
|
| 200 |
// Only check parent if it's different from child |
| 201 |
if ( |
| 202 |
$template_directory !== $base_directory && |
| 203 |
is_dir($template_directory) && |
| 204 |
glob($template_directory . '/*.json', GLOB_NOSORT) |
| 205 |
) { |
| 206 |
return true; |
| 207 |
} |
| 208 |
|
| 209 |
return false; |
| 210 |
} |
| 211 |
|
| 212 |
/** |
| 213 |
* Get suggestions for the user. |
| 214 |
* |
| 215 |
* @param array $context - The context of the current page and site. |
| 216 |
* @param array $abilities - The abilities of the user. |
| 217 |
* @return array |
| 218 |
*/ |
| 219 |
private function getSuggestions($context, $abilities) |
| 220 |
{ |
| 221 |
$suggestions = [ |
| 222 |
[ |
| 223 |
'icon' => 'video', |
| 224 |
'message' => __('What tours are available?', 'extendify-local'), |
| 225 |
'workflowId' => 'list-tours', |
| 226 |
] |
| 227 |
]; |
| 228 |
|
| 229 |
if ($context['postStatus']) { |
| 230 |
$suggestions [] = [ |
| 231 |
'icon' => ($context['postStatus'] === 'draft') ? 'published' : 'drafts', |
| 232 |
'message' => ($context['postStatus'] === 'draft') |
| 233 |
? __('Publish this page', 'extendify-local') |
| 234 |
: __('Unpublish this page', 'extendify-local'), |
| 235 |
'workflowId' => 'update-post-status', |
| 236 |
]; |
| 237 |
} |
| 238 |
|
| 239 |
if ($abilities['canEditSettings']) { |
| 240 |
$suggestions[] = [ |
| 241 |
'icon' => 'edit', |
| 242 |
'message' => __('I want to change my site title', 'extendify-local'), |
| 243 |
"feature" => true, |
| 244 |
'workflowId' => 'edit-wp-setting', |
| 245 |
]; |
| 246 |
|
| 247 |
$suggestions[] = [ |
| 248 |
'icon' => 'typography', |
| 249 |
'message' => __('I want to change my theme fonts', 'extendify-local'), |
| 250 |
"feature" => true, |
| 251 |
'workflowId' => 'change-theme-fonts-variation', |
| 252 |
]; |
| 253 |
} |
| 254 |
|
| 255 |
// If they have theme variations, suggest they can change the theme styling. |
| 256 |
if ($context['hasThemeVariations']) { |
| 257 |
$suggestions[] = [ |
| 258 |
'icon' => 'styles', |
| 259 |
'message' => __('I want to change my theme styling', 'extendify-local'), |
| 260 |
"feature" => true, |
| 261 |
'workflowId' => 'change-theme-variation', |
| 262 |
]; |
| 263 |
} |
| 264 |
|
| 265 |
if ($context['postId'] && $abilities['canEditPost'] && $context['usingBlockEditor']) { |
| 266 |
$suggestions[] = [ |
| 267 |
'icon' => 'edit', |
| 268 |
'message' => __('Edit text on this page', 'extendify-local'), |
| 269 |
"feature" => true, |
| 270 |
'workflowId' => 'edit-post-strings', |
| 271 |
]; |
| 272 |
} |
| 273 |
|
| 274 |
if ($abilities['canEditPost']) { |
| 275 |
$suggestions[] = [ |
| 276 |
'icon' => 'help', |
| 277 |
'message' => __('How can I create a post?', 'extendify-local'), |
| 278 |
'workflowId' => 'answer-general-questions', |
| 279 |
]; |
| 280 |
$suggestions[] = [ |
| 281 |
'icon' => 'help', |
| 282 |
'message' => __('How can I create a page?', 'extendify-local'), |
| 283 |
'workflowId' => 'create-page', |
| 284 |
]; |
| 285 |
} |
| 286 |
|
| 287 |
if ($abilities['canActivatePlugins']) { |
| 288 |
$suggestions[] = [ |
| 289 |
'icon' => 'help', |
| 290 |
'message' => __('How can I install a plugin?', 'extendify-local'), |
| 291 |
'workflowId' => 'recommend-plugins', |
| 292 |
]; |
| 293 |
} |
| 294 |
|
| 295 |
if ($abilities['canEditThemes']) { |
| 296 |
$suggestions[] = [ |
| 297 |
'icon' => 'help', |
| 298 |
'message' => __('How can I change my theme?', 'extendify-local'), |
| 299 |
'workflowId' => 'answer-general-questions', |
| 300 |
]; |
| 301 |
$suggestions[] = [ |
| 302 |
'icon' => 'help', |
| 303 |
'message' => __('How can I change the site footer?', 'extendify-local'), |
| 304 |
'workflowId' => 'answer-general-questions', |
| 305 |
]; |
| 306 |
$suggestions[] = [ |
| 307 |
'icon' => 'help', |
| 308 |
'message' => __('How can I change the site header?', 'extendify-local'), |
| 309 |
'workflowId' => 'answer-general-questions', |
| 310 |
]; |
| 311 |
} |
| 312 |
|
| 313 |
if ($abilities['canUploadMedia']) { |
| 314 |
$suggestions[] = [ |
| 315 |
'icon' => 'help', |
| 316 |
'message' => __('How can I upload an image?', 'extendify-local'), |
| 317 |
'workflowId' => 'answer-general-questions', |
| 318 |
]; |
| 319 |
$suggestions[] = [ |
| 320 |
'icon' => 'help', |
| 321 |
'message' => __('How can I change the site icon?', 'extendify-local'), |
| 322 |
'workflowId' => 'answer-general-questions', |
| 323 |
]; |
| 324 |
} |
| 325 |
|
| 326 |
if ($abilities['canEditSettings']) { |
| 327 |
$suggestions[] = [ |
| 328 |
'icon' => 'help', |
| 329 |
'message' => __('How can I change my site title?', 'extendify-local'), |
| 330 |
'workflowId' => 'edit-wp-setting', |
| 331 |
]; |
| 332 |
$suggestions[] = [ |
| 333 |
'icon' => 'help', |
| 334 |
'message' => __('How can I change my site tagline?', 'extendify-local'), |
| 335 |
'workflowId' => 'edit-wp-setting', |
| 336 |
]; |
| 337 |
$suggestions[] = [ |
| 338 |
'icon' => 'help', |
| 339 |
'message' => __('How can I change my site language?', 'extendify-local'), |
| 340 |
'workflowId' => 'answer-general-questions', |
| 341 |
]; |
| 342 |
} |
| 343 |
|
| 344 |
shuffle($suggestions); |
| 345 |
return $suggestions; |
| 346 |
} |
| 347 |
} |
| 348 |
|