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 22, 2024
1 parent 0c59fc9 commit c364d7b
Show file tree
Hide file tree
Showing 22 changed files with 715 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/x-components/src/__stubs__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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}`);
}
6 changes: 5 additions & 1 deletion packages/x-components/src/adapter/mocked-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
createSimpleFacetStub,
getFacetsStub,
getRelatedTagsStub,
getResultsStub
getResultsStub,
getRelatedPromptsStub
} from '../__stubs__/index';

export const mockedApiUrl = 'https://api.empathy.co';
Expand Down Expand Up @@ -198,6 +199,9 @@ export const mockedResponses = {
},
'related-tags': {
relatedTags: getRelatedTagsStub()
},
'related-prompts': {
relatedPrompts: getRelatedPromptsStub()
}
};

Expand Down
3 changes: 2 additions & 1 deletion packages/x-components/src/components/base-grid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
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,80 @@
<template>
<div
class="x-related-prompt x-bg-neutral-10 x-p-24 x-flex x-flex-col x-gap-16"
data-test="related-prompt"
>
<div class="x-related-prompt__info x-flex x-flex-col x-gap-16">
<slot name="header" :suggestionText="relatedPrompt.suggestionText">
{{ relatedPrompt.suggestionText }}
</slot>
<slot name="next-queries" :nextQueries="relatedPrompt.nextQueries">
<SlidingPanel :resetOnContentChange="false">
<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>
</slot>
</div>

<div class="x-related-prompt__query-preview">
<slot name="selected-query" :selectedQuery="selectedNextQuery">
{{ selectedNextQuery }}
</slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { RelatedPrompt } from '@empathyco/x-types';
import { relatedPromptsXModule } from '../x-module';
import CrossTinyIcon from '../../../components/icons/cross-tiny.vue';
import PlusIcon from '../../../components/icons/plus.vue';
import SlidingPanel from '../../../components/sliding-panel.vue';
export default defineComponent({
name: 'RelatedPrompt',
components: {
SlidingPanel,
CrossTinyIcon,
PlusIcon
},
xModule: relatedPromptsXModule.name,
props: {
relatedPrompt: { type: Object as PropType<RelatedPrompt>, required: true }
},
setup(props) {
const selectedNextQuery = ref(props.relatedPrompt.nextQueries[0]);
/**
* 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 { 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,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;
}
Loading

0 comments on commit c364d7b

Please sign in to comment.