Build a Production-Grade Gemini AI Chatbot WordPress Plugin — The Complete Developer’s Guide (2025)

4012 views
chatbot_gemani_php_javascript_wordpress_ipdata_code

Why Build a WordPress Chatbot Plugin Instead of Installing One?

There are dozens of AI chatbot plugins in the WordPress repository. Most of them share the same problems:

  • Monthly subscription fees ranging from $29 to $299/month
  • Third-party data storage — your visitors’ conversations go to someone else’s servers
  • Limited customisation — you can change the colour, not the behaviour
  • Breakage risk — when the plugin author abandons it, your chatbot dies
  • Bloat — they load 12 JavaScript files and 3 stylesheets for a chat widget

Building your own WordPress plugin solves all of this. You get a chatbot that:

  • Costs zero per month beyond Google’s generous Gemini free tier (60 requests/min)
  • Stores nothing externally — all conversation data stays on your server
  • Has a custom personality defined by you via a system prompt
  • Works exactly as configured, forever, because you own the code
  • Can be embedded anywhere with a single shortcode or as a floating widget

The original article on this topic gives you 4 files and wishes you luck. This guide gives you a complete, installable WordPress plugin with:

  • A professional plugin architecture following WordPress coding standards
  • A full Settings page in wp-admin with API key storage, model selection, and chatbot persona
  • A shortcode [gemini_chatbot] with configurable attributes
  • A floating chat widget that appears site-wide
  • Session-based conversation memory with per-user history
  • Rate limiting per IP/user to prevent API abuse
  • Nonce security on every AJAX request
  • Markdown rendering in the chat UI
  • Responsive dark/light UI with theme options
  • WordPress Options API for all settings (no hardcoded API keys)
  • A debug mode for development
  • Uninstall cleanup that removes all plugin data on deletion

 

Plugin Architecture — The WordPress Way

Before writing a single line, understand how a properly structured WordPress plugin is organised.

/wp-content/plugins/gemini-ai-chatbot/
│
├── gemini-ai-chatbot.php          ← Main plugin file (headers + bootstrap)
│
├── includes/
│   ├── class-gemini-chatbot.php   ← Core plugin class (singleton)
│   ├── class-gemini-api.php       ← Gemini API communication layer
│   ├── class-gemini-ajax.php      ← AJAX request handlers
│   └── class-gemini-settings.php  ← Admin settings page (Settings API)
│
├── assets/
│   ├── css/
│   │   ├── chatbot-frontend.css   ← Frontend widget styles
│   │   └── chatbot-admin.css      ← Admin settings page styles
│   └── js/
│       ├── chatbot-frontend.js    ← Frontend chat logic
│       └── chatbot-admin.js       ← Admin page interactivity
│
├── templates/
│   ├── chatbot-widget.php         ← Shortcode/widget HTML template
│   └── floating-widget.php        ← Floating bubble widget template
│
├── languages/
│   └── gemini-ai-chatbot.pot      ← Translation template (i18n ready)
│
├── uninstall.php                  ← Cleanup on plugin deletion
└── readme.txt                     ← WordPress plugin repository readme

This structure separates concerns cleanly: API logic never touches the UI, settings are never hardcoded, and every class has one responsibility.

 

WordPress Gemini Plugin Architecture

Step 1: The Main Plugin File

gemini-ai-chatbot.php

<?php
/**
 * Plugin Name:       Gemini AI Chatbot
 * Plugin URI:        https://ipdata.in/gemini-chatbot
 * Description:       Embed a fully-featured Google Gemini AI chatbot anywhere on your WordPress site via shortcode or floating widget. Zero subscriptions, full control.
 * Version:           2.0.0
 * Requires at least: 5.8
 * Requires PHP:      7.4
 * Author:            Tabir Ahmad
 * Author URI:        https://ipdata.in
 * License:           GPL v2 or later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       gemini-ai-chatbot
 * Domain Path:       /languages
 */

// ── Security: Prevent direct file access ─────────────────────────────────────
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

// ── Plugin Constants ──────────────────────────────────────────────────────────
define( 'GAICB_VERSION',     '2.0.0' );
define( 'GAICB_PLUGIN_FILE', __FILE__ );
define( 'GAICB_PLUGIN_DIR',  plugin_dir_path( __FILE__ ) );
define( 'GAICB_PLUGIN_URL',  plugin_dir_url( __FILE__ ) );
define( 'GAICB_PLUGIN_BASE', plugin_basename( __FILE__ ) );
define( 'GAICB_OPTIONS_KEY', 'gaicb_settings' );

// ── Autoloader ────────────────────────────────────────────────────────────────
spl_autoload_register( function( string $class_name ): void {
    // Only autoload our own classes
    if ( strpos( $class_name, 'Gemini_' ) !== 0 ) return;

    $file = GAICB_PLUGIN_DIR . 'includes/class-' .
            strtolower( str_replace( '_', '-', $class_name ) ) . '.php';

    if ( file_exists( $file ) ) {
        require_once $file;
    }
});

// ── Bootstrap ─────────────────────────────────────────────────────────────────
function gaicb_init(): void {
    // Load text domain for translations
    load_plugin_textdomain(
        'gemini-ai-chatbot',
        false,
        dirname( GAICB_PLUGIN_BASE ) . '/languages'
    );

    // Boot the plugin
    Gemini_Chatbot::get_instance();
}
add_action( 'plugins_loaded', 'gaicb_init' );

// ── Activation Hook ───────────────────────────────────────────────────────────
register_activation_hook( __FILE__, function(): void {
    // Set default options on first activation
    if ( ! get_option( GAICB_OPTIONS_KEY ) ) {
        add_option( GAICB_OPTIONS_KEY, [
            'api_key'          => '',
            'model'            => 'gemini-1.5-flash',
            'system_prompt'    => 'You are a helpful AI assistant for this website. Be friendly, concise, and accurate.',
            'chatbot_name'     => 'AI Assistant',
            'welcome_message'  => 'Hello! How can I help you today?',
            'theme'            => 'dark',
            'accent_color'     => '#6366f1',
            'enable_widget'    => '1',
            'widget_position'  => 'bottom-right',
            'max_history'      => '20',
            'rate_limit'       => '20',
            'enable_shortcode' => '1',
            'debug_mode'       => '0',
            'max_tokens'       => '1024',
            'temperature'      => '0.7',
        ]);
    }

    // Flush rewrite rules
    flush_rewrite_rules();
});

// ── Deactivation Hook ─────────────────────────────────────────────────────────
register_deactivation_hook( __FILE__, function(): void {
    flush_rewrite_rules();
});

 

Step 2: Core Plugin Class (Singleton Pattern)

includes/class-gemini-chatbot.php

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Main plugin class — Singleton.
 * Coordinates all plugin components: settings, AJAX, shortcodes, widgets.
 */
class Gemini_Chatbot {

    private static ?Gemini_Chatbot $instance = null;
    private array $options = [];

    // Prevent direct instantiation
    private function __construct() {
        $this->options = get_option( GAICB_OPTIONS_KEY, [] );
        $this->init_hooks();
    }

    // Prevent cloning
    private function __clone() {}

    /**
     * Get singleton instance.
     */
    public static function get_instance(): self {
        if ( self::$instance === null ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Register all WordPress hooks.
     */
    private function init_hooks(): void {
        // Frontend assets
        add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );

        // Admin assets + settings
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
        add_action( 'admin_menu',            [ $this, 'register_admin_menu' ] );
        add_action( 'admin_init',            [ $this, 'register_settings' ] );

        // Plugin action links (Settings link on Plugins page)
        add_filter(
            'plugin_action_links_' . GAICB_PLUGIN_BASE,
            [ $this, 'add_settings_link' ]
        );

        // Shortcode
        if ( ! empty( $this->options['enable_shortcode'] ) ) {
            add_shortcode( 'gemini_chatbot', [ $this, 'render_shortcode' ] );
        }

        // Floating widget (injected into footer)
        if ( ! empty( $this->options['enable_widget'] ) ) {
            add_action( 'wp_footer', [ $this, 'render_floating_widget' ] );
        }

        // AJAX handlers
        $ajax = new Gemini_Ajax( $this->options );
        add_action( 'wp_ajax_gaicb_chat',          [ $ajax, 'handle_chat'  ] );
        add_action( 'wp_ajax_nopriv_gaicb_chat',   [ $ajax, 'handle_chat'  ] );
        add_action( 'wp_ajax_gaicb_reset',         [ $ajax, 'handle_reset' ] );
        add_action( 'wp_ajax_nopriv_gaicb_reset',  [ $ajax, 'handle_reset' ] );
    }

    /**
     * Enqueue frontend CSS + JS.
     */
    public function enqueue_frontend_assets(): void {
        // Only enqueue if shortcode is on this page OR floating widget is enabled
        $has_shortcode   = is_a( get_post(), 'WP_Post' ) &&
                           has_shortcode( get_post()->post_content, 'gemini_chatbot' );
        $widget_enabled  = ! empty( $this->options['enable_widget'] );

        if ( ! $has_shortcode && ! $widget_enabled ) return;

        wp_enqueue_style(
            'gaicb-frontend',
            GAICB_PLUGIN_URL . 'assets/css/chatbot-frontend.css',
            [],
            GAICB_VERSION
        );

        wp_enqueue_script(
            'gaicb-frontend',
            GAICB_PLUGIN_URL . 'assets/js/chatbot-frontend.js',
            [], // No jQuery dependency — pure vanilla JS
            GAICB_VERSION,
            true // Load in footer
        );

        // Pass PHP variables to JavaScript
        wp_localize_script( 'gaicb-frontend', 'gaicbVars', [
            'ajaxurl'        => admin_url( 'admin-ajax.php' ),
            'nonce'          => wp_create_nonce( 'gaicb_chat_nonce' ),
            'resetNonce'     => wp_create_nonce( 'gaicb_reset_nonce' ),
            'chatbotName'    => esc_js( $this->options['chatbot_name']    ?? 'AI Assistant' ),
            'welcomeMsg'     => esc_js( $this->options['welcome_message'] ?? 'Hello! How can I help you today?' ),
            'theme'          => esc_js( $this->options['theme']           ?? 'dark' ),
            'accentColor'    => esc_js( $this->options['accent_color']    ?? '#6366f1' ),
            'widgetPosition' => esc_js( $this->options['widget_position'] ?? 'bottom-right' ),
            'i18n'           => [
                'placeholder'  => __( 'Type a message…', 'gemini-ai-chatbot' ),
                'send'         => __( 'Send', 'gemini-ai-chatbot' ),
                'reset'        => __( 'New chat', 'gemini-ai-chatbot' ),
                'error'        => __( 'Something went wrong. Please try again.', 'gemini-ai-chatbot' ),
                'rateLimited'  => __( 'Too many messages. Please wait a moment.', 'gemini-ai-chatbot' ),
                'resetConfirm' => __( 'Start a new conversation? This will clear the current chat.', 'gemini-ai-chatbot' ),
            ],
        ]);
    }

    /**
     * Enqueue admin-only assets.
     */
    public function enqueue_admin_assets( string $hook ): void {
        if ( $hook !== 'settings_page_gemini-ai-chatbot' ) return;

        wp_enqueue_style(
            'gaicb-admin',
            GAICB_PLUGIN_URL . 'assets/css/chatbot-admin.css',
            [],
            GAICB_VERSION
        );

        wp_enqueue_script(
            'gaicb-admin',
            GAICB_PLUGIN_URL . 'assets/js/chatbot-admin.js',
            [ 'wp-color-picker' ],
            GAICB_VERSION,
            true
        );

        wp_enqueue_style( 'wp-color-picker' );
    }

    /**
     * Register admin menu under Settings.
     */
    public function register_admin_menu(): void {
        add_options_page(
            __( 'Gemini AI Chatbot Settings', 'gemini-ai-chatbot' ),
            __( 'Gemini Chatbot', 'gemini-ai-chatbot' ),
            'manage_options',
            'gemini-ai-chatbot',
            [ $this, 'render_settings_page' ]
        );
    }

    /**
     * Register Settings API sections + fields.
     */
    public function register_settings(): void {
        register_setting(
            'gaicb_settings_group',
            GAICB_OPTIONS_KEY,
            [ $this, 'sanitize_settings' ]
        );

        // ── Section: API Configuration ─────────────────────────────────────
        add_settings_section(
            'gaicb_section_api',
            __( '🔑 API Configuration', 'gemini-ai-chatbot' ),
            fn() => echo '<p>' . esc_html__( 'Connect your Google Gemini API key to power the chatbot.', 'gemini-ai-chatbot' ) . '</p>',
            'gemini-ai-chatbot'
        );

        add_settings_field( 'api_key', __( 'Gemini API Key', 'gemini-ai-chatbot' ),
            [ $this, 'field_api_key' ], 'gemini-ai-chatbot', 'gaicb_section_api' );

        add_settings_field( 'model', __( 'Gemini Model', 'gemini-ai-chatbot' ),
            [ $this, 'field_model' ], 'gemini-ai-chatbot', 'gaicb_section_api' );

        add_settings_field( 'temperature', __( 'Temperature (Creativity)', 'gemini-ai-chatbot' ),
            [ $this, 'field_temperature' ], 'gemini-ai-chatbot', 'gaicb_section_api' );

        add_settings_field( 'max_tokens', __( 'Max Response Tokens', 'gemini-ai-chatbot' ),
            [ $this, 'field_max_tokens' ], 'gemini-ai-chatbot', 'gaicb_section_api' );

        // ── Section: Chatbot Personality ───────────────────────────────────
        add_settings_section(
            'gaicb_section_personality',
            __( '🤖 Chatbot Personality', 'gemini-ai-chatbot' ),
            fn() => echo '<p>' . esc_html__( 'Define your chatbot\'s name, tone, and behaviour.', 'gemini-ai-chatbot' ) . '</p>',
            'gemini-ai-chatbot'
        );

        add_settings_field( 'chatbot_name',   __( 'Chatbot Name', 'gemini-ai-chatbot' ),
            [ $this, 'field_chatbot_name' ], 'gemini-ai-chatbot', 'gaicb_section_personality' );

        add_settings_field( 'welcome_message', __( 'Welcome Message', 'gemini-ai-chatbot' ),
            [ $this, 'field_welcome_message' ], 'gemini-ai-chatbot', 'gaicb_section_personality' );

        add_settings_field( 'system_prompt', __( 'System Prompt (Persona)', 'gemini-ai-chatbot' ),
            [ $this, 'field_system_prompt' ], 'gemini-ai-chatbot', 'gaicb_section_personality' );

        // ── Section: Display Options ────────────────────────────────────────
        add_settings_section(
            'gaicb_section_display',
            __( '🎨 Display & Widget Options', 'gemini-ai-chatbot' ),
            fn() => echo '<p>' . esc_html__( 'Control how and where the chatbot appears on your site.', 'gemini-ai-chatbot' ) . '</p>',
            'gemini-ai-chatbot'
        );

        add_settings_field( 'theme',           __( 'Chat Theme', 'gemini-ai-chatbot' ),
            [ $this, 'field_theme' ], 'gemini-ai-chatbot', 'gaicb_section_display' );

        add_settings_field( 'accent_color',    __( 'Accent Colour', 'gemini-ai-chatbot' ),
            [ $this, 'field_accent_color' ], 'gemini-ai-chatbot', 'gaicb_section_display' );

        add_settings_field( 'enable_shortcode', __( 'Enable Shortcode', 'gemini-ai-chatbot' ),
            [ $this, 'field_enable_shortcode' ], 'gemini-ai-chatbot', 'gaicb_section_display' );

        add_settings_field( 'enable_widget',   __( 'Floating Chat Widget', 'gemini-ai-chatbot' ),
            [ $this, 'field_enable_widget' ], 'gemini-ai-chatbot', 'gaicb_section_display' );

        add_settings_field( 'widget_position', __( 'Widget Position', 'gemini-ai-chatbot' ),
            [ $this, 'field_widget_position' ], 'gemini-ai-chatbot', 'gaicb_section_display' );

        // ── Section: Performance & Security ────────────────────────────────
        add_settings_section(
            'gaicb_section_advanced',
            __( '⚙️ Performance & Security', 'gemini-ai-chatbot' ),
            fn() => echo '<p>' . esc_html__( 'Rate limiting and conversation memory controls.', 'gemini-ai-chatbot' ) . '</p>',
            'gemini-ai-chatbot'
        );

        add_settings_field( 'max_history',  __( 'Max History Turns', 'gemini-ai-chatbot' ),
            [ $this, 'field_max_history' ], 'gemini-ai-chatbot', 'gaicb_section_advanced' );

        add_settings_field( 'rate_limit',   __( 'Rate Limit (per minute)', 'gemini-ai-chatbot' ),
            [ $this, 'field_rate_limit' ], 'gemini-ai-chatbot', 'gaicb_section_advanced' );

        add_settings_field( 'debug_mode',   __( 'Debug Mode', 'gemini-ai-chatbot' ),
            [ $this, 'field_debug_mode' ], 'gemini-ai-chatbot', 'gaicb_section_advanced' );
    }

    /**
     * Sanitize all settings before saving.
     */
    public function sanitize_settings( array $input ): array {
        $clean = [];

        // API Key — strip whitespace, store as-is (not escaped — contains special chars)
        $clean['api_key'] = sanitize_text_field( trim( $input['api_key'] ?? '' ) );

        // Model — whitelist
        $allowed_models = [ 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-1.5-flash-8b' ];
        $clean['model'] = in_array( $input['model'] ?? '', $allowed_models, true )
            ? $input['model']
            : 'gemini-1.5-flash';

        // System prompt — allow some HTML via wp_kses
        $clean['system_prompt'] = wp_strip_all_tags( $input['system_prompt'] ?? '' );

        // Text fields
        $clean['chatbot_name']    = sanitize_text_field( $input['chatbot_name']    ?? 'AI Assistant' );
        $clean['welcome_message'] = sanitize_text_field( $input['welcome_message'] ?? 'Hello! How can I help?' );

        // Colour — validate hex
        $clean['accent_color'] = sanitize_hex_color( $input['accent_color'] ?? '#6366f1' ) ?? '#6366f1';

        // Theme — whitelist
        $clean['theme'] = in_array( $input['theme'] ?? '', [ 'dark', 'light', 'auto' ], true )
            ? $input['theme']
            : 'dark';

        // Widget position — whitelist
        $positions = [ 'bottom-right', 'bottom-left', 'top-right', 'top-left' ];
        $clean['widget_position'] = in_array( $input['widget_position'] ?? '', $positions, true )
            ? $input['widget_position']
            : 'bottom-right';

        // Numeric values with ranges
        $clean['max_history'] = max( 5, min( 50, (int)( $input['max_history'] ?? 20 ) ) );
        $clean['rate_limit']  = max( 5, min( 60, (int)( $input['rate_limit']  ?? 20 ) ) );
        $clean['max_tokens']  = max( 256, min( 8192, (int)( $input['max_tokens'] ?? 1024 ) ) );

        // Float with range
        $temp = (float)( $input['temperature'] ?? 0.7 );
        $clean['temperature'] = max( 0.0, min( 1.0, $temp ) );

        // Checkboxes
        $clean['enable_widget']    = ! empty( $input['enable_widget'] )    ? '1' : '0';
        $clean['enable_shortcode'] = ! empty( $input['enable_shortcode'] ) ? '1' : '0';
        $clean['debug_mode']       = ! empty( $input['debug_mode'] )       ? '1' : '0';

        return $clean;
    }

    // ── Settings Field Renderers ──────────────────────────────────────────────

    public function field_api_key(): void {
        $val = esc_attr( $this->options['api_key'] ?? '' );
        $masked = $val ? str_repeat('•', max(0, strlen($val) - 8)) . substr($val, -8) : '';
        echo '<input type="password" name="' . GAICB_OPTIONS_KEY . '[api_key]"
              value="' . $val . '" class="regular-text" autocomplete="new-password">';
        if ( $masked ) {
            echo '<p class="description">Current key: <code>' . esc_html($masked) . '</code></p>';
        }
        echo '<p class="description">Get your free key from <a href="https://makersuite.google.com/app" target="_blank">Google AI Studio</a>.</p>';
    }

    public function field_model(): void {
        $val = $this->options['model'] ?? 'gemini-1.5-flash';
        $models = [
            'gemini-1.5-flash'    => 'Gemini 1.5 Flash — Fast & free (Recommended)',
            'gemini-1.5-flash-8b' => 'Gemini 1.5 Flash 8B — Smallest, fastest',
            'gemini-1.5-pro'      => 'Gemini 1.5 Pro — Most capable (higher latency)',
        ];
        echo '<select name="' . GAICB_OPTIONS_KEY . '[model]">';
        foreach ( $models as $key => $label ) {
            printf( '<option value="%s" %s>%s</option>',
                esc_attr($key), selected($val, $key, false), esc_html($label) );
        }
        echo '</select>';
        echo '<p class="description">Flash is recommended for real-time chat. Pro is better for complex reasoning tasks.</p>';
    }

    public function field_temperature(): void {
        $val = esc_attr( $this->options['temperature'] ?? '0.7' );
        echo '<input type="range" name="' . GAICB_OPTIONS_KEY . '[temperature]"
              value="' . $val . '" min="0" max="1" step="0.1"
              oninput="this.nextElementSibling.value=this.value">
              <output>' . $val . '</output>';
        echo '<p class="description">0 = Deterministic/precise. 1 = Creative/varied. Default: 0.7</p>';
    }

    public function field_max_tokens(): void {
        $val = esc_attr( $this->options['max_tokens'] ?? '1024' );
        echo '<input type="number" name="' . GAICB_OPTIONS_KEY . '[max_tokens]"
              value="' . $val . '" min="256" max="8192" step="256" class="small-text">';
        echo '<p class="description">Maximum length of AI responses. 1024 ≈ ~750 words. Higher = slower + more tokens used.</p>';
    }

    public function field_chatbot_name(): void {
        $val = esc_attr( $this->options['chatbot_name'] ?? 'AI Assistant' );
        echo '<input type="text" name="' . GAICB_OPTIONS_KEY . '[chatbot_name]"
              value="' . $val . '" class="regular-text" placeholder="AI Assistant">';
        echo '<p class="description">Displayed in the chat header. Example: "Support Bot", "Aria", "HelpDesk AI"</p>';
    }

    public function field_welcome_message(): void {
        $val = esc_attr( $this->options['welcome_message'] ?? 'Hello! How can I help you today?' );
        echo '<input type="text" name="' . GAICB_OPTIONS_KEY . '[welcome_message]"
              value="' . $val . '" class="large-text">';
        echo '<p class="description">First message the bot sends when a new conversation starts.</p>';
    }

    public function field_system_prompt(): void {
        $val = esc_textarea( $this->options['system_prompt'] ?? '' );
        echo '<textarea name="' . GAICB_OPTIONS_KEY . '[system_prompt]"
              rows="6" class="large-text">' . $val . '</textarea>';
        echo '<p class="description">Defines your chatbot\'s persona, knowledge scope, and behaviour rules. This is never visible to users.</p>';
        echo '<details style="margin-top:8px"><summary><strong>Example system prompts (click to expand)</strong></summary>
        <pre style="background:#f1f5f9;padding:12px;border-radius:6px;font-size:12px;margin-top:8px">
<strong>Customer Support:</strong>
You are a support agent for AcmeCorp. Help users with orders,
returns (30-day policy), and product questions. Never promise
discounts not on the website. For complaints, collect their
order number and escalate to support@acmecorp.com.

<strong>Tech Docs Bot:</strong>
You are a technical assistant for developers using our REST API.
Always show code in the correct language. Prefer v3 endpoints.
If unsure, say so — never guess API behaviour.

<strong>Lead Generation:</strong>
You are a friendly sales assistant. Help visitors understand our
services. If they express interest, collect their name, email,
and project details. Always end by offering a free consultation.
        </pre></details>';
    }

    public function field_theme(): void {
        $val = $this->options['theme'] ?? 'dark';
        $themes = [ 'dark' => 'Dark', 'light' => 'Light', 'auto' => 'Auto (follows system)' ];
        echo '<select name="' . GAICB_OPTIONS_KEY . '[theme]">';
        foreach ( $themes as $key => $label ) {
            printf( '<option value="%s" %s>%s</option>',
                esc_attr($key), selected($val, $key, false), esc_html($label) );
        }
        echo '</select>';
    }

    public function field_accent_color(): void {
        $val = esc_attr( $this->options['accent_color'] ?? '#6366f1' );
        echo '<input type="text" name="' . GAICB_OPTIONS_KEY . '[accent_color]"
              value="' . $val . '" class="gaicb-color-picker" data-default-color="#6366f1">';
        echo '<p class="description">Used for the chat header, send button, and user message bubbles.</p>';
    }

    public function field_enable_shortcode(): void {
        $val = ! empty( $this->options['enable_shortcode'] );
        echo '<label><input type="checkbox" name="' . GAICB_OPTIONS_KEY . '[enable_shortcode]"
              value="1" ' . checked($val, true, false) . '>
              Enable <code>[gemini_chatbot]</code> shortcode</label>';
        echo '<p class="description">Place <code>[gemini_chatbot]</code> in any post, page, or widget area.</p>';
        echo '<p class="description">Shortcode options: <code>[gemini_chatbot title="Ask Us" theme="light" height="450"]</code></p>';
    }

    public function field_enable_widget(): void {
        $val = ! empty( $this->options['enable_widget'] );
        echo '<label><input type="checkbox" name="' . GAICB_OPTIONS_KEY . '[enable_widget]"
              value="1" ' . checked($val, true, false) . '>
              Show floating chat bubble on all pages</label>';
        echo '<p class="description">Adds a persistent chat button in the corner of every page. Can be overridden per-page.</p>';
    }

    public function field_widget_position(): void {
        $val = $this->options['widget_position'] ?? 'bottom-right';
        $positions = [
            'bottom-right' => 'Bottom Right',
            'bottom-left'  => 'Bottom Left',
            'top-right'    => 'Top Right',
            'top-left'     => 'Top Left',
        ];
        echo '<select name="' . GAICB_OPTIONS_KEY . '[widget_position]">';
        foreach ( $positions as $key => $label ) {
            printf( '<option value="%s" %s>%s</option>',
                esc_attr($key), selected($val, $key, false), esc_html($label) );
        }
        echo '</select>';
    }

    public function field_max_history(): void {
        $val = esc_attr( $this->options['max_history'] ?? '20' );
        echo '<input type="number" name="' . GAICB_OPTIONS_KEY . '[max_history]"
              value="' . $val . '" min="5" max="50" step="1" class="small-text">';
        echo '<p class="description">Number of message pairs (user + bot) to keep in memory per session. More = better context, more tokens used.</p>';
    }

    public function field_rate_limit(): void {
        $val = esc_attr( $this->options['rate_limit'] ?? '20' );
        echo '<input type="number" name="' . GAICB_OPTIONS_KEY . '[rate_limit]"
              value="' . $val . '" min="5" max="60" step="1" class="small-text">';
        echo '<p class="description">Maximum messages per visitor per minute. Prevents API abuse. Recommended: 15–25.</p>';
    }

    public function field_debug_mode(): void {
        $val = ! empty( $this->options['debug_mode'] );
        echo '<label><input type="checkbox" name="' . GAICB_OPTIONS_KEY . '[debug_mode]"
              value="1" ' . checked($val, true, false) . '>
              Enable debug logging to WordPress error log</label>';
        echo '<p class="description"><strong>Disable on production.</strong> Logs API calls and errors to wp-content/debug.log.</p>';
    }

    /**
     * Render the admin settings page.
     */
    public function render_settings_page(): void {
        if ( ! current_user_can( 'manage_options' ) ) return;

        // Show success/error notices after save
        if ( isset( $_GET['settings-updated'] ) ) {
            add_settings_error(
                'gaicb_messages', 'gaicb_saved',
                __( 'Settings saved successfully.', 'gemini-ai-chatbot' ),
                'updated'
            );
        }

        settings_errors( 'gaicb_messages' );

        $api_key_set = ! empty( $this->options['api_key'] );
        ?>
        <div class="wrap gaicb-settings-wrap">
            <h1>
                <span style="font-size:1.4em;margin-right:8px;">🤖</span>
                <?php esc_html_e( 'Gemini AI Chatbot Settings', 'gemini-ai-chatbot' ); ?>
            </h1>

            <?php if ( ! $api_key_set ) : ?>
            <div class="notice notice-warning">
                <p><strong><?php esc_html_e( 'Action required:', 'gemini-ai-chatbot' ); ?></strong>
                <?php esc_html_e( 'Add your Gemini API key below to activate the chatbot.', 'gemini-ai-chatbot' ); ?>
                <a href="https://makersuite.google.com/app" target="_blank">
                    <?php esc_html_e( 'Get a free key →', 'gemini-ai-chatbot' ); ?>
                </a></p>
            </div>
            <?php endif; ?>

            <!-- Quick Start Info -->
            <div class="gaicb-info-boxes">
                <div class="gaicb-info-box">
                    <strong>📋 Shortcode Usage</strong>
                    <code>[gemini_chatbot]</code>
                    <code>[gemini_chatbot title="Support" theme="light"]</code>
                    <code>[gemini_chatbot height="500" show_reset="true"]</code>
                </div>
                <div class="gaicb-info-box">
                    <strong>📊 Free Tier Limits</strong>
                    <span>Gemini 1.5 Flash: <strong>60 requests/min</strong></span>
                    <span>Gemini 1.5 Pro: <strong>2 requests/min</strong></span>
                    <span>No monthly caps on free tier</span>
                </div>
                <div class="gaicb-info-box">
                    <strong>🔒 Security</strong>
                    <span>API key stored in WordPress options (encrypted at rest if your host supports it)</span>
                    <span>All requests use nonce verification</span>
                </div>
            </div>

            <form method="post" action="options.php">
                <?php
                settings_fields( 'gaicb_settings_group' );
                do_settings_sections( 'gemini-ai-chatbot' );
                submit_button( __( 'Save Settings', 'gemini-ai-chatbot' ) );
                ?>
            </form>
        </div>
        <?php
    }

    /**
     * Render [gemini_chatbot] shortcode.
     */
    public function render_shortcode( array $atts ): string {
        $atts = shortcode_atts([
            'title'      => $this->options['chatbot_name']    ?? 'AI Assistant',
            'theme'      => $this->options['theme']           ?? 'dark',
            'height'     => '420',
            'show_reset' => 'true',
            'placeholder'=> 'Type a message…',
        ], $atts, 'gemini_chatbot' );

        ob_start();
        include GAICB_PLUGIN_DIR . 'templates/chatbot-widget.php';
        return ob_get_clean();
    }

    /**
     * Render floating widget into footer.
     */
    public function render_floating_widget(): void {
        include GAICB_PLUGIN_DIR . 'templates/floating-widget.php';
    }

    /**
     * Add Settings link to Plugins page.
     */
    public function add_settings_link( array $links ): array {
        $settings_link = '<a href="' . admin_url( 'options-general.php?page=gemini-ai-chatbot' ) . '">'
            . __( 'Settings', 'gemini-ai-chatbot' ) . '</a>';
        array_unshift( $links, $settings_link );
        return $links;
    }
}

 

Step 3: Gemini API Communication Layer

includes/class-gemini-api.php

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Handles all communication with the Google Gemini API.
 * Isolated from WordPress so it can be tested independently.
 */
class Gemini_Api {

    private const API_BASE = 'https://generativelanguage.googleapis.com/v1beta/models/';

    private string $api_key;
    private string $model;
    private array  $options;

    public function __construct( array $options ) {
        $this->options = $options;
        $this->api_key = $options['api_key']  ?? '';
        $this->model   = $options['model']    ?? 'gemini-1.5-flash';
    }

    /**
     * Send a conversation to Gemini and return the response.
     *
     * @param  array       $history  Full conversation history (user+model turns)
     * @return array|WP_Error        ['reply' => string, 'tokens' => int] or WP_Error
     */
    public function generate_content( array $history ): array|\WP_Error {

        if ( empty( $this->api_key ) ) {
            return new \WP_Error( 'no_api_key', 'Gemini API key is not configured.' );
        }

        $endpoint = self::API_BASE . $this->model . ':generateContent?key=' . $this->api_key;

        $payload = [
            'system_instruction' => [
                'parts' => [[
                    'text' => $this->options['system_prompt'] ?? 'You are a helpful assistant.'
                ]]
            ],
            'contents'         => $history,
            'generationConfig' => [
                'temperature'     => (float)( $this->options['temperature'] ?? 0.7 ),
                'maxOutputTokens' => (int)(   $this->options['max_tokens']  ?? 1024 ),
                'topP'            => 0.9,
                'topK'            => 40,
            ],
            'safetySettings'   => [
                [ 'category' => 'HARM_CATEGORY_HARASSMENT',        'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' ],
                [ 'category' => 'HARM_CATEGORY_HATE_SPEECH',       'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' ],
                [ 'category' => 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' ],
                [ 'category' => 'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold' => 'BLOCK_MEDIUM_AND_ABOVE' ],
            ],
        ];

        // Use WordPress's wp_remote_post() instead of raw cURL
        // This respects WP HTTP API filters, SSL settings, and proxy configs
        $response = wp_remote_post( $endpoint, [
            'method'    => 'POST',
            'timeout'   => 30,
            'headers'   => [ 'Content-Type' => 'application/json' ],
            'body'      => wp_json_encode( $payload ),
            'sslverify' => true,
        ]);

        // Handle WP HTTP API errors (network, SSL, timeout)
        if ( is_wp_error( $response ) ) {
            $this->log( 'WP HTTP error: ' . $response->get_error_message() );
            return new \WP_Error(
                'http_error',
                $this->friendly_http_error( $response->get_error_code() )
            );
        }

        $http_code = wp_remote_retrieve_response_code( $response );
        $body      = wp_remote_retrieve_body( $response );
        $data      = json_decode( $body, true );

        // Handle API-level errors (401, 429, 500 etc.)
        if ( $http_code !== 200 ) {
            $api_msg    = $data['error']['message'] ?? 'Unknown Gemini API error';
            $api_status = $data['error']['status']  ?? 'UNKNOWN';

            $this->log( "API error {$http_code}: [{$api_status}] {$api_msg}" );

            return new \WP_Error(
                'api_error',
                $this->friendly_api_error( $api_status, $http_code ),
                [ 'status' => $http_code ]
            );
        }

        // Check finish reason
        $finish_reason = $data['candidates'][0]['finishReason'] ?? 'UNKNOWN';
        if ( in_array( $finish_reason, [ 'SAFETY', 'RECITATION', 'PROHIBITED_CONTENT' ], true ) ) {
            return new \WP_Error(
                'blocked',
                "I'm unable to respond to that. Please try rephrasing your message."
            );
        }

        // Extract reply text
        $reply = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;

        if ( $reply === null || $reply === '' ) {
            $this->log( 'Empty reply from Gemini. Response: ' . wp_json_encode( $data ) );
            return new \WP_Error( 'empty_reply', 'The AI returned an empty response. Please try again.' );
        }

        return [
            'reply'         => $reply,
            'finish_reason' => $finish_reason,
            'tokens'        => $data['usageMetadata']['totalTokenCount'] ?? 0,
        ];
    }

    /**
     * Test the API key and connection.
     * Returns true on success, WP_Error on failure.
     */
    public function test_connection(): bool|\WP_Error {
        $result = $this->generate_content([
            [ 'role' => 'user', 'parts' => [[ 'text' => 'Reply with exactly: "OK"' ]] ]
        ]);

        if ( is_wp_error( $result ) ) return $result;
        return true;
    }

    private function friendly_http_error( string $code ): string {
        return match ( $code ) {
            'http_request_timeout' => 'The AI took too long to respond. Please try again.',
            'http_request_failed'  => 'Could not connect to the AI service. Check your server\'s internet access.',
            default                => 'A network error occurred. Please try again.',
        };
    }

    private function friendly_api_error( string $status, int $code ): string {
        return match ( $status ) {
            'INVALID_ARGUMENT'   => 'Invalid request. Please refresh and try again.',
            'RESOURCE_EXHAUSTED' => 'API quota exceeded. Please wait a moment.',
            'UNAUTHENTICATED'    => 'Invalid API key. Please check your settings.',
            'PERMISSION_DENIED'  => 'API key lacks permission for this model.',
            default              => "AI service error ({$code}). Please try again.",
        };
    }

    private function log( string $message ): void {
        if ( ! empty( $this->options['debug_mode'] ) ) {
            error_log( '[Gemini AI Chatbot] ' . $message );
        }
    }
}

 

Step 4: AJAX Request Handler

includes/class-gemini-ajax.php

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Handles all AJAX requests for the chatbot.
 * Registered in Gemini_Chatbot::init_hooks().
 */
class Gemini_Ajax {

    private array $options;

    public function __construct( array $options ) {
        $this->options = $options;
    }

    /**
     * Handle chat message request.
     * Action: wp_ajax_gaicb_chat / wp_ajax_nopriv_gaicb_chat
     */
    public function handle_chat(): void {
        // ── Security: verify nonce ────────────────────────────────────────
        if ( ! check_ajax_referer( 'gaicb_chat_nonce', 'nonce', false ) ) {
            wp_send_json_error([
                'message' => __( 'Security check failed. Please refresh the page.', 'gemini-ai-chatbot' )
            ], 403 );
        }

        // ── Start session for conversation history ────────────────────────
        if ( session_status() === PHP_SESSION_NONE ) {
            session_start();
        }

        // ── Input validation ──────────────────────────────────────────────
        $message = sanitize_text_field( wp_unslash( $_POST['message'] ?? '' ) );

        if ( empty( $message ) ) {
            wp_send_json_error([ 'message' => 'Message cannot be empty.' ], 400 );
        }

        $max_length = 2000;
        if ( mb_strlen( $message ) > $max_length ) {
            wp_send_json_error([
                'message' => sprintf( 'Message too long. Maximum %d characters.', $max_length )
            ], 400 );
        }

        // ── Rate limiting ─────────────────────────────────────────────────
        $rate_check = $this->check_rate_limit();
        if ( is_wp_error( $rate_check ) ) {
            wp_send_json_error([
                'message'     => $rate_check->get_error_message(),
                'retry_after' => $rate_check->get_error_data()['retry_after'] ?? 60,
            ], 429 );
        }

        // ── Build conversation history ────────────────────────────────────
        $session_key = 'gaicb_history_' . md5( session_id() );

        if ( ! isset( $_SESSION[ $session_key ] ) ) {
            $_SESSION[ $session_key ] = [];
        }

        // Add user message
        $_SESSION[ $session_key ][] = [
            'role'  => 'user',
            'parts' => [[ 'text' => $message ]],
        ];

        // Trim to max history
        $max_turns   = (int)( $this->options['max_history'] ?? 20 );
        $max_entries = $max_turns * 2; // × 2 for user + model pairs

        if ( count( $_SESSION[ $session_key ] ) > $max_entries ) {
            $_SESSION[ $session_key ] = array_slice( $_SESSION[ $session_key ], -$max_entries );
        }

        // ── Call Gemini API ───────────────────────────────────────────────
        $api    = new Gemini_Api( $this->options );
        $result = $api->generate_content( $_SESSION[ $session_key ] );

        if ( is_wp_error( $result ) ) {
            // Remove the failed user message from history
            array_pop( $_SESSION[ $session_key ] );

            wp_send_json_error([
                'message' => $result->get_error_message(),
            ], 200 ); // 200 so JS treats as a "bot error message", not a network failure
        }

        // ── Save bot reply to history ─────────────────────────────────────
        $_SESSION[ $session_key ][] = [
            'role'  => 'model',
            'parts' => [[ 'text' => $result['reply'] ]],
        ];

        // ── Return response ───────────────────────────────────────────────
        wp_send_json_success([
            'reply'        => $result['reply'],
            'tokens'       => $result['tokens'],
            'history_size' => count( $_SESSION[ $session_key ] ),
        ]);
    }

    /**
     * Handle conversation reset request.
     * Action: wp_ajax_gaicb_reset / wp_ajax_nopriv_gaicb_reset
     */
    public function handle_reset(): void {
        if ( ! check_ajax_referer( 'gaicb_reset_nonce', 'nonce', false ) ) {
            wp_send_json_error([ 'message' => 'Security check failed.' ], 403 );
        }

        if ( session_status() === PHP_SESSION_NONE ) {
            session_start();
        }

        $session_key = 'gaicb_history_' . md5( session_id() );
        $_SESSION[ $session_key ] = [];

        wp_send_json_success([ 'message' => 'Conversation reset.' ]);
    }

    /**
     * Check per-IP rate limit using WordPress transients.
     *
     * @return true|\WP_Error
     */
    private function check_rate_limit(): bool|\WP_Error {
        $limit  = (int)( $this->options['rate_limit'] ?? 20 );
        $window = 60; // seconds

        // Hash the IP for privacy
        $ip     = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $ip_key = 'gaicb_rl_' . md5( $ip );

        $requests = get_transient( $ip_key );

        if ( $requests === false ) {
            // First request in window
            set_transient( $ip_key, 1, $window );
            return true;
        }

        if ( (int)$requests >= $limit ) {
            $ttl = (int)( get_option( '_transient_timeout_' . $ip_key ) ?? time() ) - time();
            return new \WP_Error(
                'rate_limited',
                __( 'Too many messages. Please wait a moment before sending more.', 'gemini-ai-chatbot' ),
                [ 'retry_after' => max( 0, $ttl ) ]
            );
        }

        set_transient( $ip_key, $requests + 1, $window );
        return true;
    }
}

 

Step 5: Chat Widget Template

templates/chatbot-widget.php

<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>

<?php
// $atts is passed from render_shortcode()
$widget_id    = 'gaicb-' . wp_unique_id();
$chat_height  = absint( $atts['height']     ?? 420 );
$chat_title   = esc_html( $atts['title']    ?? 'AI Assistant' );
$show_reset   = $atts['show_reset'] !== 'false';
$chat_theme   = esc_attr( $atts['theme']    ?? 'dark' );
$placeholder  = esc_attr( $atts['placeholder'] ?? 'Type a message…' );
$accent       = esc_attr( get_option( GAICB_OPTIONS_KEY )['accent_color'] ?? '#6366f1' );
$welcome_msg  = esc_js( get_option( GAICB_OPTIONS_KEY )['welcome_message'] ?? 'Hello! How can I help?' );
?>

<div id="<?php echo esc_attr( $widget_id ); ?>"
     class="gaicb-widget gaicb-theme-<?php echo $chat_theme; ?>"
     style="--gaicb-accent: <?php echo $accent; ?>;"
     data-widget-id="<?php echo esc_attr( $widget_id ); ?>">

    <!-- Header -->
    <div class="gaicb-header">
        <div class="gaicb-header-left">
            <div class="gaicb-avatar">🤖</div>
            <div>
                <div class="gaicb-name"><?php echo $chat_title; ?></div>
                <div class="gaicb-status">
                    <span class="gaicb-dot"></span>
                    <?php esc_html_e( 'Online', 'gemini-ai-chatbot' ); ?>
                </div>
            </div>
        </div>
        <?php if ( $show_reset ) : ?>
        <button class="gaicb-reset-btn"
                data-widget="<?php echo esc_attr( $widget_id ); ?>"
                title="<?php esc_attr_e( 'New conversation', 'gemini-ai-chatbot' ); ?>">
            <?php esc_html_e( 'New chat', 'gemini-ai-chatbot' ); ?>
        </button>
        <?php endif; ?>
    </div>

    <!-- Messages -->
    <div class="gaicb-messages"
         id="<?php echo esc_attr( $widget_id ); ?>-messages"
         style="height: <?php echo $chat_height; ?>px;"
         data-welcome="<?php echo $welcome_msg; ?>">
    </div>

    <!-- Typing Indicator -->
    <div class="gaicb-typing" id="<?php echo esc_attr( $widget_id ); ?>-typing" aria-live="polite">
        <div class="gaicb-typing-bubble">
            <span></span><span></span><span></span>
        </div>
    </div>

    <!-- Input Area -->
    <div class="gaicb-input-area">
        <textarea
            id="<?php echo esc_attr( $widget_id ); ?>-input"
            class="gaicb-input"
            placeholder="<?php echo $placeholder; ?>"
            rows="1"
            maxlength="2000"
            aria-label="<?php esc_attr_e( 'Chat message', 'gemini-ai-chatbot' ); ?>"
        ></textarea>
        <button class="gaicb-send-btn"
                id="<?php echo esc_attr( $widget_id ); ?>-send"
                data-widget="<?php echo esc_attr( $widget_id ); ?>"
                aria-label="<?php esc_attr_e( 'Send message', 'gemini-ai-chatbot' ); ?>">
            <svg viewBox="0 0 24 24" fill="currentColor" width="18" height="18">
                <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
            </svg>
        </button>
    </div>

    <!-- Character Count -->
    <div class="gaicb-footer">
        <span class="gaicb-char-count" id="<?php echo esc_attr( $widget_id ); ?>-chars">0 / 2000</span>
        <span class="gaicb-powered">Powered by Gemini AI</span>
    </div>

</div>

templates/floating-widget.php

<?php if ( ! defined( 'ABSPATH' ) ) exit; ?>

<?php
$options  = get_option( GAICB_OPTIONS_KEY, [] );
$position = esc_attr( $options['widget_position'] ?? 'bottom-right' );
$accent   = esc_attr( $options['accent_color']    ?? '#6366f1' );
$name     = esc_html( $options['chatbot_name']    ?? 'AI Assistant' );
?>

<div id="gaicb-floating"
     class="gaicb-floating gaicb-pos-<?php echo $position; ?>"
     style="--gaicb-accent: <?php echo $accent; ?>;">

    <!-- Trigger Button -->
    <button class="gaicb-bubble-btn"
            id="gaicb-bubble-trigger"
            aria-label="<?php printf( esc_attr__( 'Open %s', 'gemini-ai-chatbot' ), $name ); ?>"
            aria-expanded="false"
            aria-controls="gaicb-floating-panel">
        <span class="gaicb-bubble-icon gaicb-open-icon">💬</span>
        <span class="gaicb-bubble-icon gaicb-close-icon" style="display:none;">✕</span>
        <span class="gaicb-bubble-badge" id="gaicb-badge" style="display:none;">1</span>
    </button>

    <!-- Chat Panel -->
    <div class="gaicb-floating-panel"
         id="gaicb-floating-panel"
         role="dialog"
         aria-label="<?php echo $name; ?>"
         aria-hidden="true"
         style="display:none;">

        <?php
        // Render the full chatbot widget inside the floating panel
        $atts = [
            'title'      => $name,
            'theme'      => $options['theme'] ?? 'dark',
            'height'     => '360',
            'show_reset' => 'true',
            'placeholder'=> 'Type a message…',
        ];
        include __DIR__ . '/chatbot-widget.php';
        ?>
    </div>
</div>

 

Step 6: Frontend JavaScript

assets/js/chatbot-frontend.js

/**
 * Gemini AI Chatbot — Frontend JavaScript
 * Pure vanilla JS — no jQuery dependency.
 */
( function() {
    'use strict';

    // ── State manager for multiple chatbot instances ───────────────────────────
    const instances = {};

    /**
     * Initialize a single chatbot widget instance.
     * @param {string} widgetId — The unique widget element ID
     */
    function initWidget( widgetId ) {
        const root    = document.getElementById( widgetId );
        if ( ! root || instances[ widgetId ] ) return;

        const messagesEl = document.getElementById( widgetId + '-messages' );
        const inputEl    = document.getElementById( widgetId + '-input' );
        const sendBtn    = document.getElementById( widgetId + '-send' );
        const typingEl   = document.getElementById( widgetId + '-typing' );
        const charsEl    = document.getElementById( widgetId + '-chars' );

        // Per-instance state
        const state = {
            isWaiting:   false,
            initialized: false,
        };

        instances[ widgetId ] = state;

        // Show welcome message on first view
        function showWelcome() {
            if ( state.initialized ) return;
            state.initialized = true;
            const welcomeMsg = messagesEl.dataset.welcome || gaicbVars.welcomeMsg;
            if ( welcomeMsg ) appendMessage( 'bot', welcomeMsg );
        }

        // ── Message display ───────────────────────────────────────────────────
        function appendMessage( role, text ) {
            const row = document.createElement( 'div' );
            row.className = 'gaicb-msg-row gaicb-msg-' + role;

            const avatar = document.createElement( 'div' );
            avatar.className = 'gaicb-msg-avatar';
            avatar.textContent = role === 'user' ? '🧑' : '🤖';

            const bubble = document.createElement( 'div' );
            bubble.className = 'gaicb-msg-bubble';

            if ( role === 'bot' || role === 'error' ) {
                bubble.innerHTML = renderMarkdown( text );
            } else {
                bubble.textContent = text;
            }

            if ( role === 'error' ) bubble.classList.add( 'gaicb-msg-error' );

            const time = document.createElement( 'div' );
            time.className = 'gaicb-msg-time';
            time.textContent = new Date().toLocaleTimeString( [], {
                hour: '2-digit', minute: '2-digit'
            });

            const inner = document.createElement( 'div' );
            inner.appendChild( bubble );
            inner.appendChild( time );

            if ( role === 'user' ) {
                row.appendChild( inner );
                row.appendChild( avatar );
            } else {
                row.appendChild( avatar );
                row.appendChild( inner );
            }

            // Insert before typing indicator
            messagesEl.insertBefore( row, typingEl );
            scrollBottom();
        }

        function scrollBottom() {
            setTimeout( () => { messagesEl.scrollTop = messagesEl.scrollHeight; }, 30 );
        }

        // ── Send message ──────────────────────────────────────────────────────
        async function sendMessage() {
            const message = inputEl.value.trim();
            if ( ! message || state.isWaiting ) return;

            setWaiting( true );
            appendMessage( 'user', message );
            inputEl.value = '';
            resizeTextarea();
            updateCharCount();

            try {
                const body = new URLSearchParams({
                    action:  'gaicb_chat',
                    nonce:   gaicbVars.nonce,
                    message: message,
                });

                const resp = await fetch( gaicbVars.ajaxurl, {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body:    body.toString(),
                });

                if ( ! resp.ok ) throw new Error( 'HTTP ' + resp.status );

                const data = await resp.json();

                if ( data.success ) {
                    appendMessage( 'bot', data.data.reply );
                } else {
                    const msg = data.data?.message || gaicbVars.i18n.error;
                    appendMessage( 'error', '⚠️ ' + msg );

                    // Show retry hint for rate limiting
                    if ( data.data?.retry_after ) {
                        const secs = data.data.retry_after;
                        appendMessage( 'bot', `Please wait ${secs} seconds before sending more messages.` );
                    }
                }

            } catch ( err ) {
                appendMessage( 'error', '⚠️ ' + gaicbVars.i18n.error );
                console.error( '[Gemini Chatbot]', err );
            }

            setWaiting( false );
            inputEl.focus();
        }

        // ── Reset conversation ─────────────────────────────────────────────────
        async function resetChat() {
            if ( ! confirm( gaicbVars.i18n.resetConfirm ) ) return;

            try {
                await fetch( gaicbVars.ajaxurl, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: new URLSearchParams({
                        action: 'gaicb_reset',
                        nonce:  gaicbVars.resetNonce,
                    }).toString(),
                });
            } catch ( _ ) { /* Silent — clear client side anyway */ }

            // Clear messages (keep typing indicator)
            messagesEl.querySelectorAll( '.gaicb-msg-row' ).forEach( el => el.remove() );
            state.initialized = false;
            showWelcome();
        }

        // ── Minimal markdown renderer ──────────────────────────────────────────
        function renderMarkdown( text ) {
            let html = escHtml( text );

            // Code blocks
            html = html.replace( /```(\w*)\n?([\s\S]*?)```/g, ( _, lang, code ) =>
                `<pre><code class="lang-${ lang || 'text' }">${ code.trim() }</code></pre>`
            );
            // Inline code
            html = html.replace( /`([^`\n]+)`/g, '<code>$1</code>' );
            // Bold
            html = html.replace( /\*\*(.+?)\*\*/g, '<strong>$1</strong>' );
            // Italic
            html = html.replace( /\*([^*\n]+)\*/g, '<em>$1</em>' );
            // Headings
            html = html.replace( /^### (.+)$/gm, '<h3>$1</h3>' );
            html = html.replace( /^## (.+)$/gm,  '<h2>$1</h2>' );
            html = html.replace( /^# (.+)$/gm,   '<h1>$1</h1>' );
            // Unordered list
            html = html.replace( /^[-*] (.+)$/gm, '<li>$1</li>' );
            html = html.replace( /((<li>.*?<\/li>[\n]?)+)/g, '<ul>$1</ul>' );
            // Links
            html = html.replace(
                /(?<!href=["'])(https?:\/\/[^\s<>"]+)/g,
                '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>'
            );
            // Newlines → breaks
            html = html.replace( /\n\n+/g, '</p><p>' );
            html = html.replace( /([^>])\n([^<])/g, '$1<br>$2' );

            return '<p>' + html + '</p>';
        }

        function escHtml( str ) {
            return str
                .replace( /&/g,  '&amp;' )
                .replace( /</g,  '&lt;' )
                .replace( />/g,  '&gt;' )
                .replace( /"/g,  '&quot;' )
                .replace( /'/g,  '&#039;' );
        }

        // ── UI helpers ─────────────────────────────────────────────────────────
        function setWaiting( waiting ) {
            state.isWaiting    = waiting;
            sendBtn.disabled   = waiting;
            inputEl.disabled   = waiting;
            typingEl.style.display = waiting ? 'flex' : 'none';
            if ( waiting ) scrollBottom();
        }

        function resizeTextarea() {
            inputEl.style.height = 'auto';
            inputEl.style.height = Math.min( inputEl.scrollHeight, 120 ) + 'px';
        }

        function updateCharCount() {
            const len = inputEl.value.length;
            if ( charsEl ) {
                charsEl.textContent = len + ' / 2000';
                charsEl.className = 'gaicb-char-count' +
                    ( len > 1800 ? ' warn'  : '' ) +
                    ( len >= 2000 ? ' limit' : '' );
            }
        }

        // ── Event listeners ────────────────────────────────────────────────────
        sendBtn.addEventListener( 'click', sendMessage );

        inputEl.addEventListener( 'keydown', function( e ) {
            if ( e.key === 'Enter' && ! e.shiftKey ) {
                e.preventDefault();
                sendMessage();
            }
        });

        inputEl.addEventListener( 'input', function() {
            resizeTextarea();
            updateCharCount();
        });

        // Reset button (shortcode widget)
        const resetBtn = root.querySelector( '.gaicb-reset-btn' );
        if ( resetBtn ) resetBtn.addEventListener( 'click', resetChat );

        // Show welcome on init
        showWelcome();
    }

    // ── Floating Widget Toggle ─────────────────────────────────────────────────
    function initFloatingWidget() {
        const trigger = document.getElementById( 'gaicb-bubble-trigger' );
        const panel   = document.getElementById( 'gaicb-floating-panel' );
        if ( ! trigger || ! panel ) return;

        let isOpen = false;

        trigger.addEventListener( 'click', function() {
            isOpen = ! isOpen;
            panel.style.display = isOpen ? 'block' : 'none';
            panel.setAttribute( 'aria-hidden', String( ! isOpen ) );
            trigger.setAttribute( 'aria-expanded', String( isOpen ) );
            trigger.querySelector( '.gaicb-open-icon' ).style.display  = isOpen ? 'none'  : '';
            trigger.querySelector( '.gaicb-close-icon' ).style.display = isOpen ? ''      : 'none';

            // Dismiss badge
            const badge = document.getElementById( 'gaicb-badge' );
            if ( badge ) badge.style.display = 'none';

            // Init the inner widget on first open
            if ( isOpen ) {
                const innerWidget = panel.querySelector( '[id^="gaicb-"]' );
                if ( innerWidget ) initWidget( innerWidget.id );
            }
        });

        // Close on Escape key
        document.addEventListener( 'keydown', function( e ) {
            if ( e.key === 'Escape' && isOpen ) trigger.click();
        });
    }

    // ── Bootstrap all widgets on page ─────────────────────────────────────────
    document.addEventListener( 'DOMContentLoaded', function() {
        // Init all shortcode widgets
        document.querySelectorAll( '.gaicb-widget[id]' ).forEach( el => {
            // Don't init widgets inside the floating panel (init on open)
            if ( ! el.closest( '#gaicb-floating' ) ) {
                initWidget( el.id );
            }
        });

        // Init floating widget toggle
        initFloatingWidget();
    });

} )();

 

Step 7: Frontend CSS (Complete Styled System)

assets/css/chatbot-frontend.css

/* ── Gemini AI Chatbot — Frontend Styles ──────────────────────────────────── */

/* ── CSS Custom Properties ─────────────────────────────────────────────────── */
.gaicb-widget {
    --gaicb-accent:      #6366f1;
    --gaicb-bg:          #1e293b;
    --gaicb-bg-2:        #0f172a;
    --gaicb-bg-3:        #334155;
    --gaicb-text:        #f1f5f9;
    --gaicb-text-2:      #94a3b8;
    --gaicb-text-3:      #64748b;
    --gaicb-border:      #334155;
    --gaicb-user-bubble: var(--gaicb-accent);
    --gaicb-bot-bubble:  #1e293b;
    --gaicb-radius:      14px;
    --gaicb-font:        -apple-system, 'Segoe UI', system-ui, sans-serif;

    font-family: var(--gaicb-font);
    font-size: 14px;
    line-height: 1.6;
    color: var(--gaicb-text);
    background: var(--gaicb-bg);
    border: 1px solid var(--gaicb-border);
    border-radius: var(--gaicb-radius);
    overflow: hidden;
    box-shadow: 0 8px 30px rgba(0,0,0,0.3);
    max-width: 640px;
    width: 100%;
}

/* ── Light Theme Override ──────────────────────────────────────────────────── */
.gaicb-theme-light {
    --gaicb-bg:          #ffffff;
    --gaicb-bg-2:        #f8fafc;
    --gaicb-bg-3:        #f1f5f9;
    --gaicb-text:        #0f172a;
    --gaicb-text-2:      #475569;
    --gaicb-text-3:      #94a3b8;
    --gaicb-border:      #e2e8f0;
    --gaicb-bot-bubble:  #f1f5f9;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}

/* ── Header ────────────────────────────────────────────────────────────────── */
.gaicb-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 16px;
    background: var(--gaicb-accent);
    gap: 10px;
}

.gaicb-header-left {
    display: flex;
    align-items: center;
    gap: 10px;
}

.gaicb-avatar {
    width: 36px; height: 36px;
    background: rgba(255,255,255,0.18);
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 18px;
    flex-shrink: 0;
}

.gaicb-name {
    font-weight: 700;
    font-size: 0.95rem;
    color: #fff;
}

.gaicb-status {
    font-size: 0.72rem;
    color: rgba(255,255,255,0.78);
    display: flex;
    align-items: center;
    gap: 5px;
}

.gaicb-dot {
    width: 6px; height: 6px;
    background: #4ade80;
    border-radius: 50%;
    animation: gaicb-pulse 2s infinite;
}

@keyframes gaicb-pulse {
    0%, 100% { opacity:1; transform:scale(1); }
    50%       { opacity:.5; transform:scale(.85); }
}

.gaicb-reset-btn {
    background: rgba(255,255,255,0.14);
    border: 1px solid rgba(255,255,255,0.2);
    color: #fff;
    padding: 5px 12px;
    border-radius: 20px;
    font-size: 0.75rem;
    cursor: pointer;
    transition: background .2s;
    white-space: nowrap;
}
.gaicb-reset-btn:hover { background: rgba(255,255,255,0.25); }

/* ── Messages ──────────────────────────────────────────────────────────────── */
.gaicb-messages {
    overflow-y: auto;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    scroll-behavior: smooth;
    background: var(--gaicb-bg-2);
}

.gaicb-messages::-webkit-scrollbar { width: 4px; }
.gaicb-messages::-webkit-scrollbar-thumb {
    background: var(--gaicb-bg-3);
    border-radius: 99px;
}

/* ── Message Rows ──────────────────────────────────────────────────────────── */
.gaicb-msg-row {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    animation: gaicb-msg-in .2s ease-out;
}

@keyframes gaicb-msg-in {
    from { opacity:0; transform:translateY(8px); }
    to   { opacity:1; transform:translateY(0); }
}

.gaicb-msg-user { flex-direction: row-reverse; }

.gaicb-msg-avatar {
    width: 28px; height: 28px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 14px;
    background: var(--gaicb-bg-3);
    flex-shrink: 0;
}

.gaicb-msg-user .gaicb-msg-avatar { background: rgba(99,102,241,0.2); }

.gaicb-msg-bubble {
    max-width: 78%;
    padding: 10px 14px;
    border-radius: 14px;
    font-size: 0.88rem;
    line-height: 1.65;
    word-break: break-word;
}

.gaicb-msg-bot .gaicb-msg-bubble {
    background: var(--gaicb-bot-bubble);
    color: var(--gaicb-text);
    border: 1px solid var(--gaicb-border);
    border-bottom-left-radius: 4px;
}

.gaicb-msg-user .gaicb-msg-bubble {
    background: var(--gaicb-accent);
    color: #fff;
    border-bottom-right-radius: 4px;
}

.gaicb-msg-error .gaicb-msg-bubble {
    background: rgba(239,68,68,0.08);
    border-color: rgba(239,68,68,0.25);
    color: #fca5a5;
}

.gaicb-msg-time {
    font-size: 0.68rem;
    color: var(--gaicb-text-3);
    margin-top: 3px;
}

.gaicb-msg-user .gaicb-msg-time { text-align: right; }

/* ── Markdown Inside Bot Bubbles ─────────────────────────────────────────── */
.gaicb-msg-bubble pre {
    background: rgba(0,0,0,0.35);
    border: 1px solid var(--gaicb-border);
    border-radius: 6px;
    padding: 10px;
    overflow-x: auto;
    font-size: .82rem;
    margin: 8px 0;
}

.gaicb-msg-bubble code {
    font-family: 'JetBrains Mono','Fira Code','Courier New',monospace;
    font-size: .85em;
}

.gaicb-msg-bubble p:not(:last-child) { margin-bottom: 6px; }
.gaicb-msg-bubble ul { padding-left: 18px; margin: 6px 0; }
.gaicb-msg-bubble strong { font-weight: 600; }
.gaicb-msg-bubble a { color: var(--gaicb-accent); text-decoration: underline; }

/* ── Typing Indicator ─────────────────────────────────────────────────────── */
.gaicb-typing {
    display: none;
    align-items: center;
    gap: 8px;
    padding: 0 16px 12px;
    background: var(--gaicb-bg-2);
}

.gaicb-typing-bubble {
    background: var(--gaicb-bot-bubble);
    border: 1px solid var(--gaicb-border);
    border-radius: 14px;
    border-bottom-left-radius: 4px;
    padding: 12px 16px;
    display: flex;
    gap: 5px;
    align-items: center;
}

.gaicb-typing-bubble span {
    width: 7px; height: 7px;
    background: var(--gaicb-text-3);
    border-radius: 50%;
    animation: gaicb-bounce 1.3s infinite ease-in-out;
}

.gaicb-typing-bubble span:nth-child(2) { animation-delay: .2s; }
.gaicb-typing-bubble span:nth-child(3) { animation-delay: .4s; }

@keyframes gaicb-bounce {
    0%,60%,100% { transform:translateY(0); opacity:.4; }
    30%          { transform:translateY(-5px); opacity:1; }
}

/* ── Input Area ──────────────────────────────────────────────────────────── */
.gaicb-input-area {
    display: flex;
    gap: 8px;
    align-items: flex-end;
    padding: 10px 12px;
    border-top: 1px solid var(--gaicb-border);
    background: var(--gaicb-bg);
}

.gaicb-input {
    flex: 1;
    background: var(--gaicb-bg-2);
    border: 1px solid var(--gaicb-border);
    color: var(--gaicb-text);
    padding: 9px 12px;
    border-radius: 10px;
    font-family: var(--gaicb-font);
    font-size: .88rem;
    resize: none;
    outline: none;
    min-height: 38px;
    max-height: 120px;
    transition: border-color .2s;
    line-height: 1.5;
}

.gaicb-input::placeholder { color: var(--gaicb-text-3); }
.gaicb-input:focus { border-color: var(--gaicb-accent); }
.gaicb-input:disabled { opacity: .5; cursor: not-allowed; }

.gaicb-send-btn {
    background: var(--gaicb-accent);
    border: none;
    color: #fff;
    width: 38px; height: 38px;
    border-radius: 10px;
    cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    flex-shrink: 0;
    transition: opacity .2s, transform .1s;
}

.gaicb-send-btn:hover:not(:disabled) { opacity: .88; transform: scale(1.04); }
.gaicb-send-btn:disabled { opacity: .35; cursor: not-allowed; }

/* ── Footer / Char Count ─────────────────────────────────────────────────── */
.gaicb-footer {
    display: flex;
    justify-content: space-between;
    padding: 4px 14px 10px;
    background: var(--gaicb-bg);
}

.gaicb-char-count { font-size: .7rem; color: var(--gaicb-text-3); }
.gaicb-char-count.warn  { color: #f59e0b; }
.gaicb-char-count.limit { color: #ef4444; }
.gaicb-powered { font-size: .7rem; color: var(--gaicb-text-3); font-style: italic; }

/* ── Floating Widget ──────────────────────────────────────────────────────── */
.gaicb-floating {
    position: fixed;
    z-index: 999999;
}

.gaicb-pos-bottom-right { bottom: 24px; right: 24px; }
.gaicb-pos-bottom-left  { bottom: 24px; left:  24px; }
.gaicb-pos-top-right    { top:    24px; right: 24px; }
.gaicb-pos-top-left     { top:    24px; left:  24px; }

.gaicb-bubble-btn {
    width: 56px; height: 56px;
    background: var(--gaicb-accent);
    border: none;
    border-radius: 50%;
    color: #fff;
    font-size: 24px;
    cursor: pointer;
    box-shadow: 0 4px 20px rgba(0,0,0,0.3);
    display: flex; align-items: center; justify-content: center;
    position: relative;
    transition: transform .2s, box-shadow .2s;
}

.gaicb-bubble-btn:hover {
    transform: scale(1.08);
    box-shadow: 0 6px 28px rgba(0,0,0,0.4);
}

.gaicb-bubble-badge {
    position: absolute;
    top: -4px; right: -4px;
    width: 18px; height: 18px;
    background: #ef4444;
    color: #fff;
    border-radius: 50%;
    font-size: 10px;
    font-weight: 700;
    display: flex; align-items: center; justify-content: center;
    border: 2px solid #fff;
}

.gaicb-floating-panel {
    position: absolute;
    bottom: 70px;
    right: 0;
    width: 360px;
    animation: gaicb-panel-in .25s ease-out;
}

.gaicb-pos-bottom-left .gaicb-floating-panel  { right: auto; left: 0; }
.gaicb-pos-top-right   .gaicb-floating-panel  { bottom: auto; top: 70px; }
.gaicb-pos-top-left    .gaicb-floating-panel  { bottom: auto; top: 70px; right: auto; left: 0; }

@keyframes gaicb-panel-in {
    from { opacity: 0; transform: translateY(12px) scale(.96); }
    to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* ── Responsive ────────────────────────────────────────────────────────────── */
@media ( max-width: 420px ) {
    .gaicb-floating-panel { width: calc(100vw - 32px); }
    .gaicb-msg-bubble { max-width: 88%; }
}

 

Step 8: Plugin Uninstall Cleanup

uninstall.php

<?php
/**
 * Gemini AI Chatbot — Uninstall Cleanup
 * Runs when user clicks "Delete" in WordPress admin.
 * Removes ALL plugin data from the database.
 */

// WordPress security check
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

// ── Remove plugin options ─────────────────────────────────────────────────────
delete_option( 'gaicb_settings' );

// ── Remove any leftover rate limiting transients ──────────────────────────────
global $wpdb;
$wpdb->query(
    "DELETE FROM {$wpdb->options}
     WHERE option_name LIKE '_transient_gaicb_rl_%'
        OR option_name LIKE '_transient_timeout_gaicb_rl_%'"
);

// ── Remove user meta if any was stored ───────────────────────────────────────
$wpdb->query(
    "DELETE FROM {$wpdb->usermeta}
     WHERE meta_key LIKE 'gaicb_%'"
);

 

Step 9: Using the Plugin — All Configuration Options

Basic Shortcode Usage

[gemini_chatbot]

Advanced Shortcode With All Options

[gemini_chatbot
  title="Ask Our AI"
  theme="light"
  height="500"
  show_reset="true"
  placeholder="How can we help you?"
]

Shortcode in PHP Templates

// In any WordPress theme template file
echo do_shortcode( '[gemini_chatbot title="Support" theme="dark"]' );

Shortcode in a Widget Area

Add a Text or HTML widget in Appearance → Widgets and paste [gemini_chatbot]. Works in sidebars, footers, and any widgetised area.

Shortcode with Elementor / Divi / WPBakery

All major page builders have a “Shortcode” element. Paste [gemini_chatbot] inside it. The chatbot renders fully styled within the page builder’s layout.

PHP Function (Direct Embed)

// Direct output in a template (no shortcode)
if ( function_exists( 'gaicb_init' ) ) {
    echo do_shortcode( '[gemini_chatbot]' );
}

 

Part 10 — System Prompt Examples for Different WordPress Use Cases

The system prompt is what makes your chatbot unique. Here are battle-tested prompts for common WordPress site types:

WooCommerce Store Support Bot

You are a customer support agent for [Store Name], an online store.

Knowledge base:
- Shipping: Free above ₹999. Standard delivery 3-5 days. Express available.
- Returns: 30-day return policy. Items must be unused in original packaging.
- Payment: Razorpay (UPI, cards, netbanking). COD available for orders under ₹2000.
- Tracking: Available via order confirmation email or My Account page.

Rules:
- Always be polite and solution-focused.
- For order-specific queries, ask for their order number.
- For refund requests, collect: order number, reason, preferred resolution.
- Never promise compensation not listed in official policy.
- For issues you cannot resolve, end with: "I'll escalate this to our team at support@store.com"

Real Estate Lead Generation Bot

You are a property consultant for [Agency Name].

Your role:
- Help visitors explore available properties (apartments, villas, commercial)
- Understand their requirements: location, budget, BHK, possession timeline
- Explain project features, amenities, and pricing (say "starting from ₹X")
- Collect lead information: name, phone, email, requirement

Conversation flow:
1. Greet and ask what they're looking for
2. Understand budget and preferred location
3. Recommend matching projects
4. Offer a free site visit or callback
5. Collect contact details

Always be friendly and never pressure the visitor.

Educational / LMS Platform Bot

You are a learning assistant for [Platform Name], an online education platform.

You can help with:
- Course recommendations based on learner goals and current skills
- Explaining course topics and prerequisites
- Troubleshooting login and enrollment issues (ask them to try password reset first)
- Certificate download instructions (My Courses → Certificate button)
- Study tips and learning strategies

You cannot:
- Give exam answers directly
- Access individual student accounts
- Process refunds (direct to billing@platform.com)

Always encourage learners and celebrate their progress.

Healthcare / Clinic Appointment Bot

You are a helpful assistant for [Clinic Name].

You can help with:
- Answering questions about services and specializations
- Explaining appointment booking process
- General health information (always recommend consulting a doctor)
- Clinic timings: Mon–Sat, 9 AM – 7 PM
- Contact: 011-XXXXXXXX | clinic@example.com

Important rules:
- NEVER provide medical diagnosis
- NEVER recommend specific medications or dosages
- For emergencies, always say: "Please call 112 or visit the nearest emergency room immediately"
- For appointment booking, direct to: [booking URL] or call the clinic

Always remind users that your responses are informational, not medical advice.
 

 

Part 11 — Troubleshooting: Every Common Issue Solved

Issue 1: Chatbot Shows “Security check failed”

Cause: Nonce expired (default 12-hour lifespan) or caching plugin served a stale page.

Fix: Clear your caching plugin’s cache after activating the plugin. Also exclude admin-ajax.php from page cache:

  • W3 Total Cache: Performance → Page Cache → Advanced → Never cache these pages: add /wp-admin/admin-ajax.php
  • WP Rocket: Settings → Advanced Rules → Never cache URLs: add admin-ajax.php

Issue 2: “Invalid API key” Error

Checklist:

  1. Go to Settings → Gemini Chatbot → Verify key is saved (check masked display)
  2. Re-enter the key (copy fresh from Google AI Studio)
  3. Confirm the key has Generative Language API enabled in Google Cloud Console
  4. Check if you’ve exceeded your project’s quota

Issue 3: Chatbot Works for Admin But Not Logged-Out Users

Cause: wp_ajax_nopriv_ actions not registered, or session issues for non-authenticated users.

Fix: Verify both actions are registered in init_hooks():

add_action( 'wp_ajax_gaicb_chat',        [ $ajax, 'handle_chat' ] );
add_action( 'wp_ajax_nopriv_gaicb_chat', [ $ajax, 'handle_chat' ] ); // ← This one

Issue 4: Floating Widget Not Appearing

Checklist:

  1. Go to Settings → Gemini Chatbot → Display Options → confirm “Floating Chat Widget” is checked
  2. Check for CSS z-index conflicts with your theme’s header (try setting z-index: 999999)
  3. Disable other plugins to check for JavaScript conflicts
  4. Check browser console for JS errors

Issue 5: Session History Not Persisting Between Pages

Cause A: session_start() called after output — common with WordPress where themes output HTML early.

Fix: In class-gemini-ajax.php, use WordPress’s session alternative:

// Instead of native PHP sessions, use WordPress transients per user
$user_key = 'gaicb_hist_' . md5( wp_get_session_token() ?: session_id() );
$history  = get_transient( $user_key ) ?: [];
// ... after updating history:
set_transient( $user_key, $history, 2 * HOUR_IN_SECONDS );

Issue 6: Styling Conflicts with Theme

Cause: Theme CSS overrides chatbot styles (common with .message, .chat, .button classes).

Fix: The plugin uses the gaicb- prefix namespace for all CSS classes. If conflicts persist, increase specificity:

/* In your child theme's style.css */
#gaicb-floating .gaicb-send-btn,
.gaicb-widget .gaicb-send-btn {
    background: #6366f1 !important;
    border-radius: 10px !important;
}

Issue 7: Rate Limit Hits Even With Low Traffic

Cause: Multiple users share the same IP (office, university, behind a NAT).

Fix: Increase rate limit in settings, or add user-agent variation to the rate limit key:

// In class-gemini-ajax.php — make rate limit key more granular
$ip_key = 'gaicb_rl_' . md5( $ip . ( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );

 

Part 12 — WordPress Coding Standards Compliance

This plugin follows all WordPress coding standards:

Standard Implementation
Namespacing GAICB_ prefix on all constants, functions, and hooks
Security Nonce on every AJAX call, check_ajax_referer(), current_user_can()
Sanitization All input sanitized (sanitize_text_field, absint, wp_kses)
Escaping All output escaped (esc_html, esc_attr, esc_url, wp_json_encode)
i18n All user-facing strings wrapped in __() with text domain
Settings API Uses register_setting, add_settings_section, add_settings_field
HTTP API Uses wp_remote_post() not raw cURL
Options Uses get_option/update_option — no custom DB tables
Hooks No direct execution — all code runs through action/filter hooks
Uninstall uninstall.php removes all plugin data cleanly
Singleton Core class uses singleton pattern — one instance only

Mathed 2:

Let’s create Gemini chatbot into WordPress so you can use it anywhere on your site via a shortcode (e.g., [gemini_chatbot]).

Step 1: Create a WordPress Plugin

  1. Go to your WordPress installation:
    wp-content/plugins/
    
            
  2. Create a new folder:
    gemini-chatbot
    
            
  3. Inside it, create a file:
    gemini-chatbot.php
            

Step 2: Plugin Code (gemini-chatbot.php)

 

<?php
/*
Plugin Name: Gemini Chatbot
Description: A chatbot powered by Google Gemini API.
Version: 1.0
Author: Tabir Ahmad [ipdata.in]
*/

if (!defined('ABSPATH')) exit;

// Enqueue JS + CSS
function gemini_chatbot_enqueue_scripts() {
    wp_enqueue_script('gemini-chatbot-js', plugin_dir_url(__FILE__) . 'gemini-chatbot.js', ['jquery'], '1.0', true);
    wp_localize_script('gemini-chatbot-js', 'geminiChatbot', [
        'ajaxurl' => admin_url('admin-ajax.php'),
    ]);

    wp_enqueue_style('gemini-chatbot-css', plugin_dir_url(__FILE__) . 'gemini-chatbot.css');
}
add_action('wp_enqueue_scripts', 'gemini_chatbot_enqueue_scripts');

// Shortcode to display chatbot
function gemini_chatbot_shortcode() {
    ob_start(); ?>
    <div class="gemini-chatbox">
        <div class="messages" id="gemini-messages"></div>
        <input type="text" id="gemini-input" placeholder="Type a message..." />
        <button onclick="geminiSend()">Send</button>
        <button onclick="geminiReset()">Reset</button>
    </div>
    <?php
    return ob_get_clean();
}
add_shortcode('gemini_chatbot', 'gemini_chatbot_shortcode');

// Handle AJAX request
function gemini_chatbot_ajax() {
    session_start();
    $apiKey = "YOUR_GEMINI_API_KEY"; // 🔑 Replace with your key

    $userMessage = sanitize_text_field($_POST['message']);

    if (!isset($_SESSION['gemini_history'])) {
        $_SESSION['gemini_history'] = [];
    }

    $_SESSION['gemini_history'][] = [
        "role" => "user",
        "parts" => [["text" => $userMessage]]
    ];

    $data = ["contents" => $_SESSION['gemini_history']];

    $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=" . $apiKey);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
    $response = curl_exec($ch);
    curl_close($ch);

    $responseData = json_decode($response, true);
    $botReply = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? "⚠️ No response from Gemini";

    $_SESSION['gemini_history'][] = [
        "role" => "model",
        "parts" => [["text" => $botReply]]
    ];

    wp_send_json(["reply" => $botReply]);
}
add_action('wp_ajax_gemini_chatbot', 'gemini_chatbot_ajax');
add_action('wp_ajax_nopriv_gemini_chatbot', 'gemini_chatbot_ajax');

// Handle reset
function gemini_chatbot_reset() {
    session_start();
    $_SESSION['gemini_history'] = [];
    wp_send_json(["status" => "reset"]);
}
add_action('wp_ajax_gemini_reset', 'gemini_chatbot_reset');
add_action('wp_ajax_nopriv_gemini_reset', 'gemini_chatbot_reset');

        

Step 3: JavaScript (gemini-chatbot.js)

 

async function geminiSend() {
    const input = document.getElementById("gemini-input");
    const message = input.value.trim();
    if (!message) return;

    addGeminiMessage(message, "user");
    input.value = "";

    const response = await fetch(geminiChatbot.ajaxurl + "?action=gemini_chatbot", {
        method: "POST",
        headers: { "Content-Type": "application/x-www-form-urlencoded" },
        body: "message=" + encodeURIComponent(message)
    });

    const data = await response.json();
    addGeminiMessage(data.reply, "bot");
}

function addGeminiMessage(text, type) {
    const messagesDiv = document.getElementById("gemini-messages");
    const msgDiv = document.createElement("div");
    msgDiv.className = "msg " + type;
    msgDiv.textContent = text;
    messagesDiv.appendChild(msgDiv);
    messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

async function geminiReset() {
    await fetch(geminiChatbot.ajaxurl + "?action=gemini_reset", { method: "POST" });
    document.getElementById("gemini-messages").innerHTML = "";
}

        

Step 4: CSS (gemini-chatbot.css)

 

.gemini-chatbox {
    width: 100%;
    max-width: 400px;
    background: #fff;
    border-radius: 10px;
    box-shadow: 0 0 10px rgba(0,0,0,.1);
    padding: 15px;
}
.gemini-chatbox .messages {
    height: 300px;
    overflow-y: auto;
    border: 1px solid #ddd;
    padding: 10px;
    margin-bottom: 10px;
    border-radius: 5px;
}
.msg {
    margin: 5px 0;
    padding: 8px;
    border-radius: 6px;
}
.msg.user { background: #d1e7dd; text-align: right; }
.msg.bot { background: #e2e3e5; text-align: left; }

        

Step 5: Usage

  1. Upload this plugin folder to wp-content/plugins/.

  2. Activate Gemini Chatbot in WordPress admin.

  3. Add shortcode in any post or page:

[gemini_chatbot]
        

Production Deployment Checklist

Before going live:

Configuration

  • API key entered and saved in Settings → Gemini Chatbot
  • System prompt written for your specific use case
  • Chatbot name and welcome message customised
  • Model selected (Flash recommended for production)
  • Debug mode disabled

Display

  • Shortcode tested in a test post/page
  • Floating widget enabled and positioned correctly
  • Mobile view tested (widget renders inside viewport)
  • Dark/light theme chosen to match your site

Security

  • Caching plugin configured to exclude admin-ajax.php
  • Rate limit tested (try sending 25 messages rapidly)
  • Nonce validation confirmed (check browser console for 403 errors)

Performance

  • Max history set appropriately (20 turns = ~15KB of session data)
  • Max output tokens set to reasonable value (1024 for most chat UIs)

What This Plugin Gives You vs the Original

The original article gives you 4 files and a working demo. This guide gives you a complete commercial-grade WordPress plugin built on the same foundation but with everything a production WordPress plugin needs:

  • WordPress Settings API — no hardcoded API keys, admin UI for all config
  • 3-class architecture — API, AJAX, and core logic are separated and independently testable
  • Security layer — nonces, rate limiting, sanitization, escaping, output buffering
  • Multi-instance support — place 5 different chatbots on 5 different pages with different personas
  • Floating widget — site-wide AI assistant with one checkbox, no shortcode needed
  • i18n-ready — full translation support via standard WordPress text domain
  • Uninstall cleanup — no orphaned data left in the database
  • WordPress HTTP API — uses wp_remote_post() so it respects WordPress proxy/SSL settings
  • Accessibility — ARIA labels, keyboard navigation, focus management

The architecture in this guide is the foundation. Add a database layer for conversation logging, integrate with WooCommerce for order-aware support, or connect to your product catalog for inventory queries — the class structure makes all of it straightforward to extend.

Frequently Asked Questions

+

What is a Gemini AI Chatbot for WordPress?

A Gemini AI Chatbot is an intelligent virtual assistant powered by Google's Gemini API that can answer user questions, provide support, and automate conversations directly on a WordPress website.
+

Why should I add an AI chatbot to my WordPress website?

An AI chatbot can: Provide 24/7 customer support Answer frequently asked questions Improve user engagement Generate leads Reduce support workload Increase website conversions
+

Do I need coding knowledge to create a Gemini chatbot in WordPress?

Basic knowledge of PHP, WordPress plugin development, HTML, CSS, and JavaScript is helpful. However, developers can follow step-by-step tutorials to build and customize the chatbot.
+

Is Google Gemini API free to use?

Google provides free API usage limits for Gemini. Depending on your chatbot traffic and selected AI model, additional usage may require a paid plan.
+

Can I integrate the chatbot with WooCommerce?

Yes. A Gemini chatbot can help customers with: Product recommendations Order information Shipping questions Customer support Frequently asked questions
+

Gemini AI Chatbot vs Traditional Rule-Based Chatbot

Feature Gemini AI Chatbot Rule-Based Chatbot
Understands Natural Language
Learns Context
Dynamic Responses
Multi-language Support Limited
Customer Experience Excellent Basic
AI-Powered Conversations
Previous Article

Build a Full Interactive AI Chatbot with PHP + HTML/CSS/JavaScript + Gemini API — The Complete Production Guide (2025)

Next Article

How to Send WordPress Enquiries Automatically to WhatsApp — The Complete Developer's Guide

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨