diff --git a/chat-client/package.json b/chat-client/package.json index b5a8d91e77..ec7994d35c 100644 --- a/chat-client/package.json +++ b/chat-client/package.json @@ -22,7 +22,7 @@ "package": "webpack" }, "dependencies": { - "@aws/chat-client-ui-types": "^0.1.16", + "@aws/chat-client-ui-types": "^0.1.22", "@aws/language-server-runtimes-types": "^0.1.19", "@aws/mynah-ui": "file:./lib/aws-mynah-ui-4.31.0-beta.6.tgz" }, diff --git a/chat-client/src/client/chat.ts b/chat-client/src/client/chat.ts index d881fc8e0d..60f6877a66 100644 --- a/chat-client/src/client/chat.ts +++ b/chat-client/src/client/chat.ts @@ -28,6 +28,7 @@ import { DISCLAIMER_ACKNOWLEDGED, ErrorResult, UiResultMessage, + CHAT_PROMPT_OPTION_ACKNOWLEDGED, } from '@aws/chat-client-ui-types' import { CHAT_REQUEST_METHOD, @@ -89,7 +90,10 @@ const DEFAULT_TAB_DATA = { promptInputPlaceholder: 'Ask a question or enter "/" for quick actions', } -type ChatClientConfig = Pick & { disclaimerAcknowledged?: boolean } +type ChatClientConfig = Pick & { + disclaimerAcknowledged?: boolean + pairProgrammingAcknowledged?: boolean +} export const createChat = ( clientApi: { postMessage: (msg: UiMessage | UiResultMessage | ServerMessage) => void }, @@ -242,6 +246,14 @@ export const createChat = ( disclaimerAcknowledged: () => { sendMessageToClient({ command: DISCLAIMER_ACKNOWLEDGED }) }, + chatPromptOptionAcknowledged: (messageId: string) => { + sendMessageToClient({ + command: CHAT_PROMPT_OPTION_ACKNOWLEDGED, + params: { + messageId, + }, + }) + }, onOpenTab: (requestId: string, params: OpenTabResult | ErrorResult) => { if ('tabId' in params) { sendMessageToClient({ @@ -313,6 +325,7 @@ export const createChat = ( messager, tabFactory, config?.disclaimerAcknowledged ?? false, + config?.pairProgrammingAcknowledged ?? false, chatClientAdapter ) diff --git a/chat-client/src/client/messager.ts b/chat-client/src/client/messager.ts index 555eb00168..d6f9344b99 100644 --- a/chat-client/src/client/messager.ts +++ b/chat-client/src/client/messager.ts @@ -81,6 +81,7 @@ export interface OutboundChatApi { infoLinkClick(params: InfoLinkClickParams): void uiReady(): void disclaimerAcknowledged(): void + chatPromptOptionAcknowledged(messageId: string): void onOpenTab(requestId: string, result: OpenTabResult | ErrorResult): void createPrompt(params: CreatePromptParams): void fileClick(params: FileClickParams): void @@ -115,6 +116,10 @@ export class Messager { this.chatApi.disclaimerAcknowledged() } + onChatPromptOptionAcknowledged = (messageId: string): void => { + this.chatApi.chatPromptOptionAcknowledged(messageId) + } + onFocusStateChanged = (focusState: boolean): void => { this.chatApi.telemetry({ name: focusState ? ENTER_FOCUS : EXIT_FOCUS }) } diff --git a/chat-client/src/client/mynahUi.test.ts b/chat-client/src/client/mynahUi.test.ts index f656cfb544..c74789dd33 100644 --- a/chat-client/src/client/mynahUi.test.ts +++ b/chat-client/src/client/mynahUi.test.ts @@ -46,6 +46,7 @@ describe('MynahUI', () => { infoLinkClick: sinon.stub(), uiReady: sinon.stub(), disclaimerAcknowledged: sinon.stub(), + chatPromptOptionAcknowledged: sinon.stub(), onOpenTab: sinon.stub(), createPrompt: sinon.stub(), fileClick: sinon.stub(), @@ -64,7 +65,7 @@ describe('MynahUI', () => { const tabFactory = new TabFactory({}) createTabStub = sinon.stub(tabFactory, 'createTab') createTabStub.returns({}) - const mynahUiResult = createMynahUi(messager, tabFactory, true) + const mynahUiResult = createMynahUi(messager, tabFactory, true, true) mynahUi = mynahUiResult[0] inboundChatApi = mynahUiResult[1] getSelectedTabIdStub = sinon.stub(mynahUi, 'getSelectedTabId') @@ -133,7 +134,7 @@ describe('MynahUI', () => { inboundChatApi.openTab(requestId, {}) - sinon.assert.calledOnceWithExactly(createTabStub, true, false, undefined) + sinon.assert.calledOnceWithExactly(createTabStub, true, false, false, undefined) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -162,7 +163,7 @@ describe('MynahUI', () => { }, }) - sinon.assert.calledOnceWithExactly(createTabStub, false, false, mockMessages) + sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, mockMessages) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnce(onOpenTabSpy) }) @@ -174,7 +175,7 @@ describe('MynahUI', () => { inboundChatApi.openTab(requestId, {}) - sinon.assert.calledOnceWithExactly(createTabStub, true, false, undefined) + sinon.assert.calledOnceWithExactly(createTabStub, true, false, false, undefined) sinon.assert.notCalled(selectTabSpy) sinon.assert.calledOnceWithMatch(onOpenTabSpy, requestId, { type: 'InvalidRequest' }) }) @@ -217,7 +218,7 @@ describe('MynahUI', () => { getSelectedTabIdStub.returns(undefined) inboundChatApi.sendGenericCommand({ genericCommand, selection, tabId, triggerType }) - sinon.assert.calledOnceWithExactly(createTabStub, false, false, undefined) + sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, undefined) sinon.assert.calledThrice(updateStoreSpy) }) @@ -233,7 +234,7 @@ describe('MynahUI', () => { getSelectedTabIdStub.returns(tabId) inboundChatApi.sendGenericCommand({ genericCommand, selection, tabId, triggerType }) - sinon.assert.calledOnceWithExactly(createTabStub, false, false, undefined) + sinon.assert.calledOnceWithExactly(createTabStub, false, false, false, undefined) sinon.assert.calledThrice(updateStoreSpy) }) @@ -394,7 +395,7 @@ describe('withAdapter', () => { telemetry: sinon.stub(), } as OutboundChatApi) const tabFactory = new TabFactory({}) - const mynahUiResult = createMynahUi(messager as Messager, tabFactory, true, chatClientAdapter) + const mynahUiResult = createMynahUi(messager as Messager, tabFactory, true, true, chatClientAdapter) mynahUi = mynahUiResult[0] }) diff --git a/chat-client/src/client/mynahUi.ts b/chat-client/src/client/mynahUi.ts index 833904f870..ed6f9607a6 100644 --- a/chat-client/src/client/mynahUi.ts +++ b/chat-client/src/client/mynahUi.ts @@ -45,7 +45,7 @@ import { ChatClientAdapter, ChatEventHandler } from '../contracts/chatClientAdap import { withAdapter } from './withAdapter' import { toMynahButtons, toMynahHeader, toMynahIcon } from './utils' import { ChatHistory, ChatHistoryList } from './features/history' -import { pairProgrammingModeOff, pairProgrammingModeOn } from './texts/pairProgramming' +import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming' export interface InboundChatApi { addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void @@ -132,10 +132,12 @@ export const createMynahUi = ( messager: Messager, tabFactory: TabFactory, disclaimerAcknowledged: boolean, + pairProgrammingCardAcknowledged: boolean, customChatClientAdapter?: ChatClientAdapter ): [MynahUI, InboundChatApi] => { const initialTabId = TabFactory.generateUniqueId() let disclaimerCardActive = !disclaimerAcknowledged + let programmingModeCardActive = !pairProgrammingCardAcknowledged let contextCommandGroups: ContextCommandGroups | undefined let chatEventHandlers: ChatEventHandler = { @@ -368,17 +370,30 @@ export const createMynahUi = ( handlePromptInputChange(mynahUi, tabId, optionsValues) messager.onPromptInputOptionChange({ tabId, optionsValues }) }, + onMessageDismiss: (tabId, messageId) => { + if (messageId === programmerModeCard.messageId) { + programmingModeCardActive = false + messager.onChatPromptOptionAcknowledged(messageId) + + // Update the tab defaults to hide the programmer mode card for new tabs + mynahUi.updateTabDefaults({ + store: { + chatItems: tabFactory.createTab(true, disclaimerCardActive, false).chatItems, + }, + }) + } + }, } const mynahUiProps: MynahUIProps = { tabs: { [initialTabId]: { isSelected: true, - store: tabFactory.createTab(true, disclaimerCardActive), + store: tabFactory.createTab(true, disclaimerCardActive, programmingModeCardActive), }, }, defaults: { - store: tabFactory.createTab(true, false), + store: tabFactory.createTab(true, false, programmingModeCardActive), }, config: { maxTabs: 10, @@ -405,7 +420,7 @@ export const createMynahUi = ( const createTabId = (needWelcomeMessages: boolean = false, chatMessages?: ChatMessage[]) => { const tabId = mynahUi.updateStore( '', - tabFactory.createTab(needWelcomeMessages, disclaimerCardActive, chatMessages) + tabFactory.createTab(needWelcomeMessages, disclaimerCardActive, programmingModeCardActive, chatMessages) ) if (tabId === undefined) { mynahUi.notify({ diff --git a/chat-client/src/client/tabs/tabFactory.ts b/chat-client/src/client/tabs/tabFactory.ts index 77ee7d605f..351e0aacf1 100644 --- a/chat-client/src/client/tabs/tabFactory.ts +++ b/chat-client/src/client/tabs/tabFactory.ts @@ -9,7 +9,7 @@ import { import { disclaimerCard } from '../texts/disclaimer' import { ChatMessage } from '@aws/language-server-runtimes-types' import { ChatHistory } from '../features/history' -import { pairProgrammingPromptInput } from '../texts/pairProgramming' +import { pairProgrammingPromptInput, programmerModeCard } from '../texts/pairProgramming' export type DefaultTabData = MynahUIDataModel @@ -34,12 +34,14 @@ export class TabFactory { public createTab( needWelcomeMessages: boolean, disclaimerCardActive: boolean, + pairProgrammingCardActive: boolean, chatMessages?: ChatMessage[] ): MynahUIDataModel { const tabData: MynahUIDataModel = { ...this.getDefaultTabData(), chatItems: needWelcomeMessages ? [ + ...(pairProgrammingCardActive ? [programmerModeCard] : []), { type: ChatItemType.ANSWER, body: `Hi, I'm Amazon Q. I can answer your software development questions. diff --git a/chat-client/src/client/texts/pairProgramming.ts b/chat-client/src/client/texts/pairProgramming.ts index 7fb3bdc995..79b9abf373 100644 --- a/chat-client/src/client/texts/pairProgramming.ts +++ b/chat-client/src/client/texts/pairProgramming.ts @@ -1,5 +1,19 @@ import { ChatItem, ChatItemFormItem, ChatItemType } from '@aws/mynah-ui' +export const programmerModeCard: ChatItem = { + type: ChatItemType.ANSWER, + title: 'NEW FEATURE', + header: { + icon: 'code-block', + iconStatus: 'primary', + body: '## Pair Programmer', + }, + messageId: 'programmerModeCardId', + fullWidth: true, + canBeDismissed: true, + body: 'Amazon Q Developer chat can now write code and run shell commands on your behalf. Disable Pair Programmer if you prefer a read-only experience.', +} + export const pairProgrammingPromptInput: ChatItemFormItem = { type: 'switch', id: 'pair-programmer-mode', diff --git a/chat-client/src/client/withAdapter.ts b/chat-client/src/client/withAdapter.ts index 8ab7befbd2..b52723157f 100644 --- a/chat-client/src/client/withAdapter.ts +++ b/chat-client/src/client/withAdapter.ts @@ -57,6 +57,7 @@ export const withAdapter = ( onChatPromptProgressActionButtonClicked: addDefaultRouting('onChatPromptProgressActionButtonClicked'), onTabbedContentTabChange: addDefaultRouting('onTabbedContentTabChange'), onPromptInputOptionChange: addDefaultRouting('onPromptInputOptionChange'), + onMessageDismiss: addDefaultRouting('onMessageDismiss'), /** * Handler with special routing logic diff --git a/chat-client/src/contracts/chatClientAdapter.ts b/chat-client/src/contracts/chatClientAdapter.ts index 03babfdeff..82cc542126 100644 --- a/chat-client/src/contracts/chatClientAdapter.ts +++ b/chat-client/src/contracts/chatClientAdapter.ts @@ -36,6 +36,7 @@ export interface ChatEventHandler | 'onResetStore' | 'onReady' | 'onPromptInputOptionChange' + | 'onMessageDismiss' > {} /** diff --git a/package-lock.json b/package-lock.json index b09b08015f..d90f9821fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -246,7 +246,7 @@ ], "license": "Apache-2.0", "dependencies": { - "@aws/chat-client-ui-types": "^0.1.16", + "@aws/chat-client-ui-types": "^0.1.22", "@aws/language-server-runtimes-types": "^0.1.19", "@aws/mynah-ui": "file:./lib/aws-mynah-ui-4.31.0-beta.6.tgz" }, @@ -3276,12 +3276,11 @@ "link": true }, "node_modules/@aws/chat-client-ui-types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.16.tgz", - "integrity": "sha512-ks+fXSKqXY6ThA3nDuTahVDPsoqTIgwsAaUQMmWYym1FUzN51fmkM1IHjyYcwvgkJOrN08KKK1T8Tkz4II23Pg==", - "license": "Apache-2.0", + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@aws/chat-client-ui-types/-/chat-client-ui-types-0.1.22.tgz", + "integrity": "sha512-vn+UKnh9hgZN1LCMONgeZE8WWxivWXaHQq+oG9wpbFhaTXn/nNBTQ9ON7S2fvMqo0g0Np/6hirxZy5ROcWnB9Q==", "dependencies": { - "@aws/language-server-runtimes-types": "^0.1.14" + "@aws/language-server-runtimes-types": "^0.1.19" } }, "node_modules/@aws/hello-world-lsp": {