Skip to content
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
4 changes: 4 additions & 0 deletions chat-client/src/client/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,10 @@ export const createChat = (
promptInputOptionChange: (params: PromptInputOptionChangeParams) => {
sendMessageToClient({ command: PROMPT_INPUT_OPTION_CHANGE_METHOD, params })
},
promptInputButtonClick: params => {
// TODO
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats this TODO for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requires protocol changes in https://github.com/aws/language-server-runtimes to integrate the changes from aws/mynah-ui#322 .

Not a blocker for this PR.

sendMessageToClient({ command: BUTTON_CLICK_REQUEST_METHOD, params })
},
stopChatResponse: (tabId: string) => {
sendMessageToClient({ command: STOP_CHAT_RESPONSE, params: { tabId } })
},
Expand Down
5 changes: 5 additions & 0 deletions chat-client/src/client/messager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface OutboundChatApi {
tabBarAction(params: TabBarActionParams): void
onGetSerializedChat(requestId: string, result: GetSerializedChatResult | ErrorResult): void
promptInputOptionChange(params: PromptInputOptionChangeParams): void
promptInputButtonClick(params: ButtonClickParams): void
stopChatResponse(tabId: string): void
sendButtonClickEvent(params: ButtonClickParams): void
onOpenSettings(settingKey: string): void
Expand Down Expand Up @@ -229,6 +230,10 @@ export class Messager {
this.chatApi.promptInputOptionChange(params)
}

onPromptInputButtonClick = (params: ButtonClickParams): void => {
this.chatApi.promptInputButtonClick(params)
}

onStopChatResponse = (tabId: string): void => {
this.chatApi.stopChatResponse(tabId)
}
Expand Down
1 change: 1 addition & 0 deletions chat-client/src/client/mynahUi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('MynahUI', () => {
tabBarAction: sinon.stub(),
onGetSerializedChat: sinon.stub(),
promptInputOptionChange: sinon.stub(),
promptInputButtonClick: sinon.stub(),
stopChatResponse: sinon.stub(),
sendButtonClickEvent: sinon.stub(),
onOpenSettings: sinon.stub(),
Expand Down
115 changes: 114 additions & 1 deletion chat-client/src/client/mynahUi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ import {
import { ChatHistory, ChatHistoryList } from './features/history'
import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming'
import { getModelSelectionChatItem } from './texts/modelSelection'
import {
freeTierLimitSticky,
upgradeSuccessSticky,
upgradePendingSticky,
plansAndPricingTitle,
freeTierLimitDirective,
} from './texts/paidTier'

export interface InboundChatApi {
addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void
Expand Down Expand Up @@ -458,7 +465,13 @@ export const createMynahUi = (
messager.onCreatePrompt(action.formItemValues![ContextPrompt.PromptNameFieldId])
}
},
onFormTextualItemKeyPress: (event: KeyboardEvent, formData: Record<string, string>, itemId: string) => {
onFormTextualItemKeyPress: (
event: KeyboardEvent,
formData: Record<string, string>,
itemId: string,
_tabId: string,
_eventId?: string
) => {
if (itemId === ContextPrompt.PromptNameFieldId && event.key === 'Enter') {
event.preventDefault()
messager.onCreatePrompt(formData[ContextPrompt.PromptNameFieldId])
Expand Down Expand Up @@ -488,6 +501,14 @@ export const createMynahUi = (
}
messager.onPromptInputOptionChange({ tabId, optionsValues })
},
onPromptInputButtonClick: (tabId, buttonId, eventId) => {
const payload: ButtonClickParams = {
tabId,
messageId: 'not-a-message',
buttonId: buttonId,
}
messager.onPromptInputButtonClick(payload)
},
onMessageDismiss: (tabId, messageId) => {
if (messageId === programmerModeCard.messageId) {
programmingModeCardActive = false
Expand Down Expand Up @@ -836,7 +857,99 @@ export const createMynahUi = (
})
}

/**
* Adjusts the UI when the user changes to/from free-tier/paid-tier.
* Shows a message if the user reaches free-tier limit.
* Shows a message if the user just upgraded to paid-tier.
*/
const onPaidTierModeChange = (tabId: string, mode: string | undefined) => {
if (!mode || !['freetier', 'freetier-limit', 'upgrade-pending', 'paidtier'].includes(mode)) {
return false // invalid mode
}

tabId = !!tabId ? tabId : getOrCreateTabId()!
const store = mynahUi.getTabData(tabId).getStore() || {}

// Detect if the tab is already showing the "Upgrade Q" UI.
const isFreeTierLimitUi = store.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
const isUpgradePendingUi = store.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
const isPlansAndPricingTab = plansAndPricingTitle === store.tabTitle

if (mode === 'freetier-limit') {
mynahUi.updateStore(tabId, {
promptInputStickyCard: freeTierLimitSticky,
})

if (!isFreeTierLimitUi) {
// TODO: how to set a warning icon on the user's failed prompt?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a launch blocking TODO?

Copy link
Contributor Author

@justinmk3 justinmk3 Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cosmetic issue that will need to be done as a followup. May require changes to mynah-ui.

//
// const chatItems = store.chatItems ?? []
// const lastPrompt = chatItems.filter(ci => ci.type === ChatItemType.PROMPT).at(-1)
// for (const c of chatItems) {
// c.body = 'xxx / ' + c.type
// c.icon = 'warning'
// c.iconStatus = 'warning'
// c.status = 'warning'
// }
//
// if (lastPrompt && lastPrompt.messageId) {
// lastPrompt.icon = 'warning'
// lastPrompt.iconStatus = 'warning'
// lastPrompt.status = 'warning'
//
// // Decorate the failed prompt with a warning icon.
// // mynahUi.updateChatAnswerWithMessageId(tabId, lastPrompt.messageId, lastPrompt)
// }
//
// mynahUi.updateStore(tabId, {
// chatItems: chatItems,
// })
} else {
// Show directive only on 2nd chat attempt, not the initial attempt.
mynahUi.addChatItem(tabId, freeTierLimitDirective)
}
} else if (mode === 'upgrade-pending') {
// Change the sticky banner to show a progress spinner.
const card: typeof freeTierLimitSticky = {
...(isFreeTierLimitUi ? freeTierLimitSticky : upgradePendingSticky),
icon: 'progress',
}
mynahUi.updateStore(tabId, {
// Show a progress ribbon.
promptInputVisible: true,
promptInputStickyCard: isFreeTierLimitUi ? card : null,
})
} else if (mode === 'paidtier') {
mynahUi.updateStore(tabId, {
promptInputStickyCard: null,
promptInputVisible: !isPlansAndPricingTab,
})
if (isFreeTierLimitUi || isUpgradePendingUi || isPlansAndPricingTab) {
// Transitioning from 'upgrade-pending' to upgrade success.
const card: typeof upgradeSuccessSticky = {
...upgradeSuccessSticky,
canBeDismissed: !isPlansAndPricingTab,
}
mynahUi.updateStore(tabId, {
promptInputStickyCard: card,
})
}
}

mynahUi.updateStore(tabId, {
// promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
// promptInputDisabledState: mode === 'freetier-limit',
})

return true
}

const updateChat = (params: ChatUpdateParams) => {
// HACK: Special field sent by `agenticChatController.ts:setPaidTierMode()`.
if (onPaidTierModeChange(params.tabId, (params as any).paidTierMode as string)) {
return
}

const isChatLoading = params.state?.inProgress
mynahUi.updateStore(params.tabId, {
loadingChat: isChatLoading,
Expand Down
202 changes: 202 additions & 0 deletions chat-client/src/client/texts/paidTier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { ChatItem, ChatItemButton, ChatItemFormItem, ChatItemType, TextBasedFormItem } from '@aws/mynah-ui'

export const plansAndPricingTitle = 'Plans &amp; Pricing'
export const paidTierLearnMoreUrl = 'https://aws.amazon.com/q/pricing/'
export const qProName = 'Q Developer Pro'

export const upgradeQButton: ChatItemButton = {
id: 'paidtier-upgrade-q',
flash: 'once',
fillState: 'always',
position: 'inside',
icon: 'external',
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
// icon: MynahIcons.Q,
description: `Upgrade to ${qProName}`,
text: `Subscribe to ${qProName}`,
status: 'primary',
disabled: false,
}

export const learnMoreButton: ChatItemButton = {
id: 'paidtier-upgrade-q-learnmore',
fillState: 'hover',
// position: 'inside',
icon: 'external',
description: `Learn about ${qProName}`,
text: 'Learn more',
status: 'info',
disabled: false,
}

export const continueUpgradeQButton: ChatItemButton = {
id: 'paidtier-upgrade-q-continue',
icon: 'rocket',
flash: 'once',
fillState: 'hover',
position: 'inside',
// description: `Link an AWS account to upgrade ${qProName}`,
text: 'Continue',
disabled: false,
}

export const freeTierLimitCard: ChatItem = {
type: ChatItemType.ANSWER,
// Note: starts with a non-breaking space to workaround https://github.com/aws/mynah-ui/issues/349
title: '  Monthly request limit reached',
messageId: 'freetier-limit',
status: 'warning',
buttons: [],
icon: 'warning',
// iconStatus: 'success',
header: {
icon: 'warning',
iconStatus: 'warning',
body: `Upgrade to ${qProName}`,
},
canBeDismissed: false,
fullWidth: true,
body: `To increase your limit, subscribe to ${qProName}. During the upgrade, you'll be asked to link your Builder ID to the AWS account that will be billed the monthly subscription fee. Learn more about [pricing &gt;](${paidTierLearnMoreUrl})`,
}

export const freeTierLimitDirective: ChatItem = {
type: ChatItemType.DIRECTIVE,
// title: '...',
// header: { },
messageId: 'freetier-limit-directive',
fullWidth: true,
contentHorizontalAlignment: 'center',
canBeDismissed: false,
body: 'Unable to send. Monthly invocation limit met for this month.',
}

/** "Banner" (sticky card) shown above the chat prompt. */
export const freeTierLimitSticky: Partial<ChatItem> = {
messageId: 'freetier-limit-banner',
title: freeTierLimitCard.title,
body: freeTierLimitCard.body,
buttons: [upgradeQButton],
canBeDismissed: false,
icon: 'warning',
// iconStatus: 'warning',
}

export const upgradePendingSticky: Partial<ChatItem> = {
messageId: 'upgrade-pending-banner',
// Note: starts with a non-breaking space to workaround https://github.com/aws/mynah-ui/issues/349
body: '  Waiting for subscription status...',
status: 'info',
buttons: [],
canBeDismissed: true,
icon: 'progress',
// iconStatus: 'info',
}

export const upgradeSuccessSticky: Partial<ChatItem> = {
messageId: 'upgrade-success-banner',
// body: `Successfully upgraded to ${qProName}.`,
status: 'success',
buttons: [],
// icon: 'q',
// iconStatus: 'success',
header: {
icon: 'ok-circled',
iconStatus: 'success',
body: `Successfully upgraded to ${qProName}.`,
// status: {
// status: 'success',
// position: 'right',
// text: `Successfully upgraded to ${qProName}.`,
// },
},
canBeDismissed: true,
}

export const paidTierInfoCard: ChatItem = {
type: ChatItemType.ANSWER,
title: 'UPGRADE TO AMAZON Q PRO',
buttons: [upgradeQButton],
header: {
icon: 'q',
iconStatus: 'primary',
body: `This feature requires a subscription to ${qProName}.`,
status: {
status: 'info',
icon: 'q',
},
},
body: `Upgrade to ${qProName}. [Learn More...](${paidTierLearnMoreUrl})`,
messageId: 'paidtier-info',
fullWidth: true,
canBeDismissed: true,
snapToTop: true,
}

export const paidTierSuccessCard: ChatItem = {
type: ChatItemType.ANSWER,
title: 'UPGRADED TO AMAZON Q PRO',
header: {
icon: 'q',
iconStatus: 'primary',
body: `Welcome to ${qProName}`,
status: {
status: 'success',
icon: 'q',
text: 'Success',
},
},
messageId: 'paidtier-success',
fullWidth: true,
canBeDismissed: true,
body: `Upgraded to ${qProName}\n\n[Learn More...](${paidTierLearnMoreUrl})`,
snapToTop: true,
}

export const paidTierPromptInput: TextBasedFormItem = {
placeholder: '111111111111',
type: 'textinput',
id: 'paid-tier',
tooltip: `Upgrade to ${qProName}`,
value: 'true',
icon: 'magic',
}

export const paidTierStep0: ChatItem = {
type: ChatItemType.DIRECTIVE,
body: `You have upgraded to ${qProName}`,
}

export const paidTierStep1: ChatItem = {
type: ChatItemType.DIRECTIVE,
body: `You have upgraded to ${qProName}`,
}

/** "Upgrade Q" form with a "AWS account id" user-input textbox. */
export const paidTierUpgradeForm: ChatItem = {
type: ChatItemType.ANSWER,
status: 'info',
fullWidth: true,
// title: 'Connect AWS account and upgrade',
body: `
# Connect AWS account and upgrade

Provide your AWS account number to enable your ${qProName} subscription. Upon confirming the subscription, your AWS account will begin to be charged.

[Learn More...](${paidTierLearnMoreUrl})
`,
formItems: [
{
id: 'awsAccountId',
type: 'textinput',
title: 'AWS account ID',
description: '12-digit AWS account ID',
// tooltip: `Link an AWS account to upgrade to ${qProName}`,
validationPatterns: {
patterns: [{ pattern: '[0-9]{12}', errorMessage: 'Must be a valid 12-digit AWS account ID' }],
},
},
],
buttons: [continueUpgradeQButton],
snapToTop: true,
}
1 change: 1 addition & 0 deletions chat-client/src/client/withAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const withAdapter = (
onChatPromptProgressActionButtonClicked: addDefaultRouting('onChatPromptProgressActionButtonClicked'),
onTabbedContentTabChange: addDefaultRouting('onTabbedContentTabChange'),
onPromptInputOptionChange: addDefaultRouting('onPromptInputOptionChange'),
onPromptInputButtonClick: addDefaultRouting('onPromptInputButtonClick'),
onMessageDismiss: addDefaultRouting('onMessageDismiss'),

/**
Expand Down
Loading
Loading