Skip to content

feat: add top bar component #336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 16, 2025
Merged
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
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ LICENSE
THIRD-PARTY-LICENSES
NOTICE
Dockerfile
e2e-results
e2e-results
coverage/
86 changes: 86 additions & 0 deletions docs/DATAMODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,92 @@ mynahUI.updateStore('tab-1', {
<img src="./img/data-model/tabStore/promptInputPlaceholder.png" alt="mainTitle" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
</p>

---
### `promptTopBarTitle` (default: `''`)

This is the title displayed in the prompt top bar. When set, it enables a top bar that can be used for pinned context items.

```typescript
const mynahUI = new MynahUI({
tabs: {
'tab-1': {
...
}
}
});

mynahUI.updateStore('tab-1', {
promptTopBarTitle: '@Pin Context'
})
```

<p align="center">
<img src="./img/data-model/tabStore/promptTopBarTitle.png" alt="prompt top bar title" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
</p>

---

### `promptTopBarContextItems` (default: `[]`)

These are the context items pinned to the prompt top bar. They appear as pills that can be removed by the user. Top bar only appears when `promptTopBarTitle` is not empty.

```typescript
const mynahUI = new MynahUI({
tabs: {
'tab-1': {
...
}
}
});

mynahUI.updateStore('tab-1', {
promptTopBarContextItems: [
{
command: 'ex-dom.ts',
icon: MynahIcons.FILE,
description: '.src/helper'
},
{
command: 'main',
icon: MynahIcons.FOLDER,
description: '.src/'
}
]
})
```

<p align="center">
<img src="./img/data-model/tabStore/promptTopBarContextItems.png" alt="prompt top bar context items" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
</p>

---

### `promptTopBarButton` (default: `null`)

This is a button displayed at the end of the prompt top bar. Clicking on the button will call onPromptTopBarButtonClick(). Button only appears when `promptTopBarTitle` is not empty.

```typescript
const mynahUI = new MynahUI({
tabs: {
'tab-1': {
...
}
}
});

mynahUI.updateStore('tab-1', {
promptTopBarButton: {
id: 'project-rules',
icon: MynahIcons.CHECK_LIST,
text: 'Rules'
}
})
```

<p align="center">
<img src="./img/data-model/tabStore/promptTopBarButton.png" alt="prompt top bar button" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
</p>

---

### `promptInputText` (default: `''`)
Expand Down
71 changes: 68 additions & 3 deletions docs/PROPERTIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ export interface MynahUIProps {
onSplashLoaderActionClick?: (
action: Action,
eventId?: string) => void;
onPromptTopBarItemAdded?: (
tabId: string,
topBarItem: ChatItemButton,
eventId?: string) => void;
onPromptTopBarItemRemoved?: (
tabId: string,
topBarItemId: string,
eventId?: string) => void;
onPromptTopBarButtonClick?: (
tabId: string,
topBarButton: ChatItemButton,
eventId?: string) => void;
}
```
_Let's deep dive into each property you can set._
Expand Down Expand Up @@ -1158,13 +1170,66 @@ This event will be fired when user clicks to an action inside the splash loader.

```typescript
...
onSplashLoaderActionClick?: (
action: Action,
eventId?: string) => void;
onSplashLoaderActionClick?: (
action: Action,
eventId?: string)):void => {
console.log(`Splash loader action click ${action.id}`);
};
...
```

---

### onPromptTopBarItemAdded

This event will be fired when a new item is added to the prompt top bar. It passes the `tabId`, the added `topBarItem` object, and the `eventId`.

```typescript
...
onPromptTopBarItemAdded?: (
tabId: string,
topBarItem: ChatItemButton,
eventId?: string):void => {
console.log(`Top bar item added to tab: ${tabId}`);
console.log(`Item ID: ${topBarItem.id}`);
console.log(`Item text: ${topBarItem.text}`);
};
...
```

---

### onPromptTopBarItemRemoved

This event will be fired when an item is removed from the prompt top bar. It passes the `tabId`, the `topBarItemId` of the removed item, and the `eventId`.

```typescript
...
onPromptTopBarItemRemoved?: (
tabId: string,
topBarItemId: string,
eventId?: string):void => {
console.log(`Top bar item removed from tab: ${tabId}`);
console.log(`Removed item ID: ${topBarItemId}`);
};
...
```

---

### onPromptTopBarButtonClick

This event will be fired when a user clicks on a button in the prompt top bar. It passes the `tabId`, the clicked `topBarButton` object, and the `eventId`.

```typescript
...
onPromptTopBarButtonClick?: (
tabId: string,
topBarButton: ChatItemButton,
eventId?: string):void => {
console.log(`Top bar button clicked on tab: ${tabId}`);
console.log(`Button ID: ${topBarButton.id}`);
console.log(`Button text: ${topBarButton.text}`);
};
...
```
59 changes: 58 additions & 1 deletion docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,61 @@ You can use this method to tear down the existing Mynah UI state. This is useful

```typescript
mynahUI.destroy();
```
```

---

# Opening a Top Bar Button Overlay (`openTopBarButtonOverlay`)

You can programmatically open an overlay for a top bar button using the `openTopBarButtonOverlay` function. This is useful when you want to display a detailed list or other content associated with a specific top bar button.

```typescript
mynahUI.openTopBarButtonOverlay(
tabId, // The ID of the tab containing the top bar button
topBarButtonId, // The ID of the top bar button to open the overlay for
{
topBarButtonOverlay: {
// DetailedList configuration for the overlay content
list: [
{
groupName: 'Group 1',
children: [
{
title: 'Item 1',
description: 'Description for item 1'
},
{
title: 'Item 2',
description: 'Description for item 2'
}
]
}
],
selectable: 'clickable'
},
events: {
onGroupClick: (groupName) => {
console.log(`Group clicked: ${groupName}`);
},
onItemClick: (detailedListItem) => {
console.log(`Item clicked: ${detailedListItem.title}`);
},
onKeyPress: (e) => {
// Handle keyboard events
if (e.key === 'Escape') {
// Close the overlay
}
},
onClose: () => {
console.log('Overlay closed');
}
}
}
);
```

The overlay will appear positioned relative to the specified top bar button, displaying the detailed list content you've provided. Users can interact with the items in the list, and you can handle these interactions through the event callbacks.

<p align="center">
<img src="./img/topBarButtonOverlay.png" alt="Top Bar Button Overlay" style="max-width:500px; width:100%;border: 1px solid #e0e0e0;">
</p>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/topBarButtonOverlay.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion example/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ChatItemType, MynahIcons } from '@aws/mynah-ui';
import { defaultFollowUps } from './samples/sample-data';
import { Commands } from './commands';
import { MynahUITabStoreTab, QuickActionCommandGroup, TabBarMainAction } from '../../dist/static';
import { ChatItemButton, MynahUITabStoreTab, QuickActionCommandGroup, TabBarMainAction } from '../../dist/static';
export const WelcomeMessage = `Hi, this is \`MynahUI\` and it is a **data and event driven** web based chat interface library and it is independent from any framework like react or vue etc.
In this example web app which uses mynah-ui as its renderer, we're simulating its capabilities with some static content with an IDE look&feel.

Expand All @@ -13,6 +13,10 @@ export const mcpButton: TabBarMainAction = {
description: 'Initializing MCP servers',
icon: 'mcp',
};

export const rulesButton: ChatItemButton = {id: 'Rules', status: 'clear', text: 'Rules', icon: 'check-list'}


export const tabbarButtons: TabBarMainAction[] = [
{
id: 'clear',
Expand Down Expand Up @@ -52,6 +56,11 @@ export const tabbarButtons: TabBarMainAction[] = [
id: 'show-avatars',
text: 'Show/Hide avatars',
icon: MynahIcons.USER,
},
{
id: 'show-pinned-context',
text: 'Show/Hide Pinned Context',
icon: MynahIcons.PIN,
},
{
id: 'show-code-diff',
Expand Down
55 changes: 54 additions & 1 deletion example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
generateUID,
KeyMap,
TreeNodeDetails,
QuickActionCommand,
ChatItemButton,
} from '@aws/mynah-ui';
import { mcpButton, mynahUIDefaults, tabbarButtons } from './config';
import { mcpButton, mynahUIDefaults, rulesButton, tabbarButtons } from './config';
import { Log, LogClear } from './logger';
import {
exampleCodeBlockToInsert,
Expand Down Expand Up @@ -48,16 +50,19 @@ import {
sampleMCPDetails,
mcpToolRunSampleCard,
mcpToolRunSampleCardInit,
sampleRulesList,
} from './samples/sample-data';
import escapeHTML from 'escape-html';
import './styles/styles.scss';
import { ThemeBuilder } from './theme-builder/theme-builder';
import { Commands } from './commands';
import { group } from 'console';

export const createMynahUI = (initialData?: MynahUIDataModel): MynahUI => {
const connector = new Connector();
let streamingMessageId: string | null;
let showChatAvatars: boolean = false;
let showPinnedContext: boolean = false;

const mynahUI = new MynahUI({
loadStyles: true,
Expand Down Expand Up @@ -139,6 +144,39 @@ Model - ${optionsValues['model-select'] !== '' ? optionsValues['model-select'] :
},
onFocusStateChanged: (focusState: boolean) => {
Log(`MynahUI focus state changed: <b>${focusState.toString()}</b>`);
},
onPromptTopBarItemAdded: (tabId: string, item: QuickActionCommand) => {
Log(`Prompt top bar item <b>${item.command}</b> added on tab <b>${tabId}</b>`);

mynahUI.updateStore(tabId, {
promptTopBarContextItems: [
...((mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command) ?? []),
item,
],
});
},
onPromptTopBarItemRemoved: (tabId: string, item: QuickActionCommand) => {
Log(`Prompt top bar item <b>${item.command}</b> removed on tab <b>${tabId}</b>`);

mynahUI.updateStore(tabId, {
promptTopBarContextItems:(mynahUI.getTabData(tabId).getValue('promptTopBarContextItems') as QuickActionCommand[]).filter((existingItem) => existingItem.command !== item.command)
});
},
onPromptTopBarButtonClick: (tabId: string, button: ChatItemButton) => {
Log(`Top bar button <b>${button.id}</b> clicked on tab <b>${tabId}</b>`);

const topBarOverlay = mynahUI.openTopBarButtonOverlay({tabId, topBarButtonOverlay: sampleRulesList,
events: {
onClose: () => {Log(`Top bar overlay closed on tab <b>${tabId}</b>`)},
onGroupClick: (group) => {Log(`Top bar overlay group clicked <b>${group}</b> on tab <b>${tabId}</b>`)},
onItemClick: (item) => { Log(`Top bar overlay item clicked <b>${item.id}</b> on tab <b>${tabId}</b>`); topBarOverlay.update(sampleRulesList)},
onKeyPress: (e) => {Log(`Key pressed on top bar overlay`); if (e.key === KeyMap.ESCAPE) {
topBarOverlay.close();
}}

}})


},
onTabBarButtonClick: (tabId: string, buttonId: string) => {
if (buttonId.match('mcp-')) {
Expand Down Expand Up @@ -267,6 +305,21 @@ Model - ${optionsValues['model-select'] !== '' ? optionsValues['model-select'] :
showChatAvatars: showChatAvatars,
}),
);
} else if (buttonId === 'show-pinned-context') {
showPinnedContext = !showPinnedContext;
if (showPinnedContext){
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: `@Pin Context`,
promptTopBarButton: rulesButton,
}),
); } else {
Object.keys(mynahUI.getAllTabs()).forEach((tabIdFromStore) =>
mynahUI.updateStore(tabIdFromStore, {
promptTopBarTitle: ``,
}),
)
}
} else if (buttonId === 'splash-loader') {
mynahUI.toggleSplashLoader(true, 'Showing splash loader...');
setTimeout(() => {
Expand Down
7 changes: 7 additions & 0 deletions example/src/samples/sample-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ChatItemContent,
ChatItemType,
DetailedList,
DetailedListItemGroup,
generateUID,
MynahIcons,
MynahUIDataModel,
Expand Down Expand Up @@ -2261,3 +2262,9 @@ export const sampleMCPDetails = (title: string): DetailedList => {
],
};
};


export const sampleRulesList: DetailedList = {selectable: 'clickable', list: [{children: [{id: 'README', icon: MynahIcons.CHECK_LIST,
description: 'README',actions: [{ id: 'README.md', icon: MynahIcons.OK, status: 'clear' }]}]},
{groupName: '.amazonq/rules', childrenIndented: true, icon: MynahIcons.FOLDER , actions: [{ id: 'java-expert.md', icon: MynahIcons.OK, status: 'clear' }], children: [{id: 'java-expert.md', icon: MynahIcons.CHECK_LIST,
description: 'java-expert',actions: [{ id: 'java-expert.md', icon: MynahIcons.OK, status: 'clear' }]}]}]}
Loading