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/wiring/events.types.ts b/packages/x-components/src/wiring/events.types.ts index fbca9c3efb..835e7f679d 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 | UrlXEvents} * * @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..7b97960c7a --- /dev/null +++ b/packages/x-components/src/x-modules/related-prompts/components/related-prompt.vue @@ -0,0 +1,80 @@ + + 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..e8eb1fbf01 --- /dev/null +++ b/packages/x-components/src/x-modules/related-prompts/components/related-prompts-list.vue @@ -0,0 +1,185 @@ + 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/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;