@inploi/plugin-chatbot 2.0.0 → 2.1.1
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/cdn/index.js +56 -0
- package/{public → cdn}/mockServiceWorker.js +4 -9
- package/package.json +25 -5
- package/.env +0 -3
- package/.env.example +0 -3
- package/.eslintrc.cjs +0 -10
- package/CHANGELOG.md +0 -85
- package/bunfig.toml +0 -2
- package/happydom.ts +0 -10
- package/index.html +0 -28
- package/postcss.config.cjs +0 -7
- package/src/chatbot.api.ts +0 -46
- package/src/chatbot.constants.ts +0 -9
- package/src/chatbot.css +0 -107
- package/src/chatbot.dom.ts +0 -17
- package/src/chatbot.idb.ts +0 -17
- package/src/chatbot.state.ts +0 -89
- package/src/chatbot.ts +0 -54
- package/src/chatbot.utils.ts +0 -50
- package/src/index.cdn.ts +0 -12
- package/src/index.dev.ts +0 -36
- package/src/index.ts +0 -1
- package/src/interpreter/interpreter.test.ts +0 -69
- package/src/interpreter/interpreter.ts +0 -241
- package/src/mocks/browser.ts +0 -5
- package/src/mocks/example.flows.ts +0 -763
- package/src/mocks/handlers.ts +0 -28
- package/src/ui/chat-bubble.tsx +0 -52
- package/src/ui/chat-input/chat-input.boolean.tsx +0 -62
- package/src/ui/chat-input/chat-input.file.tsx +0 -213
- package/src/ui/chat-input/chat-input.multiple-choice.tsx +0 -117
- package/src/ui/chat-input/chat-input.text.tsx +0 -111
- package/src/ui/chat-input/chat-input.tsx +0 -81
- package/src/ui/chatbot-header.tsx +0 -98
- package/src/ui/chatbot.tsx +0 -105
- package/src/ui/input-error.tsx +0 -33
- package/src/ui/job-application-content.tsx +0 -145
- package/src/ui/job-application-messages.tsx +0 -64
- package/src/ui/loading-indicator.tsx +0 -37
- package/src/ui/send-button.tsx +0 -27
- package/src/ui/transition.tsx +0 -1
- package/src/ui/typing-indicator.tsx +0 -12
- package/src/ui/useChatService.ts +0 -75
- package/src/ui/useFocus.ts +0 -10
- package/src/vite-env.d.ts +0 -1
- package/tailwind.config.ts +0 -119
- package/tsconfig.json +0 -33
- package/tsconfig.node.json +0 -10
- package/types.d.ts +0 -2
- package/vite.config.ts +0 -18
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
import { ComponentProps } from 'preact';
|
|
3
|
-
|
|
4
|
-
import { StartedJobApplication, application, cancelCurrentApplication, viewState } from '../chatbot.state';
|
|
5
|
-
|
|
6
|
-
const HeaderIconButton = ({ class: className, children, ...props }: ComponentProps<'button'>) => {
|
|
7
|
-
return (
|
|
8
|
-
<button
|
|
9
|
-
class={clsx(
|
|
10
|
-
'p-2 touch-hitbox relative rounded-full text-neutral-11 hover:text-neutral-12 active:text-neutral-12 hover:bg-neutral-12/5 active:bg-neutral-12/10',
|
|
11
|
-
className,
|
|
12
|
-
)}
|
|
13
|
-
{...props}
|
|
14
|
-
>
|
|
15
|
-
<svg
|
|
16
|
-
class="block"
|
|
17
|
-
width="16"
|
|
18
|
-
height="16"
|
|
19
|
-
viewBox="0 0 16 16"
|
|
20
|
-
fill="none"
|
|
21
|
-
stroke="currentColor"
|
|
22
|
-
stroke-width="1.5"
|
|
23
|
-
stroke-linecap="round"
|
|
24
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
25
|
-
>
|
|
26
|
-
{children}
|
|
27
|
-
</svg>
|
|
28
|
-
</button>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const ChatbotHeader = ({ currentApplication }: { currentApplication?: StartedJobApplication }) => {
|
|
33
|
-
const view = viewState.value;
|
|
34
|
-
const headerText = currentApplication
|
|
35
|
-
? `Applying for “${currentApplication.job.title}” at ${currentApplication.company.name}`
|
|
36
|
-
: 'Applying';
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<header class="z-20 flex gap-2 h-[var(--header-height)] rounded-t-2xl left-0 outline outline-1 outline-neutral-5 right-0 mx-auto items-center absolute top-0 bg-neutral-1/90 backdrop-blur-md backdrop-saturate-150">
|
|
40
|
-
<p id="chatbot-header" class="sr-only">
|
|
41
|
-
{headerText}
|
|
42
|
-
</p>
|
|
43
|
-
<button
|
|
44
|
-
tabIndex={-1}
|
|
45
|
-
aria-hidden
|
|
46
|
-
onClick={() => {
|
|
47
|
-
if (view === 'minimised') {
|
|
48
|
-
viewState.value = 'maximised';
|
|
49
|
-
} else {
|
|
50
|
-
viewState.value = 'minimised';
|
|
51
|
-
}
|
|
52
|
-
}}
|
|
53
|
-
class="flex-grow h-full overflow-hidden py-1 px-4"
|
|
54
|
-
>
|
|
55
|
-
<p aria-hidden class="font-bold text-sm tracking-tight text-neutral-12 truncate">
|
|
56
|
-
{headerText}
|
|
57
|
-
</p>
|
|
58
|
-
</button>
|
|
59
|
-
|
|
60
|
-
<div class="flex-shrink-0 flex items-center gap-3 p-1.5">
|
|
61
|
-
{viewState.value === 'minimised' ? (
|
|
62
|
-
<>
|
|
63
|
-
<HeaderIconButton
|
|
64
|
-
key="minmax"
|
|
65
|
-
aria-label="Maximise job application"
|
|
66
|
-
onClick={() => (viewState.value = 'maximised')}
|
|
67
|
-
>
|
|
68
|
-
<path d="M12.5 9.5L8 5L3.5 9.5" />
|
|
69
|
-
</HeaderIconButton>
|
|
70
|
-
<HeaderIconButton key="close" aria-label="Close application" onClick={cancelCurrentApplication}>
|
|
71
|
-
<path d="M12.5 6.5L8 11L3.5 6.5" />
|
|
72
|
-
</HeaderIconButton>
|
|
73
|
-
</>
|
|
74
|
-
) : (
|
|
75
|
-
<>
|
|
76
|
-
<HeaderIconButton
|
|
77
|
-
key="restart"
|
|
78
|
-
aria-label="Restart"
|
|
79
|
-
onClick={() => {
|
|
80
|
-
application.restart();
|
|
81
|
-
}}
|
|
82
|
-
>
|
|
83
|
-
<path d="M12 8.5C12 9.29113 11.7654 10.0645 11.3259 10.7223C10.8864 11.3801 10.2616 11.8928 9.53073 12.1955C8.79983 12.4983 7.99556 12.5775 7.21964 12.4231C6.44371 12.2688 5.73098 11.8878 5.17157 11.3284C4.61216 10.769 4.2312 10.0563 4.07686 9.28036C3.92252 8.50444 4.00173 7.70017 4.30448 6.96927C4.60723 6.23836 5.11992 5.61365 5.77772 5.17412C6.43552 4.7346 7.20887 4.5 8 4.5H9" />
|
|
84
|
-
<path d="M8 7L10 4.5L8 2.5" />
|
|
85
|
-
</HeaderIconButton>
|
|
86
|
-
<HeaderIconButton
|
|
87
|
-
key="minmax"
|
|
88
|
-
aria-label="Minimise application"
|
|
89
|
-
onClick={() => (viewState.value = 'minimised')}
|
|
90
|
-
>
|
|
91
|
-
<path d="M12.5 6.5L8 11L3.5 6.5" />
|
|
92
|
-
</HeaderIconButton>
|
|
93
|
-
</>
|
|
94
|
-
)}
|
|
95
|
-
</div>
|
|
96
|
-
</header>
|
|
97
|
-
);
|
|
98
|
-
};
|
package/src/ui/chatbot.tsx
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { AnalyticsService, ApiClient, Logger } from '@inploi/sdk';
|
|
2
|
-
import { FocusScope } from '@radix-ui/react-focus-scope';
|
|
3
|
-
import { LazyMotion, domAnimation } from 'framer-motion';
|
|
4
|
-
import { AnimatePresence, Variants, m } from 'framer-motion';
|
|
5
|
-
import { Suspense, lazy, useRef } from 'preact/compat';
|
|
6
|
-
import { HEADER_HEIGHT } from '~/chatbot.constants';
|
|
7
|
-
|
|
8
|
-
import { application, viewState } from '../chatbot.state';
|
|
9
|
-
import { ChatbotHeader } from './chatbot-header';
|
|
10
|
-
|
|
11
|
-
const JobApplicationContent = lazy(() =>
|
|
12
|
-
import('./job-application-content').then(module => module.JobApplicationContent),
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
const MotionProvider = ({ children }: { children: JSX.Element }) => {
|
|
16
|
-
return <LazyMotion features={domAnimation}>{children}</LazyMotion>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const chatbotVariants: Variants = {
|
|
20
|
-
closed: { y: 'calc(100% + 1rem)', height: HEADER_HEIGHT },
|
|
21
|
-
maximised: { y: 0, height: '75vh' },
|
|
22
|
-
minimised: { y: 0, height: HEADER_HEIGHT },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type ChatbotProps = {
|
|
26
|
-
apiClient: ApiClient;
|
|
27
|
-
logger: Logger;
|
|
28
|
-
analytics: AnalyticsService;
|
|
29
|
-
};
|
|
30
|
-
export const Chatbot = ({ logger, apiClient, analytics }: ChatbotProps) => {
|
|
31
|
-
const currentApplication = application.current$.value;
|
|
32
|
-
const isApplying = currentApplication !== null;
|
|
33
|
-
const view = viewState.value;
|
|
34
|
-
|
|
35
|
-
const drawerRef = useRef<HTMLDivElement>(null);
|
|
36
|
-
// useEffect(() => {
|
|
37
|
-
// function onVisualViewportChange() {
|
|
38
|
-
// if (!drawerRef.current) return;
|
|
39
|
-
// const visualViewportHeight = window.visualViewport?.height ?? 0;
|
|
40
|
-
// const keyboardHeight = window.innerHeight - visualViewportHeight;
|
|
41
|
-
|
|
42
|
-
// // Difference between window height and height excluding the keyboard
|
|
43
|
-
// const diffFromInitial = window.innerHeight - visualViewportHeight;
|
|
44
|
-
|
|
45
|
-
// const drawerHeight = drawerRef.current.getBoundingClientRect().height;
|
|
46
|
-
// const offset = keyboardHeight - drawerHeight;
|
|
47
|
-
|
|
48
|
-
// drawerRef.current.style.height = `${visualViewportHeight - offset}px`;
|
|
49
|
-
// drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
|
|
50
|
-
// }
|
|
51
|
-
|
|
52
|
-
// window.visualViewport?.addEventListener('resize', onVisualViewportChange);
|
|
53
|
-
|
|
54
|
-
// return () => window.visualViewport?.removeEventListener('resize', onVisualViewportChange);
|
|
55
|
-
// }, []);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<MotionProvider>
|
|
59
|
-
<AnimatePresence>
|
|
60
|
-
{isApplying && viewState.value === 'maximised' ? (
|
|
61
|
-
<m.div
|
|
62
|
-
key="bg"
|
|
63
|
-
initial={{ opacity: 0 }}
|
|
64
|
-
animate={{ opacity: 1 }}
|
|
65
|
-
exit={{ opacity: 0 }}
|
|
66
|
-
class="bg-neutral-12/60 fixed inset-0"
|
|
67
|
-
/>
|
|
68
|
-
) : null}
|
|
69
|
-
|
|
70
|
-
{isApplying ? (
|
|
71
|
-
<m.div
|
|
72
|
-
key="content"
|
|
73
|
-
ref={drawerRef}
|
|
74
|
-
aria-modal="true"
|
|
75
|
-
role="dialog"
|
|
76
|
-
aria-labelledby="chatbot-header"
|
|
77
|
-
variants={chatbotVariants}
|
|
78
|
-
initial="closed"
|
|
79
|
-
animate={view}
|
|
80
|
-
exit="closed"
|
|
81
|
-
style={{ '--header-height': `${HEADER_HEIGHT}px` }}
|
|
82
|
-
class="isolate fixed left-2 right-2 mx-auto max-w-[450px] bottom-2 max-h-full focus:outline-none"
|
|
83
|
-
>
|
|
84
|
-
<FocusScope
|
|
85
|
-
class="outline outline-1 h-full outline-neutral-5 relative flex flex-col bg-neutral-2 rounded-3xl overflow-hidden"
|
|
86
|
-
loop
|
|
87
|
-
trapped={false}
|
|
88
|
-
>
|
|
89
|
-
<ChatbotHeader currentApplication={currentApplication} />
|
|
90
|
-
|
|
91
|
-
<Suspense fallback={<div>loading…</div>}>
|
|
92
|
-
<JobApplicationContent
|
|
93
|
-
analytics={analytics}
|
|
94
|
-
currentApplication={currentApplication}
|
|
95
|
-
apiClient={apiClient}
|
|
96
|
-
logger={logger}
|
|
97
|
-
/>
|
|
98
|
-
</Suspense>
|
|
99
|
-
</FocusScope>
|
|
100
|
-
</m.div>
|
|
101
|
-
) : null}
|
|
102
|
-
</AnimatePresence>
|
|
103
|
-
</MotionProvider>
|
|
104
|
-
);
|
|
105
|
-
};
|
package/src/ui/input-error.tsx
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { AnimatePresence, m } from 'framer-motion';
|
|
2
|
-
import type { FieldError } from 'react-hook-form';
|
|
3
|
-
|
|
4
|
-
export const InputError = ({ error }: { error?: FieldError }) => {
|
|
5
|
-
return (
|
|
6
|
-
<AnimatePresence>
|
|
7
|
-
{error && (
|
|
8
|
-
<m.div
|
|
9
|
-
initial={{ scale: 0.5, opacity: 0 }}
|
|
10
|
-
animate={{ scale: 1, opacity: 1 }}
|
|
11
|
-
exit={{ scale: 0, opacity: 0 }}
|
|
12
|
-
role="alert"
|
|
13
|
-
class="opacity-0 p-0.5 px-1 rounded-full overflow-hidden flex items-center max-w-full gap-1 text-error-11"
|
|
14
|
-
>
|
|
15
|
-
<svg
|
|
16
|
-
class="text-error-10"
|
|
17
|
-
width="16"
|
|
18
|
-
height="16"
|
|
19
|
-
viewBox="0 0 16 16"
|
|
20
|
-
fill="none"
|
|
21
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
22
|
-
>
|
|
23
|
-
<circle cx="8" cy="8" r="6.3" stroke="currentColor" stroke-width="1.4" />
|
|
24
|
-
<rect x="7" y="4" width="2" height="5" fill="currentColor" />
|
|
25
|
-
<rect x="7" y="10" width="2" height="2" fill="currentColor" />
|
|
26
|
-
</svg>
|
|
27
|
-
|
|
28
|
-
<p class="text-sm truncate pr-1">{error.message}</p>
|
|
29
|
-
</m.div>
|
|
30
|
-
)}
|
|
31
|
-
</AnimatePresence>
|
|
32
|
-
);
|
|
33
|
-
};
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { AnalyticsService, ApiClient, Logger } from '@inploi/sdk';
|
|
2
|
-
import { useFocusGuards } from '@radix-ui/react-focus-guards';
|
|
3
|
-
import { AnimatePresence } from 'framer-motion';
|
|
4
|
-
import { useEffect, useLayoutEffect } from 'react';
|
|
5
|
-
import { match } from 'ts-pattern';
|
|
6
|
-
import { StartedJobApplication, application, inputHeight, viewState } from '~/chatbot.state';
|
|
7
|
-
import { submissionsToPayload } from '~/chatbot.utils';
|
|
8
|
-
|
|
9
|
-
import { ERROR_MESSAGES } from '../chatbot.constants';
|
|
10
|
-
import { createFlowInterpreter } from '../interpreter/interpreter';
|
|
11
|
-
import { ChatInput } from './chat-input/chat-input';
|
|
12
|
-
import { JobApplicationMessages } from './job-application-messages';
|
|
13
|
-
import { useChatService } from './useChatService';
|
|
14
|
-
|
|
15
|
-
type JobApplicationContentProps = {
|
|
16
|
-
apiClient: ApiClient;
|
|
17
|
-
logger: Logger;
|
|
18
|
-
currentApplication: StartedJobApplication;
|
|
19
|
-
analytics: AnalyticsService;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export const JobApplicationContent = ({
|
|
23
|
-
currentApplication,
|
|
24
|
-
logger,
|
|
25
|
-
apiClient,
|
|
26
|
-
analytics,
|
|
27
|
-
}: JobApplicationContentProps) => {
|
|
28
|
-
const { chatRef, chatService, isBotTyping, onSubmitSuccessFn, scrollToEnd } = useChatService({
|
|
29
|
-
logger,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
useFocusGuards();
|
|
33
|
-
|
|
34
|
-
const view = viewState.value;
|
|
35
|
-
useLayoutEffect(() => {
|
|
36
|
-
// This significantly improves performance for maximising the view
|
|
37
|
-
if (view === 'maximised') scrollToEnd({ behavior: 'instant' });
|
|
38
|
-
}, [scrollToEnd, view]);
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
scrollToEnd({ behavior: 'smooth' });
|
|
42
|
-
}, [currentApplication.data.messages, scrollToEnd]);
|
|
43
|
-
|
|
44
|
-
useLayoutEffect(() => {
|
|
45
|
-
scrollToEnd({ behavior: 'instant' });
|
|
46
|
-
const currentApplication = application.current$.peek();
|
|
47
|
-
if (!currentApplication || currentApplication.data.isFinished) return;
|
|
48
|
-
|
|
49
|
-
const { interpret, abort } = createFlowInterpreter({
|
|
50
|
-
flow: currentApplication.flow.nodes,
|
|
51
|
-
chatService,
|
|
52
|
-
getSubmissions: () => application.current$.peek()?.data.submissions,
|
|
53
|
-
beforeStart: async () => {
|
|
54
|
-
application.setInput(undefined);
|
|
55
|
-
|
|
56
|
-
const fromBeginning = currentApplication.data.messages.length === 0;
|
|
57
|
-
if (fromBeginning) {
|
|
58
|
-
analytics.log({
|
|
59
|
-
event: 'APPLY_START',
|
|
60
|
-
properties: { job_id: currentApplication.job.id },
|
|
61
|
-
customProperties: {
|
|
62
|
-
flow_id: currentApplication.flow.id,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
} else {
|
|
66
|
-
await chatService.send({
|
|
67
|
-
message: {
|
|
68
|
-
type: 'system',
|
|
69
|
-
text: 'Restored in progress application',
|
|
70
|
-
variant: 'info',
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
onInterpret: node => {
|
|
76
|
-
application.setCurrentNodeId(node.id);
|
|
77
|
-
},
|
|
78
|
-
onFlowEnd: async lastNode => {
|
|
79
|
-
application.markAsFinished();
|
|
80
|
-
return match(lastNode)
|
|
81
|
-
.with({ type: 'abandon-flow' }, () => {
|
|
82
|
-
chatService.send({
|
|
83
|
-
message: {
|
|
84
|
-
type: 'system',
|
|
85
|
-
text: 'Application ended',
|
|
86
|
-
variant: 'success',
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
})
|
|
90
|
-
.with({ type: 'complete-flow' }, async () => {
|
|
91
|
-
const submissions = application.current$.peek()?.data.submissions;
|
|
92
|
-
if (!submissions) throw new Error(ERROR_MESSAGES.no_submissions);
|
|
93
|
-
|
|
94
|
-
const response = await apiClient.fetch(`/flow/job/${currentApplication.job.id}`, {
|
|
95
|
-
method: 'POST',
|
|
96
|
-
body: JSON.stringify(submissionsToPayload({ application: currentApplication, submissions })),
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
match(response)
|
|
100
|
-
.with({ message: 'Success' }, () => {
|
|
101
|
-
chatService.send({
|
|
102
|
-
message: {
|
|
103
|
-
type: 'system',
|
|
104
|
-
text: 'Application submitted',
|
|
105
|
-
variant: 'success',
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
})
|
|
109
|
-
.otherwise(response => {
|
|
110
|
-
logger.error(response);
|
|
111
|
-
chatService.send({
|
|
112
|
-
message: {
|
|
113
|
-
type: 'system',
|
|
114
|
-
text: 'Error submitting application',
|
|
115
|
-
variant: 'error',
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
})
|
|
120
|
-
.otherwise(() => {
|
|
121
|
-
logger.error(ERROR_MESSAGES.invalid_end_node, lastNode);
|
|
122
|
-
});
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
interpret(currentApplication.data.currentNodeId);
|
|
127
|
-
|
|
128
|
-
return abort;
|
|
129
|
-
}, [analytics, apiClient, chatService, logger, scrollToEnd]);
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<>
|
|
133
|
-
<div
|
|
134
|
-
ref={chatRef}
|
|
135
|
-
className="relative max-w-full flex-grow flex flex-col hide-scrollbars overflow-y-scroll"
|
|
136
|
-
style={{ WebkitOverflowScrolling: 'touch', paddingBottom: inputHeight.value }}
|
|
137
|
-
>
|
|
138
|
-
<AnimatePresence>
|
|
139
|
-
<JobApplicationMessages isBotTyping={isBotTyping} messages={currentApplication.data.messages} />
|
|
140
|
-
</AnimatePresence>
|
|
141
|
-
</div>
|
|
142
|
-
<ChatInput onInputChange={() => scrollToEnd({ behavior: 'smooth' })} onSubmit={onSubmitSuccessFn} />
|
|
143
|
-
</>
|
|
144
|
-
);
|
|
145
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { AnimatePresence } from 'framer-motion';
|
|
2
|
-
import { P, match } from 'ts-pattern';
|
|
3
|
-
|
|
4
|
-
import { ChatMessage } from '../chatbot.state';
|
|
5
|
-
import { ChatBubble } from './chat-bubble';
|
|
6
|
-
import { FileThumbnail } from './chat-input/chat-input.file';
|
|
7
|
-
// import { AnimatePresence } from './motion/animate-presence';
|
|
8
|
-
import { TypingIndicator } from './typing-indicator';
|
|
9
|
-
|
|
10
|
-
type JobApplicationMessagesProps = {
|
|
11
|
-
messages: ChatMessage[];
|
|
12
|
-
isBotTyping: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const authorToSide = {
|
|
16
|
-
bot: 'left',
|
|
17
|
-
user: 'right',
|
|
18
|
-
} as const;
|
|
19
|
-
|
|
20
|
-
export const JobApplicationMessages = ({ messages, isBotTyping }: JobApplicationMessagesProps) => {
|
|
21
|
-
return (
|
|
22
|
-
<ol
|
|
23
|
-
aria-label="Chat messages"
|
|
24
|
-
class="p-2 justify-end pt-[calc(var(--header-height)+1rem)] flex flex-col gap-2 flex-grow"
|
|
25
|
-
>
|
|
26
|
-
<AnimatePresence initial={false}>
|
|
27
|
-
{messages.map((message, i) => (
|
|
28
|
-
<li class="flex" key={i}>
|
|
29
|
-
{match(message)
|
|
30
|
-
.with({ type: 'system' }, message => (
|
|
31
|
-
<p class="uppercase w-full drop-shadow-[0_1.5px_white] text-[10px] text-neutral-8 select-none tracking-widest text-center py-2">
|
|
32
|
-
{message.text}
|
|
33
|
-
</p>
|
|
34
|
-
))
|
|
35
|
-
.with({ type: 'text', author: P.union('bot', 'user') }, message => {
|
|
36
|
-
return (
|
|
37
|
-
<ChatBubble key={i} side={authorToSide[message.author]}>
|
|
38
|
-
{message.text}
|
|
39
|
-
</ChatBubble>
|
|
40
|
-
);
|
|
41
|
-
})
|
|
42
|
-
.with({ type: 'image' }, image => (
|
|
43
|
-
<img
|
|
44
|
-
class="max-w-[min(100%,24rem)] w-full rounded-2xl shadow-surface-md"
|
|
45
|
-
src={image.url}
|
|
46
|
-
style={{ aspectRatio: image.width / image.height }}
|
|
47
|
-
/>
|
|
48
|
-
))
|
|
49
|
-
.with({ type: 'file' }, file => {
|
|
50
|
-
return (
|
|
51
|
-
<FileThumbnail
|
|
52
|
-
class={file.author === 'bot' ? '' : 'ml-auto'}
|
|
53
|
-
file={{ name: file.fileName, sizeKb: file.fileSizeKb }}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
})
|
|
57
|
-
.exhaustive()}
|
|
58
|
-
</li>
|
|
59
|
-
))}
|
|
60
|
-
</AnimatePresence>
|
|
61
|
-
<aside aria-hidden>{isBotTyping && <TypingIndicator />}</aside>
|
|
62
|
-
</ol>
|
|
63
|
-
);
|
|
64
|
-
};
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export const LoadingIndicator = () => (
|
|
2
|
-
<svg viewBox="0 0 24 24">
|
|
3
|
-
<style>
|
|
4
|
-
{`#s1{animation:3s linear infinite forwards s1__to}@keyframes s1__to{0%{transform:translate(12px,0)}66.666667%{transform:translate(12px,0);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}100%{transform:translate(12px,12px)}}#s2{animation:3s linear infinite forwards s2__ts}@keyframes s2__ts{0%{transform:scale(0,0)}70%{transform:scale(0,0);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}100%{transform:scale(1,1)}}#s3{animation:3s linear infinite forwards s3__to}@keyframes s3__to{0%{transform:translate(12px,12px);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}100%,33.333333%{transform:translate(12px,24px)}}#s4{animation:3s linear infinite forwards s4__ts}@keyframes s4__ts{0%{transform:scale(1,1);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}100%,30%{transform:scale(0,0)}}#s5{animation:3s linear infinite forwards s5__to}@keyframes s5__to{0%{transform:translate(12px,0);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}33.333333%{transform:translate(12px,12.045742px);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}100%,66.666667%{transform:translate(12px,24px)}}#s6{animation:3s linear infinite forwards s6__ts}@keyframes s6__ts{0%,100%,63.333333%{transform:scale(0,0)}3.333333%{transform:scale(0,0);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}33.333333%{transform:scale(1,1);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}}#s7{animation:3s linear infinite forwards s7__to}@keyframes s7__to{0%{transform:translate(12px,0)}33.333333%{transform:translate(12px,0);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}66.666667%{transform:translate(12px,12px);animation-timing-function:cubic-bezier(0.77,0,0.175,1)}100%{transform:translate(12px,24px)}}#s8{animation:3s linear infinite forwards s8__ts}@keyframes s8__ts{0%,100%,96.666667%{transform:scale(0,0)}36.666667%{transform:scale(0,0);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}66.666667%{transform:scale(1,1);animation-timing-function:cubic-bezier(0.86,0,0.07,1)}}`}
|
|
5
|
-
</style>
|
|
6
|
-
<g id="s1" transform="translate(12,0)">
|
|
7
|
-
<g id="s2" transform="scale(0,0)">
|
|
8
|
-
<circle r="6.5" transform="translate(0,0)" fill="hsl(226, 70.0%, 55.5%)" />
|
|
9
|
-
</g>
|
|
10
|
-
</g>
|
|
11
|
-
<g id="s3" transform="translate(12,12)">
|
|
12
|
-
<g id="s4" transform="scale(1,1)">
|
|
13
|
-
<circle r="6.5" transform="translate(0,0)" fill="hsl(226, 70.0%, 55.5%)" />
|
|
14
|
-
</g>
|
|
15
|
-
</g>
|
|
16
|
-
<g id="s5" transform="translate(12,0)">
|
|
17
|
-
<g id="s6" transform="scale(0,0)">
|
|
18
|
-
<path
|
|
19
|
-
d="M6.5,13c3.5899,0,6.5-2.9101,6.5-6.5s-2.9101-6.5-6.5-6.5-6.5,2.91015-6.5,6.5s2.91015,6.5,6.5,6.5Zm0-4C7.88071,9,9,7.88071,9,6.5s-1.11929-2.5-2.5-2.5-2.5,1.11929-2.5,2.5s1.11929,2.5,2.5,2.5Z"
|
|
20
|
-
transform="translate(-6.5,-6.5)"
|
|
21
|
-
clip-rule="evenodd"
|
|
22
|
-
fill="hsl(226, 70.0%, 55.5%)"
|
|
23
|
-
fill-rule="evenodd"
|
|
24
|
-
/>
|
|
25
|
-
</g>
|
|
26
|
-
</g>
|
|
27
|
-
<g id="s7" transform="translate(12,0)">
|
|
28
|
-
<g id="s8" transform="scale(0,0)">
|
|
29
|
-
<path
|
|
30
|
-
d="M0,6c0,3.58984,2.91016,6.5,6.5,6.5s6.5-2.91016,6.5-6.5h-4C9,7.38086,7.88086,8.5,6.5,8.5s-2.5-1.11914-2.5-2.5h-4Z"
|
|
31
|
-
transform="translate(-6.5,-9.25)"
|
|
32
|
-
fill="hsl(226, 70.0%, 55.5%)"
|
|
33
|
-
/>
|
|
34
|
-
</g>
|
|
35
|
-
</g>
|
|
36
|
-
</svg>
|
|
37
|
-
);
|
package/src/ui/send-button.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
import { ComponentProps } from 'preact';
|
|
3
|
-
|
|
4
|
-
export const SendButton = ({ class: className, ...props }: ComponentProps<'button'>) => (
|
|
5
|
-
<button
|
|
6
|
-
class={clsx(
|
|
7
|
-
'p-2 flex-shrink-0 bg-accent-9 active:bg-accent-12 active:text-accent-6 rounded-full text-lowest pointer-coarse:touch-hitbox disabled:opacity-50 disabled:cursor-not-allowed',
|
|
8
|
-
className,
|
|
9
|
-
)}
|
|
10
|
-
{...props}
|
|
11
|
-
>
|
|
12
|
-
<svg
|
|
13
|
-
class="block"
|
|
14
|
-
width="16"
|
|
15
|
-
height="16"
|
|
16
|
-
viewBox="0 0 16 16"
|
|
17
|
-
fill="transparent"
|
|
18
|
-
stroke="currentColor"
|
|
19
|
-
stroke-linecap="round"
|
|
20
|
-
stroke-width="2"
|
|
21
|
-
>
|
|
22
|
-
<title>Send</title>
|
|
23
|
-
<path d="M3.5 7.5L8 3L12.5 7.5" />
|
|
24
|
-
<path d="M8 4V13" />
|
|
25
|
-
</svg>
|
|
26
|
-
</button>
|
|
27
|
-
);
|
package/src/ui/transition.tsx
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export type TransitionState = 'entering' | 'entered' | 'exiting' | 'exited';
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import clsx from 'clsx';
|
|
2
|
-
import { ComponentProps } from 'react';
|
|
3
|
-
|
|
4
|
-
export const TypingIndicator = ({ className, ...props }: ComponentProps<'div'>) => {
|
|
5
|
-
return (
|
|
6
|
-
<div class={clsx('flex gap-1 p-4', className)} {...props}>
|
|
7
|
-
{Array.from({ length: 3 }, (_, i) => (
|
|
8
|
-
<div class="h-1.5 w-1.5 rounded-full bg-accent-9 animate-bounce" style={{ animationDelay: `${-i * 200}ms` }} />
|
|
9
|
-
))}
|
|
10
|
-
</div>
|
|
11
|
-
);
|
|
12
|
-
};
|
package/src/ui/useChatService.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Logger } from '@inploi/sdk';
|
|
2
|
-
import { useMemo, useRef, useState } from 'preact/hooks';
|
|
3
|
-
import { match } from 'ts-pattern';
|
|
4
|
-
import { application } from '~/chatbot.state';
|
|
5
|
-
import { ChatService } from '~/interpreter/interpreter';
|
|
6
|
-
|
|
7
|
-
import { SubmitSuccessFn } from './chat-input/chat-input';
|
|
8
|
-
|
|
9
|
-
const TYPING_SPEED_MS_PER_CHARACTER = 25;
|
|
10
|
-
|
|
11
|
-
type UseChatServiceParams = {
|
|
12
|
-
logger?: Logger;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export const useChatService = ({ logger }: UseChatServiceParams) => {
|
|
16
|
-
const chatRef = useRef<HTMLDivElement>(null);
|
|
17
|
-
const [isBotTyping, setIsBotTyping] = useState(false);
|
|
18
|
-
const [onSubmitSuccessFn, setOnSubmitSuccessFn] = useState<SubmitSuccessFn>(() => () => {});
|
|
19
|
-
const scrollToEnd = useMemo(
|
|
20
|
-
() => (options?: Omit<ScrollToOptions, 'top'>) =>
|
|
21
|
-
chatRef.current?.scrollTo({ top: chatRef.current.scrollHeight, ...options }),
|
|
22
|
-
[chatRef],
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const chatService = useMemo(() => {
|
|
26
|
-
const chatService: ChatService = {
|
|
27
|
-
send: async ({ message, signal }) => {
|
|
28
|
-
await match(message)
|
|
29
|
-
/** Delay sending and add typing indicator if bot is sending a message */
|
|
30
|
-
.with({ author: 'bot', type: 'text' }, async message => {
|
|
31
|
-
if (signal?.aborted) {
|
|
32
|
-
logger?.info(`Aborted sending message`);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
setIsBotTyping(true);
|
|
36
|
-
const typingTime = message.text.length * TYPING_SPEED_MS_PER_CHARACTER;
|
|
37
|
-
await new Promise(resolve => {
|
|
38
|
-
return setTimeout(resolve, typingTime, { signal });
|
|
39
|
-
});
|
|
40
|
-
setIsBotTyping(false);
|
|
41
|
-
})
|
|
42
|
-
.otherwise(async () => void 0);
|
|
43
|
-
|
|
44
|
-
/** The signal could have been aborted while typing */
|
|
45
|
-
if (signal?.aborted) {
|
|
46
|
-
logger?.info(`Aborted sending message`);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
application.addMessage(message);
|
|
50
|
-
},
|
|
51
|
-
input: async input => {
|
|
52
|
-
application.setInput(input);
|
|
53
|
-
|
|
54
|
-
return await new Promise(resolve => {
|
|
55
|
-
const submitFunction: SubmitSuccessFn = submission => {
|
|
56
|
-
application.setInput(undefined);
|
|
57
|
-
application.setSubmission(input.key, submission);
|
|
58
|
-
resolve(submission as any);
|
|
59
|
-
};
|
|
60
|
-
setOnSubmitSuccessFn(() => submitFunction);
|
|
61
|
-
});
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return chatService;
|
|
66
|
-
}, [logger]);
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
chatRef,
|
|
70
|
-
chatService,
|
|
71
|
-
isBotTyping,
|
|
72
|
-
onSubmitSuccessFn,
|
|
73
|
-
scrollToEnd,
|
|
74
|
-
};
|
|
75
|
-
};
|
package/src/ui/useFocus.ts
DELETED
package/src/vite-env.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/// <reference types="vite/client" />
|