plugin = $plugin; $this->config = $plugin->config(); } /** * Settings API registration. */ private function render_subscription() { if ('development' === Entitlements::edition()) { return; } echo '
'; echo '

' . esc_html(Entitlements::can_use_pro() ? __('Embodied Assistant Pro', 'sentify-embodied-assistant') : __('Embodied Assistant Free', 'sentify-embodied-assistant')) . '

'; echo '

' . esc_html__('Pro adds shopping actions and an expanded avatar selection. AI-provider usage is billed separately by your provider.', 'sentify-embodied-assistant') . '

'; if ('free' === Entitlements::edition()) { $metadata = Entitlements::metadata(); $product_url = $metadata['product_url'] ?? 'https://embodied-assistant.store/'; echo '

' . esc_html__('Learn more about Pro', 'sentify-embodied-assistant') . '

'; if ('wporg' === ($metadata['distribution'] ?? '')) { echo '

' . esc_html__('Upgrades are available on our website. After purchasing, install the Pro plugin from your purchase account. Your settings are kept.', 'sentify-embodied-assistant') . '

'; echo '
'; return; } $sdk = Entitlements::sdk(); $url = $sdk && !$sdk->is_activation_mode() ? $sdk->get_upgrade_url() : (Entitlements::metadata()['upgrade_url'] ?? ''); if ($url) { echo '

' . esc_html__('Upgrade to Pro', 'sentify-embodied-assistant') . '

'; echo '

' . esc_html__('After purchasing, install the Pro plugin from your purchase account. Your settings are kept.', 'sentify-embodied-assistant') . '

'; } else { echo '

' . esc_html__('Preview build: the Pro upgrade link is not configured.', 'sentify-embodied-assistant') . '

'; } } echo ''; } /** Settings sections share one form and are linked from WordPress's sidebar. */ public static function sections() { return array( 'general' => __('General', 'sentify-embodied-assistant'), 'assistant' => __('Assistant', 'sentify-embodied-assistant'), 'ai-provider' => __('AI provider', 'sentify-embodied-assistant'), 'capabilities' => __('Capabilities', 'sentify-embodied-assistant'), 'widget' => __('Widget', 'sentify-embodied-assistant'), 'usage' => __('Usage', 'sentify-embodied-assistant'), ); } public static function page_slug($section) { return 'general' === $section ? self::PAGE : self::PAGE . '-' . $section; } public static function active_section() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only allowlisted navigation. $page = isset($_GET['page']) && is_string($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : self::PAGE; foreach (self::sections() as $section => $label) { if ('general' !== $section && self::page_slug($section) === $page) { return $section; } } // Keep bookmarked links from the former tab navigation working. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only allowlisted navigation. $tab = isset($_GET['tab']) && is_string($_GET['tab']) ? sanitize_key(wp_unslash($_GET['tab'])) : 'general'; return self::PAGE === $page && isset(self::sections()[$tab]) ? $tab : 'general'; } public static function highlight_submenu($submenu_file, $parent_file) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only menu highlighting. $page = isset($_GET['page']) && is_string($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; $pages = array_map(array(__CLASS__, 'page_slug'), array_keys(self::sections())); return self::PAGE === $parent_file && in_array($page, $pages, true) ? self::page_slug(self::active_section()) : $submenu_file; } public function register() { register_setting( self::GROUP, ConfigRepository::OPTION_KEY, array( 'type' => 'array', 'sanitize_callback' => array($this->config, 'sanitize'), 'default' => $this->config->defaults(), ) ); add_settings_section('embodied_assistant_general', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_agent', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_avatar', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_realtime', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_deployment', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_tools', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_limits', '', '__return_false', self::PAGE); add_settings_section('embodied_assistant_privacy', '', '__return_false', self::PAGE); add_action('wp_ajax_embodied_assistant_list_avatars', array($this, 'handle_list_avatars')); add_action('wp_ajax_embodied_assistant_import_avatar', array($this, 'handle_import_avatar')); } /** * Settings page HTML. */ public function render_page() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You do not have permission to manage this plugin.', 'sentify-embodied-assistant')); } $settings = $this->config->get(); $provider = $settings['realtime']['provider'] ?? 'openai'; $credential_status = $this->plugin->realtime_credentials()->readiness($provider); $tabs = self::sections(); $active_tab = self::active_section(); ?>

render(); ?>

> render_subscription(); } ?> render_general($settings); ?> render_limits($settings); ?>
> render_agent($settings); ?> render_avatar($settings); ?>
> render_realtime($settings); ?>
> render_capabilities($settings); ?> render_tools($settings); ?>
> render_deployment($settings); ?>
render_secret($credential_status); ?>
$profile) : ?>
>

__('Public support email', 'sentify-embodied-assistant'), 'phone' => __('Public support phone', 'sentify-embodied-assistant'), 'hours' => __('Support hours', 'sentify-embodied-assistant')) as $field => $label) { ?>
avatar_catalog_config(); ?>

__('Permission denied.', 'sentify-embodied-assistant')), 403); } check_ajax_referer('embodied_assistant_avatar_catalog', 'nonce'); require_once __DIR__ . '/class-avatar-importer.php'; $url = isset($_POST['url']) && is_string($_POST['url']) ? esc_url_raw(wp_unslash($_POST['url'])) : ''; if (!Entitlements::allows_avatar($url)) { wp_send_json_error(array('message' => __('This avatar is not included in your current selection. Choose a Free avatar or activate Pro.', 'sentify-embodied-assistant')), 403); return; } $result = (new AvatarImporter())->import($url); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message()), 400); } if (!$this->config->save_avatar_selection($result)) { wp_send_json_error(array('message' => __('The avatar was downloaded but the selection could not be saved. Please try again.', 'sentify-embodied-assistant')), 500); } wp_send_json_success($result); } /** AJAX: return the catalog for the configured Azure Blob containers. */ public function handle_list_avatars() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('Permission denied.', 'sentify-embodied-assistant')), 403); } check_ajax_referer('embodied_assistant_avatar_catalog', 'nonce'); $requested = isset($_GET['container']) ? sanitize_key(wp_unslash($_GET['container'])) : ''; $containers = $this->avatar_catalog_config(); $selected = null; foreach ($containers as $container) { if ($requested === $container['container']) { $selected = $container; break; } } if (!$selected) { wp_send_json_error(array('message' => __('Unknown avatar container.', 'sentify-embodied-assistant')), 400); } if (!Entitlements::can_use_pro()) { $avatars = array_values(array_filter(Entitlements::free_avatars(), static function ($avatar) use ($requested) { return $requested === $avatar['container']; })); wp_send_json_success(array('avatars' => $avatars, 'container' => $requested)); return; } } /** * @return array[] */ private function avatar_catalog_config() { if (!Entitlements::can_use_pro()) { $containers = array(); foreach (Entitlements::free_avatars() as $avatar) { $label = 'rpm' === $avatar['container'] ? __('Cartoon', 'sentify-embodied-assistant') : __('Realistic', 'sentify-embodied-assistant'); $containers[$avatar['container']] = array('label' => $label, 'container' => $avatar['container'], 'base_url' => 'https://sentifydavatars.blob.core.windows.net'); } return array_values($containers); } return array(); } /** * @param string $xml * @param array $container * @return array[] */ /** * @param array $container * @param string $blob_name * @return string */ /** * @param array $s */ private function render_realtime(array $s) { ?> $profile) : ?> array(__('Primary', 'sentify-embodied-assistant'), '#ffffff'), 'secondary_color' => array(__('Secondary', 'sentify-embodied-assistant'), '#1862eb'), 'secondary_color_hover' => array(__('Secondary hover', 'sentify-embodied-assistant'), '#1453c8'), 'background_color' => array(__('Surface', 'sentify-embodied-assistant'), '#ffffff'), 'text_color_primary_bg' => array(__('Text on primary', 'sentify-embodied-assistant'), '#09173b'), 'text_color_secondary_bg' => array(__('Text on secondary', 'sentify-embodied-assistant'), '#ffffff'), 'text_color_muted' => array(__('Muted text', 'sentify-embodied-assistant'), '#596780'), ); ?>

__('Get product brands', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-tags' => __('Get product tags', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-attributes' => __('Get product attributes', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-product-reviews' => __('Get product reviews', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-related-products' => __('Get related products', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-highlights' => __('Get store highlights', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-get-cart' => __('Get cart', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-add-to-cart' => __('Add to cart', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-update-cart' => __('Update cart item', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-remove-from-cart' => __('Remove from cart', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-apply-coupon' => __('Apply coupon', 'sentify-embodied-assistant'), 'embodied-assistant/woocommerce-remove-coupon' => __('Remove coupon', 'sentify-embodied-assistant'), ); } private function render_pro_hint($message) { if (Entitlements::can_use_pro()) { return; } $metadata = Entitlements::metadata(); $sdk = Entitlements::sdk(); $url = 'wporg' === ($metadata['distribution'] ?? '') ? ($metadata['product_url'] ?? '') : ($sdk && !$sdk->is_activation_mode() ? $sdk->get_upgrade_url() : ($metadata['upgrade_url'] ?? '')); echo '

' . esc_html($message); if ($url) { echo ' ' . esc_html__('Explore Pro', 'sentify-embodied-assistant') . ''; } echo '

'; } private function render_tools(array $s) { $tool_help = array( 'avatar-set-emotion' => __('Let the avatar express emotions during a conversation.', 'sentify-embodied-assistant'), 'avatar-set-view' => __('Adjust how much of the avatar is visible.', 'sentify-embodied-assistant'), 'avatar-play-animation' => __('Enable optional animation clips and allow visitors’ browsers to download them directly from Sentifyd Azure storage. Requests include IP address and browser metadata. Clips are not copied to your site. Voice, lipsync and built-in gestures work without this tool.', 'sentify-embodied-assistant'), 'avatar-play-gesture' => __('Let the avatar use gestures during a conversation.', 'sentify-embodied-assistant'), 'avatar-look-at' => __('Control where the avatar looks and when it makes eye contact.', 'sentify-embodied-assistant'), 'wordpress-search-posts' => __('Find published posts and pages on your site.', 'sentify-embodied-assistant'), 'wordpress-get-post' => __('Read a published post or page to answer questions.', 'sentify-embodied-assistant'), 'wordpress-get-menu' => __('Use your site menu to help visitors find their way.', 'sentify-embodied-assistant'), 'wordpress-get-support-options' => __('Share configured support contacts and find the contact page.', 'sentify-embodied-assistant'), 'wordpress-get-store-info' => __('Share store details such as contact information, opening hours, and address.', 'sentify-embodied-assistant'), 'wordpress-navigate' => __('Take visitors to a page, their cart, or checkout.', 'sentify-embodied-assistant'), 'wordpress-list-pages' => __('Find the pages available on your site.', 'sentify-embodied-assistant'), 'woocommerce-search-products' => __('Find products by name, category, or keyword.', 'sentify-embodied-assistant'), 'woocommerce-get-brands' => __('Help visitors browse product brands.', 'sentify-embodied-assistant'), 'woocommerce-get-tags' => __('Find product tags to help visitors explore your catalog.', 'sentify-embodied-assistant'), 'woocommerce-get-attributes' => __('Help visitors explore product options such as size and color.', 'sentify-embodied-assistant'), 'woocommerce-get-product' => __('Answer questions about product details, prices, options, and stock.', 'sentify-embodied-assistant'), 'woocommerce-get-categories' => __('Help visitors browse product categories.', 'sentify-embodied-assistant'), 'woocommerce-get-product-reviews' => __('Share published customer reviews for a product.', 'sentify-embodied-assistant'), 'woocommerce-get-related-products' => __('Suggest similar products from your catalog.', 'sentify-embodied-assistant'), 'woocommerce-get-highlights' => __('Find featured products, sale items, and best sellers.', 'sentify-embodied-assistant'), 'woocommerce-get-cart' => __('Check the visitor\'s shopping cart and totals.', 'sentify-embodied-assistant'), 'woocommerce-add-to-cart' => __('Add products to the visitor\'s shopping cart.', 'sentify-embodied-assistant'), 'woocommerce-update-cart' => __('Change the quantity of an item in the shopping cart.', 'sentify-embodied-assistant'), 'woocommerce-remove-from-cart' => __('Remove items from the visitor\'s shopping cart.', 'sentify-embodied-assistant'), 'woocommerce-apply-coupon' => __('Apply a discount code to the visitor\'s shopping cart.', 'sentify-embodied-assistant'), 'woocommerce-remove-coupon' => __('Remove a discount code from the visitor\'s shopping cart.', 'sentify-embodied-assistant'), ); $registry = $this->plugin->tool_registry(); try { $declarations = $registry ? $registry->catalog() : array(); $catalog = $registry ? $registry->runtime_definitions() : array(); } catch (\Throwable $e) { $declarations = array(); $catalog = array(); } $previews = $this->pro_tool_previews(); $can_confirm_cart = false; foreach ($catalog as $def) { if ('woocommerce' === ($def['availability'] ?? '') && !empty($def['confirm_mutation']) && class_exists('WooCommerce')) { $can_confirm_cart = true; break; } } // Preserve contact details even when the native support Ability is unavailable. if (!isset($catalog['embodied-assistant/wordpress-get-support-options'])) { foreach ($s['support'] as $field => $value) { printf('', esc_attr($field), esc_attr($value)); } } if (empty($declarations)) { echo '

' . esc_html__('Tool catalog missing (build assets not installed). Install the release package.', 'sentify-embodied-assistant') . '

'; } $enabled = array_flip((array) $s['tools']['enabled']); $unknown = array_diff(array_keys($enabled), array_keys($declarations)); $unavailable = array_diff(array_keys($enabled), array_keys($catalog), $unknown); foreach (array_merge($unavailable, $unknown) as $name) { printf('', esc_attr($name)); } if ($unavailable || !class_exists('WooCommerce')) { echo '

' . esc_html__('Some catalog operations are unavailable because their required service or native Ability is missing. Saved selections are retained; activate WooCommerce or the appropriate Pro license to use them.', 'sentify-embodied-assistant') . '

'; } if (array_diff($unknown, array_keys($previews))) { echo '

' . esc_html__('Some saved tools are unavailable in this edition. Their selections are retained but they cannot run.', 'sentify-embodied-assistant') . '

'; } $groups = array( 'avatar' => array('label' => __('Avatar controls', 'sentify-embodied-assistant'), 'tools' => array()), 'woocommerce' => array('label' => __('WooCommerce shop assistant', 'sentify-embodied-assistant'), 'tools' => array()), 'wordpress' => array('label' => __('WordPress tools', 'sentify-embodied-assistant'), 'tools' => array()), ); // Missing dependencies stay visible, while runtime discovery remains authoritative. $display_catalog = $declarations; if (!Entitlements::can_use_pro()) { foreach ($previews as $name => $label) { if (!isset($display_catalog[$name])) { $display_catalog[$name] = array('label' => $label, 'availability' => 'woocommerce'); } } } foreach ($display_catalog as $name => $def) { $group = $def['availability'] ?? 'wordpress'; if (!isset($groups[$group])) { $group = 'wordpress'; } $groups[$group]['tools'][$name] = $def; } ?>

$group) : ?>