Source Code
DigitalCompass-AgentHub Community Edition
agenthub.php
<?php
/**
* Plugin Name: DigitalCompass-AgentHub
* Description: Connect any OpenClaw AI Agent to your WordPress website. Free & Open Source.
* Version: 1.0.0
* Author: Barbara Hohensee
* Author URI: https://digitalcompass.site
* Contributors: Barbara Hohensee
* License: GPLv3
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
*/
if (!defined('ABSPATH')) exit;
function lisa_cat_i18n() {
$lang = get_option('lisa_cat_chat_language', 'en');
$strings = [
'en' => [
'admin_title' => 'AgentHub Setup',
'section_language' => 'Language',
'section_connection' => 'Connection',
'section_chat' => 'Chat Settings',
'section_colors' => 'Colors',
'label_language' => 'Interface Language',
'label_endpoint' => 'OpenClaw Endpoint',
'label_token' => 'Gateway Token',
'label_model' => 'Model',
'label_ssl' => 'SSL Verification',
'label_ssl_desc' => 'Verify SSL certificate (disable only for self-signed certificates)',
'label_welcome' => 'Welcome Message',
'label_position' => 'Position',
'label_position_r' => 'Bottom right',
'label_position_l' => 'Bottom left',
'label_color_toggle' => 'Toggle Button',
'label_color_header' => 'Header',
'label_color_user' => 'User Messages',
'label_color_send' => 'Send Button',
'label_color_accent' => 'Accent Color (Focus, Links)',
'save_button' => 'Save Settings',
'status_text' => 'AI Assistant',
'placeholder' => 'Type a message...',
'aria_input' => 'Message to Agent',
'aria_send' => 'Send',
'aria_open' => 'Open chat',
'error_general' => 'An error occurred. Please try again.',
'error_connection' => 'Connection error. Please check your internet connection.',
],
'de' => [
'admin_title' => 'AgentHub Einstellungen',
'section_language' => 'Sprache',
'section_connection' => 'Verbindung',
'section_chat' => 'Chat Einstellungen',
'section_colors' => 'Farben',
'label_language' => 'Interface Sprache',
'label_endpoint' => 'OpenClaw Endpoint',
'label_token' => 'Gateway Token',
'label_model' => 'Model',
'label_ssl' => 'SSL Verifikation',
'label_ssl_desc' => 'SSL-Zertifikat verifizieren (deaktivieren nur bei selbstsignierten Zertifikaten)',
'label_welcome' => 'Begrüßungsnachricht',
'label_position' => 'Position',
'label_position_r' => 'Rechts unten',
'label_position_l' => 'Links unten',
'label_color_toggle' => 'Toggle Button',
'label_color_header' => 'Header',
'label_color_user' => 'Benutzer-Nachrichten',
'label_color_send' => 'Senden-Button',
'label_color_accent' => 'Akzentfarbe (Focus, Links)',
'save_button' => 'Einstellungen speichern',
'status_text' => 'KI-Assistent',
'placeholder' => 'Nachricht schreiben...',
'aria_input' => 'Nachricht an Agent',
'aria_send' => 'Senden',
'aria_open' => 'Chat öffnen',
'error_general' => 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.',
'error_connection' => 'Verbindungsfehler. Bitte prüfe deine Internetverbindung.',
],
];
return $strings[$lang] ?? $strings['en'];
}
add_action('admin_enqueue_scripts', function ($hook) {
if ($hook !== 'settings_page_lisa-cat-chat') return;
wp_enqueue_style('wp-color-picker');
wp_enqueue_script('wp-color-picker');
wp_add_inline_script('wp-color-picker', 'jQuery(function($){ $(".lisa-color-picker").wpColorPicker(); });');
});
add_action('admin_menu', function () {
add_options_page('AgentHub AI Infrastructure', 'AgentHub Setup ⚙️', 'manage_options', 'lisa-cat-chat', 'lisa_cat_chat_settings_page');
});
add_action('admin_init', function () {
foreach ([
'lisa_cat_chat_language', 'lisa_cat_chat_endpoint', 'lisa_cat_chat_token',
'lisa_cat_chat_model', 'lisa_cat_chat_sslverify',
'lisa_cat_chat_welcome', 'lisa_cat_chat_position',
'lisa_cat_chat_color_toggle', 'lisa_cat_chat_color_header',
'lisa_cat_chat_color_user_msg', 'lisa_cat_chat_color_send', 'lisa_cat_chat_color_accent',
] as $option) {
register_setting('lisa_cat_chat', $option, [
'sanitize_callback' => 'sanitize_text_field',
]);
}
});
function lisa_cat_chat_settings_page() {
$t = lisa_cat_i18n();
$lang = get_option('lisa_cat_chat_language', 'en');
?>
<div class="wrap">
<h1>🤖 <?php echo esc_html($t['admin_title']); ?></h1>
<form method="post" action="options.php">
<?php settings_fields('lisa_cat_chat'); ?>
<h2><?php echo esc_html($t['section_language']); ?></h2>
<table class="form-table">
<tr>
<th><?php echo esc_html($t['label_language']); ?></th>
<td>
<select name="lisa_cat_chat_language">
<option value="en" <?php selected($lang, 'en'); ?>>English</option>
<option value="de" <?php selected($lang, 'de'); ?>>Deutsch</option>
</select>
</td>
</tr>
</table>
<h2><?php echo esc_html($t['section_connection']); ?></h2>
<table class="form-table">
<tr>
<th><?php echo esc_html($t['label_endpoint']); ?></th>
<td><input type="text" name="lisa_cat_chat_endpoint" value="<?php echo esc_attr(get_option('lisa_cat_chat_endpoint', 'http://127.0.0.1:18789/v1/chat/completions')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_token']); ?></th>
<td><input type="password" name="lisa_cat_chat_token" value="<?php echo esc_attr(get_option('lisa_cat_chat_token', '')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_model']); ?></th>
<td><input type="text" name="lisa_cat_chat_model" value="<?php echo esc_attr(get_option('lisa_cat_chat_model', 'openclaw/cat')); ?>" class="regular-text" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_ssl']); ?></th>
<td>
<label>
<input type="checkbox" name="lisa_cat_chat_sslverify" value="1" <?php checked(get_option('lisa_cat_chat_sslverify', '1'), '1'); ?> />
<?php echo esc_html($t['label_ssl_desc']); ?>
</label>
</td>
</tr>
</table>
<h2><?php echo esc_html($t['section_chat']); ?></h2>
<table class="form-table">
<tr>
<th><?php echo esc_html($t['label_welcome']); ?></th>
<td><input type="text" name="lisa_cat_chat_welcome" value="<?php echo esc_attr(get_option('lisa_cat_chat_welcome', 'Hello! How can I help you?')); ?>" class="large-text" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_position']); ?></th>
<td>
<select name="lisa_cat_chat_position">
<option value="right" <?php selected(get_option('lisa_cat_chat_position', 'right'), 'right'); ?>><?php echo esc_html($t['label_position_r']); ?></option>
<option value="left" <?php selected(get_option('lisa_cat_chat_position', 'right'), 'left'); ?>><?php echo esc_html($t['label_position_l']); ?></option>
</select>
</td>
</tr>
</table>
<h2><?php echo esc_html($t['section_colors']); ?></h2>
<table class="form-table">
<tr>
<th><?php echo esc_html($t['label_color_toggle']); ?></th>
<td><input type="text" name="lisa_cat_chat_color_toggle" value="<?php echo esc_attr(get_option('lisa_cat_chat_color_toggle', '#c4623d')); ?>" class="lisa-color-picker" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_color_header']); ?></th>
<td><input type="text" name="lisa_cat_chat_color_header" value="<?php echo esc_attr(get_option('lisa_cat_chat_color_header', '#1a5c6e')); ?>" class="lisa-color-picker" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_color_user']); ?></th>
<td><input type="text" name="lisa_cat_chat_color_user_msg" value="<?php echo esc_attr(get_option('lisa_cat_chat_color_user_msg', '#1a5c6e')); ?>" class="lisa-color-picker" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_color_send']); ?></th>
<td><input type="text" name="lisa_cat_chat_color_send" value="<?php echo esc_attr(get_option('lisa_cat_chat_color_send', '#c4623d')); ?>" class="lisa-color-picker" /></td>
</tr>
<tr>
<th><?php echo esc_html($t['label_color_accent']); ?></th>
<td><input type="text" name="lisa_cat_chat_color_accent" value="<?php echo esc_attr(get_option('lisa_cat_chat_color_accent', '#d97a5a')); ?>" class="lisa-color-picker" /></td>
</tr>
</table>
<?php submit_button(esc_html($t['save_button'])); ?>
</form>
<hr>
<p style="color:#999; font-size:12px;">
AgentHub AI Infrastructure v1.0.0 — Free & Open Source |
<a href="https://digitalcompass.site/wp-agenthub" target="_blank">Upgrade to Pro →</a>
</p>
</div>
<?php
}
add_action('wp_head', function () {
?>
<style>
:root {
--cat-toggle: <?php echo esc_attr(get_option('lisa_cat_chat_color_toggle', '#c4623d')); ?>;
--cat-header: <?php echo esc_attr(get_option('lisa_cat_chat_color_header', '#1a5c6e')); ?>;
--cat-user-msg: <?php echo esc_attr(get_option('lisa_cat_chat_color_user_msg', '#1a5c6e')); ?>;
--cat-send: <?php echo esc_attr(get_option('lisa_cat_chat_color_send', '#c4623d')); ?>;
--cat-accent: <?php echo esc_attr(get_option('lisa_cat_chat_color_accent', '#d97a5a')); ?>;
--cat-avatar-bg: #F0997B;
}
</style>
<?php
});
add_action('rest_api_init', function () {
register_rest_route('lisa/v1', '/chat', [
'methods' => 'POST',
'callback' => 'lisa_cat_chat_proxy',
'permission_callback' => '__return_true',
]);
});
function lisa_cat_chat_proxy(WP_REST_Request $request) {
$token = get_option('lisa_cat_chat_token', '');
$endpoint = get_option('lisa_cat_chat_endpoint', 'http://127.0.0.1:18789/v1/chat/completions');
$model = get_option('lisa_cat_chat_model', 'openclaw/cat');
$sslverify = (bool) get_option('lisa_cat_chat_sslverify', '1');
if (empty($token)) {
return new WP_Error('no_token', 'Gateway Token not configured', ['status' => 500]);
}
$messages = $request->get_param('messages');
if (!is_array($messages) || empty($messages)) {
return new WP_Error('no_messages', 'No messages provided', ['status' => 400]);
}
$ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
$rate_key = 'lisa_cat_rate_' . md5($ip);
$count = (int) get_transient($rate_key);
if ($count >= 20) {
return new WP_Error('rate_limit', 'Too many requests. Please wait a moment.', ['status' => 429]);
}
set_transient($rate_key, $count + 1, HOUR_IN_SECONDS);
$response = wp_remote_post($endpoint, [
'timeout' => 60,
'sslverify' => $sslverify,
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $token,
],
'body' => wp_json_encode([
'model' => $model,
'messages' => $messages,
'max_tokens' => 800,
]),
]);
if (is_wp_error($response)) {
return new WP_Error('openclaw_error', $response->get_error_message(), ['status' => 502]);
}
$code = wp_remote_retrieve_response_code($response);
$data = json_decode(wp_remote_retrieve_body($response), true);
if ($code !== 200) {
return new WP_REST_Response(['error' => 'OpenClaw error', 'code' => $code], 502);
}
$content = $data['choices'][0]['message']['content'] ?? '';
return new WP_REST_Response(['reply' => $content], 200);
}
add_action('wp_enqueue_scripts', function () {
$t = lisa_cat_i18n();
wp_enqueue_style('lisa-cat-chat', plugin_dir_url(__FILE__) . 'chat.css', [], '1.0.0');
wp_enqueue_script('lisa-cat-chat', plugin_dir_url(__FILE__) . 'chat.js', [], '1.0.0', true);
wp_localize_script('lisa-cat-chat', 'LisaCat', [
'endpoint' => rest_url('lisa/v1/chat'),
'nonce' => wp_create_nonce('wp_rest'),
'welcome' => get_option('lisa_cat_chat_welcome', 'Hello! How can I help you?'),
'position' => get_option('lisa_cat_chat_position', 'right'),
'bot_name' => 'AI Agent',
'bot_avatar' => '🤖',
'status_text' => $t['status_text'],
'powered_text' => 'DigitalCompass',
'powered_url' => 'https://digitalcompass.site',
'placeholder' => $t['placeholder'],
'aria_input' => $t['aria_input'],
'aria_send' => $t['aria_send'],
'aria_open' => $t['aria_open'],
'error_general' => $t['error_general'],
'error_connection' => $t['error_connection'],
]);
});
chat.css
/* ── LISA CAT Chat Widget ── Deep Purple #26215C / Coral #F0997B ── */
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500;600&display=swap');
:root {
--cat-purple: #26215C;
--cat-purple-mid: #3a3478;
--cat-purple-light: #f0eeff;
--cat-coral: #F0997B;
--cat-coral-dark: #d97a5a;
--cat-white: #ffffff;
--cat-grey: #f5f4fa;
--cat-text: #1a1730;
--cat-muted: #8b87a8;
--cat-radius: 18px;
--cat-shadow: 0 8px 40px rgba(38,33,92,0.18), 0 2px 8px rgba(38,33,92,0.10);
}
/* ── Toggle Button ── */
#lisa-cat-toggle {
position: fixed;
bottom: 28px;
width: 60px;
height: 60px;
border-radius: 50%;
background: var(--cat-toggle, #c4623d);
border: none;
cursor: pointer;
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
z-index: 99998;
transition: transform 0.2s ease, box-shadow 0.2s ease;
font-family: 'DM Sans', sans-serif;
}
#lisa-cat-toggle.position-right { right: 28px; }
#lisa-cat-toggle.position-left { left: 28px; }
#lisa-cat-toggle:hover {
transform: scale(1.08);
box-shadow: 0 6px 28px rgba(38,33,92,0.45);
}
#lisa-cat-toggle svg {
width: 28px;
height: 28px;
transition: opacity 0.2s;
}
#lisa-cat-toggle .icon-chat { opacity: 1; }
#lisa-cat-toggle .icon-close { opacity: 0; position: absolute; }
#lisa-cat-toggle.is-open .icon-chat { opacity: 0; }
#lisa-cat-toggle.is-open .icon-close { opacity: 1; }
/* ── Chat Window ── */
#lisa-cat-window {
position: fixed;
bottom: 104px;
width: 370px;
height: 540px;
background: var(--cat-white);
border-radius: var(--cat-radius);
box-shadow: var(--cat-shadow);
display: flex;
flex-direction: column;
z-index: 99997;
overflow: hidden;
opacity: 0;
transform: translateY(16px) scale(0.97);
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
font-family: 'DM Sans', sans-serif;
}
#lisa-cat-window.position-right { right: 28px; }
#lisa-cat-window.position-left { left: 28px; }
#lisa-cat-window.is-open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: all;
}
/* ── Header ── */
#lisa-cat-header {
background: linear-gradient(135deg, var(--cat-header, #1a5c6e) 0%, var(--cat-header, #1a5c6e) 100%);
padding: 18px 20px 16px;
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.cat-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: var(--cat-avatar-bg, #F0997B);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.cat-header-info { flex: 1; }
.cat-header-name {
font-family: 'DM Serif Display', serif;
font-size: 17px;
color: var(--cat-white);
line-height: 1.2;
letter-spacing: 0.01em;
}
.cat-header-status {
font-size: 12px;
color: rgba(255,255,255,0.65);
margin-top: 2px;
display: flex;
align-items: center;
gap: 5px;
}
.cat-status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #5ddf8a;
box-shadow: 0 0 0 2px rgba(93,223,138,0.3);
animation: pulse-dot 2s ease infinite;
}
@keyframes pulse-dot {
0%, 100% { box-shadow: 0 0 0 2px rgba(93,223,138,0.3); }
50% { box-shadow: 0 0 0 5px rgba(93,223,138,0.1); }
}
/* ── Messages ── */
#lisa-cat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 12px;
background: var(--cat-grey);
scroll-behavior: smooth;
}
#lisa-cat-messages::-webkit-scrollbar { width: 4px; }
#lisa-cat-messages::-webkit-scrollbar-track { background: transparent; }
#lisa-cat-messages::-webkit-scrollbar-thumb { background: var(--cat-muted); border-radius: 4px; }
.cat-msg {
max-width: 82%;
padding: 10px 14px;
border-radius: 14px;
font-size: 14px;
line-height: 1.55;
animation: msg-in 0.22s ease;
}
@keyframes msg-in {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.cat-msg.cat-msg--bot {
background: var(--cat-white);
color: var(--cat-text);
border-bottom-left-radius: 4px;
box-shadow: 0 1px 4px rgba(38,33,92,0.08);
align-self: flex-start;
}
.cat-msg.cat-msg--user {
background: var(--cat-user-msg, #1a5c6e);
color: var(--cat-white);
border-bottom-right-radius: 4px;
align-self: flex-end;
}
/* ── Typing indicator ── */
.cat-typing {
display: flex;
align-items: center;
gap: 5px;
padding: 12px 14px;
background: var(--cat-white);
border-radius: 14px;
border-bottom-left-radius: 4px;
align-self: flex-start;
box-shadow: 0 1px 4px rgba(38,33,92,0.08);
}
.cat-typing span {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--cat-muted);
animation: typing-bounce 1.2s ease infinite;
}
.cat-typing span:nth-child(2) { animation-delay: 0.2s; }
.cat-typing span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-6px); opacity: 1; }
}
/* ── Input Area ── */
#lisa-cat-input-area {
padding: 14px 16px;
background: var(--cat-white);
border-top: 1px solid rgba(38,33,92,0.08);
display: flex;
gap: 10px;
align-items: flex-end;
flex-shrink: 0;
}
#lisa-cat-input {
flex: 1;
border: 1.5px solid rgba(38,33,92,0.15);
border-radius: 12px;
padding: 10px 14px;
font-family: 'DM Sans', sans-serif;
font-size: 14px;
color: var(--cat-text);
resize: none;
outline: none;
min-height: 42px;
max-height: 100px;
line-height: 1.45;
background: var(--cat-grey);
transition: border-color 0.2s;
}
#lisa-cat-input:focus {
border-color: var(--cat-accent, #d97a5a);
background: var(--cat-white);
}
#lisa-cat-input::placeholder { color: var(--cat-muted); }
#lisa-cat-send {
width: 42px;
height: 42px;
border-radius: 12px;
background: var(--cat-send, #c4623d);
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: transform 0.15s, box-shadow 0.15s;
box-shadow: 0 2px 8px rgba(240,153,123,0.4);
}
#lisa-cat-send:hover {
transform: scale(1.06);
box-shadow: 0 4px 14px rgba(240,153,123,0.5);
}
#lisa-cat-send:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
#lisa-cat-send svg {
width: 18px;
height: 18px;
color: white;
}
/* ── Powered by ── */
.cat-powered {
text-align: center;
font-size: 10px;
color: #666;
padding: 6px 0 10px;
background: var(--cat-white);
letter-spacing: 0.03em;
}
.cat-powered a {
color: var(--cat-accent, #d97a5a);
text-decoration: none;
font-weight: 500;
}
/* ── Mobile ── */
@media (max-width: 480px) {
#lisa-cat-window {
width: calc(100vw - 20px);
height: calc(100vh - 120px);
right: 10px !important;
left: 10px !important;
bottom: 90px;
}
}
chat.js
(function () {
'use strict';
const cfg = window.LisaCat || {};
const endpoint = cfg.endpoint || '';
const nonce = cfg.nonce || '';
const welcome = cfg.welcome || 'Hello! How can I help you?';
const position = cfg.position || 'right';
const botName = cfg.bot_name || 'CAT';
const botAvatar = cfg.bot_avatar || '🦞';
const statusText = cfg.status_text || 'AI Assistant';
const poweredText = cfg.powered_text || 'Powered by DIGITAL COMPASS';
const poweredUrl = cfg.powered_url || 'https://digitalcompass.site';
const i18n = {
placeholder: cfg.placeholder || 'Type a message...',
aria_input: cfg.aria_input || 'Message to Agent',
aria_send: cfg.aria_send || 'Send',
aria_open: cfg.aria_open || 'Open chat',
error_general: cfg.error_general || 'An error occurred. Please try again.',
error_connection: cfg.error_connection || 'Connection error. Please check your internet connection.',
};
const history = [];
const toggleBtn = document.createElement('button');
toggleBtn.id = 'lisa-cat-toggle';
toggleBtn.className = 'position-' + position;
toggleBtn.setAttribute('aria-label', i18n.aria_open);
toggleBtn.innerHTML = `
<svg class="icon-chat" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
<svg class="icon-close" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
`;
const chatWindow = document.createElement('div');
chatWindow.id = 'lisa-cat-window';
chatWindow.className = 'position-' + position;
chatWindow.setAttribute('role', 'dialog');
chatWindow.setAttribute('aria-label', botName + ' Chatbot');
chatWindow.innerHTML = `
<div id="lisa-cat-header">
<div class="cat-avatar">${botAvatar}</div>
<div class="cat-header-info">
<div class="cat-header-name">${botName}</div>
<div class="cat-header-status">
<span class="cat-status-dot"></span>
${statusText}
</div>
</div>
</div>
<div id="lisa-cat-messages" role="log" aria-live="polite"></div>
<div id="lisa-cat-input-area">
<textarea
id="lisa-cat-input"
placeholder="${i18n.placeholder}"
rows="1"
aria-label="${i18n.aria_input}"
></textarea>
<button id="lisa-cat-send" aria-label="${i18n.aria_send}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
</button>
</div>
<div class="cat-powered">Powered by <a href="${poweredUrl}" target="_blank" rel="noopener">${poweredText}</a></div>
`;
document.body.appendChild(toggleBtn);
document.body.appendChild(chatWindow);
const messagesEl = document.getElementById('lisa-cat-messages');
const inputEl = document.getElementById('lisa-cat-input');
const sendBtn = document.getElementById('lisa-cat-send');
let isOpen = false;
toggleBtn.addEventListener('click', function () {
isOpen = !isOpen;
toggleBtn.classList.toggle('is-open', isOpen);
chatWindow.classList.toggle('is-open', isOpen);
if (isOpen && messagesEl.children.length === 0) {
addMessage(welcome, 'bot');
}
if (isOpen) {
setTimeout(() => inputEl.focus(), 300);
}
});
function parseMarkdown(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code style="background:#f0f0f0;padding:1px 5px;border-radius:3px;font-size:12px;">$1</code>')
.replace(/\[(.+?)\]\((https?:\/\/[^\)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" style="color:var(--cat-accent,#d97a5a);">$1</a>')
.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>')
.replace(/(<li>[\s\S]*<\/li>)/, '<ul style="margin:6px 0 6px 16px;padding:0;">$1</ul>')
.replace(/\n<li>/g, '<li>')
.replace(/<\/li>\n/g, '</li>')
.replace(/\n/g, '<br>');
}
function addMessage(text, role) {
const div = document.createElement('div');
div.className = 'cat-msg cat-msg--' + role;
if (role === 'bot') {
div.innerHTML = parseMarkdown(text);
} else {
div.textContent = text;
}
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
return div;
}
function showTyping() {
const div = document.createElement('div');
div.className = 'cat-typing';
div.id = 'lisa-cat-typing';
div.innerHTML = '<span></span><span></span><span></span>';
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function hideTyping() {
const el = document.getElementById('lisa-cat-typing');
if (el) el.remove();
}
async function sendMessage() {
const text = inputEl.value.trim();
if (!text) return;
inputEl.value = '';
inputEl.style.height = 'auto';
sendBtn.disabled = true;
addMessage(text, 'user');
history.push({ role: 'user', content: text });
showTyping();
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
body: JSON.stringify({ messages: history }),
});
hideTyping();
if (!res.ok) {
const err = await res.json().catch(() => ({}));
addMessage('⚠️ ' + (err.message || i18n.error_general), 'bot');
return;
}
const data = await res.json();
const reply = data.reply || '...';
addMessage(reply, 'bot');
history.push({ role: 'assistant', content: reply });
} catch (e) {
hideTyping();
addMessage('⚠️ ' + i18n.error_connection, 'bot');
} finally {
sendBtn.disabled = false;
inputEl.focus();
}
}
inputEl.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});
})();