- Created a new eveai_chat plugin to support the new dynamic possibilities of the Specialists. Currently only supports standard Rag retrievers (i.e. no extra arguments).

This commit is contained in:
Josako
2024-11-26 13:35:29 +01:00
parent 7702a6dfcc
commit 07d89d204f
42 changed files with 1771 additions and 989 deletions

View File

@@ -1,74 +0,0 @@
<?php
class EveAI_Chat_Admin {
private $version;
public function __construct($version) {
$this->version = $version;
}
public function add_plugin_admin_menu() {
add_options_page(
'EveAI Chat Settings', // Page title
'EveAI Chat', // Menu title
'manage_options', // Capability required
'eveai-chat-settings', // Menu slug
array($this, 'display_plugin_settings_page') // Callback function
);
}
public function register_settings() {
register_setting(
'eveai_chat_settings', // Option group
'eveai_chat_settings', // Option name
array($this, 'validate_settings') // Sanitization callback
);
add_settings_section(
'eveai_chat_general', // ID
'General Settings', // Title
array($this, 'section_info'), // Callback
'eveai-chat-settings' // Page
);
add_settings_field(
'api_key', // ID
'API Key', // Title
array($this, 'api_key_callback'), // Callback
'eveai-chat-settings', // Page
'eveai_chat_general' // Section
);
// Add more settings fields as needed
}
public function section_info() {
echo 'Enter your EveAI Chat configuration settings below:';
}
public function api_key_callback() {
$options = get_option('eveai_chat_settings');
$api_key = isset($options['api_key']) ? $options['api_key'] : '';
?>
<input type="password"
id="api_key"
name="eveai_chat_settings[api_key]"
value="<?php echo esc_attr($api_key); ?>"
class="regular-text">
<p class="description">Enter your EveAI API key. You can find this in your EveAI dashboard.</p>
<?php
}
public function validate_settings($input) {
$new_input = array();
if(isset($input['api_key']))
$new_input['api_key'] = sanitize_text_field($input['api_key']);
return $new_input;
}
public function display_plugin_settings_page() {
// Load the settings page template
require_once plugin_dir_path(__FILE__) . 'views/settings-page.php';
}
}

View File

@@ -1,31 +0,0 @@
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post">
<?php
// Output security fields
settings_fields('eveai_chat_settings');
// Output setting sections and their fields
do_settings_sections('eveai-chat-settings');
// Output save settings button
submit_button('Save Settings');
?>
</form>
<div class="eveai-chat-help">
<h2>How to Use EveAI Chat</h2>
<p>To add the chat widget to your pages or posts, use the following shortcode:</p>
<code>[eveai_chat tenant_id="YOUR_TENANT_ID" language="en" supported_languages="en,fr,de,es"]</code>
<h3>Available Shortcode Parameters:</h3>
<ul>
<li><strong>tenant_id</strong> (required): Your EveAI tenant ID</li>
<li><strong>language</strong> (optional): Default language for the chat widget (default: en)</li>
<li><strong>supported_languages</strong> (optional): Comma-separated list of supported languages (default: en,fr,de,es)</li>
<li><strong>server_url</strong> (optional): EveAI server URL (default: https://evie.askeveai.com)</li>
<li><strong>specialist_id</strong> (optional): ID of the specialist to use (default: 1)</li>
</ul>
</div>
</div>

View File

@@ -1,26 +0,0 @@
<?php
/**
* Plugin Name: EveAI Chat Widget
* Description: Integrates the EveAI chat interface into your WordPress site.
* Version: 2.0.0
*/
if (!defined('WPINC')) {
die;
}
// Define plugin constants
define('EVEAI_CHAT_VERSION', '2.0.0');
define('EVEAI_CHAT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('EVEAI_CHAT_PLUGIN_URL', plugin_dir_url(__FILE__));
// Require the loader class
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-loader.php';
// Initialize the plugin
function run_eveai_chat() {
$plugin = new EveAI_Chat_Loader();
$plugin->run();
}
run_eveai_chat();

View File

@@ -1,70 +0,0 @@
<?php
/*
Plugin Name: EveAI Chat Widget
Plugin URI: https://askeveai.com/
Description: Integrates the EveAI chat interface into your WordPress site.
Version: 1.5.0
Author: Josako, Pieter Laroy
Author URI: https://askeveai.com/about/
*/
// Enqueue necessary scripts and styles
function eveai_chat_enqueue_scripts() {
wp_enqueue_script('socket-io', 'https://cdn.socket.io/4.0.1/socket.io.min.js', array(), '4.0.1', true);
wp_enqueue_script('marked', 'https://cdn.jsdelivr.net/npm/marked/marked.min.js', array(), '1.0.0', true);
wp_enqueue_script('eveai-sdk', plugin_dir_url(__FILE__) . 'js/eveai-sdk.js', array(), '1.0.0', true);
wp_enqueue_script('eveai-chat-widget', plugin_dir_url(__FILE__) . 'js/eveai-chat-widget.js', array('eveai-sdk'), '1.0.0', true);
wp_enqueue_style('material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons');
wp_enqueue_style('eveai-chat-style', plugin_dir_url(__FILE__) . 'css/eveai-chat-style.css');
}
add_action('wp_enqueue_scripts', 'eveai_chat_enqueue_scripts');
add_action('admin_enqueue_scripts', 'eveai_chat_enqueue_scripts');
// Shortcode function
function eveai_chat_shortcode($atts) {
// Default values
$defaults = array(
'tenant_id' => '',
'api_key' => '',
'domain' => '',
'language' => 'en',
'supported_languages' => 'en,fr,de,es',
'server_url' => 'https://evie.askeveai.com',
'specialist_id' => '1' // Added specialist_id parameter
);
// Merge provided attributes with defaults
$atts = shortcode_atts($defaults, $atts, 'eveai_chat');
// Sanitize inputs
$tenant_id = sanitize_text_field($atts['tenant_id']);
$api_key = sanitize_text_field($atts['api_key']);
$domain = esc_url_raw($atts['domain']);
$language = sanitize_text_field($atts['language']);
$supported_languages = sanitize_text_field($atts['supported_languages']);
$server_url = esc_url_raw($atts['server_url']);
$specialist_id = sanitize_text_field($atts['specialist_id']); // Sanitize specialist_id
// Generate a unique ID for this instance of the chat widget
$chat_id = 'chat-container-' . uniqid();
$output = "<div id='$chat_id'></div>";
$output .= "<script>
document.addEventListener('DOMContentLoaded', function() {
const eveAI = new EveAI(
'$tenant_id',
'$api_key',
'$domain',
'$language',
'$supported_languages',
'$server_url',
'$specialist_id'
);
eveAI.initializeChat('$chat_id');
});
</script>";
return $output;
}
add_shortcode('eveai_chat', 'eveai_chat_shortcode');

View File

@@ -1,164 +0,0 @@
<?php
class EveAI_Chat_API {
private $security;
private $eveai_api_url;
public function __construct() {
$this->security = new EveAI_Chat_Security();
$this->eveai_api_url = 'https://api.eveai.com'; // Should come from settings
}
public function register_routes() {
register_rest_route('eveai/v1', '/session-token', array(
'methods' => 'POST',
'callback' => array($this, 'get_session_token'),
'permission_callback' => array($this, 'verify_request'),
'args' => array(
'tenant_id' => array(
'required' => true,
'validate_callback' => function($param) {
return is_numeric($param);
}
),
'domain' => array(
'required' => true,
'validate_callback' => function($param) {
return is_string($param) && !empty($param);
}
)
)
));
}
public function verify_request($request) {
// Origin verification
$origin = $request->get_header('origin');
if (!$this->security->verify_origin($origin)) {
return new WP_Error(
'invalid_origin',
'Invalid request origin',
array('status' => 403)
);
}
// Nonce verification
$nonce = $request->get_header('X-WP-Nonce');
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error(
'invalid_nonce',
'Invalid nonce',
array('status' => 403)
);
}
return true;
}
public function get_session_token($request) {
try {
// Get the API key from WordPress options and decrypt it
$settings = get_option('eveai_chat_settings');
$encrypted_api_key = $settings['api_key'] ?? '';
if (empty($encrypted_api_key)) {
return new WP_Error(
'no_api_key',
'API key not configured',
array('status' => 500)
);
}
$api_key = $this->security->decrypt_sensitive_data($encrypted_api_key);
// Get parameters from request
$tenant_id = $request->get_param('tenant_id');
$domain = $request->get_param('domain');
// Request a session token from EveAI server
$response = wp_remote_post(
$this->eveai_api_url . '/session',
array(
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json'
),
'body' => json_encode(array(
'tenant_id' => $tenant_id,
'domain' => $domain,
'origin' => get_site_url()
)),
'timeout' => 15,
'data_format' => 'body'
)
);
if (is_wp_error($response)) {
throw new Exception($response->get_error_message());
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
throw new Exception('Invalid response from EveAI server: ' . $response_code);
}
$body = json_decode(wp_remote_retrieve_body($response), true);
if (empty($body['token'])) {
throw new Exception('No token received from EveAI server');
}
// Log the token generation (optional, for debugging)
error_log(sprintf(
'Generated session token for tenant %d from domain %s',
$tenant_id,
$domain
));
return array(
'success' => true,
'session_token' => $body['token']
);
} catch (Exception $e) {
error_log('EveAI session token generation failed: ' . $e->getMessage());
return new WP_Error(
'token_generation_failed',
'Failed to generate session token: ' . $e->getMessage(),
array('status' => 500)
);
}
}
/**
* Validates the session token with EveAI server
* Can be used for additional security checks
*/
public function validate_session_token($token) {
try {
$response = wp_remote_post(
$this->eveai_api_url . '/validate-token',
array(
'headers' => array(
'Content-Type' => 'application/json'
),
'body' => json_encode(array(
'token' => $token
)),
'timeout' => 15
)
);
if (is_wp_error($response)) {
return false;
}
$body = json_decode(wp_remote_retrieve_body($response), true);
return isset($body['valid']) && $body['valid'] === true;
} catch (Exception $e) {
error_log('Token validation failed: ' . $e->getMessage());
return false;
}
}
}

View File

@@ -1,129 +0,0 @@
<?php
class EveAI_Chat_Loader {
private $version;
public function __construct() {
$this->version = EVEAI_CHAT_VERSION;
$this->load_dependencies();
}
private function load_dependencies() {
// Load required files
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-api.php';
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-eveai-security.php';
// Load admin if in admin area
if (is_admin()) {
require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/class-eveai-admin.php';
}
}
public function run() {
// Initialize components
$this->define_admin_hooks();
$this->define_public_hooks();
$this->define_shortcodes();
}
private function define_admin_hooks() {
if (is_admin()) {
$admin = new EveAI_Chat_Admin($this->version);
add_action('admin_menu', array($admin, 'add_plugin_admin_menu'));
add_action('admin_init', array($admin, 'register_settings'));
}
}
private function define_public_hooks() {
// Enqueue scripts and styles
add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));
// Register REST API endpoints
add_action('rest_api_init', array($this, 'register_rest_routes'));
}
private function define_shortcodes() {
add_shortcode('eveai_chat', array($this, 'render_chat_widget'));
}
public function enqueue_assets() {
// Enqueue required scripts
wp_enqueue_script('socket-io', 'https://cdn.socket.io/4.0.1/socket.io.min.js', array(), '4.0.1', true);
wp_enqueue_script('marked', 'https://cdn.jsdelivr.net/npm/marked/marked.min.js', array(), '1.0.0', true);
// Enqueue our scripts
wp_enqueue_script(
'eveai-sdk',
EVEAI_CHAT_PLUGIN_URL . 'public/js/eveai-sdk.js',
array('socket-io', 'marked'),
$this->version,
true
);
wp_enqueue_script(
'eveai-chat-widget',
EVEAI_CHAT_PLUGIN_URL . 'public/js/eveai-chat-widget.js',
array('eveai-sdk'),
$this->version,
true
);
// Enqueue styles
wp_enqueue_style('material-icons', 'https://fonts.googleapis.com/icon?family=Material+Icons');
wp_enqueue_style(
'eveai-chat-style',
EVEAI_CHAT_PLUGIN_URL . 'public/css/eveai-chat-style.css',
array(),
$this->version
);
// Add WordPress-specific configuration
wp_localize_script('eveai-sdk', 'eveaiWP', array(
'nonce' => wp_create_nonce('wp_rest'),
'ajaxUrl' => admin_url('admin-ajax.php'),
'restUrl' => rest_url('eveai/v1/')
));
}
public function register_rest_routes() {
$api = new EveAI_Chat_API();
$api->register_routes();
}
public function render_chat_widget($atts) {
$defaults = array(
'tenant_id' => '',
'language' => 'en',
'supported_languages' => 'en,fr,de,es',
'server_url' => 'https://evie.askeveai.com',
'specialist_id' => '1'
);
$atts = shortcode_atts($defaults, $atts, 'eveai_chat');
$chat_id = 'chat-container-' . uniqid();
return sprintf(
'<div id="%s"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const eveAI = new EveAI({
tenantId: "%s",
language: "%s",
languages: "%s",
serverUrl: "%s",
specialistId: "%s",
proxyUrl: "%s"
});
eveAI.initializeChat("%s");
});
</script>',
$chat_id,
esc_js($atts['tenant_id']),
esc_js($atts['language']),
esc_js($atts['supported_languages']),
esc_js($atts['server_url']),
esc_js($atts['specialist_id']),
esc_js(rest_url('eveai/v1/session-token')),
esc_js($chat_id)
);
}
}

View File

@@ -1,133 +0,0 @@
<?php
class EveAI_Chat_Security {
public function verify_request($request) {
// Verify nonce
$nonce = $request->get_header('X-WP-Nonce');
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return false;
}
// Verify origin
$origin = $request->get_header('origin');
if (!$this->verify_origin($origin)) {
return false;
}
return true;
}
private function verify_origin($origin) {
// Get the site URL
$site_url = parse_url(get_site_url(), PHP_URL_HOST);
$origin_host = parse_url($origin, PHP_URL_HOST);
// Check if origin matches site URL or is a subdomain
return $origin_host === $site_url ||
strpos($origin_host, '.' . $site_url) !== false;
}
public function encrypt_sensitive_data($data) {
if (empty($data)) {
return '';
}
$encryption_key = $this->get_encryption_key();
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt(
$data,
'AES-256-CBC',
$encryption_key,
0,
$iv
);
return base64_encode($iv . $encrypted);
}
public function decrypt_sensitive_data($encrypted_data) {
if (empty($encrypted_data)) {
return '';
}
$encryption_key = $this->get_encryption_key();
$data = base64_decode($encrypted_data);
$iv = substr($data, 0, 16);
$encrypted = substr($data, 16);
return openssl_decrypt(
$encrypted,
'AES-256-CBC',
$encryption_key,
0,
$iv
);
}
private function get_encryption_key() {
$key = get_option('eveai_chat_encryption_key');
if (!$key) {
$key = bin2hex(random_bytes(32));
update_option('eveai_chat_encryption_key', $key);
}
return $key;
}
/**
* Generates a local temporary token for additional security
*/
public function generate_local_token($tenant_id, $domain) {
$data = array(
'tenant_id' => $tenant_id,
'domain' => $domain,
'timestamp' => time(),
'site_url' => get_site_url()
);
return $this->encrypt_sensitive_data(json_encode($data));
}
/**
* Verifies if the domain is allowed for the given tenant
*/
public function verify_tenant_domain($tenant_id, $domain) {
// This could be enhanced with a database check of allowed domains per tenant
$allowed_domains = array(
parse_url(get_site_url(), PHP_URL_HOST),
'localhost',
// Add other allowed domains as needed
);
$domain_host = parse_url($domain, PHP_URL_HOST);
return in_array($domain_host, $allowed_domains);
}
/**
* Enhanced origin verification
*/
public function verify_origin($origin) {
if (empty($origin)) {
return false;
}
// Get the allowed origins
$site_url = parse_url(get_site_url(), PHP_URL_HOST);
$allowed_origins = array(
$site_url,
'www.' . $site_url,
'localhost',
// Add any additional allowed origins
);
$origin_host = parse_url($origin, PHP_URL_HOST);
// Check if origin matches allowed origins or is a subdomain
foreach ($allowed_origins as $allowed_origin) {
if ($origin_host === $allowed_origin ||
strpos($origin_host, '.' . $allowed_origin) !== false) {
return true;
}
}
return false;
}
}

View File

@@ -1,2 +0,0 @@
<?php
// Silence is golden.

View File

@@ -1,87 +0,0 @@
class EveAI {
constructor(config) {
// Required parameters
this.tenantId = config.tenantId;
this.serverUrl = config.serverUrl;
this.specialistId = config.specialistId;
this.proxyUrl = config.proxyUrl;
// Optional parameters with defaults
this.language = config.language || 'en';
this.languages = config.languages?.split(',') || ['en'];
this.domain = config.domain || window.location.origin;
// Internal state
this.sessionToken = null;
this.initialized = false;
}
async getSessionToken() {
try {
const headers = {
'Content-Type': 'application/json'
};
// Add WordPress-specific headers if they exist
if (window.eveaiWP?.nonce) {
headers['X-WP-Nonce'] = window.eveaiWP.nonce;
}
const response = await fetch(this.proxyUrl, {
method: 'POST',
headers: headers,
body: JSON.stringify({
tenant_id: this.tenantId,
domain: this.domain
})
});
if (!response.ok) {
throw new Error('Failed to get session token');
}
const data = await response.json();
return data.session_token;
} catch (error) {
console.error('Error getting session token:', error);
throw error;
}
}
async initializeChat(containerId) {
try {
// Get session token before initializing chat
this.sessionToken = await this.getSessionToken();
const container = document.getElementById(containerId);
if (!container) {
throw new Error('Container not found');
}
const chatWidget = document.createElement('eveai-chat-widget');
Object.entries({
'tenant-id': this.tenantId,
'session-token': this.sessionToken,
'domain': this.domain,
'language': this.language,
'languages': this.languages.join(','),
'server-url': this.serverUrl,
'specialist-id': this.specialistId
}).forEach(([attr, value]) => {
chatWidget.setAttribute(attr, value);
});
container.appendChild(chatWidget);
this.initialized = true;
return chatWidget;
} catch (error) {
console.error('Failed to initialize chat:', error);
// Re-throw to allow custom error handling
throw error;
}
}
}
// Make available globally
window.EveAI = EveAI;

View File

@@ -1,79 +0,0 @@
=== EveAI Chat Widget ===
Contributors: Josako
Tags: chat, ai
Requires at least: 5.0
Tested up to: 5.9
Stable tag: 1.5.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Integrates the EveAI chat interface into your WordPress site.
== Description ==
This plugin allows you to easily add the EveAI chat widget to your WordPress site. It provides a configurable interface to set up your EveAI chat parameters.
== Installation ==
1. Upload the `eveai-chat-widget` folder to the `/wp-content/plugins/` directory
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Add EveAI Chat Widget to your page or post using the instructions below.
== Usage ==
To add an EveAI Chat Widget to your page or post, use the following shortcode:
[eveai_chat tenant_id="YOUR_TENANT_ID" api_key="YOUR_API_KEY" domain="YOUR_DOMAIN" language="LANGUAGE_CODE" supported_languages="COMMA_SEPARATED_LANGUAGE_CODES" server_url="Server URL for Evie"]
Example:
[eveai_chat tenant_id="123456" api_key="your_api_key_here" domain="https://your-domain.com" language="en" supported_languages="en,fr,de,es" server_url="https://evie.askeveai.com"]
You can add multiple chat widgets with different configurations by using the shortcode multiple times with different parameters.
== Frequently Asked Questions ==
= Where do I get my EveAI credentials? =
Contact your EveAI service provider to obtain your Tenant ID, API Key, and Domain.
== Changelog ==
= 1.5.0 =
* Allow for multiple servers to serve Evie
= 1.4.1 - 1.4...=
* Bug fixes
= 1.4.0 =
* Allow for multiple instances of Evie on the same website
* Parametrization of the shortcode
= 1.3.3 - =
* ensure all attributes (also height and supportedLanguages) are set before initializing the socket
* Bugfixing
= 1.3.2 =
* Correct supportedLanguages to be an Array
= 1.3.1 =
* Correct evie domain
= 1.3.0 =
* Enable user to select language
* Make Question area multi-line
* Enable height to be set in shortcode
= 1.2.0 =
* Create shortcodes
= 1.1.0 =
* Added configurable settings
* Improved security with server-side API key handling
= 1.0.0 =
* Initial release
== Upgrade Notice ==
= 1.1.0 =
This version adds configurable settings and improves security. Please update your EveAI credentials after upgrading.

View File

@@ -0,0 +1,130 @@
<?php
namespace EveAI\Chat;
class Admin implements Loadable {
public function init() {
add_action('admin_menu', [$this, 'add_plugin_page']);
add_action('admin_init', [$this, 'register_settings']);
}
public function add_plugin_page() {
add_options_page(
__('EveAI Chat Settings', 'eveai-chat'),
__('EveAI Chat', 'eveai-chat'),
'manage_options',
'eveai-chat-settings',
[$this, 'render_settings_page']
);
}
public function register_settings() {
register_setting(
'eveai_chat_settings',
'eveai_chat_settings',
[$this, 'sanitize_settings']
);
add_settings_section(
'eveai_chat_general',
__('General Settings', 'eveai-chat'),
[$this, 'render_section_info'],
'eveai-chat-settings'
);
// Add settings fields
$this->add_settings_fields();
}
private function add_settings_fields() {
$fields = [
'tenant_id' => [
'label' => __('Tenant ID', 'eveai-chat'),
'type' => 'number'
],
'api_key' => [
'label' => __('API Key', 'eveai-chat'),
'type' => 'password'
],
'socket_url' => [
'label' => __('Socket URL', 'eveai-chat'),
'type' => 'url'
],
'auth_url' => [
'label' => __('Auth URL', 'eveai-chat'),
'type' => 'url'
]
];
foreach ($fields as $key => $field) {
add_settings_field(
"eveai_chat_{$key}",
$field['label'],
[$this, 'render_field'],
'eveai-chat-settings',
'eveai_chat_general',
[
'key' => $key,
'type' => $field['type'],
'label_for' => "eveai_chat_{$key}"
]
);
}
}
public function render_section_info() {
echo '<p>' . esc_html__('Configure your EveAI Chat settings below.', 'eveai-chat') . '</p>';
}
public function render_field($args) {
$options = get_option('eveai_chat_settings');
$key = $args['key'];
$type = $args['type'];
$value = isset($options[$key]) ? $options[$key] : '';
// If it's an API key and not empty, show placeholder
if ($key === 'api_key' && !empty($value)) {
$value = str_repeat('•', 20);
}
printf(
'<input type="%s" id="eveai_chat_%s" name="eveai_chat_settings[%s]" value="%s" class="regular-text" />',
esc_attr($type),
esc_attr($key),
esc_attr($key),
esc_attr($value)
);
}
public function sanitize_settings($input) {
$sanitized = [];
// Sanitize tenant_id
$sanitized['tenant_id'] = isset($input['tenant_id']) ?
absint($input['tenant_id']) : '';
// Handle API key (only update if changed)
$old_settings = get_option('eveai_chat_settings');
if (isset($input['api_key']) && !empty($input['api_key']) &&
$input['api_key'] !== str_repeat('•', 20)) {
$sanitized['api_key'] = Security::encrypt_api_key($input['api_key']);
} else {
$sanitized['api_key'] = $old_settings['api_key'] ?? '';
}
// Sanitize URLs
$sanitized['socket_url'] = isset($input['socket_url']) ?
esc_url_raw($input['socket_url']) : 'https://chat.askeveai.com';
$sanitized['auth_url'] = isset($input['auth_url']) ?
esc_url_raw($input['auth_url']) : 'https://api.askeveai.com';
return $sanitized;
}
public function render_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/views/settings-page.php';
}
}

View File

@@ -0,0 +1,24 @@
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<form action="options.php" method="post">
<?php
settings_fields('eveai_chat_settings');
do_settings_sections('eveai-chat-settings');
submit_button('Save Settings');
?>
</form>
<div class="eveai-chat-help">
<h2><?php esc_html_e('How to Use EveAI Chat', 'eveai-chat'); ?></h2>
<p><?php esc_html_e('To add the chat widget to your pages or posts, use the following shortcode:', 'eveai-chat'); ?></p>
<code>[eveai_chat language="en" languages="en,fr,de" specialist_id="1"]</code>
<h3><?php esc_html_e('Available Shortcode Parameters:', 'eveai-chat'); ?></h3>
<ul>
<li><strong>language</strong>: <?php esc_html_e('Default language for the chat widget (default: en)', 'eveai-chat'); ?></li>
<li><strong>languages</strong>: <?php esc_html_e('Comma-separated list of supported languages (default: en)', 'eveai-chat'); ?></li>
<li><strong>specialist_id</strong>: <?php esc_html_e('ID of the specialist to use (default: 1)', 'eveai-chat'); ?></li>
</ul>
</div>
</div>

View File

@@ -1,34 +1,53 @@
class EveAIChatWidget extends HTMLElement {
static get observedAttributes() {
return ['tenant-id', 'session-token', 'domain', 'language', 'languages', 'server-url', 'specialist-id'];
return [
'tenant-id',
'session-token',
'language',
'languages',
'specialist-id',
'server-url'
];
}
constructor() {
super();
// Networking attributes
this.socket = null; // Initialize socket to null
this.attributesSet = false; // Flag to check if all attributes are set
this.room = null;
this.lastRoom = null; // Store last known room
this.userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; // Detect user's timezone
this.heartbeatInterval = null;
this.idleTime = 0; // in milliseconds
this.maxConnectionIdleTime = 1 * 60 * 60 * 1000; // 1 hour in milliseconds
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
// EveAI specific attributes
this.languages = []
this.currentLanguage = null;
this.specialistId = null;
console.log('EveAIChatWidget constructor called');
// Bind methods to ensure correct 'this' context
this.handleSendMessage = this.handleSendMessage.bind(this);
this.handleTokenUpdate = this.handleTokenUpdate.bind(this);
this.updateAttributes = this.updateAttributes.bind(this);
}
connectedCallback() {
console.log('connectedCallback called');
console.log('Chat Widget Connected');
this.innerHTML = this.getTemplate();
this.setupElements()
this.populateLanguageDropdown()
this.addEventListeners()
if (this.areAllAttributesSet() && !this.socket) {
console.log('Attributes already set in connectedCallback, initializing socket');
this.initializeSocket();
if (this.areAllAttributesSet()) {
console.log('All attributes are set, initializing socket');
this.initializeSocket();
} else {
console.warn('Not all required attributes are set yet');
}
}
@@ -79,20 +98,39 @@ class EveAIChatWidget extends HTMLElement {
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
this.updateAttributes();
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
if (this.areAllAttributesSet() && !this.socket) {
this.attributesSet = true;
this.populateLanguageDropdown();
this.initializeSocket();
}
// Handle token updates specially
if (name === 'session-token' && oldValue !== newValue) {
this.updateAttributes();
if (newValue) {
console.log('Received new session token');
this.sessionToken = newValue;
// If socket exists, reconnect with new token
if (this.socket) {
this.socket.disconnect();
this.initializeSocket();
} else if (this.areAllAttributesSet()) {
// Initialize socket if all other attributes are ready
this.initializeSocket();
}
}
return;
}
if (name === 'languages' || name === 'language') {
this.updateAttributes();
this.populateLanguageDropdown();
return;
}
this.updateAttributes();
}
updateAttributes() {
this.tenantId = parseInt(this.getAttribute('tenant-id'));
this.sessionToken = this.getAttribute('session_token');
this.domain = this.getAttribute('domain');
this.sessionToken = this.getAttribute('session-token');
this.language = this.getAttribute('language');
const languageAttr = this.getAttribute('languages');
this.languages = languageAttr ? languageAttr.split(',') : [];
@@ -102,7 +140,6 @@ class EveAIChatWidget extends HTMLElement {
console.log('Updated attributes:', {
tenantId: this.tenantId,
sessionToken: this.sessionToken,
domain: this.domain,
language: this.language,
currentLanguage: this.currentLanguage,
languages: this.languages,
@@ -112,69 +149,67 @@ class EveAIChatWidget extends HTMLElement {
}
areAllAttributesSet() {
const tenantId = this.getAttribute('tenant-id');
const sessionToken = this.getAttribute('session-token');
const domain = this.getAttribute('domain');
const language = this.getAttribute('language');
const languages = this.getAttribute('languages');
const serverUrl = this.getAttribute('server-url');
const specialistId = this.getAttribute('specialist-id')
console.log('Checking if all attributes are set:', {
tenantId,
sessionToken,
domain,
language,
languages,
serverUrl,
specialistId
tenantId: this.tenantId,
sessionToken: this.sessionToken,
language: this.language,
languages: this.languages,
serverUrl: this.serverUrl,
specialistId: this.specialistId
});
return tenantId && sessionToken && domain && language && languages && serverUrl && specialistId;
}
createLanguageDropdown() {
const select = document.createElement('select');
select.id = 'languageSelect';
this.languages.forEach(lang => {
const option = document.createElement('option');
option.value = lang;
option.textContent = lang.toUpperCase();
if (lang === this.currentLanguage) {
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (e) => {
this.currentLanguage = e.target.value;
// You might want to emit an event or update the backend about the language change
});
return select;
const requiredAttributes = [
'tenant-id',
'session-token',
'language',
'languages',
'specialist-id',
'server-url'
];
return requiredAttributes.every(attr => this.getAttribute(attr));
}
handleTokenUpdate(newToken) {
if (this.socket && this.socket.connected) {
console.log('Updating socket connection with new token');
// Emit token update event to server
this.socket.emit('update_token', { token: newToken });
} else if (newToken && !this.socket) {
// If we have a new token but no socket, try to initialize
this.initializeSocket();
}
}
initializeSocket() {
console.log(`Initializing socket connection to Evie at ${this.serverUrl}`);
if (this.socket) {
console.log('Socket already initialized');
return;
}
console.log(`Initializing socket connection to Evie`);
if (!this.sessionToken) {
console.error('Cannot initialize socket without session token');
return;
}
// Ensure apiKey is passed in the query parameters
this.socket = io(this.serverUrl, {
path: '/chat/socket.io/',
transports: ['websocket', 'polling'],
query: {
tenantId: this.tenantId,
sessionToken: this.sessionToken
path: '/socket.io/',
transports: ['websocket'],
query: { // Change from auth to query
token: this.sessionToken
},
auth: {
token: this.sessionToken // Ensure token is included here
// token: 'Bearer ' + this.sessionToken // Old setup - remove if everything works fine without Bearer
},
reconnectionAttempts: Infinity, // Infinite reconnection attempts
reconnectionAttempts: 5, // Infinite reconnection attempts
reconnectionDelay: 5000, // Delay between reconnections
timeout: 20000, // Connection timeout
});
if (!this.socket) {
console.error('Error initializing socket')
} else {
console.log('Socket initialized')
}
this.setupSocketEventHandlers();
}
@@ -185,8 +220,10 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Connected to EveAI.');
this.updateConnectionStatus(true);
this.startHeartbeat();
if (data?.room) {
this.room = data.room;
this.lastRoom = this.room;
console.log(`Joined room: ${this.room}`);
} else {
console.log('Room information not received on connect');
@@ -199,12 +236,23 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Authenticated.');
if (data?.room) {
this.room = data.room;
this.lastRoom = this.room;
console.log(`Confirmed room: ${this.room}`);
} else {
console.log('Room information not received on authentication');
}
});
// Room join handler ------------------------------------------------------
this.socket.on('room_join', (data) => {
console.log('Room join event received:', data);
if (data?.room) {
this.room = data.room;
this.lastRoom = this.room;
console.log(`Joined room: ${this.room}`);
}
});
// connect-error handler --------------------------------------------------
this.socket.on('connect_error', (err) => {
console.error('Socket connection error:', err);
@@ -229,12 +277,21 @@ class EveAIChatWidget extends HTMLElement {
this.setStatusMessage('Disconnected from EveAI. Please refresh the page for further interaction.');
this.updateConnectionStatus(false);
this.stopHeartbeat();
this.room = null;
});
// Token related handlers -------------------------------------------------
this.socket.on('token_expired', () => {
console.log('Token expired');
this.setStatusMessage('Session expired. Please refresh the page.');
this.updateConnectionStatus(false);
});
// reconnect_attempt handler ----------------------------------------------
this.socket.on('reconnect_attempt', () => {
console.log('Attempting to reconnect to the server...');
this.setStatusMessage('Attempting to reconnect...');
this.socket.on('reconnect_attempt', (attemptNumber) => {
console.log(`Reconnection attempt ${attemptNumber}`);
this.setStatusMessage(`Reconnecting... (Attempt ${attemptNumber})`);
this.reconnectAttempts = attemptNumber;
});
// reconnect handler ------------------------------------------------------
@@ -245,20 +302,103 @@ class EveAIChatWidget extends HTMLElement {
this.startHeartbeat();
});
// reconnect failed -------------------------------------------------------
this.socket.on('reconnect_failed', () => {
console.log('Reconnection failed');
this.setStatusMessage('Unable to reconnect. Please refresh the page.');
this.handleReconnectFailure();
});
// room rejoin result -----------------------------------------------------
this.socket.on('room_rejoin_result', (response) => {
if (response.success) {
console.log('Successfully rejoined room');
this.room = response.room;
this.setStatusMessage('Reconnected successfully.');
} else {
console.error('Failed to rejoin room');
this.handleRoomRejoinFailure();
}
});
// bot_response handler ---------------------------------------------------
this.socket.on('bot_response', (data) => {
console.log('Bot response received: ', data);
if (data.tenantId === this.tenantId) {
if (data.tenantId === this.tenantId && data?.room === this.room) {
setTimeout(() => this.startTaskCheck(data.taskId), 1000);
this.setStatusMessage('Processing...');
} else {
console.log('Received message for different room or tenant, ignoring');
}
});
// task_status handler ----------------------------------------------------
this.socket.on('task_status', (data) => {
console.log('Task status received:', data);
if (!this.room) {
console.log('No room assigned, cannot process task status');
return;
}
this.handleTaskStatus(data);
});
// Feedback handler -------------------------------------------------------
this.socket.on('feedback_received', (data) => {
if (data?.room === this.room) {
this.setStatusMessage(data.status === 'success' ? 'Feedback recorded.' : 'Failed to record feedback.');
}
});
}
attemptRoomRejoin() {
console.log(`Attempting to rejoin room: ${this.lastRoom}`);
this.socket.emit('rejoin_room', {
token: this.sessionToken,
tenantId: this.tenantId,
previousRoom: this.lastRoom,
timestamp: Date.now()
});
}
handleReconnectFailure() {
this.room = null;
this.lastRoom = null;
this.reconnectAttempts = 0;
this.updateConnectionStatus(false);
// Optionally reload the widget
if (confirm('Connection lost. Would you like to refresh the chat?')) {
window.location.reload();
}
}
handleRoomRejoinFailure() {
// Clear room state
this.room = null;
this.lastRoom = null;
// Request new room
this.socket.emit('request_new_room', {
token: this.sessionToken,
tenantId: this.tenantId
});
}
clearRoomState() {
// Use when intentionally leaving/clearing a room
this.room = null;
this.lastRoom = null;
this.reconnectAttempts = 0;
}
handleAuthError(error) {
console.error('Authentication error:', error);
this.setStatusMessage('Authentication failed. Please refresh the page.');
this.updateConnectionStatus(false);
if (this.socket) {
this.socket.disconnect();
}
}
setStatusMessage(message) {
@@ -353,13 +493,14 @@ class EveAIChatWidget extends HTMLElement {
console.error('Socket is not initialized');
return;
}
if (!this.jwtToken) {
console.error('JWT token is not available');
if (!this.validateRoom()) {
console.log("No valid room to handle feedback")
return;
}
console.log('Sending message to backend');
console.log(`Feedback for ${interactionId}: ${feedback}`);
this.socket.emit('feedback', { tenantId: this.tenantId, token: this.jwtToken, feedback, interactionId });
console.log(`Sending feedback for ${interactionId}: ${feedback}`);
this.socket.emit('feedback', { tenant_id: this.tenantId, token: this.sessionToken, feedback, interactionId, room: this.room });
this.setStatusMessage('Feedback sent.');
}
@@ -408,24 +549,24 @@ class EveAIChatWidget extends HTMLElement {
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
}
toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
console.log('feedback called');
this.idleTime = 0; // Reset idle time
if (feedback === 'up') {
thumbsUp.textContent = 'thumb_up'; // Change to filled icon
thumbsUp.classList.remove('outlined');
thumbsUp.classList.add('filled');
thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
thumbsDown.classList.add('outlined');
thumbsDown.classList.remove('filled');
} else {
thumbsDown.textContent = 'thumb_down'; // Change to filled icon
thumbsDown.classList.remove('outlined');
thumbsDown.classList.add('filled');
thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
thumbsUp.classList.add('outlined');
thumbsUp.classList.remove('filled');
}
toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
console.log('feedback called');
this.idleTime = 0; // Reset idle time
if (feedback === 'up') {
thumbsUp.textContent = 'thumb_up'; // Change to filled icon
thumbsUp.classList.remove('outlined');
thumbsUp.classList.add('filled');
thumbsDown.textContent = 'thumb_down_off_alt'; // Keep the other icon outlined
thumbsDown.classList.add('outlined');
thumbsDown.classList.remove('filled');
} else {
thumbsDown.textContent = 'thumb_down'; // Change to filled icon
thumbsDown.classList.remove('outlined');
thumbsDown.classList.add('filled');
thumbsUp.textContent = 'thumb_up_off_alt'; // Keep the other icon outlined
thumbsUp.classList.add('outlined');
thumbsUp.classList.remove('filled');
}
// Send feedback to the backend
this.handleFeedback(feedback, interactionId);
@@ -434,6 +575,21 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
handleSendMessage() {
console.log('handleSendMessage called');
this.idleTime = 0; // Reset idle time
if (!this.socket?.connected) {
console.error('Cannot send message: socket not connected');
this.setStatusMessage('Not connected to server. Please try again.');
return;
}
if (!this.room) {
console.error('Cannot send message: no room assigned');
this.setStatusMessage('Connection not ready. Please wait...');
// Try to rejoin room if we have a last known room
if (this.lastRoom) {
this.attemptRoomRejoin();
}
return;
}
const message = this.questionInput.value.trim();
if (message) {
this.addUserMessage(message);
@@ -444,12 +600,17 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
}
startTaskCheck(taskId) {
console.log('Emitting check_task_status for:', taskId);
this.socket.emit('check_task_status', {
task_id: taskId,
token: this.jwtToken,
tenantId: this.tenantId
});
if (!this.validateRoom()) {
console.error('Cannot check task status: no room assigned');
return;
}
console.log('Emitting check_task_status for:', taskId);
this.socket.emit('check_task_status', {
task_id: taskId,
token: this.sessionToken,
tenant_id: this.tenantId,
room: this.room
});
}
handleTaskStatus(data) {
@@ -474,23 +635,26 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
}
}
sendMessageToBackend(message) {
console.log('sendMessageToBackend called');
if (!this.socket) {
console.error('Socket is not initialized');
if (!this.socket || !this.room) {
console.error('Cannot send message: socket or room not available');
return;
}
if (!this.validateRoom()) {
return;
}
const selectedLanguage = this.languageSelect.value;
const messageData = {
tenantId: parseInt(this.tenantId),
token: this.jwtToken,
specialistId: parseInt(this.specialistId),
arguments: {
language: selectedLanguage,
query: message
},
timezone: this.userTimezone
tenant_id: parseInt(this.tenantId),
token: this.sessionToken,
specialist_id: parseInt(this.specialistId),
arguments: {
language: selectedLanguage,
query: message
},
timezone: this.userTimezone,
room: this.room
};
console.log('Sending message to backend:', messageData);
@@ -513,6 +677,19 @@ toggleFeedback(thumbsUp, thumbsDown, feedback, interactionId) {
this.sendButton.style.pointerEvents = 'auto'; // Re-enable click events
}
}
validateRoom() {
if (!this.room) {
console.error('No room assigned');
this.setStatusMessage('Connection not ready. Please wait...');
// Try to rejoin room if we have a last known room
if (this.lastRoom) {
this.attemptRoomRejoin();
}
return false;
}
return true;
}
}
customElements.define('eveai-chat-widget', EveAIChatWidget);

View File

@@ -0,0 +1,167 @@
class EveAI {
constructor(config) {
// Required parameters
this.tenantId = config.tenantId;
// Chat configuration
this.language = config.language || 'en';
this.languages = config.languages?.split(',') || ['en'];
this.specialistId = config.specialistId;
// Server Configuration
this.socketUrl = config.socketUrl || 'https://chat.askeveai.com';
this.authUrl = config.authUrl || 'https://api.askeveai.com';
this.proxyUrl = config.proxyUrl; // URL for auth proxy (WP or standalone)
this.wpRestNamespace = 'eveai/v1'; // This should match the PHP constant
this.wpRestUrl = `${config.wpBaseUrl || '/wp-json'}/${this.wpRestNamespace}`;
// Initialize token management
this.tokenManager = new EveAITokenManager({
proxyUrl: this.proxyUrl,
onTokenChange: this.handleTokenChange.bind(this),
onError: this.handleAuthError.bind(this)
});
this.chatWidget = null;
}
async initialize(containerId) {
try {
if (!containerId) {
throw new Error('Container ID is required');
}
console.log('Starting initialization with settings:', {
tenantId: this.tenantId,
wpRestUrl: this.wpRestUrl
});
// Get the WordPress nonce
const wpNonce = window.eveaiWP?.nonce;
if (!wpNonce) {
throw new Error('WordPress nonce not found');
}
// Use WordPress REST API endpoint instead of direct API call
const response = await fetch(`${this.wpRestUrl}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpNonce,
},
credentials: 'same-origin', // Important for WP cookie handling
body: JSON.stringify({
tenant_id: this.tenantId
})
});
console.log('Token request response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('Auth error response:', errorText);
throw new Error('Authentication failed');
}
const { access_token, expires_in } = await response.json();
console.log('Token received:', access_token);
console.log('Token Expiry:', expires_in);
// Store token and expiry
this.sessionToken = access_token;
this.tokenExpiry = Date.now() + (expires_in * 1000);
// Initialize token refresh timer
this.setupTokenRefresh(expires_in);
return this.initializeChat(containerId, access_token);
} catch (error) {
console.error('Full initialization error:', error);
throw error;
}
}
setupTokenRefresh(expiresIn) {
// Set up refresh 5 minutes before expiry
const refreshTime = (expiresIn - 300) * 1000; // Convert to milliseconds
setTimeout(() => this.refreshToken(), refreshTime);
}
async refreshToken() {
try {
const response = await fetch(`${this.wpRestUrl}/refresh`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.sessionToken}`
}
});
if (response.ok) {
const { access_token, expires_in } = await response.json();
this.sessionToken = access_token;
this.tokenExpiry = Date.now() + (expires_in * 1000);
// Update token in chat widget
if (this.chatWidget) {
this.chatWidget.setAttribute('session-token', access_token);
}
// Setup next refresh
this.setupTokenRefresh(expires_in);
} else {
console.error('Token refresh failed');
}
} catch (error) {
console.error('Token refresh error:', error);
}
}
async initializeChat(containerId) {
const container = document.getElementById(containerId);
if (!container) {
throw new Error('Container not found');
}
// Create chat widget with all necessary attributes
const chatWidget = document.createElement('eveai-chat-widget');
// Set all required attributes
const attributes = {
'tenant-id': this.tenantId,
'session-token': this.sessionToken,
'language': this.language,
'languages': this.languages.join(','),
'specialist-id': this.specialistId,
'server-url': this.socketUrl
};
console.log('Setting widget attributes:', attributes);
Object.entries(attributes).forEach(([attr, value]) => {
if (value === null || value === undefined) {
console.warn(`Warning: ${attr} is ${value}`);
}
chatWidget.setAttribute(attr, value);
});
container.appendChild(chatWidget);
this.chatWidget = chatWidget;
return chatWidget;
}
handleTokenChange(newToken) {
if (this.chatWidget) {
this.chatWidget.setAttribute('session-token', newToken);
}
}
handleAuthError(error) {
if (this.chatWidget) {
this.chatWidget.handleAuthError(error);
}
}
}
// Make available globally
// window.EveAI = EveAI;

View File

@@ -0,0 +1,69 @@
// eveai-token-manager.js
class EveAITokenManager extends EventTarget {
constructor() {
super();
this.token = null;
this.checkInterval = null;
this.isRefreshing = false;
this.refreshThreshold = 60; // Refresh token if less than 60s remaining
}
// Initialize with a token
async initialize(token) {
this.token = token;
this.startTokenCheck();
this.dispatchEvent(new CustomEvent('tokenChanged', { detail: { token } }));
}
// Start periodic token verification
startTokenCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
this.checkInterval = setInterval(async () => {
await this.verifyAndRefreshToken();
}, 5000); // Check every 5 seconds
}
// Verify token and refresh if needed
async verifyAndRefreshToken() {
if (!this.token || this.isRefreshing) return;
try {
const response = await fetch(`${this.proxyUrl}/verify`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
if (!response.ok) {
throw new Error('Token verification failed');
}
const data = await response.json();
if (data.expires_in < this.refreshThreshold) {
await this.refreshToken();
}
} catch (error) {
this.handleTokenError(error);
}
}
// Handle any token errors
handleTokenError(error) {
this.dispatchEvent(new CustomEvent('tokenError', { detail: { error } }));
this.token = null;
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
}
// Clean up
destroy() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
this.token = null;
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Plugin Name: EveAI Chat Widget
* Description: Integrates the EveAI chat interface into your WordPress site.
* Version: 2.0.16
* Author: Your Company
* Text Domain: eveai-chat
* Domain Path: /languages
* Requires at least: 5.8
* Requires PHP: 7.4
*/
if (!defined('WPINC')) {
die;
}
// Define plugin constants
define('EVEAI_CHAT_VERSION', '2.0.16');
define('EVEAI_CHAT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('EVEAI_CHAT_PLUGIN_URL', plugin_dir_url(__FILE__));
// Autoloader
spl_autoload_register(function ($class) {
$prefix = 'EveAI\\Chat\\';
$base_dir = EVEAI_CHAT_PLUGIN_DIR . 'includes/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
});
// Load the main plugin class
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-plugin.php';
// Initialize the plugin
function run_eveai_chat() {
$plugin = \EveAI\Chat\Plugin::get_instance();
}
run_eveai_chat();

View File

@@ -0,0 +1,108 @@
<?php
namespace EveAI\Chat;
class Assets implements Loadable {
public function init() {
add_action('wp_enqueue_scripts', [$this, 'register_assets']);
add_action('wp_enqueue_scripts', [$this, 'maybe_load_assets']);
}
public function register_assets() {
// Register Socket.IO (from CDN)
wp_register_script(
'socket-io',
'https://cdn.socket.io/4.0.1/socket.io.min.js',
[],
'4.0.1',
true
);
// Register Marked library for markdown (from CDN)
wp_register_script(
'marked',
'https://cdn.jsdelivr.net/npm/marked/marked.min.js',
[],
'1.0.0',
true
);
// Register EveAI core scripts
wp_register_script(
'eveai-token-manager',
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-token-manager.js',
['socket-io'],
EVEAI_CHAT_VERSION,
true
);
wp_register_script(
'eveai-sdk',
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-sdk.js',
['eveai-token-manager'],
EVEAI_CHAT_VERSION,
true
);
wp_register_script(
'eveai-chat-widget',
EVEAI_CHAT_PLUGIN_URL . 'assets/js/eveai-chat-widget.js',
['eveai-sdk', 'marked'],
EVEAI_CHAT_VERSION,
true
);
// Register styles
wp_register_style(
'eveai-chat',
EVEAI_CHAT_PLUGIN_URL . 'assets/css/eveai-chat-style.css',
[],
EVEAI_CHAT_VERSION
);
wp_register_style(
'material-icons',
'https://fonts.googleapis.com/icon?family=Material+Icons',
[],
EVEAI_CHAT_VERSION
);
}
public function maybe_load_assets() {
global $post;
// Only load if shortcode is present
if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'eveai_chat')) {
$this->load_assets();
}
}
private function load_assets() {
// Enqueue all required scripts
wp_enqueue_script('socket-io');
wp_enqueue_script('marked');
wp_enqueue_script('eveai-token-manager');
wp_enqueue_script('eveai-sdk');
wp_enqueue_script('eveai-chat-widget');
// Enqueue styles
wp_enqueue_style('material-icons');
wp_enqueue_style('eveai-chat');
// Localize script with WordPress-specific data
wp_localize_script('eveai-sdk', 'eveaiWP', [
'nonce' => wp_create_nonce('wp_rest'),
'settings' => $this->get_public_settings()
]);
}
private function get_public_settings() {
$settings = get_option('eveai_chat_settings', []);
return [
'socket_url' => $settings['socket_url'] ?? 'http://localhost:5002',
'auth_url' => $settings['auth_url'] ?? 'http://localhost:5001',
'tenant_id' => $settings['tenant_id'] ?? '',
'wpBaseUrl' => rest_url(),
];
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace EveAI\Chat;
class CacheManager {
private const SETTINGS_CACHE_KEY = 'eveai_settings_cache';
private const CACHE_EXPIRATION = 3600; // 1 hour
public static function get_settings(): array {
$cached = wp_cache_get(self::SETTINGS_CACHE_KEY);
if ($cached !== false) {
return $cached;
}
$settings = get_option('eveai_chat_settings', []);
wp_cache_set(self::SETTINGS_CACHE_KEY, $settings, '', self::CACHE_EXPIRATION);
return $settings;
}
public static function clear_settings_cache(): void {
wp_cache_delete(self::SETTINGS_CACHE_KEY);
}
public static function maybe_clear_caches(): void {
if (defined('WP_CACHE') && WP_CACHE) {
wp_cache_flush();
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace EveAI\Chat;
class Plugin {
/**
* Plugin instance
*/
private static $instance = null;
/**
* Plugin components
*/
private $components = [];
/**
* Get plugin instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Initialize plugin
*/
private function __construct() {
$this->load_dependencies();
$this->init_components();
$this->register_hooks();
}
/**
* Load dependencies
*/
private function load_dependencies() {
// Core files
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/interface-loadable.php';
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-assets.php';
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-shortcode.php';
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-rest-controller.php';
require_once EVEAI_CHAT_PLUGIN_DIR . 'includes/class-security.php';
// Admin
if (is_admin()) {
require_once EVEAI_CHAT_PLUGIN_DIR . 'admin/class-admin.php';
}
}
/**
* Initialize components
*/
private function init_components() {
// Initialize REST controller
$this->components['rest'] = new RESTController();
// Initialize assets manager
$this->components['assets'] = new Assets();
// Initialize shortcode handler
$this->components['shortcode'] = new Shortcode();
// Initialize admin if in admin area
if (is_admin()) {
$this->components['admin'] = new Admin();
}
// Initialize all components
foreach ($this->components as $component) {
if ($component instanceof Loadable) {
$component->init();
}
}
}
/**
* Register WordPress hooks
*/
private function register_hooks() {
// Plugin activation/deactivation
register_activation_hook(EVEAI_CHAT_PLUGIN_DIR . 'eveai-chat.php', [$this, 'activate']);
register_deactivation_hook(EVEAI_CHAT_PLUGIN_DIR . 'eveai-chat.php', [$this, 'deactivate']);
// Load text domain
add_action('plugins_loaded', [$this, 'load_plugin_textdomain']);
}
/**
* Plugin activation
*/
public function activate() {
// Set default options if not exists
if (!get_option('eveai_chat_settings')) {
add_option('eveai_chat_settings', [
'auth_url' => 'https://api.askeveai.com',
'socket_url' => 'https://chat.askeveai.com',
'tenant_id' => '',
'api_key' => ''
]);
}
// Clear permalinks
flush_rewrite_rules();
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Clear any scheduled hooks, clean up temporary data, etc.
flush_rewrite_rules();
}
/**
* Load plugin textdomain
*/
public function load_plugin_textdomain() {
load_plugin_textdomain(
'eveai-chat',
false,
dirname(plugin_basename(EVEAI_CHAT_PLUGIN_DIR)) . '/languages/'
);
}
}

View File

@@ -0,0 +1,188 @@
<?php
namespace EveAI\Chat;
class RESTController implements Loadable {
const API_NAMESPACE = 'eveai/v1';
public function init() {
add_action('rest_api_init', [$this, 'register_routes']);
error_log('REST routes registered for EveAI Chat');
}
public function register_routes() {
register_rest_route(
self::API_NAMESPACE,
'/token',
[
'methods' => 'POST',
'callback' => [$this, 'get_token'],
'permission_callback' => [$this, 'verify_request'],
]
);
register_rest_route(
self::API_NAMESPACE,
'/verify',
[
'methods' => 'POST',
'callback' => [$this, 'verify_token'],
'permission_callback' => [$this, 'verify_request'],
]
);
register_rest_route(
self::API_NAMESPACE,
'/refresh',
[
'methods' => 'POST',
'callback' => [$this, 'refresh_token'],
'permission_callback' => [$this, 'verify_request'],
]
);
}
public function verify_request(\WP_REST_Request $request): bool {
// error_log('Verifying EveAI request: ' . print_r([
// 'route' => $request->get_route(),
// 'headers' => $request->get_headers(),
// 'params' => $request->get_params()
// ], true));
// Verify nonce
$nonce = $request->get_header('X-WP-Nonce');
if (!wp_verify_nonce($nonce, 'wp_rest')) {
error_log('EveAI nonce verification failed');
return false;
}
// Verify origin
$origin = $request->get_header('origin');
if (!$this->verify_origin($origin)) {
return false;
}
return true;
}
public function get_token(\WP_REST_Request $request) {
try {
$settings = get_option('eveai_chat_settings');
if (empty($settings['tenant_id']) || empty($settings['api_key'])) {
return new \WP_Error(
'configuration_error',
'EveAI Chat is not properly configured.',
['status' => 500]
);
}
$auth_url = rtrim($settings['auth_url'], '/');
$token_endpoint = '/api/v1/auth/token';
$full_url = $auth_url . $token_endpoint;
error_log('Attempting to get token from: ' . $full_url);
// Get decrypted API key
$api_key = Security::decrypt_api_key($settings['api_key']);
$response = wp_remote_post($full_url, [
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json'
],
'body' => json_encode([
'tenant_id' => $settings['tenant_id'],
'api_key' => $api_key
])
]);
error_log('EveAI API Response: ' . print_r($response, true));
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
error_log('Token response body: ' . print_r($body, true)); // Add this for debugging
if (!isset($body['access_token'])) {
throw new \Exception('No token in response');
}
return new \WP_REST_Response($body, 200);
} catch (\Exception $e) {
error_log('EveAI token error: ' . $e->getMessage());
return new \WP_Error(
'token_error',
$e->getMessage(),
['status' => 500]
);
}
}
public function verify_token(\WP_REST_Request $request) {
try {
$token = $request->get_header('Authorization');
if (!$token) {
throw new \Exception('No token provided');
}
$settings = get_option('eveai_chat_settings');
$response = wp_remote_post($settings['auth_url'] . '/auth/verify', [
'headers' => ['Authorization' => $token]
]);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
return new \WP_REST_Response($body, 200);
} catch (\Exception $e) {
return new \WP_Error(
'verify_error',
$e->getMessage(),
['status' => 401]
);
}
}
public function refresh_token(\WP_REST_Request $request) {
try {
$token = $request->get_header('Authorization');
if (!$token) {
throw new \Exception('No token provided');
}
$settings = get_option('eveai_chat_settings');
$response = wp_remote_post($settings['auth_url'] . '/auth/refresh', [
'headers' => ['Authorization' => $token]
]);
if (is_wp_error($response)) {
throw new \Exception($response->get_error_message());
}
$body = json_decode(wp_remote_retrieve_body($response), true);
return new \WP_REST_Response($body, 200);
} catch (\Exception $e) {
return new \WP_Error(
'refresh_error',
$e->getMessage(),
['status' => 401]
);
}
}
private function verify_origin($origin): bool {
if (empty($origin)) {
return false;
}
$site_url = parse_url(get_site_url(), PHP_URL_HOST);
$origin_host = parse_url($origin, PHP_URL_HOST);
return $origin_host === $site_url;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace EveAI\Chat;
class Security {
private static $encryption_method = 'aes-256-cbc';
public static function encrypt_api_key(string $key): string {
if (empty($key)) return '';
$salt = wp_salt('auth');
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$encryption_method));
$encrypted = openssl_encrypt(
$key,
self::$encryption_method,
$salt,
0,
$iv
);
return base64_encode($iv . $encrypted);
}
public static function decrypt_api_key(string $encrypted): string {
if (empty($encrypted)) return '';
$salt = wp_salt('auth');
$data = base64_decode($encrypted);
$iv_length = openssl_cipher_iv_length(self::$encryption_method);
$iv = substr($data, 0, $iv_length);
$encrypted_data = substr($data, $iv_length);
return openssl_decrypt(
$encrypted_data,
self::$encryption_method,
$salt,
0,
$iv
);
}
public static function generate_nonce(): string {
return wp_create_nonce('eveai_chat_nonce');
}
public static function verify_nonce(string $nonce): bool {
return wp_verify_nonce($nonce, 'eveai_chat_nonce');
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace EveAI\Chat;
class Shortcode implements Loadable {
public function init() {
add_shortcode('eveai_chat', [$this, 'render_chat']);
}
public function render_chat($atts) {
$settings = get_option('eveai_chat_settings');
error_log('Rendering chat with settings: ' . print_r($settings, true));
if (empty($settings['tenant_id']) || empty($settings['api_key'])) {
return '<div class="eveai-error">' .
esc_html__('EveAI Chat is not properly configured. Please check the admin settings.', 'eveai-chat') .
'</div>';
}
// Parse shortcode attributes
$atts = shortcode_atts([
'language' => 'en',
'languages' => 'en',
'specialist_id' => '1'
], $atts, 'eveai_chat');
// Generate unique container ID
$container_id = 'eveai-chat-' . uniqid();
ob_start();
?>
<div id="<?php echo esc_attr($container_id); ?>" class="eveai-chat-container"></div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
console.log('Initializing EveAI Chat');
const eveAI = new EveAI({
tenantId: <?php echo esc_js($settings['tenant_id']); ?>,
language: '<?php echo esc_js($atts['language']); ?>',
languages: '<?php echo esc_js($atts['languages']); ?>',
specialistId: '<?php echo esc_js($atts['specialist_id']); ?>',
socketUrl: '<?php echo esc_js($settings['socket_url']); ?>',
authUrl: '<?php echo esc_js($settings['auth_url']); ?>'
});
try {
await eveAI.initialize('<?php echo esc_js($container_id); ?>');
console.log('Chat initialized successfully');
} catch (error) {
console.error('Failed to initialize chat:', error);
document.getElementById('<?php echo esc_js($container_id); ?>').innerHTML =
'<div class="eveai-error">Failed to initialize chat. Please check console for details.</div>';
}
});
</script>
<?php
return ob_get_clean();
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace EveAI\Chat;
class RateLimiter {
private const RATE_LIMIT_KEY = 'eveai_rate_limit_';
private const MAX_REQUESTS = 60; // Maximum requests per window
private const WINDOW_SECONDS = 60; // Time window in seconds
public static function check_rate_limit($identifier): bool {
$key = self::RATE_LIMIT_KEY . $identifier;
$requests = get_transient($key);
if ($requests === false) {
set_transient($key, 1, self::WINDOW_SECONDS);
return true;
}
if ($requests >= self::MAX_REQUESTS) {
return false;
}
set_transient($key, $requests + 1, self::WINDOW_SECONDS);
return true;
}
public static function get_remaining_requests($identifier): int {
$key = self::RATE_LIMIT_KEY . $identifier;
$requests = get_transient($key);
if ($requests === false) {
return self::MAX_REQUESTS;
}
return max(0, self::MAX_REQUESTS - $requests);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace EveAI\Chat;
interface Loadable {
/**
* Initialize the component
*/
public function init();
}

View File

@@ -6,7 +6,6 @@ if (!defined('WP_UNINSTALL_PLUGIN')) {
// Delete plugin options
delete_option('eveai_chat_settings');
delete_option('eveai_chat_encryption_key');
// Clean up any additional options or data that your plugin may have created
// For example, if you've created any custom tables, you might want to drop them here
@@ -17,11 +16,7 @@ if (is_multisite()) {
$blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
foreach ($blog_ids as $blog_id) {
switch_to_blog($blog_id);
// Delete options for each site
delete_option('eveai_chat_settings');
delete_option('eveai_chat_encryption_key');
restore_current_blog();
}
}