# UtilioIn AI Website Assistant

> **Turn any WordPress site into an AI-powered, conversational knowledge base.**
>
> UtilioIn AI Website Assistant indexes your existing content (posts, pages, custom post
> types, taxonomies, widgets and more), stores it in a local vector index, and answers
> visitor questions with **grounded, cited** responses generated by the AI provider *you*
> choose. No external SaaS, no lock-in — you bring your own API key (BYOK).

---

## Table of Contents

1. [Overview](#overview)
2. [Key Features](#key-features)
3. [How It Works](#how-it-works)
4. [Requirements](#requirements)
5. [Installation](#installation)
6. [Quick Start](#quick-start)
7. [Admin Screens](#admin-screens)
8. [Settings Reference](#settings-reference)
9. [Frontend Integration](#frontend-integration)
10. [REST API Reference](#rest-api-reference)
11. [Database Schema](#database-schema)
12. [Supported AI Providers](#supported-ai-providers)
13. [Developer Guide](#developer-guide)
14. [Security & Privacy](#security--privacy)
15. [Troubleshooting & FAQ](#troubleshooting--faq)
16. [Support](#support)
17. [Changelog](#changelog)
18. [License](#license)

---

## Overview

UtilioIn AI Website Assistant is a self-contained WordPress plugin that adds a
retrieval-augmented generation (RAG) assistant to your website. Instead of hoping the
language model "knows" your content, the plugin **retrieves the most relevant chunks of
your own indexed content** and feeds them to the model as context for every question. The
result is a chatbot that answers accurately about *your* site and admits when it does not
know, rather than hallucinating generic answers.

Everything runs inside WordPress:

- Content is discovered and chunked locally.
- Embeddings are generated through your provider and stored in your database.
- Search combines **keyword matching and vector similarity** (hybrid search).
- The chat widget is a lightweight, dependency-light JavaScript bundle.

---

## Key Features

| Feature | Description |
| --- | --- |
| 🔍 **Automatic content indexing** | Scans posts, pages, custom post types, taxonomies, widgets and (optionally) uploaded documents. |
| 🧠 **Hybrid search (RAG)** | Blends keyword scoring with cosine similarity over embeddings for precise retrieval. |
| 📚 **Grounded, cited answers** | Replies are generated only from retrieved context; the widget lists the sources used. |
| ✍️ **Manual knowledge** | A free-text field for facts that live outside your content (address, phone, hours, policies). |
| 🛡️ **AI instructions & guardrails** | Admin-defined rules appended to the system prompt of every request (tone, red lines, escalation). |
| 🔑 **Bring Your Own Key** | Supports OpenAI, OpenRouter and any OpenAI-compatible endpoint. |
| 🔐 **Encrypted secrets** | The API key is stored AES-256-GCM encrypted at rest. |
| 🎨 **Custom launcher icon** | Upload your own chat button icon from the Media Library; falls back to a built-in SVG. |
| 🖼️ **Per-assistant logo & avatar** | Each assistant can carry its own uploaded **Logo** (launcher) and a selectable **Avatar** preset; both fall back gracefully. |
| 🧩 **Multiple embed options** | Floating widget, `[utilioin_aiwa]` shortcode, and a native Gutenberg block. |
| 🔌 **Test Connection** | One-click check that both the chat and embedding models respond correctly. |
| 📊 **Analytics & diagnostics** | Conversation logs, feedback, indexing jobs and a developer diagnostics screen. |
| ♿ **Accessible & responsive** | Keyboard-friendly widget that adapts to small screens. |
| 🧹 **Clean uninstall** | Optionally purge all plugin data on uninstall. |

---

## How It Works

```
                    ┌──────────────────────── WordPress ────────────────────────┐
                    │                                                           │
  Visitor ── asks ──▶  Chat widget / shortcode / block                         │
                    │        │                                                  │
                    │        ▼                                                  │
                    │  ChatController (AJAX / REST)                             │
                    │        │                                                  │
                    │        ├─▶ HybridSearch ──▶ embeddings + keyword score   │
                    │        │        │                    │                    │
                    │        │        ▼                    ▼                    │
                    │        │   Relevant chunks      wp_utilioin_aiwa_* tables │
                    │        │        │                                         │
                    │        ▼        ▼                                         │
                    │  Build prompt: system + manual knowledge + guardrails     │
                    │        │         + retrieved CONTEXT + history            │
                    │        ▼                                                  │
                    │  AI Provider (chat model) ──▶ grounded answer + sources   │
                    └───────────────────────────────────────────────────────────┘
```

1. **Discovery** — `ContentDiscovery` collects indexable documents from the site.
2. **Extraction** — `ContentExtractor` cleans HTML into readable text/markdown.
3. **Chunking** — `Chunker` splits documents into overlapping chunks (~1000 tokens, 200 overlap).
4. **Embedding** — Each chunk is embedded via your embedding model and stored in the DB.
5. **Retrieval** — On a question, `HybridSearch` ranks chunks by keyword + vector similarity.
6. **Generation** — The top chunks are injected into the prompt as `CONTEXT`; the model answers
   strictly from that context, with manual knowledge and guardrails layered on top.

If the AI provider is unreachable, the plugin **degrades gracefully** and returns an
extractive answer built from the retrieved content instead of a hard error or endless spinner.

---

## Requirements

| Requirement | Minimum |
| --- | --- |
| WordPress | 6.4+ |
| PHP | 8.1+ |
| Database | MySQL 5.7+ / MariaDB 10.3+ |
| Composer | Required only for development |

You also need an **API key** for an AI provider that exposes a chat model and an embedding
model (for example OpenAI).

---

## Installation

### From a ZIP (production)

1. In WordPress, go to **Plugins → Add New → Upload Plugin**.
2. Upload `utilioin-ai-website-assistant.zip` and click **Install Now**.
3. Click **Activate**.
4. You are redirected to the **Setup Wizard** — follow it to configure your provider.

### From source (development)

```bash
cd wp-content/plugins
git clone <repository-url> utilioin-ai-website-assistant
cd utilioin-ai-website-assistant
composer install
```

Then activate the plugin from **Plugins** in the WordPress admin.

> **Dependencies:** The plugin uses Composer's PSR-4 autoloader
> (`UtilioIn\WordPressSuite\AIWebsiteAssistant\` → `src/`). Run
> `composer install --no-dev --optimize-autoloader` before packaging for production.

---

## Quick Start

The **Setup Wizard** walks through eight short steps:

1. **Welcome** — what the plugin does and what you'll need.
2. **Provider** — choose OpenAI / OpenRouter / OpenAI-compatible.
3. **API Key** — paste your secret (stored encrypted).
4. **Models** — pick the chat model and embedding model.
5. **Indexing** — choose what to index.
6. **Behaviour** — manual knowledge and toggles.
7. **Appearance** — launcher icon and placement.
8. **Finish** — run the first indexing job.

After setup, visit **AI Assistant → Settings** at any time to change anything. Use the
**Test Connection** button to confirm your credentials work before relying on the assistant.

---

## Admin Screens

All screens live under the **AI Assistant** top-level menu and require the
`manage_utilioin_aiwa` capability (granted to Administrators).

| Screen | Slug | Purpose |
| --- | --- | --- |
| **Dashboard** | `utilioin-aiwa-dashboard` | At-a-glance status, quick actions. |
| **Assistant** | `utilioin-aiwa-assistant` | Create and edit assistant profiles, including per-assistant **Logo** (Media Library upload) and **Avatar** (preset pool). |
| **Knowledge** | `utilioin-aiwa-knowledge` | Manage knowledge bases and their sources. |
| **Indexing** | `utilioin-aiwa-indexing` | Run indexing and view a detailed coverage report of **what the plugin actually indexes**, plus live job progress. |
| **Conversations** | `utilioin-aiwa-conversations` | Browse past visitor conversations and messages. |
| **Analytics** | `utilioin-aiwa-analytics` | Usage, feedback and top questions. |
| **Settings** | `utilioin-aiwa-settings` | Provider, models, API key, knowledge, guardrails and widget appearance. |
| **Diagnostics** | `utilioin-aiwa-diagnostics` | Health checks and logs for troubleshooting. |

---

## Settings Reference

Settings are stored as WordPress options with the `utilioin_aiwa_` prefix and managed
through the [`Config`](src/Core/Config.php) service.

| Setting key | Label | Description |
| --- | --- | --- |
| `provider_type` | Provider Type | `openai`, `openrouter` or `openai-compatible`. |
| `provider_name` | Provider Name | Friendly label shown in the dashboard. |
| `provider_base_url` | Base URL | API endpoint; blank uses the provider default. |
| `api_key` | API Key | **Encrypted at rest** (AES-256-GCM). Leave blank to keep the existing key. |
| `chat_model` | Chat Model | Model used to generate replies (e.g. `gpt-4o-mini`). |
| `embedding_model` | Embedding Model | Model used to index content (e.g. `text-embedding-3-small`). |
| `indexing_enabled` | Enable content indexing | Continuously index site content. |
| `assistant_enabled` | Enable the chat widget | Show the widget to visitors on the front end. |
| `manual_knowledge` | Additional information | Free-text facts (address, phone, hours) always sent to the model. |
| `ai_instructions` | AI Instructions & Guardrails | Rules appended to the system prompt on **every** request. |
| `widget_icon_id` | Launcher icon (attachment ID) | Media Library attachment id for the custom icon. |
| `widget_icon_url` | Launcher icon (URL) | Resolved URL of the custom icon; empty = built-in SVG. |
| `widget_primary_color` | Primary colour | Accent colour for the launcher and widget; empty = theme default. |
| `widget_header_color` | Header colour | Widget header background; empty = theme default. |
| `widget_offset_x` | Horizontal offset | Distance from the left/right edge (per the assistant's corner), in px. |
| `widget_offset_y` | Vertical offset | Distance from the top/bottom edge, in px. |
| `widget_z_index` | Z-index | Stacking order of the launcher and window. |
| `widget_display_mode` | Display mode | `all`, or include/exclude the ids in `widget_display_ids`. |
| `widget_display_ids` | Display ids | Comma-separated page ids used by the include/exclude rule. |
| `widget_show_home` | Show on home | Render on the front page. |
| `widget_show_shop` | Show on shop | Render on WooCommerce shop/archive screens. |
| `widget_show_product` | Show on product | Render on WooCommerce single-product screens. |
| `widget_show_cart` | Show on cart | Render on the cart screen. |
| `widget_show_checkout` | Show on checkout | Render on the checkout screen. |
| `widget_show_logged_in` | Show for logged-in | Render to authenticated visitors. |
| `widget_show_logged_out` | Show for logged-out | Render to guests. |
| `widget_hide_mobile` | Hide on mobile | Do not render the launcher on small screens. |
| `widget_fullscreen_mobile` | Full-screen on mobile | Open the chat full-screen on small screens. |
| `widget_launcher_size` | Launcher size | Diameter of the round launcher button, in px (default 60). |
| `widget_launcher_label` | Launcher label | Optional pill label shown next to the launcher. |
| `widget_status_text` | Status text | Text shown beneath the widget header (e.g. online status). |
| `widget_teaser_enabled` | Greeting bubble | Show a teaser bubble next to the launcher. |
| `widget_teaser_text` | Greeting bubble text | The teaser message. |
| `widget_teaser_delay` | Teaser delay | Seconds before the teaser appears. |
| `widget_auto_open` | Auto-open | Open the chat automatically after a delay. |
| `widget_auto_open_delay` | Auto-open delay | Seconds before auto-open. |
| `widget_auto_open_once` | Auto-open once | Only auto-open once per visitor session. |

> The launcher **corner** is chosen per assistant on the **Assistants** screen; the
> offsets above are applied relative to that corner.
| `chunk_size` | Chunk size | Target characters/tokens per chunk. |
| `chunk_overlap` | Chunk overlap | Overlap between consecutive chunks. |
| `embedding_batch_size` | Embedding batch size | Chunks embedded per provider call. |
| `rate_limit_chat` | Chat rate limit | Max chat requests per window per client (`0` = disabled). |
| `rate_limit_search` | Search rate limit | Max search requests per window per client (`0` = disabled). |
| `rate_limit_window` | Rate-limit window | Length of the throttling window, in seconds. |
| `conversation_retention_days` | Conversation retention | Days to keep conversations before the daily purge. |
| `uninstall_delete_data` | Delete data on uninstall | Remove all tables and options when the plugin is uninstalled. |

### Manual knowledge vs. AI instructions

- **Manual knowledge** answers *"what"* — facts about your organisation that are not in
  your posts/pages. It is injected as an authoritative `Website Information (manual)` block.
- **AI instructions / guardrails** answers *"how"* — behavioural rules such as tone of
  voice, language preference, topics to avoid, or escalation policy. It is appended to the
  system prompt under *"Admin instructions and guardrails (always follow these)"*.

Both are sent with **every** chat request, in both the front-end widget path and the REST path.

### Assistant branding: Logo & Avatar

Each assistant profile can be branded individually for maximum flexibility — useful when you
run several assistants (for example a *Sales* bot and a *Support* bot) side by side.

| Field | Source | Behaviour |
| --- | --- | --- |
| `logo_id` | **Logo** — uploaded from the WordPress Media Library. | Stored as an **attachment ID** so the logo survives URL/media changes. |
| `logo_url` | Resolved at read time. | Computed from `logo_id` via `wp_get_attachment_image_url()` (falls back to the legacy `avatar_url`), then to the global launcher icon, then to the built-in SVG. |
| `avatar_preset` | **Avatar** — chosen from a fixed preset pool. | One of `ai`, `male`, `female`, `young`, `aged`. Rendered as an inline SVG in the widget header. |

The preset pool and logo resolver are centralised in [`helpers.php`](src/helpers.php)
(`utilioin_aiwa_avatar_pool()`, `utilioin_aiwa_avatar_preset()`,
`utilioin_aiwa_assistant_logo_url()`) so the admin form, the REST API and the front-end
widget all resolve branding identically.

**Resolution order for the launcher icon:** per-assistant **Logo** → global
`widget_icon_url` → built-in SVG. The header **Avatar** falls back to a neutral `ai` preset
when no preset is set, preserving backwards compatibility.

---

## Frontend Integration

### 1. Floating widget (default)

When `assistant_enabled` is on, a chat launcher is rendered in the bottom-right corner of
every front-end page. Its icon is the **per-assistant logo** (`logo_url`) when set, otherwise
the global uploaded image (`widget_icon_url`), otherwise the built-in SVG fallback. The
widget header shows the assistant's selected **avatar** preset.

### 2. Shortcode

Embed the assistant anywhere with:

```text
[utilioin_aiwa assistant="1"]
```

| Attribute | Default | Description |
| --- | --- | --- |
| `assistant` / `assistant_id` | `1` | ID of the assistant profile to load. |

### 3. Gutenberg block

Search for **"AI Website Assistant"** in the block inserter. Block name:
`utilioin/ai-website-assistant`.

| Attribute | Default | Description |
| --- | --- | --- |
| `assistant` | `""` | Assistant profile to load. |
| `theme` | `light` | Colour theme. |
| `position` | `bottom-right` | Widget position. |

### Reply formatting

Assistant replies are rendered through a small, **injection-safe** formatter: content is
HTML-escaped first, then a whitelist of inline Markdown (`**bold**`, `*italic*`, bullet
lines, line breaks) is applied. Source links are only emitted for absolute `http(s)` URLs,
so a stray placeholder value can never produce a broken relative link.

### Widget keyboard shortcuts

- **Ctrl/⌘ + Enter** — send the current message.
- **Ctrl/⌘ + H** — copy the assistant's last reply.
- Focus is managed on open/close and the widget is fully ARIA-labelled.

---

## REST API Reference

Base namespace: **`utilioin-aiwa/v1`** (WordPress REST API, `/wp-json/`).

| Method | Route | Description |
| --- | --- | --- |
| `POST` | `/chat` | Send a message and receive a grounded answer. |
| `GET` | `/assistants` | List assistants. |
| `POST` | `/assistants` | Create an assistant. |
| `GET` | `/assistants/{id}` | Retrieve an assistant. |
| `PUT` | `/assistants/{id}` | Update an assistant. |
| `DELETE` | `/assistants/{id}` | Delete an assistant. |
| `POST` | `/feedback` | Record 👍 / 👎 feedback for a message. |
| `GET` | `/health` | Health check. |
| `GET` | `/search` | Hybrid search over indexed content. |
| `POST` | `/index` | Trigger an indexing run. |
| `GET` | `/indexing-jobs` | List indexing jobs and their status. |
| `GET` | `/conversations` | List conversations. |
| `POST` | `/conversations` | Start a conversation. |
| `GET` | `/conversations/{id}/messages` | List messages in a conversation. |
| `POST` | `/conversations/{id}/end` | End a conversation. |

Requests to protected routes are authenticated with the standard WordPress nonce
(`X-WP-Nonce`) or application passwords.

### Example: ask a question

```bash
curl -X POST "https://example.com/wp-json/utilioin-aiwa/v1/chat" \
  -H "Content-Type: application/json" \
  -d '{"message": "What are your opening hours?", "assistant_id": 1}'
```

---

## Database Schema

Custom tables use the prefix **`wp_utilioin_aiwa_`** (table prefix follows `$wpdb->prefix`).
Schema is installed by the [`CreateTablesV1`](src/Core/CreateTablesV1.php) migration.

| Table | Purpose |
| --- | --- |
| `assistants` | Assistant profiles and their configuration, including per-assistant branding (`logo_id`, `avatar_preset`). |
| `knowledge_bases` | Logical containers for indexed content. |
| `documents` | One row per indexed source (post, page, widget, file…). |
| `chunks` | Overlapping text chunks belonging to a document. |
| `embeddings` | Vector embeddings for chunks/content. |
| `images` | Images linked to documents (multimodal support). |
| `conversations` | Visitor chat sessions. |
| `messages` | Individual user/assistant messages and feedback. |
| `unanswered_questions` | Questions the assistant could not answer. |
| `indexing_jobs` | Indexing job queue, progress and summary. |
| `pdf_files` | Uploaded PDF documents. |
| `external_urls` | External URLs queued for crawling/indexing. |
| `analytics` | Aggregated daily usage metrics. |
| `settings` | Key/value plugin settings. |

Migrations are versioned and run automatically on activation via
[`MigrationRunner`](src/Core/MigrationRunner.php). A **clean uninstall** (when
`uninstall_delete_data` is enabled) drops these tables.

---

## Supported AI Providers

| Provider | `provider_type` | Notes |
| --- | --- | --- |
| **OpenAI** | `openai` | Default. Chat + embeddings. |
| **OpenRouter** | `openrouter` | Access to many models via one key. |
| **OpenAI-compatible** | `openai-compatible` | Any endpoint implementing the OpenAI API (Ollama, LM Studio, Azure gateways, self-hosted, …). |
| **Llama** | `llama` | Meta Llama models via an OpenAI-compatible gateway. |
| **Mistral** | `mistral` | Mistral models via an OpenAI-compatible gateway. |
| **Groq** | `groq` | Fast inference via the Groq OpenAI-compatible API. |

The provider catalogue lives in [`ProviderRegistry`](src/AI/Provider/ProviderRegistry.php);
the abstraction lives in [`ProviderFactory`](src/AI/Provider/ProviderFactory.php)
and [`ProviderInterface`](src/AI/Provider/ProviderInterface.php). Embeddings are produced
by [`OpenAIEmbeddingProvider`](src/AI/Embedding/OpenAIEmbeddingProvider.php).

To use a custom endpoint, set **Provider Type = OpenAI Compatible** and enter the
**Base URL** (for example `http://localhost:11434/v1`).

---

## Developer Guide

### Directory structure

```text
utilioin-ai-website-assistant/
├── assets/                     # Front-end CSS/JS bundle
│   ├── css/widget.css
│   └── js/chat-widget.js
├── build/build-plugin.php      # Packaging script
├── languages/                  # Translations (.pot/.po/.mo)
├── src/
│   ├── Admin/                  # Admin screens & assets
│   │   ├── Analytics/  Assistant/  Conversations/  Dashboard/
│   │   ├── Diagnostics/  Indexing/  Knowledge/  Settings/
│   │   ├── Assets/             # admin.css, wizard.css, wizard.js
│   │   ├── Menu.php            # Menus, pages, asset enqueue
│   │   └── SetupWizard/        # 8-step onboarding
│   ├── AI/
│   │   ├── Embedding/          # Embedding providers
│   │   └── Provider/           # Chat provider abstraction
│   ├── Core/                   # Plugin kernel, config, migrations
│   ├── Frontend/               # Widget, shortcode, block
│   ├── Indexing/               # Discovery, extraction, chunking, search
│   ├── REST/RestApi.php        # REST routes
│   └── Services/ChatService.php
├── composer.json
└── utilioin-ai-website-assistant.php   # Bootstrap
```

### Architecture

- **Bootstrap** — [`utilioin-ai-website-assistant.php`](utilioin-ai-website-assistant.php)
  defines constants, loads Composer, and boots the singleton
  [`Plugin`](src/Core/Plugin.php).
- **Dependency injection** — [`Container`](src/Core/Container.php) is a minimal PSR-11
  container; services are registered as singletons in
  [`Plugin::registerCoreServices()`](src/Core/Plugin.php:229).
- **Config** — [`Config`](src/Core/Config.php) wraps WordPress options and provides
  transparent encryption for secrets.
- **Hooks/Events** — [`Hooks`](src/Core/Hooks.php) and [`Events`](src/Core/Events.php)
  centralise WordPress action/filter registration.
- **Logging** — [`Logger`](src/Core/Logger.php) implements PSR-3, writes to a rotated
  log file and supports a `debug_mode` toggle.

### Indexing pipeline

`ContentDiscovery` → `ContentExtractor` → `Chunker` → `OpenAIEmbeddingProvider` →
`IndexingService` persist, orchestrated by [`IndexingService::indexKnowledgeBase()`](src/Indexing/IndexingService.php:57).
Long runs are queued as **indexing jobs** and executed through WP-Cron
(`handleCronIndexing`), with progress surfaced on the Indexing screen.

### Build & packaging

```bash
composer install --no-dev --optimize-autoloader
php build/build-plugin.php
```

### Code quality

```bash
# PHP syntax check
php -l src/Core/Plugin.php

# JS syntax check
node --check assets/js/chat-widget.js

# Coding standards & static analysis (dev deps)
vendor/bin/phpcs
vendor/bin/phpstan analyse
vendor/bin/phpunit
```

---

## New Features

### 1. Adaptive Chunking (Chunker)

The `Chunker` class now implements intelligent chunking that adapts to content length:

- **Small content** (< 1.5× chunk size): Kept as a single chunk to preserve context.
- **Moderate content** (1–2× chunk size): Paragraph-based splitting with overlap.
- **Long content** (> 2× chunk size): Sentence-level splitting into overlapping chunks for better semantic coherence.
- **Overlap handling**: Consecutive chunks share a small amount of overlapping text to maintain context continuity.
- **Configurable parameters**: `chunkSize`, `chunkOverlap`, and `maxChunkSize` can be adjusted via the admin panel.

### 2. Per-Document Processing with Checkpointing

The `IndexingService` now supports granular, fault-tolerant document processing:

- **`processDocumentWithCheckpointing()`** – Processes each document independently, allowing partial completion even if later documents fail.
- **Checkpointing** – Progress is saved after each document, enabling seamless resumption after interruptions.
- **Incremental skipping** – Documents whose content hasn't changed are automatically skipped.
- **Batch discovery** – Content is discovered in paginated batches so large sites index without exhausting memory.
- **Granular error handling** – Individual chunk failures don't block the whole document; only the failing chunk is marked as failed.
- **Persistent chunk storage** – Each chunk is saved with its embedding vector, ensuring no data loss.
- **Single-pass embedding** – Content is embedded once per document and the vectors are handed straight to persistence, avoiding duplicate provider calls.

### 3. Enhanced Hybrid Search (RAG)

The `HybridSearch` class already featured a powerful hybrid approach, now further refined:

- **Wider candidate window** – Retrieves up to 1500 candidates (default 300 × 40) to improve recall for older content.
- **Keyword boosting** – Stronger weighting for exact keyword matches in titles and bodies.
- **Slug matching** – Recognizes hyphenated slugs (e.g., "the-lighthouse-at-greyhaven") and matches them against document metadata.
- **Title relevance** – Additional scoring for exact title matches.
- **Cosine similarity** – Vector similarity computation for semantic matching.
- **Result caching** – Search results are cached and invalidated through a `search_cache_version` option.

### 4. Rate Limiting

The public chat and search surfaces are throttled so a single visitor (or bot)
cannot drive unbounded AI provider spend:

- **Shared limiter** – A single `RateLimiter` service guards the REST `/chat`,
  REST `/search`, and the visitor `utilioin_aiwa_chat` AJAX endpoint, so a client
  cannot bypass the budget by switching entry points.
- **Configurable budgets** – Chat, search and the window length are all editable
  under **Settings → Rate Limiting**. Set a limit to `0` to disable throttling.
- **Cheap and private** – Counters live in one non-autoloaded option, keyed by a
  hashed client identifier (session cookie or IP); no raw IP is ever stored.
- **Graceful response** – Exceeded clients receive HTTP `429` with a `retry_after`
  hint instead of a provider call.

### 5. Other Improvements

- **Better error reporting** – Detailed logging for chunking, embedding, and search failures.
- **Resilient embedding** – If the embedding API fails, the system gracefully falls back to extractive answers from retrieved content.
- **Metadata preservation** – Document metadata (headings, slugs, descriptions) is retained alongside content.
- **Secure API key storage** – Encrypted at rest using AES-256-GCM, with API key rotation and a previous-key migration path.
- **Export / import** – Knowledge bases can be exported to JSON and imported from JSON or CSV.
- **Conversation retention** – A daily task purges conversations older than the configured retention period (default 30 days).
- **Conversation filtering** – Conversations can be filtered by date range, assistant and keyword.
- **CMS auto-import** – Address, phone, email and business hours can be discovered from theme, WooCommerce and contact-page fields.
- **Centralized i18n** – A small `I18n` helper plus registered script translations keep the UI fully translatable.
- **Capacity-gated admin** – All admin actions require the `manage_utilioin_aiwa` capability.

---

## Security & Privacy

- **API keys are encrypted at rest.** See `Config::setEncrypted()` using `aes-256-gcm`
  with a per-site key stored in `utilioin_aiwa_encryption_key`.
- **Capability-gated admin.** All admin actions require `manage_utilioin_aiwa`.
- **Nonce-protected AJAX & REST.** Admin AJAX actions verify a nonce; REST routes use the
  WordPress nonce/app-password standard.
- **Output escaping.** Assistant HTML is escaped before a small Markdown whitelist is applied,
  preventing XSS from model output.
- **Data ownership.** Content, embeddings and conversations stay in *your* database — nothing
  is sent anywhere except the AI provider you configure, for the specific request being made.
- **Clean uninstall.** Enable *Delete data on uninstall* to remove all tables and options.

> Never commit real API keys or credentials to version control.

---

## Troubleshooting & FAQ

**The assistant says it has no information, but the content exists.**
Run **Indexing** (AI Assistant → Indexing) and confirm the documents appear in the coverage
report. If your content changed, re-run indexing to refresh stale rows.

**"AI provider is not configured."**
Add your API key in **Settings** and click **Test Connection**. An empty key or an
unreachable base URL produces this message.

**Chat returns an error or a long spinner.**
The plugin degrades gracefully: if the provider times out, it returns an extractive answer
from retrieved content. Check **Diagnostics** and the plugin log for the underlying cause
(quota, rate limit, invalid model name).

**The source link points to a wrong URL.**
Source links are only rendered for absolute `http(s)` URLs; stale index rows holding
placeholder values are ignored. Re-index to refresh the affected documents.

**Replies contain `**asterisks**`.**
Assistant output is rendered through a safe Markdown formatter that converts `**bold**` to
bold text. If raw asterisks appear, ensure you are running the latest
[`chat-widget.js`](assets/js/chat-widget.js).

**Indexing is slow.**
Indexing speed is dominated by the embedding API. Large sites make many embedding calls;
this is expected and is shown as progress on the Indexing screen.

---

## Support

Running into a problem? Our support team is happy to help.

- **Email:** support@utilio.in
- **Website:** <https://utilio.in/ai-website-assistant>

When contacting support, please include your WordPress and PHP versions, the
plugin version (**1.1.8**), your AI provider and model, and the relevant entries
from the plugin log on the **Diagnostics** screen. For your security, **never
send your API key**.

---

## Changelog

> See [`CHANGELOG.md`](CHANGELOG.md) for the full, itemized history following
> [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
> [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### 1.1.8 — 2026-10-05

**Fixed**

- Chat answer feedback (👍 / 👎) was never saved: the REST `/feedback` handler
  wrote a non-existent `updated_at` column (HTTP 500). Feedback now writes only
  the `feedback` enum, validates the message id, and reports real failures.
- Message ids are returned to the widget (`message_id`), stamped as
  `data-message-id`, and sent with the feedback request so a rating attaches to
  the right answer.
- Stored messages had a zero `created_at` because `ChatService::addMessage()`
  passed 14 columns with 13 format specifiers; the format array is corrected and
  a migration (`BackfillMessageTimestamps`, schema 1.0.3) repairs historical rows.
- Analytics "Tracked Questions" now renders with the shared badge styling.

### 1.1.1 – 1.1.7 — 2026-10-04

Consolidated interim releases (full admin control of the widget, per-assistant
branding and fixes).

**Added**

- Full admin control of the chat widget: placement corner (per assistant) with
  X/Y offsets and z-index, page/context visibility rules, mobile options,
  launcher size and label, colour pickers, a greeting/teaser bubble, and
  auto-open (with delay and once-per-session).
- Per-assistant **Logo** (media attachment id with resolution fallbacks) and
  **Avatar** preset.
- Answer Mode descriptions on the Assistant screen.

**Changed**

- Removed the draggable chat window; it now opens fully on-screen for any corner.
- The launcher click reliably opens the chat window.

**Fixed**

- `Unknown column 'logo_id'` on the assistant read path (self-healing upgrade
  hook applies pending migrations when the stored version differs).
- False-positive configuration warnings for OpenRouter models not in the
  built-in catalogue after a successful **Test Connection**.

### 1.1.0 — 2026-10-03

**Added**

- REST & AJAX rate limiting for the chat and search surfaces.
- Auto-import of address, phone, email and business hours from CMS fields
  (theme customizer, WooCommerce settings, contact pages).
- API key rotation with decryption-only backup keys and migration.
- Centralized i18n helper plus JavaScript script-translation registration.
- Llama and Mistral model adapters.
- Knowledge base export (JSON) and import (JSON/CSV).
- Conversation TTL cleanup (configurable retention, default 30 days).
- Conversation filters (date range, assistant, keyword).
- Chat widget shortcuts (`Ctrl+Enter` send, `Ctrl+H` copy) and improved a11y.
- Configurable chunk size, overlap, max chunk size and embedding batch size.
- Analytics dashboard, "Reset to defaults", and warnings for unused assistants.
- Hybrid search caching layer.

**Fixed**

- Setup wizard / dashboard page reload loop.
- Premature `wp_script_is()` call that triggered a `_doing_it_wrong()` notice.
- Dashboard no longer redirected into the wizard automatically.
- Missing `maxChunkSize` property declaration in the chunker.
- Double-embedding of chunks during indexing.

**Removed**

- Dead code (`Chunker::splitIntoChunks()`, `IndexingService::processDocument()`),
  broken helpers, and the redundant `README.md`.

### 1.0.0

- Initial release.
- Hybrid-search RAG assistant with grounded, cited answers.
- Automatic content discovery and indexing across posts, pages, CPTs, taxonomies and widgets.
- BYOK support for OpenAI, OpenRouter and OpenAI-compatible endpoints.
- Encrypted API-key storage.
- Floating widget, shortcode and Gutenberg block.
- **Manual knowledge** field for facts outside site content.
- **AI instructions & guardrails** field applied to every request.
- **Test Connection** for both chat and embedding models.
- Custom launcher icon upload with SVG fallback.
- Safe Markdown rendering of assistant replies and hardened source links.
- Graceful degradation when the AI provider is unavailable.
- Analytics, conversations, diagnostics and clean uninstall.

---

## License

Released under the **GPL v2 or later**.

```text
Copyright (C) UtilioIn

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2 or later,
as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```

See the [GNU General Public License](https://www.gnu.org/licenses/gpl-2.0.html) for details.
