@inploi/plugin-chatbot 2.0.0 → 2.1.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/.env +0 -1
- package/.env.example +0 -1
- package/.env.test +2 -0
- package/CHANGELOG.md +6 -0
- package/index.html +2 -1
- package/package.json +12 -3
- package/playwright.config.ts +82 -0
- package/public/mockServiceWorker.js +4 -9
- package/src/chatbot.css +0 -14
- package/src/chatbot.dom.ts +11 -0
- package/src/chatbot.state.ts +40 -15
- package/src/chatbot.ts +12 -7
- package/src/chatbot.utils.ts +6 -0
- package/src/index.dev.ts +7 -12
- package/src/interpreter/interpreter.ts +28 -20
- package/src/mocks/browser.ts +2 -2
- package/src/mocks/example.flows.ts +55 -17
- package/src/mocks/handlers.ts +37 -8
- package/src/style/palette.test.ts +20 -0
- package/src/style/palette.ts +69 -0
- package/src/ui/chat-bubble.tsx +1 -2
- package/src/ui/chat-input/chat-input.file.tsx +1 -1
- package/src/ui/chat-input/chat-input.multiple-choice.tsx +1 -1
- package/src/ui/chat-input/chat-input.text.tsx +2 -2
- package/src/ui/chat-input/chat-input.tsx +2 -2
- package/src/ui/chatbot-header.tsx +6 -9
- package/src/ui/chatbot.tsx +54 -65
- package/src/ui/job-application-content.tsx +19 -20
- package/src/ui/send-button.tsx +1 -1
- package/src/ui/typing-indicator.tsx +1 -1
- package/src/ui/useChatService.ts +11 -19
- package/tests/integration.spec.ts +19 -0
- package/tests/test.ts +22 -0
- package/tsconfig.json +1 -1
package/.env
CHANGED
package/.env.example
CHANGED
package/.env.test
ADDED
package/CHANGELOG.md
CHANGED
package/index.html
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
<h1 style="font-size: 2rem; letter-spacing: -0.02em; text-align: center">Super legit careers hub</h1>
|
|
20
20
|
<p>Welcome, start applying by clicking on one of the buttons below</p>
|
|
21
21
|
|
|
22
|
+
<button onclick="chatbot.startApplication({ jobId: '150153' })">Apply for Compass job</button>
|
|
22
23
|
<button onclick="chatbot.startApplication({ jobId: '1' })">Apply for Wagamama job</button>
|
|
23
|
-
<button onclick="chatbot.startApplication({ jobId: '
|
|
24
|
+
<button onclick="chatbot.startApplication({ jobId: 'test' })">Apply for job Test flow</button>
|
|
24
25
|
</div>
|
|
25
26
|
|
|
26
27
|
<script type="module" src="/src/index.dev.ts"></script>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inploi/plugin-chatbot",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@hookform/resolvers": "^3.3.2",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"@radix-ui/react-slot": "^1.0.2",
|
|
12
12
|
"class-variance-authority": "^0.7.0",
|
|
13
13
|
"clsx": "^2.0.0",
|
|
14
|
+
"culori": "^3.3.0",
|
|
14
15
|
"framer-motion": "^10.16.5",
|
|
15
16
|
"idb-keyval": "^6.2.1",
|
|
16
17
|
"preact": "^10.16.0",
|
|
@@ -30,14 +31,19 @@
|
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@happy-dom/global-registrator": "^12.6.0",
|
|
34
|
+
"@playwright/test": "^1.40.1",
|
|
33
35
|
"@preact/preset-vite": "^2.5.0",
|
|
34
36
|
"@total-typescript/ts-reset": "^0.5.1",
|
|
37
|
+
"@types/culori": "^2.0.4",
|
|
38
|
+
"@types/node": "^20.10.0",
|
|
35
39
|
"@types/react-transition-group": "^4.4.9",
|
|
36
40
|
"autoprefixer": "^10.4.16",
|
|
41
|
+
"dotenv": "^16.3.1",
|
|
37
42
|
"eslint": "^7.32.0",
|
|
38
43
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
39
44
|
"happy-dom": "^12.6.0",
|
|
40
|
-
"msw": "^2.0.
|
|
45
|
+
"msw": "^2.0.10",
|
|
46
|
+
"playwright-msw": "^3.0.0",
|
|
41
47
|
"postcss": "^8.4.31",
|
|
42
48
|
"postcss-nesting": "^12.0.1",
|
|
43
49
|
"rollup-plugin-visualizer": "^5.9.2",
|
|
@@ -46,7 +52,7 @@
|
|
|
46
52
|
"typescript": "^5.3.2",
|
|
47
53
|
"vite": "^4.4.5",
|
|
48
54
|
"vite-tsconfig-paths": "^4.2.1",
|
|
49
|
-
"@inploi/sdk": "1.5.
|
|
55
|
+
"@inploi/sdk": "1.5.1",
|
|
50
56
|
"eslint-config-custom": "0.1.0",
|
|
51
57
|
"tsconfig": "0.1.0"
|
|
52
58
|
},
|
|
@@ -58,6 +64,9 @@
|
|
|
58
64
|
"build": "tsc && vite build",
|
|
59
65
|
"setup-local": "cp -n .env.example .env || true",
|
|
60
66
|
"check": "eslint src --fix --max-warnings 0 && tsc",
|
|
67
|
+
"test:unit": "bun test",
|
|
68
|
+
"test:int": "playwright test",
|
|
69
|
+
"test:ui": "playwright test --ui",
|
|
61
70
|
"preview": "vite preview"
|
|
62
71
|
}
|
|
63
72
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read environment variables from file.
|
|
7
|
+
* https://github.com/motdotla/dotenv
|
|
8
|
+
*/
|
|
9
|
+
dotenv.config({ path: path.resolve(process.cwd(), '.env.test') });
|
|
10
|
+
|
|
11
|
+
const PORT = 3333;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* See https://playwright.dev/docs/test-configuration.
|
|
15
|
+
*/
|
|
16
|
+
export default defineConfig({
|
|
17
|
+
testDir: './tests',
|
|
18
|
+
/* Run tests in files in parallel */
|
|
19
|
+
fullyParallel: true,
|
|
20
|
+
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
|
21
|
+
forbidOnly: !!process.env.CI,
|
|
22
|
+
/* Retry on CI only */
|
|
23
|
+
retries: process.env.CI ? 2 : 0,
|
|
24
|
+
/* Opt out of parallel tests on CI. */
|
|
25
|
+
workers: process.env.CI ? 1 : undefined,
|
|
26
|
+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
|
27
|
+
reporter: 'html',
|
|
28
|
+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
|
29
|
+
use: {
|
|
30
|
+
/* Base URL to use in actions like `await page.goto('/')`. */
|
|
31
|
+
baseURL: process.env.BASE_URL || `http://localhost:${PORT}`,
|
|
32
|
+
|
|
33
|
+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
|
34
|
+
trace: 'on-first-retry',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
/* Configure projects for major browsers */
|
|
38
|
+
projects: [
|
|
39
|
+
{
|
|
40
|
+
name: 'chromium',
|
|
41
|
+
use: { ...devices['Desktop Chrome'] },
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// {
|
|
45
|
+
// name: 'firefox',
|
|
46
|
+
// use: { ...devices['Desktop Firefox'] },
|
|
47
|
+
// },
|
|
48
|
+
|
|
49
|
+
// {
|
|
50
|
+
// name: 'webkit',
|
|
51
|
+
// use: { ...devices['Desktop Safari'] },
|
|
52
|
+
// },
|
|
53
|
+
|
|
54
|
+
/* Test against mobile viewports. */
|
|
55
|
+
{
|
|
56
|
+
name: 'Mobile Chrome',
|
|
57
|
+
use: { ...devices['Pixel 5'] },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Mobile Safari',
|
|
61
|
+
use: { ...devices['iPhone 12'] },
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/* Test against branded browsers. */
|
|
65
|
+
// {
|
|
66
|
+
// name: 'Microsoft Edge',
|
|
67
|
+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
68
|
+
// },
|
|
69
|
+
// {
|
|
70
|
+
// name: 'Google Chrome',
|
|
71
|
+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
72
|
+
// },
|
|
73
|
+
],
|
|
74
|
+
|
|
75
|
+
/* Run your local dev server before starting the tests */
|
|
76
|
+
webServer: process.env.BASE_URL
|
|
77
|
+
? undefined
|
|
78
|
+
: {
|
|
79
|
+
command: `pnpm dev --port ${PORT} --mode test`,
|
|
80
|
+
port: PORT,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
/* tslint:disable */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Mock Service Worker (2.0.
|
|
5
|
+
* Mock Service Worker (2.0.10).
|
|
6
6
|
* @see https://github.com/mswjs/msw
|
|
7
7
|
* - Please do NOT modify this file.
|
|
8
8
|
* - Please do NOT serve this file on production.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const INTEGRITY_CHECKSUM = '
|
|
11
|
+
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39'
|
|
12
12
|
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
|
13
13
|
const activeClientIds = new Set()
|
|
14
14
|
|
|
@@ -121,11 +121,6 @@ async function handleRequest(event, requestId) {
|
|
|
121
121
|
if (client && activeClientIds.has(client.id)) {
|
|
122
122
|
;(async function () {
|
|
123
123
|
const responseClone = response.clone()
|
|
124
|
-
// When performing original requests, response body will
|
|
125
|
-
// always be a ReadableStream, even for 204 responses.
|
|
126
|
-
// But when creating a new Response instance on the client,
|
|
127
|
-
// the body for a 204 response must be null.
|
|
128
|
-
const responseBody = response.status === 204 ? null : responseClone.body
|
|
129
124
|
|
|
130
125
|
sendToClient(
|
|
131
126
|
client,
|
|
@@ -137,11 +132,11 @@ async function handleRequest(event, requestId) {
|
|
|
137
132
|
type: responseClone.type,
|
|
138
133
|
status: responseClone.status,
|
|
139
134
|
statusText: responseClone.statusText,
|
|
140
|
-
body:
|
|
135
|
+
body: responseClone.body,
|
|
141
136
|
headers: Object.fromEntries(responseClone.headers.entries()),
|
|
142
137
|
},
|
|
143
138
|
},
|
|
144
|
-
[
|
|
139
|
+
[responseClone.body],
|
|
145
140
|
)
|
|
146
141
|
})()
|
|
147
142
|
}
|
package/src/chatbot.css
CHANGED
|
@@ -19,20 +19,6 @@
|
|
|
19
19
|
--i-n-11: 206 6% 43.5%;
|
|
20
20
|
--i-n-12: 206 24% 9%;
|
|
21
21
|
|
|
22
|
-
/* Accent colors */
|
|
23
|
-
--i-a-1: 240 33% 99%;
|
|
24
|
-
--i-a-2: 225 100% 98%;
|
|
25
|
-
--i-a-3: 222 89% 96%;
|
|
26
|
-
--i-a-4: 224 100% 94%;
|
|
27
|
-
--i-a-5: 224 100% 91%;
|
|
28
|
-
--i-a-6: 225 100% 88%;
|
|
29
|
-
--i-a-7: 226 87% 82%;
|
|
30
|
-
--i-a-8: 226 75% 75%;
|
|
31
|
-
--i-a-9: 226 70% 55%;
|
|
32
|
-
--i-a-10: 226 65% 52%;
|
|
33
|
-
--i-a-11: 226 56% 50%;
|
|
34
|
-
--i-a-12: 226 50% 24%;
|
|
35
|
-
|
|
36
22
|
/** Error colours */
|
|
37
23
|
--i-e-1: 340 100% 99%;
|
|
38
24
|
--i-e-2: 353 100% 98%;
|
package/src/chatbot.dom.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { CHATBOT_ELEMENT_ID } from './chatbot.constants';
|
|
|
2
2
|
|
|
3
3
|
export const createChatbotDomManager = () => {
|
|
4
4
|
let chatbotElement: HTMLDivElement | null = null;
|
|
5
|
+
const styleElements: HTMLStyleElement[] = [];
|
|
6
|
+
|
|
5
7
|
return {
|
|
6
8
|
getOrCreateChatbotElement: () => {
|
|
7
9
|
if (chatbotElement) return chatbotElement;
|
|
@@ -12,6 +14,15 @@ export const createChatbotDomManager = () => {
|
|
|
12
14
|
chatbotElement = newElement;
|
|
13
15
|
return newElement;
|
|
14
16
|
},
|
|
17
|
+
addStyle: (css: string, id: string) => {
|
|
18
|
+
const head = document.head;
|
|
19
|
+
const element = document.createElement('style');
|
|
20
|
+
element.id = id;
|
|
21
|
+
element.innerHTML = css;
|
|
22
|
+
|
|
23
|
+
styleElements.push(element);
|
|
24
|
+
head.appendChild(element);
|
|
25
|
+
},
|
|
15
26
|
};
|
|
16
27
|
};
|
|
17
28
|
export type ChatbotDomManager = ReturnType<typeof createChatbotDomManager>;
|
package/src/chatbot.state.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { invariant } from '@inploi/core/common';
|
|
2
|
-
import {
|
|
2
|
+
import { batch, signal } from '@preact/signals';
|
|
3
3
|
|
|
4
4
|
import { JobApplication } from './chatbot.api';
|
|
5
5
|
import { idb } from './chatbot.idb';
|
|
@@ -13,19 +13,25 @@ export const getCacheKey = (application: JobApplication) =>
|
|
|
13
13
|
export type ViewState = 'maximised' | 'minimised';
|
|
14
14
|
export const viewState = signal<ViewState>('maximised');
|
|
15
15
|
|
|
16
|
-
export const inputHeight = signal(
|
|
16
|
+
export const inputHeight = signal(53);
|
|
17
17
|
|
|
18
18
|
export type StartedJobApplication = JobApplication & { data: ApplicationData };
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
19
|
+
|
|
20
|
+
type CurrentApplication =
|
|
21
|
+
| { state: 'idle'; application?: never }
|
|
22
|
+
| { state: 'loading'; application?: never }
|
|
23
|
+
| { state: 'loaded'; application: StartedJobApplication }
|
|
24
|
+
| { state: 'error'; application?: never; error: string };
|
|
25
|
+
|
|
26
|
+
const currentApplication = signal<CurrentApplication>({ state: 'idle' });
|
|
23
27
|
|
|
24
28
|
const updateApplicationData = async (updateFn: (data: ApplicationData) => ApplicationData) => {
|
|
25
|
-
const application = currentApplication.value;
|
|
29
|
+
const { state, application } = currentApplication.value;
|
|
30
|
+
if (state !== 'loaded') return;
|
|
26
31
|
invariant(application, 'No application to update');
|
|
27
32
|
const newData = updateFn(application.data);
|
|
28
|
-
|
|
33
|
+
const newApplication = { ...application, data: newData };
|
|
34
|
+
currentApplication.value = { state, application: newApplication };
|
|
29
35
|
await idb.setApplicationData({ application, data: newData });
|
|
30
36
|
};
|
|
31
37
|
|
|
@@ -35,22 +41,41 @@ export const application = {
|
|
|
35
41
|
const data = (await idb.getApplicationData(application)) ?? createNewApplicationData(application);
|
|
36
42
|
batch(() => {
|
|
37
43
|
viewState.value = 'maximised';
|
|
38
|
-
currentApplication.value = { ...application, data };
|
|
44
|
+
currentApplication.value = { state: 'loaded', application: { ...application, data } };
|
|
39
45
|
});
|
|
40
46
|
data.isFinished = false;
|
|
41
47
|
idb.setApplicationData({ application, data });
|
|
42
48
|
},
|
|
49
|
+
cancel: () => {
|
|
50
|
+
currentApplication.value = { state: 'idle' };
|
|
51
|
+
},
|
|
43
52
|
markAsFinished: () => updateApplicationData(data => ({ ...data, isFinished: true })),
|
|
44
53
|
setCurrentNodeId: (currentNodeId: string) => updateApplicationData(data => ({ ...data, currentNodeId })),
|
|
45
54
|
restart: () => {
|
|
46
|
-
const application = currentApplication.value;
|
|
47
|
-
|
|
55
|
+
const { state, application } = currentApplication.value;
|
|
56
|
+
if (state !== 'loaded') throw new Error('Application cannot be restarted: not in valid state');
|
|
48
57
|
const data = createNewApplicationData(application);
|
|
49
|
-
currentApplication.value = { ...application, data };
|
|
58
|
+
currentApplication.value = { state, application: { ...application, data } };
|
|
50
59
|
idb.setApplicationData({ application, data });
|
|
51
60
|
},
|
|
52
|
-
addMessage: (message: ChatMessage) =>
|
|
53
|
-
|
|
61
|
+
addMessage: (message: ChatMessage, groupId?: string) => {
|
|
62
|
+
const newMessage = { ...message, groupId };
|
|
63
|
+
return updateApplicationData(data => ({ ...data, messages: [...data.messages, newMessage] }));
|
|
64
|
+
},
|
|
65
|
+
/** Removes from the last message backwards, all the messages that have the groupId passed, until it reaches one that doesn't */
|
|
66
|
+
removeLastGroupMessagesById: (groupId: string) => {
|
|
67
|
+
const { state, application } = currentApplication.value;
|
|
68
|
+
if (state !== 'loaded') throw new Error('Application cannot be restarted: not in valid state');
|
|
69
|
+
|
|
70
|
+
const messages = [...application.data.messages];
|
|
71
|
+
let i = messages.length - 1;
|
|
72
|
+
while (i >= 0 && messages[i] && messages[i]?.groupId === groupId) {
|
|
73
|
+
messages.pop();
|
|
74
|
+
i--;
|
|
75
|
+
}
|
|
76
|
+
application.data.messages = messages;
|
|
77
|
+
idb.setApplicationData({ application, data: application.data });
|
|
78
|
+
},
|
|
54
79
|
setSubmission: (fieldKey: string, submission: ApplicationSubmission) =>
|
|
55
80
|
updateApplicationData(data => ({ ...data, submissions: { ...data.submissions, [fieldKey]: submission } })),
|
|
56
81
|
setInput: (input: ChatInput | undefined) => updateApplicationData(data => ({ ...data, currentInput: input })),
|
|
@@ -73,7 +98,7 @@ export type KeyToSubmissionMap = {
|
|
|
73
98
|
/** Dynamic part of an application */
|
|
74
99
|
export type ApplicationData = {
|
|
75
100
|
/** History of messages left in the chat */
|
|
76
|
-
messages: ChatMessage[];
|
|
101
|
+
messages: (ChatMessage & { groupId?: string })[];
|
|
77
102
|
submissions: KeyToSubmissionMap;
|
|
78
103
|
currentNodeId: string;
|
|
79
104
|
/** Needs to be separate because a node can have many inputs */
|
package/src/chatbot.ts
CHANGED
|
@@ -3,20 +3,27 @@ import { h, render } from 'preact';
|
|
|
3
3
|
import { Chatbot } from '~/ui/chatbot';
|
|
4
4
|
|
|
5
5
|
import { getApplicationData } from './chatbot.api';
|
|
6
|
-
import './chatbot.css';
|
|
6
|
+
import tailwind from './chatbot.css?inline';
|
|
7
7
|
import { ChatbotDomManager, createChatbotDomManager } from './chatbot.dom';
|
|
8
|
-
import { application
|
|
8
|
+
import { application } from './chatbot.state';
|
|
9
|
+
import { formatCssVariables, generatePalette } from './style/palette';
|
|
9
10
|
|
|
10
11
|
export const chatbotPlugin = ({
|
|
11
12
|
_internal_domManager: dom = createChatbotDomManager(),
|
|
13
|
+
hue,
|
|
12
14
|
}: {
|
|
13
15
|
geolocationApiKey?: string;
|
|
16
|
+
hue: number;
|
|
14
17
|
_internal_domManager?: ChatbotDomManager;
|
|
15
18
|
}) =>
|
|
16
19
|
createPlugin(({ apiClient, logger, analytics }) => {
|
|
17
20
|
let prepared = false;
|
|
18
21
|
const renderAndPrepare = () => {
|
|
19
22
|
const chatbotElement = dom.getOrCreateChatbotElement();
|
|
23
|
+
// Add styles generated by tailwind
|
|
24
|
+
dom.addStyle(tailwind, 'inploi-chatbot-style');
|
|
25
|
+
// Add dynamic styles generated by the plugin
|
|
26
|
+
dom.addStyle(formatCssVariables(generatePalette(hue)), 'inploi-chatbot-theme');
|
|
20
27
|
render(h(Chatbot, { apiClient, logger, analytics }), chatbotElement);
|
|
21
28
|
prepared = true;
|
|
22
29
|
};
|
|
@@ -35,10 +42,8 @@ export const chatbotPlugin = ({
|
|
|
35
42
|
},
|
|
36
43
|
startApplication: async ({ jobId }: { jobId: string }) => {
|
|
37
44
|
try {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
await application.start(applicationData);
|
|
45
|
+
application.cancel();
|
|
46
|
+
getApplicationData({ jobId, apiClient }).then(application.start);
|
|
42
47
|
|
|
43
48
|
if (!prepared) renderAndPrepare();
|
|
44
49
|
} catch (error) {
|
|
@@ -48,7 +53,7 @@ export const chatbotPlugin = ({
|
|
|
48
53
|
},
|
|
49
54
|
closeApplication: async () => {
|
|
50
55
|
logger.info('Closing application from an external source');
|
|
51
|
-
|
|
56
|
+
application.cancel();
|
|
52
57
|
},
|
|
53
58
|
};
|
|
54
59
|
});
|
package/src/chatbot.utils.ts
CHANGED
package/src/index.dev.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** This file is only used for dev mode. The build output is index.ts */
|
|
2
|
-
import {
|
|
2
|
+
import { initialiseSdk, inploiBrandedLogger } from '@inploi/sdk';
|
|
3
3
|
|
|
4
4
|
import { chatbotPlugin } from './chatbot';
|
|
5
5
|
|
|
@@ -19,18 +19,13 @@ async function enableMocking() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
enableMocking().then(() => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
baseUrl: import.meta.env.VITE_BASE_URL,
|
|
25
|
-
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
26
|
-
}),
|
|
22
|
+
const sdk = initialiseSdk({
|
|
23
|
+
env: 'sandbox',
|
|
27
24
|
logger: inploiBrandedLogger,
|
|
28
|
-
|
|
29
|
-
log: async params => {
|
|
30
|
-
inploiBrandedLogger.log('stub logging', params);
|
|
31
|
-
return { success: true, data: {} } as any;
|
|
32
|
-
},
|
|
33
|
-
},
|
|
25
|
+
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
34
26
|
});
|
|
27
|
+
|
|
28
|
+
// inploi’s hue: 265
|
|
29
|
+
window.chatbot = sdk.registerPlugin(chatbotPlugin({ hue: 372 }));
|
|
35
30
|
window.chatbot.prepare();
|
|
36
31
|
});
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { FlowNode, FlowNodeType, IfBlockNode } from '@inploi/core/flows';
|
|
2
2
|
import { P, match } from 'ts-pattern';
|
|
3
|
-
import { getHeadOrThrow } from '~/chatbot.utils';
|
|
3
|
+
import { AbortedError, getHeadOrThrow } from '~/chatbot.utils';
|
|
4
4
|
import { ChatInput } from '~/ui/chat-input/chat-input';
|
|
5
5
|
|
|
6
6
|
import { ApplicationSubmission, ChatMessage, KeyToSubmissionMap } from '../chatbot.state';
|
|
7
7
|
|
|
8
|
-
export type ChatServiceSendParams = { signal?: AbortSignal; message: ChatMessage };
|
|
8
|
+
export type ChatServiceSendParams = { signal?: AbortSignal; groupId?: string; message: ChatMessage };
|
|
9
9
|
export type ChatService = {
|
|
10
10
|
send: (params: ChatServiceSendParams) => Promise<void>;
|
|
11
|
-
input: <TType extends ChatInput['type']>(
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
input: <TType extends ChatInput['type']>(params: {
|
|
12
|
+
input: Extract<ChatInput, { type: TType }>;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}) => Promise<Extract<ApplicationSubmission, { type: TType }>>;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
type ChatbotInterpreterParams = {
|
|
@@ -33,24 +34,31 @@ export const createFlowInterpreter = ({
|
|
|
33
34
|
onInterpret,
|
|
34
35
|
}: ChatbotInterpreterParams) => {
|
|
35
36
|
const controller = new AbortController();
|
|
37
|
+
|
|
36
38
|
const interpretNode = async (node: FlowNode) => {
|
|
37
39
|
const submissions = getSubmissions();
|
|
38
40
|
onInterpret?.(node);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
}
|
|
54
62
|
};
|
|
55
63
|
|
|
56
64
|
return {
|
package/src/mocks/browser.ts
CHANGED
|
@@ -1,44 +1,82 @@
|
|
|
1
1
|
import { FlowNode } from '@inploi/core/flows';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
export const automatedTestFlow: FlowNode[] = [
|
|
4
4
|
{
|
|
5
|
-
id: '
|
|
5
|
+
id: '1',
|
|
6
6
|
type: 'text',
|
|
7
7
|
data: {
|
|
8
|
-
text: '
|
|
8
|
+
text: 'Text node',
|
|
9
9
|
},
|
|
10
10
|
isHead: true,
|
|
11
|
-
nextId: 'name',
|
|
12
11
|
},
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const textOnly: FlowNode[] = [
|
|
13
15
|
{
|
|
14
|
-
id: '
|
|
15
|
-
|
|
16
|
+
id: '443c2374-13d1-4b02-b2ed-17f148d2a3da',
|
|
17
|
+
isHead: true,
|
|
18
|
+
nextId: '5df04ee2-b786-4df6-874b-9d1c740363d9',
|
|
19
|
+
type: 'question-boolean',
|
|
16
20
|
data: {
|
|
17
|
-
|
|
21
|
+
key: 'like',
|
|
22
|
+
question: 'do u hate cats',
|
|
23
|
+
trueLabel: 'Yes',
|
|
24
|
+
falseLabel: 'No',
|
|
18
25
|
},
|
|
19
|
-
nextId: 'experience',
|
|
20
26
|
},
|
|
21
27
|
{
|
|
22
|
-
id: '
|
|
23
|
-
|
|
28
|
+
id: '5df04ee2-b786-4df6-874b-9d1c740363d9',
|
|
29
|
+
nextId: 'b6060696-ebb5-4b17-9d88-470394ccd1d3',
|
|
30
|
+
type: 'if-block',
|
|
24
31
|
data: {
|
|
25
|
-
|
|
32
|
+
compareKey: 'like',
|
|
33
|
+
compareValue: 'true',
|
|
34
|
+
compare: 'equals',
|
|
26
35
|
},
|
|
27
|
-
|
|
36
|
+
branchId: 'f94662b2-5229-4602-bc4e-d84666a56a25',
|
|
28
37
|
},
|
|
29
38
|
{
|
|
30
|
-
id: '
|
|
39
|
+
id: '801ccc8c-d5db-4864-9b89-1465c014e20e',
|
|
40
|
+
nextId: '042a8657-132d-4eb7-be4e-c1d300ecc538',
|
|
31
41
|
type: 'text',
|
|
32
42
|
data: {
|
|
33
|
-
text: '
|
|
43
|
+
text: 'nice',
|
|
34
44
|
},
|
|
35
|
-
nextId: 'end',
|
|
36
45
|
},
|
|
37
46
|
{
|
|
38
|
-
id: '
|
|
47
|
+
id: 'fec23e69-ef6c-4ae5-b10a-11104d36158c',
|
|
48
|
+
nextId: '801ccc8c-d5db-4864-9b89-1465c014e20e',
|
|
49
|
+
type: 'question-boolean',
|
|
50
|
+
data: {
|
|
51
|
+
key: 'free-will',
|
|
52
|
+
question: 'do you have free will?',
|
|
53
|
+
trueLabel: 'Yes',
|
|
54
|
+
falseLabel: 'No',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'b6060696-ebb5-4b17-9d88-470394ccd1d3',
|
|
59
|
+
nextId: 'fec23e69-ef6c-4ae5-b10a-11104d36158c',
|
|
39
60
|
type: 'text',
|
|
40
61
|
data: {
|
|
41
|
-
text: '
|
|
62
|
+
text: 'nice youre a decent person',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'f94662b2-5229-4602-bc4e-d84666a56a25',
|
|
67
|
+
type: 'abandon-flow',
|
|
68
|
+
data: {
|
|
69
|
+
text: 'you cant hate cats to work here',
|
|
70
|
+
redirectUrl: '',
|
|
71
|
+
cta: '',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: '042a8657-132d-4eb7-be4e-c1d300ecc538',
|
|
76
|
+
type: 'complete-flow',
|
|
77
|
+
data: {
|
|
78
|
+
text: "we'll hire you",
|
|
79
|
+
submitCta: 'submit',
|
|
42
80
|
},
|
|
43
81
|
},
|
|
44
82
|
];
|