@inploi/plugin-chatbot 1.0.7 → 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 +17 -0
- package/index.html +2 -1
- package/package.json +21 -8
- package/playwright.config.ts +82 -0
- package/public/mockServiceWorker.js +4 -9
- package/src/chatbot.api.ts +14 -14
- package/src/chatbot.constants.ts +3 -0
- package/src/chatbot.css +8 -15
- package/src/chatbot.dom.ts +10 -5
- package/src/chatbot.idb.ts +17 -0
- package/src/chatbot.state.ts +78 -144
- package/src/chatbot.ts +25 -35
- package/src/chatbot.utils.ts +27 -9
- package/src/index.dev.ts +7 -6
- package/src/interpreter/interpreter.ts +28 -20
- package/src/mocks/browser.ts +2 -2
- package/src/mocks/example.flows.ts +56 -18
- 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 +20 -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 +23 -17
- package/src/ui/chat-input/chat-input.tsx +47 -23
- package/src/ui/chatbot-header.tsx +34 -28
- package/src/ui/chatbot.tsx +83 -42
- package/src/ui/input-error.tsx +25 -31
- package/src/ui/job-application-content.tsx +68 -46
- package/src/ui/job-application-messages.tsx +42 -34
- package/src/ui/send-button.tsx +1 -1
- package/src/ui/typing-indicator.tsx +1 -1
- package/src/ui/useChatService.ts +18 -33
- package/src/ui/useFocus.ts +10 -0
- package/tests/integration.spec.ts +19 -0
- package/tests/test.ts +22 -0
- package/tsconfig.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { ApiClient, Logger } from '@inploi/sdk';
|
|
2
|
-
import {
|
|
1
|
+
import { AnalyticsService, ApiClient, Logger } from '@inploi/sdk';
|
|
2
|
+
import { AnimatePresence } from 'framer-motion';
|
|
3
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
3
4
|
import { match } from 'ts-pattern';
|
|
4
|
-
import {
|
|
5
|
+
import { StartedJobApplication, application, inputHeight, viewState } from '~/chatbot.state';
|
|
5
6
|
import { submissionsToPayload } from '~/chatbot.utils';
|
|
6
7
|
|
|
7
8
|
import { ERROR_MESSAGES } from '../chatbot.constants';
|
|
8
|
-
import { useApplicationLocalState, useLocalState } from '../chatbot.state';
|
|
9
9
|
import { createFlowInterpreter } from '../interpreter/interpreter';
|
|
10
10
|
import { ChatInput } from './chat-input/chat-input';
|
|
11
11
|
import { JobApplicationMessages } from './job-application-messages';
|
|
@@ -13,45 +13,62 @@ import { useChatService } from './useChatService';
|
|
|
13
13
|
|
|
14
14
|
type JobApplicationContentProps = {
|
|
15
15
|
apiClient: ApiClient;
|
|
16
|
-
logger
|
|
17
|
-
|
|
16
|
+
logger: Logger;
|
|
17
|
+
currentApplication: StartedJobApplication;
|
|
18
|
+
analytics: AnalyticsService;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
export const JobApplicationContent = ({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
export const JobApplicationContent = ({
|
|
22
|
+
currentApplication,
|
|
23
|
+
logger,
|
|
24
|
+
apiClient,
|
|
25
|
+
analytics,
|
|
26
|
+
}: JobApplicationContentProps) => {
|
|
27
|
+
const { chatRef, chatService, isBotTyping, onSubmitSuccessFn, scrollToEnd } = useChatService();
|
|
28
|
+
|
|
29
|
+
const view = viewState.value;
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
// This significantly improves performance for maximising the view
|
|
32
|
+
if (view === 'maximised') scrollToEnd({ behavior: 'instant' });
|
|
33
|
+
}, [scrollToEnd, view]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
scrollToEnd({ behavior: 'smooth' });
|
|
37
|
+
}, [currentApplication.data.messages, scrollToEnd]);
|
|
24
38
|
|
|
25
39
|
useLayoutEffect(() => {
|
|
26
|
-
const { getApplicationLocalState, setInput, updateApplicationLocalState, updateApplicationCurrentNode } =
|
|
27
|
-
useLocalState.getState();
|
|
28
|
-
const applicationLocalState = getApplicationLocalState(application);
|
|
29
40
|
scrollToEnd({ behavior: 'instant' });
|
|
30
|
-
|
|
41
|
+
|
|
42
|
+
const { state, application: currentApplication } = application.current$.value;
|
|
43
|
+
if (state !== 'loaded' || currentApplication.data.isFinished) return;
|
|
31
44
|
|
|
32
45
|
const { interpret, abort } = createFlowInterpreter({
|
|
33
|
-
flow:
|
|
46
|
+
flow: currentApplication.flow.nodes,
|
|
34
47
|
chatService,
|
|
35
|
-
getSubmissions: () =>
|
|
48
|
+
getSubmissions: () => application.current$.peek().application?.data.submissions,
|
|
36
49
|
beforeStart: async node => {
|
|
37
|
-
setInput(
|
|
50
|
+
application.setInput(undefined);
|
|
38
51
|
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
const fromBeginning = currentApplication.data.messages.length === 0;
|
|
53
|
+
if (fromBeginning) {
|
|
54
|
+
analytics.log({
|
|
55
|
+
event: 'APPLY_START',
|
|
56
|
+
properties: { job_id: currentApplication.job.id },
|
|
57
|
+
customProperties: {
|
|
58
|
+
flow_id: currentApplication.flow.id,
|
|
46
59
|
},
|
|
47
60
|
});
|
|
61
|
+
} else {
|
|
62
|
+
// We restart the last node.
|
|
63
|
+
const restoredFromId = node.id;
|
|
64
|
+
application.removeLastGroupMessagesById(restoredFromId);
|
|
48
65
|
}
|
|
49
66
|
},
|
|
50
67
|
onInterpret: node => {
|
|
51
|
-
|
|
68
|
+
application.setCurrentNodeId(node.id);
|
|
52
69
|
},
|
|
53
70
|
onFlowEnd: async lastNode => {
|
|
54
|
-
|
|
71
|
+
application.markAsFinished();
|
|
55
72
|
return match(lastNode)
|
|
56
73
|
.with({ type: 'abandon-flow' }, () => {
|
|
57
74
|
chatService.send({
|
|
@@ -60,15 +77,16 @@ export const JobApplicationContent = ({ application, logger, apiClient }: JobApp
|
|
|
60
77
|
text: 'Application ended',
|
|
61
78
|
variant: 'success',
|
|
62
79
|
},
|
|
80
|
+
groupId: 'system',
|
|
63
81
|
});
|
|
64
82
|
})
|
|
65
83
|
.with({ type: 'complete-flow' }, async () => {
|
|
66
|
-
const submissions =
|
|
84
|
+
const submissions = application.current$.peek().application?.data.submissions;
|
|
67
85
|
if (!submissions) throw new Error(ERROR_MESSAGES.no_submissions);
|
|
68
86
|
|
|
69
|
-
const response = await apiClient.fetch(`/flow/
|
|
87
|
+
const response = await apiClient.fetch(`/flow/apply`, {
|
|
70
88
|
method: 'POST',
|
|
71
|
-
body: JSON.stringify(submissionsToPayload(submissions)),
|
|
89
|
+
body: JSON.stringify(submissionsToPayload({ application: currentApplication, submissions })),
|
|
72
90
|
});
|
|
73
91
|
|
|
74
92
|
match(response)
|
|
@@ -79,44 +97,48 @@ export const JobApplicationContent = ({ application, logger, apiClient }: JobApp
|
|
|
79
97
|
text: 'Application submitted',
|
|
80
98
|
variant: 'success',
|
|
81
99
|
},
|
|
100
|
+
groupId: 'system',
|
|
82
101
|
});
|
|
83
102
|
})
|
|
84
103
|
.otherwise(response => {
|
|
85
|
-
logger
|
|
104
|
+
logger.error(response);
|
|
86
105
|
chatService.send({
|
|
87
106
|
message: {
|
|
88
107
|
type: 'system',
|
|
89
108
|
text: 'Error submitting application',
|
|
90
109
|
variant: 'error',
|
|
91
110
|
},
|
|
111
|
+
groupId: 'system',
|
|
92
112
|
});
|
|
93
113
|
});
|
|
94
114
|
})
|
|
95
115
|
.otherwise(() => {
|
|
96
|
-
logger
|
|
116
|
+
logger.error(ERROR_MESSAGES.invalid_end_node, lastNode);
|
|
97
117
|
});
|
|
98
118
|
},
|
|
99
119
|
});
|
|
100
120
|
|
|
101
|
-
interpret(
|
|
121
|
+
interpret(currentApplication.data.currentNodeId);
|
|
102
122
|
|
|
103
123
|
return abort;
|
|
104
|
-
}, [
|
|
124
|
+
}, [analytics, apiClient, chatService, logger, scrollToEnd]);
|
|
105
125
|
|
|
106
126
|
return (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
application={application}
|
|
117
|
-
onSubmit={onSubmitSuccessFn}
|
|
118
|
-
/>
|
|
127
|
+
<>
|
|
128
|
+
<div
|
|
129
|
+
ref={chatRef}
|
|
130
|
+
className="relative max-w-full flex-grow flex flex-col hide-scrollbars overflow-y-scroll"
|
|
131
|
+
style={{ WebkitOverflowScrolling: 'touch', paddingBottom: inputHeight.value }}
|
|
132
|
+
>
|
|
133
|
+
<AnimatePresence>
|
|
134
|
+
<JobApplicationMessages isBotTyping={isBotTyping} messages={currentApplication.data.messages} />
|
|
135
|
+
</AnimatePresence>
|
|
119
136
|
</div>
|
|
120
|
-
|
|
137
|
+
<ChatInput
|
|
138
|
+
input={currentApplication.data.currentInput}
|
|
139
|
+
onInputChange={() => scrollToEnd({ behavior: 'smooth' })}
|
|
140
|
+
onSubmit={onSubmitSuccessFn}
|
|
141
|
+
/>
|
|
142
|
+
</>
|
|
121
143
|
);
|
|
122
144
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { AnimatePresence } from 'framer-motion';
|
|
1
2
|
import { P, match } from 'ts-pattern';
|
|
2
3
|
|
|
3
4
|
import { ChatMessage } from '../chatbot.state';
|
|
4
5
|
import { ChatBubble } from './chat-bubble';
|
|
5
6
|
import { FileThumbnail } from './chat-input/chat-input.file';
|
|
7
|
+
// import { AnimatePresence } from './motion/animate-presence';
|
|
6
8
|
import { TypingIndicator } from './typing-indicator';
|
|
7
9
|
|
|
8
10
|
type JobApplicationMessagesProps = {
|
|
@@ -17,40 +19,46 @@ const authorToSide = {
|
|
|
17
19
|
|
|
18
20
|
export const JobApplicationMessages = ({ messages, isBotTyping }: JobApplicationMessagesProps) => {
|
|
19
21
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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>
|
|
53
61
|
<aside aria-hidden>{isBotTyping && <TypingIndicator />}</aside>
|
|
54
|
-
</
|
|
62
|
+
</ol>
|
|
55
63
|
);
|
|
56
64
|
};
|
package/src/ui/send-button.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { ComponentProps } from 'preact';
|
|
|
4
4
|
export const SendButton = ({ class: className, ...props }: ComponentProps<'button'>) => (
|
|
5
5
|
<button
|
|
6
6
|
class={clsx(
|
|
7
|
-
'p-2 flex-shrink-0 bg-accent-
|
|
7
|
+
'p-2 flex-shrink-0 bg-accent-7 active:bg-accent-10 active:text-accent-4 rounded-full text-lowest pointer-coarse:touch-hitbox disabled:opacity-50 disabled:cursor-not-allowed',
|
|
8
8
|
className,
|
|
9
9
|
)}
|
|
10
10
|
{...props}
|
|
@@ -5,7 +5,7 @@ export const TypingIndicator = ({ className, ...props }: ComponentProps<'div'>)
|
|
|
5
5
|
return (
|
|
6
6
|
<div class={clsx('flex gap-1 p-4', className)} {...props}>
|
|
7
7
|
{Array.from({ length: 3 }, (_, i) => (
|
|
8
|
-
<div class="h-1.5 w-1.5 rounded-full bg-accent-
|
|
8
|
+
<div class="h-1.5 w-1.5 rounded-full bg-accent-7 animate-bounce" style={{ animationDelay: `${-i * 200}ms` }} />
|
|
9
9
|
))}
|
|
10
10
|
</div>
|
|
11
11
|
);
|
package/src/ui/useChatService.ts
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Ref, useMemo, useState } from 'preact/hooks';
|
|
1
|
+
import { useMemo, useRef, useState } from 'preact/hooks';
|
|
3
2
|
import { match } from 'ts-pattern';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { application } from '~/chatbot.state';
|
|
4
|
+
import { AbortedError } from '~/chatbot.utils';
|
|
6
5
|
import { ChatService } from '~/interpreter/interpreter';
|
|
7
6
|
|
|
8
7
|
import { SubmitSuccessFn } from './chat-input/chat-input';
|
|
9
8
|
|
|
10
9
|
const TYPING_SPEED_MS_PER_CHARACTER = 25;
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
chatRef
|
|
14
|
-
application: JobApplication;
|
|
15
|
-
logger?: Logger;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const useChatService = ({ logger, application, chatRef }: UseChatServiceParams) => {
|
|
11
|
+
export const useChatService = () => {
|
|
12
|
+
const chatRef = useRef<HTMLDivElement>(null);
|
|
19
13
|
const [isBotTyping, setIsBotTyping] = useState(false);
|
|
20
14
|
const [onSubmitSuccessFn, setOnSubmitSuccessFn] = useState<SubmitSuccessFn>(() => () => {});
|
|
21
15
|
const scrollToEnd = useMemo(
|
|
@@ -25,18 +19,14 @@ export const useChatService = ({ logger, application, chatRef }: UseChatServiceP
|
|
|
25
19
|
);
|
|
26
20
|
|
|
27
21
|
const chatService = useMemo(() => {
|
|
28
|
-
const { setInput, updateSubmission, addMessage } = useLocalState.getState();
|
|
29
22
|
const chatService: ChatService = {
|
|
30
|
-
send: async ({ message, signal }) => {
|
|
23
|
+
send: async ({ message, signal, groupId }) => {
|
|
31
24
|
await match(message)
|
|
32
25
|
/** Delay sending and add typing indicator if bot is sending a message */
|
|
33
26
|
.with({ author: 'bot', type: 'text' }, async message => {
|
|
34
|
-
if (signal?.aborted)
|
|
35
|
-
logger?.info(`Aborted sending message`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
27
|
+
if (signal?.aborted) throw new AbortedError();
|
|
38
28
|
setIsBotTyping(true);
|
|
39
|
-
const typingTime = message.text.length * TYPING_SPEED_MS_PER_CHARACTER;
|
|
29
|
+
const typingTime = Math.max(20, message.text.length) * TYPING_SPEED_MS_PER_CHARACTER;
|
|
40
30
|
await new Promise(resolve => {
|
|
41
31
|
return setTimeout(resolve, typingTime, { signal });
|
|
42
32
|
});
|
|
@@ -45,24 +35,18 @@ export const useChatService = ({ logger, application, chatRef }: UseChatServiceP
|
|
|
45
35
|
.otherwise(async () => void 0);
|
|
46
36
|
|
|
47
37
|
/** The signal could have been aborted while typing */
|
|
48
|
-
if (signal?.aborted)
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
addMessage({ application, data: message });
|
|
53
|
-
scrollToEnd({ behavior: 'smooth' });
|
|
38
|
+
if (signal?.aborted) throw new AbortedError();
|
|
39
|
+
application.addMessage(message, groupId);
|
|
54
40
|
},
|
|
55
|
-
input: async input => {
|
|
56
|
-
|
|
41
|
+
input: async ({ input, signal }) => {
|
|
42
|
+
if (signal?.aborted) throw new AbortedError();
|
|
43
|
+
application.setInput(input);
|
|
57
44
|
|
|
58
45
|
return await new Promise(resolve => {
|
|
59
46
|
const submitFunction: SubmitSuccessFn = submission => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
fieldKey: input.key,
|
|
64
|
-
data: submission,
|
|
65
|
-
});
|
|
47
|
+
if (signal?.aborted) throw new AbortedError();
|
|
48
|
+
application.setInput(undefined);
|
|
49
|
+
application.setSubmission(input.key, submission);
|
|
66
50
|
resolve(submission as any);
|
|
67
51
|
};
|
|
68
52
|
setOnSubmitSuccessFn(() => submitFunction);
|
|
@@ -71,9 +55,10 @@ export const useChatService = ({ logger, application, chatRef }: UseChatServiceP
|
|
|
71
55
|
};
|
|
72
56
|
|
|
73
57
|
return chatService;
|
|
74
|
-
}, [
|
|
58
|
+
}, []);
|
|
75
59
|
|
|
76
60
|
return {
|
|
61
|
+
chatRef,
|
|
77
62
|
chatService,
|
|
78
63
|
isBotTyping,
|
|
79
64
|
onSubmitSuccessFn,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { flowHandler } from '~/mocks/handlers';
|
|
2
|
+
|
|
3
|
+
import { expect, test } from './test';
|
|
4
|
+
|
|
5
|
+
test('when backend responds with correct data, can start application', async ({ page, worker }) => {
|
|
6
|
+
await worker.use(flowHandler.success);
|
|
7
|
+
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
8
|
+
|
|
9
|
+
await page.getByRole('button', { name: 'Apply for job Test flow' }).click();
|
|
10
|
+
await expect(page.getByText('Text node')).toBeVisible();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('when backend responds with malformed data, user sees error message', async ({ page, worker }) => {
|
|
14
|
+
await worker.use(flowHandler.invalid_payload);
|
|
15
|
+
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
16
|
+
|
|
17
|
+
await page.getByRole('button', { name: 'Apply for job Test flow' }).click();
|
|
18
|
+
await expect(page.getByText('Text node')).toBeVisible();
|
|
19
|
+
});
|
package/tests/test.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { test as base, expect } from '@playwright/test';
|
|
2
|
+
import { http } from 'msw';
|
|
3
|
+
import type { Config, MockServiceWorker } from 'playwright-msw';
|
|
4
|
+
import { createWorkerFixture } from 'playwright-msw';
|
|
5
|
+
|
|
6
|
+
const testFactory = (config?: Config) =>
|
|
7
|
+
base.extend<{
|
|
8
|
+
worker: MockServiceWorker;
|
|
9
|
+
http: typeof http;
|
|
10
|
+
}>({
|
|
11
|
+
worker: createWorkerFixture(
|
|
12
|
+
[
|
|
13
|
+
// default handlers go here
|
|
14
|
+
],
|
|
15
|
+
config,
|
|
16
|
+
),
|
|
17
|
+
http,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const test = testFactory();
|
|
21
|
+
|
|
22
|
+
export { test, expect };
|
package/tsconfig.json
CHANGED
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"~/*": ["./src/*"]
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
-
"include": ["src", ".eslintrc.cjs", "tailwind.config.ts", "vite.config.ts"],
|
|
31
|
+
"include": ["src", "tests", "playwright.config.ts", ".eslintrc.cjs", "tailwind.config.ts", "vite.config.ts"],
|
|
32
32
|
"references": [{ "path": "./tsconfig.node.json" }]
|
|
33
33
|
}
|