Skip to content

Commit e73043a

Browse files
authored
feat: custom dropdown component (#386)
* feat: custom dropdown * fix: resolve dropdown got hidden by other components * fix: update size and position of the check * fix: dont uncheck the item if it's already selected when click it * feat: add event handlers for dropdown list component * feat: handle icon color * feat: add messageId and tabId when dispatch event + refactor code * feat: add docs for dropdown-list component * feat: add new link component in dropdown list and update colors to be dynamic * feat: add images for this component * fix: revert sample-data welcome page; add this to samples of card-with-headers * feat: add e2e tests for the component * fix: removed hardcoded styles and console.log in * fix: remove title of the dropdown * fix: behavior of dropdownlist when click option, move option to the top * fix: refactor dropdown, make it more generic * fix: update documentation * fix: update new UI * fix: remove title and icon * fix: remove hardcoded styling; fix dropdown go away when a tooltip is shown * fix: test errors * fix: remove extra spaces * feat: add destination to Link Click event in Dropdown * fix: fixing failing example in sample-data * fix: update spacing to align with mock * fix: hover color of option; alignment of arrow in summary card; clean up * fix: update border of the button label in dropdown to match other component * fix: comments * fix: update padding for button in dropdown to match header's button * chore: update snapshots to match the latests UI * fix: update button to be secondary instead of primary button, update images in docs * fix: add ChatItemType to test suite instead of any
1 parent b5ddf36 commit e73043a

File tree

36 files changed

+1289
-8
lines changed

36 files changed

+1289
-8
lines changed

docs/DATAMODEL.md

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,7 @@ interface ChatItemContent {
13701370
content: ChatItemContent;
13711371
}> | null;
13721372
codeBlockActions?: CodeBlockActions | null;
1373+
quickSettings?: DropdownFactoryProps | null;
13731374
fullWidth?: boolean;
13741375
padding?: boolean;
13751376
wrapCodes?: boolean;
@@ -2315,6 +2316,105 @@ mynahUI.addChatItem(tabId, {
23152316

23162317
---
23172318

2319+
## `quickSettings`
2320+
This parameter allows you to add a dropdown selector to the footer of a chat item card. The `quickSettings` component provides a flexible dropdown interface that can be extended for different form item types in the future (radio buttons, checkboxes, etc.).
2321+
2322+
Currently, it supports the `select` type for single-selection dropdowns with visual feedback and customizable options.
2323+
2324+
The `DropdownList` component provides a customizable dropdown selector that allows users to choose from a list of options. It supports single selection with visual feedback and can be positioned relative to its parent elements.
2325+
2326+
```typescript
2327+
interface DropdownListOption {
2328+
id: string; // Unique identifier for the option
2329+
label: string; // Display text for the option
2330+
value: string; // Value associated with the option
2331+
selected?: boolean; // Whether the option is initially selected
2332+
}
2333+
2334+
interface DropdownFactoryProps {
2335+
type: 'select'; // Type of dropdown (currently only 'select', extensible for 'radio', 'checkbox', etc.)
2336+
title: string; // The title displayed in the dropdown header
2337+
titleIcon?: MynahIcons; // Icon displayed next to the title
2338+
description?: string; // Description text displayed below the title
2339+
descriptionLink?: { // Optional clickable link that appears within the description text
2340+
id: string; // Unique identifier for the link
2341+
text: string; // Display text for the link
2342+
destination: string; // Link destination
2343+
onClick?: () => void; // Optional callback function triggered when the link is clicked
2344+
};
2345+
options: DropdownListOption[]; // Array of options to display in the dropdown
2346+
onChange?: (selectedOptions: DropdownListOption[]) => void; // Callback when selection changes
2347+
tabId?: string; // Tab identifier for event dispatching
2348+
messageId?: string; // Message identifier for event dispatching
2349+
classNames?: string[]; // Additional CSS class names to apply
2350+
}
2351+
```
2352+
2353+
When a dropdown option is selected, the component dispatches a `MynahEventNames.DROPDOWN_OPTION_CHANGE` event with the selected options. You can handle this event by implementing the [`onDropDownOptionChange`](./PROPERTIES.md#ondropdownoptionchange) callback in your MynahUI constructor properties.
2354+
2355+
When a link in the dropdown description is clicked, the component dispatches a `MynahEventNames.DROP_DOWN_LINK_CLICK` event. You can handle this event by implementing the [`onDropDownLinkClick`](./PROPERTIES.md#ondropdownlinkclick) callback in your MynahUI constructor properties.
2356+
2357+
```typescript
2358+
const mynahUI = new MynahUI({
2359+
tabs: {
2360+
'tab-1': {
2361+
...
2362+
}
2363+
}
2364+
});
2365+
2366+
mynahUI.addChatItem('tab-1', {
2367+
type: ChatItemType.ANSWER,
2368+
messageId: 'dropdown-example',
2369+
body: 'Please select your preferred option:',
2370+
quickSettings: {
2371+
type: 'select',
2372+
title: 'Select an option',
2373+
description: 'Choose one of the following options',
2374+
tabId: 'tab-1',
2375+
messageId: 'dropdown-example',
2376+
options: [
2377+
{ id: 'option1', label: 'Option 1', value: 'option1', selected: false },
2378+
{ id: 'option2', label: 'Option 2', value: 'option2', selected: true },
2379+
{ id: 'option3', label: 'Option 3', value: 'option3', selected: false }
2380+
]
2381+
}
2382+
});
2383+
2384+
// Example with descriptionLink
2385+
mynahUI.addChatItem('tab-1', {
2386+
type: ChatItemType.ANSWER,
2387+
messageId: 'dropdown-with-link-example',
2388+
body: 'Configure your settings:',
2389+
quickSettings: {
2390+
type: 'select',
2391+
title: 'Model Selection',
2392+
description: 'Choose your preferred AI model. Need help choosing?',
2393+
descriptionLink: {
2394+
id: 'model-help-link',
2395+
text: 'Learn more about models',
2396+
onClick: () => {
2397+
console.log('Help link clicked - opening model documentation');
2398+
// Handle the link click, e.g., open documentation or show help dialog
2399+
}
2400+
},
2401+
tabId: 'tab-1',
2402+
messageId: 'dropdown-with-link-example',
2403+
options: [
2404+
{ id: 'gpt4', label: 'GPT-4', value: 'gpt4', selected: true },
2405+
{ id: 'claude', label: 'Claude', value: 'claude', selected: false },
2406+
{ id: 'llama', label: 'Llama', value: 'llama', selected: false }
2407+
]
2408+
}
2409+
});
2410+
```
2411+
2412+
<p align="center">
2413+
<img src="./img/data-model/chatItems/dropdown-list.png" alt="QuickSettings dropdown" style="max-width:300px; width:100%;border: 1px solid #e0e0e0;">
2414+
</p>
2415+
2416+
---
2417+
23182418
## `footer`
23192419
With this parameter, you can add another `ChatItem` only with contents to the footer of a ChatItem.
23202420
@@ -3660,7 +3760,6 @@ export interface DetailedListItem {
36603760
children?: DetailedListItemGroup[];
36613761
keywords?: string[];
36623762
}
3663-
}
36643763
```
36653764
36663765
---
@@ -3698,4 +3797,4 @@ mynahUI.updateStore('tab-1', {
36983797
36993798
The header will appear above the quick action commands list and provides information to users about new features.
37003799
3701-
---
3800+
---

docs/PROPERTIES.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,53 @@ onFormChange?: (
989989
...
990990
```
991991

992+
---
993+
994+
### `onDropDownOptionChange`
995+
996+
This event will be fired when a user selects an option from a dropdown list. It passes the `tabId`, `messageId`, and the selected options.
997+
998+
```typescript
999+
...
1000+
onDropDownOptionChange?: (
1001+
tabId: string,
1002+
messageId: string,
1003+
selectedOptions: DropdownListOption[]): void => {
1004+
console.log(`Dropdown selection changed in tab: ${tabId}`);
1005+
console.log(`From message: ${messageId}`);
1006+
console.log(`Selected option: ${selectedOptions[0].label}`);
1007+
};
1008+
...
1009+
```
1010+
1011+
<p align="center">
1012+
<img src="./img/onDropDownOptionChange.png" alt="Dropdown List" style="max-width:300px; width:100%;border: 1px solid #e0e0e0;">
1013+
</p>
1014+
1015+
---
1016+
1017+
### `onDropDownLinkClick`
1018+
1019+
This event will be fired when a user clicks on a link within a dropdown list's description. It passes the `tabId` and the `actionId` of the clicked link.
1020+
1021+
```typescript
1022+
...
1023+
onDropDownLinkClick?: (
1024+
tabId: string,
1025+
destination: string,
1026+
actionId: string): void => {
1027+
console.log(`Dropdown link clicked in tab: ${tabId}`);
1028+
console.log(`Link action ID: ${actionId}`);
1029+
console.log(`Destination ID: ${destination}`);
1030+
};
1031+
...
1032+
```
1033+
1034+
<p align="center">
1035+
<img src="./img/onDropDownLinkClick.png" alt="Dropdown List" style="max-width:300px; width:100%;border: 1px solid #e0e0e0;">
1036+
</p>
1037+
1038+
9921039
---
9931040

9941041
### `onCustomFormAction`
41.4 KB
Loading

docs/img/onDropDownLinkClick.png

84.8 KB
Loading
85.1 KB
Loading

example/src/main.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
TreeNodeDetails,
1717
QuickActionCommand,
1818
ChatItemButton,
19-
CustomQuickActionCommand
19+
CustomQuickActionCommand,
20+
DropdownListOption
2021
} from '@aws/mynah-ui';
2122
import { mcpButton, mynahUIDefaults, promptTopBarTitle, rulesButton, tabbarButtons } from './config';
2223
import { Log, LogClear } from './logger';
@@ -144,6 +145,12 @@ Model - ${optionsValues['model-select'] !== '' ? optionsValues['model-select'] :
144145
});
145146
}
146147
},
148+
onDropDownOptionChange: (tabId: string, messageId: string, value: DropdownListOption []) => {
149+
Log(`Dropdown Option changed in message ${messageId} on tab ${tabId}`)
150+
},
151+
onDropDownLinkClick: (tabId, actionId, destination) => {
152+
Log(`Dropdown link click with id ${tabId}, ${actionId}, ${destination}`)
153+
},
147154
onPromptInputButtonClick: (tabId, buttonId) => {
148155
Log(`Prompt input button ${buttonId} clicked on tab <b>${tabId}</b>`);
149156
},

example/src/samples/sample-data.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,7 +1805,58 @@ mkdir -p src/ lalalaaaa
18051805
`,
18061806
codeBlockActions: { copy: null, 'insert-to-cursor': null },
18071807
},
1808-
1808+
{
1809+
fullWidth: true,
1810+
padding: false,
1811+
type: ChatItemType.ANSWER,
1812+
header: {
1813+
body: 'Shell',
1814+
status: {
1815+
position: 'left',
1816+
icon: MynahIcons.WARNING,
1817+
status: 'warning',
1818+
description: 'This command may cause\nsignificant data loss or damage.',
1819+
},
1820+
buttons: [
1821+
{
1822+
status: 'clear',
1823+
icon: 'play',
1824+
text: 'Run',
1825+
id: 'run-bash-command',
1826+
},
1827+
{
1828+
status: 'dimmed-clear',
1829+
icon: 'cancel',
1830+
text: 'Reject',
1831+
id: 'reject-bash-command',
1832+
},
1833+
],
1834+
},
1835+
body: `
1836+
\`\`\`bash
1837+
mkdir -p src/ lalalaaaa
1838+
\`\`\`
1839+
`,
1840+
quickSettings: {
1841+
type: "select",
1842+
messageId: "1",
1843+
tabId: "hello",
1844+
description: '',
1845+
descriptionLink: {
1846+
id: "button-id",
1847+
destination: "Built-in",
1848+
text: 'More control, modify the commands',
1849+
},
1850+
options: [
1851+
{ id: 'option1', label: 'Ask to Run', selected: true, value: 'Destructive' },
1852+
{ id: 'option2', label: 'Auto run', value: 'Destructive' },
1853+
],
1854+
onChange: (selectedOptions: any) => {
1855+
console.log('Selected options:', selectedOptions);
1856+
}
1857+
},
1858+
codeBlockActions: { copy: null, 'insert-to-cursor': null },
1859+
},
18091860
{
18101861
fullWidth: true,
18111862
padding: false,
@@ -2174,7 +2225,39 @@ export const mcpToolRunSampleCard:ChatItem = // Summary Card
21742225
header: {
21752226
icon: MynahIcons.TOOLS,
21762227
body: 'Ran Filesystem tool search-files',
2177-
fileList: null
2228+
fileList: null,
2229+
buttons: [
2230+
{
2231+
status: 'clear',
2232+
icon: 'play',
2233+
text: 'Run',
2234+
id: 'run-bash-command',
2235+
},
2236+
{
2237+
status: 'dimmed-clear',
2238+
icon: 'cancel',
2239+
text: 'Reject',
2240+
id: 'reject-bash-command',
2241+
},
2242+
],
2243+
},
2244+
quickSettings: {
2245+
type: "select",
2246+
messageId: "1",
2247+
tabId: "hello",
2248+
description: '',
2249+
descriptionLink: {
2250+
id: "button-id",
2251+
destination: "Built-in",
2252+
text: 'Auto-approve settings',
2253+
},
2254+
options: [
2255+
{ id: 'option1', label: 'Ask to Run', selected: true, value: 'Destructive' },
2256+
{ id: 'option2', label: 'Auto run', value: 'Destructive' },
2257+
],
2258+
onChange: (selectedOptions: any) => {
2259+
console.log('Selected options:', selectedOptions);
2260+
}
21782261
},
21792262
},
21802263
collapsedContent: [

src/components/chat-item/chat-item-card.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Button } from '../button';
2828
import { Overlay, OverlayHorizontalDirection, OverlayVerticalDirection } from '../overlay';
2929
import { marked } from 'marked';
3030
import { parseMarkdown } from '../../helper/marked';
31+
import { DropdownWrapper } from '../dropdown-form/dropdown-wrapper';
3132

3233
const TOOLTIP_DELAY = 350;
3334
export interface ChatItemCardProps {
@@ -193,7 +194,7 @@ export class ChatItemCard {
193194
...(this.canShowAvatar() && MynahUITabsStore.getInstance().getTabDataStore(this.props.tabId).getValue('showChatAvatars') === true ? [ this.chatAvatar ] : []),
194195
...(this.card != null ? [ this.card?.render ] : []),
195196
...(this.chatButtonsOutside != null ? [ this.chatButtonsOutside?.render ] : []),
196-
...(this.props.chatItem.followUp?.text !== undefined ? [ new ChatItemFollowUpContainer({ tabId: this.props.tabId, chatItem: this.props.chatItem }).render ] : [])
197+
...(this.props.chatItem.followUp?.text !== undefined ? [ new ChatItemFollowUpContainer({ tabId: this.props.tabId, chatItem: this.props.chatItem }).render ] : []),
197198
],
198199
});
199200

@@ -863,7 +864,8 @@ export class ChatItemCard {
863864
this.cardFooter.remove();
864865
this.cardFooter = null;
865866
}
866-
if (this.props.chatItem.footer != null || this.props.chatItem.canBeVoted === true) {
867+
868+
if (this.props.chatItem.footer != null || this.props.chatItem.canBeVoted === true || this.shouldShowQuickSettings()) {
867869
this.cardFooter = this.getCardFooter();
868870
this.card?.render.insertChild('beforeend', this.cardFooter);
869871

@@ -902,6 +904,22 @@ export class ChatItemCard {
902904
});
903905
this.cardFooter.insertChild('beforeend', this.votes.render);
904906
}
907+
908+
/**
909+
* Add QuickSettings to footer if available
910+
*/
911+
if (this.props.chatItem.quickSettings != null) {
912+
const dropdownContainer = DomBuilder.getInstance().build({
913+
type: 'div',
914+
classNames: [ 'mynah-dropdown-list-container' ],
915+
children: [
916+
new DropdownWrapper({
917+
dropdownProps: this.props.chatItem.quickSettings
918+
}).render
919+
]
920+
});
921+
this.cardFooter.insertChild('beforeend', dropdownContainer);
922+
}
905923
}
906924

907925
/**
@@ -926,6 +944,10 @@ export class ChatItemCard {
926944

927945
private readonly canShowAvatar = (): boolean => (this.props.chatItem.type === ChatItemType.ANSWER_STREAM || (this.props.inline !== true && chatItemHasContent({ ...this.props.chatItem, followUp: undefined })));
928946

947+
private readonly shouldShowQuickSettings = (): boolean => {
948+
return this.props.chatItem.quickSettings != null;
949+
};
950+
929951
private readonly showTooltip = (content: string, elm: HTMLElement): void => {
930952
if (content.trim() !== undefined) {
931953
clearTimeout(this.tooltipTimeout);

0 commit comments

Comments
 (0)