- 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:
130
integrations/Wordpress/eveai-chat/admin/class-admin.php
Normal file
130
integrations/Wordpress/eveai-chat/admin/class-admin.php
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,294 @@
|
||||
/* eveai_chat.css */
|
||||
:root {
|
||||
--user-message-bg: #292929; /* Default user message background color */
|
||||
--bot-message-bg: #1e1e1e; /* Default bot message background color */
|
||||
--chat-bg: #1e1e1e; /* Default chat background color */
|
||||
--status-line-color: #e9e9e9; /* Color for the status line text */
|
||||
--status-line-bg: #1e1e1e; /* Background color for the status line */
|
||||
--status-line-height: 30px; /* Fixed height for the status line */
|
||||
|
||||
--algorithm-color-rag-tenant: #0f0; /* Green for RAG_TENANT */
|
||||
--algorithm-color-rag-wikipedia: #00f; /* Blue for RAG_WIKIPEDIA */
|
||||
--algorithm-color-rag-google: #ff0; /* Yellow for RAG_GOOGLE */
|
||||
--algorithm-color-llm: #800080; /* Purple for RAG_LLM */
|
||||
|
||||
/*--font-family: 'Arial, sans-serif'; !* Default font family *!*/
|
||||
--font-family: 'Segoe UI, Roboto, Cantarell, Noto Sans, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol';
|
||||
--font-color: #e9e9e9; /* Default font color */
|
||||
--user-message-font-color: #e9e9e9; /* User message font color */
|
||||
--bot-message-font-color: #e9e9e9; /* Bot message font color */
|
||||
--input-bg: #292929; /* Input background color */
|
||||
--input-border: #ccc; /* Input border color */
|
||||
--input-text-color: #e9e9e9; /* Input text color */
|
||||
--button-color: #007bff; /* Button text color */
|
||||
|
||||
/* Variables for hyperlink backgrounds */
|
||||
--link-bg: #1e1e1e; /* Default background color for hyperlinks */
|
||||
--link-hover-bg: #1e1e1e; /* Background color on hover for hyperlinks */
|
||||
--link-color: #dec981; /* Default text color for hyperlinks */
|
||||
--link-hover-color: #D68F53; /* Text color on hover for hyperlinks */
|
||||
|
||||
/* New scrollbar variables */
|
||||
--scrollbar-bg: #292929; /* Background color for the scrollbar track */
|
||||
--scrollbar-thumb: #4b4b4b; /* Color for the scrollbar thumb */
|
||||
--scrollbar-thumb-hover: #dec981; /* Color for the thumb on hover */
|
||||
--scrollbar-thumb-active: #D68F53; /* Color for the thumb when active (dragged) */
|
||||
|
||||
/* Thumb colors */
|
||||
--thumb-icon-outlined: #4b4b4b;
|
||||
--thumb-icon-filled: #e9e9e9;
|
||||
|
||||
/* Connection Status colors */
|
||||
--status-connected-color: #28a745; /* Green color for connected status */
|
||||
--status-disconnected-color: #ffc107; /* Orange color for disconnected status */
|
||||
}
|
||||
|
||||
/* Connection status styles */
|
||||
.connection-status-icon {
|
||||
vertical-align: middle;
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
color: var(--status-connected-color);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
color: var(--status-disconnected-color);
|
||||
}
|
||||
|
||||
/* Custom scrollbar styles */
|
||||
.messages-area::-webkit-scrollbar {
|
||||
width: 12px; /* Width of the scrollbar */
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-bg); /* Background color for the track */
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scrollbar-thumb); /* Color of the thumb */
|
||||
border-radius: 10px; /* Rounded corners for the thumb */
|
||||
border: 3px solid var(--scrollbar-bg); /* Space around the thumb */
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--scrollbar-thumb-hover); /* Color when hovering over the thumb */
|
||||
}
|
||||
|
||||
.messages-area::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--scrollbar-thumb-active); /* Color when active (dragging) */
|
||||
}
|
||||
|
||||
/* For Firefox */
|
||||
.messages-area {
|
||||
scrollbar-width: thin; /* Make scrollbar thinner */
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-bg); /* Thumb and track colors */
|
||||
}
|
||||
|
||||
/* General Styles */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 75vh;
|
||||
/*max-height: 100vh;*/
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: var(--chat-bg);
|
||||
font-family: var(--font-family); /* Apply the default font family */
|
||||
color: var(--font-color); /* Apply the default font color */
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
font-size: 0.7em;
|
||||
text-align: right;
|
||||
padding: 5px 20px 5px 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.messages-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background-color: var(--bot-message-bg);
|
||||
}
|
||||
|
||||
.message {
|
||||
max-width: 90%;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: auto;
|
||||
background-color: var(--user-message-bg);
|
||||
color: var(--user-message-font-color); /* Apply user message font color */
|
||||
}
|
||||
|
||||
.message.bot {
|
||||
background-color: var(--bot-message-bg);
|
||||
color: var(--bot-message-font-color); /* Apply bot message font color */
|
||||
}
|
||||
|
||||
.message-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Scoped styles for thumb icons */
|
||||
.thumb-icon.outlined {
|
||||
color: var(--thumb-icon-outlined); /* Color for outlined state */
|
||||
}
|
||||
|
||||
.thumb-icon.filled {
|
||||
color: var(--thumb-icon-filled); /* Color for filled state */
|
||||
}
|
||||
|
||||
/* Default styles for material icons */
|
||||
.material-icons {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.question-area {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--user-message-bg);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.language-select-container {
|
||||
width: 100%;
|
||||
margin-bottom: 10px; /* Spacing between the dropdown and the textarea */
|
||||
}
|
||||
|
||||
.language-select {
|
||||
width: 100%;
|
||||
margin-bottom: 5px; /* Space between the dropdown and the send button */
|
||||
padding: 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--input-border);
|
||||
background-color: var(--input-bg);
|
||||
color: var(--input-text-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.question-area textarea {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
color: var(--input-text-color);
|
||||
font-family: var(--font-family); /* Apply the default font family */
|
||||
font-size: 1rem;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-right: 10px; /* Space between textarea and right-side container */
|
||||
}
|
||||
|
||||
.right-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.question-area button {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--button-color);
|
||||
}
|
||||
|
||||
/* Styles for the send icon */
|
||||
.send-icon {
|
||||
font-size: 24px;
|
||||
color: var(--button-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.send-icon.disabled {
|
||||
color: grey; /* Color for the disabled state */
|
||||
cursor: not-allowed; /* Change cursor to indicate disabled state */
|
||||
}
|
||||
|
||||
/* New CSS for the status-line */
|
||||
.status-line {
|
||||
height: var(--status-line-height); /* Fixed height for the status line */
|
||||
padding: 5px 10px;
|
||||
background-color: var(--status-line-bg); /* Background color */
|
||||
color: var(--status-line-color); /* Text color */
|
||||
font-size: 0.9rem; /* Slightly smaller font size */
|
||||
text-align: center; /* Centered text */
|
||||
border-top: 1px solid #ccc; /* Subtle top border */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Algorithm-specific colors for fingerprint icon */
|
||||
.fingerprint-rag-tenant {
|
||||
color: var(--algorithm-color-rag-tenant);
|
||||
}
|
||||
|
||||
.fingerprint-rag-wikipedia {
|
||||
color: var(--algorithm-color-rag-wikipedia);
|
||||
}
|
||||
|
||||
.fingerprint-rag-google {
|
||||
color: var(--algorithm-color-rag-google);
|
||||
}
|
||||
|
||||
.fingerprint-llm {
|
||||
color: var(--algorithm-color-llm);
|
||||
}
|
||||
|
||||
/* Styling for citation links */
|
||||
.citations a {
|
||||
background-color: var(--link-bg); /* Apply default background color */
|
||||
color: var(--link-color); /* Apply default link color */
|
||||
padding: 2px 4px; /* Add padding for better appearance */
|
||||
border-radius: 3px; /* Add slight rounding for a modern look */
|
||||
text-decoration: none; /* Remove default underline */
|
||||
transition: background-color 0.3s, color 0.3s; /* Smooth transition for hover effects */
|
||||
}
|
||||
|
||||
.citations a:hover {
|
||||
background-color: var(--link-hover-bg); /* Background color on hover */
|
||||
color: var(--link-hover-color); /* Text color on hover */
|
||||
}
|
||||
|
||||
/* Media queries for responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.chat-container {
|
||||
max-width: 90%; /* Reduce max width on smaller screens */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-container {
|
||||
max-width: 95%; /* Further reduce max width on very small screens */
|
||||
}
|
||||
|
||||
.question-area input {
|
||||
font-size: 0.9rem; /* Adjust input font size for smaller screens */
|
||||
}
|
||||
|
||||
.status-line {
|
||||
font-size: 0.8rem; /* Adjust status line font size for smaller screens */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
696
integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js
Normal file
696
integrations/Wordpress/eveai-chat/assets/js/eveai-chat-widget.js
Normal file
@@ -0,0 +1,696 @@
|
||||
class EveAIChatWidget extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
'tenant-id',
|
||||
'session-token',
|
||||
'language',
|
||||
'languages',
|
||||
'specialist-id',
|
||||
'server-url'
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Networking attributes
|
||||
this.socket = null; // Initialize socket to null
|
||||
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('Chat Widget Connected');
|
||||
this.innerHTML = this.getTemplate();
|
||||
this.setupElements()
|
||||
this.populateLanguageDropdown()
|
||||
this.addEventListeners()
|
||||
|
||||
if (this.areAllAttributesSet()) {
|
||||
console.log('All attributes are set, initializing socket');
|
||||
this.initializeSocket();
|
||||
} else {
|
||||
console.warn('Not all required attributes are set yet');
|
||||
}
|
||||
}
|
||||
|
||||
setupElements() {
|
||||
// Centralizes element querying
|
||||
this.messagesArea = this.querySelector('.messages-area');
|
||||
this.questionInput = this.querySelector('.question-area textarea');
|
||||
this.sendButton = this.querySelector('.send-icon');
|
||||
this.languageSelect = this.querySelector('.language-select');
|
||||
this.statusLine = this.querySelector('.status-line');
|
||||
this.statusMessage = this.querySelector('.status-message');
|
||||
this.connectionStatusIcon = this.querySelector('.connection-status-icon');
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
// Centralized event listener setup
|
||||
this.sendButton.addEventListener('click', this.handleSendMessage);
|
||||
this.questionInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
this.handleSendMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
populateLanguageDropdown() {
|
||||
// Clear existing options
|
||||
this.languageSelect.innerHTML = '';
|
||||
console.log(`languages for options: ${this.languages}`)
|
||||
|
||||
// Populate with new options
|
||||
this.languages.forEach(lang => {
|
||||
const option = document.createElement('option');
|
||||
option.value = lang;
|
||||
option.textContent = lang.toUpperCase();
|
||||
if (lang === this.currentLanguage) {
|
||||
option.selected = true;
|
||||
}
|
||||
console.log(`Adding option for language: ${lang}`)
|
||||
this.languageSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Add event listener for language change
|
||||
this.languageSelect.addEventListener('change', (e) => {
|
||||
this.currentLanguage = e.target.value;
|
||||
// You might want to emit an event or update the backend about the language change
|
||||
});
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
|
||||
|
||||
// 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.language = this.getAttribute('language');
|
||||
const languageAttr = this.getAttribute('languages');
|
||||
this.languages = languageAttr ? languageAttr.split(',') : [];
|
||||
this.serverUrl = this.getAttribute('server-url');
|
||||
this.currentLanguage = this.language;
|
||||
this.specialistId = this.getAttribute('specialist-id');
|
||||
console.log('Updated attributes:', {
|
||||
tenantId: this.tenantId,
|
||||
sessionToken: this.sessionToken,
|
||||
language: this.language,
|
||||
currentLanguage: this.currentLanguage,
|
||||
languages: this.languages,
|
||||
serverUrl: this.serverUrl,
|
||||
specialistId: this.specialistId
|
||||
});
|
||||
}
|
||||
|
||||
areAllAttributesSet() {
|
||||
console.log('Checking if all attributes are set:', {
|
||||
tenantId: this.tenantId,
|
||||
sessionToken: this.sessionToken,
|
||||
language: this.language,
|
||||
languages: this.languages,
|
||||
serverUrl: this.serverUrl,
|
||||
specialistId: this.specialistId
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this.sessionToken) {
|
||||
console.error('Cannot initialize socket without session token');
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket = io(this.serverUrl, {
|
||||
path: '/socket.io/',
|
||||
transports: ['websocket'],
|
||||
query: { // Change from auth to query
|
||||
token: this.sessionToken
|
||||
},
|
||||
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();
|
||||
}
|
||||
|
||||
setupSocketEventHandlers() {
|
||||
// connect handler --------------------------------------------------------
|
||||
this.socket.on('connect', (data) => {
|
||||
console.log('Socket connected OK');
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
// authenticated handler --------------------------------------------------
|
||||
this.socket.on('authenticated', (data) => {
|
||||
console.log('Authenticated event received');
|
||||
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);
|
||||
this.setStatusMessage('Connection Error: EveAI Chat Widget needs further configuration by site administrator.');
|
||||
this.updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
// connect-timeout handler ------------------------------------------------
|
||||
this.socket.on('connect_timeout', () => {
|
||||
console.error('Socket connection timeout');
|
||||
this.setStatusMessage('Connection Timeout: EveAI Chat Widget needs further configuration by site administrator.');
|
||||
this.updateConnectionStatus(false);
|
||||
});
|
||||
|
||||
// disconnect handler -----------------------------------------------------
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
console.log('Socket disconnected: ', reason);
|
||||
if (reason === 'io server disconnect') {
|
||||
// Server disconnected the socket
|
||||
this.socket.connect(); // Attempt to reconnect
|
||||
}
|
||||
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', (attemptNumber) => {
|
||||
console.log(`Reconnection attempt ${attemptNumber}`);
|
||||
this.setStatusMessage(`Reconnecting... (Attempt ${attemptNumber})`);
|
||||
this.reconnectAttempts = attemptNumber;
|
||||
});
|
||||
|
||||
// reconnect handler ------------------------------------------------------
|
||||
this.socket.on('reconnect', () => {
|
||||
console.log('Successfully reconnected to the server');
|
||||
this.setStatusMessage('Reconnected to EveAI.');
|
||||
this.updateConnectionStatus(true);
|
||||
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 && 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) {
|
||||
this.statusMessage.textContent = message;
|
||||
}
|
||||
|
||||
updateConnectionStatus(isConnected) {
|
||||
if (isConnected) {
|
||||
this.connectionStatusIcon.textContent = 'link';
|
||||
this.connectionStatusIcon.classList.remove('status-disconnected');
|
||||
this.connectionStatusIcon.classList.add('status-connected');
|
||||
} else {
|
||||
this.connectionStatusIcon.textContent = 'link_off';
|
||||
this.connectionStatusIcon.classList.remove('status-connected');
|
||||
this.connectionStatusIcon.classList.add('status-disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat(); // Clear any existing interval
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.socket && this.socket.connected) {
|
||||
this.socket.emit('heartbeat');
|
||||
this.idleTime += 30000;
|
||||
if (this.idleTime >= this.maxConnectionIdleTime) {
|
||||
this.socket.disconnect();
|
||||
this.setStatusMessage('Disconnected due to inactivity.');
|
||||
this.updateConnectionStatus(false);
|
||||
this.stopHeartbeat();
|
||||
}
|
||||
}
|
||||
}, 30000); // Send a heartbeat every 30 seconds
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
if (!this.statusMessage.textContent) {
|
||||
this.statusMessage.textContent = 'Processing...';
|
||||
} else {
|
||||
this.statusMessage.textContent += '.'; // Append a dot
|
||||
}
|
||||
}
|
||||
|
||||
clearProgress() {
|
||||
this.statusMessage.textContent = '';
|
||||
this.toggleSendButton(false); // Re-enable and revert send button to outlined version
|
||||
}
|
||||
|
||||
checkTaskStatus(taskId) {
|
||||
this.updateProgress();
|
||||
this.socket.emit('check_task_status', { task_id: taskId });
|
||||
}
|
||||
|
||||
getTemplate() {
|
||||
return `
|
||||
<div class="chat-container">
|
||||
<div class="messages-area"></div>
|
||||
<div class="disclaimer">Evie can make mistakes. Please double-check responses.</div>
|
||||
<div class="question-area">
|
||||
<textarea placeholder="Type your message here..." rows="3"></textarea>
|
||||
<div class="right-side">
|
||||
<select class="language-select"></select>
|
||||
<i class="material-icons send-icon outlined">send</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-line">
|
||||
<i class="material-icons connection-status-icon">link_off</i>
|
||||
<span class="status-message"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
addUserMessage(text) {
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('message', 'user');
|
||||
message.innerHTML = `<p>${text}</p>`;
|
||||
this.messagesArea.appendChild(message);
|
||||
this.messagesArea.scrollTop = this.messagesArea.scrollHeight;
|
||||
}
|
||||
|
||||
handleFeedback(feedback, interactionId) {
|
||||
// Send feedback to the backend
|
||||
console.log('handleFeedback called');
|
||||
if (!this.socket) {
|
||||
console.error('Socket is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.validateRoom()) {
|
||||
console.log("No valid room to handle feedback")
|
||||
return;
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
addBotMessage(text, interactionId, algorithm = 'default', citations = []) {
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('message', 'bot');
|
||||
|
||||
let content = marked.parse(text);
|
||||
let citationsHtml = citations.map(url => `<a href="${url}" target="_blank">${url}</a>`).join('<br>');
|
||||
|
||||
let algorithmClass;
|
||||
switch (algorithm) {
|
||||
case 'RAG_TENANT':
|
||||
algorithmClass = 'fingerprint-rag-tenant';
|
||||
break;
|
||||
case 'RAG_WIKIPEDIA':
|
||||
algorithmClass = 'fingerprint-rag-wikipedia';
|
||||
break;
|
||||
case 'RAG_GOOGLE':
|
||||
algorithmClass = 'fingerprint-rag-google';
|
||||
break;
|
||||
case 'LLM':
|
||||
algorithmClass = 'fingerprint-llm';
|
||||
break;
|
||||
default:
|
||||
algorithmClass = '';
|
||||
}
|
||||
|
||||
message.innerHTML = `
|
||||
<p>${content}</p>
|
||||
${citationsHtml ? `<p class="citations">${citationsHtml}</p>` : ''}
|
||||
<div class="message-icons">
|
||||
<i class="material-icons ${algorithmClass}">fingerprint</i>
|
||||
<i class="material-icons thumb-icon outlined" data-feedback="up" data-interaction-id="${interactionId}">thumb_up_off_alt</i>
|
||||
<i class="material-icons thumb-icon outlined" data-feedback="down" data-interaction-id="${interactionId}">thumb_down_off_alt</i>
|
||||
</div>
|
||||
`;
|
||||
this.messagesArea.appendChild(message);
|
||||
|
||||
// Add event listeners for feedback buttons
|
||||
const thumbsUp = message.querySelector('i[data-feedback="up"]');
|
||||
const thumbsDown = message.querySelector('i[data-feedback="down"]');
|
||||
thumbsUp.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'up', interactionId));
|
||||
thumbsDown.addEventListener('click', () => this.toggleFeedback(thumbsUp, thumbsDown, 'down', interactionId));
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
// Send feedback to the backend
|
||||
this.handleFeedback(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);
|
||||
this.questionInput.value = '';
|
||||
this.sendMessageToBackend(message);
|
||||
this.toggleSendButton(true); // Disable and change send button to filled version
|
||||
}
|
||||
}
|
||||
|
||||
startTaskCheck(taskId) {
|
||||
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) {
|
||||
if (data.status === 'pending') {
|
||||
this.updateProgress();
|
||||
setTimeout(() => this.startTaskCheck(data.taskId), 1000);
|
||||
} else if (data.status === 'success') {
|
||||
if (data.results) {
|
||||
this.addBotMessage(
|
||||
data.results.answer,
|
||||
data.interaction_id,
|
||||
'RAG_TENANT',
|
||||
data.results.citations || []
|
||||
);
|
||||
} else {
|
||||
console.error('Missing results in task status response:', data);
|
||||
}
|
||||
this.clearProgress();
|
||||
} else {
|
||||
console.error('Task error:', data);
|
||||
this.setStatusMessage('Failed to process message.');
|
||||
}
|
||||
}
|
||||
sendMessageToBackend(message) {
|
||||
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 = {
|
||||
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);
|
||||
this.socket.emit('user_message', messageData);
|
||||
this.setStatusMessage('Processing started ...');
|
||||
}
|
||||
|
||||
toggleSendButton(isProcessing) {
|
||||
if (isProcessing) {
|
||||
this.sendButton.textContent = 'send'; // Filled send icon
|
||||
this.sendButton.classList.remove('outlined');
|
||||
this.sendButton.classList.add('filled');
|
||||
this.sendButton.classList.add('disabled'); // Add disabled class for styling
|
||||
this.sendButton.style.pointerEvents = 'none'; // Disable click events
|
||||
} else {
|
||||
this.sendButton.textContent = 'send'; // Outlined send icon
|
||||
this.sendButton.classList.add('outlined');
|
||||
this.sendButton.classList.remove('filled');
|
||||
this.sendButton.classList.remove('disabled'); // Remove disabled class
|
||||
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);
|
||||
|
||||
167
integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
Normal file
167
integrations/Wordpress/eveai-chat/assets/js/eveai-sdk.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
48
integrations/Wordpress/eveai-chat/eveai-chat.php
Normal file
48
integrations/Wordpress/eveai-chat/eveai-chat.php
Normal 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();
|
||||
108
integrations/Wordpress/eveai-chat/includes/class-assets.php
Normal file
108
integrations/Wordpress/eveai-chat/includes/class-assets.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
125
integrations/Wordpress/eveai-chat/includes/class-plugin.php
Normal file
125
integrations/Wordpress/eveai-chat/includes/class-plugin.php
Normal 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/'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
namespace EveAI\Chat;
|
||||
|
||||
interface Loadable {
|
||||
/**
|
||||
* Initialize the component
|
||||
*/
|
||||
public function init();
|
||||
}
|
||||
22
integrations/Wordpress/eveai-chat/uninstall.php
Normal file
22
integrations/Wordpress/eveai-chat/uninstall.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
// If uninstall not called from WordPress, exit
|
||||
if (!defined('WP_UNINSTALL_PLUGIN')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete plugin options
|
||||
delete_option('eveai_chat_settings');
|
||||
|
||||
// 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
|
||||
|
||||
// If using multisite, you might want to loop through all sites
|
||||
if (is_multisite()) {
|
||||
global $wpdb;
|
||||
$blog_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs");
|
||||
foreach ($blog_ids as $blog_id) {
|
||||
switch_to_blog($blog_id);
|
||||
delete_option('eveai_chat_settings');
|
||||
restore_current_blog();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user