Skip to content

Commit 82be5b1

Browse files
committed
feat(paidtier): Chat UI gets AWS Account ID from user
1 parent f37abdd commit 82be5b1

File tree

11 files changed

+561
-131
lines changed

11 files changed

+561
-131
lines changed

chat-client/src/client/mynahUi.ts

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

6371
export interface InboundChatApi {
6472
addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void
@@ -94,6 +102,31 @@ const getTabPairProgrammingMode = (mynahUi: MynahUI, tabId: string) =>
94102
const getTabModelSelection = (mynahUi: MynahUI, tabId: string) =>
95103
getTabPromptInputValue(mynahUi, tabId, 'model-selection')
96104

105+
/** When user provides AWS account by clicking "Continue" or hitting Enter key. */
106+
function onLinkAwsAccountId(
107+
tabId: string,
108+
messageId: string,
109+
messager: Messager,
110+
action: { id: string; text?: string; formData?: Record<string, string> }
111+
) {
112+
const awsAccountId = action.formData?.['awsAccountId']
113+
if (!awsAccountId) {
114+
return false
115+
// throw new Error(`onInBodyButtonClicked: ${continueUpgradeQButton.id} button did not provide awsAccountId`)
116+
}
117+
// HACK: emit "followUp" to send form data "outbound".
118+
const payload: FollowUpClickParams = {
119+
tabId,
120+
messageId,
121+
followUp: {
122+
pillText: awsAccountId,
123+
type: 'awsAccountId',
124+
},
125+
}
126+
messager.onFollowUpClicked(payload)
127+
return true
128+
}
129+
97130
export const handlePromptInputChange = (mynahUi: MynahUI, tabId: string, optionsValues: Record<string, string>) => {
98131
const previousPairProgrammerValue = getTabPairProgrammingMode(mynahUi, tabId)
99132
const currentPairProgrammerValue = optionsValues['pair-programmer-mode'] === 'true'
@@ -385,7 +418,9 @@ export const createMynahUi = (
385418
messager.onInfoLinkClick(payload)
386419
},
387420
onInBodyButtonClicked: (tabId, messageId, action, eventId) => {
388-
if (action.id === disclaimerAcknowledgeButtonId) {
421+
if (action.id === continueUpgradeQButton.id) {
422+
onLinkAwsAccountId(tabId, messageId, messager, { id: action.id, formData: action.formItemValues })
423+
} else if (action.id === disclaimerAcknowledgeButtonId) {
389424
// Hide the legal disclaimer card
390425
disclaimerCardActive = false
391426

@@ -459,11 +494,20 @@ export const createMynahUi = (
459494
messager.onCreatePrompt(action.formItemValues![ContextPrompt.PromptNameFieldId])
460495
}
461496
},
462-
onFormTextualItemKeyPress: (event: KeyboardEvent, formData: Record<string, string>, itemId: string) => {
497+
onFormTextualItemKeyPress: (
498+
event: KeyboardEvent,
499+
formData: Record<string, string>,
500+
itemId: string,
501+
tabId: string,
502+
eventId?: string
503+
) => {
463504
if (itemId === ContextPrompt.PromptNameFieldId && event.key === 'Enter') {
464505
event.preventDefault()
465506
messager.onCreatePrompt(formData[ContextPrompt.PromptNameFieldId])
466507
return true
508+
} else if (itemId === 'awsAccountId' && event.key === 'Enter') {
509+
event.preventDefault()
510+
return onLinkAwsAccountId(tabId, '', messager, { id: continueUpgradeQButton.id, formData: formData })
467511
}
468512
return false
469513
},
@@ -850,34 +894,102 @@ export const createMynahUi = (
850894
* Shows a message if the user reaches free-tier limit.
851895
* Shows a message if the user just upgraded to paid-tier.
852896
*/
853-
const onPaidTierModeChange = (
854-
tabId: string,
855-
mode: 'paidtier' | 'paidtier-success' | 'freetier' | 'freetier-limit'
856-
) => {
857-
if (!['paidtier', 'paidtier-success', 'freetier', 'freetier-limit'].includes(mode)) {
858-
return // invalid mode
897+
const onPaidTierModeChange = (tabId: string, mode: string | undefined) => {
898+
if (
899+
!mode ||
900+
![
901+
'freetier',
902+
'freetier-limit',
903+
'freetier-upgrade-info',
904+
'upgrade-start',
905+
'upgrade-pending',
906+
'paidtier',
907+
].includes(mode)
908+
) {
909+
return false // invalid mode
859910
}
860911

861-
tabId = tabId !== '' ? tabId : getOrCreateTabId()!
912+
tabId = !!tabId ? tabId : getOrCreateTabId()!
913+
914+
// Detect if the tab is already showing the "Upgrade Q" UI.
915+
const isFreeTierLimitUi =
916+
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
917+
const isUpgradePendingUi =
918+
mynahUi.getTabData(tabId)?.getStore()?.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
919+
const isPlansAndPricingTab = plansAndPricingTitle === mynahUi.getTabData(tabId).getStore()?.tabTitle
920+
921+
if (mode === 'freetier-limit') {
922+
mynahUi.updateStore(tabId, {
923+
promptInputStickyCard: freeTierLimitSticky,
924+
})
862925

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

871982
mynahUi.updateStore(tabId, {
872-
promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
873-
promptInputDisabledState: mode === 'freetier-limit',
983+
// promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
984+
// promptInputDisabledState: mode === 'freetier-limit',
874985
})
986+
987+
return true
875988
}
876989

877990
const updateChat = (params: ChatUpdateParams) => {
878991
// HACK: Special field sent by `agenticChatController.ts:setPaidTierMode()`.
879-
if ((params as any).paidTierMode) {
880-
onPaidTierModeChange(params.tabId, (params as any).paidTierMode as any)
992+
if (onPaidTierModeChange(params.tabId, (params as any).paidTierMode as string)) {
881993
return
882994
}
883995

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

Lines changed: 112 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,63 @@ 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+
snapToTop: true,
99+
}
100+
17101
export const paidTierSuccessCard: ChatItem = {
18102
type: ChatItemType.ANSWER,
19103
title: 'UPGRADED TO AMAZON Q PRO',
@@ -31,6 +115,7 @@ export const paidTierSuccessCard: ChatItem = {
31115
fullWidth: true,
32116
canBeDismissed: true,
33117
body: 'Upgraded to Amazon Q Pro\n\n[Learn More...](https://aws.amazon.com/q/)',
118+
snapToTop: true,
34119
}
35120

36121
export const paidTierPromptInput: TextBasedFormItem = {
@@ -52,15 +137,31 @@ export const paidTierStep1: ChatItem = {
52137
body: 'You have upgraded to Amazon Q Pro',
53138
}
54139

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',
140+
/** "Upgrade Q" form with a "AWS account id" user-input textbox. */
141+
export const paidTierUpgradeForm: ChatItem = {
142+
type: ChatItemType.ANSWER,
65143
status: 'info',
144+
fullWidth: true,
145+
// title: 'Connect AWS account and upgrade',
146+
body: `
147+
# Connect AWS account and upgrade
148+
149+
Provide your AWS account number to enable your Pro subscription. Upon confirming the subscription, your AWS account will begin to be charged.
150+
151+
[Learn More...](https://aws.amazon.com/q/)
152+
`,
153+
formItems: [
154+
{
155+
id: 'awsAccountId',
156+
type: 'textinput',
157+
title: 'AWS account ID',
158+
description: '12-digit AWS account ID',
159+
// tooltip: 'Link an AWS account to upgrade to Amazon Q Pro',
160+
validationPatterns: {
161+
patterns: [{ pattern: '[0-9]{12}', errorMessage: 'Must be a valid 12-digit AWS account ID' }],
162+
},
163+
},
164+
],
165+
buttons: [continueUpgradeQButton],
166+
snapToTop: true,
66167
}

0 commit comments

Comments
 (0)