Datalumo Fresh is a disposable WordPress site. Wipe it with bin/fresh-site --force.
',
+], true);
+
+$posts = [
+ [
+ 'Opening hours and pickup',
+ 'We are open Tuesday to Saturday, 10:00 to 18:00. Weekend pickup is at the side door on Kerkstraat. Closed on Mondays.',
+ ],
+ [
+ 'Shipping and returns',
+ 'Orders placed before 15:00 ship the same day inside the Netherlands. You have 30 days to return unused items. Start a return from your account or reply to the order email.',
+ ],
+ [
+ 'Care for wool',
+ 'Hand wash wool in cold water and lay it flat to dry. A wool beanie can go in a mesh bag on a gentle cycle if you skip the dryer.',
+ ],
+ [
+ 'How we roast coffee',
+ 'Beans are roasted in small batches every Thursday. Light roasts land on Friday. Dark roasts are bagged the same afternoon.',
+ ],
+ [
+ 'Gift wrapping',
+ 'Add a note at checkout and we wrap the order in recycled paper. Gift receipts hide prices. We do not print prices on the packing slip when you ask.',
+ ],
+];
+
+foreach ($posts as [$title, $content]) {
+ wp_insert_post([
+ 'post_title' => $title,
+ 'post_status' => 'publish',
+ 'post_type' => 'post',
+ 'post_content' => '
'.$content.'
',
+ ], true);
+}
+
+if ($guide && ! is_wp_error($guide)) {
+ update_option('show_on_front', 'page');
+ update_option('page_on_front', $guide);
+}
+
+if (! class_exists('WooCommerce')) {
+ WP_CLI::warning('WooCommerce is not active; skipped products.');
+
+ return;
+}
+
+$simple = [
+ ['Canvas Tote', '29.00', 'A heavy canvas tote that stands on its own. Natural colour, one size.', 'tote'],
+ ['Ceramic Mug', '14.00', 'A 300ml stoneware mug. Dishwasher safe. Speckled cream glaze.', 'mug'],
+ ['Wool Beanie', '22.00', 'Merino beanie, unisex. Hand wash or a gentle cycle in a mesh bag.', 'beanie'],
+ ['Drip Coffee 250g', '11.50', 'Thursday roast. Light and chocolatey. Ground for filter on request.', 'coffee'],
+];
+
+foreach ($simple as [$name, $price, $description, $sku]) {
+ $product = new WC_Product_Simple();
+ $product->set_name($name);
+ $product->set_regular_price($price);
+ $product->set_short_description($description);
+ $product->set_description($description);
+ $product->set_sku($sku);
+ $product->set_manage_stock(true);
+ $product->set_stock_quantity(25);
+ $product->set_catalog_visibility('visible');
+ $product->set_status('publish');
+ $product->save();
+}
+
+foreach (['color' => 'Color', 'size' => 'Size'] as $slug => $label) {
+ if (wc_attribute_taxonomy_id_by_name($slug)) {
+ continue;
+ }
+
+ wc_create_attribute([
+ 'name' => $label,
+ 'slug' => $slug,
+ 'type' => 'select',
+ 'order_by' => 'menu_order',
+ 'has_archives' => false,
+ ]);
+}
+
+delete_transient('wc_attribute_taxonomies');
+WC_Cache_Helper::invalidate_cache_group('woocommerce-attributes');
+WC()->attributes = null;
+
+if (! taxonomy_exists('pa_color') || ! taxonomy_exists('pa_size')) {
+ foreach (wc_get_attribute_taxonomies() as $taxonomy) {
+ $name = wc_attribute_taxonomy_name($taxonomy->attribute_name);
+ register_taxonomy($name, ['product'], []);
+ }
+}
+
+foreach (['Blue', 'Red'] as $color) {
+ wp_insert_term($color, 'pa_color');
+}
+
+foreach (['Small', 'Large'] as $size) {
+ wp_insert_term($size, 'pa_size');
+}
+
+$colorAttribute = new WC_Product_Attribute();
+$colorAttribute->set_id(wc_attribute_taxonomy_id_by_name('color'));
+$colorAttribute->set_name('pa_color');
+$colorAttribute->set_options(array_values(array_filter([
+ get_term_by('name', 'Blue', 'pa_color')->term_id ?? null,
+ get_term_by('name', 'Red', 'pa_color')->term_id ?? null,
+])));
+$colorAttribute->set_visible(true);
+$colorAttribute->set_variation(true);
+
+$sizeAttribute = new WC_Product_Attribute();
+$sizeAttribute->set_id(wc_attribute_taxonomy_id_by_name('size'));
+$sizeAttribute->set_name('pa_size');
+$sizeAttribute->set_options(array_values(array_filter([
+ get_term_by('name', 'Small', 'pa_size')->term_id ?? null,
+ get_term_by('name', 'Large', 'pa_size')->term_id ?? null,
+])));
+$sizeAttribute->set_visible(true);
+$sizeAttribute->set_variation(true);
+
+$jacket = new WC_Product_Variable();
+$jacket->set_name('Trail Jacket');
+$jacket->set_sku('jacket');
+$jacket->set_short_description('A packable shell. Pick a colour, then a size.');
+$jacket->set_description('Two-way zip, stuffs into its own pocket. Colour and size are variations so chat add-to-cart can walk those steps.');
+$jacket->set_attributes([$colorAttribute, $sizeAttribute]);
+$jacket->set_catalog_visibility('visible');
+$jacket->set_status('publish');
+$jacketId = $jacket->save();
+
+$prices = [
+ 'Blue' => ['Small' => '89.00', 'Large' => '89.00'],
+ 'Red' => ['Small' => '92.00', 'Large' => '92.00'],
+];
+
+foreach ($prices as $color => $sizes) {
+ foreach ($sizes as $size => $price) {
+ $variation = new WC_Product_Variation();
+ $variation->set_parent_id($jacketId);
+ $variation->set_attributes([
+ 'pa_color' => sanitize_title($color),
+ 'pa_size' => sanitize_title($size),
+ ]);
+ $variation->set_regular_price($price);
+ $variation->set_sku('jacket-'.sanitize_title($color).'-'.sanitize_title($size));
+ $variation->set_manage_stock(true);
+ $variation->set_stock_quantity(8);
+ $variation->set_status('publish');
+ $variation->save();
+ }
+}
+
+WC_Product_Variable::sync($jacketId);
+
+WP_CLI::success('Seeded pages, posts, simple products, and a variable Trail Jacket.');
diff --git a/bin/index.php b/bin/index.php
new file mode 100644
index 0000000..6220032
--- /dev/null
+++ b/bin/index.php
@@ -0,0 +1,2 @@
+=8.1",
"automattic/jetpack-autoloader": "^3.0",
- "woocommerce/action-scheduler": "^3.0",
- "yahnis-elsts/plugin-update-checker": "^5.7"
+ "woocommerce/action-scheduler": "^3.0"
},
"autoload": {
"psr-4": {
@@ -36,6 +35,8 @@
"brain/monkey": "^2.7"
},
"scripts": {
- "test": "pest"
+ "test": "pest",
+ "fresh-site": "bin/fresh-site",
+ "post-autoload-dump": "@php bin/ensure-index-php"
}
}
diff --git a/datalumo.php b/datalumo.php
index 1ac81ff..b1f5641 100644
--- a/datalumo.php
+++ b/datalumo.php
@@ -7,11 +7,11 @@
* Plugin Name: Datalumo
* Plugin URI: https://github.com/datalumo/wordpress
* Description: Sync your WordPress content to Datalumo and add AI-powered search and chat to your site.
- * Version: 0.0.7
+ * Version: 0.0.8
* Requires at least: 6.0
* Requires PHP: 8.1
- * Author: Jeffrey van Rossum
- * Author URI: https://vanrossum.dev
+ * Author: Datalumo
+ * Author URI: https://datalumo.app
* Text Domain: datalumo
* Domain Path: /languages
* License: GPL-2.0-or-later
@@ -25,7 +25,7 @@
}
if (! defined('DATALUMO_VERSION')) {
- define('DATALUMO_VERSION', '0.0.7');
+ define('DATALUMO_VERSION', '0.0.8');
define('DATALUMO_FILE', __FILE__);
define('DATALUMO_DIR', __DIR__);
define('DATALUMO_URL', plugin_dir_url(__FILE__));
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..6220032
--- /dev/null
+++ b/index.php
@@ -0,0 +1,2 @@
+.
+
+License: GPL-2.0-or-later
+License URI: https://www.gnu.org/licenses/gpl-2.0.html
diff --git a/readme.txt b/readme.txt
index 5d80067..1b1b3e3 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,10 +1,10 @@
=== Datalumo ===
-Contributors: jeffreyvanrossum
+Contributors: datalumo, jeffreyvr
Tags: search, ai, chatbot, rag
Requires at least: 6.0
-Tested up to: 6.9
+Tested up to: 7.1
Requires PHP: 8.1
-Stable tag: 0.0.7
+Stable tag: 0.0.8
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
@@ -19,17 +19,72 @@ Datalumo keeps a searchable, AI-ready copy of your WordPress content and gives y
* **Search box** — drop-in search via the `[datalumo_search]` shortcode.
* **Enhanced search** — serve WordPress' native search results from Datalumo's ranking, with an optional streamed AI summary above the results. Falls back to native search automatically.
* **Visitor identity** — logged-in users can continue their chat conversations across visits, verified with a server-side signature.
+* **Chat page actions**: the official plugin listens for confirmed chat events (`add_to_cart`, `view_cart`, `open_checkout`, `open_page`). Add those actions in Datalumo under Reply → What it can do.
+
+This plugin is an interface to a [Datalumo](https://datalumo.app) instance. You need a Datalumo account (or a self-hosted instance). Connect with Datalumo from Settings, or use Manual setup to paste an API token. Nothing is sent until an administrator connects the site and turns a feature on.
+
+= External services =
+
+The plugin talks to the Datalumo instance you configure. The default host is `https://datalumo.app`; you can point it at your own instance instead.
+
+* Service: [https://datalumo.app](https://datalumo.app)
+* Documentation: [https://datalumo.app/docs](https://datalumo.app/docs)
+* Terms of Service: [https://datalumo.app/terms](https://datalumo.app/terms)
+* Privacy Policy: [https://datalumo.app/privacy](https://datalumo.app/privacy)
+
+When a feature is enabled, the plugin may:
+
+* Load the widget script from `{your Datalumo URL}/widget/v1/datalumo.js` (chat, search box, AI summary, and click tracking).
+* Send published post content to the Datalumo API so it can be indexed (title, HTML, permalink, author display name, categories, tags, dates, and any custom field mappings you add). WooCommerce products also send short description, product categories and tags, visible attributes, and SKU.
+* Send visitor search queries to Datalumo when enhanced search is on.
+* Send result-click events (URL, rank, search session) when enhanced search is on.
+* Send a signed visitor id (`wp-user-{id}` HMAC) when visitor identity is enabled on the Chatbot tab.
+
+= Development =
+
+Source code: [https://github.com/datalumo/wordpress](https://github.com/datalumo/wordpress)
+
+Production dependencies are installed with `composer install --no-dev`.
== Installation ==
1. Install and activate the plugin.
-2. In your Datalumo dashboard, create an API token (pages abilities) and a source of type API.
-3. Go to Settings → Datalumo, paste your organisation ID and token, and connect.
-4. Pick which post types sync to which source, and run the first full sync.
-5. Add a widget key to enable the chat, search box, or enhanced search.
+2. Go to Settings → Datalumo and press Connect with Datalumo.
+3. Pick or create widgets, then finish the checklist (what to sync, chat, search).
+4. Or choose Manual setup, paste an API token, and press Connect & test.
+
+== Frequently Asked Questions ==
+
+= Do I need a Datalumo account? =
+
+Yes. The plugin does not search or chat on its own. It connects WordPress to a Datalumo instance — the hosted service at datalumo.app or one you run yourself.
+
+= What data is sent to Datalumo? =
+
+Only after you connect and enable a feature. Content sync sends the published posts you choose. Enhanced search sends visitor queries and optional result-click analytics. The chat and search widgets load Datalumo's script and talk to that instance. Visitor identity is off unless you turn it on.
+
+= Can I use a self-hosted Datalumo instance? =
+
+Yes. On the Connection tab, open Using a self-hosted Datalumo? and set the URL before you connect. Manual setup has the same field.
+
+= Can the chatbot add products to a WooCommerce cart? =
+
+Yes, if WooCommerce is active. In Datalumo, add the WordPress Add to cart action (event add_to_cart, field product_id). The visitor confirms in chat. Variable products send colour, then that colour's sizes, as nested choice steps (up to 8 options per step); more than that opens the product page.
+
+= What other chat actions does the plugin handle? =
+
+View cart and Open checkout (WooCommerce), and Open this page (a post or page by id, slug, or same-site URL). Add them from Reply → Plugin actions → WordPress.
+
== Changelog ==
+= 0.0.8 =
+* Connect with Datalumo: grant access from your Datalumo account. The plugin stores the source, token, and widget keys, then offers a short checklist for sync, chat, and search.
+* Manual setup is token-only. The organisation is inferred from the token. Widget keys and secrets are rejected in the wrong field.
+* Chat page actions: add_to_cart (WooCommerce, including variable products with nested colour/size choices), view_cart, open_checkout, and open_page.
+* Product and other singular pages send page_id (and product_id on products) as chat context.
+* WooCommerce products sync short description, product categories and tags, visible attributes, and SKU (including variation SKUs) as searchable fields.
+
= 0.0.7 =
* Full sync: when the push completes, the plugin now tells Datalumo to start indexing right away instead of waiting for its next background sweep. Best-effort — older Datalumo versions without the endpoint are unaffected.
@@ -57,5 +112,5 @@ Datalumo keeps a searchable, AI-ready copy of your WordPress content and gives y
= 0.0.1 =
* Initial public release.
* Content sync, chat/search widgets, enhanced search, and visitor identity.
-* GitHub auto-updates via Plugin Update Checker.
+* GitHub auto-updates via Plugin Update Checker (removed ahead of the WordPress.org release).
* Dutch and Chinese translations.
diff --git a/resources/css/admin.css b/resources/css/admin.css
index 47d5a4c..74885da 100644
--- a/resources/css/admin.css
+++ b/resources/css/admin.css
@@ -4,11 +4,16 @@
}
.datalumo-settings .datalumo-sync-row {
- border: 1px solid #dcdcde;
- border-radius: 8px;
- padding: 0 1rem;
- margin: 1rem 0;
- background: #fff;
+ margin: 0;
+ padding: 0;
+ border: 0;
+ min-inline-size: 0;
+}
+
+.datalumo-settings .datalumo-sync-row.is-multiple + .datalumo-sync-row {
+ margin-top: 1.25rem;
+ padding-top: 1.25rem;
+ border-top: 1px solid #dcdcde;
}
.datalumo-inline-result {
@@ -48,3 +53,248 @@
transform: rotate(360deg);
}
}
+
+.datalumo-connect-hero,
+.datalumo-checklist {
+ margin: 0;
+}
+
+.datalumo-connect-hero form {
+ margin-top: 0.75rem;
+}
+
+.datalumo-org-status {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.datalumo-setup-header h2 {
+ margin: 0 0 0.25rem;
+}
+
+.datalumo-setup-header p {
+ margin: 0 0 1.25rem;
+ color: #646970;
+}
+
+.datalumo-setup-card {
+ min-inline-size: 0;
+ border: 1px solid #dcdcde;
+ border-radius: 8px;
+ padding: 1rem 1.15rem 1.1rem;
+ margin: 0 0 0.75rem;
+ background: #fff;
+}
+
+.datalumo-setup-card legend {
+ float: left;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0;
+ margin: 0 0 0.75rem;
+ font-weight: 600;
+}
+
+.datalumo-setup-card::after {
+ content: "";
+ display: block;
+ clear: both;
+}
+
+.datalumo-setup-step {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: 22px;
+ height: 22px;
+ padding: 0 0 1px 1px;
+ border-radius: 50%;
+ background: #1d2327;
+ color: #fff;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.datalumo-setup-types {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ clear: both;
+}
+
+.datalumo-setup-types .datalumo-checkbox,
+.datalumo-setup-options .datalumo-checkbox {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ margin: 0;
+ line-height: 1.3;
+}
+
+.datalumo-setup-types .datalumo-checkbox {
+ padding: 0.4rem 0.7rem;
+ border: 1px solid #dcdcde;
+ border-radius: 6px;
+ background: #f6f7f7;
+}
+
+.datalumo-setup-types .datalumo-checkbox:has(input:checked) {
+ border-color: #2271b1;
+ background: #f0f6fc;
+}
+
+.datalumo-setup-types .datalumo-checkbox input,
+.datalumo-setup-options .datalumo-checkbox input {
+ margin: 0;
+}
+
+.datalumo-setup-options {
+ display: flex;
+ flex-direction: column;
+ gap: 0.65rem;
+ clear: both;
+}
+
+.datalumo-setup-actions {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem;
+ margin: 1.15rem 0 0;
+}
+
+.datalumo-setup-disconnect {
+ margin-top: 1.25rem;
+}
+
+.datalumo-setup-disconnect .button-link {
+ padding: 0;
+ color: #646970;
+}
+
+.datalumo-connect-hero .button-link,
+.datalumo-manual .button-link {
+ margin-top: 0.5rem;
+ padding: 0;
+}
+
+.datalumo-self-host {
+ margin-top: 1rem;
+ color: #646970;
+}
+
+.datalumo-self-host summary {
+ cursor: pointer;
+ font-size: 13px;
+}
+
+.datalumo-self-host[open] summary {
+ margin-bottom: 0.5rem;
+}
+
+.datalumo-self-host label {
+ display: block;
+ margin-bottom: 0.25rem;
+}
+
+.datalumo-self-host p {
+ margin: 0;
+}
+
+.datalumo-settings-layout {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 1.5rem 2.5rem;
+ margin-top: 1.25rem;
+}
+
+.datalumo-settings-main {
+ flex: 1 1 44rem;
+ min-width: 0;
+ max-width: 50rem;
+}
+
+.datalumo-settings-main > form,
+.datalumo-settings-main .form-table {
+ margin-top: 0;
+}
+
+.datalumo-settings-main .form-table tr:first-child th,
+.datalumo-settings-main .form-table tr:first-child td {
+ padding-top: 0;
+}
+
+.datalumo-settings-aside {
+ display: flex;
+ flex: 0 1 18rem;
+ flex-direction: column;
+ gap: 0.75rem;
+ width: 18rem;
+ position: sticky;
+ top: 2.75rem;
+}
+
+.datalumo-help-card,
+.datalumo-docs-card {
+ margin: 0;
+ padding: 1rem 1.1rem;
+ border: 1px solid #dcdcde;
+ border-radius: 8px;
+ background: #fff;
+}
+
+.datalumo-help-card h2 {
+ margin: 0 0 0.65rem;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.datalumo-help-fields {
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+}
+
+.datalumo-help-fields textarea {
+ display: block;
+ box-sizing: border-box;
+ width: 100%;
+ max-width: 100%;
+ min-height: 5.5rem;
+ margin: 0;
+ resize: vertical;
+}
+
+.datalumo-settings-aside .datalumo-help-submit.button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.4rem;
+ width: 100%;
+ margin: 0;
+ padding: 0 12px;
+ line-height: 1;
+}
+
+.datalumo-help-icon {
+ display: block;
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.datalumo-docs-card p {
+ margin: 0;
+}
+
+.datalumo-docs-card p + p {
+ margin-top: 0.25rem;
+ color: #646970;
+}
diff --git a/resources/css/index.php b/resources/css/index.php
new file mode 100644
index 0000000..6220032
--- /dev/null
+++ b/resources/css/index.php
@@ -0,0 +1,2 @@
+ 0) {
+ return true;
+ }
+ }
+
+ return Boolean(payload.sku && String(payload.sku).trim());
+ }
+
+ function finish(detail, result) {
+ if (typeof detail.respond === 'function') {
+ detail.respond(result);
+
+ return;
+ }
+
+ window.dispatchEvent(new CustomEvent('datalumo:action-result', {
+ detail: {
+ answer_id: detail.answer_id,
+ tool_id: detail.tool_id,
+ run: detail.run,
+ ok: result.ok !== false,
+ message: result.message || '',
+ },
+ }));
+ }
+
+ function refreshCart(fragments, cartHash) {
+ if (typeof window.jQuery === 'undefined') {
+ return;
+ }
+
+ var $ = window.jQuery;
+
+ $.each(fragments, function (selector, html) {
+ $(selector).replaceWith(html);
+ });
+
+ $(document.body).trigger('added_to_cart', [fragments, cartHash, null]);
+ $(document.body).trigger('wc_fragment_refresh');
+ }
+})();
diff --git a/resources/js/admin.js b/resources/js/admin.js
index 98b6191..15b12dc 100644
--- a/resources/js/admin.js
+++ b/resources/js/admin.js
@@ -36,6 +36,24 @@
// --- Connection tab ---------------------------------------------------
+ var modeButtons = document.querySelectorAll('[data-datalumo-mode]');
+ var grantPanel = document.getElementById('datalumo-mode-grant');
+ var manualPanel = document.getElementById('datalumo-mode-manual');
+
+ modeButtons.forEach(function (button) {
+ button.addEventListener('click', function () {
+ var mode = button.getAttribute('data-datalumo-mode');
+
+ if (grantPanel) {
+ grantPanel.hidden = mode !== 'grant';
+ }
+
+ if (manualPanel) {
+ manualPanel.hidden = mode !== 'manual';
+ }
+ });
+ });
+
var connectButton = document.getElementById('datalumo-connect');
if (connectButton) {
@@ -46,7 +64,6 @@
result.textContent = '…';
var payload = {
- organisation_id: document.getElementById('datalumo-organisation-id').value.trim(),
token: document.getElementById('datalumo-token').value.trim(),
};
@@ -59,20 +76,14 @@
}
post('datalumo_connect', payload).then(function (response) {
- connectButton.disabled = false;
-
if (response.success) {
- var organisation = response.data.organisation || {};
- result.textContent = sprintf(
- config.i18n.connected,
- organisation.name || organisation.id || '',
- (response.data.sources || []).length
- );
- result.classList.add('is-success');
- } else {
- result.textContent = config.i18n.connectionFailed + ' ' + ((response.data || {}).message || '');
- result.classList.add('is-error');
+ window.location.reload();
+ return;
}
+
+ connectButton.disabled = false;
+ result.textContent = config.i18n.connectionFailed + ' ' + ((response.data || {}).message || '');
+ result.classList.add('is-error');
});
});
}
@@ -183,4 +194,14 @@
document.querySelectorAll('.datalumo-sync-status[data-sync]').forEach(function (status) {
pollStatus(status.getAttribute('data-sync'));
});
+
+ var autoSync = new URLSearchParams(window.location.search).get('datalumo_sync');
+
+ if (autoSync) {
+ var autoStart = document.querySelector('.datalumo-sync-start[data-sync="' + autoSync + '"]');
+
+ if (autoStart && ! autoStart.disabled) {
+ autoStart.click();
+ }
+ }
})();
diff --git a/resources/js/host-navigation.js b/resources/js/host-navigation.js
new file mode 100644
index 0000000..5fac3ec
--- /dev/null
+++ b/resources/js/host-navigation.js
@@ -0,0 +1,144 @@
+/**
+ * Handles Datalumo host actions that open a page: cart, checkout, or a
+ * WordPress post.
+ */
+(function () {
+ 'use strict';
+
+ var config = window.datalumoHostNavigation;
+
+ if (! config) {
+ return;
+ }
+
+ var events = config.events || ['view_cart', 'open_checkout', 'open_page'];
+
+ window.addEventListener('datalumo:action', function (event) {
+ var detail = event.detail || {};
+
+ if (events.indexOf(detail.name) === -1) {
+ return;
+ }
+
+ event.preventDefault();
+
+ var payload = isPlainObject(detail.payload) ? detail.payload : {};
+
+ if (detail.name === 'view_cart') {
+ go(detail, config.cartUrl, (config.i18n && config.i18n.no_cart) || '');
+
+ return;
+ }
+
+ if (detail.name === 'open_checkout') {
+ go(detail, config.checkoutUrl, (config.i18n && config.i18n.no_checkout) || '');
+
+ return;
+ }
+
+ resolveThenGo(detail, payload);
+ });
+
+ function resolveThenGo(detail, payload) {
+ var body = new FormData();
+ var request = {};
+ var key;
+
+ for (key in payload) {
+ if (Object.prototype.hasOwnProperty.call(payload, key)) {
+ request[key] = payload[key];
+ }
+ }
+
+ request.event = detail.name;
+ body.append('action', 'datalumo_host_navigation');
+ body.append('_ajax_nonce', config.nonce);
+ body.append('payload', JSON.stringify(request));
+
+ fetch(config.ajaxUrl, {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: body,
+ })
+ .then(function (response) {
+ return response.json();
+ })
+ .then(function (json) {
+ var data = (json && json.data) || {};
+ var url = typeof data.url === 'string' ? data.url : '';
+
+ if (json && json.success && isSameSite(url)) {
+ finish(detail, { ok: true, message: '' });
+ window.location.assign(url);
+
+ return;
+ }
+
+ finish(detail, {
+ ok: false,
+ message: (typeof data.message === 'string' && data.message)
+ || (config.i18n && config.i18n.failed)
+ || '',
+ });
+ })
+ .catch(function () {
+ finish(detail, {
+ ok: false,
+ message: (config.i18n && config.i18n.failed) || '',
+ });
+ });
+ }
+
+ function go(detail, url, missingMessage) {
+ if (! isSameSite(url)) {
+ finish(detail, { ok: false, message: missingMessage || (config.i18n && config.i18n.failed) || '' });
+
+ return;
+ }
+
+ finish(detail, { ok: true, message: '' });
+ window.location.assign(url);
+ }
+
+ function isSameSite(url) {
+ if (! url || typeof url !== 'string') {
+ return false;
+ }
+
+ try {
+ var parsed = new URL(url, window.location.href);
+ var home = String(config.homeHost || window.location.hostname).toLowerCase();
+ var host = parsed.hostname.toLowerCase();
+
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
+ return false;
+ }
+
+ return host === home || host === 'www.' + home || 'www.' + host === home;
+ } catch (error) {
+ return false;
+ }
+ }
+
+ function isPlainObject(value) {
+ return Boolean(value) && typeof value === 'object' && ! Array.isArray(value);
+ }
+
+ function finish(detail, result) {
+ if (typeof detail.respond === 'function') {
+ detail.respond(result);
+
+ return;
+ }
+
+ window.dispatchEvent(new CustomEvent('datalumo:action-result', {
+ detail: {
+ answer_id: detail.answer_id,
+ tool_id: detail.tool_id,
+ run: detail.run,
+ ok: result.ok !== false,
+ message: result.message || '',
+ },
+ }));
+ }
+})();
diff --git a/resources/js/index.php b/resources/js/index.php
new file mode 100644
index 0000000..6220032
--- /dev/null
+++ b/resources/js/index.php
@@ -0,0 +1,2 @@
+ true], 'objects');
-unset($postTypes['attachment']);
+/** @var string $datalumo_tab (set by SettingsPage::render) */
+$datalumo_organisation = Options::get('organisation', []);
+$datalumo_sources = (array) Options::get('sources', []);
+$datalumo_syncs = (array) Options::get('syncs', []);
+$datalumo_post_types = get_post_types(['public' => true], 'objects');
+unset($datalumo_post_types['attachment']);
-$tabs = [
+$datalumo_tabs = [
'connection' => __('Connection', 'datalumo'),
'content-sync' => __('Content sync', 'datalumo'),
'chatbot' => __('Chatbot', 'datalumo'),
'search-box' => __('Search box', 'datalumo'),
'enhanced-search' => __('Enhanced search', 'datalumo'),
];
+
+$datalumo_nonce_ok = isset($_GET['_wpnonce'])
+ && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'datalumo_settings_updated');
+$datalumo_notice = $datalumo_nonce_ok && isset($_GET['datalumo_notice'])
+ ? sanitize_key(wp_unslash($_GET['datalumo_notice']))
+ : '';
+$datalumo_notices = [
+ 'connected' => [__('Connected to Datalumo.', 'datalumo'), 'success'],
+ 'grant_failed' => [__('Connect did not finish. Start again from this page.', 'datalumo'), 'error'],
+ 'disconnected' => [__('Disconnected. The API key remains in Datalumo under API keys until you revoke it there.', 'datalumo'), 'success'],
+ 'setup_saved' => [__('Setup saved.', 'datalumo'), 'success'],
+ 'invalid_widget_key' => [__('Widget keys look like org-id/widget-id. An API token or secret will not work there.', 'datalumo'), 'error'],
+];
+$datalumo_connected = Options::isConnected();
+$datalumo_setup_pending = $datalumo_connected && (bool) Options::get('setup_pending');
+$datalumo_chat_key = (string) Options::get('chatbot.widget_key', '');
+$datalumo_search_key = (string) Options::get('search_box.widget_key', '') ?: (string) Options::get('enhanced.widget_key', '');
+$datalumo_setup_source_id = (string) Options::get('setup_source_id', '');
+$datalumo_setup_types = ['post', 'page'];
+
+foreach ($datalumo_syncs as $datalumo_sync_row) {
+ if (($datalumo_sync_row['source_id'] ?? '') === $datalumo_setup_source_id) {
+ $datalumo_setup_types = $datalumo_sync_row['post_types'] ?? $datalumo_setup_types;
+ break;
+ }
+}
?>