app-tutor-ai-consumer 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -95,7 +95,7 @@ jobs:
95
95
  uses: newrelic/deployment-marker-action@v2.5.1
96
96
  env:
97
97
  VERSION: ${{ env.VERSION }}
98
- NEW_RELIC_API_KEY: NRAK-NX0XDEQLIGCEWTLWS6EQ17B4VY1
98
+ NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }}
99
99
  NEW_RELIC_DEPLOYMENT_ENTITY_GUID: Mjc1MDN8QVBNfEFQUExJQ0FUSU9OfDEwOTUyNjI4MTc
100
100
  with:
101
101
  apiKey: ${{ env.NEW_RELIC_API_KEY }}
@@ -139,7 +139,7 @@ jobs:
139
139
  "textButton": {
140
140
  "onClick": {
141
141
  "openLink": {
142
- "url": "https://github.com/Hotmart-Org/app-club-consumer/actions/runs/${{ github.run_id }}"
142
+ "url": "https://github.com/Hotmart-Org/app-tutor-ai-consumer/actions/runs/${{ github.run_id }}"
143
143
  }
144
144
  },
145
145
  "text": "Follow this deploy"
@@ -57,7 +57,7 @@ jobs:
57
57
  uses: newrelic/deployment-marker-action@v2.5.1
58
58
  env:
59
59
  ROLLBACK_VERSION: ${{ github.event.inputs.rollbackVersion }}
60
- NEW_RELIC_API_KEY: NRAK-14TLYFWRA5RCKUGK4A3TJSAPU78
60
+ NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }}
61
61
  NEW_RELIC_DEPLOYMENT_ENTITY_GUID: Mjc1MDN8QVBNfEFQUExJQ0FUSU9OfDEwOTE1MDMzMTE
62
62
  with:
63
63
  apiKey: ${{ env.NEW_RELIC_API_KEY }}
@@ -104,7 +104,7 @@ jobs:
104
104
  uses: newrelic/deployment-marker-action@v2.5.1
105
105
  env:
106
106
  ROLLBACK_VERSION: ${{ github.event.inputs.rollbackVersion }}
107
- NEW_RELIC_API_KEY: NRAK-NX0XDEQLIGCEWTLWS6EQ17B4VY1
107
+ NEW_RELIC_API_KEY: ${{ secrets.NEW_RELIC_API_KEY }}
108
108
  NEW_RELIC_DEPLOYMENT_ENTITY_GUID: Mjc1MDN8QVBNfEFQUExJQ0FUSU9OfDEwOTUyNjI4MTc
109
109
  with:
110
110
  apiKey: ${{ env.NEW_RELIC_API_KEY }}
@@ -123,12 +123,17 @@ jobs:
123
123
  cdn-url: ${{ env.CLOUDFRONT_URL }}
124
124
 
125
125
  staging-notification:
126
- name: 'Staging Notification'
127
- runs-on: [buildstaging-iac]
128
- needs: ['deploy-staging']
129
- steps:
126
+ name: 'Staging Notification'
127
+ runs-on: [buildstaging-iac]
128
+ needs: ['deploy-staging']
129
+ steps:
130
130
  - name: Checkout
131
131
  uses: actions/checkout@v4
132
+ with:
133
+ token: ${{ secrets.CI_GH_TOKEN }}
134
+ fetch-depth: 0
135
+ ref: ${{ env.BRANCH_REF }}
136
+ fetch-tags: true
132
137
 
133
138
  - name: Extract version from package.json
134
139
  run: |
@@ -146,7 +151,57 @@ jobs:
146
151
  apiKey: ${{ env.NEW_RELIC_API_KEY }}
147
152
  guid: ${{ env.NEW_RELIC_DEPLOYMENT_ENTITY_GUID }}
148
153
  version: ${{ env.VERSION }}
149
- user: '${{ github.actor }}'
150
- changelog: 'https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md'
151
- commit: '${{ github.sha }}'
152
- description: 'Deploy: ${{env.APP_NAME}} v${{env.VERSION}} - ${{ github.ref_name }}'
154
+ user: "${{ github.actor }}"
155
+ changelog: "https://github.com/${{ github.repository }}/blob/master/CHANGELOG.md"
156
+ commit: "${{ github.sha }}"
157
+ description: "Deploy: ${{env.APP_NAME}} v${{ env.VERSION }} - ${{ github.ref_name }}"
158
+
159
+ - name: Notify Google Chat
160
+ uses: Hotmart-Org/actions/notification@master
161
+ env:
162
+ VERSION: ${{ env.VERSION }}
163
+ with:
164
+ type: 'Original'
165
+ author: true
166
+ webhook-chat: 'https://chat.googleapis.com/v1/spaces/AAAA1nvOyjo/messages?key=AIzaSyDdI0hCZtE6vySjMm-WEfRq3CPzqKqqsHI&token=uuL7DA8zTxUkJjQa39HIM0TYVZV0DvneZ0mklNhEr5M'
167
+ body: |
168
+ {
169
+ "text": "📦 *[STAGING] app-tutor-ai-consumer v${{ env.VERSION }}*: staging deploy: ${{ github.ref_name }}",
170
+ "cards": [
171
+ {
172
+ "header": {
173
+ "title": "Started by ${{ github.actor }}"
174
+ },
175
+ "sections": [
176
+ {
177
+ "widgets": [
178
+ {
179
+ "buttons": [
180
+ {
181
+ "textButton": {
182
+ "onClick": {
183
+ "openLink": {
184
+ "url": "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}"
185
+ }
186
+ },
187
+ "text": "View commit"
188
+ }
189
+ },
190
+ {
191
+ "textButton": {
192
+ "onClick": {
193
+ "openLink": {
194
+ "url": "https://github.com/Hotmart-Org/app-tutor-ai-consumer/actions/runs/${{ github.run_id }}"
195
+ }
196
+ },
197
+ "text": "Follow this deploy"
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ ]
203
+ }
204
+ ]
205
+ }
206
+ ]
207
+ }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [1.8.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.7.0...v1.8.0) (2025-07-14)
2
+
3
+ ### Features
4
+
5
+ - changing notifications and keys ([57312bc](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/57312bc069bc1f9ba0fefde00f252f615e52efa4))
6
+
7
+ # [1.7.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.6.0...v1.7.0) (2025-07-11)
8
+
9
+ ### Features
10
+
11
+ - add send text message validation ([4f242ca](https://github.com/Hotmart-Org/app-tutor-ai-consumer/commit/4f242caf92ec3cdab2ae5866d67abd6f1b5d864f))
12
+
1
13
  # [1.6.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.5.0...v1.6.0) (2025-07-11)
2
14
 
3
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "app-tutor-ai-consumer",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "dev": "rspack serve --env=development --config config/rspack/rspack.config.js",
@@ -121,6 +121,7 @@
121
121
  "react-dom": "~19.1.0",
122
122
  "react-i18next": "~15.5.2",
123
123
  "react-markdown": "~10.1.0",
124
+ "react-textarea-autosize": "~8.5.9",
124
125
  "rehype-raw": "~7.0.0",
125
126
  "rehype-sanitize": "~6.0.0",
126
127
  "remark-breaks": "~4.0.0",
@@ -79,14 +79,15 @@
79
79
  --ai-color-chat-response: #1e1926;
80
80
 
81
81
  /* Size */
82
+ --hc-size-spacing-1: 0.25rem;
82
83
  --hc-size-spacing-2: 0.5rem;
83
84
  --hc-size-border-medium: 0.5rem;
84
85
  }
85
86
 
86
87
  #hotmart-app-tutor-ai-consumer-root {
87
88
  & *::-webkit-scrollbar {
88
- width: var(--hc-size-spacing-2);
89
- height: var(--hc-size-spacing-2);
89
+ width: var(--custom-scrollbar-width, var(--hc-size-spacing-2));
90
+ height: var(--custom-scrollbar-width, var(--hc-size-spacing-2));
90
91
  }
91
92
 
92
93
  & *::-webkit-scrollbar-track {
@@ -1,2 +1,2 @@
1
1
  // Auto-generated file - DO NOT EDIT
2
- export type ValidIconNames = 'ai-color' | 'arrow-down' | 'chevron-down' | 'send'
2
+ export type ValidIconNames = 'ai-color' | 'arrow-down' | 'chevron-down' | 'send' | 'stop'
@@ -0,0 +1,4 @@
1
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
2
+ >
3
+ <rect x="7" y="7" width="10" height="10" rx="1.25" fill="currentColor"></rect>
4
+ </svg>
@@ -8,7 +8,7 @@ import { useChatInputValueAtom } from './chat-input.atom'
8
8
  vi.mock('./chat-input.atom', () => ({ useChatInputValueAtom: vi.fn() }))
9
9
 
10
10
  describe('ChatInput', () => {
11
- const ref = createRef<HTMLInputElement>()
11
+ const ref = createRef<HTMLTextAreaElement>()
12
12
  const chatInputValueAtomMock = { val: '', setVal: vi.fn() }
13
13
  const defaultProps = { name: chance.name() }
14
14
 
@@ -53,20 +53,24 @@ describe('ChatInput', () => {
53
53
  expect(onSend).toHaveBeenCalledTimes(1)
54
54
  })
55
55
 
56
- it('should disable the button when there is no value', () => {
57
- renderComponent()
56
+ it('should disable the button when buttonDisabled prop is true', () => {
57
+ renderComponent({ buttonDisabled: true } as never)
58
58
 
59
59
  expect(screen.getByRole('button', { name: /Submit Button/i })).toBeDisabled()
60
60
  })
61
61
 
62
- it('should the button be enabled and input have defaultValue when there is value', () => {
62
+ it('should enable the button when buttonDisabled prop is falsy', () => {
63
+ renderComponent()
64
+
65
+ expect(screen.getByRole('button', { name: /Submit Button/i })).toBeEnabled()
66
+ })
67
+
68
+ it('should set textarea value correctly', () => {
63
69
  const name = chance.name()
64
70
  vi.mocked(useChatInputValueAtom).mockReturnValue([name, vi.fn()])
65
71
 
66
72
  renderComponent()
67
73
 
68
- expect(ref.current?.defaultValue).toBe(name)
69
-
70
- expect(screen.getByRole('button', { name: /Submit Button/i })).toBeEnabled()
74
+ expect(ref.current?.value).toBe(name)
71
75
  })
72
76
  })
@@ -2,77 +2,108 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
2
2
  import clsx from 'clsx'
3
3
  import type { ChangeEvent, KeyboardEvent } from 'react'
4
4
  import { useTranslation } from 'react-i18next'
5
+ import TextareaAutosize from 'react-textarea-autosize'
5
6
 
6
- import { Icon } from '@/src/lib/components'
7
+ import { Icon, Spinner } from '@/src/lib/components'
7
8
 
8
9
  import { useChatInputValueAtom } from './chat-input.atom'
9
10
  import type { ChatInputProps } from './types'
10
11
 
11
- const ChatInput = forwardRef<HTMLInputElement, ChatInputProps>(({ name, onSend }, forwardedRef) => {
12
- const { t } = useTranslation()
13
- const [value, setValue] = useChatInputValueAtom()
14
- const ref = useRef<HTMLInputElement>(null)
12
+ import styles from './styles.module.css'
15
13
 
16
- useImperativeHandle(forwardedRef, () => ref?.current as HTMLInputElement)
14
+ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(
15
+ (
16
+ { name, onSend, loading = false, inputDisabled = false, buttonDisabled = false },
17
+ forwardedRef
18
+ ) => {
19
+ const { t } = useTranslation()
20
+ const [value, setValue] = useChatInputValueAtom()
21
+ const ref = useRef<HTMLTextAreaElement>(null)
17
22
 
18
- const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
19
- setValue(e.target.value?.trim())
20
- }
23
+ useImperativeHandle(forwardedRef, () => ref?.current as HTMLTextAreaElement)
21
24
 
22
- const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
23
- const isEnterKey = e.code === 'Enter'
24
- const isShiftKey = e.shiftKey
25
+ const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
26
+ if (inputDisabled) return
25
27
 
26
- if (isEnterKey && !isShiftKey) {
27
- e.preventDefault()
28
- onSend?.()
28
+ setValue(e.target.value)
29
29
  }
30
- }
31
30
 
32
- useEffect(() => {
33
- const input = ref?.current
31
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
32
+ if (loading || buttonDisabled) return
33
+
34
+ const isEnterKey = e.code === 'Enter'
35
+ const isShiftKey = e.shiftKey
34
36
 
35
- if (input) {
36
- input.focus()
37
+ if (isEnterKey && !isShiftKey) {
38
+ e.preventDefault()
39
+ onSend?.()
40
+ }
37
41
  }
38
- }, [])
39
-
40
- return (
41
- <div className='flex items-center rounded-full border-neutral-800 bg-neutral-800 px-4 py-2'>
42
- <input
43
- id={name}
44
- name={name}
45
- ref={ref}
46
- type='text'
47
- className='h-6 w-full border-none bg-transparent text-neutral-400 outline-none outline-0 placeholder:text-neutral-400'
48
- placeholder={t('send_message.field.placeholder')}
49
- defaultValue={value}
50
- onChange={handleChange}
51
- onKeyDown={handleKeyDown}
52
- />
53
- <button
54
- onClick={onSend}
55
- disabled={!value}
42
+
43
+ useEffect(() => {
44
+ if (inputDisabled) return
45
+
46
+ const input = ref?.current
47
+
48
+ if (input) {
49
+ input.focus()
50
+
51
+ const position = input.textLength ?? 0
52
+ input.setSelectionRange(position, position)
53
+ }
54
+ }, [inputDisabled])
55
+
56
+ return (
57
+ <div
56
58
  className={clsx(
57
- 'flex size-8 flex-col items-center justify-center rounded-full outline-none transition-colors duration-300 ease-in',
58
- {
59
- 'cursor-pointer hover:scale-110 hover:bg-neutral-600 focus:bg-neutral-700 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2':
60
- Boolean(value),
61
- 'cursor-not-allowed': !value
62
- }
63
- )}
64
- aria-label='Submit Button'>
65
- <Icon
66
- name='send'
67
- className={clsx('h-4 w-4 pr-0.5 pt-0.5 transition-colors duration-150', {
68
- 'text-neutral-50': Boolean(value),
69
- 'text-neutral-500': !value
70
- })}
59
+ 'flex items-center rounded-full border border-neutral-800 bg-neutral-900 px-4 py-2',
60
+ { 'cursor-not-allowed opacity-40': inputDisabled }
61
+ )}>
62
+ <TextareaAutosize
63
+ id={name}
64
+ name={name}
65
+ ref={ref}
66
+ className={clsx(
67
+ clsx(
68
+ 'max-h-12 w-full resize-none border-none bg-transparent text-neutral-100 outline-none outline-0 placeholder:text-neutral-400',
69
+ styles.textArea
70
+ ),
71
+ { 'cursor-not-allowed': inputDisabled, 'opacity-40': inputDisabled || loading }
72
+ )}
73
+ placeholder={t('send_message.field.placeholder')}
74
+ value={value}
75
+ onChange={handleChange}
76
+ onKeyDown={handleKeyDown}
77
+ disabled={inputDisabled}
71
78
  />
72
- </button>
73
- </div>
74
- )
75
- })
79
+ <button
80
+ onClick={onSend}
81
+ disabled={buttonDisabled || loading}
82
+ className={clsx(
83
+ 'flex size-8 flex-col items-center justify-center rounded-full outline-none transition-colors duration-300 ease-in',
84
+ {
85
+ 'cursor-pointer hover:scale-110 hover:bg-neutral-600 focus:bg-neutral-700 focus:outline-none focus:ring-1 focus:ring-neutral-500 focus:ring-offset-2':
86
+ !buttonDisabled,
87
+ 'cursor-not-allowed': buttonDisabled
88
+ }
89
+ )}
90
+ aria-label='Submit Button'>
91
+ {loading ? (
92
+ <Spinner className='h-5 w-5 text-neutral-500' />
93
+ ) : (
94
+ <Icon
95
+ name='send'
96
+ className={clsx('h-4 w-4 pr-0.5 pt-0.5 transition-colors duration-150', {
97
+ 'text-neutral-50': !buttonDisabled,
98
+ 'text-neutral-500': buttonDisabled
99
+ })}
100
+ />
101
+ )}
102
+ </button>
103
+ </div>
104
+ )
105
+ }
106
+ )
76
107
 
77
108
  ChatInput.displayName = 'ChatInput'
78
109
 
@@ -0,0 +1,3 @@
1
+ .textArea {
2
+ --custom-scrollbar-width: var(--hc-size-spacing-1);
3
+ }
@@ -1,4 +1,7 @@
1
1
  export type ChatInputProps = {
2
2
  name: string
3
3
  onSend?: () => void
4
+ loading?: boolean
5
+ inputDisabled?: boolean
6
+ buttonDisabled?: boolean
4
7
  }
@@ -1,14 +1,17 @@
1
1
  import { useRef } from 'react'
2
2
 
3
3
  import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
4
- import { ChatInput, MessagesList } from '@/src/modules/messages/components'
5
- import { useSendTextMessage } from '@/src/modules/messages/hooks'
6
- import { useWidgetTabsValueAtom } from '../../store'
4
+ import { ChatInput, MessagesList, useChatInputValueAtom } from '@/src/modules/messages/components'
5
+ import { useAllMessages, useSendTextMessage } from '@/src/modules/messages/hooks'
6
+ import { useWidgetLoadingAtomValue, useWidgetTabsValueAtom } from '../../store'
7
7
 
8
8
  function ChatPage() {
9
9
  const widgetTabs = useWidgetTabsValueAtom()
10
- const chatInputRef = useRef<HTMLInputElement>(null)
10
+ const chatInputRef = useRef<HTMLTextAreaElement>(null)
11
11
  const sendTextMessageMutation = useSendTextMessage()
12
+ const { messagesQuery } = useAllMessages()
13
+ const widgetLoading = useWidgetLoadingAtomValue()
14
+ const [value, setValue] = useChatInputValueAtom()
12
15
 
13
16
  const handleSendMessage = () => {
14
17
  const text = chatInputRef.current?.value ?? ''
@@ -18,6 +21,7 @@ function ChatPage() {
18
21
  sendTextMessageMutation.mutate(text, {
19
22
  onSuccess() {
20
23
  if (chatInputRef.current?.value) chatInputRef.current.value = ''
24
+ setValue('')
21
25
  }
22
26
  })
23
27
  }
@@ -30,6 +34,9 @@ function ChatPage() {
30
34
  name='new-chat-msg-input'
31
35
  ref={chatInputRef}
32
36
  onSend={widgetTabs.currentTab === 'chat' ? handleSendMessage : undefined}
37
+ loading={widgetLoading || sendTextMessageMutation.isPending}
38
+ inputDisabled={messagesQuery?.isLoading}
39
+ buttonDisabled={messagesQuery?.isLoading || !value.trim()}
33
40
  />
34
41
  </div>
35
42
  </>
@@ -7,16 +7,16 @@ import { GreetingsCard } from '../greetings-card'
7
7
 
8
8
  function WidgetStarterPage() {
9
9
  const [settings] = useWidgetSettingsAtom()
10
- const chatInputRef = useRef<HTMLInputElement>(null)
10
+ const chatInputRef = useRef<HTMLTextAreaElement>(null)
11
11
  const [, setChatInputValue] = useChatInputValueAtom()
12
12
  const [, setWidgetTabs] = useWidgetTabsAtom()
13
13
 
14
- useRefEventListener<HTMLInputElement>({
14
+ useRefEventListener<HTMLTextAreaElement>({
15
15
  config: {
16
16
  ref: chatInputRef,
17
17
  eventTypes: ['input', 'change'],
18
18
  handler: (e) => {
19
- const target = e.target as HTMLInputElement
19
+ const target = e.target as HTMLTextAreaElement
20
20
  setChatInputValue(target.value)
21
21
  }
22
22
  }