@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
package/src/index.dev.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/** This file is only used for dev mode. The build output is index.ts */
|
|
2
|
-
import { createApiClient, 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
|
-
window.chatbot = chatbotPlugin({})({
|
|
23
|
-
apiClient: createApiClient({
|
|
24
|
-
baseUrl: import.meta.env.VITE_BASE_URL,
|
|
25
|
-
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
26
|
-
}),
|
|
27
|
-
logger: inploiBrandedLogger,
|
|
28
|
-
analytics: {
|
|
29
|
-
log: async params => {
|
|
30
|
-
inploiBrandedLogger.log('stub logging', params);
|
|
31
|
-
return { success: true, data: {} } as any;
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
|
-
window.chatbot.prepare();
|
|
36
|
-
});
|
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,241 +0,0 @@
|
|
|
1
|
-
import { FlowNode, FlowNodeType, IfBlockNode } from '@inploi/core/flows';
|
|
2
|
-
import { P, match } from 'ts-pattern';
|
|
3
|
-
import { 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; message: ChatMessage };
|
|
9
|
-
export type ChatService = {
|
|
10
|
-
send: (params: ChatServiceSendParams) => Promise<void>;
|
|
11
|
-
input: <TType extends ChatInput['type']>(
|
|
12
|
-
params: Extract<ChatInput, { type: TType }>,
|
|
13
|
-
) => Promise<Extract<ApplicationSubmission, { type: TType }>>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type ChatbotInterpreterParams = {
|
|
17
|
-
flow: FlowNode[];
|
|
18
|
-
getSubmissions: () => KeyToSubmissionMap | undefined;
|
|
19
|
-
chatService: ChatService;
|
|
20
|
-
/** When interpreter starts */
|
|
21
|
-
beforeStart?: (firstNode: FlowNode) => Promise<void>;
|
|
22
|
-
/** When flow has no more nodes to interpret */
|
|
23
|
-
onFlowEnd?: (lastNode: FlowNode) => void;
|
|
24
|
-
/** When node is interpreted */
|
|
25
|
-
onInterpret?: (node: FlowNode) => void;
|
|
26
|
-
};
|
|
27
|
-
export const createFlowInterpreter = ({
|
|
28
|
-
flow,
|
|
29
|
-
getSubmissions,
|
|
30
|
-
chatService,
|
|
31
|
-
beforeStart,
|
|
32
|
-
onFlowEnd,
|
|
33
|
-
onInterpret,
|
|
34
|
-
}: ChatbotInterpreterParams) => {
|
|
35
|
-
const controller = new AbortController();
|
|
36
|
-
const interpretNode = async (node: FlowNode) => {
|
|
37
|
-
const submissions = getSubmissions();
|
|
38
|
-
onInterpret?.(node);
|
|
39
|
-
await interpret({
|
|
40
|
-
node,
|
|
41
|
-
submissions,
|
|
42
|
-
chat: {
|
|
43
|
-
sendMessage: async message => chatService.send({ message, signal: controller.signal }),
|
|
44
|
-
userInput: async input => chatService.input(input),
|
|
45
|
-
},
|
|
46
|
-
next: () => {
|
|
47
|
-
const nextNodeId = getNextNodeId(node, getSubmissions());
|
|
48
|
-
const nextNode = flow.find(node => node.id === nextNodeId);
|
|
49
|
-
if (nextNode) {
|
|
50
|
-
return interpretNode(nextNode);
|
|
51
|
-
} else onFlowEnd?.(node);
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
interpret: async (startFromNodeId?: string) => {
|
|
58
|
-
const startNode = flow.find(node => node.id === startFromNodeId) ?? getHeadOrThrow(flow);
|
|
59
|
-
await beforeStart?.(startNode);
|
|
60
|
-
return interpretNode(startNode);
|
|
61
|
-
},
|
|
62
|
-
abort: () => {
|
|
63
|
-
controller.abort();
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type InterpretNodeParams<TNodeType extends FlowNodeType> = {
|
|
69
|
-
node: Extract<FlowNode, { type: TNodeType }>;
|
|
70
|
-
submissions?: KeyToSubmissionMap;
|
|
71
|
-
chat: {
|
|
72
|
-
sendMessage: (message: ChatMessage) => Promise<void>;
|
|
73
|
-
userInput: <TType extends ChatInput['type']>(
|
|
74
|
-
params: Extract<ChatInput, { type: TType }>,
|
|
75
|
-
) => Promise<Extract<ApplicationSubmission, { type: TType }>>;
|
|
76
|
-
};
|
|
77
|
-
next: () => void;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
async function interpret(params: InterpretNodeParams<FlowNodeType>) {
|
|
81
|
-
return await match(params)
|
|
82
|
-
.with({ node: { type: 'text' } }, interpretTextNode)
|
|
83
|
-
.with({ node: { type: 'image' } }, interpretImageNode)
|
|
84
|
-
.with({ node: { type: 'question-text' } }, interpretQuestionTextNode)
|
|
85
|
-
.with({ node: { type: 'question-enum' } }, interpretQuestionEnumNode)
|
|
86
|
-
.with({ node: { type: 'question-number' } }, interpretQuestionNumberNode)
|
|
87
|
-
.with({ node: { type: 'question-boolean' } }, interpretQuestionBooleanNode)
|
|
88
|
-
.with({ node: { type: 'question-file' } }, interpretQuestionFileNode)
|
|
89
|
-
.with({ node: { type: 'question-address' } }, interpretQuestionAddressNode)
|
|
90
|
-
.with({ node: { type: 'abandon-flow' } }, interpretAbandonFlowNode)
|
|
91
|
-
.with({ node: { type: 'complete-flow' } }, interpretCompleteFlowNode)
|
|
92
|
-
.with({ node: { type: P.union('jump', 'if-block') } }, ({ next }) => next())
|
|
93
|
-
.exhaustive();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function interpretTextNode({ chat, next, node }: InterpretNodeParams<'text'>) {
|
|
97
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
|
|
98
|
-
next();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function interpretImageNode({ chat, next, node }: InterpretNodeParams<'image'>) {
|
|
102
|
-
await chat.sendMessage({
|
|
103
|
-
author: 'bot',
|
|
104
|
-
type: 'image',
|
|
105
|
-
url: node.data.url,
|
|
106
|
-
height: node.data.height,
|
|
107
|
-
width: node.data.width,
|
|
108
|
-
});
|
|
109
|
-
next();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function interpretQuestionTextNode({ chat, next, node }: InterpretNodeParams<'question-text'>) {
|
|
113
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
|
|
114
|
-
const reply = await chat.userInput({
|
|
115
|
-
key: node.data.key,
|
|
116
|
-
type: 'text',
|
|
117
|
-
config: { placeholder: node.data.placeholder, format: node.data.format },
|
|
118
|
-
});
|
|
119
|
-
await chat.sendMessage({ author: 'user', type: 'text', text: reply.value });
|
|
120
|
-
next();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function interpretQuestionNumberNode({ chat, next, node }: InterpretNodeParams<'question-number'>) {
|
|
124
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
|
|
125
|
-
const reply = await chat.userInput({
|
|
126
|
-
key: node.data.key,
|
|
127
|
-
type: 'text',
|
|
128
|
-
config: { placeholder: node.data.placeholder, format: 'text' },
|
|
129
|
-
});
|
|
130
|
-
await chat.sendMessage({ author: 'user', type: 'text', text: reply.value });
|
|
131
|
-
next();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function interpretQuestionEnumNode({ chat, next, node }: InterpretNodeParams<'question-enum'>) {
|
|
135
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
|
|
136
|
-
const reply = await chat.userInput({
|
|
137
|
-
key: node.data.key,
|
|
138
|
-
type: 'multiple-choice',
|
|
139
|
-
config: node.data,
|
|
140
|
-
});
|
|
141
|
-
await chat.sendMessage({ author: 'user', type: 'text', text: reply.value.join(', ') });
|
|
142
|
-
next();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function interpretQuestionBooleanNode({ chat, next, node }: InterpretNodeParams<'question-boolean'>) {
|
|
146
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
|
|
147
|
-
const input = await chat.userInput({
|
|
148
|
-
key: node.data.key,
|
|
149
|
-
type: 'boolean',
|
|
150
|
-
config: {
|
|
151
|
-
labels: {
|
|
152
|
-
true: node.data.trueLabel,
|
|
153
|
-
false: node.data.falseLabel,
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
const reply = input.value;
|
|
158
|
-
const labelMap = {
|
|
159
|
-
true: node.data.trueLabel,
|
|
160
|
-
false: node.data.falseLabel,
|
|
161
|
-
};
|
|
162
|
-
await chat.sendMessage({ author: 'user', type: 'text', text: labelMap[reply] });
|
|
163
|
-
next();
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function interpretQuestionAddressNode({ chat, next }: InterpretNodeParams<'question-address'>) {
|
|
167
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: 'Address questions are not implemented yet' });
|
|
168
|
-
next();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function interpretQuestionFileNode({ node, chat, next }: InterpretNodeParams<'question-file'>) {
|
|
172
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.question });
|
|
173
|
-
const files = await chat.userInput({
|
|
174
|
-
key: node.data.key,
|
|
175
|
-
type: 'file',
|
|
176
|
-
config: {
|
|
177
|
-
extensions: node.data.extensions,
|
|
178
|
-
fileSizeLimitKib: node.data.maxSizeKb,
|
|
179
|
-
allowMultiple: node.data.multiple === true,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
for await (const file of files.value) {
|
|
183
|
-
await chat.sendMessage({ author: 'user', type: 'file', fileName: file.name, fileSizeKb: file.sizeKb });
|
|
184
|
-
}
|
|
185
|
-
next();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function interpretAbandonFlowNode({ chat, next, node }: InterpretNodeParams<'abandon-flow'>) {
|
|
189
|
-
if (node.data.text) {
|
|
190
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
|
|
191
|
-
}
|
|
192
|
-
next();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async function interpretCompleteFlowNode({ chat, next, node }: InterpretNodeParams<'complete-flow'>) {
|
|
196
|
-
if (node.data.text) {
|
|
197
|
-
await chat.sendMessage({ author: 'bot', type: 'text', text: node.data.text });
|
|
198
|
-
}
|
|
199
|
-
next();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const stringOrStringArray = P.union(P.string, P.array(P.string));
|
|
203
|
-
|
|
204
|
-
const isIfBlockConditionMet = (ifBlock: IfBlockNode, submissions?: KeyToSubmissionMap) => {
|
|
205
|
-
const answer = submissions?.[ifBlock.data.compareKey];
|
|
206
|
-
if (!answer) return false;
|
|
207
|
-
|
|
208
|
-
return (
|
|
209
|
-
match({ ...ifBlock.data, answer })
|
|
210
|
-
.with({ compare: 'equals' }, ({ compareValue }) => {
|
|
211
|
-
if (typeof answer.value === 'string' || typeof answer.value === 'boolean')
|
|
212
|
-
return compareValue === answer.value.toString();
|
|
213
|
-
return false;
|
|
214
|
-
})
|
|
215
|
-
.with({ compare: 'notEquals' }, ({ compareValue }) => {
|
|
216
|
-
if (typeof answer.value === 'string' || typeof answer.value === 'boolean')
|
|
217
|
-
return compareValue !== answer.value.toString();
|
|
218
|
-
return false;
|
|
219
|
-
})
|
|
220
|
-
.with({ compare: 'contains', answer: { value: stringOrStringArray } }, ({ compareValue, answer }) =>
|
|
221
|
-
answer.value.includes(compareValue),
|
|
222
|
-
)
|
|
223
|
-
.with(
|
|
224
|
-
{ compare: 'notContains', answer: { value: stringOrStringArray } },
|
|
225
|
-
({ compareValue, answer }) => !answer.value.includes(compareValue),
|
|
226
|
-
)
|
|
227
|
-
// There cannot be conditions for files yet
|
|
228
|
-
.with({ answer: { type: 'file' } }, () => false)
|
|
229
|
-
.exhaustive()
|
|
230
|
-
);
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
export function getNextNodeId(fromNode: FlowNode, submissions?: KeyToSubmissionMap) {
|
|
234
|
-
return match(fromNode)
|
|
235
|
-
.with({ type: 'jump' }, jumpNode => jumpNode.data.targetId)
|
|
236
|
-
.with({ type: 'if-block' }, ifBlockNode => {
|
|
237
|
-
if (isIfBlockConditionMet(ifBlockNode, submissions)) return ifBlockNode.branchId;
|
|
238
|
-
return ifBlockNode.nextId;
|
|
239
|
-
})
|
|
240
|
-
.otherwise(node => node.nextId);
|
|
241
|
-
}
|