diff --git a/common/utils/middleware.py b/common/utils/middleware.py
index d1c3503..e14a497 100644
--- a/common/utils/middleware.py
+++ b/common/utils/middleware.py
@@ -4,7 +4,8 @@ for handling tenant requests
"""
from flask_security import current_user
-from flask import session, current_app
+from flask import session, current_app, redirect
+from common.utils.nginx_utils import prefixed_url_for
from .database import Database
@@ -15,6 +16,10 @@ def mw_before_request():
switch tenant schema
"""
+ if 'tenant' not in session:
+ current_app.logger.warning('No tenant defined in session')
+ return redirect(prefixed_url_for('security_bp.login'))
+
tenant_id = session['tenant']['id']
if not tenant_id:
raise Exception('Cannot switch schema for tenant: no tenant defined in session')
diff --git a/config/config.py b/config/config.py
index b7e15b2..d37db88 100644
--- a/config/config.py
+++ b/config/config.py
@@ -13,8 +13,11 @@ class Config(object):
SECRET_KEY = environ.get('SECRET_KEY')
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
+ SESSION_KEY_PREFIX = f'{environ.get('COMPONENT_NAME')}_'
WTF_CSRF_ENABLED = True
+ WTF_CSRF_TIME_LIMIT = None
+ WTF_CSRF_SSL_STRICT = False # Set to True if using HTTPS
# flask-security-too settings
# SECURITY_URL_PREFIX = '/admin'
@@ -31,7 +34,7 @@ class Config(object):
# SECURITY_BLUEPRINT_NAME = 'security_bp'
SECURITY_PASSWORD_SALT = environ.get('SECURITY_PASSWORD_SALT')
REMEMBER_COOKIE_SAMESITE = 'strict'
- SESSION_COOKIE_SAMESITE = 'strict'
+ SESSION_COOKIE_SAMESITE = 'Lax'
SECURITY_CONFIRMABLE = True
SECURITY_TRACKABLE = True
SECURITY_PASSWORD_COMPLEXITY_CHECKER = 'zxcvbn'
@@ -93,7 +96,7 @@ class Config(object):
# Session Settings
SESSION_TYPE = 'redis'
- SESSION_PERMANENT = False
+ SESSION_PERMANENT = True
SESSION_USE_SIGNER = True
PERMANENT_SESSION_LIFETIME = timedelta(minutes=60)
SESSION_REFRESH_EACH_REQUEST = True
@@ -200,6 +203,11 @@ class ProdConfig(Config):
FLASK_DEBUG = False
EXPLAIN_TEMPLATE_LOADING = False
+ # SESSION SETTINGS
+ SESSION_COOKIE_SECURE = True
+
+ WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS
+
# Database Settings
DB_HOST = environ.get('DB_HOST')
DB_USER = environ.get('DB_USER')
diff --git a/config/logging_config.py b/config/logging_config.py
index 00ac088..790351e 100644
--- a/config/logging_config.py
+++ b/config/logging_config.py
@@ -117,11 +117,11 @@ LOGGING = {
'formatters': {
'standard': {
'format': '%(asctime)s [%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] '
- '[Thread: %(threadName)s] [Host: %(hostname)s]: %(message)s'
+ '[Thread: %(threadName)s]: %(message)s'
},
'graylog': {
'format': '[%(levelname)s] %(name)s (%(component)s) [%(module)s:%(lineno)d in %(funcName)s] '
- '[Thread: %(threadName)s] [Host: %(hostname)s]: %(message)s',
+ '[Thread: %(threadName)s]: %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
},
},
diff --git a/eveai_app/__init__.py b/eveai_app/__init__.py
index ed90506..630f8d9 100644
--- a/eveai_app/__init__.py
+++ b/eveai_app/__init__.py
@@ -53,6 +53,10 @@ def create_app(config_file=None):
register_extensions(app)
+ # Configure CSRF protection
+ app.config['WTF_CSRF_CHECK_DEFAULT'] = False # Disable global CSRF protection
+ app.config['WTF_CSRF_TIME_LIMIT'] = None # Remove time limit for CSRF tokens
+
app.celery = make_celery(app.name, app.config)
init_celery(app.celery, app)
@@ -88,6 +92,12 @@ def create_app(config_file=None):
}
return jsonify(response), 500
+ @app.before_request
+ def before_request():
+ # app.logger.debug(f"Before request - Session ID: {session.sid}")
+ app.logger.debug(f"Before request - Session data: {session}")
+ app.logger.debug(f"Before request - Request headers: {request.headers}")
+
# Register API
register_api(app)
diff --git a/eveai_app/errors.py b/eveai_app/errors.py
index 2d9279e..4c6f0ec 100644
--- a/eveai_app/errors.py
+++ b/eveai_app/errors.py
@@ -27,9 +27,18 @@ def access_forbidden(error):
return render_template('error/403.html')
+def key_error_handler(error):
+ # Check if the KeyError is specifically for 'tenant'
+ if str(error) == "'tenant'":
+ return redirect(prefixed_url_for('security.login'))
+ # For other KeyErrors, you might want to log the error and return a generic error page
+ return render_template('error/generic.html', error_message="An unexpected error occurred"), 500
+
+
def register_error_handlers(app):
app.register_error_handler(404, not_found_error)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(401, not_authorised_error)
app.register_error_handler(403, not_authorised_error)
+ app.register_error_handler(KeyError, key_error_handler)
diff --git a/eveai_app/templates/error/generic.html b/eveai_app/templates/error/generic.html
new file mode 100644
index 0000000..2c7bb71
--- /dev/null
+++ b/eveai_app/templates/error/generic.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% block title %}Unexpected Error{% endblock %}
+
+{% block content_title %}Internal Server error{% endblock %}
+{% block content_description %}Something unexpected happened! The administrator has been notified.{% endblock %}
+{% block content %}
+
Return home
+{% endblock %}
\ No newline at end of file
diff --git a/eveai_app/templates/security/login_user.html b/eveai_app/templates/security/login_user.html
index 7433c94..cf9e117 100644
--- a/eveai_app/templates/security/login_user.html
+++ b/eveai_app/templates/security/login_user.html
@@ -20,6 +20,13 @@
{# #}
{{ login_user_form.submit() }}
+
+
+{# #}
+{#
Debugging Information:
#}
+{#
CSRF Token: {{ login_user_form.csrf_token.current_token }}
#}
+{#
Session ID: {{ session.sid }}
#}
+{#
#}
{% endblock %}
{% block content_footer %}
First time here? Forgot your password?
diff --git a/eveai_app/views/basic_views.py b/eveai_app/views/basic_views.py
index 979c1cf..ca15454 100644
--- a/eveai_app/views/basic_views.py
+++ b/eveai_app/views/basic_views.py
@@ -1,5 +1,6 @@
from flask import request, render_template, Blueprint, session, current_app, jsonify
from flask_security import roles_required, roles_accepted
+from flask_wtf.csrf import generate_csrf
from .basic_forms import SessionDefaultsForm
@@ -59,3 +60,14 @@ def set_user_timezone():
def health():
return jsonify({'status': 'ok'}), 200
+
+@basic_bp.route('/check_csrf', methods=['GET'])
+def check_csrf():
+ csrf_token = generate_csrf()
+ return jsonify({
+ 'csrf_token_in_session': session.get('csrf_token'),
+ 'generated_csrf_token': csrf_token,
+ 'session_id': session.sid if hasattr(session, 'sid') else None,
+ 'session_data': dict(session)
+ })
+
diff --git a/eveai_app/views/security_views.py b/eveai_app/views/security_views.py
index 23c965d..e0ae75f 100644
--- a/eveai_app/views/security_views.py
+++ b/eveai_app/views/security_views.py
@@ -3,6 +3,7 @@ from flask import Blueprint, render_template, redirect, request, flash, current_
from flask_security import current_user, login_required, login_user, logout_user
from flask_security.utils import verify_and_update_password, get_message, do_flash, config_value, hash_password
from flask_security.forms import LoginForm
+from flask_wtf.csrf import CSRFError, generate_csrf
from urllib.parse import urlparse
from datetime import datetime as dt, timezone as tz
@@ -45,27 +46,41 @@ def login():
form = LoginForm()
- if form.validate_on_submit():
- current_app.logger.debug(f'Validating login form: {form.email.data}')
- user = User.query.filter_by(email=form.email.data).first()
- if user is None or not verify_and_update_password(form.password.data, user):
- flash('Invalid username or password', 'danger')
+ if request.method == 'POST':
+ current_app.logger.debug(f"Starting login procedure for {form.email.data}")
+ try:
+ if form.validate_on_submit():
+ user = User.query.filter_by(email=form.email.data).first()
+ if user is None or not verify_and_update_password(form.password.data, user):
+ flash('Invalid username or password', 'danger')
+ current_app.logger.debug(f'Failed to login user')
+ return redirect(prefixed_url_for('security_bp.login'))
+
+ if login_user(user):
+ current_app.logger.info(f'Login successful! Current User is {current_user.email}')
+ db.session.commit()
+ if current_user.has_roles('Super User'):
+ return redirect(prefixed_url_for('user_bp.select_tenant'))
+ else:
+ return redirect(prefixed_url_for('user_bp.tenant_overview'))
+ else:
+ flash('Invalid username or password', 'danger')
+ current_app.logger.debug(f'Failed to login user {user.email}')
+ abort(401)
+ else:
+ current_app.logger.debug(f'Invalid login form: {form.errors}')
+
+ except CSRFError:
+ current_app.logger.warning('CSRF token mismatch during login attempt')
+ flash('Your session has expired. Please try logging in again.', 'danger')
return redirect(prefixed_url_for('security_bp.login'))
- if login_user(user):
- current_app.logger.info(f'Login successful! Current User is {current_user.email}')
- db.session.commit()
- if current_user.has_roles('Super User'):
- return redirect(prefixed_url_for('user_bp.select_tenant'))
- else:
- return redirect(prefixed_url_for('user_bp.tenant_overview'))
- else:
- flash('Invalid username or password', 'danger')
- current_app.logger.debug(f'Failed to login user {user.email}')
- abort(401)
- else:
- current_app.logger.debug(f'Invalid login form: {form.errors}')
+ if request.method == 'GET':
+ csrf_token = generate_csrf()
+ current_app.logger.debug(f'Generated new CSRF token: {csrf_token}')
+ # current_app.logger.debug(f"Login route completed - Session ID: {session.sid}")
+ current_app.logger.debug(f"Login route completed - Session data: {session}")
return render_template('security/login_user.html', login_user_form=form)
diff --git a/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css b/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css
new file mode 100644
index 0000000..ba3fae8
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat-widget/css/eveai-chat-style.css
@@ -0,0 +1,256 @@
+/* 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: 'ui-sans-serif, -apple-system, system-ui, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, Helvetica, Apple Color Emoji, Arial, 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: 99vh;
+ /*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 */
+}
+
+.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;
+}
+
+.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 {
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ background-color: var(--user-message-bg);
+}
+
+.question-area input {
+ flex: 1;
+ border: none;
+ padding: 10px;
+ border-radius: 15px;
+ margin-right: 10px;
+ background-color: var(--input-bg); /* Apply input background color */
+ border: 1px solid var(--input-border); /* Apply input border color */
+ color: var(--input-text-color); /* Apply input text color */
+}
+
+.question-area button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: var(--button-color);
+}
+
+/* Styles for the send icon */
+.send-icon {
+ font-size: 24px; /* Size of the icon */
+ color: var(--button-color); /* Color of the icon */
+}
+
+.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 */
+ }
+}
+
+
+
diff --git a/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php b/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php
new file mode 100644
index 0000000..3f866d7
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat-widget/eveai-chat_plugin.php
@@ -0,0 +1,118 @@
+";
+ $output .= "";
+
+ return $output;
+}
+add_shortcode('eveai_chat', 'eveai_chat_shortcode');
+
+// Add admin menu
+function eveai_chat_admin_menu() {
+ add_options_page('EveAI Chat Settings', 'EveAI Chat', 'manage_options', 'eveai-chat-settings', 'eveai_chat_settings_page');
+}
+add_action('admin_menu', 'eveai_chat_admin_menu');
+
+// Settings page
+function eveai_chat_settings_page() {
+ ?>
+
+
EveAI Chat Settings
+
+
+ Enter your EveAI Chat configuration details below:';
+}
+
+function eveai_chat_tenant_id_input() {
+ $options = get_option('eveai_chat_options');
+ echo "";
+}
+
+function eveai_chat_api_key_input() {
+ $options = get_option('eveai_chat_options');
+ echo "";
+}
+
+function eveai_chat_domain_input() {
+ $options = get_option('eveai_chat_options');
+ echo "";
+}
+
+function eveai_chat_language_input() {
+ $options = get_option('eveai_chat_options');
+ echo "";
+}
+
+function eveai_chat_options_validate($input) {
+ $new_input = array();
+ $new_input['tenant_id'] = sanitize_text_field($input['tenant_id']);
+ $new_input['api_key'] = sanitize_text_field($input['api_key']);
+ $new_input['domain'] = esc_url_raw($input['domain']);
+ $new_input['language'] = sanitize_text_field($input['language']);
+ return $new_input;
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js b/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js
new file mode 100644
index 0000000..8878906
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat-widget/js/eveai-chat-widget.js
@@ -0,0 +1,402 @@
+class EveAIChatWidget extends HTMLElement {
+ static get observedAttributes() {
+ return ['tenant-id', 'api-key', 'domain', 'language'];
+ }
+
+ constructor() {
+ super();
+ this.socket = null; // Initialize socket to null
+ this.attributesSet = false; // Flag to check if all attributes are set
+ this.jwtToken = null; // Initialize jwtToken to null
+ 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 hours in milliseconds
+ console.log('EveAIChatWidget constructor called');
+ }
+
+ connectedCallback() {
+ console.log('connectedCallback called');
+ this.innerHTML = this.getTemplate();
+ this.messagesArea = this.querySelector('.messages-area');
+ this.questionInput = this.querySelector('.question-area input');
+ this.sendButton = this.querySelector('.send-icon');
+ this.statusLine = this.querySelector('.status-line');
+ this.statusMessage = this.querySelector('.status-message');
+ this.connectionStatusIcon = this.querySelector('.connection-status-icon');
+
+ this.sendButton.addEventListener('click', () => this.handleSendMessage());
+ this.questionInput.addEventListener('keydown', (event) => {
+ if (event.key === 'Enter') {
+ this.handleSendMessage();
+ }
+ });
+
+ if (this.areAllAttributesSet() && !this.socket) {
+ console.log('Attributes already set in connectedCallback, initializing socket');
+ this.initializeSocket();
+ }
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ console.log(`attributeChangedCallback called: ${name} changed from ${oldValue} to ${newValue}`);
+ this.updateAttributes();
+
+ if (this.areAllAttributesSet() && !this.socket) {
+ console.log('All attributes set in attributeChangedCallback, initializing socket');
+ this.attributesSet = true;
+ this.initializeSocket();
+ }
+ }
+
+ updateAttributes() {
+ this.tenantId = this.getAttribute('tenant-id');
+ this.apiKey = this.getAttribute('api-key');
+ this.domain = this.getAttribute('domain');
+ this.language = this.getAttribute('language');
+ console.log('Updated attributes:', {
+ tenantId: this.tenantId,
+ apiKey: this.apiKey,
+ domain: this.domain,
+ language: this.language
+ });
+ }
+
+ areAllAttributesSet() {
+ const tenantId = this.getAttribute('tenant-id');
+ const apiKey = this.getAttribute('api-key');
+ const domain = this.getAttribute('domain');
+ const language = this.getAttribute('language');
+ console.log('Checking if all attributes are set:', {
+ tenantId,
+ apiKey,
+ domain,
+ language
+ });
+ return tenantId && apiKey && domain && language;
+ }
+
+ initializeSocket() {
+ if (this.socket) {
+ console.log('Socket already initialized');
+ return;
+ }
+ if (!this.domain || this.domain === 'null') {
+ console.error('Domain attribute is missing or invalid');
+ this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.');
+ return;
+ }
+ console.log(`Initializing socket connection to ${this.domain}`);
+
+ // Ensure apiKey is passed in the query parameters
+ this.socket = io(this.domain, {
+ path: '/chat/socket.io/',
+ transports: ['websocket', 'polling'],
+ query: {
+ tenantId: this.tenantId,
+ apiKey: this.apiKey // Ensure apiKey is included here
+ },
+ auth: {
+ token: 'Bearer ' + this.apiKey // Ensure token is included here
+ },
+ reconnectionAttempts: Infinity, // Infinite reconnection attempts
+ reconnectionDelay: 5000, // Delay between reconnections
+ timeout: 20000 // Connection timeout
+ });
+
+ console.log(`Finished initializing socket connection to ${this.domain}`);
+
+ this.socket.on('connect', (data) => {
+ console.log('Socket connected OK');
+ this.setStatusMessage('Connected to EveAI.');
+ this.updateConnectionStatus(true);
+ this.startHeartbeat();
+ });
+
+ this.socket.on('authenticated', (data) => {
+ console.log('Authenticated event received: ', data);
+ this.setStatusMessage('Authenticated.');
+ if (data.token) {
+ this.jwtToken = data.token; // Store the JWT token received from the server
+ }
+ });
+
+ this.socket.on('connect_error', (err) => {
+ console.error('Socket connection error:', err);
+ this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.');
+ this.updateConnectionStatus(false);
+ });
+
+ this.socket.on('connect_timeout', () => {
+ console.error('Socket connection timeout');
+ this.setStatusMessage('EveAI Chat Widget needs further configuration by site administrator.');
+ this.updateConnectionStatus(false);
+ });
+
+ 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.socket.on('reconnect_attempt', () => {
+ console.log('Attempting to reconnect to the server...');
+ this.setStatusMessage('Attempting to reconnect...');
+ });
+
+ this.socket.on('reconnect', () => {
+ console.log('Successfully reconnected to the server');
+ this.setStatusMessage('Reconnected to EveAI.');
+ this.updateConnectionStatus(true);
+ this.startHeartbeat();
+ });
+
+ this.socket.on('bot_response', (data) => {
+ if (data.tenantId === this.tenantId) {
+ console.log('Initial response received:', data);
+ console.log('Task ID received:', data.taskId);
+ this.checkTaskStatus(data.taskId);
+ this.setStatusMessage('Processing...');
+ }
+ });
+
+ this.socket.on('task_status', (data) => {
+ console.log('Task status received:', data.status);
+ console.log('Task ID received:', data.taskId);
+ console.log('Citations type:', typeof data.citations, 'Citations:', data.citations);
+
+ if (data.status === 'pending') {
+ this.updateProgress();
+ setTimeout(() => this.checkTaskStatus(data.taskId), 1000); // Poll every second
+ } else if (data.status === 'success') {
+ this.addBotMessage(data.answer, data.interaction_id, data.algorithm, data.citations);
+ this.clearProgress(); // Clear progress indicator when done
+ } else {
+ this.setStatusMessage('Failed to process message.');
+ }
+ });
+ }
+
+ 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 `
+
+
+
+
+ send
+
+
+ link_off
+
+
+
+ `;
+ }
+
+ addUserMessage(text) {
+ const message = document.createElement('div');
+ message.classList.add('message', 'user');
+ message.innerHTML = `${text}
`;
+ 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.jwtToken) {
+ console.error('JWT token is not available');
+ 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 });
+ 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 => `${url}`).join('
');
+
+ 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 = `
+ ${content}
+ ${citationsHtml ? `${citationsHtml}
` : ''}
+
+ fingerprint
+ thumb_up_off_alt
+ thumb_down_off_alt
+
+ `;
+ 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
+ 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
+ }
+ }
+
+ sendMessageToBackend(message) {
+ console.log('sendMessageToBackend called');
+ if (!this.socket) {
+ console.error('Socket is not initialized');
+ return;
+ }
+ if (!this.jwtToken) {
+ console.error('JWT token is not available');
+ return;
+ }
+ console.log('Sending message to backend');
+ this.socket.emit('user_message', {
+ tenantId: this.tenantId,
+ token: this.jwtToken,
+ message,
+ language: this.language,
+ timezone: this.userTimezone
+ });
+ 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
+ }
+ }
+}
+
+customElements.define('eveai-chat-widget', EveAIChatWidget);
+
diff --git a/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js b/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js
new file mode 100644
index 0000000..9e53d41
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat-widget/js/eveai-sdk.js
@@ -0,0 +1,27 @@
+// static/js/eveai-sdk.js
+class EveAI {
+ constructor(tenantId, apiKey, domain, language) {
+ this.tenantId = tenantId;
+ this.apiKey = apiKey;
+ this.domain = domain;
+ this.language = language;
+
+ console.log('EveAI constructor:', { tenantId, apiKey, domain });
+ }
+
+ initializeChat(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ container.innerHTML = '';
+ customElements.whenDefined('eveai-chat-widget').then(() => {
+ const chatWidget = container.querySelector('eveai-chat-widget');
+ chatWidget.setAttribute('tenant-id', this.tenantId);
+ chatWidget.setAttribute('api-key', this.apiKey);
+ chatWidget.setAttribute('domain', this.domain);
+ chatWidget.setAttribute('language', this.language);
+ });
+ } else {
+ console.error('Container not found');
+ }
+ }
+}
\ No newline at end of file
diff --git a/integrations/Wordpress/eveai-chat-widget/readme.txt b/integrations/Wordpress/eveai-chat-widget/readme.txt
new file mode 100644
index 0000000..4671f11
--- /dev/null
+++ b/integrations/Wordpress/eveai-chat-widget/readme.txt
@@ -0,0 +1,43 @@
+=== EveAI Chat Widget ===
+Contributors: Josako
+Tags: chat, ai
+Requires at least: 5.0
+Tested up to: 5.9
+Stable tag: 1.2
+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. Go to Settings > EveAI Chat to configure your chat widget 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.2 =
+* Create shortcodes
+
+= 1.1 =
+* Added configurable settings
+* Improved security with server-side API key handling
+
+= 1.0 =
+* Initial release
+
+== Upgrade Notice ==
+
+= 1.1 =
+This version adds configurable settings and improves security. Please update your EveAI credentials after upgrading.
\ No newline at end of file