diff --git a/assets/js/post-types/components/control.js b/assets/js/post-types/components/control.js
new file mode 100644
index 00000000..925c774c
--- /dev/null
+++ b/assets/js/post-types/components/control.js
@@ -0,0 +1,256 @@
+/**
+ * WordPress dependencies.
+ */
+import {
+ CheckboxControl,
+ FormTokenField,
+ RadioControl,
+ SelectControl,
+ TextControl,
+ TextareaControl,
+ ToggleControl,
+ Card,
+ CardHeader,
+ CardBody,
+} from '@wordpress/components';
+import { safeHTML } from '@wordpress/dom';
+import { RawHTML } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { usePostTypeSettings } from '../provider';
+
+const Control = ({ type, settings, value, onChange }) => {
+ /**
+ * Handle change to checkbox values.
+ *
+ * @param {boolean} checked Whether checkbox is checked.
+ */
+ const onChangeCheckbox = (checked) => {
+ const value = checked ? '1' : '0';
+
+ onChange(value);
+ };
+
+ /**
+ * Handle change to token field values.
+ *
+ * The FormTokenField control does not support separate values and labels,
+ * so whenever a change is made we need to set the field value based on the
+ * selected label.
+ *
+ * @param {string[]} values Selected values.
+ */
+ const onChangeFormTokenField = (values) => {
+ const value = values
+ .map((v) => settings.options.find((o) => o.label === v)?.value)
+ .filter(Boolean)
+ .join(',');
+
+ onChange(value);
+ };
+
+ const { values } = usePostTypeSettings();
+
+ return (
+
+ {(() => {
+ switch (type) {
+ case 'checkbox': {
+ return (
+
+ );
+ }
+ case 'hidden': {
+ return null;
+ }
+ case 'markup': {
+ return
{safeHTML(settings.label)};
+ }
+ case 'multiple': {
+ const suggestions = settings.options.map((o) => o.label);
+ const values = value
+ .split(',')
+ .map((v) => settings.options.find((o) => o.value === v)?.label)
+ .filter(Boolean);
+
+ return (
+
+ );
+ }
+ case 'radio': {
+ return (
+
+ );
+ }
+ case 'select': {
+ return (
+
+ );
+ }
+ case 'toggle': {
+ return (
+
+ );
+ }
+ case 'textarea': {
+ return (
+
+ );
+ }
+ case 'field_group': {
+ const shouldRenderField = (requires_fields) => {
+ if (!requires_fields || Object.keys(requires_fields).length === 0) {
+ return true;
+ }
+
+ // Get field requirements from 'conditions' key
+ let fieldRequirements;
+
+ if (requires_fields.conditions) {
+ fieldRequirements = Object.entries(requires_fields.conditions);
+ }
+
+ // If no actual field requirements, return true
+ if (fieldRequirements.length === 0) {
+ return true;
+ }
+
+ // Define the condition check function
+ const checkCondition = ([fieldKey, requiredValue]) => {
+ const actualValue = values[fieldKey];
+ // const defaultValue = defaultSettings[fieldKey] ?? false;
+ return actualValue === requiredValue;
+ };
+
+ // Extract relationship type, default to 'AND'
+ const relationship = (
+ requires_fields.relationship || 'AND'
+ ).toUpperCase();
+
+ // Apply the appropriate logic based on relationship type
+ switch (relationship) {
+ case 'OR':
+ return fieldRequirements.some(checkCondition);
+ case 'AND':
+ default:
+ // Default to AND for any unexpected values
+ return fieldRequirements.every(checkCondition);
+ }
+ };
+
+ return (
+
+
+ {settings.label && (
+
+ {settings.label}
+
+ )}
+
+ {settings.fields.map((field) => {
+ const shouldRender = shouldRenderField(
+ field.requires_fields,
+ );
+
+ // Skip rendering if the field's requirements aren't met
+ if (!shouldRender) {
+ return null;
+ }
+ // Get the current value for this field
+ const fieldValue = value?.[field.key] ?? field.default;
+
+ return (
+ {
+ // Create a new object with the updated field value
+ const updatedValue = {
+ ...value,
+ [field.key]: newValue,
+ };
+
+ // Call the parent onChange with the updated nested value
+ onChange(updatedValue);
+ }}
+ />
+ );
+ })}
+
+
+
+ );
+ }
+ default: {
+ return (
+
+ );
+ }
+ }
+ })()}
+
+ );
+};
+
+export default Control;
diff --git a/assets/js/post-types/config.js b/assets/js/post-types/config.js
new file mode 100644
index 00000000..4ea8c001
--- /dev/null
+++ b/assets/js/post-types/config.js
@@ -0,0 +1,6 @@
+/**
+ * Window dependencies.
+ */
+const { activePostType, postTypes, metaFields } = window.epPostTypes;
+
+export { activePostType, postTypes, metaFields };
diff --git a/assets/js/post-types/index.js b/assets/js/post-types/index.js
new file mode 100644
index 00000000..6b057304
--- /dev/null
+++ b/assets/js/post-types/index.js
@@ -0,0 +1,163 @@
+/**
+ * WordPress dependencies.
+ */
+import { createRoot, WPElement, useState, useEffect, useMemo } from '@wordpress/element';
+
+/**
+ * Internal dependencies.
+ */
+import { activePostType, postTypes, metaFields } from './config';
+import Control from './components/control';
+import { PostTypeContext } from './provider';
+
+/**
+ * Styles.
+ */
+import './style.css';
+// eslint-disable-next-line import/no-relative-packages
+import '../../../../elasticpress/assets/css/dashboard.css';
+
+/**
+ * App component.
+ *
+ * @returns {WPElement} App component.
+ */
+const App = () => {
+ const [values, setValues] = useState(metaFields);
+ const currentPostType = postTypes.find((t) => t.slug === activePostType);
+
+ const contextValue = useMemo(() => ({ values, setValues }), [values]);
+
+ useEffect(() => {
+ const form = document.querySelector('form#post');
+ const submit = () => {
+ const nonce = window.epPostTypes?.nonce;
+ const nonceInput = document.createElement('input');
+ nonceInput.type = 'hidden';
+ nonceInput.name = 'ep_post_type_nonce';
+ nonceInput.value = nonce;
+ form.appendChild(nonceInput);
+
+ currentPostType.settingsSchema.forEach((settings) => {
+ const existing = form.querySelector(`input[name="${settings.key}"]`);
+ if (existing) {
+ existing.remove();
+ }
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = settings.key;
+
+ if (settings.type === 'field_group') {
+ input.value = JSON.stringify(values[settings.key] || {});
+ } else {
+ const defaultVal = settings.default ?? '';
+ input.value =
+ values[settings.key].length > 0 ? values[settings.key] : defaultVal;
+ }
+
+ form.appendChild(input);
+ });
+ };
+ form.addEventListener('submit', submit);
+ return () => form.removeEventListener('submit', submit);
+ }, [currentPostType.settingsSchema, values, currentPostType.defaultSettings]);
+
+ /**
+ * Determines whether a control should be rendered based on its requirements.
+ *
+ * @param {object} requires_fields An object representing the required field values for rendering.
+ * Can contain 'conditions' object with field requirements and 'relationship' key ('AND' or 'OR').
+ * @returns {boolean} Returns `true` if the control should be rendered, otherwise `false`.
+ */
+ const shouldRenderControl = (requires_fields) => {
+ if (!requires_fields || Object.keys(requires_fields).length === 0) {
+ return true;
+ }
+
+ // Get field requirements from 'conditions' key
+ let fieldRequirements;
+
+ if (requires_fields.conditions) {
+ fieldRequirements = Object.entries(requires_fields.conditions);
+ }
+
+ // If no actual field requirements, return true
+ if (fieldRequirements.length === 0) {
+ return true;
+ }
+
+ // Define the condition check function
+ const checkCondition = ([fieldKey, requiredValue]) => {
+ const actualValue = values[fieldKey];
+ // const defaultValue = defaultSettings[fieldKey] ?? false;
+ return actualValue === requiredValue;
+ };
+
+ // Extract relationship type, default to 'AND'
+ const relationship = (requires_fields.relationship || 'AND').toUpperCase();
+
+ // Apply the appropriate logic based on relationship type
+ switch (relationship) {
+ case 'OR':
+ return fieldRequirements.some(checkCondition);
+ case 'AND':
+ default:
+ // Default to AND for any unexpected values
+ return fieldRequirements.every(checkCondition);
+ }
+ };
+
+ return (
+
+
+ {currentPostType.settingsSchema.map((schema) => {
+ // Handle conditional rendering
+ if (!shouldRenderControl(schema.requires_fields)) {
+ return null;
+ }
+
+ // Handle default values
+ let value = values[schema.key];
+ if (
+ value.length === 0 &&
+ schema.type === 'field_group' &&
+ schema.fields.some((f) => 'default' in f)
+ ) {
+ const defaultValues = schema.fields.reduce((acc, field) => {
+ if ('default' in field && 'key' in field) {
+ acc[field.key] = field.default;
+ }
+ return acc;
+ }, {});
+ value = JSON.stringify(defaultValues);
+ } else if (value.length === 0 && schema.default?.length > 0) {
+ value = schema.default;
+ }
+
+ return (
+ {
+ if (schema.type === 'field_group') {
+ setValues((prev) => ({
+ ...prev,
+ [schema.key]: val,
+ }));
+ } else {
+ setValues((prev) => ({ ...prev, [schema.key]: val }));
+ }
+ }}
+ />
+ );
+ })}
+
+
+ );
+};
+
+const rootEl = document.getElementById('ep-post-types-dashboard');
+if (rootEl) {
+ createRoot(rootEl).render();
+}
diff --git a/assets/js/post-types/provider.js b/assets/js/post-types/provider.js
new file mode 100644
index 00000000..c7cedb63
--- /dev/null
+++ b/assets/js/post-types/provider.js
@@ -0,0 +1,5 @@
+import { createContext, useContext } from '@wordpress/element';
+
+export const PostTypeContext = createContext();
+
+export const usePostTypeSettings = () => useContext(PostTypeContext);
diff --git a/assets/js/post-types/style.css b/assets/js/post-types/style.css
new file mode 100644
index 00000000..3aedd8fa
--- /dev/null
+++ b/assets/js/post-types/style.css
@@ -0,0 +1,30 @@
+#ep-post-types-dashboard {
+
+ & > div {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding: 20px;
+ }
+}
+
+/* stylelint-disable-next-line selector-id-pattern */
+#custom_fields_app {
+
+ & #message {
+ display: none;
+ }
+
+ & .ep-header-menu {
+ margin-left: 0;
+ }
+}
+
+#poststuff .inside {
+ margin: 0;
+ padding: 0;
+}
+
+.postbox-header {
+ display: none;
+}
diff --git a/includes/classes/Feature/AIBot.php b/includes/classes/Feature/AIBot.php
new file mode 100644
index 00000000..a3a3e103
--- /dev/null
+++ b/includes/classes/Feature/AIBot.php
@@ -0,0 +1,84 @@
+slug = 'ai_bot';
+
+ parent::__construct();
+ }
+
+ /**
+ * Sets i18n strings.
+ *
+ * @return void
+ * @since 2.4.0
+ */
+ public function set_i18n_strings(): void {
+ $this->title = esc_html__( 'AI Bots', 'elasticpress-labs' );
+ }
+
+ /**
+ * Setup your feature functionality.
+ * Use this method to hook your feature functionality to ElasticPress or WordPress.
+ */
+ public function setup() {}
+
+ /**
+ * Generate the instructions text
+ *
+ * @since 2.2.0
+ */
+ public function get_instructions() {
+ ob_start();
+ ?>
+
+
+
+
+
+
+
+ settings_schema = [
+ [
+ 'key' => 'epio',
+ 'label' => $this->get_instructions(),
+ 'type' => 'markup',
+ ],
+ ];
+ }
+}
diff --git a/includes/classes/PostType.php b/includes/classes/PostType.php
new file mode 100644
index 00000000..668421b2
--- /dev/null
+++ b/includes/classes/PostType.php
@@ -0,0 +1,159 @@
+ $this->slug,
+ 'settingsSchema' => $this->get_settings_schema(),
+ ];
+
+ if ( property_exists( $this, 'default_settings' ) && ! empty( $this->default_settings ) ) {
+ $feature_desc['defaultSettings'] = $this->default_settings;
+ }
+
+ return $feature_desc;
+ }
+
+ /**
+ * Return the feature settings schema
+ *
+ * @since 5.3.0
+ * @return array
+ */
+ public function get_settings_schema() {
+ if ( [] === $this->settings_schema ) {
+ $this->set_settings_schema();
+ }
+
+ /**
+ * Filter the settings schema of a feature
+ *
+ * @hook ep_post_type_settings_schema
+ * @since 5.3.0
+ * @param {array} $settings_schema True if the feature is available
+ * @param {string} $feature_slug Feature slug
+ * @param {Feature} $feature Feature object
+ * @return {array} New $settings_schema value
+ */
+ return apply_filters( 'ep_post_type_settings_schema', $this->settings_schema, $this );
+ }
+
+ /**
+ * Sets the settings_schema
+ *
+ * @since 5.3.0
+ */
+ protected function set_settings_schema() {
+ }
+}
diff --git a/includes/classes/PostType/Bot.php b/includes/classes/PostType/Bot.php
new file mode 100644
index 00000000..ade476b6
--- /dev/null
+++ b/includes/classes/PostType/Bot.php
@@ -0,0 +1,150 @@
+slug = 'ai-bot';
+ $this->icon = 'dashicons-nametag';
+ $this->order = 20;
+
+ $this->classic_editor_only = true;
+
+ $this->overridable_features = [ 'vector_embeddings', 'semantic_search', 'ai_search_summary' ];
+
+ parent::__construct();
+ }
+
+ /**
+ * Default settings
+ *
+ * @var array $default_settings.
+ */
+ public $default_settings = [
+ 'user_prompt' => "You are an assistent in a website and you need to reply to a user search. If you do not know the answer, reply saying you could not find any results. Your answer should come formatted in HTML, but not as a full HTML page, just wrap everything in a div with the 'epio-response' class. Also, do not wrap it with ```html``` tags.
+
+The following JSON object contains the URL and the page content. You should use it as context:
+
+{posts}",
+ 'ai_feature_config' => [
+ 'highlight_excerpt' => '1',
+ ],
+ ];
+
+ /**
+ * Setup search functionality.
+ *
+ * @return void
+ */
+ public function setup() {
+ }
+
+ /**
+ * Sets i18n strings.
+ *
+ * @return void
+ * @since 5.3.0
+ */
+ public function set_i18n_strings(): void {
+ $this->name_plural = esc_html__( 'AI Bots', 'elasticpress' );
+ $this->name_single = esc_html__( 'AI Bot', 'elasticpress' );
+ }
+
+ /**
+ * Pulls each overridable feature's setting schema
+ *
+ * @since 5.3.0
+ */
+ public function set_overridable_features(): void {
+ $store = Features::factory();
+ $features = $store->registered_features;
+ $options = [
+ [
+ 'label' => __( 'None', 'elasticpress' ),
+ 'value' => '',
+ ],
+ ];
+
+ $feature_settings_all = [];
+ foreach ( $this->overridable_features as $slug ) {
+ $feature = $features[ $slug ];
+ $feature_settings = $feature->get_settings_schema();
+
+ $options[] = [
+ 'label' => $feature->title,
+ 'value' => $feature->slug,
+ ];
+
+ $feature = $features[ $slug ];
+ $feature_settings = array_slice( $feature->get_settings_schema(), 1 );
+
+ $group = [
+ 'type' => 'field_group',
+ 'key' => $feature->slug . '_config',
+ 'label' => $feature->title,
+ 'fields' => $feature_settings,
+ 'requires_fields' => [
+ 'conditions' => [
+ 'feature_selection' => $slug,
+ ],
+ ],
+ ];
+
+ $feature_settings_all[] = $group;
+
+ }
+ $this->settings_schema[] = [
+ 'help' => __( 'Select an AI-enabled ElasticPress Feature to override its configuration.', 'elasticpress' ),
+ 'key' => 'feature_selection',
+ 'label' => __( 'Feature Selection', 'elasticpress' ),
+ 'options' => $options,
+ 'type' => 'select',
+ ];
+
+ $this->settings_schema = array_merge( $this->settings_schema, $feature_settings_all );
+ }
+
+ /**
+ * Set the `settings_schema` attribute
+ *
+ * @since 5.3.0
+ */
+ protected function set_settings_schema() {
+ $this->settings_schema = [
+ [
+ 'key' => 'user_prompt',
+ 'label' => __( 'AI User Prompt', 'elasticpress-labs' ),
+ 'help' => __( 'The {posts} string will be replaced.', 'elasticpress-labs' ),
+ 'type' => 'textarea',
+ 'default' => $this->default_settings['user_prompt'],
+ ],
+ [
+ 'key' => 'system_prompt',
+ 'label' => __( 'AI System Prompt', 'elasticpress-labs' ),
+ 'help' => __( 'The {posts} string will be replaced.', 'elasticpress-labs' ),
+ 'type' => 'textarea',
+ ],
+ ];
+ $this->set_overridable_features();
+ }
+}
diff --git a/includes/classes/PostTypes.php b/includes/classes/PostTypes.php
new file mode 100644
index 00000000..308586f9
--- /dev/null
+++ b/includes/classes/PostTypes.php
@@ -0,0 +1,286 @@
+registered_post_types ), true ) ) {
+ return;
+ }
+
+ wp_enqueue_script(
+ 'ep_post_types_script',
+ ELASTICPRESS_LABS_URL . 'dist/js/post-types-script.js',
+ Utils\get_asset_info( 'post-types-script', 'dependencies' ),
+ Utils\get_asset_info( 'post-types-script', 'version' ),
+ true
+ );
+
+ wp_set_script_translations( 'ep_post_types_script', 'elasticpress' );
+
+ wp_enqueue_style(
+ 'ep_post_types_script',
+ ELASTICPRESS_LABS_URL . 'dist/css/post-types-script.css',
+ [ 'wp-components', 'wp-edit-post' ],
+ Utils\get_asset_info( 'features-script', 'version' )
+ );
+
+ $post_types = $this->registered_post_types;
+ $post_types = array_map( fn( $post_type ) => $post_type->get_json(), $post_types );
+ $post_types = array_values( $post_types );
+
+ // Build meta_fields using get_post_meta( get_the_ID(), $key, true ) for each key in the settings schema
+ $meta_fields = [];
+ $current_post_type = get_post_type();
+ if ( isset( $this->registered_post_types[ $current_post_type ] ) ) {
+ $settings_schema = $this->registered_post_types[ $current_post_type ]->get_settings_schema();
+ foreach ( $settings_schema as $settings ) {
+ $key = $settings['key'];
+ $value = get_post_meta( get_the_ID(), $key, true );
+ if ( is_string( $value ) && is_serialized( $value ) ) {
+ $value = maybe_unserialize( $value );
+ }
+ $meta_fields[ $key ] = $value;
+ }
+ }
+
+ $data = [
+ 'activePostType' => get_post_type(),
+ 'postTypes' => $post_types,
+ 'metaFields' => $meta_fields,
+ 'nonce' => wp_create_nonce( 'ep_post_type_save' ),
+ ];
+
+ wp_localize_script( 'ep_post_types_script', 'epPostTypes', $data );
+ }
+
+ /**
+ * Set up all active features
+ *
+ * @since 5.3.0
+ */
+ public function setup_post_types() {
+ /**
+ * Fires before post types are setup
+ *
+ * @hook ep_setup_post_types
+ * @since 5.3.0
+ */
+ do_action( 'ep_setup_post_types' );
+
+ foreach ( $this->registered_post_types as $post_type_slug => $post_type ) {
+ $post_type->set_i18n_strings();
+
+ $name_single = $post_type->name_single ?? '';
+ $name_plural = $post_type->name_plural ?? '';
+
+ register_post_type(
+ $post_type_slug,
+ [
+ 'labels' => [
+ 'name' => $post_type->name_plural,
+ 'singular_name' => $post_type->name_single,
+ 'menu_name' => $post_type->name_plural,
+ 'name_admin_bar' => $post_type->name_single,
+ 'add_new' => 'Add New',
+ 'add_new_item' => "Add New $post_type->name_single",
+ 'new_item' => "New $post_type->name_single",
+ 'edit_item' => "Edit $post_type->name_single",
+ 'view_item' => "View $post_type->name_single",
+ 'all_items' => "All $post_type->name_plural",
+ 'search_items' => "Search $post_type->name_plural",
+ 'not_found' => "No $post_type->name_plural found.",
+ 'not_found_in_trash' => "No $post_type->name_plural found in Trash.",
+ ],
+ 'public' => true,
+ 'has_archive' => true,
+ 'show_in_rest' => true,
+ 'supports' => [ 'title' ],
+ 'menu_position' => $post_type->order ?? '',
+ 'menu_icon' => $post_type->icon ?? '',
+ ]
+ );
+ }
+ }
+
+ /**
+ * Registers meta fields
+ */
+ public function register_meta_fields() {
+ foreach ( $this->registered_post_types as $slug => $post_type ) {
+ foreach ( $post_type->get_settings_schema() as $settings ) {
+ if ( 'field_group' === $settings['type'] ) {
+ register_post_meta(
+ $slug,
+ $settings['key'],
+ [
+ 'show_in_rest' => true,
+ 'type' => 'array',
+ 'auth_callback' => '__return_true',
+ ]
+ );
+ } else {
+ register_post_meta(
+ $slug,
+ $settings['key'],
+ [
+ 'show_in_rest' => true,
+ 'type' => 'string',
+ 'single' => true,
+ 'auth_callback' => '__return_true',
+ ]
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers a meta box for custom fields rendered by a React app.
+ *
+ * @since 5.3.0
+ *
+ * @return void
+ */
+ public function setup_meta_fields() {
+ add_meta_box(
+ 'custom_fields_app',
+ 'Custom Fields (React)',
+ function ( $post ) {
+ include EP_PATH . '/includes/partials/header.php';
+ echo '';
+ }
+ );
+ }
+
+ /**
+ * Registers a meta box for custom fields rendered by a React app.
+ *
+ * @since 5.3.0
+ * @param int $post_id - id of the current post
+ *
+ * @return void
+ */
+ public function save_meta_fields( $post_id ) {
+ if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
+ return;
+ }
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+ if ( ! isset( $_POST['ep_post_type_nonce'] ) || ! wp_verify_nonce( $_POST['ep_post_type_nonce'], 'ep_post_type_save' ) ) {
+ return;
+ }
+ $post_type = get_post_type( $post_id );
+ if ( isset( $this->registered_post_types[ $post_type ] ) ) {
+ $settings_schema = $this->registered_post_types[ $post_type ]->get_settings_schema();
+ foreach ( $settings_schema as $settings ) {
+ $key = $settings['key'];
+ if ( isset( $_POST[ $key ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below
+ if ( 'field_group' === $settings['type'] ) {
+ $decoded = json_decode( wp_unslash( $_POST[ $key ] ), true ); // phpcs:ignore
+ if ( json_last_error() === JSON_ERROR_NONE && is_array( $decoded ) ) {
+ update_post_meta( $post_id, $key, $decoded );
+ } else {
+ update_post_meta( $post_id, $key, [] );
+ }
+ } else {
+ update_post_meta( $post_id, $key, sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) );
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers a Post Type for use in ElasticPress
+ *
+ * @param PostType $post_type An instance of the PostType class
+ * @since 5.3.0
+ * @return boolean
+ */
+ public function register_post_type( PostType $post_type ) {
+ $this->registered_post_types[ $post_type->slug ] = $post_type;
+ return true;
+ }
+
+ /**
+ * Determines whether to disable the Gutenberg block editor for specific post types.
+ *
+ * @since 5.3.0
+ *
+ * @param bool $use_block_editor Whether the block editor is enabled for this post type.
+ * @param string $current_post_type The post type being checked.
+ * @return bool False if the block editor should be disabled for the post type, otherwise the original value.
+ */
+ public function maybe_disable_gutenberg( $use_block_editor, $current_post_type ) {
+ $disabled_post_types = [];
+ foreach ( $this->registered_post_types as $post_type_slug => $post_type ) {
+ if ( $post_type->classic_editor_only ) {
+ $disabled_post_types[] = $post_type_slug;
+ }
+ }
+ return in_array( $current_post_type, $disabled_post_types, true ) ? false : $use_block_editor;
+ }
+
+ /**
+ * Return singleton instance of class
+ *
+ * @return object
+ * @since 2.1
+ */
+ public static function factory() {
+ static $instance = false;
+
+ if ( ! $instance ) {
+ $instance = new self();
+ $instance->setup();
+ }
+
+ return $instance;
+ }
+}
diff --git a/includes/functions/core.php b/includes/functions/core.php
index 2befaa21..1f14681d 100644
--- a/includes/functions/core.php
+++ b/includes/functions/core.php
@@ -37,6 +37,8 @@ function setup() {
do_action( 'elasticpress_labs_loaded' );
+ add_action( 'elasticpress_loaded', $n( 'setup_post_types' ) );
+
setup_updater();
}
@@ -249,6 +251,9 @@ function maybe_load_features() {
$ai_search_summary = new \ElasticPressLabs\Feature\AISearchSummary();
\ElasticPress\Features::factory()->register_feature( $ai_search_summary );
+
+ $ai_bot = new \ElasticPressLabs\Feature\AIBot();
+ \ElasticPress\Features::factory()->register_feature( $ai_bot );
}
/**
@@ -317,3 +322,24 @@ function ( $plugin_info ) {
}
);
}
+
+
+/**
+ * Setup the post types
+ *
+ * @since 2.1.1
+ * @return void
+ */
+function setup_post_types() {
+ $settings = \ElasticPress\Features::factory()->get_feature_settings();
+
+ /**
+ * Handle Bots Post Type
+ */
+ $ai_bot = $settings['ai_bot'];
+ if ( $ai_bot['active'] ) {
+ \ElasticPressLabs\PostTypes::factory()->register_post_type(
+ new \ElasticPressLabs\PostType\Bot()
+ );
+ }
+}
diff --git a/package.json b/package.json
index f6b5483e..34d8a009 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,7 @@
"geo-location-editor-script": "./assets/js/geo-location/editor.js",
"geo-location-script": "./assets/js/geo-location/index.js",
"search-templates-script": "./assets/js/search-templates/index.js",
-
+ "post-types-script": "./assets/js/post-types/index.js",
"blocks-styles": "./assets/css/blocks.css"
},
"wpDependencyExternals": true