Skip to content

Commit d25bcb6

Browse files
authored
feat(q): builderid "paid tier" #1197
* build: update service model for bearer-token-service.json 1. get latest service models: - `client/token/bearer-token-service.json` - from: `<AWSVectorConsolasRuntimeServiceModel>/aws-sdk-external-2022-11-11/c2j/codewhispererruntime-2022-11-11.json` - `client/sigv4/service.json` - from: `<AWSVectorConsolasRuntimeServiceModel>/aws-sdk-external/c2j/codewhisperer-2024-10-25.json` 2. !!NOTE!! remove line `"signatureVersion": "bearer",` from `bearer-token-service.json` 3. restore the script that was mysteriously reverted in #161 without any documented alternative... 4. run `./node_modules/.bin/ts-node server/aws-lsp-codewhisperer/script/genclient.ts` service model version: 1.0.20901.0 * feat(paidtier): detect Free Tier, show "Upgrade" button - control MynahUI from Flare server * feat(paidtier): "/manage" quickaction * feat(paidtier): Chat UI gets AWS Account ID from user * fix(paidtier): ux changes * fix(paidtier): skip for non-builderid user * fix(paidtier): ux changes: Chat UI does *not* get AWS Account ID * fix(paidtier): ux changes
1 parent cf79a8c commit d25bcb6

22 files changed

+4473
-646
lines changed

chat-client/src/client/chat.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ export const createChat = (
390390
promptInputOptionChange: (params: PromptInputOptionChangeParams) => {
391391
sendMessageToClient({ command: PROMPT_INPUT_OPTION_CHANGE_METHOD, params })
392392
},
393+
promptInputButtonClick: params => {
394+
// TODO
395+
sendMessageToClient({ command: BUTTON_CLICK_REQUEST_METHOD, params })
396+
},
393397
stopChatResponse: (tabId: string) => {
394398
sendMessageToClient({ command: STOP_CHAT_RESPONSE, params: { tabId } })
395399
},

chat-client/src/client/messager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export interface OutboundChatApi {
9292
tabBarAction(params: TabBarActionParams): void
9393
onGetSerializedChat(requestId: string, result: GetSerializedChatResult | ErrorResult): void
9494
promptInputOptionChange(params: PromptInputOptionChangeParams): void
95+
promptInputButtonClick(params: ButtonClickParams): void
9596
stopChatResponse(tabId: string): void
9697
sendButtonClickEvent(params: ButtonClickParams): void
9798
onOpenSettings(settingKey: string): void
@@ -229,6 +230,10 @@ export class Messager {
229230
this.chatApi.promptInputOptionChange(params)
230231
}
231232

233+
onPromptInputButtonClick = (params: ButtonClickParams): void => {
234+
this.chatApi.promptInputButtonClick(params)
235+
}
236+
232237
onStopChatResponse = (tabId: string): void => {
233238
this.chatApi.stopChatResponse(tabId)
234239
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('MynahUI', () => {
6464
tabBarAction: sinon.stub(),
6565
onGetSerializedChat: sinon.stub(),
6666
promptInputOptionChange: sinon.stub(),
67+
promptInputButtonClick: sinon.stub(),
6768
stopChatResponse: sinon.stub(),
6869
sendButtonClickEvent: sinon.stub(),
6970
onOpenSettings: sinon.stub(),

chat-client/src/client/mynahUi.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ import {
5858
import { ChatHistory, ChatHistoryList } from './features/history'
5959
import { pairProgrammingModeOff, pairProgrammingModeOn, programmerModeCard } from './texts/pairProgramming'
6060
import { getModelSelectionChatItem } from './texts/modelSelection'
61+
import {
62+
freeTierLimitSticky,
63+
upgradeSuccessSticky,
64+
upgradePendingSticky,
65+
plansAndPricingTitle,
66+
freeTierLimitDirective,
67+
} from './texts/paidTier'
6168

6269
export interface InboundChatApi {
6370
addChatResponse(params: ChatResult, tabId: string, isPartialResult: boolean): void
@@ -458,7 +465,13 @@ export const createMynahUi = (
458465
messager.onCreatePrompt(action.formItemValues![ContextPrompt.PromptNameFieldId])
459466
}
460467
},
461-
onFormTextualItemKeyPress: (event: KeyboardEvent, formData: Record<string, string>, itemId: string) => {
468+
onFormTextualItemKeyPress: (
469+
event: KeyboardEvent,
470+
formData: Record<string, string>,
471+
itemId: string,
472+
_tabId: string,
473+
_eventId?: string
474+
) => {
462475
if (itemId === ContextPrompt.PromptNameFieldId && event.key === 'Enter') {
463476
event.preventDefault()
464477
messager.onCreatePrompt(formData[ContextPrompt.PromptNameFieldId])
@@ -488,6 +501,14 @@ export const createMynahUi = (
488501
}
489502
messager.onPromptInputOptionChange({ tabId, optionsValues })
490503
},
504+
onPromptInputButtonClick: (tabId, buttonId, eventId) => {
505+
const payload: ButtonClickParams = {
506+
tabId,
507+
messageId: 'not-a-message',
508+
buttonId: buttonId,
509+
}
510+
messager.onPromptInputButtonClick(payload)
511+
},
491512
onMessageDismiss: (tabId, messageId) => {
492513
if (messageId === programmerModeCard.messageId) {
493514
programmingModeCardActive = false
@@ -836,7 +857,99 @@ export const createMynahUi = (
836857
})
837858
}
838859

860+
/**
861+
* Adjusts the UI when the user changes to/from free-tier/paid-tier.
862+
* Shows a message if the user reaches free-tier limit.
863+
* Shows a message if the user just upgraded to paid-tier.
864+
*/
865+
const onPaidTierModeChange = (tabId: string, mode: string | undefined) => {
866+
if (!mode || !['freetier', 'freetier-limit', 'upgrade-pending', 'paidtier'].includes(mode)) {
867+
return false // invalid mode
868+
}
869+
870+
tabId = !!tabId ? tabId : getOrCreateTabId()!
871+
const store = mynahUi.getTabData(tabId).getStore() || {}
872+
873+
// Detect if the tab is already showing the "Upgrade Q" UI.
874+
const isFreeTierLimitUi = store.promptInputStickyCard?.messageId === freeTierLimitSticky.messageId
875+
const isUpgradePendingUi = store.promptInputStickyCard?.messageId === upgradePendingSticky.messageId
876+
const isPlansAndPricingTab = plansAndPricingTitle === store.tabTitle
877+
878+
if (mode === 'freetier-limit') {
879+
mynahUi.updateStore(tabId, {
880+
promptInputStickyCard: freeTierLimitSticky,
881+
})
882+
883+
if (!isFreeTierLimitUi) {
884+
// TODO: how to set a warning icon on the user's failed prompt?
885+
//
886+
// const chatItems = store.chatItems ?? []
887+
// const lastPrompt = chatItems.filter(ci => ci.type === ChatItemType.PROMPT).at(-1)
888+
// for (const c of chatItems) {
889+
// c.body = 'xxx / ' + c.type
890+
// c.icon = 'warning'
891+
// c.iconStatus = 'warning'
892+
// c.status = 'warning'
893+
// }
894+
//
895+
// if (lastPrompt && lastPrompt.messageId) {
896+
// lastPrompt.icon = 'warning'
897+
// lastPrompt.iconStatus = 'warning'
898+
// lastPrompt.status = 'warning'
899+
//
900+
// // Decorate the failed prompt with a warning icon.
901+
// // mynahUi.updateChatAnswerWithMessageId(tabId, lastPrompt.messageId, lastPrompt)
902+
// }
903+
//
904+
// mynahUi.updateStore(tabId, {
905+
// chatItems: chatItems,
906+
// })
907+
} else {
908+
// Show directive only on 2nd chat attempt, not the initial attempt.
909+
mynahUi.addChatItem(tabId, freeTierLimitDirective)
910+
}
911+
} else if (mode === 'upgrade-pending') {
912+
// Change the sticky banner to show a progress spinner.
913+
const card: typeof freeTierLimitSticky = {
914+
...(isFreeTierLimitUi ? freeTierLimitSticky : upgradePendingSticky),
915+
icon: 'progress',
916+
}
917+
mynahUi.updateStore(tabId, {
918+
// Show a progress ribbon.
919+
promptInputVisible: true,
920+
promptInputStickyCard: isFreeTierLimitUi ? card : null,
921+
})
922+
} else if (mode === 'paidtier') {
923+
mynahUi.updateStore(tabId, {
924+
promptInputStickyCard: null,
925+
promptInputVisible: !isPlansAndPricingTab,
926+
})
927+
if (isFreeTierLimitUi || isUpgradePendingUi || isPlansAndPricingTab) {
928+
// Transitioning from 'upgrade-pending' to upgrade success.
929+
const card: typeof upgradeSuccessSticky = {
930+
...upgradeSuccessSticky,
931+
canBeDismissed: !isPlansAndPricingTab,
932+
}
933+
mynahUi.updateStore(tabId, {
934+
promptInputStickyCard: card,
935+
})
936+
}
937+
}
938+
939+
mynahUi.updateStore(tabId, {
940+
// promptInputButtons: mode === 'freetier-limit' ? [upgradeQButton] : [],
941+
// promptInputDisabledState: mode === 'freetier-limit',
942+
})
943+
944+
return true
945+
}
946+
839947
const updateChat = (params: ChatUpdateParams) => {
948+
// HACK: Special field sent by `agenticChatController.ts:setPaidTierMode()`.
949+
if (onPaidTierModeChange(params.tabId, (params as any).paidTierMode as string)) {
950+
return
951+
}
952+
840953
const isChatLoading = params.state?.inProgress
841954
mynahUi.updateStore(params.tabId, {
842955
loadingChat: isChatLoading,
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { ChatItem, ChatItemButton, ChatItemFormItem, ChatItemType, TextBasedFormItem } from '@aws/mynah-ui'
2+
3+
export const plansAndPricingTitle = 'Plans &amp; Pricing'
4+
export const paidTierLearnMoreUrl = 'https://aws.amazon.com/q/pricing/'
5+
export const qProName = 'Q Developer Pro'
6+
7+
export const upgradeQButton: ChatItemButton = {
8+
id: 'paidtier-upgrade-q',
9+
flash: 'once',
10+
fillState: 'always',
11+
position: 'inside',
12+
icon: 'external',
13+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/q.svg
14+
// https://github.com/aws/mynah-ui/blob/main/src/components/icon/icons/rocket.svg
15+
// icon: MynahIcons.Q,
16+
description: `Upgrade to ${qProName}`,
17+
text: `Subscribe to ${qProName}`,
18+
status: 'primary',
19+
disabled: false,
20+
}
21+
22+
export const learnMoreButton: ChatItemButton = {
23+
id: 'paidtier-upgrade-q-learnmore',
24+
fillState: 'hover',
25+
// position: 'inside',
26+
icon: 'external',
27+
description: `Learn about ${qProName}`,
28+
text: 'Learn more',
29+
status: 'info',
30+
disabled: false,
31+
}
32+
33+
export const continueUpgradeQButton: ChatItemButton = {
34+
id: 'paidtier-upgrade-q-continue',
35+
icon: 'rocket',
36+
flash: 'once',
37+
fillState: 'hover',
38+
position: 'inside',
39+
// description: `Link an AWS account to upgrade ${qProName}`,
40+
text: 'Continue',
41+
disabled: false,
42+
}
43+
44+
export const freeTierLimitCard: ChatItem = {
45+
type: ChatItemType.ANSWER,
46+
// Note: starts with a non-breaking space to workaround https://github.com/aws/mynah-ui/issues/349
47+
title: '  Monthly request limit reached',
48+
messageId: 'freetier-limit',
49+
status: 'warning',
50+
buttons: [],
51+
icon: 'warning',
52+
// iconStatus: 'success',
53+
header: {
54+
icon: 'warning',
55+
iconStatus: 'warning',
56+
body: `Upgrade to ${qProName}`,
57+
},
58+
canBeDismissed: false,
59+
fullWidth: true,
60+
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})`,
61+
}
62+
63+
export const freeTierLimitDirective: ChatItem = {
64+
type: ChatItemType.DIRECTIVE,
65+
// title: '...',
66+
// header: { },
67+
messageId: 'freetier-limit-directive',
68+
fullWidth: true,
69+
contentHorizontalAlignment: 'center',
70+
canBeDismissed: false,
71+
body: 'Unable to send. Monthly invocation limit met for this month.',
72+
}
73+
74+
/** "Banner" (sticky card) shown above the chat prompt. */
75+
export const freeTierLimitSticky: Partial<ChatItem> = {
76+
messageId: 'freetier-limit-banner',
77+
title: freeTierLimitCard.title,
78+
body: freeTierLimitCard.body,
79+
buttons: [upgradeQButton],
80+
canBeDismissed: false,
81+
icon: 'warning',
82+
// iconStatus: 'warning',
83+
}
84+
85+
export const upgradePendingSticky: Partial<ChatItem> = {
86+
messageId: 'upgrade-pending-banner',
87+
// Note: starts with a non-breaking space to workaround https://github.com/aws/mynah-ui/issues/349
88+
body: '  Waiting for subscription status...',
89+
status: 'info',
90+
buttons: [],
91+
canBeDismissed: true,
92+
icon: 'progress',
93+
// iconStatus: 'info',
94+
}
95+
96+
export const upgradeSuccessSticky: Partial<ChatItem> = {
97+
messageId: 'upgrade-success-banner',
98+
// body: `Successfully upgraded to ${qProName}.`,
99+
status: 'success',
100+
buttons: [],
101+
// icon: 'q',
102+
// iconStatus: 'success',
103+
header: {
104+
icon: 'ok-circled',
105+
iconStatus: 'success',
106+
body: `Successfully upgraded to ${qProName}.`,
107+
// status: {
108+
// status: 'success',
109+
// position: 'right',
110+
// text: `Successfully upgraded to ${qProName}.`,
111+
// },
112+
},
113+
canBeDismissed: true,
114+
}
115+
116+
export const paidTierInfoCard: ChatItem = {
117+
type: ChatItemType.ANSWER,
118+
title: 'UPGRADE TO AMAZON Q PRO',
119+
buttons: [upgradeQButton],
120+
header: {
121+
icon: 'q',
122+
iconStatus: 'primary',
123+
body: `This feature requires a subscription to ${qProName}.`,
124+
status: {
125+
status: 'info',
126+
icon: 'q',
127+
},
128+
},
129+
body: `Upgrade to ${qProName}. [Learn More...](${paidTierLearnMoreUrl})`,
130+
messageId: 'paidtier-info',
131+
fullWidth: true,
132+
canBeDismissed: true,
133+
snapToTop: true,
134+
}
135+
136+
export const paidTierSuccessCard: ChatItem = {
137+
type: ChatItemType.ANSWER,
138+
title: 'UPGRADED TO AMAZON Q PRO',
139+
header: {
140+
icon: 'q',
141+
iconStatus: 'primary',
142+
body: `Welcome to ${qProName}`,
143+
status: {
144+
status: 'success',
145+
icon: 'q',
146+
text: 'Success',
147+
},
148+
},
149+
messageId: 'paidtier-success',
150+
fullWidth: true,
151+
canBeDismissed: true,
152+
body: `Upgraded to ${qProName}\n\n[Learn More...](${paidTierLearnMoreUrl})`,
153+
snapToTop: true,
154+
}
155+
156+
export const paidTierPromptInput: TextBasedFormItem = {
157+
placeholder: '111111111111',
158+
type: 'textinput',
159+
id: 'paid-tier',
160+
tooltip: `Upgrade to ${qProName}`,
161+
value: 'true',
162+
icon: 'magic',
163+
}
164+
165+
export const paidTierStep0: ChatItem = {
166+
type: ChatItemType.DIRECTIVE,
167+
body: `You have upgraded to ${qProName}`,
168+
}
169+
170+
export const paidTierStep1: ChatItem = {
171+
type: ChatItemType.DIRECTIVE,
172+
body: `You have upgraded to ${qProName}`,
173+
}
174+
175+
/** "Upgrade Q" form with a "AWS account id" user-input textbox. */
176+
export const paidTierUpgradeForm: ChatItem = {
177+
type: ChatItemType.ANSWER,
178+
status: 'info',
179+
fullWidth: true,
180+
// title: 'Connect AWS account and upgrade',
181+
body: `
182+
# Connect AWS account and upgrade
183+
184+
Provide your AWS account number to enable your ${qProName} subscription. Upon confirming the subscription, your AWS account will begin to be charged.
185+
186+
[Learn More...](${paidTierLearnMoreUrl})
187+
`,
188+
formItems: [
189+
{
190+
id: 'awsAccountId',
191+
type: 'textinput',
192+
title: 'AWS account ID',
193+
description: '12-digit AWS account ID',
194+
// tooltip: `Link an AWS account to upgrade to ${qProName}`,
195+
validationPatterns: {
196+
patterns: [{ pattern: '[0-9]{12}', errorMessage: 'Must be a valid 12-digit AWS account ID' }],
197+
},
198+
},
199+
],
200+
buttons: [continueUpgradeQButton],
201+
snapToTop: true,
202+
}

chat-client/src/client/withAdapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const withAdapter = (
5757
onChatPromptProgressActionButtonClicked: addDefaultRouting('onChatPromptProgressActionButtonClicked'),
5858
onTabbedContentTabChange: addDefaultRouting('onTabbedContentTabChange'),
5959
onPromptInputOptionChange: addDefaultRouting('onPromptInputOptionChange'),
60+
onPromptInputButtonClick: addDefaultRouting('onPromptInputButtonClick'),
6061
onMessageDismiss: addDefaultRouting('onMessageDismiss'),
6162

6263
/**

0 commit comments

Comments
 (0)