@inploi/plugin-chatbot 3.8.0 → 3.10.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/dist/chatbot.d.ts CHANGED
@@ -6,7 +6,6 @@ export type ChatbotPlugin = typeof chatbotPlugin;
6
6
  export type ChatbotPluginParams = RemoveInternal<Parameters<ChatbotPlugin>[0]>;
7
7
  export type Chatbot = ReturnType<ReturnType<ChatbotPlugin>>;
8
8
  export declare const chatbotPlugin: ({ _internal_domManager: dom, theme, }: {
9
- geolocationApiKey?: string | undefined;
10
9
  theme: {
11
10
  /** Rotation for the hue. Between 0 and 360. */
12
11
  hue: number;
@@ -1,6 +1,7 @@
1
1
  import { FlowNode } from '@inploi/core/flows';
2
2
  import { ApplicationSubmission, KeyToSubmissionMap } from './chatbot.state';
3
3
  export type DistributivePick<T, K extends keyof T> = T extends unknown ? Pick<T, K> : never;
4
+ export declare const kbToReadableSize: (kb: number) => string;
4
5
  export declare const getHeadOrThrow: (nodes: FlowNode[]) => {
5
6
  id: string;
6
7
  data: {
@@ -20,8 +21,6 @@ export declare const getHeadOrThrow: (nodes: FlowNode[]) => {
20
21
  } | {
21
22
  id: string;
22
23
  data: {
23
- type: "partial" | "full";
24
- ats: "cornerstone";
25
24
  integrationId: string;
26
25
  };
27
26
  type: "integration-application-submit";
@@ -1047,6 +1047,9 @@ function invariant$1(condition, message) {
1047
1047
  }
1048
1048
  throw new Error(message);
1049
1049
  }
1050
+ function hasProp(data, prop) {
1051
+ return prop in data;
1052
+ }
1050
1053
  function _extends() {
1051
1054
  _extends = Object.assign ? Object.assign.bind() : function(target) {
1052
1055
  for (var i2 = 1; i2 < arguments.length; i2++) {
@@ -8281,7 +8284,7 @@ const StatusBar = ({
8281
8284
  })]
8282
8285
  });
8283
8286
  };
8284
- const JobApplicationContent = M(() => import("./job-application-content-b9799966.js").then((module) => module.JobApplicationContent));
8287
+ const JobApplicationContent = M(() => import("./job-application-content-c89a480d.js").then((module) => module.JobApplicationContent));
8285
8288
  const MotionProvider = ({
8286
8289
  children
8287
8290
  }) => {
@@ -9326,6 +9329,7 @@ const chatbotPlugin = ({
9326
9329
  });
9327
9330
  export {
9328
9331
  AnimatePresence as A,
9332
+ chatbotPlugin as B,
9329
9333
  Cn as C,
9330
9334
  ERROR_MESSAGES as E,
9331
9335
  F$1 as F,
@@ -9338,25 +9342,25 @@ export {
9338
9342
  clsx as c,
9339
9343
  picklist as d,
9340
9344
  application as e,
9341
- parseAsync as f,
9342
- object as g,
9343
- h$1 as h,
9345
+ h$1 as f,
9346
+ parseAsync as g,
9347
+ hasProp as h,
9344
9348
  invariant$1 as i,
9345
- minLength as j,
9349
+ object as j,
9346
9350
  k$3 as k,
9347
- boolean as l,
9351
+ minLength as l,
9348
9352
  maxLength as m,
9349
- email as n,
9353
+ boolean as n,
9350
9354
  o,
9351
9355
  p$3 as p,
9352
- regex as q,
9356
+ email as q,
9353
9357
  record as r,
9354
9358
  string as s,
9355
9359
  transform as t,
9356
9360
  url as u,
9357
- inputHeight as v,
9358
- m$1 as w,
9359
- viewState as x,
9361
+ regex as v,
9362
+ inputHeight as w,
9363
+ m$1 as x,
9360
9364
  y$2 as y,
9361
- chatbotPlugin as z
9365
+ viewState as z
9362
9366
  };
@@ -1048,6 +1048,9 @@ function invariant$1(condition, message) {
1048
1048
  }
1049
1049
  throw new Error(message);
1050
1050
  }
1051
+ function hasProp(data, prop) {
1052
+ return prop in data;
1053
+ }
1051
1054
  function _extends() {
1052
1055
  _extends = Object.assign ? Object.assign.bind() : function(target) {
1053
1056
  for (var i2 = 1; i2 < arguments.length; i2++) {
@@ -8282,7 +8285,7 @@ const StatusBar = ({
8282
8285
  })]
8283
8286
  });
8284
8287
  };
8285
- const JobApplicationContent = M(() => Promise.resolve().then(() => require("./job-application-content-8b7e62bb.cjs")).then((module2) => module2.JobApplicationContent));
8288
+ const JobApplicationContent = M(() => Promise.resolve().then(() => require("./job-application-content-191ecc14.cjs")).then((module2) => module2.JobApplicationContent));
8286
8289
  const MotionProvider = ({
8287
8290
  children
8288
8291
  }) => {
@@ -9340,6 +9343,7 @@ exports.chatbotPlugin = chatbotPlugin;
9340
9343
  exports.clsx = clsx;
9341
9344
  exports.email = email;
9342
9345
  exports.h = h$1;
9346
+ exports.hasProp = hasProp;
9343
9347
  exports.inputHeight = inputHeight;
9344
9348
  exports.invariant = invariant$1;
9345
9349
  exports.k = k$3;
@@ -23,11 +23,11 @@ export type ChatService = {
23
23
  type: TType;
24
24
  }>>;
25
25
  };
26
- type ChatbotInterpreterParams = {
26
+ type ChatbotInterpreterParams<TContext extends Record<string, unknown>> = {
27
27
  apiClient: ApiClient;
28
28
  analytics: AnalyticsService;
29
29
  logger: Logger;
30
- context: Record<string, unknown>;
30
+ context: TContext;
31
31
  flow: FlowNode[];
32
32
  getSubmissions: () => KeyToSubmissionMap | undefined;
33
33
  chatService: ChatService;
@@ -38,8 +38,9 @@ type ChatbotInterpreterParams = {
38
38
  /** When node is interpreted */
39
39
  onInterpret?: (node: FlowNode, prevNode?: FlowNode) => void;
40
40
  };
41
- export declare const createFlowInterpreter: ({ flow, analytics, logger, context, apiClient, getSubmissions, chatService, onFlowEnd, onInterpret, }: ChatbotInterpreterParams) => {
41
+ export declare const createFlowInterpreter: <TContext extends Record<string, unknown>>({ flow, analytics, logger, context, apiClient, getSubmissions, chatService, onFlowEnd, onInterpret, }: ChatbotInterpreterParams<TContext>) => {
42
42
  interpret: (startFromNodeId?: string) => Promise<void>;
43
43
  abort: () => void;
44
44
  };
45
+ export declare const interpolateString: (str: string, context: KeyToSubmissionMap | undefined) => string;
45
46
  export {};
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const index = require("./index-efb34443.cjs");
3
+ const index = require("./index-2d0cac15.cjs");
4
4
  require("@inploi/sdk");
5
+ const kbToReadableSize = (kb) => index.N(kb).with(index._.number.lte(1e3), () => `${Math.round(kb)}KB`).with(index._.number.lt(1e3 * 10), () => `${(kb / 1e3).toFixed(1)}MB`).otherwise(() => `${Math.round(kb / 1e3)}MB`);
5
6
  const getHeadOrThrow = (nodes) => {
6
7
  const head = nodes.find((n2) => n2.isHead);
7
8
  if (!head)
@@ -231,14 +232,17 @@ async function interpretSubmitNode({
231
232
  variant: "info",
232
233
  text: "Submitting your application…"
233
234
  });
235
+ const {
236
+ anonymous_id,
237
+ session_id
238
+ } = analytics.getSessionInfo();
234
239
  const response = await apiClient.fetch(`/flow/apply`, {
235
240
  method: "POST",
236
241
  body: JSON.stringify({
237
242
  ...context,
238
- type: node.data.type,
239
- ats: node.data.ats,
240
243
  integration_id: node.data.integrationId,
241
- anonymous_id: analytics.getAnonymousId(),
244
+ anonymous_id,
245
+ session_id,
242
246
  submissions: getApplicationSubmissionsPayload(submissions || {})
243
247
  })
244
248
  }).catch((e) => e);
@@ -252,10 +256,9 @@ async function interpretSubmitNode({
252
256
  author: "bot",
253
257
  text: "Almost there! Please finalise your application on our partner’s website."
254
258
  });
255
- const currentAnonymousId = analytics.getAnonymousId();
256
259
  const href = new URL(response2.ats_data.redirect_url);
257
- if (currentAnonymousId && !href.searchParams.has("anonymous_id")) {
258
- href.searchParams.set("anonymous_id", currentAnonymousId);
260
+ if (anonymous_id && !href.searchParams.has("anonymous_id")) {
261
+ href.searchParams.set("anonymous_id", anonymous_id);
259
262
  }
260
263
  logApplyComplete();
261
264
  await chat.sendMessage({
@@ -287,12 +290,13 @@ async function interpretSubmitNode({
287
290
  async function interpretLinkNode({
288
291
  chat,
289
292
  next,
290
- node
293
+ node,
294
+ submissions
291
295
  }) {
292
296
  await chat.sendMessage({
293
297
  type: "link",
294
298
  href: node.data.href,
295
- text: node.data.cta
299
+ text: interpolateString(node.data.cta, submissions)
296
300
  });
297
301
  next(node.nextId);
298
302
  }
@@ -307,12 +311,13 @@ async function interpretIfBlockNode({
307
311
  async function interpretTextNode({
308
312
  chat,
309
313
  next,
310
- node
314
+ node,
315
+ submissions
311
316
  }) {
312
317
  await chat.sendMessage({
313
318
  author: "bot",
314
319
  type: "text",
315
- text: node.data.text
320
+ text: interpolateString(node.data.text, submissions)
316
321
  });
317
322
  next(node.nextId);
318
323
  }
@@ -333,12 +338,13 @@ async function interpretImageNode({
333
338
  async function interpretQuestionTextNode({
334
339
  chat,
335
340
  next,
336
- node
341
+ node,
342
+ submissions
337
343
  }) {
338
344
  await chat.sendMessage({
339
345
  author: "bot",
340
346
  type: "text",
341
- text: node.data.question
347
+ text: interpolateString(node.data.question, submissions)
342
348
  });
343
349
  const reply = await chat.userInput({
344
350
  key: node.data.key,
@@ -367,12 +373,13 @@ async function interpretQuestionTextNode({
367
373
  async function interpretQuestionNumberNode({
368
374
  chat,
369
375
  next,
370
- node
376
+ node,
377
+ submissions
371
378
  }) {
372
379
  await chat.sendMessage({
373
380
  author: "bot",
374
381
  type: "text",
375
- text: node.data.question
382
+ text: interpolateString(node.data.question, submissions)
376
383
  });
377
384
  const reply = await chat.userInput({
378
385
  key: node.data.key,
@@ -401,12 +408,13 @@ async function interpretQuestionNumberNode({
401
408
  async function interpretQuestionEnumNode({
402
409
  chat,
403
410
  next,
404
- node
411
+ node,
412
+ submissions
405
413
  }) {
406
414
  await chat.sendMessage({
407
415
  author: "bot",
408
416
  type: "text",
409
- text: node.data.question
417
+ text: interpolateString(node.data.question, submissions)
410
418
  });
411
419
  const reply = await chat.userInput({
412
420
  key: node.data.key,
@@ -431,12 +439,13 @@ async function interpretQuestionEnumNode({
431
439
  async function interpretQuestionBooleanNode({
432
440
  chat,
433
441
  next,
434
- node
442
+ node,
443
+ submissions
435
444
  }) {
436
445
  await chat.sendMessage({
437
446
  author: "bot",
438
447
  type: "text",
439
- text: node.data.question
448
+ text: interpolateString(node.data.question, submissions)
440
449
  });
441
450
  const input = await chat.userInput({
442
451
  key: node.data.key,
@@ -468,27 +477,205 @@ async function interpretQuestionBooleanNode({
468
477
  }
469
478
  next(node.nextId);
470
479
  }
480
+ const dummyDivBecauseGoogleRequiresIt = document.createElement("div");
481
+ const keyToAddressComponnents = {
482
+ line1: ["street_number", "floor", "room", "premise"],
483
+ line2: ["subpremise", "street_address", "route"],
484
+ line3: ["sublocality", "neighborhood"],
485
+ city: ["locality", "postal_town"],
486
+ state: ["administrative_area_level_1"],
487
+ postcode: ["postal_code"],
488
+ country: ["country"]
489
+ };
490
+ const fieldMapKeys = Object.keys(keyToAddressComponnents);
471
491
  async function interpretQuestionAddressNode({
472
492
  chat,
473
493
  next,
474
- node
494
+ node,
495
+ logger
475
496
  }) {
497
+ const {
498
+ google
499
+ } = window;
476
500
  await chat.sendMessage({
477
501
  author: "bot",
478
502
  type: "text",
479
- text: "Address questions are not implemented yet"
503
+ text: node.data.question
480
504
  });
481
- next(node.nextId);
505
+ const askForAddress = async (defaultValues) => {
506
+ const addressFields = [{
507
+ label: "Postcode",
508
+ key: node.data.keys.postcode,
509
+ optional: false,
510
+ defaultValue: defaultValues.postcode
511
+ }, {
512
+ label: "Line 1",
513
+ key: node.data.keys.line1,
514
+ optional: false,
515
+ defaultValue: defaultValues.line1
516
+ }, {
517
+ label: "Line 2",
518
+ key: node.data.keys.line2,
519
+ optional: true,
520
+ defaultValue: defaultValues.line2
521
+ }, {
522
+ label: "Line 3",
523
+ key: node.data.keys.line3,
524
+ optional: true,
525
+ defaultValue: defaultValues.line3
526
+ }, {
527
+ label: "City",
528
+ key: node.data.keys.city,
529
+ optional: false,
530
+ defaultValue: defaultValues.city
531
+ }, {
532
+ label: "State/County/Province",
533
+ key: node.data.keys.state,
534
+ optional: true,
535
+ defaultValue: defaultValues.state
536
+ }, {
537
+ label: "Country",
538
+ key: node.data.keys.country,
539
+ optional: false,
540
+ defaultValue: defaultValues.country
541
+ }];
542
+ for (const field of addressFields) {
543
+ await chat.sendMessage({
544
+ author: "bot",
545
+ type: "text",
546
+ text: field.label
547
+ });
548
+ const {
549
+ value
550
+ } = await chat.userInput({
551
+ type: "text",
552
+ key: field.key,
553
+ config: {
554
+ format: "text",
555
+ optional: field.optional,
556
+ defaultValue: field.defaultValue
557
+ }
558
+ });
559
+ if (value === null) {
560
+ await chat.sendMessage({
561
+ type: "system",
562
+ variant: "info",
563
+ text: "Skipped"
564
+ });
565
+ } else {
566
+ await chat.sendMessage({
567
+ author: "user",
568
+ type: "text",
569
+ text: value
570
+ });
571
+ }
572
+ }
573
+ };
574
+ if (!index.hasProp(window, "google") || !index.hasProp(window.google, "maps") || !index.hasProp(window.google.maps, "places")) {
575
+ logger.warn("Google maps not available, falling back to manual input.");
576
+ logger.info("If you’d like to use the address autocomplete, please insert the google maps API snippet in your website and make sure it has access to the *places* library.");
577
+ await askForAddress({});
578
+ return next(node.nextId);
579
+ }
580
+ const autocomplete = new google.maps.places.AutocompleteService();
581
+ const places = new google.maps.places.PlacesService(dummyDivBecauseGoogleRequiresIt);
582
+ const {
583
+ value: search
584
+ } = await chat.userInput({
585
+ type: "text",
586
+ key: "_internal-address-search",
587
+ config: {
588
+ format: "text",
589
+ optional: false,
590
+ placeholder: "Search for your address"
591
+ }
592
+ });
593
+ if (search === null)
594
+ return next(node.id);
595
+ await chat.sendMessage({
596
+ author: "user",
597
+ type: "text",
598
+ text: `Search for “${search}”`
599
+ });
600
+ const {
601
+ predictions
602
+ } = await autocomplete.getPlacePredictions({
603
+ input: search
604
+ });
605
+ const {
606
+ value: [selected]
607
+ } = await chat.userInput({
608
+ type: "multiple-choice",
609
+ key: void 0,
610
+ config: {
611
+ options: predictions.slice(0, 4).map((p) => ({
612
+ label: p.description,
613
+ value: p.place_id
614
+ })).concat({
615
+ label: "None of these",
616
+ value: "none"
617
+ }),
618
+ maxSelected: 1,
619
+ minSelected: 1
620
+ }
621
+ });
622
+ if (!selected || selected === "none") {
623
+ return next(node.id);
624
+ }
625
+ const result = await new Promise((resolve, reject) => places.getDetails({
626
+ placeId: selected,
627
+ fields: ["address_components"]
628
+ }, (result2, status) => {
629
+ if (status !== google.maps.places.PlacesServiceStatus["OK"])
630
+ return reject(status);
631
+ if (result2 === null)
632
+ return reject("ZERO_RESULTS");
633
+ return resolve({
634
+ ok: true,
635
+ place: result2
636
+ });
637
+ })).catch(async (e) => {
638
+ logger.error("Failed to get address details", e);
639
+ return {
640
+ ok: false
641
+ };
642
+ });
643
+ if (result.ok === false) {
644
+ await chat.sendMessage({
645
+ type: "system",
646
+ variant: "error",
647
+ text: "Failed to get address details"
648
+ });
649
+ await askForAddress({});
650
+ return next(node.id);
651
+ }
652
+ const addressComponents = result.place.address_components;
653
+ const autoFilledInputs = addressComponents ? fieldMapKeys.reduce((acc, key) => {
654
+ const componentTypes = keyToAddressComponnents[key];
655
+ const value = addressComponents.filter((component) => component.types.some((type) => componentTypes.includes(type))).map((component) => component.long_name).join(", ");
656
+ if (value) {
657
+ acc[key] = value;
658
+ }
659
+ return acc;
660
+ }, {}) : {};
661
+ await chat.sendMessage({
662
+ author: "bot",
663
+ type: "text",
664
+ text: "Please confirm or adjust your address:"
665
+ });
666
+ await askForAddress(autoFilledInputs);
667
+ return next(node.nextId);
482
668
  }
483
669
  async function interpretQuestionFileNode({
484
670
  node,
485
671
  chat,
486
- next
672
+ next,
673
+ submissions
487
674
  }) {
488
675
  await chat.sendMessage({
489
676
  author: "bot",
490
677
  type: "text",
491
- text: node.data.question
678
+ text: interpolateString(node.data.question, submissions)
492
679
  });
493
680
  const files = await chat.userInput({
494
681
  key: node.data.key,
@@ -577,6 +764,27 @@ const isIfBlockConditionMet = (ifBlock, submissions) => {
577
764
  }
578
765
  }, () => false).exhaustive();
579
766
  };
767
+ const interpolateString = (str, context) => {
768
+ const regex = /{{\s*([^}]+?)\s*(?:\|\s*([^}]+?)\s*)?}}/g;
769
+ return str.replace(regex, (match2, key, defaultValue = "") => {
770
+ key = key.trim();
771
+ const submission = context == null ? void 0 : context[key];
772
+ if (!submission)
773
+ return defaultValue;
774
+ switch (submission.type) {
775
+ case "boolean":
776
+ return submission.value === "true" ? "true" : "false";
777
+ case "file":
778
+ if (!submission.value)
779
+ return "no files";
780
+ return submission.value.map((file) => `${file.name} (${kbToReadableSize(file.sizeKb)})`).join(", ");
781
+ case "multiple-choice":
782
+ return submission.value.join(", ");
783
+ default:
784
+ return submission.value || defaultValue;
785
+ }
786
+ });
787
+ };
580
788
  const SendButton = ({
581
789
  class: className,
582
790
  ...props
@@ -741,7 +949,6 @@ const toBase64 = (file) => new Promise((resolve, reject) => {
741
949
  };
742
950
  reader.onerror = reject;
743
951
  });
744
- const kbToReadableSize = (kb) => index.N(kb).with(index._.number.lte(1e3), () => `${Math.round(kb)}KB`).with(index._.number.lt(1e3 * 10), () => `${(kb / 1e3).toFixed(1)}MB`).otherwise(() => `${Math.round(kb / 1e3)}MB`);
745
952
  const addFileSizesKb = (files) => files.reduce((acc, cur) => acc + cur.sizeKb, 0);
746
953
  const isFileSubmission = isSubmissionOfType("file");
747
954
  const FILENAMES_TO_SHOW_QTY = 3;
@@ -778,6 +985,19 @@ const FilenameBadge = ({
778
985
  class: index.clsx("outline-neutral-6 text-neutral-11 bg-neutral-1 block rounded-md px-1 py-0.5 text-xs outline outline-1", className),
779
986
  ...props
780
987
  });
988
+ const getFileExtension = (fileName) => {
989
+ const extension = fileName.split(".").pop();
990
+ if (!extension)
991
+ throw new Error("No file extension found");
992
+ return extension ? "." + extension : "";
993
+ };
994
+ const validateExtensions = ({
995
+ allowedExtensions,
996
+ files
997
+ }) => {
998
+ const normalisedExtensions = allowedExtensions.map((ext) => ext.toLowerCase());
999
+ return files.every((file) => normalisedExtensions.includes(getFileExtension(file.name).toLowerCase()));
1000
+ };
781
1001
  const ChatInputFile = ({
782
1002
  input,
783
1003
  onSubmitSuccess,
@@ -801,6 +1021,15 @@ const ChatInputFile = ({
801
1021
  message: "Please select a file"
802
1022
  });
803
1023
  }
1024
+ if (input.config.extensions.length > 0 && !validateExtensions({
1025
+ allowedExtensions: input.config.extensions,
1026
+ files
1027
+ })) {
1028
+ return setError({
1029
+ type: "validate",
1030
+ message: `Please upload ${input.config.extensions.join(", ")} files only`
1031
+ });
1032
+ }
804
1033
  if (input.config.fileSizeLimitKib && totalSize > input.config.fileSizeLimitKib) {
805
1034
  return setError({
806
1035
  type: "max",
@@ -2534,6 +2763,7 @@ const ChatInputText = ({
2534
2763
  }) => {
2535
2764
  var _a;
2536
2765
  const submission = input.key ? (_a = index.application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2766
+ const defaultValue = input.config.defaultValue;
2537
2767
  const {
2538
2768
  register,
2539
2769
  handleSubmit,
@@ -2542,7 +2772,7 @@ const ChatInputText = ({
2542
2772
  }
2543
2773
  } = useForm({
2544
2774
  defaultValues: {
2545
- text: isTextSubmission(submission) ? submission.value : ""
2775
+ text: defaultValue ? defaultValue : isTextSubmission(submission) ? submission.value : ""
2546
2776
  },
2547
2777
  resolver: getResolver(input.config)
2548
2778
  });