diff --git a/packages/x-components/src/__stubs__/index.ts b/packages/x-components/src/__stubs__/index.ts
index 2a437254f3..aeab9517d3 100644
--- a/packages/x-components/src/__stubs__/index.ts
+++ b/packages/x-components/src/__stubs__/index.ts
@@ -15,3 +15,4 @@ export * from './results-stubs.factory';
export * from './search-response-stubs.factory';
export * from './semantic-queries-stubs.factory';
export * from './tagging-response-stubs.factory';
+export * from './related-prompts-stubs.factory';
diff --git a/packages/x-components/src/__stubs__/related-prompts-stubs.factory.ts b/packages/x-components/src/__stubs__/related-prompts-stubs.factory.ts
new file mode 100644
index 0000000000..ce47147c43
--- /dev/null
+++ b/packages/x-components/src/__stubs__/related-prompts-stubs.factory.ts
@@ -0,0 +1,43 @@
+import { RelatedPrompt } from '@empathyco/x-types';
+
+/**
+ * Creates a {@link @empathyco/x-types#RelatedPrompt | related prompts} stub.
+ *
+ * @param amount - Number of stubbed related prompts to create.
+ *
+ * @returns Array of related prompts stub.
+ *
+ * @internal
+ */
+export function getRelatedPromptsStub(amount = 12): RelatedPrompt[] {
+ return Array.from({ length: amount }, (_, index) =>
+ createRelatedPromptStub(`Related Prompt ${index + 1}`)
+ );
+}
+
+/**
+ * Creates a related prompt stub with the provided options.
+ *
+ * @param suggestionText - The suggested text of the related prompt.
+ *
+ * @returns A related prompt.
+ */
+export function createRelatedPromptStub(suggestionText: string): RelatedPrompt {
+ return {
+ suggestionText,
+ nextQueries: createNextQueriesArrayStub(10),
+ modelName: 'RelatedPrompt',
+ type: Math.random() < 0.5 ? 'CURATED' : 'SYNTHETIC'
+ };
+}
+
+/**
+ * Creates an array of next queries.
+ *
+ * @param amount - Number of next queries to create.
+ *
+ * @returns Array of next queries.
+ */
+function createNextQueriesArrayStub(amount: number): string[] {
+ return Array.from({ length: amount }, (_, index) => `Next query ${index + 1}`);
+}
diff --git a/packages/x-components/src/adapter/mocked-responses.ts b/packages/x-components/src/adapter/mocked-responses.ts
index 768ee3443a..23d8049ff3 100644
--- a/packages/x-components/src/adapter/mocked-responses.ts
+++ b/packages/x-components/src/adapter/mocked-responses.ts
@@ -10,7 +10,8 @@ import {
createSimpleFacetStub,
getFacetsStub,
getRelatedTagsStub,
- getResultsStub
+ getResultsStub,
+ getRelatedPromptsStub
} from '../__stubs__/index';
export const mockedApiUrl = 'https://api.empathy.co';
@@ -198,6 +199,9 @@ export const mockedResponses = {
},
'related-tags': {
relatedTags: getRelatedTagsStub()
+ },
+ 'related-prompts': {
+ relatedPrompts: getRelatedPromptsStub()
}
};
diff --git a/packages/x-components/src/components/base-grid.vue b/packages/x-components/src/components/base-grid.vue
index 6f57d626e4..967fcbc41a 100644
--- a/packages/x-components/src/components/base-grid.vue
+++ b/packages/x-components/src/components/base-grid.vue
@@ -219,7 +219,8 @@
}
.x-base-grid__banner,
- .x-base-grid__next-queries-group {
+ .x-base-grid__next-queries-group,
+ .x-base-grid__related-prompts-group {
grid-column-start: 1;
grid-column-end: -1;
}
diff --git a/packages/x-components/src/index.ts b/packages/x-components/src/index.ts
index c32bee25e7..f215f3c1ba 100644
--- a/packages/x-components/src/index.ts
+++ b/packages/x-components/src/index.ts
@@ -26,6 +26,7 @@ export * from './x-modules/popular-searches';
export * from './x-modules/queries-preview';
export * from './x-modules/query-suggestions';
export * from './x-modules/recommendations';
+export * from './x-modules/related-prompts';
export * from './x-modules/related-tags';
export * from './x-modules/scroll';
export * from './x-modules/search';
diff --git a/packages/x-components/src/views/home/Home.vue b/packages/x-components/src/views/home/Home.vue
index 9de9623b72..a8f42d5ab9 100644
--- a/packages/x-components/src/views/home/Home.vue
+++ b/packages/x-components/src/views/home/Home.vue
@@ -76,7 +76,7 @@
-
+
next-queries-list - showOnlyAfterOffset
+
+
+ related-prompts-list - showOnlyAfterOffset
+
+
+
Use mocked adapter
@@ -315,64 +326,111 @@
:show-only-after-offset="controls.nextQueriesList.showOnlyAfterOffset"
class="x-mot-next-queries-list"
>
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
- Others clients have searched
-
+
- {{ nextQueries[0].query }}
-
-
- Others clients have searched
+
+ {{ nextQueries[0].query }}
+
+
+
+ {{ 'View all results' }}
+
+
+
+
+
+
- {{ 'View all results' }}
-
-
-
-
+
+ {{ suggestionText }}
+
+
+
+
+
+
+ {{ selectedQuery }}
+ ({{ totalResults }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -530,6 +588,9 @@
import { QueryPreviewInfo } from '../../x-modules/queries-preview/store/types';
import QueryPreviewButton from '../../x-modules/queries-preview/components/query-preview-button.vue';
import DisplayEmitter from '../../components/display-emitter.vue';
+ import RelatedPromptsList from '../../x-modules/related-prompts/components/related-prompts-list.vue';
+ import RelatedPrompt from '../../x-modules/related-prompts/components/related-prompt.vue';
+ import QueryPreview from '../../x-modules/queries-preview/components/query-preview.vue';
import Aside from './aside.vue';
import PredictiveLayer from './predictive-layer.vue';
import Result from './result.vue';
@@ -569,6 +630,8 @@
NextQueriesList,
NextQuery,
NextQueryPreview,
+ RelatedPromptsList,
+ RelatedPrompt,
OpenMainModal,
PartialQueryButton,
PartialResultsList,
@@ -576,6 +639,7 @@
PreselectedFilters,
Promoted,
PromotedsList,
+ QueryPreview,
QueryPreviewButton,
QueryPreviewList,
Recommendations,
@@ -641,6 +705,9 @@
nextQueriesList: {
showOnlyAfterOffset: true
},
+ relatedPromptsList: {
+ showOnlyAfterOffset: true
+ },
adapter: {
useE2EAdapter: false
}
diff --git a/packages/x-components/src/views/home/types.ts b/packages/x-components/src/views/home/types.ts
index 8591a3d65c..b9b4dfcdd0 100644
--- a/packages/x-components/src/views/home/types.ts
+++ b/packages/x-components/src/views/home/types.ts
@@ -18,6 +18,9 @@ export interface HomeControls {
nextQueriesList: {
showOnlyAfterOffset: boolean;
};
+ relatedPromptsList: {
+ showOnlyAfterOffset: boolean;
+ };
adapter: {
useE2EAdapter: boolean;
};
diff --git a/packages/x-components/src/wiring/events.types.ts b/packages/x-components/src/wiring/events.types.ts
index fbca9c3efb..a527a08488 100644
--- a/packages/x-components/src/wiring/events.types.ts
+++ b/packages/x-components/src/wiring/events.types.ts
@@ -22,6 +22,7 @@ import { UrlXEvents } from '../x-modules/url/events.types';
import { XModuleName } from '../x-modules/x-modules.types';
import { SemanticQueriesXEvents } from '../x-modules/semantic-queries/events.types';
import { ExperienceControlsXEvents } from '../x-modules/experience-controls/events.types';
+import { RelatedPromptsXEvents } from '../x-modules/related-prompts/events.types';
import { WireMetadata } from './wiring.types';
/* eslint-disable max-len */
/**.
@@ -51,6 +52,7 @@ import { WireMetadata } from './wiring.types';
* {@link https://github.com/empathyco/x/blob/main/packages/x-components/src/x-modules/search/events.types.ts | SearchXEvents}
* {@link https://github.com/empathyco/x/blob/main/packages/x-components/src/x-modules/tagging/events.types.ts | TaggingXEvents}
* {@link https://github.com/empathyco/x/blob/main/packages/x-components/src/x-modules/url/events.types.ts | UrlXEvents}
+ * {@link https://github.com/empathyco/x/blob/main/packages/x-components/src/x-modules/related-prompts/events.types.ts | RelatedPromptsXEvents}
*
* @public
*/
@@ -73,7 +75,8 @@ export interface XEventsTypes
SemanticQueriesXEvents,
TaggingXEvents,
ExperienceControlsXEvents,
- UrlXEvents {
+ UrlXEvents,
+ RelatedPromptsXEvents {
/**
* The provided number of columns of a grid has changed.
* Payload: the columns number.
diff --git a/packages/x-components/src/x-modules/related-prompts/components/index.ts b/packages/x-components/src/x-modules/related-prompts/components/index.ts
new file mode 100644
index 0000000000..557325c884
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/components/index.ts
@@ -0,0 +1,2 @@
+export { default as RelatedPrompt } from './related-prompt.vue';
+export { default as RelatedPromptsList } from './related-prompts-list.vue';
diff --git a/packages/x-components/src/x-modules/related-prompts/components/related-prompt.vue b/packages/x-components/src/x-modules/related-prompts/components/related-prompt.vue
new file mode 100644
index 0000000000..7663c3a688
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/components/related-prompt.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
diff --git a/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue b/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue
new file mode 100644
index 0000000000..0c0429689d
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue
@@ -0,0 +1,362 @@
+
+
+
+## Events
+
+This component emits no events.
+
+## See it in action
+
+
+
+:::warning Backend microservice required To use this component, the QuerySignals microservice
+must be implemented. :::
+
+
+
+Usually, this component is going to be used together with the `ResultsList` one. Related prompts
+groups will be inserted between the results, guiding users to discover new searches directly from
+the results list.
+
+```vue live
+
+
+
+
+
+
+
+
+
+
+```
+
+### Play with the index that related prompts groups are inserted at
+
+The component allows to customise where are the related prompts groups inserted. In the following
+example, the first group of related prompts will be inserted at the index `48` (`offset`), and then
+a second group will be inserted at index `120` because of the `frequency` prop configured to `72`.
+Finally, a third group will be inserted at index `192`. Because `maxGroups` is configured to `3`, no
+more groups will be inserted. Each one of this groups will have up to `6` related prompts
+(`maxRelatedPromptsPerGroup`).
+
+```vue live
+
+
+
+
+
+
+
+
+
+
+```
+
+### Showing/hiding first related prompts group when no more items
+
+By default, the first related prompts group will be inserted when the total number of results is
+smaller than the offset, but this behavior can be deactivated by setting the `showOnlyAfterOffset`
+to `true`.
+
+```vue live
+
+
+
+
+
+
+
+
+
+
+```
+
+### Customise the layout of the component
+
+This component will render by default the `id` of each search item, both the injected, and for the
+groups of related prompts generated, but the common case is to integrate it with another layout
+component, for example the `BaseGrid`. To do so, you can use the `default` slot
+
+```vue
+
+
+
+
+
+
+
+
+ RelatedPromptsGroup:
+ {{ prompt }}
+
+
+
+ Result: {{ item.name }}
+
+
+ Default: {{ item }}
+
+
+
+
+
+
+
+
+```
+
diff --git a/packages/x-components/src/x-modules/related-prompts/events.types.ts b/packages/x-components/src/x-modules/related-prompts/events.types.ts
new file mode 100644
index 0000000000..12d8c1f126
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/events.types.ts
@@ -0,0 +1,16 @@
+import { RelatedPromptsRequest } from '@empathyco/x-types';
+
+/**
+ * Dictionary of the events of RelatedPrompts XModule, where each key is the event name,
+ * and the value is the event payload type or `void` if it has no payload.
+ *
+ * @public
+ */
+export interface RelatedPromptsXEvents {
+ /**
+ * Any property of the related-prompts request has changed
+ * Payload: The new related-prompts request or `null` if there is not enough data in the state
+ * to conform a valid request.
+ */
+ RelatedPromptsRequestUpdated: RelatedPromptsRequest | null;
+}
diff --git a/packages/x-components/src/x-modules/related-prompts/index.ts b/packages/x-components/src/x-modules/related-prompts/index.ts
new file mode 100644
index 0000000000..0baeb1b99e
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/index.ts
@@ -0,0 +1,5 @@
+export * from './components';
+export * from './events.types';
+export * from './store';
+export * from './wiring';
+export * from './x-module';
diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts
new file mode 100644
index 0000000000..79efe65bec
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/actions.spec.ts
@@ -0,0 +1,118 @@
+import { mount } from '@vue/test-utils';
+import { Store } from 'vuex';
+import { getRelatedPromptsStub } from '../../../../__stubs__';
+import { getMockedAdapter, installNewXPlugin } from '../../../../__tests__/utils';
+import { SafeStore } from '../../../../store/__tests__/utils';
+import {
+ RelatedPromptsActions,
+ RelatedPromptsGetters,
+ RelatedPromptsMutations,
+ RelatedPromptsState
+} from '../types';
+import { relatedPromptsXStoreModule } from '../module';
+import { resetRelatedPromptsStateWith } from './utils';
+
+describe('testing related prompts module actions', () => {
+ const mockedRelatedPrompts = getRelatedPromptsStub();
+
+ const adapter = getMockedAdapter({
+ relatedPrompts: { relatedPrompts: mockedRelatedPrompts }
+ });
+
+ const store: SafeStore<
+ RelatedPromptsState,
+ RelatedPromptsGetters,
+ RelatedPromptsMutations,
+ RelatedPromptsActions
+ > = new Store(relatedPromptsXStoreModule as any);
+ mount(
+ {},
+ {
+ global: {
+ plugins: [installNewXPlugin({ adapter, store })]
+ }
+ }
+ );
+
+ beforeEach(() => {
+ resetRelatedPromptsStateWith(store);
+ });
+
+ describe('fetchRelatedPrompts', () => {
+ it('should return related prompts', async () => {
+ resetRelatedPromptsStateWith(store, {
+ query: 'honeyboo'
+ });
+
+ const relatedPrompts = await store.dispatch('fetchRelatedPrompts', store.getters.request);
+ expect(relatedPrompts).toEqual(mockedRelatedPrompts);
+ });
+
+ it('should return `null` if there is not request', async () => {
+ const relatedPrompts = await store.dispatch('fetchRelatedPrompts', store.getters.request);
+ expect(relatedPrompts).toBeNull();
+ });
+ });
+
+ describe('fetchAndSaveRelatedPrompts', () => {
+ it('should request and store related prompts in the state', async () => {
+ resetRelatedPromptsStateWith(store, {
+ query: 'honeyboo'
+ });
+
+ const actionPromise = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request);
+ expect(store.state.status).toEqual('loading');
+ await actionPromise;
+ expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts);
+ expect(store.state.status).toEqual('success');
+ });
+
+ it('should not clear related prompts in the state if the query is empty', async () => {
+ resetRelatedPromptsStateWith(store, { relatedPrompts: mockedRelatedPrompts });
+
+ await store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request);
+ expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts);
+ });
+
+ it('should cancel the previous request if it is not yet resolved', async () => {
+ resetRelatedPromptsStateWith(store, { query: 'steak' });
+ const initialRelatedPrompts = store.state.relatedPrompts;
+ adapter.relatedPrompts.mockResolvedValueOnce({
+ relatedPrompts: mockedRelatedPrompts.slice(0, 1)
+ });
+
+ const firstRequest = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request);
+ const secondRequest = store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request);
+
+ await firstRequest;
+ expect(store.state.status).toEqual('loading');
+ expect(store.state.relatedPrompts).toBe(initialRelatedPrompts);
+ await secondRequest;
+ expect(store.state.status).toEqual('success');
+ expect(store.state.relatedPrompts).toEqual(mockedRelatedPrompts);
+ });
+
+ it('should set the status to error when it fails', async () => {
+ resetRelatedPromptsStateWith(store, { query: 'milk' });
+ adapter.relatedPrompts.mockRejectedValueOnce('Generic error');
+ const relatedPrompts = store.state.relatedPrompts;
+ await store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request);
+
+ expect(store.state.relatedPrompts).toBe(relatedPrompts);
+ expect(store.state.status).toEqual('error');
+ });
+ });
+
+ describe('cancelFetchAndSaveRelatedPrompts', () => {
+ it('should cancel the request and do not modify the stored related prompts', async () => {
+ resetRelatedPromptsStateWith(store, { query: 'honeyboo' });
+ const previousRelatedPrompts = store.state.relatedPrompts;
+ await Promise.all([
+ store.dispatch('fetchAndSaveRelatedPrompts', store.getters.request),
+ store.dispatch('cancelFetchAndSaveRelatedPrompts')
+ ]);
+ expect(store.state.relatedPrompts).toEqual(previousRelatedPrompts);
+ expect(store.state.status).toEqual('success');
+ });
+ });
+});
diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts
new file mode 100644
index 0000000000..304ea29c0c
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/getters.spec.ts
@@ -0,0 +1,37 @@
+import { RelatedPromptsRequest } from '@empathyco/x-types';
+import { map } from '@empathyco/x-utils';
+import { Store } from 'vuex';
+import { relatedPromptsXStoreModule } from '../module';
+import { RelatedPromptsState } from '../types';
+import { resetRelatedPromptsStateWith } from './utils';
+
+describe('testing related prompts module getters', () => {
+ const gettersKeys = map(relatedPromptsXStoreModule.getters, getter => getter);
+ const store: Store = new Store(relatedPromptsXStoreModule as any);
+
+ beforeEach(() => {
+ resetRelatedPromptsStateWith(store);
+ });
+
+ describe(`${gettersKeys.request} getter`, () => {
+ it('should return a request object if there is a query', () => {
+ resetRelatedPromptsStateWith(store, {
+ query: 'queso',
+ params: {
+ catalog: 'es'
+ }
+ });
+
+ expect(store.getters[gettersKeys.request]).toEqual({
+ query: 'queso',
+ extraParams: {
+ catalog: 'es'
+ }
+ });
+ });
+
+ it('should return null when there is not query', () => {
+ expect(store.getters[gettersKeys.request]).toBeNull();
+ });
+ });
+});
diff --git a/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts b/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts
new file mode 100644
index 0000000000..cf4e0254f7
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/__tests__/utils.ts
@@ -0,0 +1,21 @@
+import { DeepPartial } from '@empathyco/x-utils';
+import { Store } from 'vuex';
+import { resetStoreModuleState } from '../../../../__tests__/utils';
+import { relatedPromptsXStoreModule } from '../module';
+import { RelatedPromptsState } from '../types';
+
+/**
+ * Reset related prompt module state with its original state and the partial state passes as
+ * parameter.
+ *
+ * @param store - Related prompt store state.
+ * @param state - Partial related prompt store state to be replaced.
+ *
+ * @internal
+ */
+export function resetRelatedPromptsStateWith(
+ store: Store,
+ state?: DeepPartial
+): void {
+ resetStoreModuleState(store, relatedPromptsXStoreModule.state(), state);
+}
diff --git a/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-and-save-related-prompts.action.ts b/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-and-save-related-prompts.action.ts
new file mode 100644
index 0000000000..7c515cee7e
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-and-save-related-prompts.action.ts
@@ -0,0 +1,34 @@
+import { RelatedPrompt, RelatedPromptsRequest } from '@empathyco/x-types';
+import { createFetchAndSaveActions } from '../../../../store/utils/fetch-and-save-action.utils';
+import { RelatedPromptsActionContext } from '../types';
+
+const { fetchAndSave, cancelPrevious } = createFetchAndSaveActions<
+ RelatedPromptsActionContext,
+ RelatedPromptsRequest | null,
+ RelatedPrompt[] | null
+>({
+ fetch({ dispatch }, request) {
+ return dispatch('fetchRelatedPrompts', request);
+ },
+ onSuccess({ commit }, relatedPrompts) {
+ if (relatedPrompts) {
+ commit('setRelatedPromptsProducts', relatedPrompts);
+ }
+ }
+});
+
+/**
+ * Default implementation for
+ * {@link RelatedPromptsActions.fetchAndSaveRelatedPrompts} action.
+ *
+ * @public
+ */
+export const fetchAndSaveRelatedPrompts = fetchAndSave;
+
+/**
+ * Default implementation for
+ * {@link RelatedPromptsActions.cancelFetchAndSaveRelatedPrompts} action.
+ *
+ * @public
+ */
+export const cancelFetchAndSaveRelatedPrompts = cancelPrevious;
diff --git a/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-related-prompts.action.ts b/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-related-prompts.action.ts
new file mode 100644
index 0000000000..01471c0518
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/actions/fetch-related-prompts.action.ts
@@ -0,0 +1,21 @@
+import { XPlugin } from '../../../../plugins/x-plugin';
+import { RelatedPromptsXStoreModule } from '../types';
+
+/**
+ * Default implementation for the {@link RelatedPromptsActions.fetchRelatedPrompts}.
+ *
+ * @param _ - The {@link https://vuex.vuejs.org/guide/actions.html | context} of the actions,
+ * provided by Vuex.
+ * @param request - The related prompts request to make.
+ * @returns The related prompts response.
+ *
+ * @public
+ */
+export const fetchRelatedPrompts: RelatedPromptsXStoreModule['actions']['fetchRelatedPrompts'] = (
+ _,
+ request
+) => {
+ return request
+ ? XPlugin.adapter.relatedPrompts(request).then(({ relatedPrompts }) => relatedPrompts)
+ : null;
+};
diff --git a/packages/x-components/src/x-modules/related-prompts/store/emitters.ts b/packages/x-components/src/x-modules/related-prompts/store/emitters.ts
new file mode 100644
index 0000000000..ac53143aa3
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/emitters.ts
@@ -0,0 +1,11 @@
+import { createStoreEmitters } from '../../../store';
+import { relatedPromptsXStoreModule } from './module';
+
+/**
+ * {@link StoreEmitters} For the related-prompts module.
+ *
+ * @internal
+ */
+export const relatedPromptsStoreEmitters = createStoreEmitters(relatedPromptsXStoreModule, {
+ RelatedPromptsRequestUpdated: (_, getters) => getters.request
+});
diff --git a/packages/x-components/src/x-modules/related-prompts/store/getters/request.getter.ts b/packages/x-components/src/x-modules/related-prompts/store/getters/request.getter.ts
new file mode 100644
index 0000000000..ca76032a0b
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/getters/request.getter.ts
@@ -0,0 +1,15 @@
+import { RelatedPromptsXStoreModule } from '../types';
+
+/**
+ * Default implementation for the {@link RelatedPromptsGetters.request} getter.
+ *
+ * @param state - Current {@link https://vuex.vuejs.org/guide/state.html | state} of the related
+ * prompts module.
+ *
+ * @returns The related prompts request to fetch data from the API.
+ *
+ * @public
+ */
+export const request: RelatedPromptsXStoreModule['getters']['request'] = ({ params, query }) => {
+ return query ? { query, extraParams: params } : null;
+};
diff --git a/packages/x-components/src/x-modules/related-prompts/store/index.ts b/packages/x-components/src/x-modules/related-prompts/store/index.ts
new file mode 100644
index 0000000000..5b601e6469
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/index.ts
@@ -0,0 +1,6 @@
+export * from './actions/fetch-and-save-related-prompts.action';
+export * from './actions/fetch-related-prompts.action';
+export { request as relatedPromptRequest } from './getters/request.getter';
+export * from './emitters';
+export * from './module';
+export * from './types';
diff --git a/packages/x-components/src/x-modules/related-prompts/store/module.ts b/packages/x-components/src/x-modules/related-prompts/store/module.ts
new file mode 100644
index 0000000000..91d4a6d614
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/module.ts
@@ -0,0 +1,44 @@
+import { setStatus } from '../../../store/utils/status-store.utils';
+import { setQuery } from '../../../store/utils/query.utils';
+import { RelatedPromptsXStoreModule } from './types';
+import {
+ cancelFetchAndSaveRelatedPrompts,
+ fetchAndSaveRelatedPrompts
+} from './actions/fetch-and-save-related-prompts.action';
+import { fetchRelatedPrompts } from './actions/fetch-related-prompts.action';
+import { request } from './getters/request.getter';
+
+/**
+ * {@link XStoreModule} For the related prompt module.
+ *
+ * @internal
+ */
+export const relatedPromptsXStoreModule: RelatedPromptsXStoreModule = {
+ state: () => ({
+ query: '',
+ relatedPrompts: [],
+ status: 'initial',
+ params: {}
+ }),
+ getters: {
+ request
+ },
+ mutations: {
+ setStatus,
+ setQuery,
+ setParams(state, params) {
+ state.params = params;
+ },
+ setRelatedPromptsProducts(state, products) {
+ state.relatedPrompts = products;
+ },
+ resetRelatedPromptsState(state) {
+ state.relatedPrompts = [];
+ }
+ },
+ actions: {
+ fetchRelatedPrompts,
+ fetchAndSaveRelatedPrompts,
+ cancelFetchAndSaveRelatedPrompts
+ }
+};
diff --git a/packages/x-components/src/x-modules/related-prompts/store/types.ts b/packages/x-components/src/x-modules/related-prompts/store/types.ts
new file mode 100644
index 0000000000..6de258ab1d
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/store/types.ts
@@ -0,0 +1,103 @@
+import { RelatedPrompt, RelatedPromptsRequest } from '@empathyco/x-types';
+import { Dictionary } from '@empathyco/x-utils';
+import { QueryMutations, QueryState } from '../../../store/utils/query.utils';
+import { StatusMutations, StatusState } from '../../../store/utils/status-store.utils';
+import { XActionContext, XStoreModule } from '../../../store';
+
+/**
+ * Related prompts module state.
+ *
+ * @public
+ */
+export interface RelatedPromptsState extends StatusState, QueryState {
+ /** The list of the related-prompts, related to the `query` property of the state. */
+ relatedPrompts: RelatedPrompt[];
+ /** The extra params property of the state. */
+ params: Dictionary;
+}
+
+/**
+ * Related prompts module getters.
+ *
+ * @public
+ */
+export interface RelatedPromptsGetters {
+ /**
+ * Request object to retrieve the related prompts using the search adapter, or null if there is
+ * no valid data to conform a valid request.
+ */
+ request: RelatedPromptsRequest | null;
+}
+
+/**
+ * Related prompts module mutations.
+ *
+ * @public
+ */
+export interface RelatedPromptsMutations extends StatusMutations, QueryMutations {
+ /**
+ * Sets the extra params of the module.
+ *
+ * @param params - The new extra params.
+ */
+ setParams(params: Dictionary): void;
+ /**
+ * Sets the related prompts of the module.
+ *
+ * @param relatedPrompts - The new related prompts to save to the state.
+ */
+ setRelatedPromptsProducts(products: RelatedPrompt[]): void;
+ /**
+ * Resets the related prompts state.
+ */
+ resetRelatedPromptsState(): void;
+}
+
+/**
+ * Related prompts module actions.
+ *
+ * @public
+ */
+export interface RelatedPromptsActions {
+ /**
+ * Requests a new set of related prompts for the module query, and returns them.
+ *
+ * @param request - The related prompts request.
+ */
+ fetchRelatedPrompts(request: RelatedPromptsRequest | null): RelatedPrompt[] | null;
+ /**
+ * Requests a new set of related prompts and stores them in the module.
+ *
+ * @param request - The related prompts request.
+ */
+ fetchAndSaveRelatedPrompts(request: RelatedPromptsRequest | null): void;
+ /**
+ * Cancels / interrupt {@link RelatedPromptsActions.fetchAndSaveRelatedPrompts}
+ * synchronous promise.
+ */
+ cancelFetchAndSaveRelatedPrompts(): void;
+}
+
+/**
+ * Related prompts store module.
+ *
+ * @public
+ */
+export type RelatedPromptsXStoreModule = XStoreModule<
+ RelatedPromptsState,
+ RelatedPromptsGetters,
+ RelatedPromptsMutations,
+ RelatedPromptsActions
+>;
+
+/**
+ * Alias type for actions context of the {@link RelatedPromptsXStoreModule}.
+ *
+ * @public
+ */
+export type RelatedPromptsActionContext = XActionContext<
+ RelatedPromptsState,
+ RelatedPromptsGetters,
+ RelatedPromptsMutations,
+ RelatedPromptsActions
+>;
diff --git a/packages/x-components/src/x-modules/related-prompts/types.ts b/packages/x-components/src/x-modules/related-prompts/types.ts
new file mode 100644
index 0000000000..3b26690fb2
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/types.ts
@@ -0,0 +1,10 @@
+import { RelatedPrompt } from '@empathyco/x-types';
+import { ListItem } from '../../utils';
+
+/**
+ * Related promts group interface for the RelatedPrompts.
+ */
+export interface RelatedPromptsGroup extends ListItem {
+ modelName: 'RelatedPromptsGroup';
+ relatedPrompts: RelatedPrompt[];
+}
diff --git a/packages/x-components/src/x-modules/related-prompts/wiring.ts b/packages/x-components/src/x-modules/related-prompts/wiring.ts
new file mode 100644
index 0000000000..72758d813f
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/wiring.ts
@@ -0,0 +1,97 @@
+import {
+ namespacedWireCommit,
+ namespacedWireCommitWithoutPayload,
+ namespacedWireDispatch,
+ namespacedWireDispatchWithoutPayload
+} from '../../wiring/namespaced-wires.factory';
+import {
+ NamespacedWireCommit,
+ NamespacedWireCommitWithoutPayload
+} from '../../wiring/namespaced-wiring.types';
+import { createWiring } from '../../wiring/wiring.utils';
+
+/**
+ * `relatedPrompts` {@link XModuleName | XModule name}.
+ */
+const moduleName = 'relatedPrompts';
+
+/**
+ * WireCommit for {@link RelatedPromptsXModule}.
+ */
+const wireCommit: NamespacedWireCommit = namespacedWireCommit(moduleName);
+
+/**
+ * WireCommitWithoutPayload for {@link RelatedPromptsXModule}.
+ */
+const wireCommitWithoutPayload: NamespacedWireCommitWithoutPayload =
+ namespacedWireCommitWithoutPayload(moduleName);
+
+/**
+ * WireDispatch for {@link RelatedPromptsXModule}.
+ */
+const wireDispatch = namespacedWireDispatch(moduleName);
+
+/**
+ * WireDispatchWithoutPayload for {@link RelatedPromptsXModule}.
+ */
+const wireDispatchWithoutPayload = namespacedWireDispatchWithoutPayload(moduleName);
+
+/**
+ * Sets the related prompts state `query`.
+ */
+const setRelatedPromptsQuery = wireCommit('setQuery');
+
+/**
+ * Sets the related prompts state `query` from url params.
+ */
+const setRelatedPromptsQueryFromUrl = wireCommit(
+ 'setQuery',
+ ({ eventPayload: { query } }) => query
+);
+
+/**
+ * Sets the related prompts state `params`.
+ */
+const setRelatedPromptsExtraParams = wireCommit('setParams');
+
+/**
+ * Resets the related prompts state.
+ */
+const resetRelatedPromptsStateWire = wireCommitWithoutPayload('resetRelatedPromptsState');
+
+/**
+ * Fetches and saves the related prompts response.
+ */
+const fetchAndSaveRelatedPromptsResponseWire = wireDispatch('fetchAndSaveRelatedPrompts');
+
+/**
+ * Cancels the fetch and save related prompts response.
+ */
+const cancelFetchAndSaveSearchResponseWire = wireDispatchWithoutPayload(
+ 'cancelFetchAndSaveRelatedPrompts'
+);
+
+/**
+ * Wiring configuration for the {@link RelatedPromptsXModule | related prompts module}.
+ *
+ * @internal
+ */
+export const relatedPromptsWiring = createWiring({
+ ParamsLoadedFromUrl: {
+ setRelatedPromptsQueryFromUrl
+ },
+ UserAcceptedAQuery: {
+ setRelatedPromptsQuery
+ },
+ UserClearedQuery: {
+ cancelFetchAndSaveSearchResponseWire,
+ resetRelatedPromptsStateWire,
+ setRelatedPromptsQuery
+ },
+ RelatedPromptsRequestUpdated: {
+ fetchAndSaveRelatedPromptsResponseWire
+ },
+ ExtraParamsChanged: {
+ setRelatedPromptsExtraParams
+ }
+});
diff --git a/packages/x-components/src/x-modules/related-prompts/x-module.ts b/packages/x-components/src/x-modules/related-prompts/x-module.ts
new file mode 100644
index 0000000000..8c93988817
--- /dev/null
+++ b/packages/x-components/src/x-modules/related-prompts/x-module.ts
@@ -0,0 +1,28 @@
+import { XPlugin } from '../../plugins/x-plugin';
+import { XModule } from '../x-modules.types';
+import { RelatedPromptsXStoreModule } from './store/types';
+import { relatedPromptsXStoreModule } from './store/module';
+import { relatedPromptsStoreEmitters } from './store/emitters';
+import { relatedPromptsWiring } from './wiring';
+
+/**
+ * RelatedPrompts {@link XModule} alias.
+ *
+ * @public
+ */
+export type RelatedPromptsXModule = XModule;
+
+/**
+ * Related Prompts {@link XModule} implementation. This module is auto-registered as soon as you
+ * import any component from the `related-prompts` entry point.
+ *
+ * @public
+ */
+export const relatedPromptsXModule: RelatedPromptsXModule = {
+ name: 'relatedPrompts',
+ storeModule: relatedPromptsXStoreModule,
+ storeEmitters: relatedPromptsStoreEmitters,
+ wiring: relatedPromptsWiring
+};
+
+XPlugin.registerXModule(relatedPromptsXModule);
diff --git a/packages/x-components/src/x-modules/x-modules.types.ts b/packages/x-components/src/x-modules/x-modules.types.ts
index 858ec432e5..c9f97a7a1d 100644
--- a/packages/x-components/src/x-modules/x-modules.types.ts
+++ b/packages/x-components/src/x-modules/x-modules.types.ts
@@ -20,6 +20,7 @@ import { SemanticQueriesXModule } from './semantic-queries/x-module';
import { TaggingXModule } from './tagging';
import { UrlXModule } from './url';
import { ExperienceControlsXModule } from './experience-controls/x-module';
+import { RelatedPromptsXModule } from './related-prompts/x-module';
/**
* Gives each {@link XModule} a name, that can be used to retrieve then its value.
@@ -38,6 +39,7 @@ export interface XModulesTree {
queriesPreview: QueriesPreviewXModule;
querySuggestions: QuerySuggestionsXModule;
recommendations: RecommendationsXModule;
+ relatedPrompts: RelatedPromptsXModule;
relatedTags: RelatedTagsXModule;
scroll: ScrollXModule;
search: SearchXModule;