Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 6 additions & 1 deletion packages/runtime/src/experiences/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
Widget,
} from 'instantsearch.js/es/types';
import type { ChatWidget } from 'instantsearch.js/es/widgets/chat/chat';
import type { SearchBoxWidget } from 'instantsearch.js/es/widgets/search-box/search-box';

export type Environment = 'prod' | 'beta';

Expand Down Expand Up @@ -44,5 +45,9 @@ export type ExperienceWidget = Widget & {
$$supportedWidgets: {
'ais.chat': SupportedWidget<Parameters<ChatWidget>[0]>;
'ais.autocomplete': SupportedWidget;
} & Record<'ais.chat' | 'ais.autocomplete' | (string & {}), SupportedWidget>;
'ais.searchBox': SupportedWidget<Parameters<SearchBoxWidget>[0]>;
} & Record<
'ais.chat' | 'ais.autocomplete' | 'ais.searchBox' | (string & {}),
SupportedWidget
>;
};
9 changes: 9 additions & 0 deletions packages/runtime/src/experiences/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { getExperience } from './get-experience';
import chat from 'instantsearch.js/es/widgets/chat/chat';
import { EXPERIMENTAL_autocomplete } from 'instantsearch.js/es/widgets/autocomplete/autocomplete';
import searchBox from 'instantsearch.js/es/widgets/search-box/search-box';

import { renderTemplate, renderTool } from './renderer';
import type { ExperienceWidget } from './types';
Expand Down Expand Up @@ -127,6 +128,14 @@ export default (function experience(widgetParams: ExperienceWidgetParams) {
});
},
},
// TODO: Add support for `templates` (submit, reset, loadingIndicator)
// TODO: Add support for `queryHook` (bucket 3 — function)
'ais.searchBox': {
widget: searchBox,
async transformParams(params) {
return params;
},
},
},
render: () => {},
dispose: () => {},
Expand Down
108 changes: 105 additions & 3 deletions packages/toolbar/__tests__/ai-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('describeWidgetTypes', () => {
expect(result).toContain('Autocomplete');
expect(result).toContain('ais.chat');
expect(result).toContain('Chat');
expect(result).toContain('ais.searchBox');
});

it('includes widget descriptions and parameter descriptions', () => {
Expand All @@ -51,7 +52,6 @@ describe('describeWidgetTypes', () => {
it('excludes disabled widget types', () => {
const result = describeWidgetTypes();
expect(result).not.toContain('ais.hits');
expect(result).not.toContain('ais.searchBox');
expect(result).not.toContain('ais.pagination');
});
});
Expand Down Expand Up @@ -380,7 +380,11 @@ describe('getTools', () => {
type: 'ais.autocomplete',
container: '#search',
placement: 'before',
parameters: { container: '#other', placement: 'after', showRecent: true },
parameters: {
container: '#other',
placement: 'after',
showRecent: true,
},
},
{ toolCallId: 'tc1', messages: [] }
);
Expand Down Expand Up @@ -429,6 +433,38 @@ describe('getTools', () => {
expect(callbacks.onAddBlock).toHaveBeenCalledWith('ais.autocomplete');
});

it('adds searchBox with default inside placement and container', async () => {
const experience: ExperienceApiResponse = { blocks: [] };
const callbacks = createCallbacks(experience);
const tools = getTools(callbacks);

const result = await tools.add_widget.execute!(
{ type: 'ais.searchBox', container: '#search-box' },
{ toolCallId: 'tc1', messages: [] }
);

expect(callbacks.onAddBlock).toHaveBeenCalledWith('ais.searchBox');
expect(callbacks.onParameterChange).toHaveBeenCalledWith(
0,
'placement',
'inside'
);
expect(callbacks.onParameterChange).toHaveBeenCalledWith(
0,
'container',
'#search-box'
);
expect(result).toMatchObject({
success: true,
index: 0,
type: 'ais.searchBox',
placement: 'inside',
container: '#search-box',
applied: ['placement', 'container'],
rejected: [],
});
});

it('computes the correct index for non-empty experiences', async () => {
const experience: ExperienceApiResponse = {
blocks: [
Expand Down Expand Up @@ -656,7 +692,10 @@ describe('getTools', () => {

expect(result).toMatchObject({
success: true,
applied: ['cssVariables.primary-color-rgb', 'cssVariables.secondary-color'],
applied: [
'cssVariables.primary-color-rgb',
'cssVariables.secondary-color',
],
});
expect(callbacks.onCssVariableChange).toHaveBeenCalledTimes(2);
});
Expand Down Expand Up @@ -757,6 +796,69 @@ describe('getTools', () => {
error: expect.stringContaining('indices 0–1'),
});
});

it('applies a boolean parameter with switch override on searchBox', async () => {
const experience: ExperienceApiResponse = {
blocks: [
{
type: 'ais.searchBox',
parameters: { container: '#search-box', searchAsYouType: true },
},
],
};
const callbacks = createCallbacks(experience);
const tools = getTools(callbacks);

const result = await tools.edit_widget.execute!(
{ index: 0, parameters: { searchAsYouType: false } },
{ toolCallId: 'tc1', messages: [] }
);

expect(result).toMatchObject({
success: true,
applied: ['searchAsYouType'],
rejected: [],
});
expect(callbacks.onParameterChange).toHaveBeenCalledWith(
0,
'searchAsYouType',
false
);
});

it('applies an object parameter (cssClasses) on searchBox', async () => {
const experience: ExperienceApiResponse = {
blocks: [
{
type: 'ais.searchBox',
parameters: { container: '#search-box', cssClasses: false },
},
],
};
const callbacks = createCallbacks(experience);
const tools = getTools(callbacks);

const result = await tools.edit_widget.execute!(
{
index: 0,
parameters: {
cssClasses: { root: 'my-root', input: 'my-input' },
},
},
{ toolCallId: 'tc1', messages: [] }
);

expect(result).toMatchObject({
success: true,
applied: ['cssClasses'],
rejected: [],
});
expect(callbacks.onParameterChange).toHaveBeenCalledWith(
0,
'cssClasses',
{ root: 'my-root', input: 'my-input' }
);
});
});

describe('remove_widget', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/toolbar/src/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@
background: none;
padding: 0;
}
ul, ol {
ul,
ol {
margin: 0.25em 0;
padding-left: 1.25em;
}
Expand Down
79 changes: 78 additions & 1 deletion packages/toolbar/src/widget-types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,10 +292,87 @@ export const WIDGET_TYPES: Record<string, WidgetTypeConfig> = {
},
'ais.searchBox': {
label: 'Search Box',
enabled: false,
enabled: true,
description: 'A search input with submit, reset, and loading indicators.',
icon: SEARCH_ICON,
defaultParameters: {
container: '',
placeholder: '',
autofocus: false,
showLoadingIndicator: true,
showSubmit: true,
showReset: true,
searchAsYouType: true,
ignoreCompositionEvents: false,
cssClasses: false,
},
fieldOrder: [
'container',
'placement',
'placeholder',
'autofocus',
'searchAsYouType',
'ignoreCompositionEvents',
'showLoadingIndicator',
'showSubmit',
'showReset',
'cssClasses',
],
fieldOverrides: {
autofocus: { type: 'switch', label: 'Autofocus' },
searchAsYouType: { type: 'switch', label: 'Search as you type' },
ignoreCompositionEvents: {
type: 'switch',
label: 'Ignore composition events',
},
showLoadingIndicator: { type: 'switch', label: 'Show loading indicator' },
showSubmit: { type: 'switch', label: 'Show submit button' },
showReset: { type: 'switch', label: 'Show reset button' },
cssClasses: {
type: 'object',
label: 'CSS classes',
defaultValue: {
root: '',
form: '',
input: '',
submit: '',
submitIcon: '',
reset: '',
resetIcon: '',
loadingIndicator: '',
loadingIcon: '',
},
fields: [
{ key: 'root', label: 'Root' },
{ key: 'form', label: 'Form' },
{ key: 'input', label: 'Input' },
{ key: 'submit', label: 'Submit' },
{ key: 'submitIcon', label: 'Submit Icon' },
{ key: 'reset', label: 'Reset' },
{ key: 'resetIcon', label: 'Reset Icon' },
{ key: 'loadingIndicator', label: 'Loading Indicator' },
{ key: 'loadingIcon', label: 'Loading Icon' },
],
},
},
paramLabels: {
container: 'Container',
placeholder: 'Placeholder',
},
paramDescriptions: {
container:
'CSS selector for the DOM element to render into (e.g. "#search-box").',
placeholder: 'Placeholder text shown in the search input.',
autofocus: 'Whether the input should be focused on page load.',
searchAsYouType:
'When enabled, triggers a search on each keystroke. When disabled, searches only on submit.',
ignoreCompositionEvents:
'When enabled, ignores IME composition events for CJK input.',
showLoadingIndicator:
'Whether to show a loading indicator while results are being fetched.',
showSubmit: 'Whether to show the submit button.',
showReset: 'Whether to show the reset button.',
cssClasses: 'CSS classes to apply to the widget DOM elements.',
},
},
'ais.refinementList': {
Expand Down