@inploi/plugin-chatbot 2.1.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.
Files changed (56) hide show
  1. package/cdn/index.js +56 -0
  2. package/package.json +14 -3
  3. package/.env +0 -2
  4. package/.env.example +0 -2
  5. package/.env.test +0 -2
  6. package/.eslintrc.cjs +0 -10
  7. package/CHANGELOG.md +0 -91
  8. package/bunfig.toml +0 -2
  9. package/happydom.ts +0 -10
  10. package/index.html +0 -29
  11. package/playwright.config.ts +0 -82
  12. package/postcss.config.cjs +0 -7
  13. package/src/chatbot.api.ts +0 -46
  14. package/src/chatbot.constants.ts +0 -9
  15. package/src/chatbot.css +0 -93
  16. package/src/chatbot.dom.ts +0 -28
  17. package/src/chatbot.idb.ts +0 -17
  18. package/src/chatbot.state.ts +0 -114
  19. package/src/chatbot.ts +0 -59
  20. package/src/chatbot.utils.ts +0 -56
  21. package/src/index.cdn.ts +0 -12
  22. package/src/index.dev.ts +0 -31
  23. package/src/index.ts +0 -1
  24. package/src/interpreter/interpreter.test.ts +0 -69
  25. package/src/interpreter/interpreter.ts +0 -249
  26. package/src/mocks/browser.ts +0 -5
  27. package/src/mocks/example.flows.ts +0 -801
  28. package/src/mocks/handlers.ts +0 -57
  29. package/src/style/palette.test.ts +0 -20
  30. package/src/style/palette.ts +0 -69
  31. package/src/ui/chat-bubble.tsx +0 -51
  32. package/src/ui/chat-input/chat-input.boolean.tsx +0 -62
  33. package/src/ui/chat-input/chat-input.file.tsx +0 -213
  34. package/src/ui/chat-input/chat-input.multiple-choice.tsx +0 -117
  35. package/src/ui/chat-input/chat-input.text.tsx +0 -111
  36. package/src/ui/chat-input/chat-input.tsx +0 -81
  37. package/src/ui/chatbot-header.tsx +0 -95
  38. package/src/ui/chatbot.tsx +0 -94
  39. package/src/ui/input-error.tsx +0 -33
  40. package/src/ui/job-application-content.tsx +0 -144
  41. package/src/ui/job-application-messages.tsx +0 -64
  42. package/src/ui/loading-indicator.tsx +0 -37
  43. package/src/ui/send-button.tsx +0 -27
  44. package/src/ui/transition.tsx +0 -1
  45. package/src/ui/typing-indicator.tsx +0 -12
  46. package/src/ui/useChatService.ts +0 -67
  47. package/src/ui/useFocus.ts +0 -10
  48. package/src/vite-env.d.ts +0 -1
  49. package/tailwind.config.ts +0 -119
  50. package/tests/integration.spec.ts +0 -19
  51. package/tests/test.ts +0 -22
  52. package/tsconfig.json +0 -33
  53. package/tsconfig.node.json +0 -10
  54. package/types.d.ts +0 -2
  55. package/vite.config.ts +0 -18
  56. /package/{public → cdn}/mockServiceWorker.js +0 -0
@@ -1,56 +0,0 @@
1
- import { FlowNode } from '@inploi/core/flows';
2
-
3
- import { ApplicationSubmission, KeyToSubmissionMap, StartedJobApplication } from './chatbot.state';
4
-
5
- export type DistributivePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
6
-
7
- export const getHeadOrThrow = (nodes: FlowNode[]) => {
8
- const head = nodes.find(n => n.isHead);
9
- if (!head) throw new Error('No head node found');
10
- return head;
11
- };
12
-
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;
33
- };
34
-
35
- export const isSubmissionOfType =
36
- <T extends ApplicationSubmission['type']>(type: T) =>
37
- (submission?: ApplicationSubmission): submission is Extract<ApplicationSubmission, { type: T }> => {
38
- if (!submission) return false;
39
- return submission.type === type;
40
- };
41
-
42
- export function gzip(string: string) {
43
- if (!('CompressionStream' in window)) return string;
44
- const byteArray = new TextEncoder().encode(string);
45
- const cs = new CompressionStream('gzip');
46
- const writer = cs.writable.getWriter();
47
- writer.write(byteArray);
48
- writer.close();
49
- return new Response(cs.readable).text();
50
- }
51
-
52
- export class AbortedError extends Error {
53
- constructor() {
54
- super('Aborted');
55
- }
56
- }
package/src/index.cdn.ts DELETED
@@ -1,12 +0,0 @@
1
- import { chatbotPlugin } from './chatbot';
2
-
3
- declare global {
4
- interface Window {
5
- inploi?: {
6
- chatbotPlugin?: typeof chatbotPlugin;
7
- };
8
- }
9
- }
10
-
11
- if (!window.inploi) throw new Error('Please insert the SDK script tag above the plugins.');
12
- window.inploi.chatbotPlugin = chatbotPlugin;
package/src/index.dev.ts DELETED
@@ -1,31 +0,0 @@
1
- /** This file is only used for dev mode. The build output is index.ts */
2
- import { initialiseSdk, inploiBrandedLogger } from '@inploi/sdk';
3
-
4
- import { chatbotPlugin } from './chatbot';
5
-
6
- declare global {
7
- interface Window {
8
- chatbot: ReturnType<ReturnType<typeof chatbotPlugin>>;
9
- }
10
- }
11
-
12
- async function enableMocking() {
13
- if (import.meta.env.VITE_MOCKS === 'false') return;
14
- const { worker } = await import('./mocks/browser');
15
-
16
- // `worker.start()` returns a Promise that resolves
17
- // once the Service Worker is up and ready to intercept requests.
18
- return worker.start({ onUnhandledRequest: 'bypass' });
19
- }
20
-
21
- enableMocking().then(() => {
22
- const sdk = initialiseSdk({
23
- env: 'sandbox',
24
- logger: inploiBrandedLogger,
25
- publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
26
- });
27
-
28
- // inploi’s hue: 265
29
- window.chatbot = sdk.registerPlugin(chatbotPlugin({ hue: 372 }));
30
- window.chatbot.prepare();
31
- });
package/src/index.ts DELETED
@@ -1 +0,0 @@
1
- export { chatbotPlugin } from './chatbot';
@@ -1,69 +0,0 @@
1
- import { FlowNode } from '@inploi/core/flows';
2
- import { expect, mock, test } from 'bun:test';
3
-
4
- import { ChatService, createFlowInterpreter } from './interpreter';
5
-
6
- const chatServiceStub: ChatService = {
7
- input: () => new Promise(resolve => resolve({ type: 'text', value: 'lol' } as any)),
8
- send: async () => void 0,
9
- };
10
-
11
- test('throws if no head', async () => {
12
- const interpreter = createFlowInterpreter({
13
- chatService: chatServiceStub,
14
- flow: [],
15
- getSubmissions: () => ({}),
16
- onFlowEnd: () => void 0,
17
- });
18
-
19
- try {
20
- await interpreter.interpret();
21
- expect().toThrow();
22
- } catch (error) {
23
- expect(error).toBeInstanceOf(Error);
24
- if (error instanceof Error) {
25
- expect(error.message).toEqual('No head node found');
26
- }
27
- }
28
- });
29
-
30
- test('calls onFinish with nodeType when there are no more nodes to interpret', async () => {
31
- const onFinish = mock(() => void 0);
32
- const flowNode: FlowNode = { id: '1', type: 'text', isHead: true, data: { text: 'Node one' } };
33
- const interpreter = createFlowInterpreter({
34
- chatService: chatServiceStub,
35
- flow: [flowNode],
36
- getSubmissions: () => ({}),
37
- onFlowEnd: onFinish,
38
- });
39
-
40
- await interpreter.interpret();
41
-
42
- expect(onFinish).toHaveBeenCalled();
43
- expect(onFinish.mock.calls[0]).toEqual([flowNode]);
44
- });
45
-
46
- test("moves on to next node when 'next' is called", async () => {
47
- const messageFn = mock((str: string) => {
48
- str;
49
- });
50
-
51
- const interpreter = createFlowInterpreter({
52
- chatService: {
53
- input: () => new Promise(resolve => resolve({ type: 'text', value: 'lol' } as any)),
54
- send: async ({ message }) => (message.type === 'text' ? messageFn(message.text) : void 0),
55
- },
56
- flow: [
57
- { id: '1', type: 'text', isHead: true, nextId: '2', data: { text: 'Node one' } },
58
- { id: '2', type: 'text', data: { text: 'Node two' } },
59
- ],
60
- getSubmissions: () => ({}),
61
- onFlowEnd: () => void 0,
62
- });
63
-
64
- await interpreter.interpret();
65
-
66
- expect(messageFn.mock.calls).toHaveLength(2);
67
- expect(messageFn.mock.calls[0]).toEqual(['Node one']);
68
- expect(messageFn.mock.calls[1]).toEqual(['Node two']);
69
- });
@@ -1,249 +0,0 @@
1
- import { FlowNode, FlowNodeType, IfBlockNode } from '@inploi/core/flows';
2
- import { P, match } from 'ts-pattern';
3
- import { AbortedError, getHeadOrThrow } from '~/chatbot.utils';
4
- import { ChatInput } from '~/ui/chat-input/chat-input';
5
-
6
- import { ApplicationSubmission, ChatMessage, KeyToSubmissionMap } from '../chatbot.state';
7
-
8
- export type ChatServiceSendParams = { signal?: AbortSignal; groupId?: string; message: ChatMessage };
9
- export type ChatService = {
10
- send: (params: ChatServiceSendParams) => Promise<void>;
11
- input: <TType extends ChatInput['type']>(params: {
12
- input: Extract<ChatInput, { type: TType }>;
13
- signal?: AbortSignal;
14
- }) => Promise<Extract<ApplicationSubmission, { type: TType }>>;
15
- };
16
-
17
- type ChatbotInterpreterParams = {
18
- flow: FlowNode[];
19
- getSubmissions: () => KeyToSubmissionMap | undefined;
20
- chatService: ChatService;
21
- /** When interpreter starts */
22
- beforeStart?: (firstNode: FlowNode) => Promise<void>;
23
- /** When flow has no more nodes to interpret */
24
- onFlowEnd?: (lastNode: FlowNode) => void;
25
- /** When node is interpreted */
26
- onInterpret?: (node: FlowNode) => void;
27
- };
28
- export const createFlowInterpreter = ({
29
- flow,
30
- getSubmissions,
31
- chatService,
32
- beforeStart,
33
- onFlowEnd,
34
- onInterpret,
35
- }: ChatbotInterpreterParams) => {
36
- const controller = new AbortController();
37
-
38
- const interpretNode = async (node: FlowNode) => {
39
- const submissions = getSubmissions();
40
- onInterpret?.(node);
41
- try {
42
- await interpret({
43
- node,
44
- submissions,
45
- chat: {
46
- sendMessage: async message => chatService.send({ groupId: node.id, message, signal: controller.signal }),
47
- userInput: async input => chatService.input({ input, signal: controller.signal }),
48
- },
49
- next: () => {
50
- const nextNodeId = getNextNodeId(node, getSubmissions());
51
- const nextNode = flow.find(node => node.id === nextNodeId);
52
- if (nextNode) {
53
- return interpretNode(nextNode);
54
- } else onFlowEnd?.(node);
55
- },
56
- });
57
- } catch (e) {
58
- // we let aborting the flow be silent
59
- if (e instanceof AbortedError) return;
60
- throw e;
61
- }
62
- };
63
-
64
- return {
65
- interpret: async (startFromNodeId?: string) => {
66
- const startNode = flow.find(node => node.id === startFromNodeId) ?? getHeadOrThrow(flow);
67
- await beforeStart?.(startNode);
68
- return interpretNode(startNode);
69
- },
70
- abort: () => {
71
- controller.abort();
72
- },
73
- };
74
- };
75
-
76
- type InterpretNodeParams<TNodeType extends FlowNodeType> = {
77
- node: Extract<FlowNode, { type: TNodeType }>;
78
- submissions?: KeyToSubmissionMap;
79
- chat: {
80
- sendMessage: (message: ChatMessage) => Promise<void>;
81
- userInput: <TType extends ChatInput['type']>(
82
- params: Extract<ChatInput, { type: TType }>,
83
- ) => Promise<Extract<ApplicationSubmission, { type: TType }>>;
84
- };
85
- next: () => void;
86
- };
87
-
88
- async function interpret(params: InterpretNodeParams<FlowNodeType>) {
89
- return await match(params)
90
- .with({ node: { type: 'text' } }, interpretTextNode)
91
- .with({ node: { type: 'image' } }, interpretImageNode)
92
- .with({ node: { type: 'question-text' } }, interpretQuestionTextNode)
93
- .with({ node: { type: 'question-enum' } }, interpretQuestionEnumNode)
94
- .with({ node: { type: 'question-number' } }, interpretQuestionNumberNode)
95
- .with({ node: { type: 'question-boolean' } }, interpretQuestionBooleanNode)
96
- .with({ node: { type: 'question-file' } }, interpretQuestionFileNode)
97
- .with({ node: { type: 'question-address' } }, interpretQuestionAddressNode)
98
- .with({ node: { type: 'abandon-flow' } }, interpretAbandonFlowNode)
99
- .with({ node: { type: 'complete-flow' } }, interpretCompleteFlowNode)
100
- .with({ node: { type: P.union('jump', 'if-block') } }, ({ next }) => next())
101
- .exhaustive();
102
- }
103
-
104
- async function interpretTextNode({ chat, next, node }: InterpretNodeParams<'text'>) {
105
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
106
- next();
107
- }
108
-
109
- async function interpretImageNode({ chat, next, node }: InterpretNodeParams<'image'>) {
110
- await chat.sendMessage({
111
- author: 'bot',
112
- type: 'image',
113
- url: node.data.url,
114
- height: node.data.height,
115
- width: node.data.width,
116
- });
117
- next();
118
- }
119
-
120
- async function interpretQuestionTextNode({ chat, next, node }: InterpretNodeParams<'question-text'>) {
121
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
122
- const reply = await chat.userInput({
123
- key: node.data.key,
124
- type: 'text',
125
- config: { placeholder: node.data.placeholder, format: node.data.format },
126
- });
127
- await chat.sendMessage({ author: 'user', type: 'text', text: reply.value });
128
- next();
129
- }
130
-
131
- async function interpretQuestionNumberNode({ chat, next, node }: InterpretNodeParams<'question-number'>) {
132
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
133
- const reply = await chat.userInput({
134
- key: node.data.key,
135
- type: 'text',
136
- config: { placeholder: node.data.placeholder, format: 'text' },
137
- });
138
- await chat.sendMessage({ author: 'user', type: 'text', text: reply.value });
139
- next();
140
- }
141
-
142
- async function interpretQuestionEnumNode({ chat, next, node }: InterpretNodeParams<'question-enum'>) {
143
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
144
- const reply = await chat.userInput({
145
- key: node.data.key,
146
- type: 'multiple-choice',
147
- config: node.data,
148
- });
149
- await chat.sendMessage({ author: 'user', type: 'text', text: reply.value.join(', ') });
150
- next();
151
- }
152
-
153
- async function interpretQuestionBooleanNode({ chat, next, node }: InterpretNodeParams<'question-boolean'>) {
154
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
155
- const input = await chat.userInput({
156
- key: node.data.key,
157
- type: 'boolean',
158
- config: {
159
- labels: {
160
- true: node.data.trueLabel,
161
- false: node.data.falseLabel,
162
- },
163
- },
164
- });
165
- const reply = input.value;
166
- const labelMap = {
167
- true: node.data.trueLabel,
168
- false: node.data.falseLabel,
169
- };
170
- await chat.sendMessage({ author: 'user', type: 'text', text: labelMap[reply] });
171
- next();
172
- }
173
-
174
- async function interpretQuestionAddressNode({ chat, next }: InterpretNodeParams<'question-address'>) {
175
- await chat.sendMessage({ author: 'bot', type: 'text', text: 'Address questions are not implemented yet' });
176
- next();
177
- }
178
-
179
- async function interpretQuestionFileNode({ node, chat, next }: InterpretNodeParams<'question-file'>) {
180
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
181
- const files = await chat.userInput({
182
- key: node.data.key,
183
- type: 'file',
184
- config: {
185
- extensions: node.data.extensions,
186
- fileSizeLimitKib: node.data.maxSizeKb,
187
- allowMultiple: node.data.multiple === true,
188
- },
189
- });
190
- for await (const file of files.value) {
191
- await chat.sendMessage({ author: 'user', type: 'file', fileName: file.name, fileSizeKb: file.sizeKb });
192
- }
193
- next();
194
- }
195
-
196
- async function interpretAbandonFlowNode({ chat, next, node }: InterpretNodeParams<'abandon-flow'>) {
197
- if (node.data.text) {
198
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
199
- }
200
- next();
201
- }
202
-
203
- async function interpretCompleteFlowNode({ chat, next, node }: InterpretNodeParams<'complete-flow'>) {
204
- if (node.data.text) {
205
- await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
206
- }
207
- next();
208
- }
209
-
210
- const stringOrStringArray = P.union(P.string, P.array(P.string));
211
-
212
- const isIfBlockConditionMet = (ifBlock: IfBlockNode, submissions?: KeyToSubmissionMap) => {
213
- const answer = submissions?.[ifBlock.data.compareKey];
214
- if (!answer) return false;
215
-
216
- return (
217
- match({ ...ifBlock.data, answer })
218
- .with({ compare: 'equals' }, ({ compareValue }) => {
219
- if (typeof answer.value === 'string' || typeof answer.value === 'boolean')
220
- return compareValue === answer.value.toString();
221
- return false;
222
- })
223
- .with({ compare: 'notEquals' }, ({ compareValue }) => {
224
- if (typeof answer.value === 'string' || typeof answer.value === 'boolean')
225
- return compareValue !== answer.value.toString();
226
- return false;
227
- })
228
- .with({ compare: 'contains', answer: { value: stringOrStringArray } }, ({ compareValue, answer }) =>
229
- answer.value.includes(compareValue),
230
- )
231
- .with(
232
- { compare: 'notContains', answer: { value: stringOrStringArray } },
233
- ({ compareValue, answer }) => !answer.value.includes(compareValue),
234
- )
235
- // There cannot be conditions for files yet
236
- .with({ answer: { type: 'file' } }, () => false)
237
- .exhaustive()
238
- );
239
- };
240
-
241
- export function getNextNodeId(fromNode: FlowNode, submissions?: KeyToSubmissionMap) {
242
- return match(fromNode)
243
- .with({ type: 'jump' }, jumpNode => jumpNode.data.targetId)
244
- .with({ type: 'if-block' }, ifBlockNode => {
245
- if (isIfBlockConditionMet(ifBlockNode, submissions)) return ifBlockNode.branchId;
246
- return ifBlockNode.nextId;
247
- })
248
- .otherwise(node => node.nextId);
249
- }
@@ -1,5 +0,0 @@
1
- import { setupWorker } from 'msw/browser';
2
-
3
- import { browserHandlers } from './handlers';
4
-
5
- export const worker = setupWorker(...browserHandlers);