should_display()) { return; } self::enqueue_scripts(false); wp_localize_script('rsvl-chat-widget', 'rsvlChat', self::script_data(false)); } /** * Render the chat widget HTML in the footer. */ public function render_widget() { if (! $this->should_display()) { return; } self::render_html(); } /** * Check if the widget should display on the current page. */ private function should_display(): bool { // Don't show in admin. if (is_admin()) { return false; } if (!RSVL_Index::has_sources()) { return false; } if (!self::passes_api_checks()) { return false; } if (RSVL_Token_Budget::is_limit_reached()) { return false; } if (resolver_get_setting('display_mode') !== 'site') { return false; } $current_id = get_queried_object_id(); $excluded_ids = (array) resolver_get_setting('display_excluded_ids'); // Exclusions always win if (is_singular() && in_array($current_id, $excluded_ids, true)) { return false; } // Explicit includes always show $included_ids = (array) resolver_get_setting('display_included_ids'); if (in_array($current_id, $included_ids, true)) { return true; } // All page types — show everywhere not explicitly excluded if (resolver_get_setting('display_scope') === 'all') { return true; } // Singular post types $singular_types = (array) resolver_get_setting('display_singular_types'); if (! empty($singular_types) && is_singular($singular_types)) { return true; } // Archive types $archives = (array) resolver_get_setting('display_archives'); foreach ($archives as $key) { if ($key === 'category' && is_category()) { return true; } if ($key === 'tag' && is_tag()) { return true; } if ($key === 'author' && is_author()) { return true; } if ($key === 'date' && is_date()) { return true; } if (is_tax($key)) { return true; } } // Special pages $special = (array) resolver_get_setting('display_special'); if (in_array('home', $special) && is_home()) { return true; } if (in_array('search', $special) && is_search()) { return true; } if (in_array('404', $special) && is_404()) { return true; } return false; } /** * Enqueue the widget's stylesheet and script: deferred on the site, and * loaded normally for the admin preview, whose admin.js depends on it. */ public static function enqueue_scripts(bool $is_preview): void { wp_enqueue_style( 'rsvl-chat-widget', RSVL_PLUGIN_URL . 'public/css/chat-widget.css', [], RSVL_VERSION ); $load = $is_preview ? true : ['strategy' => 'defer', 'in_footer' => true]; // The Markdown renderer, which the Chats screen shares. wp_register_script('rsvl-markdown', RSVL_PLUGIN_URL . 'public/js/markdown.js', [], RSVL_VERSION, $load); wp_enqueue_script( 'rsvl-chat-widget', RSVL_PLUGIN_URL . 'public/js/chat-widget.js', ['rsvl-markdown'], RSVL_VERSION, $load ); RSVL_Script_Strings::attach('rsvl-chat-widget', 'widget'); } /** * The widget script's settings (rsvlChat). The admin preview is rendered * without a shadow root. */ public static function script_data(bool $is_preview): array { $options = self::get_options(); $hp_fields = resolver_get_honeypot_fields(); return [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('resolver_chat_nonce'), 'botName' => resolver_get_setting('bot_name'), 'greeting' => resolver_get_setting('greeting_message'), 'position' => resolver_get_setting('position'), 'avatarUrl' => $options['avatar_url'], 'avatarRadius' => $options['avatar_radius'], 'iconUrl' => $options['icon_url'], 'rateSvgUp' => resolver_get_svg('like'), 'rateSvgDown' => resolver_get_svg('dislike'), 'jsField' => $hp_fields['js'], 'cssUrl' => RSVL_PLUGIN_URL . 'public/css/chat-widget.css?ver=' . RSVL_VERSION, 'useShadow' => ! $is_preview, // For numbers formatted in the browser, e.g. "de-DE". 'locale' => str_replace('_', '-', determine_locale()), ]; } /** * Readiness of the admin preview, independent of public-site visibility. */ public static function is_preview_ready(): bool { return RSVL_Index::has_trained_content() && self::passes_api_checks(); } /** * Output the chat widget HTML. Called both from the frontend hook and the admin preview. */ public static function render_html(bool $is_preview = false): void { // Keep the preview mounted so it can become usable without a reload. $hidden = $is_preview && ! self::is_preview_ready(); $options = self::get_options(); $hp_fields = resolver_get_honeypot_fields(); $hp_field = $hp_fields['hp']; $accent = resolver_get_setting('accent_color'); $position = resolver_get_setting('position'); $bot_name = resolver_get_setting('bot_name'); $greeting = resolver_get_setting('greeting_message'); $avatar_url = $options['avatar_url']; $avatar_radius = $options['avatar_radius']; $icon_url = $options['icon_url']; $extra_class = $is_preview ? ' rsvl-chat--preview' : ''; if ($avatar_url) { $extra_class .= ' rsvl-chat--has-avatar'; } ?>
style="--rsvl-accent: ; --rsvl-avatar-radius: px;">
✨
'', 'avatar_radius' => 0, 'icon_url' => '', ]; /** * Filter the widget's image options. Pro sets them from * its Appearance settings. * * @param array $options See RSVL_Widget_Renderer::get_options(). */ $options = wp_parse_args((array) apply_filters('resolver_widget_options', $defaults), $defaults); return [ 'avatar_url' => (string) $options['avatar_url'], 'avatar_radius' => (int) $options['avatar_radius'], 'icon_url' => (string) $options['icon_url'], ]; } private static function passes_api_checks(): bool { // Require a chat provider to be selected, and its API key saved. $provider = RSVL_Provider::get_provider((string) resolver_get_setting('chat_provider')); if (! $provider || ! $provider->is_connected()) { return false; } // Require a chat model to be selected if (resolver_get_setting('chat_model') === '') { return false; } // Require an embedding provider to be selected and have active API key $embedder = new RSVL_Embedder(); if (!$embedder->is_available()) { return false; } // Don't show if the configured embedding model's deprecation date has passed. $dep_date = $embedder->get_deprecation_date(); if ($dep_date && strtotime($dep_date) <= time()) { return false; } return true; } }