[ 'label' => 'Midnight', 'base' => '#0f1117', 'accent' => '#6366f1' ], 'monoDark' => [ 'label' => 'Mono Dark', 'base' => '#09090b', 'accent' => '#fafafa' ], 'forest' => [ 'label' => 'Forest', 'base' => '#1b362c', 'accent' => '#22c55e' ], 'cherry' => [ 'label' => 'Cherry', 'base' => '#ffffff', 'accent' => '#ef4444' ], 'cherryDark' => [ 'label' => 'Cherry Dark', 'base' => '#440b19', 'accent' => '#ef4444' ], 'slate' => [ 'label' => 'Slate', 'base' => '#334155', 'accent' => '#38bdf8' ], 'amber' => [ 'label' => 'Amber', 'base' => '#3d2c15', 'accent' => '#f59e0b' ], 'sunrise' => [ 'label' => 'Sunrise', 'base' => '#ffffff', 'accent' => '#f97316' ], 'ivory' => [ 'label' => 'Ivory', 'base' => '#f1ede5', 'accent' => '#e08229' ], 'sky' => [ 'label' => 'Sky', 'base' => '#ffffff', 'accent' => '#5ba1da' ], 'lavender' => [ 'label' => 'Lavender', 'base' => '#faf5ff', 'accent' => '#8b5cf6' ], 'nice' => [ 'label' => 'Nice', 'base' => '#ffffff', 'accent' => '#3f7e83' ], 'navy' => [ 'label' => 'Navy', 'base' => '#ebf3f2', 'accent' => '#bed6dd' ], 'graphite' => [ 'label' => 'Graphite', 'base' => '#313949', 'accent' => '#4185eb' ], 'stone' => [ 'label' => 'Stone', 'base' => '#44403c', 'accent' => '#fbbf24' ], 'olive' => [ 'label' => 'Olive', 'base' => '#2e3220', 'accent' => '#a3b14b' ], 'mint' => [ 'label' => 'Mint', 'base' => '#f4fbf7', 'accent' => '#0f9d63' ], 'rose' => [ 'label' => 'Rose', 'base' => '#fff7f9', 'accent' => '#db2777' ], 'linen' => [ 'label' => 'Linen', 'base' => '#fcfbf8', 'accent' => '#d6e6e0' ], 'mono' => [ 'label' => 'Mono', 'base' => '#ffffff', 'accent' => '#18181b' ], ]; } // ─── Defaults ──────────────────────────────────────────────────────────────── function elia_assistant_defaults(): array { return [ 'widget_id' => '', 'enabled' => '1', 'show_floating' => '1', 'language_override_enabled' => '0', 'language_override' => '', 'is_test' => '0', ]; } // ─── Sanitize ───────────────────────────────────────────────────────────────── function elia_assistant_sanitize( $raw ): array { $out = []; // options.php has already verified the settings nonce before it calls this sanitize callback. // phpcs:ignore WordPress.Security.NonceVerification.Missing $disconnect = ! empty( $_POST['ec_disconnect'] ); $widget_id = strtolower( trim( sanitize_text_field( $raw['widget_id'] ?? '' ) ) ); if ( $disconnect ) { $widget_id = ''; } elseif ( $widget_id !== '' && ! elia_assistant_is_widget_id( $widget_id ) ) { add_settings_error( ELIA_ASSISTANT_OPTION, 'elia_assistant_widget_id', __( 'This is not a widget ID. Copy it again from the WordPress card in the ELIA Assistant dashboard.', 'elia-assistant' ) ); // Keep the ID that worked rather than replacing it with a typo. $widget_id = elia_assistant_widget_id( (array) get_option( ELIA_ASSISTANT_OPTION, [] ) ); } $out['widget_id'] = $widget_id; $out['enabled'] = empty( $raw['enabled'] ) ? '0' : '1'; $out['show_floating'] = empty( $raw['show_floating'] ) ? '0' : '1'; $language = sanitize_key( $raw['language_override'] ?? '' ); $out['language_override'] = array_key_exists( $language, elia_assistant_languages() ) ? $language : ''; $out['language_override_enabled'] = ! empty( $raw['language_override_enabled'] ) && $out['language_override'] !== '' ? '1' : '0'; $out['is_test'] = empty( $raw['is_test'] ) ? '0' : '1'; return $out; } // ─── Widget id ──────────────────────────────────────────────────────────────── /** * The widget id is the id of the agent whose chat widget the site shows, copied from the WordPress * card in the dashboard. It is public: the dashboard's embed snippet carries the same value. */ function elia_assistant_is_widget_id( string $value ): bool { return (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value ); } function elia_assistant_widget_id( array $opts ): string { $widget_id = isset( $opts['widget_id'] ) && is_string( $opts['widget_id'] ) ? $opts['widget_id'] : ''; return elia_assistant_is_widget_id( $widget_id ) ? $widget_id : ''; } // ─── Language detection ─────────────────────────────────────────────────────── /** * Returns the 2-letter code of the language the current page is in. * Asks the language plugin first: WPML (whose wpml_current_language filter Polylang answers too), * Polylang, then Weglot, which translates the page without switching the WordPress locale. Then the * locale, which TranslatePress and the others switch per page. Codes like pt-br or zh_CN are cut * to their language part. A site with another language plugin can say which language the page is * in through the elia_assistant_language filter. */ function elia_assistant_current_language(): string { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML's own filter, read here, not defined by this plugin. $candidates = [ apply_filters( 'wpml_current_language', null ) ]; if ( function_exists( 'pll_current_language' ) ) { $candidates[] = pll_current_language( 'slug' ); } if ( function_exists( 'weglot_get_current_language' ) ) { $candidates[] = weglot_get_current_language(); } $candidates[] = get_locale(); $lang = 'en'; foreach ( $candidates as $candidate ) { // WPML answers "all" in the admin when every language is listed; that is not a language. $code = is_string( $candidate ) ? strtolower( (string) preg_split( '/[-_]/', $candidate )[0] ) : ''; if ( preg_match( '/^[a-z]{2}$/', $code ) ) { $lang = $code; break; } } $filtered = apply_filters( 'elia_assistant_language', $lang ); return is_string( $filtered ) && preg_match( '/^[a-z]{2}$/', $filtered ) ? $filtered : $lang; } function elia_assistant_resolve_language( array $opts ): string { if ( ( $opts['language_override_enabled'] ?? '0' ) === '1' && ! empty( $opts['language_override'] ) ) { return $opts['language_override']; } return elia_assistant_current_language(); } // ─── WooCommerce detection and read-only key ───────────────────────────────── /** * Returns WooCommerce store info when the plugin is active, or null. * class_exists() is enough - by the time admin_menu runs, an active WooCommerce * has already loaded. */ function elia_assistant_woocommerce_info(): ?array { if ( ! class_exists( 'WooCommerce' ) ) { return null; } return [ 'version' => defined( 'WC_VERSION' ) ? WC_VERSION : '', // The value to enter as "Store URL" in ELIA: the MCP server appends // /wp-json/wc/v3/, which is where WordPress mounts REST routes. 'store_url' => home_url(), ]; } function elia_assistant_wc_table(): string { global $wpdb; return $wpdb->prefix . 'woocommerce_api_keys'; } function elia_assistant_wc_transient_key(): string { return 'elia_assistant_wc_new_key_' . get_current_user_id(); } /** * Returns the stored key metadata, but only if the key still exists in WooCommerce. * Someone may have deleted it from WooCommerce → Settings → Advanced → REST API. */ function elia_assistant_wc_stored_key(): ?array { $stored = get_option( ELIA_ASSISTANT_WC_OPTION ); if ( ! is_array( $stored ) || empty( $stored['key_id'] ) ) { return null; } global $wpdb; $table = elia_assistant_wc_table(); // The table name is $wpdb->prefix plus a constant, so it is safe to interpolate. The lookup must // hit the live table (the key may have been deleted in WooCommerce), so it is not cached. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter $row = $wpdb->get_row( $wpdb->prepare( "SELECT truncated_key, permissions FROM {$table} WHERE key_id = %d", (int) $stored['key_id'] ), ARRAY_A ); // phpcs:enable return $row ? array_merge( $stored, $row ) : null; } function elia_assistant_wc_redirect( string $notice ): void { // Back to the WooCommerce block, not the top of the page: the new key or the notice is there. wp_safe_redirect( add_query_arg( [ 'page' => 'elia-assistant', 'ec_wc' => $notice ], admin_url( 'options-general.php' ) ) . '#elia-wc-title' ); exit; } /** * Creates a read-only WooCommerce REST key, exactly as the WooCommerce admin screen does. * WooCommerce exposes no public API for this, hence the direct insert. * The consumer key is stored one-way hashed, so the full value only exists here - it is * handed to the transient for a single display and never persisted by this plugin. */ function elia_assistant_wc_generate(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'elia-assistant' ) ); } check_admin_referer( 'elia_assistant_wc_generate' ); if ( ! class_exists( 'WooCommerce' ) || ! function_exists( 'wc_rand_hash' ) || ! function_exists( 'wc_api_hash' ) ) { elia_assistant_wc_redirect( 'unavailable' ); } global $wpdb; $table = elia_assistant_wc_table(); // Regenerating replaces the old key rather than leaving it live. $previous = get_option( ELIA_ASSISTANT_WC_OPTION ); // WooCommerce has no public API for creating or deleting REST keys, so this mirrors what its admin screen does. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( is_array( $previous ) && ! empty( $previous['key_id'] ) ) { $wpdb->delete( $table, [ 'key_id' => (int) $previous['key_id'] ], [ '%d' ] ); } $consumer_key = 'ck_' . wc_rand_hash(); $consumer_secret = 'cs_' . wc_rand_hash(); $inserted = $wpdb->insert( $table, [ 'user_id' => get_current_user_id(), 'description' => 'ELIA Assistant', 'permissions' => 'read', 'consumer_key' => wc_api_hash( $consumer_key ), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr( $consumer_key, -7 ), ], [ '%d', '%s', '%s', '%s', '%s', '%s' ] ); // phpcs:enable if ( ! $inserted ) { delete_option( ELIA_ASSISTANT_WC_OPTION ); elia_assistant_wc_redirect( 'failed' ); } update_option( ELIA_ASSISTANT_WC_OPTION, [ 'key_id' => (int) $wpdb->insert_id, 'truncated' => substr( $consumer_key, -7 ), 'created' => time(), ], false ); set_transient( elia_assistant_wc_transient_key(), [ 'consumer_key' => $consumer_key, 'consumer_secret' => $consumer_secret, ], 120 ); elia_assistant_wc_redirect( 'generated' ); } function elia_assistant_wc_revoke(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Insufficient permissions.', 'elia-assistant' ) ); } check_admin_referer( 'elia_assistant_wc_revoke' ); $stored = get_option( ELIA_ASSISTANT_WC_OPTION ); if ( is_array( $stored ) && ! empty( $stored['key_id'] ) ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->delete( elia_assistant_wc_table(), [ 'key_id' => (int) $stored['key_id'] ], [ '%d' ] ); } delete_option( ELIA_ASSISTANT_WC_OPTION ); delete_transient( elia_assistant_wc_transient_key() ); elia_assistant_wc_redirect( 'revoked' ); } add_action( 'admin_post_elia_assistant_wc_generate', 'elia_assistant_wc_generate' ); add_action( 'admin_post_elia_assistant_wc_revoke', 'elia_assistant_wc_revoke' ); // ─── Remote config fetch ───────────────────────────────────────────────────── /** * Fetch the widget's saved config from the ELIA API, for the preview on the settings page. The site * does not need it: widget.js loads the config by widget id in the visitor's browser. * Returns the decoded response array on success, an array with '_status'=>'not_found' on HTTP 404 * (no widget with this id, or it was never saved), or null on error / no widget id. * Not cached: it runs once each time the settings page opens, so the preview is always current. */ function elia_assistant_fetch_remote_config(): ?array { $opts = array_merge( elia_assistant_defaults(), (array) get_option( ELIA_ASSISTANT_OPTION, [] ) ); $widget_id = elia_assistant_widget_id( $opts ); if ( $widget_id === '' ) { return null; } $api_url = untrailingslashit( ELIA_ASSISTANT_API_URL ) . '/public/chat-widget/' . rawurlencode( $widget_id ); $response = wp_remote_get( $api_url, [ 'timeout' => 10 ] ); if ( is_wp_error( $response ) ) { return null; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( $code === 404 ) { return [ '_status' => 'not_found' ]; } if ( $code !== 200 ) { return null; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) || ! isset( $body['config'] ) || ! is_array( $body['config'] ) ) { return null; } return $body; } // ─── Admin: helpers ─────────────────────────────────────────────────────────── /** * Languages the chat widget ships translations for, with their own names. */ function elia_assistant_languages(): array { return [ 'bg' => 'Български', 'cs' => 'Čeština', 'da' => 'Dansk', 'de' => 'Deutsch', 'el' => 'Ελληνικά', 'en' => 'English', 'es' => 'Español', 'et' => 'Eesti', 'fi' => 'Suomi', 'fr' => 'Français', 'hr' => 'Hrvatski', 'hu' => 'Magyar', 'it' => 'Italiano', 'lt' => 'Lietuvių', 'lv' => 'Latviešu', 'nl' => 'Nederlands', 'pl' => 'Polski', 'pt' => 'Português', 'ro' => 'Română', 'ru' => 'Русский', 'sk' => 'Slovenčina', 'sl' => 'Slovenščina', 'sv' => 'Svenska', 'uk' => 'Українська', ]; } /** * Reads a text the dashboard may store per language (config.i18n[lang][key]) or globally. */ function elia_assistant_config_text( array $config, string $key, string $lang ): string { $i18n = isset( $config['i18n'] ) && is_array( $config['i18n'] ) ? $config['i18n'] : []; foreach ( [ $lang, 'en' ] as $code ) { if ( isset( $i18n[ $code ][ $key ] ) && is_string( $i18n[ $code ][ $key ] ) && trim( $i18n[ $code ][ $key ] ) !== '' ) { return trim( $i18n[ $code ][ $key ] ); } } if ( isset( $config[ $key ] ) && is_string( $config[ $key ] ) ) { return trim( $config[ $key ] ); } return ''; } /** * Picks dark or light text for a background colour, so the preview bubble stays readable on every theme. */ function elia_assistant_text_on( string $hex ): string { $hex = ltrim( $hex, '#' ); if ( strlen( $hex ) !== 6 ) { return '#1d2327'; } [ $r, $g, $b ] = array_map( 'hexdec', str_split( $hex, 2 ) ); return ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) > 150 ? '#1d2327' : '#ffffff'; } function elia_assistant_mode_label( string $mode ): string { switch ( $mode ) { case 'fullscreen': return __( 'opens full screen', 'elia-assistant' ); case 'mixed': return __( 'chat window that can expand to full screen', 'elia-assistant' ); default: return __( 'chat window in the corner', 'elia-assistant' ); } } /** * The ELIA mark (frontend/public/favicon.svg) with its transforms flattened, drawn inline so nothing * is loaded from outside. */ function elia_assistant_mark(): string { return ''; } /** * Save and Discard for a block whose changes are saved only when the admin is happy with them * (data-elia-manual). admin.js shows it once something in the block differs from what is saved; * without JS it stays hidden and the form's own Save changes button is shown instead. */ function elia_assistant_block_save_bar(): void { ?> %3$s %4$s', esc_attr( $class ), esc_url( $url ), esc_html( $label ), esc_html__( '(opens in a new tab)', 'elia-assistant' ) ); } // ─── Admin: settings page ───────────────────────────────────────────────────── add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), function ( array $links ): array { array_unshift( $links, sprintf( '%s', esc_url( admin_url( 'options-general.php?page=elia-assistant' ) ), esc_html__( 'Settings', 'elia-assistant' ) ) ); return $links; } ); /** * Right after activation the site shows no chat until a widget ID is entered. Say so on the * Plugins screen, where the person who just activated the plugin is looking. The notice can be * dismissed; the choice is remembered per user and the notice also goes away once an ID is saved. */ add_action( 'admin_notices', function () { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || $screen->id !== 'plugins' || ! current_user_can( 'manage_options' ) ) { return; } $opts = (array) get_option( ELIA_ASSISTANT_OPTION, [] ); if ( elia_assistant_widget_id( $opts ) !== '' || get_user_meta( get_current_user_id(), ELIA_ASSISTANT_NOTICE_META, true ) ) { return; } printf( '

%1$s %2$s %4$s

', esc_html__( 'ELIA Assistant is almost ready.', 'elia-assistant' ), esc_html__( 'Enter your widget ID to show the chat on your site.', 'elia-assistant' ), esc_url( admin_url( 'options-general.php?page=elia-assistant' ) ), esc_html__( 'Connect now', 'elia-assistant' ) ); // WordPress only hides a dismissed notice; remember the choice so it stays hidden. wp_register_script( 'elia-assistant-notice', false, [], ELIA_ASSISTANT_VERSION, true ); wp_enqueue_script( 'elia-assistant-notice' ); wp_add_inline_script( 'elia-assistant-notice', sprintf( 'document.addEventListener("click", function (e) {' . 'if (!e.target.closest || !e.target.closest(".elia-assistant-connect-notice .notice-dismiss")) { return; }' . 'fetch(%1$s, { method: "POST", credentials: "same-origin", body: new URLSearchParams({ action: "elia_assistant_dismiss_notice", _ajax_nonce: %2$s }) });' . '});', wp_json_encode( admin_url( 'admin-ajax.php' ) ), wp_json_encode( wp_create_nonce( 'elia_assistant_dismiss_notice' ) ) ) ); } ); add_action( 'wp_ajax_elia_assistant_dismiss_notice', function () { check_ajax_referer( 'elia_assistant_dismiss_notice' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } update_user_meta( get_current_user_id(), ELIA_ASSISTANT_NOTICE_META, '1' ); wp_send_json_success(); } ); add_action( 'admin_menu', function () { add_options_page( __( 'ELIA Assistant', 'elia-assistant' ), __( 'ELIA Assistant', 'elia-assistant' ), 'manage_options', 'elia-assistant', 'elia_assistant_render_settings_page' ); } ); add_action( 'admin_init', function () { register_setting( 'elia_assistant', ELIA_ASSISTANT_OPTION, [ 'sanitize_callback' => 'elia_assistant_sanitize', ] ); } ); add_action( 'admin_enqueue_scripts', function ( string $hook ) { if ( $hook !== 'settings_page_elia-assistant' ) { return; } $base = plugin_dir_url( __FILE__ ) . 'admin/'; wp_enqueue_style( 'elia-assistant-admin', $base . 'admin.css', [ 'dashicons' ], ELIA_ASSISTANT_VERSION ); wp_enqueue_script( 'elia-assistant-admin', $base . 'admin.js', [], ELIA_ASSISTANT_VERSION, true ); wp_localize_script( 'elia-assistant-admin', 'eliaAssistantAdmin', [ 'copied' => __( 'Copied', 'elia-assistant' ), 'copyFailed' => __( 'Press Ctrl+C to copy', 'elia-assistant' ), 'saving' => __( 'Saving…', 'elia-assistant' ), 'saved' => __( 'Saved', 'elia-assistant' ), 'saveFailed' => __( 'Could not save. Check your connection and try again.', 'elia-assistant' ), ] ); } ); function elia_assistant_render_settings_page(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $opts = array_merge( elia_assistant_defaults(), (array) get_option( ELIA_ASSISTANT_OPTION, [] ) ); $has_id = elia_assistant_widget_id( $opts ) !== ''; $remote = $has_id ? elia_assistant_fetch_remote_config() : null; if ( ! $has_id ) { $state = 'disconnected'; } elseif ( $remote === null ) { $state = 'error'; } elseif ( isset( $remote['_status'] ) ) { $state = 'not_found'; } else { $state = 'connected'; } $statuses = [ 'disconnected' => [ 'neutral', __( 'Not connected', 'elia-assistant' ) ], 'error' => [ 'warning', __( 'Preview unavailable', 'elia-assistant' ) ], 'not_found' => [ 'error', __( 'Widget not found', 'elia-assistant' ) ], 'connected' => [ 'ok', __( 'Connected', 'elia-assistant' ) ], ]; $languages = elia_assistant_languages(); $detected = elia_assistant_current_language(); $detected_name = $languages[ $detected ] ?? $detected; $manual_lang = $opts['language_override_enabled'] === '1' && isset( $languages[ $opts['language_override'] ] ); $floating = $opts['show_floating'] !== '0'; $themes = elia_assistant_themes(); $is_test = $opts['is_test'] === '1'; $enabled = $opts['enabled'] !== '0'; ?>


> >
>
  1. [elia_assistant]

plugin_basename( __FILE__ ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'elia_assistant_uninstall_choice' ), 'i18n' => [ 'title' => __( 'Delete ELIA Assistant', 'elia-assistant' ), 'text' => __( 'Do you want to keep the plugin settings? If you keep them, your widget ID, settings and the generated WooCommerce key stay in place and are used again if you reinstall the plugin.', 'elia-assistant' ), 'keep' => __( 'Delete plugin, keep settings', 'elia-assistant' ), 'remove' => __( 'Delete plugin and settings', 'elia-assistant' ), 'cancel' => __( 'Cancel', 'elia-assistant' ), 'error' => __( 'Could not save your choice. Please try again.', 'elia-assistant' ), ], ]; wp_register_style( 'elia-assistant-uninstall', false, [], ELIA_ASSISTANT_VERSION ); wp_enqueue_style( 'elia-assistant-uninstall' ); wp_add_inline_style( 'elia-assistant-uninstall', ' .elia-un-overlay { position: fixed; inset: 0; z-index: 100000; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; padding: 16px; } .elia-un-dialog { background: #fff; max-width: 480px; width: 100%; padding: 20px 24px; border-radius: 4px; box-shadow: 0 8px 30px rgba(0,0,0,.3); } .elia-un-dialog h2 { margin: 0 0 12px; font-size: 1.3em; } .elia-un-dialog p { margin: 0 0 20px; } .elia-un-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; } .elia-un-actions .elia-un-remove { color: #b32d2e; border-color: #b32d2e; } .elia-un-error { color: #b32d2e; margin: 0 0 12px; } ' ); wp_register_script( 'elia-assistant-uninstall', false, [ 'jquery', 'updates' ], ELIA_ASSISTANT_VERSION, true ); wp_enqueue_script( 'elia-assistant-uninstall' ); wp_add_inline_script( 'elia-assistant-uninstall', 'var eliaAssistantUninstall = ' . wp_json_encode( $data ) . ';', 'before' ); wp_add_inline_script( 'elia-assistant-uninstall', '(function () { var cfg = window.eliaAssistantUninstall; if (!cfg) { return; } var selector = \'tr[data-plugin="\' + cfg.plugin + \'"] .delete a\'; function el(tag, cls, text) { var node = document.createElement(tag); if (cls) { node.className = cls; } if (text) { node.textContent = text; } return node; } function runDelete(link) { var row = link.closest(\'tr\'); if (window.wp && wp.updates && typeof wp.updates.deletePlugin === \'function\') { wp.updates.deletePlugin({ plugin: row.getAttribute(\'data-plugin\'), slug: row.getAttribute(\'data-slug\') }); } else { window.location.href = link.href; } } function openDialog(link) { var overlay = el(\'div\', \'elia-un-overlay\'); var dialog = el(\'div\', \'elia-un-dialog\'); dialog.setAttribute(\'role\', \'dialog\'); dialog.setAttribute(\'aria-modal\', \'true\'); dialog.setAttribute(\'aria-labelledby\', \'elia-un-title\'); var title = el(\'h2\', \'\', cfg.i18n.title); title.id = \'elia-un-title\'; var error = el(\'p\', \'elia-un-error\'); error.hidden = true; var actions = el(\'div\', \'elia-un-actions\'); var cancel = el(\'button\', \'button\', cfg.i18n.cancel); var keep = el(\'button\', \'button button-primary\', cfg.i18n.keep); var remove = el(\'button\', \'button elia-un-remove\', cfg.i18n.remove); [cancel, keep, remove].forEach(function (b) { b.type = \'button\'; actions.appendChild(b); }); dialog.appendChild(title); dialog.appendChild(el(\'p\', \'\', cfg.i18n.text)); dialog.appendChild(error); dialog.appendChild(actions); overlay.appendChild(dialog); document.body.appendChild(overlay); keep.focus(); function close() { document.removeEventListener(\'keydown\', onKey); overlay.remove(); link.focus(); } function onKey(e) { if (e.key === \'Escape\') { close(); } } document.addEventListener(\'keydown\', onKey); overlay.addEventListener(\'click\', function (e) { if (e.target === overlay) { close(); } }); cancel.addEventListener(\'click\', close); function choose(keepData) { keep.disabled = remove.disabled = true; var body = new URLSearchParams({ action: \'elia_assistant_uninstall_choice\', _ajax_nonce: cfg.nonce, keep: keepData ? \'1\' : \'0\' }); fetch(cfg.ajaxUrl, { method: \'POST\', credentials: \'same-origin\', body: body }) .then(function (r) { return r.json(); }) .then(function (res) { if (!res || !res.success) { throw new Error(\'failed\'); } close(); runDelete(link); }) .catch(function () { error.textContent = cfg.i18n.error; error.hidden = false; keep.disabled = remove.disabled = false; }); } keep.addEventListener(\'click\', function () { choose(true); }); remove.addEventListener(\'click\', function () { choose(false); }); } // Capture phase, so this runs before the WordPress handler and its own confirm(). document.addEventListener(\'click\', function (e) { var link = e.target.closest ? e.target.closest(selector) : null; if (!link) { return; } e.preventDefault(); e.stopImmediatePropagation(); openDialog(link); }, true); })();' ); } ); add_action( 'wp_ajax_elia_assistant_uninstall_choice', function () { check_ajax_referer( 'elia_assistant_uninstall_choice' ); if ( ! current_user_can( 'delete_plugins' ) ) { wp_send_json_error( null, 403 ); } $keep = isset( $_POST['keep'] ) && sanitize_key( wp_unslash( $_POST['keep'] ) ) === '1'; update_option( ELIA_ASSISTANT_KEEP_DATA_OPTION, $keep ? '1' : '0', false ); wp_send_json_success(); } ); // ─── Embed options ──────────────────────────────────────────────────────────── /** * Options for Chatui.createChat() on the site, in the same shape as the dashboard's embed snippet: * { widgetId: "…" } plus what this plugin decides per page (target, mode, language) and, * on a test site, isTest: true, which marks every message from it as a test one. * widget.js loads the saved config by widgetId on every page view and lays these options over it, * so a Save in the dashboard reaches the site at once. Null when the chat is switched off or no * widget id is set. */ function elia_assistant_embed_options( array $opts, array $overrides ): ?array { $widget_id = elia_assistant_widget_id( $opts ); if ( $widget_id === '' || ( $opts['enabled'] ?? '1' ) === '0' ) { return null; } $config = array_merge( [ 'widgetId' => $widget_id ], $overrides ); $config['language'] = elia_assistant_resolve_language( $opts ); if ( ( $opts['is_test'] ?? '0' ) === '1' ) { $config['isTest'] = true; } return $config; } // ─── Frontend: floating widget ──────────────────────────────────────────────── add_action( 'wp_footer', function () { $opts = array_merge( elia_assistant_defaults(), (array) get_option( ELIA_ASSISTANT_OPTION, [] ) ); if ( empty( $opts['show_floating'] ) || $opts['show_floating'] === '0' ) { return; } $config = elia_assistant_embed_options( $opts, [ 'target' => '#elia-chat' ] ); if ( ! $config ) { return; } wp_enqueue_script( 'elia-assistant', ELIA_ASSISTANT_CDN, [], ELIA_ASSISTANT_VERSION, true ); wp_add_inline_script( 'elia-assistant', 'Chatui.createChat(' . wp_json_encode( $config ) . ');' ); echo '
' . "\n"; } ); // ─── Shortcode: [elia_assistant] ────────────────────────────────────────────── add_shortcode( 'elia_assistant', function ( $atts ) { $atts = shortcode_atts( [ 'height' => '500px', ], $atts, 'elia_assistant' ); $opts = array_merge( elia_assistant_defaults(), (array) get_option( ELIA_ASSISTANT_OPTION, [] ) ); $id = 'elia-chat-' . wp_unique_id(); $overrides = [ 'mode' => 'embedded', 'target' => '#' . $id ]; $config = elia_assistant_embed_options( $opts, $overrides ); if ( ! $config ) { return ''; } $config_json = wp_json_encode( $config ); $height = trim( (string) $atts['height'] ); if ( ! preg_match( '/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height ) ) { $height = '500px'; } wp_enqueue_script( 'elia-assistant', ELIA_ASSISTANT_CDN, [], ELIA_ASSISTANT_VERSION, true ); $init_js = '(function(){' . 'var el = document.getElementById(' . wp_json_encode( $id ) . ');' . 'if (!el || el.dataset.eliaInit) return;' . 'el.dataset.eliaInit = "1";' . 'Chatui.createChat(' . $config_json . ');' . '})();'; wp_add_inline_script( 'elia-assistant', $init_js ); return sprintf( '
', esc_attr( $id ), esc_attr( $height ) ); } );