app-tutor-ai-consumer 1.8.0 → 1.8.2
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.
- package/CHANGELOG.md +4 -0
- package/config/rspack/rspack.config.js +4 -6
- package/config/rspack/utils/plugins.js +2 -2
- package/package.json +1 -1
- package/public/index.html +1 -1
- package/src/development-bootstrap.tsx +16 -1
- package/src/index.tsx +21 -11
- package/src/lib/contexts/index.ts +1 -0
- package/src/lib/contexts/shared-ref/index.ts +1 -0
- package/src/lib/contexts/shared-ref/shared-ref.tsx +26 -0
- package/src/lib/hooks/use-ref-client-height/use-ref-client-height.tsx +5 -5
- package/src/main/main.spec.tsx +6 -3
- package/src/modules/global-providers/global-providers.tsx +5 -0
- package/src/modules/messages/components/messages-list/messages-list.tsx +12 -19
- package/src/modules/sparkie/service.ts +1 -1
- package/src/modules/widget/components/chat-page/chat-page.tsx +6 -6
- package/src/modules/widget/components/container/container.tsx +2 -4
- package/src/modules/widget/components/index.ts +1 -0
- package/src/modules/widget/components/onboarding-page/onboarding-page.tsx +4 -3
- package/src/modules/widget/components/page-layout/constants.tsx +8 -0
- package/src/modules/widget/components/page-layout/index.ts +3 -0
- package/src/modules/widget/components/page-layout/page-layout.tsx +42 -0
- package/src/modules/widget/components/scroll-to-bottom-button/scroll-to-bottom-button.tsx +3 -4
- package/src/modules/widget/components/starter-page/starter-page.tsx +7 -6
- package/src/modules/widget/events.ts +31 -6
- package/src/modules/widget/store/widget-tabs.atom.ts +2 -2
- package/src/modules/widget/types.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## [1.8.2](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.1...v1.8.2) (2025-07-16)
|
|
2
|
+
|
|
3
|
+
## [1.8.1](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.8.0...v1.8.1) (2025-07-15)
|
|
4
|
+
|
|
1
5
|
# [1.8.0](https://github.com/Hotmart-Org/app-tutor-ai-consumer/compare/v1.7.0...v1.8.0) (2025-07-14)
|
|
2
6
|
|
|
3
7
|
### Features
|
|
@@ -11,6 +11,7 @@ require('dotenv').config({
|
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
module.exports = async function (env) {
|
|
14
|
+
const standaloneMode = env?.local
|
|
14
15
|
const productionMode = env?.production || env?.staging
|
|
15
16
|
const releaseFullName = `app-tutor-ai-consumer_v${packageJSON.version}`
|
|
16
17
|
const fileVersion = `${productionMode ? 'prod' : 'dev'}-${packageJSON?.version}`
|
|
@@ -23,7 +24,7 @@ module.exports = async function (env) {
|
|
|
23
24
|
*/
|
|
24
25
|
const config = {
|
|
25
26
|
mode: productionMode ? 'production' : 'development',
|
|
26
|
-
entry:
|
|
27
|
+
entry: standaloneMode ? paths.DEV_BOOTSTRAP : paths.INDEX,
|
|
27
28
|
...(productionMode ? {} : require('./utils/devserver.config')),
|
|
28
29
|
optimization: {
|
|
29
30
|
usedExports: true,
|
|
@@ -159,17 +160,14 @@ module.exports = async function (env) {
|
|
|
159
160
|
new rspack.DefinePlugin({
|
|
160
161
|
'process.env.PROJECT_VERSION': JSON.stringify(packageJSON.version),
|
|
161
162
|
'process.env.TARGET_ENV': JSON.stringify(process.env.NODE_ENV),
|
|
162
|
-
'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName)
|
|
163
|
+
'process.env.RELEASE_FULL_NAME': JSON.stringify(releaseFullName),
|
|
164
|
+
'process.env.STANDALONE_MODE': JSON.stringify(standaloneMode)
|
|
163
165
|
}),
|
|
164
166
|
new rspack.ProgressPlugin(),
|
|
165
167
|
new TsCheckerRspackPlugin(),
|
|
166
168
|
new rspack.HtmlRspackPlugin({
|
|
167
169
|
template: path.resolve(paths.PUBLIC, 'index.html'),
|
|
168
170
|
favicon: path.resolve(paths.PUBLIC, 'favicon.ico')
|
|
169
|
-
}),
|
|
170
|
-
new rspack.CssExtractRspackPlugin({
|
|
171
|
-
filename: productionMode ? `app-tutor-ai-consumer.css` : '[name].[contenthash].css',
|
|
172
|
-
chunkFilename: productionMode ? '[id].[contenthash].css' : '[id].[contenthash].css'
|
|
173
171
|
})
|
|
174
172
|
].concat(await getEnvironmentPlugins(!productionMode)),
|
|
175
173
|
watchOptions: {
|
|
@@ -27,8 +27,8 @@ async function getEnvironmentPlugins(isDevelopment) {
|
|
|
27
27
|
return [
|
|
28
28
|
new rspack.EnvironmentPlugin(allEnvs),
|
|
29
29
|
new rspack.CssExtractRspackPlugin({
|
|
30
|
-
filename: '
|
|
31
|
-
chunkFilename: '[id]
|
|
30
|
+
filename: 'app-tutor-ai-consumer.css',
|
|
31
|
+
chunkFilename: '[id].[contenthash].css'
|
|
32
32
|
}),
|
|
33
33
|
new CompressionPlugin({
|
|
34
34
|
algorithm: 'gzip'
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -5,10 +5,25 @@ import { v4 } from 'uuid'
|
|
|
5
5
|
import { LANGUAGES } from './config/i18n'
|
|
6
6
|
import { devMode } from './lib/utils'
|
|
7
7
|
|
|
8
|
+
const rootId = 'app-tutor-ai-widget'
|
|
9
|
+
|
|
8
10
|
if (devMode) {
|
|
9
11
|
window.TOKEN = process.env.TOKEN ?? ''
|
|
10
12
|
void (async () => {
|
|
11
|
-
|
|
13
|
+
// TODO: Remove after sidebar implementation
|
|
14
|
+
// Add Local popup css config
|
|
15
|
+
const root = document.getElementById('c3po-app-widget')
|
|
16
|
+
const container = document.createElement('div')
|
|
17
|
+
|
|
18
|
+
container.setAttribute('id', rootId)
|
|
19
|
+
container.setAttribute(
|
|
20
|
+
'class',
|
|
21
|
+
'bg-ai-dark fixed bottom-5 right-5 w-[27rem] h-[min(43rem,calc(100vh-2.5rem))] max-h-[calc(100vh-2.5rem)] z-10 rounded-[0.625rem] border border-neutral-800 shadow-lg overflow-hidden flex flex-col'
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
root?.appendChild(container)
|
|
25
|
+
|
|
26
|
+
await window.startChatWidget(rootId, {
|
|
12
27
|
hotmartToken: window.TOKEN,
|
|
13
28
|
locale: LANGUAGES.PT_BR,
|
|
14
29
|
conversationId: '21506473-a93c-4b38-9c32-68a5ca37ce73', // OWNER
|
package/src/index.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { createRoot } from 'react-dom/client'
|
|
|
7
7
|
import { initDayjs } from './config/dayjs'
|
|
8
8
|
import { initLanguage } from './config/i18n'
|
|
9
9
|
import { initAxios } from './config/request/api'
|
|
10
|
-
import { productionMode } from './lib/utils'
|
|
10
|
+
import { devMode, productionMode } from './lib/utils'
|
|
11
11
|
import { Main } from './main'
|
|
12
12
|
import { SparkieService } from './modules/sparkie'
|
|
13
13
|
import { TutorWidgetEvents, TutorWidgetEventTypes } from './modules/widget'
|
|
@@ -33,24 +33,34 @@ window.startChatWidget = async (
|
|
|
33
33
|
elementId = 'tutor-chat-app-widget',
|
|
34
34
|
settings: WidgetSettingProps
|
|
35
35
|
) => {
|
|
36
|
-
|
|
36
|
+
if (!devMode) {
|
|
37
|
+
loadMainStyles()
|
|
38
|
+
}
|
|
37
39
|
|
|
38
40
|
const rootElement = document.getElementById(elementId) as HTMLElement
|
|
39
41
|
const root = createRoot(rootElement)
|
|
40
42
|
|
|
41
|
-
await SparkieService.initSparkie({
|
|
42
|
-
token: settings.hotmartToken,
|
|
43
|
-
skipPresenceSetup: true,
|
|
44
|
-
retryOptions: {
|
|
45
|
-
maxRetries: 5,
|
|
46
|
-
retryDelay: 2000,
|
|
47
|
-
backoffMultiplier: 1.5
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
43
|
initAxios(settings.hotmartToken)
|
|
51
44
|
await initLanguage(settings.locale)
|
|
52
45
|
await initDayjs(settings.locale)
|
|
53
46
|
|
|
47
|
+
try {
|
|
48
|
+
await SparkieService.initSparkie({
|
|
49
|
+
token: settings?.hotmartToken,
|
|
50
|
+
skipPresenceSetup: true,
|
|
51
|
+
retryOptions: {
|
|
52
|
+
maxRetries: 5,
|
|
53
|
+
retryDelay: 2000,
|
|
54
|
+
backoffMultiplier: 1.5
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
await SparkieService.ensureInitialized()
|
|
58
|
+
TutorWidgetEvents.get(TutorWidgetEventTypes.LOADED)?.dispatch()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error)
|
|
61
|
+
TutorWidgetEvents.get(TutorWidgetEventTypes.LOADED)?.dispatch({ detail: { isSuccess: false } })
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
if (root)
|
|
55
65
|
root.render(
|
|
56
66
|
<StrictMode>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './shared-ref'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as createSharedRefContext } from './shared-ref'
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext, useRef } from 'react'
|
|
2
|
+
import type { PropsWithChildren, RefObject } from 'react'
|
|
3
|
+
|
|
4
|
+
function createSharedRefContext<T extends HTMLElement>() {
|
|
5
|
+
const SharedRefContext = createContext<RefObject<T | null> | null>(null)
|
|
6
|
+
|
|
7
|
+
const SharedRefContextProvider = ({ children }: PropsWithChildren) => {
|
|
8
|
+
const sharedRef = useRef<T>(null)
|
|
9
|
+
return <SharedRefContext.Provider value={sharedRef}>{children}</SharedRefContext.Provider>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const useSharedRefContext = () => {
|
|
13
|
+
const ctx = useContext(SharedRefContext)
|
|
14
|
+
|
|
15
|
+
if (!ctx) throw new Error('SharedRefContext must be used inside a SharedRefContextProvider')
|
|
16
|
+
return ctx
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
SharedRefContext,
|
|
21
|
+
SharedRefContextProvider,
|
|
22
|
+
useSharedRefContext
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default createSharedRefContext
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { type RefObject, useEffect, useState } from 'react'
|
|
1
|
+
import { type RefObject, useEffect, useMemo, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useThrottle } from '@/src/lib/hooks'
|
|
4
4
|
|
|
5
5
|
function useRefClientHeight<T extends HTMLElement>(
|
|
6
6
|
refElement: RefObject<T | null>,
|
|
7
|
-
defaultHeight =
|
|
7
|
+
defaultHeight = 0
|
|
8
8
|
) {
|
|
9
|
-
const [clientHeight, setClientHeight] = useState(defaultHeight)
|
|
9
|
+
const [clientHeight, setClientHeight] = useState<number>(defaultHeight)
|
|
10
10
|
|
|
11
11
|
const { throttledCallback: resizeHandler } = useThrottle({
|
|
12
12
|
callback: () => {
|
|
13
13
|
if (!refElement?.current?.clientHeight) return
|
|
14
14
|
|
|
15
|
-
setClientHeight(
|
|
15
|
+
setClientHeight(refElement.current.clientHeight)
|
|
16
16
|
},
|
|
17
17
|
delay: 650
|
|
18
18
|
})
|
|
@@ -32,7 +32,7 @@ function useRefClientHeight<T extends HTMLElement>(
|
|
|
32
32
|
}
|
|
33
33
|
}, [refElement, resizeHandler])
|
|
34
34
|
|
|
35
|
-
return clientHeight
|
|
35
|
+
return useMemo(() => clientHeight, [clientHeight])
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export default useRefClientHeight
|
package/src/main/main.spec.tsx
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { chance, render, screen, waitFor } from '@/config/tests'
|
|
2
|
+
import { useWidgetSettingsAtom } from '@/src/modules/widget/store/widget-settings.atom'
|
|
2
3
|
import WidgetSettingPropsBuilder from '../modules/widget/__tests__/widget-settings-props.builder'
|
|
3
|
-
import { useWidgetSettingsAtom } from '../modules/widget/store/widget-settings.atom'
|
|
4
4
|
import { Main } from '.'
|
|
5
5
|
|
|
6
|
-
vi.mock('
|
|
6
|
+
vi.mock('@/src/modules/widget/store/widget-settings.atom', async (importOriginal) => ({
|
|
7
|
+
...(await importOriginal()),
|
|
8
|
+
useWidgetSettingsAtom: vi.fn()
|
|
9
|
+
}))
|
|
7
10
|
|
|
8
11
|
describe('Main', () => {
|
|
9
12
|
const defaultProps = new WidgetSettingPropsBuilder()
|
|
@@ -20,7 +23,7 @@ describe('Main', () => {
|
|
|
20
23
|
renderComponent({ settings: props })
|
|
21
24
|
|
|
22
25
|
await waitFor(() => {
|
|
23
|
-
expect(screen.getByText(/
|
|
26
|
+
expect(screen.getByText(/send/i)).toBeInTheDocument()
|
|
24
27
|
})
|
|
25
28
|
})
|
|
26
29
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PropsWithChildren, useEffect } from 'react'
|
|
2
|
+
import { v4 } from 'uuid'
|
|
2
3
|
|
|
3
4
|
import { OptimizelyProvider } from '@/src/config/optimizely'
|
|
4
5
|
import { QueryProvider } from '@/src/config/tanstack'
|
|
@@ -13,6 +14,10 @@ function GlobalProviders({ children, settings }: GlobalProvidersProps) {
|
|
|
13
14
|
useEffect(() => {
|
|
14
15
|
if (!settings || !Object.keys(settings)?.length) return
|
|
15
16
|
|
|
17
|
+
if (!settings?.sessionId) {
|
|
18
|
+
settings.sessionId = v4()
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
setWidgetSettings(settings)
|
|
17
22
|
}, [setWidgetSettings, settings])
|
|
18
23
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { lazy, useCallback, useRef } from 'react'
|
|
2
2
|
import clsx from 'clsx'
|
|
3
|
+
import { createPortal } from 'react-dom'
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
-
import { useWidgetLoadingAtomValue } from '@/src/modules/widget'
|
|
5
|
+
import { usePageLayoutMainRefContext, useWidgetLoadingAtomValue } from '@/src/modules/widget'
|
|
6
6
|
import { useAllMessages, useManageScroll } from '../../hooks'
|
|
7
7
|
import { useSkeletonRef } from '../../hooks/use-skeleton-ref'
|
|
8
8
|
import { MessageItem } from '../message-item'
|
|
@@ -22,12 +22,12 @@ const ScrollToBottomButton = lazy(
|
|
|
22
22
|
|
|
23
23
|
function MessagesList() {
|
|
24
24
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
|
25
|
-
const scrollerClientHeight = useRefClientHeight(scrollerRef)
|
|
26
25
|
const scrollToButtonRef = useRef<HTMLButtonElement>(null)
|
|
27
26
|
const { allMessages, messagesQuery } = useAllMessages()
|
|
28
27
|
const widgetIsLoading = useWidgetLoadingAtomValue()
|
|
29
28
|
const skeletonRef = useSkeletonRef()
|
|
30
29
|
const { showScrollButton } = useManageScroll(scrollerRef)
|
|
30
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
31
31
|
|
|
32
32
|
const scrollToBottom = useCallback(() => {
|
|
33
33
|
const { current: scroller } = scrollerRef
|
|
@@ -41,28 +41,21 @@ function MessagesList() {
|
|
|
41
41
|
}, [])
|
|
42
42
|
|
|
43
43
|
return (
|
|
44
|
-
<div
|
|
45
|
-
ref={scrollerRef}
|
|
46
|
-
className={clsx(
|
|
47
|
-
'relative mx-2 flex flex-col gap-2 overflow-auto p-4',
|
|
48
|
-
`h-[${scrollerClientHeight}]`
|
|
49
|
-
)}>
|
|
44
|
+
<div ref={scrollerRef} className='mx-2 my-4 flex flex-col gap-2 overflow-auto px-4'>
|
|
50
45
|
<MessageItemLoading show={messagesQuery.isFetching} />
|
|
51
46
|
|
|
52
47
|
<MessageItemEndOfScroll
|
|
53
48
|
show={!messagesQuery.isFetching && !messagesQuery.hasNextPage && allMessages.length > 0}
|
|
54
49
|
/>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
{mainLayoutRef.current &&
|
|
51
|
+
createPortal(
|
|
52
|
+
<ScrollToBottomButton
|
|
53
|
+
ref={scrollToButtonRef}
|
|
54
|
+
show={showScrollButton}
|
|
55
|
+
onClick={scrollToBottom}
|
|
56
|
+
/>,
|
|
57
|
+
mainLayoutRef.current
|
|
62
58
|
)}
|
|
63
|
-
show={showScrollButton}
|
|
64
|
-
onClick={scrollToBottom}
|
|
65
|
-
/>
|
|
66
59
|
|
|
67
60
|
{allMessages?.map(([publishingDate, messages], i) => (
|
|
68
61
|
<div key={i} className='flex flex-1 flex-col justify-center gap-6'>
|
|
@@ -4,6 +4,7 @@ import { isTextEmpty } from '@/src/lib/utils/is-text-empty'
|
|
|
4
4
|
import { ChatInput, MessagesList, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
5
5
|
import { useAllMessages, useSendTextMessage } from '@/src/modules/messages/hooks'
|
|
6
6
|
import { useWidgetLoadingAtomValue, useWidgetTabsValueAtom } from '../../store'
|
|
7
|
+
import { PageLayout } from '../page-layout'
|
|
7
8
|
|
|
8
9
|
function ChatPage() {
|
|
9
10
|
const widgetTabs = useWidgetTabsValueAtom()
|
|
@@ -27,9 +28,8 @@ function ChatPage() {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<div className='border-t border-t-neutral-700 px-5 py-4'>
|
|
31
|
+
<PageLayout
|
|
32
|
+
asideChild={
|
|
33
33
|
<ChatInput
|
|
34
34
|
name='new-chat-msg-input'
|
|
35
35
|
ref={chatInputRef}
|
|
@@ -38,9 +38,9 @@ function ChatPage() {
|
|
|
38
38
|
inputDisabled={messagesQuery?.isLoading}
|
|
39
39
|
buttonDisabled={messagesQuery?.isLoading || !value.trim()}
|
|
40
40
|
/>
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
}>
|
|
42
|
+
<MessagesList />
|
|
43
|
+
</PageLayout>
|
|
43
44
|
)
|
|
44
45
|
}
|
|
45
|
-
|
|
46
46
|
export default ChatPage
|
|
@@ -7,10 +7,8 @@ function WidgetContainer() {
|
|
|
7
7
|
useSubscribeMessageReceivedEvent()
|
|
8
8
|
|
|
9
9
|
return (
|
|
10
|
-
<div className='flex
|
|
11
|
-
|
|
12
|
-
{WIDGET_TABS[widgetTabs.currentTab]}
|
|
13
|
-
</div>
|
|
10
|
+
<div className='flex h-full flex-col items-center justify-stretch overflow-hidden'>
|
|
11
|
+
{WIDGET_TABS[widgetTabs.currentTab]}
|
|
14
12
|
</div>
|
|
15
13
|
)
|
|
16
14
|
}
|
|
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
|
|
4
4
|
import TutorOnboardingSVG from '@/public/assets/svg/tutor-onboarding.svg?url'
|
|
5
5
|
import { Button } from '@/src/lib/components'
|
|
6
6
|
import { useWidgetTabsAtom } from '../../store'
|
|
7
|
+
import { PageLayout } from '../page-layout'
|
|
7
8
|
|
|
8
9
|
import styles from './styles.module.css'
|
|
9
10
|
|
|
@@ -12,8 +13,8 @@ function WidgetOnboardingPage() {
|
|
|
12
13
|
const { t } = useTranslation()
|
|
13
14
|
|
|
14
15
|
return (
|
|
15
|
-
|
|
16
|
-
<div className={styles.bg}>
|
|
16
|
+
<PageLayout>
|
|
17
|
+
<div className={clsx('flex-1', styles.bg)}>
|
|
17
18
|
<div className='mx-4 flex h-full flex-col justify-center gap-6 px-0.5'>
|
|
18
19
|
<div className='mx-auto max-w-[67%]'>
|
|
19
20
|
<img src={TutorOnboardingSVG} aria-hidden />
|
|
@@ -33,7 +34,7 @@ function WidgetOnboardingPage() {
|
|
|
33
34
|
{t('general.buttons.start')}
|
|
34
35
|
</Button>
|
|
35
36
|
</div>
|
|
36
|
-
|
|
37
|
+
</PageLayout>
|
|
37
38
|
)
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createSharedRefContext } from '@/src/lib/contexts'
|
|
2
|
+
|
|
3
|
+
const { useSharedRefContext, SharedRefContext, SharedRefContextProvider } =
|
|
4
|
+
createSharedRefContext<HTMLDivElement>()
|
|
5
|
+
|
|
6
|
+
export const usePageLayoutMainRefContext = useSharedRefContext
|
|
7
|
+
export const PageLayoutMainRefContext = SharedRefContext
|
|
8
|
+
export const PageLayoutMainRefContextProvider = SharedRefContextProvider
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PropsWithChildren, type ReactNode } from 'react'
|
|
2
|
+
import clsx from 'clsx'
|
|
3
|
+
|
|
4
|
+
import { PageLayoutMainRefContextProvider, usePageLayoutMainRefContext } from './constants'
|
|
5
|
+
|
|
6
|
+
export type PageLayoutProps = PropsWithChildren<{
|
|
7
|
+
asideChild?: ReactNode
|
|
8
|
+
className?: string
|
|
9
|
+
}>
|
|
10
|
+
|
|
11
|
+
function PageLayout({ asideChild, children, className }: PageLayoutProps) {
|
|
12
|
+
const mainLayoutRef = usePageLayoutMainRefContext()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={clsx(
|
|
17
|
+
'grid-areas-[main_aside] grid h-full min-h-0 w-full grid-cols-1 grid-rows-[1fr_auto]',
|
|
18
|
+
className
|
|
19
|
+
)}>
|
|
20
|
+
<div
|
|
21
|
+
ref={mainLayoutRef}
|
|
22
|
+
className='grid-area-[main] relative flex min-h-0 flex-col overflow-y-auto overflow-x-hidden'>
|
|
23
|
+
{children}
|
|
24
|
+
</div>
|
|
25
|
+
{asideChild && (
|
|
26
|
+
<div className='grid-area-[aside] flex-shrink-0 border-t border-t-neutral-700 px-5 py-4'>
|
|
27
|
+
{asideChild}
|
|
28
|
+
</div>
|
|
29
|
+
)}
|
|
30
|
+
</div>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PageLayoutWrapper(props: PageLayoutProps) {
|
|
35
|
+
return (
|
|
36
|
+
<PageLayoutMainRefContextProvider>
|
|
37
|
+
<PageLayout {...props} />
|
|
38
|
+
</PageLayoutMainRefContextProvider>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default PageLayoutWrapper
|
|
@@ -10,14 +10,13 @@ export interface IScrollToBottomButtonProps
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const ScrollToBottomButton = forwardRef<HTMLButtonElement, IScrollToBottomButtonProps>(
|
|
13
|
-
({ show = false,
|
|
13
|
+
({ show = false, onClick, className, ...props }, ref) => (
|
|
14
14
|
<button
|
|
15
15
|
{...props}
|
|
16
|
-
style={isNaN(Number(top)) ? undefined : { top }}
|
|
17
16
|
ref={ref}
|
|
18
17
|
className={clsx(
|
|
19
|
-
'
|
|
20
|
-
{ 'pointer-events-none opacity-0': !show },
|
|
18
|
+
'absolute bottom-4 left-1/2 flex size-7 cursor-pointer flex-col items-center justify-center rounded-full bg-neutral-600 text-sm text-neutral-50 outline-none transition-colors duration-300 ease-in hover:scale-110 hover:bg-neutral-700 focus:outline-none focus:ring-neutral-500 focus:ring-offset-2 focus-visible:ring-2 active:ring-2',
|
|
19
|
+
{ 'opacity-85': show, 'pointer-events-none opacity-0': !show },
|
|
21
20
|
className
|
|
22
21
|
)}
|
|
23
22
|
onClick={onClick}
|
|
@@ -4,6 +4,7 @@ import { useRefEventListener } from '@/src/lib/hooks'
|
|
|
4
4
|
import { ChatInput, useChatInputValueAtom } from '@/src/modules/messages/components'
|
|
5
5
|
import { useWidgetSettingsAtom, useWidgetTabsAtom } from '../../store'
|
|
6
6
|
import { GreetingsCard } from '../greetings-card'
|
|
7
|
+
import { PageLayout } from '../page-layout'
|
|
7
8
|
|
|
8
9
|
function WidgetStarterPage() {
|
|
9
10
|
const [settings] = useWidgetSettingsAtom()
|
|
@@ -23,18 +24,18 @@ function WidgetStarterPage() {
|
|
|
23
24
|
})
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
|
|
29
|
-
</div>
|
|
30
|
-
<div className='border-t border-t-neutral-700 px-5 py-4'>
|
|
27
|
+
<PageLayout
|
|
28
|
+
asideChild={
|
|
31
29
|
<ChatInput
|
|
32
30
|
name='new-chat-msg-input'
|
|
33
31
|
ref={chatInputRef}
|
|
34
32
|
onSend={() => setWidgetTabs('chat')}
|
|
35
33
|
/>
|
|
34
|
+
}>
|
|
35
|
+
<div className='flex flex-col justify-center px-5 py-4'>
|
|
36
|
+
<GreetingsCard author={settings?.author ?? ''} tutorName={settings?.tutorName ?? ''} />
|
|
36
37
|
</div>
|
|
37
|
-
|
|
38
|
+
</PageLayout>
|
|
38
39
|
)
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import type { ITutorWidgetEvent } from './types'
|
|
2
2
|
|
|
3
3
|
export const TutorWidgetEventTypes = {
|
|
4
|
-
OPEN: '
|
|
5
|
-
CLOSE: '
|
|
4
|
+
OPEN: 'c3po-app-widget-open',
|
|
5
|
+
CLOSE: 'c3po-app-widget-close',
|
|
6
|
+
LOADED: 'tutor-app-widget-loaded'
|
|
6
7
|
} as const
|
|
7
8
|
|
|
8
|
-
const TutorWidgetEventsList
|
|
9
|
+
const TutorWidgetEventsList = [
|
|
9
10
|
{
|
|
10
11
|
name: TutorWidgetEventTypes.OPEN,
|
|
11
12
|
handler: () => () => undefined,
|
|
12
|
-
dispatch: () =>
|
|
13
|
+
dispatch: () => {
|
|
14
|
+
window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.OPEN))
|
|
15
|
+
}
|
|
13
16
|
},
|
|
14
17
|
{
|
|
15
18
|
name: TutorWidgetEventTypes.CLOSE,
|
|
16
19
|
handler: () => () => undefined,
|
|
17
|
-
dispatch: () =>
|
|
18
|
-
|
|
20
|
+
dispatch: () => {
|
|
21
|
+
window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.CLOSE))
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: TutorWidgetEventTypes.LOADED,
|
|
26
|
+
handler: (callback) => {
|
|
27
|
+
const listener: EventListener = (e) => {
|
|
28
|
+
const evt = e as CustomEvent<{ isSuccess: boolean }>
|
|
29
|
+
|
|
30
|
+
console.log(evt.detail)
|
|
31
|
+
|
|
32
|
+
callback(evt.detail)
|
|
33
|
+
}
|
|
34
|
+
window.addEventListener(TutorWidgetEventTypes.LOADED, listener)
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
window.removeEventListener(TutorWidgetEventTypes.LOADED, listener)
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
dispatch: (payload = { detail: { isSuccess: true } }) => {
|
|
41
|
+
window.dispatchEvent(new CustomEvent(TutorWidgetEventTypes.LOADED, payload))
|
|
42
|
+
}
|
|
43
|
+
} as ITutorWidgetEvent<{ isSuccess: boolean }>
|
|
19
44
|
] as const
|
|
20
45
|
|
|
21
46
|
export const TutorWidgetEvents = new Map(TutorWidgetEventsList.map((e) => [e.name, e]))
|
|
@@ -8,8 +8,8 @@ export type WidgetTabsProps = {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const INITIAL_PROPS: WidgetTabsProps = {
|
|
11
|
-
currentTab: '
|
|
12
|
-
history: new Set(['
|
|
11
|
+
currentTab: 'chat',
|
|
12
|
+
history: new Set(['chat'])
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const widgetTabsAtom = atom<WidgetTabsProps>(INITIAL_PROPS)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TutorWidgetEventTypes } from './events'
|
|
2
2
|
|
|
3
|
-
export type ITutorWidgetEvent = {
|
|
3
|
+
export type ITutorWidgetEvent<T = unknown> = {
|
|
4
4
|
name: (typeof TutorWidgetEventTypes)[keyof typeof TutorWidgetEventTypes]
|
|
5
|
-
handler: (
|
|
6
|
-
dispatch:
|
|
5
|
+
handler: (callback: (payload: T) => void) => () => void
|
|
6
|
+
dispatch: (payload?: CustomEventInit<T>) => void
|
|
7
7
|
}
|