Skip to content

Commit

Permalink
feat: create related prompts module
Browse files Browse the repository at this point in the history
  • Loading branch information
victorcg88 committed Oct 21, 2024
1 parent 0c59fc9 commit ebb2621
Show file tree
Hide file tree
Showing 18 changed files with 655 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/x-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion packages/x-components/src/wiring/events.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
/**.
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as RelatedPrompt } from './related-prompt.vue';
export { default as RelatedPromptsList } from './related-prompts-list.vue';
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<div class="x-flex x-flex-col x-gap-16 x-bg-neutral-10 x-p-24 x-pl-16 x-pr-0 desktop:x-px-24">
<div class="x-flex x-flex-col x-gap-16">
<h1 class="x-text1 x-text1-lg x-pr-16 x-font-main x-text-neutral-90 desktop:x-pr-0">
{{ relatedPrompt.suggestionText }}
</h1>
<SlidingPanel>
<div class="x-flex x-gap-8 x-pr-8">
<button
v-for="(nextQuery, index) in relatedPrompt.nextQueries"
:key="index"
@click="onClick(nextQuery)"
class="x-button x-button-lead x-button-sm x-button-outlined x-rounded-sm x-border-lead-50 x-text-neutral-75 hover:x-text-neutral-0 selected:x-text-neutral-0 selected:hover:x-bg-lead-50"
:class="{ 'x-selected': selectedNextQuery === nextQuery }"
>
<span
class="x-whitespace-nowrap"
:class="
selectedNextQuery === nextQuery ? 'x-title3 x-title3-md' : 'x-text1 x-text1-lg'
"
>
{{ nextQuery }}
</span>
<CrossTinyIcon v-if="selectedNextQuery === nextQuery" class="x-icon" />
<PlusIcon v-else class="x-icon" />
</button>
</div>
</SlidingPanel>
</div>
<QueryPreview :queryPreviewInfo="queryPreviewInfo" #default="{ totalResults, results }">
<SlidingPanel>
<template #header>
<QueryPreviewButton
:queryPreviewInfo="queryPreviewInfo"
class="x-button x-button-lead x-button-tight x-title3 x-title3-sm desktop:x-title3-md"
>
{{ queryPreviewInfo.query }}
({{ totalResults }})
<ArrowRightIcon class="x-icon-lg" />
</QueryPreviewButton>
</template>

<div class="x-transform-style-3d x-flex x-gap-16 x-pr-8 x-pt-8 desktop:x-pt-16">
<Result
v-for="result in results"
:key="result.id"
:result="result"
class="x-w-[calc(38vw-16px)] x-min-w-[142px] desktop:x-w-[216px]"
/>
</div>
</SlidingPanel>
</QueryPreview>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, ref, ComputedRef } from 'vue';
import { RelatedPrompt } from '@empathyco/x-types';
import { relatedPromptsXModule } from '../x-module';
import ArrowRightIcon from '../../../components/icons/arrow-right.vue';
import CrossTinyIcon from '../../../components/icons/cross-tiny.vue';
import PlusIcon from '../../../components/icons/plus.vue';
// import { default as Result } from '../../../components/results/result.vue';
import {
QueryPreview,
QueryPreviewButton,
QueryPreviewInfo
} from '../../../x-modules/queries-preview';
import SlidingPanel from '../../../components/sliding-panel.vue';
export default defineComponent({
name: 'RelatedPrompt',
components: {
SlidingPanel,
QueryPreview,
QueryPreviewButton,
// Result,
ArrowRightIcon,
CrossTinyIcon,
PlusIcon
},
xModule: relatedPromptsXModule.name,
props: {
relatedPrompt: { type: Object as PropType<RelatedPrompt>, required: true }
},
setup(props) {
const selectedNextQuery = ref(props.relatedPrompt.nextQueries[0]);
const queryPreviewInfo: ComputedRef<QueryPreviewInfo> = computed(() => ({
query: selectedNextQuery.value
}));
/**
* Handles the click event on a next query button.
*
* @param nextQuery - The clicked next query.
*/
function onClick(nextQuery: string): void {
if (selectedNextQuery.value === nextQuery) {
selectedNextQuery.value = '';
} else {
selectedNextQuery.value = nextQuery;
}
}
return { queryPreviewInfo, selectedNextQuery, onClick };
}
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<script lang="ts">
import { computed, ComputedRef, defineComponent, h, inject, provide, Ref } from 'vue';
import { RelatedPrompt } from '@empathyco/x-types';
import { AnimationProp } from '../../../types/animation-prop';
import { groupItemsBy } from '../../../utils/array';
import ItemsList from '../../../components/items-list.vue';
import { ListItem } from '../../../utils/types';
import {
HAS_MORE_ITEMS_KEY,
LIST_ITEMS_KEY,
QUERY_KEY
} from '../../../components/decorators/injection.consts';
import { relatedPromptsXModule } from '../x-module';
import { useState } from '../../../composables/use-state';
import { RelatedPromptsGroup } from '../types';
/**
* Component that inserts groups of related prompts in different positions of the injected search
* items list, based on the provided configuration.
*
* @public
*/
export default defineComponent({
name: 'RelatedPromptsList',
xModule: relatedPromptsXModule.name,
props: {
/**
* Animation component that will be used to animate the related prompts groups.
*/
animation: {
type: AnimationProp,
default: 'ul'
},
/**
* The first index to insert a group of related prompts at.
*/
offset: {
type: Number,
default: 24
},
/**
* The items cycle size to keep inserting related prompts groups at.
*/
frequency: {
type: Number,
default: 24
},
/**
* The maximum amount of related prompts to add in a single group.
*/
maxNextQueriesPerPrompt: {
type: Number,
default: 4
},
/**
* The maximum number of groups to insert into the injected list items list.
*/
maxGroups: {
type: Number,
default: undefined
},
/**
* Determines if a group is added to the injected items list in case the number
* of items is smaller than the offset.
*/
showOnlyAfterOffset: {
type: Boolean,
default: false
}
},
setup(props, { slots }) {
const { query, status } = useState('relatedPrompts', ['query', 'status']);
/**
* The state related prompts.
*/
const relatedPrompts: ComputedRef<RelatedPrompt[]> = useState('relatedPrompts', [
'relatedPrompts'
]).relatedPrompts;
/**
* Injected query, updated when the related request(s) have succeeded.
*/
const injectedQuery = inject<Ref<string | undefined>>(QUERY_KEY as string);
/**
* Indicates if there are more available results than the injected.
*/
const hasMoreItems = inject<Ref<boolean | undefined>>(HAS_MORE_ITEMS_KEY as string);
/**
* The grouped related prompts based on the given config.
*
* @returns A list of related prompts groups.
*/
const relatedPromptsGroups = computed<RelatedPromptsGroup[]>(() =>
Object.values(
groupItemsBy(relatedPrompts.value, (_, index) =>
Math.floor(index / props.maxNextQueriesPerPrompt)
)
)
.slice(0, props.maxGroups)
.map((relatedPrompts, index) => ({
modelName: 'RelatedPromptsGroup' as const,
id: `related-prompts-group-${index}`,
relatedPrompts
}))
);
/**
* It injects {@link ListItem} provided by an ancestor as injectedListItems.
*/
const injectedListItems = inject<Ref<ListItem[]>>(LIST_ITEMS_KEY as string);
/**
* Checks if the related prompts are outdated taking into account the injected query.
*
* @returns True if the related prompts are outdated, false if not.
*/
const relatedPromptsAreOutdated = computed(
() =>
!!injectedQuery?.value &&
(query.value !== injectedQuery.value || status.value !== 'success')
);
/**
* Checks if the number of items is smaller than the offset so a group
* should be added to the injected items list.
*
* @returns True if a group should be added, false if not.
*/
const hasNotEnoughListItems = computed(
() =>
!props.showOnlyAfterOffset &&
!hasMoreItems?.value &&
injectedListItems !== undefined &&
injectedListItems.value.length > 0 &&
props.offset > injectedListItems.value.length
);
/**
* New list of {@link ListItem}s to render.
*
* @returns The new list of {@link ListItem}s with the related prompts groups inserted.
*/
const items = computed((): ListItem[] => {
if (!injectedListItems?.value) {
return relatedPromptsGroups.value;
}
if (relatedPromptsAreOutdated.value) {
return injectedListItems.value;
}
if (hasNotEnoughListItems.value) {
return injectedListItems.value.concat(relatedPromptsGroups.value[0] ?? []);
}
return relatedPromptsGroups?.value.reduce(
(items, relatedPromptsGroup, index) => {
const targetIndex = props.offset + props.frequency * index;
if (targetIndex <= items.length) {
items.splice(targetIndex, 0, relatedPromptsGroup);
}
return items;
},
[...injectedListItems.value]
);
});
/**
* The computed list items of the entity that uses the mixin.
*
* @remarks It should be overridden in the component that uses the mixin and
* it's intended to be filled with items from the state. Vue doesn't allow
* mixins as abstract classes.
* @returns An empty array as fallback in case it is not overridden.
*/
provide(LIST_ITEMS_KEY as string, items);
return () => {
const innerProps = { items: items.value, animation: props.animation };
// https://vue-land.github.io/faq/forwarding-slots#passing-all-slots
return slots.default?.(innerProps)[0] ?? h(ItemsList, innerProps, slots);
};
}
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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.
*/
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;
}
5 changes: 5 additions & 0 deletions packages/x-components/src/x-modules/related-prompts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './components';
export * from './events.types';
export * from './store';
export * from './wiring';
export * from './x-module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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.
*/
export const fetchAndSaveRelatedPrompts = fetchAndSave;

/**
* Default implementation for
* {@link RelatedPromptsActions.cancelFetchAndSaveRelatedPrompts} action.
*/
export const cancelFetchAndSaveRelatedPrompts = cancelPrevious;
Loading

0 comments on commit ebb2621

Please sign in to comment.