@inploi/plugin-chatbot 1.0.7 → 2.0.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.
- package/CHANGELOG.md +11 -0
- package/package.json +11 -7
- package/src/chatbot.api.ts +14 -14
- package/src/chatbot.constants.ts +3 -0
- package/src/chatbot.css +8 -1
- package/src/chatbot.dom.ts +0 -6
- package/src/chatbot.idb.ts +17 -0
- package/src/chatbot.state.ts +52 -143
- package/src/chatbot.ts +16 -31
- package/src/chatbot.utils.ts +21 -9
- package/src/index.dev.ts +6 -0
- package/src/mocks/example.flows.ts +1 -1
- package/src/ui/chat-bubble.tsx +21 -5
- package/src/ui/chat-input/chat-input.boolean.tsx +10 -5
- package/src/ui/chat-input/chat-input.file.tsx +8 -6
- package/src/ui/chat-input/chat-input.multiple-choice.tsx +52 -27
- package/src/ui/chat-input/chat-input.text.tsx +22 -16
- package/src/ui/chat-input/chat-input.tsx +47 -23
- package/src/ui/chatbot-header.tsx +35 -26
- package/src/ui/chatbot.tsx +95 -43
- package/src/ui/input-error.tsx +25 -31
- package/src/ui/job-application-content.tsx +65 -42
- package/src/ui/job-application-messages.tsx +42 -34
- package/src/ui/useChatService.ts +10 -17
- package/src/ui/useFocus.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @inploi/plugin-chatbot
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2975c09: Replace `zustand` with `idb-keyval` and `@preact/signals` to better utilise indexeddb and reduce bundle size
|
|
8
|
+
- cc46781: Track started applications via the new `analytics` service exposed by sdk
|
|
9
|
+
- Updated dependencies [d8dc36f]
|
|
10
|
+
- Updated dependencies [d8dc36f]
|
|
11
|
+
- @inploi/sdk@1.5.0
|
|
12
|
+
- @inploi/core@1.5.6
|
|
13
|
+
|
|
3
14
|
## 1.0.7
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inploi/plugin-chatbot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hookform/resolvers": "^3.3.2",
|
|
7
|
+
"@preact/signals": "^1.2.2",
|
|
8
|
+
"@radix-ui/react-dialog": "^1.0.5",
|
|
9
|
+
"@radix-ui/react-focus-guards": "^1.0.1",
|
|
10
|
+
"@radix-ui/react-focus-scope": "^1.0.4",
|
|
11
|
+
"@radix-ui/react-slot": "^1.0.2",
|
|
7
12
|
"class-variance-authority": "^0.7.0",
|
|
8
13
|
"clsx": "^2.0.0",
|
|
14
|
+
"framer-motion": "^10.16.5",
|
|
9
15
|
"idb-keyval": "^6.2.1",
|
|
10
|
-
"immer": "^10.0.3",
|
|
11
16
|
"preact": "^10.16.0",
|
|
12
17
|
"react": "npm:@preact/compat",
|
|
13
18
|
"react-dom": "npm:@preact/compat",
|
|
14
19
|
"react-hook-form": "^7.48.2",
|
|
20
|
+
"react-remove-scroll": "^2.5.7",
|
|
15
21
|
"react-transition-group": "^4.4.5",
|
|
16
22
|
"swr": "^2.2.4",
|
|
17
23
|
"tailwindcss-touch": "^1.0.1",
|
|
18
24
|
"ts-pattern": "^5.0.5",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"zustand": "^4.4.4",
|
|
22
|
-
"@inploi/core": "1.5.5"
|
|
25
|
+
"valibot": "^0.21.0",
|
|
26
|
+
"@inploi/core": "1.5.6"
|
|
23
27
|
},
|
|
24
28
|
"peerDependencies": {
|
|
25
29
|
"@inploi/sdk": "*"
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
"typescript": "^5.3.2",
|
|
43
47
|
"vite": "^4.4.5",
|
|
44
48
|
"vite-tsconfig-paths": "^4.2.1",
|
|
45
|
-
"@inploi/sdk": "1.
|
|
49
|
+
"@inploi/sdk": "1.5.0",
|
|
46
50
|
"eslint-config-custom": "0.1.0",
|
|
47
51
|
"tsconfig": "0.1.0"
|
|
48
52
|
},
|
package/src/chatbot.api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { FlowNode
|
|
1
|
+
import { FlowNode } from '@inploi/core/flows';
|
|
2
2
|
import { ApiClient } from '@inploi/sdk';
|
|
3
|
-
import
|
|
3
|
+
import { any, array, coerce, number, object, optional, parse, string } from 'valibot';
|
|
4
4
|
|
|
5
5
|
export type JobApplication = {
|
|
6
6
|
job: {
|
|
@@ -18,19 +18,19 @@ export type JobApplication = {
|
|
|
18
18
|
};
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
job:
|
|
23
|
-
id:
|
|
24
|
-
title:
|
|
21
|
+
const ApplicationSchema = object({
|
|
22
|
+
job: object({
|
|
23
|
+
id: coerce(string(), String),
|
|
24
|
+
title: string(),
|
|
25
25
|
}),
|
|
26
|
-
company:
|
|
27
|
-
name:
|
|
28
|
-
logo:
|
|
26
|
+
company: object({
|
|
27
|
+
name: string(),
|
|
28
|
+
logo: optional(string()),
|
|
29
29
|
}),
|
|
30
|
-
flow:
|
|
31
|
-
id:
|
|
32
|
-
nodes:
|
|
33
|
-
version:
|
|
30
|
+
flow: object({
|
|
31
|
+
id: coerce(string(), String),
|
|
32
|
+
nodes: array(any()),
|
|
33
|
+
version: number(),
|
|
34
34
|
}),
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -42,5 +42,5 @@ export async function getApplicationData({
|
|
|
42
42
|
apiClient: ApiClient;
|
|
43
43
|
}): Promise<JobApplication> {
|
|
44
44
|
const rawData = await apiClient.fetch(`/flow/job/${jobId}`);
|
|
45
|
-
return
|
|
45
|
+
return parse(ApplicationSchema, rawData);
|
|
46
46
|
}
|
package/src/chatbot.constants.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export const CHATBOT_ELEMENT_ID = 'isdk';
|
|
2
2
|
|
|
3
|
+
export const HEADER_HEIGHT = 44;
|
|
4
|
+
|
|
3
5
|
export const ERROR_MESSAGES = {
|
|
6
|
+
not_in_local_storage: 'Application not found in local storage',
|
|
4
7
|
invalid_end_node: 'Unexpected node type to finish flow',
|
|
5
8
|
no_submissions: 'Application ended without any fields submitted',
|
|
6
9
|
};
|
package/src/chatbot.css
CHANGED
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
box-sizing: border-box;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
:is(ul) {
|
|
82
|
+
:is(ul, ol) {
|
|
83
83
|
list-style: none;
|
|
84
84
|
padding: 0;
|
|
85
85
|
margin: 0;
|
|
@@ -90,11 +90,18 @@
|
|
|
90
90
|
padding: 0;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
:is(li) {
|
|
94
|
+
margin: 0;
|
|
95
|
+
padding: 0;
|
|
96
|
+
list-style: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
:is(button) {
|
|
94
100
|
margin: 0;
|
|
95
101
|
padding: 0;
|
|
96
102
|
border: unset;
|
|
97
103
|
background: unset;
|
|
104
|
+
text-align: unset;
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
107
|
}
|
package/src/chatbot.dom.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { CHATBOT_ELEMENT_ID } from './chatbot.constants';
|
|
2
2
|
|
|
3
|
-
export const overlayClassNames =
|
|
4
|
-
'data-[transition=entering]:opacity-0 data-[transition=exiting]:opacity-0 ease-expo-out duration-500 transition-opacity bg-neutral-12/60 fixed inset-0';
|
|
5
|
-
|
|
6
3
|
export const createChatbotDomManager = () => {
|
|
7
4
|
let chatbotElement: HTMLDivElement | null = null;
|
|
8
5
|
return {
|
|
@@ -15,9 +12,6 @@ export const createChatbotDomManager = () => {
|
|
|
15
12
|
chatbotElement = newElement;
|
|
16
13
|
return newElement;
|
|
17
14
|
},
|
|
18
|
-
renderLoading: (element: HTMLElement) => {
|
|
19
|
-
element.innerHTML = `<div class="${overlayClassNames}" />`;
|
|
20
|
-
},
|
|
21
15
|
};
|
|
22
16
|
};
|
|
23
17
|
export type ChatbotDomManager = ReturnType<typeof createChatbotDomManager>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createStore, get, set } from 'idb-keyval';
|
|
2
|
+
|
|
3
|
+
import { JobApplication } from './chatbot.api';
|
|
4
|
+
import { ApplicationData, getCacheKey } from './chatbot.state';
|
|
5
|
+
|
|
6
|
+
const store = createStore('inploi', 'applications');
|
|
7
|
+
|
|
8
|
+
export const idb = {
|
|
9
|
+
getApplicationData: async (application: JobApplication) => {
|
|
10
|
+
const key = getCacheKey(application);
|
|
11
|
+
return await get<ApplicationData>(key, store);
|
|
12
|
+
},
|
|
13
|
+
setApplicationData: async (params: { application: JobApplication; data: ApplicationData }) => {
|
|
14
|
+
const key = getCacheKey(params.application);
|
|
15
|
+
return await set(key, params.data, store);
|
|
16
|
+
},
|
|
17
|
+
};
|
package/src/chatbot.state.ts
CHANGED
|
@@ -1,52 +1,59 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { create } from 'zustand';
|
|
4
|
-
import { StateStorage, createJSONStorage, persist } from 'zustand/middleware';
|
|
1
|
+
import { invariant } from '@inploi/core/common';
|
|
2
|
+
import { Signal, batch, signal } from '@preact/signals';
|
|
5
3
|
|
|
6
4
|
import { JobApplication } from './chatbot.api';
|
|
5
|
+
import { idb } from './chatbot.idb';
|
|
7
6
|
import { DistributivePick, getHeadOrThrow } from './chatbot.utils';
|
|
8
7
|
import { ChatInput } from './ui/chat-input/chat-input';
|
|
9
8
|
import { ChatbotInput } from './ui/chat-input/chat-input';
|
|
10
9
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
return (await get(name)) || null;
|
|
14
|
-
},
|
|
15
|
-
setItem: async (name: string, value: string): Promise<void> => {
|
|
16
|
-
await set(name, value);
|
|
17
|
-
},
|
|
18
|
-
removeItem: async (name: string): Promise<void> => {
|
|
19
|
-
await del(name);
|
|
20
|
-
},
|
|
21
|
-
};
|
|
10
|
+
export const getCacheKey = (application: JobApplication) =>
|
|
11
|
+
[application.job.id, application.flow.id, application.flow.version].join('/');
|
|
22
12
|
|
|
23
13
|
export type ViewState = 'maximised' | 'minimised';
|
|
14
|
+
export const viewState = signal<ViewState>('maximised');
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
export const inputHeight = signal(0);
|
|
17
|
+
|
|
18
|
+
export type StartedJobApplication = JobApplication & { data: ApplicationData };
|
|
19
|
+
const currentApplication: Signal<StartedJobApplication | null> = signal<StartedJobApplication | null>(null);
|
|
20
|
+
export const cancelCurrentApplication = () => {
|
|
21
|
+
currentApplication.value = null;
|
|
31
22
|
};
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
currentApplication
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
},
|
|
41
|
-
cancelCurrentApplication: () => set({ currentApplication: null }),
|
|
42
|
-
setViewState: viewState => set({ viewState }),
|
|
43
|
-
}));
|
|
24
|
+
const updateApplicationData = async (updateFn: (data: ApplicationData) => ApplicationData) => {
|
|
25
|
+
const application = currentApplication.value;
|
|
26
|
+
invariant(application, 'No application to update');
|
|
27
|
+
const newData = updateFn(application.data);
|
|
28
|
+
currentApplication.value = { ...application, data: newData };
|
|
29
|
+
await idb.setApplicationData({ application, data: newData });
|
|
30
|
+
};
|
|
44
31
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
32
|
+
export const application = {
|
|
33
|
+
current$: currentApplication,
|
|
34
|
+
start: async (application: JobApplication) => {
|
|
35
|
+
const data = (await idb.getApplicationData(application)) ?? createNewApplicationData(application);
|
|
36
|
+
batch(() => {
|
|
37
|
+
viewState.value = 'maximised';
|
|
38
|
+
currentApplication.value = { ...application, data };
|
|
39
|
+
});
|
|
40
|
+
data.isFinished = false;
|
|
41
|
+
idb.setApplicationData({ application, data });
|
|
42
|
+
},
|
|
43
|
+
markAsFinished: () => updateApplicationData(data => ({ ...data, isFinished: true })),
|
|
44
|
+
setCurrentNodeId: (currentNodeId: string) => updateApplicationData(data => ({ ...data, currentNodeId })),
|
|
45
|
+
restart: () => {
|
|
46
|
+
const application = currentApplication.value;
|
|
47
|
+
invariant(application, 'No application to restart');
|
|
48
|
+
const data = createNewApplicationData(application);
|
|
49
|
+
currentApplication.value = { ...application, data };
|
|
50
|
+
idb.setApplicationData({ application, data });
|
|
51
|
+
},
|
|
52
|
+
addMessage: (message: ChatMessage) =>
|
|
53
|
+
updateApplicationData(data => ({ ...data, messages: [...data.messages, message] })),
|
|
54
|
+
setSubmission: (fieldKey: string, submission: ApplicationSubmission) =>
|
|
55
|
+
updateApplicationData(data => ({ ...data, submissions: { ...data.submissions, [fieldKey]: submission } })),
|
|
56
|
+
setInput: (input: ChatInput | undefined) => updateApplicationData(data => ({ ...data, currentInput: input })),
|
|
50
57
|
};
|
|
51
58
|
|
|
52
59
|
export type MessageAuthor = 'bot' | 'user';
|
|
@@ -63,8 +70,8 @@ export type KeyToSubmissionMap = {
|
|
|
63
70
|
[key: string]: ApplicationSubmission;
|
|
64
71
|
};
|
|
65
72
|
|
|
66
|
-
/**
|
|
67
|
-
export type
|
|
73
|
+
/** Dynamic part of an application */
|
|
74
|
+
export type ApplicationData = {
|
|
68
75
|
/** History of messages left in the chat */
|
|
69
76
|
messages: ChatMessage[];
|
|
70
77
|
submissions: KeyToSubmissionMap;
|
|
@@ -74,107 +81,9 @@ export type ApplicationLocalState = {
|
|
|
74
81
|
isFinished: boolean;
|
|
75
82
|
};
|
|
76
83
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
updateSubmission: (params: { application: JobApplication; fieldKey: string; data: ApplicationSubmission }) => void;
|
|
84
|
-
addMessage: (params: { application: JobApplication; data: ChatMessage }) => void;
|
|
85
|
-
resetLocalState: () => void;
|
|
86
|
-
setInput: (params: { application: JobApplication; data: ChatInput | null }) => void;
|
|
87
|
-
resetInput: (params: { application: JobApplication }) => void;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const getOrCreateApplication = (params: { application: JobApplication }, state: ApplicationsStore) => {
|
|
91
|
-
const existingApplication = state.applicationsLocalState[getCacheKey(params.application)];
|
|
92
|
-
if (existingApplication) return existingApplication;
|
|
93
|
-
const newApplication: ApplicationLocalState = {
|
|
94
|
-
messages: [],
|
|
95
|
-
submissions: {},
|
|
96
|
-
currentNodeId: getHeadOrThrow(params.application.flow.nodes).id,
|
|
97
|
-
isFinished: false,
|
|
98
|
-
};
|
|
99
|
-
state.applicationsLocalState[getCacheKey(params.application)] = newApplication;
|
|
100
|
-
return newApplication;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
export const getCacheKey = (application: JobApplication) =>
|
|
104
|
-
[application.job.id, application.flow.id, application.flow.version].join('/');
|
|
105
|
-
|
|
106
|
-
export const useLocalState = create<ApplicationsStore>()(
|
|
107
|
-
persist(
|
|
108
|
-
(set, get) => ({
|
|
109
|
-
applicationsLocalState: {},
|
|
110
|
-
getApplicationLocalState: application => get().applicationsLocalState[getCacheKey(application)],
|
|
111
|
-
updateApplicationLocalState: params =>
|
|
112
|
-
set(
|
|
113
|
-
produce((state: ApplicationsStore) => {
|
|
114
|
-
const existingApplication = state.applicationsLocalState[getCacheKey(params.application)];
|
|
115
|
-
if (!existingApplication) throw new Error('Application not found');
|
|
116
|
-
Object.assign(existingApplication, params.data);
|
|
117
|
-
}),
|
|
118
|
-
),
|
|
119
|
-
updateApplicationCurrentNode: params =>
|
|
120
|
-
set(
|
|
121
|
-
produce((state: ApplicationsStore) => {
|
|
122
|
-
const application = getOrCreateApplication(params, state);
|
|
123
|
-
application.currentNodeId = params.currentNodeId;
|
|
124
|
-
}),
|
|
125
|
-
),
|
|
126
|
-
resetApplicationState: application =>
|
|
127
|
-
set(
|
|
128
|
-
produce((state: ApplicationsStore) => {
|
|
129
|
-
const cacheKey = getCacheKey(application);
|
|
130
|
-
delete state.applicationsLocalState[cacheKey];
|
|
131
|
-
}),
|
|
132
|
-
),
|
|
133
|
-
updateSubmission: params =>
|
|
134
|
-
set(
|
|
135
|
-
produce((state: ApplicationsStore) => {
|
|
136
|
-
const application = getOrCreateApplication(params, state);
|
|
137
|
-
application.submissions[params.fieldKey] = params.data;
|
|
138
|
-
}),
|
|
139
|
-
),
|
|
140
|
-
addMessage: params =>
|
|
141
|
-
set(
|
|
142
|
-
produce((state: ApplicationsStore) => {
|
|
143
|
-
const application = getOrCreateApplication(params, state);
|
|
144
|
-
application.messages.push(params.data);
|
|
145
|
-
state.applicationsLocalState[getCacheKey(params.application)] = application;
|
|
146
|
-
}),
|
|
147
|
-
),
|
|
148
|
-
resetLocalState: () => set({ applicationsLocalState: {} }),
|
|
149
|
-
resetInput: params =>
|
|
150
|
-
set(
|
|
151
|
-
produce((state: ApplicationsStore) => {
|
|
152
|
-
const application = getOrCreateApplication(params, state);
|
|
153
|
-
application.currentInput = undefined;
|
|
154
|
-
}),
|
|
155
|
-
),
|
|
156
|
-
setInput: params =>
|
|
157
|
-
set(
|
|
158
|
-
produce((state: ApplicationsStore) => {
|
|
159
|
-
const application = getOrCreateApplication(params, state);
|
|
160
|
-
application.currentInput = params.data ?? undefined;
|
|
161
|
-
}),
|
|
162
|
-
),
|
|
163
|
-
}),
|
|
164
|
-
{
|
|
165
|
-
name: 'inploi-chatbot-application',
|
|
166
|
-
storage: createJSONStorage(() => storage),
|
|
167
|
-
// only store the jobApplications part of the state
|
|
168
|
-
partialize: state => ({ applicationsLocalState: state.applicationsLocalState }),
|
|
169
|
-
},
|
|
170
|
-
),
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
export const useApplicationLocalState = (application: JobApplication) =>
|
|
174
|
-
useLocalState(s => s.applicationsLocalState[getCacheKey(application)]);
|
|
175
|
-
|
|
176
|
-
export const useApplicationInput = (application: JobApplication) =>
|
|
177
|
-
useLocalState(s => s.applicationsLocalState[getCacheKey(application)]?.currentInput);
|
|
178
|
-
|
|
179
|
-
export const useApplicationSubmission = (application: JobApplication, key: string) =>
|
|
180
|
-
useLocalState(s => s.applicationsLocalState[getCacheKey(application)]?.submissions[key]);
|
|
84
|
+
const createNewApplicationData = (application: JobApplication): ApplicationData => ({
|
|
85
|
+
messages: [],
|
|
86
|
+
submissions: {},
|
|
87
|
+
currentNodeId: getHeadOrThrow(application.flow.nodes).id,
|
|
88
|
+
isFinished: false,
|
|
89
|
+
});
|
package/src/chatbot.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createPlugin } from '@inploi/sdk';
|
|
2
2
|
import { h, render } from 'preact';
|
|
3
|
+
import { Chatbot } from '~/ui/chatbot';
|
|
3
4
|
|
|
4
5
|
import { getApplicationData } from './chatbot.api';
|
|
5
6
|
import './chatbot.css';
|
|
6
7
|
import { ChatbotDomManager, createChatbotDomManager } from './chatbot.dom';
|
|
7
|
-
import {
|
|
8
|
+
import { application, cancelCurrentApplication } from './chatbot.state';
|
|
8
9
|
|
|
9
10
|
export const chatbotPlugin = ({
|
|
10
11
|
_internal_domManager: dom = createChatbotDomManager(),
|
|
@@ -12,15 +13,20 @@ export const chatbotPlugin = ({
|
|
|
12
13
|
geolocationApiKey?: string;
|
|
13
14
|
_internal_domManager?: ChatbotDomManager;
|
|
14
15
|
}) =>
|
|
15
|
-
createPlugin(({ apiClient, logger }) => {
|
|
16
|
-
|
|
17
|
-
const
|
|
16
|
+
createPlugin(({ apiClient, logger, analytics }) => {
|
|
17
|
+
let prepared = false;
|
|
18
|
+
const renderAndPrepare = () => {
|
|
19
|
+
const chatbotElement = dom.getOrCreateChatbotElement();
|
|
20
|
+
render(h(Chatbot, { apiClient, logger, analytics }), chatbotElement);
|
|
21
|
+
prepared = true;
|
|
22
|
+
};
|
|
18
23
|
|
|
19
24
|
return {
|
|
20
|
-
/** Optionally eagerly
|
|
25
|
+
/** Optionally eagerly renders the interface ahead of application requests. */
|
|
21
26
|
prepare: async () => {
|
|
22
27
|
try {
|
|
23
|
-
|
|
28
|
+
if (prepared) return;
|
|
29
|
+
renderAndPrepare();
|
|
24
30
|
logger.info('Chatbot plugin prepared');
|
|
25
31
|
} catch (error) {
|
|
26
32
|
console.error(error);
|
|
@@ -29,40 +35,19 @@ export const chatbotPlugin = ({
|
|
|
29
35
|
},
|
|
30
36
|
startApplication: async ({ jobId }: { jobId: string }) => {
|
|
31
37
|
try {
|
|
32
|
-
const chatbotElement = dom.getOrCreateChatbotElement();
|
|
33
38
|
cancelCurrentApplication();
|
|
39
|
+
const applicationData = await getApplicationData({ jobId, apiClient });
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/** We concurrently lazy load the module and data */
|
|
38
|
-
const [dataPromise, chatbotPromise] = await Promise.allSettled([
|
|
39
|
-
getApplicationData({ jobId, apiClient }),
|
|
40
|
-
import('~/ui/chatbot').then(m => m.Chatbot),
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
if (dataPromise.status === 'rejected') throw dataPromise.reason;
|
|
44
|
-
if (chatbotPromise.status === 'rejected') throw chatbotPromise.reason;
|
|
41
|
+
await application.start(applicationData);
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
const application = dataPromise.value;
|
|
48
|
-
startApplication(application);
|
|
49
|
-
logger.info(`Starting application for job "${application.job.id}" using flow "${application.flow.id}"`);
|
|
50
|
-
chatbotElement.innerHTML = '';
|
|
51
|
-
|
|
52
|
-
render(h(Chatbot, { apiClient, logger }), chatbotElement);
|
|
43
|
+
if (!prepared) renderAndPrepare();
|
|
53
44
|
} catch (error) {
|
|
54
45
|
console.error(error);
|
|
55
46
|
logger.error('Error starting application', error);
|
|
56
47
|
}
|
|
57
48
|
},
|
|
58
49
|
closeApplication: async () => {
|
|
59
|
-
|
|
60
|
-
chatbotElement.innerHTML = '';
|
|
61
|
-
cancelCurrentApplication();
|
|
62
|
-
},
|
|
63
|
-
/** Resets the user saved data for every application. */
|
|
64
|
-
resetLocalState: async () => {
|
|
65
|
-
resetLocalState();
|
|
50
|
+
logger.info('Closing application from an external source');
|
|
66
51
|
cancelCurrentApplication();
|
|
67
52
|
},
|
|
68
53
|
};
|
package/src/chatbot.utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FlowNode } from '@inploi/core/flows';
|
|
2
2
|
|
|
3
|
-
import { ApplicationSubmission, KeyToSubmissionMap } from './chatbot.state';
|
|
3
|
+
import { ApplicationSubmission, KeyToSubmissionMap, StartedJobApplication } from './chatbot.state';
|
|
4
4
|
|
|
5
5
|
export type DistributivePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
|
|
6
6
|
|
|
@@ -10,14 +10,26 @@ export const getHeadOrThrow = (nodes: FlowNode[]) => {
|
|
|
10
10
|
return head;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const submissionsToPayload = (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
export const submissionsToPayload = ({
|
|
14
|
+
application,
|
|
15
|
+
submissions,
|
|
16
|
+
}: {
|
|
17
|
+
application: StartedJobApplication;
|
|
18
|
+
submissions: KeyToSubmissionMap;
|
|
19
|
+
}) => {
|
|
20
|
+
const payload = {
|
|
21
|
+
flowId: application.flow.id,
|
|
22
|
+
jobId: application.job.id,
|
|
23
|
+
submissions: Object.entries(submissions).reduce(
|
|
24
|
+
(acc, [key, submission]) => {
|
|
25
|
+
acc[key] = submission.value;
|
|
26
|
+
return acc;
|
|
27
|
+
},
|
|
28
|
+
{} as Record<string, ApplicationSubmission['value']>,
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return payload;
|
|
21
33
|
};
|
|
22
34
|
|
|
23
35
|
export const isSubmissionOfType =
|
package/src/index.dev.ts
CHANGED
|
@@ -25,6 +25,12 @@ enableMocking().then(() => {
|
|
|
25
25
|
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
26
26
|
}),
|
|
27
27
|
logger: inploiBrandedLogger,
|
|
28
|
+
analytics: {
|
|
29
|
+
log: async params => {
|
|
30
|
+
inploiBrandedLogger.log('stub logging', params);
|
|
31
|
+
return { success: true, data: {} } as any;
|
|
32
|
+
},
|
|
33
|
+
},
|
|
28
34
|
});
|
|
29
35
|
window.chatbot.prepare();
|
|
30
36
|
});
|
package/src/ui/chat-bubble.tsx
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { VariantProps } from 'class-variance-authority';
|
|
2
2
|
import { cva } from 'class-variance-authority';
|
|
3
|
+
import { Variants, m } from 'framer-motion';
|
|
3
4
|
import type { ComponentProps } from 'react';
|
|
4
5
|
|
|
5
6
|
const chatBubbleVariants = cva(
|
|
6
|
-
'max-w-[min(100%,24rem)] flex-shrink-1 min-w-[2rem] text-
|
|
7
|
+
'max-w-[min(100%,24rem)] flex-shrink-1 min-w-[2rem] text-md py-2 px-3 rounded-[18px] min-h-[36px] break-words',
|
|
7
8
|
{
|
|
8
9
|
variants: {
|
|
9
10
|
side: {
|
|
10
|
-
left: '
|
|
11
|
-
right:
|
|
11
|
+
left: 'bg-lowest text-neutral-12 shadow-surface-md outline outline-1 outline-accent-11/[.08] rounded-bl-md',
|
|
12
|
+
right:
|
|
13
|
+
'ml-auto bg-gradient-to-t from-[#2D51D2] to-[#4A7BEC] border border-[#405FCC] shadow-[inset_0_5px_3px_-3px_#6F99F1,inset_0_-5px_5px_-2px_#6F99F1DD,0_4px_6px_-1px_rgb(0_0_0_/_0.1),_0_2px_4px_-2px_rgb(0_0_0_/_0.1)] text-lowest rounded-br-md bubble-right',
|
|
12
14
|
},
|
|
13
15
|
transitionState: {
|
|
14
16
|
entering: 'opacity-0 translate-y-8',
|
|
@@ -26,11 +28,25 @@ const chatBubbleVariants = cva(
|
|
|
26
28
|
|
|
27
29
|
type ChatBubbleVariants = VariantProps<typeof chatBubbleVariants>;
|
|
28
30
|
|
|
31
|
+
const motionVariants: Variants = {
|
|
32
|
+
hidden: { y: '100%', scale: 0.75 },
|
|
33
|
+
shown: { y: 0, scale: 1 },
|
|
34
|
+
};
|
|
35
|
+
|
|
29
36
|
type ChatBubbleProps = ComponentProps<'p'> & ChatBubbleVariants;
|
|
30
37
|
export const ChatBubble = ({ children, className, transitionState, side, ...props }: ChatBubbleProps) => {
|
|
31
38
|
return (
|
|
32
|
-
<p
|
|
39
|
+
<m.p
|
|
40
|
+
variants={motionVariants}
|
|
41
|
+
initial="hidden"
|
|
42
|
+
animate="shown"
|
|
43
|
+
transition={{ type: 'spring', damping: 25, stiffness: 500 }}
|
|
44
|
+
data-transition={transitionState}
|
|
45
|
+
style={{ transformOrigin: side === 'left' ? '0% 50%' : '100% 50%' }}
|
|
46
|
+
class={chatBubbleVariants({ className, side, transitionState })}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
33
49
|
{children}
|
|
34
|
-
</p>
|
|
50
|
+
</m.p>
|
|
35
51
|
);
|
|
36
52
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { P, match } from 'ts-pattern';
|
|
2
|
-
import {
|
|
2
|
+
import { parse, picklist } from 'valibot';
|
|
3
3
|
|
|
4
|
+
import { useFocusOnMount } from '../useFocus';
|
|
4
5
|
import { ChatInputProps } from './chat-input';
|
|
5
6
|
|
|
6
7
|
export type BooleanChoicePayload = {
|
|
@@ -11,12 +12,15 @@ export type BooleanChoicePayload = {
|
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
const options = ['true', 'false'] as const;
|
|
14
|
-
const AnswerSchema =
|
|
15
|
+
const AnswerSchema = picklist(options);
|
|
15
16
|
const FIELD_NAME = 'answer';
|
|
16
17
|
|
|
17
18
|
export const ChatInputBoolean = ({ input, onSubmitSuccess }: ChatInputProps<'boolean'>) => {
|
|
19
|
+
const focusRef = useFocusOnMount();
|
|
20
|
+
|
|
18
21
|
return (
|
|
19
22
|
<form
|
|
23
|
+
noValidate
|
|
20
24
|
class="flex gap-2 items-center"
|
|
21
25
|
onSubmit={e => {
|
|
22
26
|
e.preventDefault();
|
|
@@ -36,19 +40,20 @@ export const ChatInputBoolean = ({ input, onSubmitSuccess }: ChatInputProps<'boo
|
|
|
36
40
|
.otherwise(() => {
|
|
37
41
|
throw new Error('invalid form');
|
|
38
42
|
});
|
|
39
|
-
const answer =
|
|
43
|
+
const answer = parse(AnswerSchema, value);
|
|
40
44
|
onSubmitSuccess(answer);
|
|
41
45
|
}}
|
|
42
46
|
>
|
|
43
|
-
{options.map(value => {
|
|
47
|
+
{options.map((value, i) => {
|
|
44
48
|
return (
|
|
45
49
|
<button
|
|
50
|
+
ref={i === 0 ? focusRef : null}
|
|
46
51
|
type="submit"
|
|
47
52
|
name={FIELD_NAME}
|
|
48
53
|
value={value}
|
|
49
54
|
class="flex-1 overflow-hidden rounded-2xl block px-2.5 py-2.5 selection:bg-transparent transition-all bg-lowest ring-transparent ring-0 focus-visible:ring-offset-2 focus-visible:ring-4 focus-visible:ring-accent-7 ease-expo-out duration-300 outline outline-2 outline-neutral-12/5 text-neutral-12 active:outline-accent-9 active:bg-accent-4 active:text-accent-11"
|
|
50
55
|
>
|
|
51
|
-
<p class="truncate text-base">{input.config.labels[value]}</p>
|
|
56
|
+
<p class="truncate text-center text-base">{input.config.labels[value]}</p>
|
|
52
57
|
</button>
|
|
53
58
|
);
|
|
54
59
|
})}
|