Skip to content

Commit 4d7efa4

Browse files
committed
feat(paidtier): Chat UI gets AWS Account ID from user
1 parent 675121b commit 4d7efa4

File tree

11 files changed

+559
-131
lines changed

11 files changed

+559
-131
lines changed

chat-client/src/client/mynahUi.ts

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,15 @@ import {
5757
} from './utils'
5858
import { ChatHistory, ChatHistoryList } from './features/history'
5959
import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming'
60-
import { paidTierSuccessCard, freeTierLimitReachedCard, upgradeQButton } from './texts/paidTier'
60+
import {
61+
paidTierInfoCard,
62+
paidTierUpgradeForm,
63+
freeTierLimitSticky,
64+
continueUpgradeQButton,
65+
upgradeSuccessSticky,
66+
upgradePendingSticky,
67+
plansAndPricingTitle,
68+
} from './texts/paidTier'
6169

6270
export interface InboundChatApi {
6371
addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void
@@ -87,6 +95,31 @@ const getTabPairProgrammingMode = (mynahUi: MynahUI, tabId: string) => {
8795
return promptInputOptions.find(item => item.id === 'pair-programmer-mode')?.value === 'true'
8896
}
8997

98+
/** When user provides AWS account by clicking "Continue" or hitting Enter key. */
99+
function onLinkAwsAccountId(
100+
tabId: string,
101+
messageId: string,
102+
messager: Messager,
103+
action: { id: string; text?: string; formData?: Record<string, string> }
104+
) {
105+
const awsAccountId = action.formData?.['awsAccountId']
106+
if (!awsAccountId) {
107+
return false
108+
// throw new Error(`onInBodyButtonClicked: ${continueUpgradeQButton.id} button did not provide awsAccountId`)
109+
}
110+
// HACK: emit "followUp" to send form data "outbound".
111+
const payload: FollowUpClickParams = {
112+
tabId,
113+
messageId,
114+
followUp: {
115+
pillText: awsAccountId,
116+
type: 'awsAccountId',
117+
},
118+
}
119+
messager.onFollowUpClicked(payload)
120+
return true
121+
}
122+
90123
export const handlePromptInputChange = (mynahUi: MynahUI, tabId: string, optionsValues: Record<string, string>) => {
91124
const promptTypeValue = optionsValues['pair-programmer-mode']
92125

@@ -374,7 +407,9 @@ export const createMynahUi = (
374407
messager.onInfoLinkClick(payload)
375408
},
376409
onInBodyButtonClicked: (tabId, messageId, action, eventId) => {
377-
if (action.id === disclaimerAcknowledgeButtonId) {
410+
if (action.id === continueUpgradeQButton.id) {
411+
onLinkAwsAccountId(tabId, messageId, messager, { id: action.id, formData: action.formItemValues })
412+
} else if (action.id === disclaimerAcknowledgeButtonId) {
378413
// Hide the legal disclaimer card
379414
disclaimerCardActive = false
380415

@@ -448,11 +483,20 @@ export const createMynahUi = (
448483
messager.onCreatePrompt(action.formItemValues![ContextPrompt.PromptNameFieldId])
449484
}
450485
},
451-
onFormTextualItemKeyPress: (event: KeyboardEvent, formData: Record<string, string>, itemId: string) => {
486+
onFormTextualItemKeyPress: (
487+
event: KeyboardEvent,
488+
formData: Record<string, string>,
489+
itemId: string,
490+
tabId: string,
491+
eventId?: string
492+
) => {
452493
if (itemId === ContextPrompt.PromptNameFieldId && event.key === 'Enter') {
453494
event.preventDefault()
454495
messager.onCreatePrompt(formData[ContextPrompt.PromptNameFieldId])
455496
return true
497+
} else if (itemId === 'awsAccountId' && event.key === 'Enter') {
498+
event.preventDefault()
499+
return onLinkAwsAccountId(tabId, '', messager, { id: continueUpgradeQButton.id, formData: formData })
456500
}
457501
return false
458502
},
@@ -839,34 +883,102 @@ export const createMynahUi = (
839883
* Shows a message if the user reaches free-tier limit.
840884
* Shows a message if the user just upgraded to paid-tier.
841885
*/
842-
const onPaidTierModeChange = (
843-
tabId: string,
844-
mode: 'paidtier' | 'paidtier-success' | 'freetier' | 'freetier-limit'
845-
) => {
846-
if (!['paidtier', 'paidtier-success', 'freetier', 'freetier-limit'].includes(mode)) {
847-
return // invalid mode
886+
const onPaidTierModeChange = (tabId: string, mode: string | undefined) => {
887+
if (
888+
!mode ||
889+
![
890+
'freetier',
891+
'freetier-limit',
892+
'freetier-upgrade-info',
893+
'upgrade-start',
894+
'upgrade-pending',
895+
'paidtier',
896+
].includes(mode)
897+
) {
898+
return false // invalid mode
848899
}
849900

850-
tabId = tabId !== '' ? tabId : getOrCreateTabId()!
901+
tabId = !!tabId ? tabId : getOrCreateTabId()!
902+
903+
// Detect if the tab is already showing the "Upgrade Q" UI.
904+
const isFreeTierLimitUi =
905+
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
906+
const isUpgradePendingUi =
907+
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
908+
const isPlansAndPricingTab = plansAndPricingTitle === mynahUi.getTabData(tabId).getStore()?.tabTitle
909+
910+
if (mode === 'freetier-limit') {
911+
mynahUi.updateStore(tabId, {
912+
promptInputStickyCard: freeTierLimitSticky,
913+
})
851914

852-
// Detect if the tab is already showing the "Upgrade Q" calls-to-action.
853-
const didShowLimitReached = mynahUi.getTabData(tabId)?.getStore()?.promptInputButtons?.[0] === upgradeQButton
854-
if (mode === 'freetier-limit' && !didShowLimitReached) {
855-
mynahUi.addChatItem(tabId, freeTierLimitReachedCard)
856-
} else if (mode === 'paidtier-success') {
857-
mynahUi.addChatItem(tabId, paidTierSuccessCard)
915+
if (!isFreeTierLimitUi) {
916+
// Avoid duplicate "limit reached" cards.
917+
// REMOVED: don't want the "card", just use the "banner" only.
918+
// mynahUi.addChatItem(tabId, freeTierLimitCard)
919+
}
920+
} else if (mode === 'freetier-upgrade-info') {
921+
mynahUi.addChatItem(tabId, paidTierInfoCard)
922+
} else if (mode === 'upgrade-start') {
923+
// Show the "Upgrade" form in its own tab.
924+
const newTabId = createTabId() ?? tabId
925+
mynahUi.updateStore(newTabId, {
926+
tabTitle: plansAndPricingTitle,
927+
chatItems: [], // Clear the tab.
928+
promptInputDisabledState: true, // This special tab is not a "chat" tab.
929+
promptInputButtons: [],
930+
promptInputOptions: [],
931+
promptInputPlaceholder: '',
932+
promptInputVisible: false,
933+
})
934+
mynahUi.addChatItem(newTabId, paidTierUpgradeForm)
935+
// openTab('upgrade-start', { tabId: 'upgrade-start' })
936+
} else if (mode === 'upgrade-pending') {
937+
// Change the sticky banner to show a progress spinner.
938+
const card: typeof freeTierLimitSticky = {
939+
...(isFreeTierLimitUi ? freeTierLimitSticky : upgradePendingSticky),
940+
icon: 'progress',
941+
}
942+
mynahUi.updateStore(tabId, {
943+
// Show a progress ribbon.
944+
promptInputVisible: true,
945+
promptInputProgress: {
946+
status: 'default',
947+
text: 'Waiting for subscription status...',
948+
value: -1, // infinite
949+
// valueText: 'Waiting 2...',
950+
},
951+
promptInputStickyCard: isFreeTierLimitUi ? card : null,
952+
})
953+
} else if (mode === 'paidtier') {
954+
mynahUi.updateStore(tabId, {
955+
promptInputStickyCard: null,
956+
promptInputProgress: null,
957+
promptInputVisible: !isPlansAndPricingTab,
958+
})
959+
if (isFreeTierLimitUi || isUpgradePendingUi || isPlansAndPricingTab) {
960+
// Transitioning from 'upgrade-pending' to upgrade success.
961+
const card: typeof upgradeSuccessSticky = {
962+
...upgradeSuccessSticky,
963+
canBeDismissed: !isPlansAndPricingTab,
964+
}
965+
mynahUi.updateStore(tabId, {
966+
promptInputStickyCard: card,
967+
})
968+
}
858969
}
859970

860971
mynahUi.updateStore(tabId, {
861-
promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
862-
promptInputDisabledState: mode === 'freetier-limit',
972+
// promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
973+
// promptInputDisabledState: mode === 'freetier-limit',
863974
})
975+
976+
return true
864977
}
865978

866979
const updateChat = (params: ChatUpdateParams) => {
867980
// HACK: Special field sent by `agenticChatController.ts:setPaidTierMode()`.
868-
if ((params as any).paidTierMode) {
869-
onPaidTierModeChange(params.tabId, (params as any).paidTierMode as any)
981+
if (onPaidTierModeChange(params.tabId, (params as any).paidTierMode as string)) {
870982
return
871983
}
872984

chat-client/src/client/texts/paidTier.ts

Lines changed: 110 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
11
import { ChatItem, ChatItemButton, ChatItemFormItem, ChatItemType, TextBasedFormItem } from '@aws/mynah-ui'
22

3-
export const freeTierLimitReachedCard: ChatItem = {
3+
export const plansAndPricingTitle = 'Plans &amp; Pricing'
4+
5+
export const upgradeQButton: ChatItemButton = {
6+
flash: 'once',
7+
fillState: 'hover',
8+
position: 'inside',
9+
id: 'paidtier-upgrade-q',
10+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
11+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
12+
// icon: MynahIcons.Q,
13+
description: 'Upgrade to Amazon Q Pro',
14+
text: 'Upgrade Q',
15+
status: 'info',
16+
disabled: false,
17+
}
18+
19+
export const continueUpgradeQButton: ChatItemButton = {
20+
id: 'paidtier-upgrade-q-continue',
21+
icon: 'rocket',
22+
flash: 'once',
23+
fillState: 'hover',
24+
position: 'inside',
25+
// description: 'Link an AWS account to upgrade Amazon Q',
26+
text: 'Continue',
27+
disabled: false,
28+
}
29+
30+
export const freeTierLimitCard: ChatItem = {
431
type: ChatItemType.ANSWER,
532
title: 'FREE TIER LIMIT REACHED',
633
header: {
@@ -14,6 +41,62 @@ export const freeTierLimitReachedCard: ChatItem = {
1441
body: 'You have reached the free tier limit. Upgrade to Amazon Q Pro.\n\n[Learn More...](https://aws.amazon.com/q/pricing/)',
1542
}
1643

44+
/** "Banner" (sticky card) shown above the chat prompt. */
45+
export const freeTierLimitSticky: Partial<ChatItem> = {
46+
messageId: 'freetier-limit-banner',
47+
title: 'FREE TIER LIMIT REACHED',
48+
body: "You've reached your invocation limit for this month. Upgrade to Amazon Q Pro. [Learn More...](https://aws.amazon.com/q/pricing/)",
49+
buttons: [upgradeQButton],
50+
canBeDismissed: false,
51+
}
52+
53+
export const upgradePendingSticky: Partial<ChatItem> = {
54+
messageId: 'upgrade-pending-banner',
55+
body: 'Waiting for subscription status...',
56+
status: 'info',
57+
buttons: [],
58+
canBeDismissed: true,
59+
}
60+
61+
export const upgradeSuccessSticky: Partial<ChatItem> = {
62+
messageId: 'upgrade-success-banner',
63+
// body: 'Successfully upgraded to Amazon Q Pro.',
64+
status: 'success',
65+
buttons: [],
66+
// icon: 'q',
67+
// iconStatus: 'success',
68+
header: {
69+
icon: 'ok-circled',
70+
iconStatus: 'success',
71+
body: 'Successfully upgraded to Amazon Q Pro.',
72+
// status: {
73+
// status: 'success',
74+
// position: 'right',
75+
// text: 'Successfully upgraded to Amazon Q Pro.',
76+
// },
77+
},
78+
canBeDismissed: true,
79+
}
80+
81+
export const paidTierInfoCard: ChatItem = {
82+
type: ChatItemType.ANSWER,
83+
title: 'UPGRADE TO AMAZON Q PRO',
84+
buttons: [upgradeQButton],
85+
header: {
86+
icon: 'q',
87+
iconStatus: 'primary',
88+
body: 'This feature requires a subscription to Amazon Q Pro.',
89+
status: {
90+
status: 'info',
91+
icon: 'q',
92+
},
93+
},
94+
body: 'Upgrade to Amazon Q Pro. [Learn More...](https://aws.amazon.com/q/pricing/)',
95+
messageId: 'paidtier-info',
96+
fullWidth: true,
97+
canBeDismissed: true,
98+
}
99+
17100
export const paidTierSuccessCard: ChatItem = {
18101
type: ChatItemType.ANSWER,
19102
title: 'UPGRADED TO AMAZON Q PRO',
@@ -52,15 +135,31 @@ export const paidTierStep1: ChatItem = {
52135
body: 'You have upgraded to Amazon Q Pro',
53136
}
54137

55-
export const upgradeQButton: ChatItemButton = {
56-
flash: 'once',
57-
fillState: 'hover',
58-
position: 'outside',
59-
id: 'upgrade-q',
60-
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
61-
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
62-
// icon: MynahIcons.Q,
63-
description: 'Upgrade to Amazon Q Pro',
64-
text: 'Upgrade Q',
138+
/** "Upgrade Q" form with a "AWS account id" user-input textbox. */
139+
export const paidTierUpgradeForm: ChatItem = {
140+
type: ChatItemType.ANSWER,
65141
status: 'info',
142+
fullWidth: true,
143+
// title: 'Connect AWS account and upgrade',
144+
body: `
145+
# Connect AWS account and upgrade
146+
147+
Provide your AWS account number to enable your Pro subscription. Upon confirming the subscription, your AWS account will begin to be charged.
148+
149+
[Learn More...](https://aws.amazon.com/q/)
150+
`,
151+
formItems: [
152+
{
153+
id: 'awsAccountId',
154+
type: 'textinput',
155+
title: 'AWS account ID',
156+
description: '12-digit AWS account ID',
157+
// tooltip: 'Link an AWS account to upgrade to Amazon Q Pro',
158+
validationPatterns: {
159+
patterns: [{ pattern: '[0-9]{12}', errorMessage: 'Must be a valid 12-digit AWS account ID' }],
160+
},
161+
},
162+
],
163+
buttons: [continueUpgradeQButton],
164+
snapToTop: true,
66165
}

0 commit comments

Comments
 (0)