Skip to content

Commit e4d4ef0

Browse files
authored
feat(chat-client): support profile banner changes (#988)
1 parent 392a31d commit e4d4ef0

File tree

6 files changed

+125
-43
lines changed

6 files changed

+125
-43
lines changed

chat-client/src/client/chat.test.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { createChat } from './chat'
1818
import sinon = require('sinon')
1919
import { TELEMETRY } from '../contracts/serverContracts'
2020
import {
21+
ENTER_FOCUS,
2122
ERROR_MESSAGE_TELEMETRY_EVENT,
2223
SEND_TO_PROMPT_TELEMETRY_EVENT,
2324
TAB_ADD_TELEMETRY_EVENT,
@@ -65,26 +66,41 @@ describe('Chat', () => {
6566
global.ResizeObserver = undefined
6667
})
6768

68-
it('publishes ready event and initial tab add event, when initialized', () => {
69-
assert.callCount(clientApi.postMessage, 4)
69+
it('publishes ready event when initialized', () => {
70+
assert.calledWithExactly(clientApi.postMessage.firstCall, { command: READY_NOTIFICATION_METHOD })
71+
})
7072

71-
assert.calledWithExactly(clientApi.postMessage.firstCall, {
73+
it('creates initial tab when chat options are provided', () => {
74+
const bannerText = 'This is a test banner message'
75+
const eventParams = {
76+
command: CHAT_OPTIONS,
77+
params: {
78+
chatNotifications: {
79+
bannerText: bannerText,
80+
},
81+
},
82+
}
83+
const sendToPromptEvent = createInboundEvent(eventParams)
84+
window.dispatchEvent(sendToPromptEvent)
85+
86+
assert.calledWithExactly(clientApi.postMessage.firstCall, { command: READY_NOTIFICATION_METHOD })
87+
88+
assert.calledWithExactly(clientApi.postMessage.secondCall, {
7289
command: TELEMETRY,
73-
params: { name: 'enterFocus' },
90+
params: { name: ENTER_FOCUS },
7491
})
75-
assert.calledWithExactly(clientApi.postMessage.secondCall, { command: READY_NOTIFICATION_METHOD })
7692

7793
assert.calledWithExactly(clientApi.postMessage.thirdCall, {
7894
command: TAB_ADD_NOTIFICATION_METHOD,
79-
params: { tabId: initialTabId },
95+
params: { tabId: sinon.match.string },
8096
})
8197

8298
assert.calledWithExactly(clientApi.postMessage.lastCall, {
8399
command: TELEMETRY,
84100
params: {
85101
triggerType: 'click',
86102
name: TAB_ADD_TELEMETRY_EVENT,
87-
tabId: initialTabId,
103+
tabId: sinon.match.string,
88104
},
89105
})
90106
})
@@ -313,9 +329,9 @@ describe('Chat', () => {
313329
assert.called(TabFactory.prototype.enableHistory)
314330
// @ts-ignore
315331
assert.called(TabFactory.prototype.enableExport)
316-
})
332+
}).timeout(20000)
317333

318-
it('does not enable history and export features support if flags are falsy', () => {
334+
it('does not enable history and export features support if flags are falsy', async () => {
319335
const chatOptionsRequest = createInboundEvent({
320336
command: CHAT_OPTIONS,
321337
params: {
@@ -329,7 +345,7 @@ describe('Chat', () => {
329345
assert.notCalled(TabFactory.prototype.enableHistory)
330346
// @ts-ignore
331347
assert.notCalled(TabFactory.prototype.enableExport)
332-
})
348+
}).timeout(20000)
333349
})
334350

335351
describe('onGetSerializedChat', () => {

chat-client/src/client/chat.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ import {
3535
} from '@aws/chat-client-ui-types'
3636
import {
3737
BUTTON_CLICK_REQUEST_METHOD,
38+
CHAT_OPTIONS_UPDATE_NOTIFICATION_METHOD,
3839
CHAT_REQUEST_METHOD,
3940
CHAT_UPDATE_NOTIFICATION_METHOD,
4041
CONTEXT_COMMAND_NOTIFICATION_METHOD,
4142
CONVERSATION_CLICK_REQUEST_METHOD,
4243
CREATE_PROMPT_NOTIFICATION_METHOD,
44+
ChatOptionsUpdateParams,
4345
ChatParams,
4446
ChatUpdateParams,
4547
ContextCommandParams,
@@ -181,8 +183,16 @@ export const createChat = (
181183
case GET_SERIALIZED_CHAT_REQUEST_METHOD:
182184
mynahApi.getSerializedChat(message.requestId, message.params as GetSerializedChatParams)
183185
break
186+
case CHAT_OPTIONS_UPDATE_NOTIFICATION_METHOD:
187+
tabFactory.setInfoMessages((message.params as ChatOptionsUpdateParams).chatNotifications)
188+
break
184189
case CHAT_OPTIONS: {
185190
const params = (message as ChatOptionsMessage).params
191+
192+
if (params?.chatNotifications) {
193+
tabFactory.setInfoMessages((message.params as ChatOptionsUpdateParams).chatNotifications)
194+
}
195+
186196
if (params?.quickActions?.quickActionsCommandGroups) {
187197
const quickActionCommandGroups = params.quickActions.quickActionsCommandGroups.map(group => ({
188198
...group,
@@ -202,6 +212,9 @@ export const createChat = (
202212
tabFactory.enableExport()
203213
}
204214

215+
const initialTabId = mynahApi.createTabId()
216+
if (initialTabId) mynahUi.selectTab(initialTabId)
217+
205218
const allExistingTabs: MynahUITabStoreModel = mynahUi.getAllTabs()
206219
const highlightCommand = featureConfig.get('highlightCommand')
207220

chat-client/src/client/mynahUi.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('MynahUI', () => {
1717

1818
let getSelectedTabIdStub: sinon.SinonStub
1919
let createTabStub: sinon.SinonStub
20+
let getChatItemsStub: sinon.SinonStub
2021
let getAllTabsStub: sinon.SinonStub
2122
let updateStoreSpy: sinon.SinonSpy
2223
let addChatItemSpy: sinon.SinonSpy
@@ -68,6 +69,8 @@ describe('MynahUI', () => {
6869
const tabFactory = new TabFactory({})
6970
createTabStub = sinon.stub(tabFactory, 'createTab')
7071
createTabStub.returns({})
72+
getChatItemsStub = sinon.stub(tabFactory, 'getChatItems')
73+
getChatItemsStub.returns([])
7174
const mynahUiResult = createMynahUi(messager, tabFactory, true, true)
7275
mynahUi = mynahUiResult[0]
7376
inboundChatApi = mynahUiResult[1]
@@ -142,10 +145,12 @@ describe('MynahUI', () => {
142145
describe('openTab', () => {
143146
it('should create a new tab with welcome messages if tabId not passed and previous messages not passed', () => {
144147
createTabStub.resetHistory()
148+
getChatItemsStub.resetHistory()
145149

146150
inboundChatApi.openTab(requestId, {})
147151

148-
sinon.assert.calledOnceWithExactly(createTabStub, true, false, false, undefined)
152+
sinon.assert.calledOnceWithExactly(createTabStub, false)
153+
sinon.assert.calledOnceWithExactly(getChatItemsStub, true, false, undefined)
149154
sinon.assert.notCalled(selectTabSpy)
150155
sinon.assert.calledOnce(onOpenTabSpy)
151156
})
@@ -165,6 +170,7 @@ describe('MynahUI', () => {
165170
]
166171

167172
createTabStub.resetHistory()
173+
getChatItemsStub.resetHistory()
168174

169175
inboundChatApi.openTab(requestId, {
170176
newTabOptions: {
@@ -174,19 +180,21 @@ describe('MynahUI', () => {
174180
},
175181
})
176182

177-
sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, mockMessages)
183+
sinon.assert.calledOnceWithExactly(createTabStub, false)
184+
sinon.assert.calledOnceWithExactly(getChatItemsStub, false, false, mockMessages)
178185
sinon.assert.notCalled(selectTabSpy)
179186
sinon.assert.calledOnce(onOpenTabSpy)
180187
})
181188

182189
it('should call onOpenTab if a new tab if tabId not passed and tab not created', () => {
183190
createTabStub.resetHistory()
191+
getChatItemsStub.resetHistory()
184192
updateStoreSpy.restore()
185193
sinon.stub(mynahUi, 'updateStore').returns(undefined)
186194

187195
inboundChatApi.openTab(requestId, {})
188196

189-
sinon.assert.calledOnceWithExactly(createTabStub, true, false, false, undefined)
197+
sinon.assert.calledOnceWithExactly(createTabStub, false)
190198
sinon.assert.notCalled(selectTabSpy)
191199
sinon.assert.calledOnceWithMatch(onOpenTabSpy, requestId, { type: 'InvalidRequest' })
192200
})
@@ -229,7 +237,7 @@ describe('MynahUI', () => {
229237
getSelectedTabIdStub.returns(undefined)
230238
inboundChatApi.sendGenericCommand({ genericCommand, selection, tabId, triggerType })
231239

232-
sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, undefined)
240+
sinon.assert.calledOnceWithExactly(createTabStub, false)
233241
sinon.assert.calledThrice(updateStoreSpy)
234242
})
235243

@@ -245,7 +253,7 @@ describe('MynahUI', () => {
245253
getSelectedTabIdStub.returns(tabId)
246254
inboundChatApi.sendGenericCommand({ genericCommand, selection, tabId, triggerType })
247255

248-
sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, undefined)
256+
sinon.assert.calledOnceWithExactly(createTabStub, false)
249257
sinon.assert.calledThrice(updateStoreSpy)
250258
})
251259

chat-client/src/client/mynahUi.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface InboundChatApi {
6767
listConversations(params: ListConversationsResult): void
6868
conversationClicked(params: ConversationClickResult): void
6969
getSerializedChat(requestId: string, params: GetSerializedChatParams): void
70+
createTabId(openTab?: boolean): string | undefined
7071
}
7172

7273
type ContextCommandGroups = MynahUIDataModel['contextCommands']
@@ -83,6 +84,8 @@ const getTabPairProgrammingMode = (mynahUi: MynahUI, tabId: string) => {
8384
return promptInputOptions.find(item => item.id === 'pair-programmer-mode')?.value === 'true'
8485
}
8586

87+
const openTabKey = 'openTab'
88+
8689
export const handlePromptInputChange = (mynahUi: MynahUI, tabId: string, optionsValues: Record<string, string>) => {
8790
const promptTypeValue = optionsValues['pair-programmer-mode']
8891

@@ -170,7 +173,6 @@ export const createMynahUi = (
170173
customChatClientAdapter?: ChatClientAdapter,
171174
featureConfig?: Map<string, any>
172175
): [MynahUI, InboundChatApi] => {
173-
const initialTabId = TabFactory.generateUniqueId()
174176
let disclaimerCardActive = !disclaimerAcknowledged
175177
let programmingModeCardActive = !pairProgrammingCardAcknowledged
176178
let contextCommandGroups: ContextCommandGroups | undefined
@@ -227,7 +229,6 @@ export const createMynahUi = (
227229
},
228230
onReady: () => {
229231
messager.onUiReady()
230-
messager.onTabAdd(initialTabId)
231232
},
232233
onFileClick: (tabId, filePath, deleted, messageId, eventId, fileDetails) => {
233234
messager.onFileClick({ tabId, filePath, messageId, fullPath: fileDetails?.data?.['fullPath'] })
@@ -250,6 +251,16 @@ export const createMynahUi = (
250251
],
251252
...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}),
252253
}
254+
255+
const tabStore = mynahUi.getTabData(tabId).getStore()
256+
257+
// Tabs can be opened through different methods, including server-initiated 'openTab' requests.
258+
// The 'openTab' request is specifically used for loading historical chat sessions with pre-existing messages.
259+
// We check if tabMetadata.openTabKey exists - if it does and is set to true, we skip showing welcome messages
260+
// since this indicates we're loading a previous chat session rather than starting a new one.
261+
if (!tabStore?.tabMetadata || !tabStore.tabMetadata.openTabKey) {
262+
defaultTabConfig.chatItems = tabFactory.getChatItems(true, programmingModeCardActive, [])
263+
}
253264
mynahUi.updateStore(tabId, defaultTabConfig)
254265
messager.onTabAdd(tabId)
255266
},
@@ -446,7 +457,7 @@ export const createMynahUi = (
446457
// Update the tab defaults to hide the programmer mode card for new tabs
447458
mynahUi.updateTabDefaults({
448459
store: {
449-
chatItems: tabFactory.createTab(true, disclaimerCardActive, false).chatItems,
460+
chatItems: tabFactory.getChatItems(true, false),
450461
},
451462
})
452463
}
@@ -464,14 +475,9 @@ export const createMynahUi = (
464475
}
465476

466477
const mynahUiProps: MynahUIProps = {
467-
tabs: {
468-
[initialTabId]: {
469-
isSelected: true,
470-
store: tabFactory.createTab(true, disclaimerCardActive, programmingModeCardActive),
471-
},
472-
},
478+
tabs: {},
473479
defaults: {
474-
store: tabFactory.createTab(true, false, programmingModeCardActive),
480+
store: tabFactory.createTab(false),
475481
},
476482
config: {
477483
maxTabs: 10,
@@ -495,11 +501,14 @@ export const createMynahUi = (
495501
return tabId ? mynahUi.getAllTabs()[tabId]?.store : undefined
496502
}
497503

498-
const createTabId = (needWelcomeMessages: boolean = false, chatMessages?: ChatMessage[]) => {
499-
const tabId = mynahUi.updateStore(
500-
'',
501-
tabFactory.createTab(needWelcomeMessages, disclaimerCardActive, programmingModeCardActive, chatMessages)
502-
)
504+
// The 'openTab' parameter indicates whether this tab creation is initiated by 'openTab' server request
505+
// to restore a previous chat session (true) or if it's a new client-side tab creation (false/undefined).
506+
// This distinction helps maintain consistent tab behavior between fresh conversations and restored sessions.
507+
const createTabId = (openTab?: boolean) => {
508+
const tabId = mynahUi.updateStore('', {
509+
...tabFactory.createTab(disclaimerCardActive),
510+
tabMetadata: { openTabKey: openTab ? true : false },
511+
})
503512
if (tabId === undefined) {
504513
mynahUi.notify({
505514
content: uiComponentsTexts.noMoreTabsTooltip,
@@ -789,8 +798,11 @@ ${params.message}`,
789798
messager.onOpenTab(requestId, { tabId: params.tabId })
790799
} else {
791800
const messages = params.newTabOptions?.data?.messages
792-
const tabId = createTabId(messages ? false : true, messages)
801+
const tabId = createTabId(true)
793802
if (tabId) {
803+
mynahUi.updateStore(tabId, {
804+
chatItems: tabFactory.getChatItems(messages ? false : true, programmingModeCardActive, messages),
805+
})
794806
messager.onOpenTab(requestId, { tabId })
795807
} else {
796808
messager.onOpenTab(requestId, {
@@ -902,6 +914,7 @@ ${params.message}`,
902914
listConversations: listConversations,
903915
conversationClicked: conversationClicked,
904916
getSerializedChat: getSerializedChat,
917+
createTabId: createTabId,
905918
}
906919

907920
return [mynahUi, api]

chat-client/src/client/tabs/tabFactory.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,27 @@ export class TabFactory {
2828

2929
constructor(
3030
private defaultTabData: DefaultTabData,
31-
private quickActionCommands?: QuickActionCommandGroup[]
31+
private quickActionCommands?: QuickActionCommandGroup[],
32+
private bannerMessage?: ChatMessage
3233
) {}
3334

34-
public createTab(
35+
public createTab(disclaimerCardActive: boolean): MynahUIDataModel {
36+
const tabData: MynahUIDataModel = {
37+
...this.getDefaultTabData(),
38+
...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}),
39+
promptInputOptions: [pairProgrammingPromptInput],
40+
}
41+
return tabData
42+
}
43+
44+
public getChatItems(
3545
needWelcomeMessages: boolean,
36-
disclaimerCardActive: boolean,
3746
pairProgrammingCardActive: boolean,
3847
chatMessages?: ChatMessage[]
39-
): MynahUIDataModel {
40-
const tabData: MynahUIDataModel = {
41-
...this.getDefaultTabData(),
42-
chatItems: needWelcomeMessages
48+
): ChatItem[] {
49+
return [
50+
...(this.bannerMessage ? [this.getBannerMessage() as ChatItem] : []),
51+
...(needWelcomeMessages
4352
? [
4453
...(pairProgrammingCardActive ? [programmerModeCard] : []),
4554
{
@@ -51,11 +60,8 @@ export class TabFactory {
5160
]
5261
: chatMessages
5362
? (chatMessages as ChatItem[])
54-
: [],
55-
...(disclaimerCardActive ? { promptInputStickyCard: disclaimerCard } : {}),
56-
promptInputOptions: [pairProgrammingPromptInput],
57-
}
58-
return tabData
63+
: []),
64+
]
5965
}
6066

6167
public updateQuickActionCommands(quickActionCommands: QuickActionCommandGroup[]) {
@@ -80,6 +86,24 @@ export class TabFactory {
8086
return tabData
8187
}
8288

89+
public setInfoMessages(messages: ChatMessage[] | undefined) {
90+
if (messages?.length) {
91+
// For now this messages array is only populated with banner data hence we use the first item
92+
this.bannerMessage = messages[0]
93+
}
94+
}
95+
96+
private getBannerMessage(): ChatItem | undefined {
97+
if (this.bannerMessage) {
98+
return {
99+
type: ChatItemType.ANSWER,
100+
status: 'info',
101+
...this.bannerMessage,
102+
} as ChatItem
103+
}
104+
return undefined
105+
}
106+
83107
private getTabBarButtons(): TabBarMainAction[] | undefined {
84108
const tabBarButtons = [...(this.defaultTabData.tabBarButtons ?? [])]
85109

0 commit comments

Comments
 (0)