Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bundlesize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
"maxSize": "121 kB"
"maxSize": "121.25 kB"
},
{
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
"maxSize": "250 kB"
"maxSize": "250.5 kB"
},
{
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
"maxSize": "59.5 kB"
},
{
"path": "packages/react-instantsearch/dist/umd/ReactInstantSearch.min.js",
"maxSize": "93.75 kB"
"maxSize": "99 kB"
},
{
"path": "packages/vue-instantsearch/vue2/umd/index.js",
Expand Down Expand Up @@ -78,7 +78,7 @@
},
{
"path": "./packages/instantsearch.css/components/autocomplete-min.css",
"maxSize": "3.75 kB"
"maxSize": "4 kB"
}
]
}
1 change: 1 addition & 0 deletions examples/js/query-suggestions/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ <h2>Filters</h2>
</main>
<div id="chat"></div>
</div>
<div id="chat"></div>

<button class="filters-button" data-action="open-overlay">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 14">
Expand Down
19 changes: 17 additions & 2 deletions examples/js/query-suggestions/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';
import {
chat,
clearRefinements,
configure,
EXPERIMENTAL_autocomplete,
chat,
hits,
pagination,
panel,
Expand Down Expand Up @@ -69,15 +69,30 @@ search.addWidgets([
`,
},
},
showSuggestions: {
showQuerySuggestions: {
indexName: 'instant_search_demo_query_suggestions',
searchParameters: {
hitsPerPage: 2,
},
templates: {
header: (_, { html }) => html`
<span class="ais-AutocompleteIndexHeaderTitle">Suggestions</span>
<span class="ais-AutocompleteIndexHeaderLine" />
`,
},
},
showPromptSuggestions: {
indexName: 'instant_search_prompt_suggestions',
searchParameters: {
hitsPerPage: 2,
},
templates: {
header: (_, { html }) => html`
<span class="ais-AutocompleteIndexHeaderTitle">Ask AI</span>
<span class="ais-AutocompleteIndexHeaderLine" />
`,
},
},
}),
hits({
container: '#hits',
Expand Down
16 changes: 15 additions & 1 deletion examples/react/query-suggestions/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function App() {
</>
),
}}
showSuggestions={{
showQuerySuggestions={{
indexName: 'instant_search_demo_query_suggestions',
headerComponent: () => (
<>
Expand All @@ -108,6 +108,20 @@ export function App() {
</>
),
}}
showPromptSuggestions={{
indexName: 'instant_search_prompt_suggestions_poc',
searchParameters: {
hitsPerPage: 2,
},
headerComponent: () => (
<>
<span className="ais-AutocompleteIndexHeaderTitle">
Ask AI
</span>
<span className="ais-AutocompleteIndexHeaderLine" />
</>
),
}}
/>
<Hits hitComponent={HitComponent} />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/** @jsx createElement */

import { cx } from '../../lib';
import { SparklesIcon } from '../chat/icons';

import type { ComponentChildren, Renderer } from '../../types';

export type AutocompletePromptSuggestionProps<
T = { prompt: string; label?: string } & Record<string, unknown>
> = {
item: T;
onSelect: () => void;
children?: ComponentChildren;
classNames?: Partial<AutocompletePromptSuggestionClassNames>;
};

export type AutocompletePromptSuggestionClassNames = {
/**
* Class names to apply to the root element.
*/
root: string | string[];
/**
* Class names to apply to the content element.
*/
content: string | string[];
/**
* Class names to apply to the icon element.
*/
icon: string | string[];
/**
* Class names to apply to the body element.
*/
body: string | string[];
};

export function createAutocompletePromptSuggestionComponent({
createElement,
}: Renderer) {
return function AutocompletePromptSuggestion(
userProps: AutocompletePromptSuggestionProps
) {
const { item, onSelect, children, classNames = {} } = userProps;
const label = item.label || item.prompt;

return (
<div
onClick={onSelect}
className={cx(
'ais-AutocompleteItemWrapper',
'ais-AutocompletePromptSuggestionWrapper',
classNames.root
)}
>
<div
className={cx(
'ais-AutocompleteItemContent',
'ais-AutocompletePromptSuggestionItemContent',
classNames.content
)}
>
<div
className={cx(
'ais-AutocompleteItemIcon',
'ais-AutocompletePromptSuggestionItemIcon',
classNames.icon
)}
>
<SparklesIcon createElement={createElement} />
</div>
<div
className={cx(
'ais-AutocompleteItemContentBody',
'ais-AutocompletePromptSuggestionItemContentBody',
classNames.body
)}
>
{children ?? label}
</div>
</div>
</div>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ export * from './AutocompleteDetachedOverlay';
export * from './AutocompleteDetachedSearchButton';
export * from './AutocompleteIndex';
export * from './AutocompletePanel';
export * from './AutocompletePromptSuggestion';
export * from './AutocompleteRecentSearch';
export * from './AutocompleteSearch';
export * from './AutocompleteSuggestion';
export * from './createAutocompletePropGetters';
export * from './createAutocompleteStorage';
export * from './promptSuggestionUtils';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
export const PROMPT_SUGGESTION_FLAG = '__isPromptSuggestion' as const;
export const PROMPT_SUGGESTION_FALLBACK_FLAG =
'__isPromptSuggestionFallback' as const;

export function getPromptSuggestionHits({
hits,
query,
limit,
}: {
hits: Array<{ objectID: string } & Record<string, unknown>>;
query: string;
limit: number;
}): Array<{ objectID: string } & Record<string, unknown>> {
const promptHits = hits.slice(0, limit).map((hit) => ({
...hit,
[PROMPT_SUGGESTION_FLAG]: true,
}));

if (promptHits.length > 0 || query.trim().length === 0) {
return promptHits;
}

return [
{
objectID: `ask-about:${encodeURIComponent(query)}`,
prompt: query,
label: `Ask about "${query}"`,
[PROMPT_SUGGESTION_FLAG]: true,
[PROMPT_SUGGESTION_FALLBACK_FLAG]: true,
},
];
}

export function isPromptSuggestion(item: unknown): item is {
prompt: string;
[PROMPT_SUGGESTION_FLAG]: true;
} {
return Boolean(
item &&
typeof item === 'object' &&
(item as Record<string, unknown>)[PROMPT_SUGGESTION_FLAG]
);
}

export function isPromptSuggestionFallback(item: unknown): item is {
prompt: string;
label?: string;
[PROMPT_SUGGESTION_FALLBACK_FLAG]: true;
} {
return Boolean(
item &&
typeof item === 'object' &&
(item as Record<string, unknown>)[PROMPT_SUGGESTION_FALLBACK_FLAG]
);
}
9 changes: 9 additions & 0 deletions packages/instantsearch.css/src/components/autocomplete.scss
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,15 @@
}
}

.ais-AutocompletePromptSuggestionItemIcon {
color: rgba(var(--ais-primary-color-rgb), 1);
}

.ais-AutocompletePromptSuggestionItemIcon svg {
width: var(--ais-icon-size);
height: var(--ais-icon-size);
}

// ----------------
// Detached Mode (Mobile)
// ----------------
Expand Down
10 changes: 10 additions & 0 deletions packages/instantsearch.js/src/connectors/chat/connectChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export type ChatRenderState<TUiMessage extends UIMessage = UIMessage> = {
setIndexUiState: IndexWidget['setIndexUiState'];
setInput: (input: string) => void;
setOpen: (open: boolean) => void;
/**
* Opens the chat (if needed) and focuses the prompt input.
*/
focusInput: () => void;
/**
* Updates the `messages` state locally. This is useful when you want to
* edit the messages on the client, and then trigger the `reload` method
Expand Down Expand Up @@ -246,6 +250,7 @@ export default (function connectChat<TWidgetParams extends UnknownWidgetParams>(
let sendEvent: SendEventForHits;
let setInput: ChatRenderState<TUiMessage>['setInput'];
let setOpen: ChatRenderState<TUiMessage>['setOpen'];
let focusInput: ChatRenderState<TUiMessage>['focusInput'];
let setIsClearing: (value: boolean) => void;

const agentId = 'agentId' in options ? options.agentId : undefined;
Expand Down Expand Up @@ -456,6 +461,10 @@ export default (function connectChat<TWidgetParams extends UnknownWidgetParams>(
render();
};

focusInput = () => {
setOpen(true);
};

setInput = (i) => {
input = i;
render();
Expand Down Expand Up @@ -541,6 +550,7 @@ export default (function connectChat<TWidgetParams extends UnknownWidgetParams>(
setIndexUiState: parent.setIndexUiState.bind(parent),
setInput,
setOpen,
focusInput,
setMessages,
suggestions: getSuggestionsFromMessages(_chatInstance.messages),
isClearing,
Expand Down
Loading