@inploi/plugin-chatbot 3.11.0 → 3.12.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.
@@ -1,31 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const index = require("./index-356e7fb6.cjs");
3
+ const index = require("./index-9f0cf58e.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`);
6
- const getHeadOrThrow = (nodes) => {
7
- const head = nodes.find((n2) => n2.isHead);
8
- if (!head)
9
- throw new Error("No head node found");
10
- return head;
11
- };
12
- const getApplicationSubmissionsPayload = (submissions) => {
13
- const payload = Object.entries(submissions).reduce((acc, [key, submission]) => {
14
- acc[key] = submission.value;
15
- return acc;
16
- }, {});
17
- return payload;
18
- };
19
- const isSubmissionOfType = (type) => (submission) => {
20
- if (!submission)
21
- return false;
22
- return submission.type === type;
23
- };
24
- class AbortedError extends Error {
25
- constructor() {
26
- super("Aborted");
27
- }
28
- }
29
5
  const followNodes = ({
30
6
  node,
31
7
  nodes,
@@ -77,7 +53,7 @@ const createFlowInterpreter = ({
77
53
  onFlowEnd,
78
54
  onInterpret
79
55
  }) => {
80
- const controller = new AbortController();
56
+ let controller = new AbortController();
81
57
  const interpretNode = async (node, prevNode) => {
82
58
  const submissions = getSubmissions();
83
59
  onInterpret == null ? void 0 : onInterpret(node, prevNode);
@@ -112,16 +88,37 @@ const createFlowInterpreter = ({
112
88
  end: () => onFlowEnd == null ? void 0 : onFlowEnd(node)
113
89
  });
114
90
  } catch (e) {
115
- if (e instanceof AbortedError)
91
+ if (e instanceof index.AbortedError)
116
92
  return;
117
93
  throw e;
118
94
  }
119
95
  };
120
96
  return {
121
97
  interpret: async (startFromNodeId) => {
122
- const startNode = flow.find((node) => node.id === startFromNodeId) ?? getHeadOrThrow(flow);
98
+ const startNode = flow.find((node) => node.id === startFromNodeId) ?? index.getHeadOrThrow(flow);
123
99
  return interpretNode(startNode);
124
100
  },
101
+ undo: (nodeHistory) => {
102
+ let removed = 1;
103
+ const formerLastNode = flow.find((n2) => n2.id === nodeHistory[nodeHistory.length - 1]);
104
+ for (let i2 = nodeHistory.length - 2; i2 > 0; i2--) {
105
+ const nodeId = nodeHistory[i2];
106
+ const node = flow.find((n2) => n2.id === nodeId);
107
+ if (!node)
108
+ break;
109
+ removed++;
110
+ if (node.type.startsWith("question-"))
111
+ break;
112
+ }
113
+ controller.abort();
114
+ controller = new AbortController();
115
+ const newStartNode = flow.find((node) => node.id === nodeHistory[nodeHistory.length - removed]);
116
+ index.invariant(newStartNode, "Undo failed: new start node not found");
117
+ interpretNode(newStartNode, formerLastNode);
118
+ return {
119
+ removed
120
+ };
121
+ },
125
122
  abort: () => {
126
123
  controller.abort();
127
124
  }
@@ -200,22 +197,17 @@ async function interpretSubmitNode({
200
197
  analytics
201
198
  }) {
202
199
  await chat.userInput({
203
- type: "multiple-choice",
200
+ type: "submit",
204
201
  key: void 0,
205
202
  config: {
206
- options: [{
207
- label: "Submit my application",
208
- value: "submit"
209
- }],
210
- maxSelected: 1,
211
- minSelected: 1
203
+ label: "Submit my application"
212
204
  }
213
205
  });
214
206
  const logApplyComplete = () => {
215
- const contextJobId = typeof context.jobId === "string" ? context.jobId : void 0;
216
- const contextFlowId = typeof context.flowId === "string" ? context.flowId : void 0;
217
- index.invariant(contextJobId);
218
- index.invariant(contextFlowId);
207
+ const contextJobId = typeof context.job_id === "string" ? context.job_id : void 0;
208
+ const contextFlowId = typeof context.flow_id === "string" ? context.flow_id : void 0;
209
+ if (!contextJobId || !contextFlowId)
210
+ return;
219
211
  if (contextJobId) {
220
212
  analytics.log({
221
213
  event: "APPLY_COMPLETE",
@@ -243,7 +235,7 @@ async function interpretSubmitNode({
243
235
  integration_id: node.data.integrationId,
244
236
  anonymous_id,
245
237
  session_id,
246
- submissions: getApplicationSubmissionsPayload(submissions || {})
238
+ submissions: index.getFlowSubmissionsPayload(submissions || {})
247
239
  })
248
240
  }).catch((e) => e);
249
241
  await index.N(response).with({
@@ -781,7 +773,7 @@ const interpolateString = (str, context) => {
781
773
  case "file":
782
774
  if (!submission.value)
783
775
  return "no files";
784
- return submission.value.map((file) => `${file.name} (${kbToReadableSize(file.sizeKb)})`).join(", ");
776
+ return submission.value.map((file) => `${file.name} (${index.kbToReadableSize(file.sizeKb)})`).join(", ");
785
777
  case "multiple-choice":
786
778
  return submission.value.join(", ");
787
779
  default:
@@ -814,6 +806,96 @@ const SendButton = ({
814
806
  })]
815
807
  })
816
808
  });
809
+ const TYPING_SPEED_MS_PER_CHARACTER = 25;
810
+ const scrollToEndFn$ = index.a({
811
+ instant: () => {
812
+ },
813
+ smooth: () => {
814
+ }
815
+ });
816
+ const chatStore = {
817
+ onSubmitSuccessFn$: index.a(() => {
818
+ }),
819
+ isBotTyping$: index.a(false),
820
+ scrollToEnd: {
821
+ instant: () => scrollToEndFn$.value.instant(),
822
+ smooth: () => scrollToEndFn$.value.smooth()
823
+ }
824
+ };
825
+ const useChatService = () => {
826
+ const chatRef = index._$1(null);
827
+ index.y(() => {
828
+ scrollToEndFn$.value = {
829
+ instant: () => {
830
+ var _a;
831
+ return (_a = chatRef.current) == null ? void 0 : _a.scrollTo({
832
+ top: chatRef.current.scrollHeight,
833
+ behavior: "instant"
834
+ });
835
+ },
836
+ smooth: () => {
837
+ if (!chatRef.current)
838
+ return;
839
+ if (chatRef.current.scrollHeight - chatRef.current.scrollTop <= chatRef.current.clientHeight * 1.5) {
840
+ chatRef.current.scrollTo({
841
+ top: chatRef.current.scrollHeight,
842
+ behavior: "smooth"
843
+ });
844
+ }
845
+ }
846
+ };
847
+ }, [chatRef]);
848
+ const chatService = index.F(() => ({
849
+ send: async ({
850
+ message,
851
+ signal: signal2,
852
+ groupId
853
+ }) => {
854
+ await index.N(message).with({
855
+ author: "bot",
856
+ type: "text"
857
+ }, async (message2) => {
858
+ if (signal2 == null ? void 0 : signal2.aborted)
859
+ throw new index.AbortedError();
860
+ chatStore.isBotTyping$.value = true;
861
+ const typingTime = Math.min(Math.max(20, message2.text.length), 100) * TYPING_SPEED_MS_PER_CHARACTER;
862
+ await new Promise((resolve) => {
863
+ return setTimeout(resolve, typingTime, {
864
+ signal: signal2
865
+ });
866
+ });
867
+ chatStore.isBotTyping$.value = false;
868
+ }).otherwise(async () => void 0);
869
+ if (signal2 == null ? void 0 : signal2.aborted)
870
+ throw new index.AbortedError();
871
+ index.store.addMessage(message, groupId);
872
+ },
873
+ input: async ({
874
+ input,
875
+ signal: signal2
876
+ }) => {
877
+ if (signal2 == null ? void 0 : signal2.aborted)
878
+ throw new index.AbortedError();
879
+ index.store.setInput(input);
880
+ return await new Promise((resolve) => {
881
+ const submitFunction = (submission) => {
882
+ if (signal2 == null ? void 0 : signal2.aborted)
883
+ throw new index.AbortedError();
884
+ index.store.setInput(void 0);
885
+ if (input.key) {
886
+ index.store.setSubmission(input.key, submission);
887
+ }
888
+ resolve(submission);
889
+ };
890
+ chatStore.onSubmitSuccessFn$.value = submitFunction;
891
+ });
892
+ }
893
+ }), []);
894
+ return {
895
+ chatRef,
896
+ chatService
897
+ };
898
+ };
817
899
  const SkipButton = ({
818
900
  class: className,
819
901
  ...props
@@ -954,7 +1036,7 @@ const toBase64 = (file) => new Promise((resolve, reject) => {
954
1036
  reader.onerror = reject;
955
1037
  });
956
1038
  const addFileSizesKb = (files) => files.reduce((acc, cur) => acc + cur.sizeKb, 0);
957
- const isFileSubmission = isSubmissionOfType("file");
1039
+ const isFileSubmission = index.isSubmissionOfType("file");
958
1040
  const FILENAMES_TO_SHOW_QTY = 3;
959
1041
  const FileThumbnail = ({
960
1042
  file,
@@ -978,7 +1060,7 @@ const FileThumbnail = ({
978
1060
  }), index.o("p", {
979
1061
  "aria-label": "File size",
980
1062
  class: "text-accent-11",
981
- children: kbToReadableSize(file.sizeKb)
1063
+ children: index.kbToReadableSize(file.sizeKb)
982
1064
  })]
983
1065
  });
984
1066
  };
@@ -1008,7 +1090,7 @@ const ChatInputFile = ({
1008
1090
  onHeightChange
1009
1091
  }) => {
1010
1092
  var _a;
1011
- const submission = (_a = index.application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key];
1093
+ const submission = (_a = index.store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key];
1012
1094
  const [files, setFiles] = index.h(isFileSubmission(submission) && submission.value !== null ? submission.value : []);
1013
1095
  const [error, setError] = index.h();
1014
1096
  const hiddenFileCount = files.length - FILENAMES_TO_SHOW_QTY;
@@ -1037,7 +1119,7 @@ const ChatInputFile = ({
1037
1119
  if (input.config.fileSizeLimitKib && totalSize > input.config.fileSizeLimitKib) {
1038
1120
  return setError({
1039
1121
  type: "max",
1040
- message: `File size exceeds limit of ${kbToReadableSize(input.config.fileSizeLimitKib)}`
1122
+ message: `File size exceeds limit of ${index.kbToReadableSize(input.config.fileSizeLimitKib)}`
1041
1123
  });
1042
1124
  }
1043
1125
  if (input.config.allowMultiple === false && files.length > 1)
@@ -1073,7 +1155,7 @@ const ChatInputFile = ({
1073
1155
  }) : null]
1074
1156
  }), index.o("p", {
1075
1157
  class: "text-neutral-11 text-xs",
1076
- children: [kbToReadableSize(totalSize), " ", files.length > 1 ? "total" : ""]
1158
+ children: [index.kbToReadableSize(totalSize), " ", files.length > 1 ? "total" : ""]
1077
1159
  })]
1078
1160
  }) : index.o("div", {
1079
1161
  class: "flex flex-col justify-center gap-4 pb-6 pt-5",
@@ -1097,7 +1179,7 @@ const ChatInputFile = ({
1097
1179
  children: [input.config.allowMultiple ? "Select files" : "Select a file", " to upload"]
1098
1180
  }), input.config.fileSizeLimitKib ? index.o("p", {
1099
1181
  class: "text-neutral-10 text-xs",
1100
- children: ["(max ", kbToReadableSize(input.config.fileSizeLimitKib), ")"]
1182
+ children: ["(max ", index.kbToReadableSize(input.config.fileSizeLimitKib), ")"]
1101
1183
  }) : null]
1102
1184
  }), index.o("aside", {
1103
1185
  class: "flex flex-col items-center gap-2",
@@ -2629,7 +2711,7 @@ const submitIfSingleChecked = (form) => {
2629
2711
  bubbles: true
2630
2712
  }));
2631
2713
  };
2632
- const isMultipleChoiceSubmission = isSubmissionOfType("multiple-choice");
2714
+ const isMultipleChoiceSubmission = index.isSubmissionOfType("multiple-choice");
2633
2715
  const getResolver$1 = (config) => {
2634
2716
  const length = {
2635
2717
  min: config.minSelected ?? 0,
@@ -2645,7 +2727,8 @@ const ChatInputMultipleChoice = ({
2645
2727
  onHeightChange
2646
2728
  }) => {
2647
2729
  var _a, _b;
2648
- const submission = input.key ? (_a = index.application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2730
+ const submission = input.key ? (_a = index.store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2731
+ const isSingleChoice = (input.config.minSelected === 1 || input.config.minSelected === void 0) && input.config.maxSelected === 1;
2649
2732
  const {
2650
2733
  register,
2651
2734
  handleSubmit,
@@ -2654,12 +2737,11 @@ const ChatInputMultipleChoice = ({
2654
2737
  }
2655
2738
  } = useForm({
2656
2739
  defaultValues: {
2657
- checked: isMultipleChoiceSubmission(submission) ? Object.fromEntries(submission.value.map((key) => [key, true])) : {}
2740
+ checked: isSingleChoice ? {} : isMultipleChoiceSubmission(submission) ? Object.fromEntries(submission.value.map((key) => [key, true])) : {}
2658
2741
  },
2659
2742
  resolver: getResolver$1(input.config)
2660
2743
  });
2661
2744
  const focusRef = useFocusOnMount();
2662
- const isSingleChoice = (input.config.minSelected === 1 || input.config.minSelected === void 0) && input.config.maxSelected === 1;
2663
2745
  return index.o("form", {
2664
2746
  noValidate: true,
2665
2747
  class: "flex flex-col gap-1 pr-2.5",
@@ -2724,6 +2806,41 @@ const ChatInputMultipleChoice = ({
2724
2806
  })]
2725
2807
  });
2726
2808
  };
2809
+ const ChatInputSubmit = ({
2810
+ input,
2811
+ onSubmitSuccess
2812
+ }) => {
2813
+ return index.o("div", {
2814
+ class: "flex flex-col items-center py-3",
2815
+ children: index.o("button", {
2816
+ class: "bg-accent-9 hover:bg-accent-10 active:bg-submit-bg-active hover:border-accent-10 active:border-submit-bg-active border-accent-9 ring-accent-6 focus-visible:outline-accent-8 ring-offset-neutral-1 group flex cursor-pointer rounded-full border border-solid px-5 py-3 pr-4 text-white shadow-[inset_0px_-6px_2px_-1px_oklch(100_0_0/.45),inset_0px_1px_1px_oklch(100_0_0/.3)] outline-none ring-1 ring-offset-[1.5px] transition-all duration-300 focus-visible:outline-2 active:shadow-[inset_0px_0px_2px_-1px_oklch(100_0_0/.45),inset_0px_3px_1px_.5px_oklch(0_0_0/.08)] active:ring-2 active:ring-offset-2",
2817
+ name: input.key,
2818
+ onClick: () => {
2819
+ onSubmitSuccess(null);
2820
+ },
2821
+ children: index.o("span", {
2822
+ class: "relative bottom-[2px] top-[-2px] flex items-center gap-1.5 transition-all duration-300 group-active:bottom-0 group-active:top-0",
2823
+ children: [index.o("span", {
2824
+ class: "inline-flex items-center text-sm font-medium",
2825
+ children: input.config.label
2826
+ }), index.o("svg", {
2827
+ stroke: "currentColor",
2828
+ "stroke-width": "1.5",
2829
+ width: "16",
2830
+ height: "16",
2831
+ viewBox: "0 0 16 16",
2832
+ fill: "none",
2833
+ xmlns: "http://www.w3.org/2000/svg",
2834
+ children: [index.o("path", {
2835
+ d: "M4 8L8 4L12 8"
2836
+ }), index.o("path", {
2837
+ d: "M8 4V13"
2838
+ })]
2839
+ })]
2840
+ })
2841
+ })
2842
+ });
2843
+ };
2727
2844
  const errors = {
2728
2845
  empty: "Please enter some text",
2729
2846
  email: "That doesn’t look like a valid email address",
@@ -2751,7 +2868,7 @@ const inputFormatToProps = {
2751
2868
  formNoValidate: true
2752
2869
  }
2753
2870
  };
2754
- const isTextSubmission = isSubmissionOfType("text");
2871
+ const isTextSubmission = index.isSubmissionOfType("text");
2755
2872
  const getResolver = (config) => i(index.object({
2756
2873
  text: {
2757
2874
  email: index.string(errors.email, [index.email(errors.email)]),
@@ -2766,7 +2883,7 @@ const ChatInputText = ({
2766
2883
  onHeightChange
2767
2884
  }) => {
2768
2885
  var _a;
2769
- const submission = input.key ? (_a = index.application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2886
+ const submission = input.key ? (_a = index.store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2770
2887
  const defaultValue = input.config.defaultValue;
2771
2888
  const {
2772
2889
  register,
@@ -2832,22 +2949,19 @@ const ChatInputText = ({
2832
2949
  })
2833
2950
  });
2834
2951
  };
2835
- const ChatInput = ({
2836
- onSubmit,
2837
- onInputChange,
2838
- input
2839
- }) => {
2952
+ const ChatInput = () => {
2953
+ var _a;
2954
+ const input = (_a = index.store.current$.value.flow) == null ? void 0 : _a.data.currentInput;
2840
2955
  const inputWrapperRef = index._$1(null);
2841
2956
  const updateHeight = index.T(() => {
2842
2957
  if (inputWrapperRef.current) {
2843
- index.inputHeight.value = inputWrapperRef.current.getBoundingClientRect().height;
2958
+ index.store.inputHeight$.value = inputWrapperRef.current.getBoundingClientRect().height;
2844
2959
  }
2845
2960
  }, []);
2846
2961
  index.p(() => {
2847
2962
  updateHeight();
2848
- onInputChange(input == null ? void 0 : input.type);
2849
- }, [input == null ? void 0 : input.type, onInputChange, updateHeight]);
2850
- const handleSubmitSuccess = (type) => (value) => onSubmit({
2963
+ }, [input == null ? void 0 : input.type, updateHeight]);
2964
+ const handleSubmitSuccess = (type) => (value) => chatStore.onSubmitSuccessFn$.value({
2851
2965
  type,
2852
2966
  value
2853
2967
  });
@@ -2856,18 +2970,19 @@ const ChatInput = ({
2856
2970
  height: 0
2857
2971
  },
2858
2972
  animate: {
2859
- height: index.inputHeight.value
2973
+ height: index.store.inputHeight$.value
2860
2974
  },
2861
2975
  exit: {
2862
2976
  height: 0,
2863
2977
  opacity: 0
2864
2978
  },
2979
+ onAnimationStart: chatStore.scrollToEnd.smooth,
2980
+ onAnimationComplete: chatStore.scrollToEnd.smooth,
2865
2981
  class: "bg-statusbar absolute bottom-0 w-full overflow-hidden rounded-b-3xl backdrop-blur-md backdrop-saturate-150",
2866
2982
  children: index.o("div", {
2867
2983
  ref: inputWrapperRef,
2868
2984
  class: "border-divider border-t",
2869
2985
  children: index.N({
2870
- application: index.application,
2871
2986
  input,
2872
2987
  onHeightChange: updateHeight
2873
2988
  }).with({
@@ -2912,6 +3027,13 @@ const ChatInput = ({
2912
3027
  }, (props) => index.o(ChatInputFile, {
2913
3028
  onSubmitSuccess: handleSubmitSuccess(props.input.type),
2914
3029
  ...props
3030
+ })).with({
3031
+ input: {
3032
+ type: "submit"
3033
+ }
3034
+ }, (props) => index.o(ChatInputSubmit, {
3035
+ onSubmitSuccess: handleSubmitSuccess(props.input.type),
3036
+ ...props
2915
3037
  })).exhaustive()
2916
3038
  })
2917
3039
  });
@@ -2960,7 +3082,7 @@ const cva = (base, config) => {
2960
3082
  return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
2961
3083
  };
2962
3084
  };
2963
- const chatBubbleVariants = cva("max-w-[min(100%,24rem)] [text-wrap:pretty] leading-snug flex-shrink min-w-[2rem] py-2 px-3 rounded-[18px] min-h-[36px] break-words", {
3085
+ const chatBubbleVariants = cva("max-w-[min(100%,24rem)] [text-wrap:pretty] leading-snug flex-shrink min-w-[2rem] py-2 px-3 rounded-[18px] min-h-[36px] break-words relative", {
2964
3086
  variants: {
2965
3087
  side: {
2966
3088
  left: "bg-bubble-weak-bg text-neutral-12 shadow-surface-sm outline outline-1 outline-bubble-weak rounded-bl-md",
@@ -3020,18 +3142,24 @@ const TypingIndicator = ({
3020
3142
  className,
3021
3143
  ...props
3022
3144
  }) => {
3145
+ index.p(() => {
3146
+ chatStore.scrollToEnd.smooth();
3147
+ });
3023
3148
  return index.o("div", {
3024
- "aria-label": "Typing…",
3025
- class: index.clsx("flex gap-1 p-4", className),
3026
- ...props,
3027
- children: Array.from({
3028
- length: 3
3029
- }, (_, i2) => index.o("div", {
3030
- class: "bg-accent-9 h-1.5 w-1.5 animate-bounce rounded-full",
3031
- style: {
3032
- animationDelay: `${-i2 * 200}ms`
3033
- }
3034
- }))
3149
+ "aria-hidden": true,
3150
+ children: chatStore.isBotTyping$.value === true ? index.o("div", {
3151
+ "aria-label": "Typing…",
3152
+ class: index.clsx("flex gap-1 p-4", className),
3153
+ ...props,
3154
+ children: Array.from({
3155
+ length: 3
3156
+ }, (_, i2) => index.o("div", {
3157
+ class: "bg-accent-9 h-1.5 w-1.5 animate-bounce rounded-full",
3158
+ style: {
3159
+ animationDelay: `${-i2 * 200}ms`
3160
+ }
3161
+ }))
3162
+ }) : void 0
3035
3163
  });
3036
3164
  };
3037
3165
  const authorToSide = {
@@ -3049,213 +3177,131 @@ const systemMessageStyle = cva("w-full select-none py-2 text-wrap-balance text-c
3049
3177
  }
3050
3178
  });
3051
3179
  const Conversation = ({
3052
- messages,
3053
- isBotTyping
3180
+ lastSentMessageFooter
3054
3181
  }) => {
3182
+ var _a;
3183
+ const messages = ((_a = index.store.current$.value.flow) == null ? void 0 : _a.data.messages) ?? [];
3184
+ index.p(() => {
3185
+ chatStore.scrollToEnd.smooth();
3186
+ }, [messages.length]);
3055
3187
  return index.o("ol", {
3056
3188
  "aria-label": "Chat messages",
3057
3189
  class: "flex flex-col justify-end gap-2 p-2 pt-[calc(var(--header-height)+1rem)]",
3058
3190
  children: [index.o(index.AnimatePresence, {
3059
3191
  initial: false,
3060
- children: messages.map((message, i2) => index.o("li", {
3061
- class: "flex",
3062
- children: index.N(message).with({
3063
- type: "system"
3064
- }, (message2) => index.o("p", {
3065
- class: systemMessageStyle({
3066
- variant: message2.variant
3067
- }),
3068
- children: message2.text
3069
- })).with({
3070
- type: "text",
3071
- author: index._.union("bot", "user")
3072
- }, (message2) => {
3073
- return index.o(ChatBubble, {
3074
- side: authorToSide[message2.author],
3075
- children: message2.text
3076
- }, i2);
3077
- }).with({
3078
- type: "link"
3079
- }, (message2) => {
3080
- return index.o("div", {
3081
- class: "bg-accent-3 flex w-full items-center justify-center overflow-hidden rounded-xl px-2 py-2",
3082
- children: index.o("a", {
3083
- class: "bg-lowest shadow-surface-sm ring-accent-6 hover:ring-accent-8 active:bg-accent-2 active:text-accent-10 text-accent-9 focus-visible:ring-accent-7/50 text-wrap-balance flex items-center gap-1.5 rounded-full py-2 pl-4 pr-2.5 text-center no-underline ring-1 transition-all focus:outline-none focus-visible:ring-4 focus-visible:ring-offset-2",
3084
- target: "_blank",
3085
- href: message2.href,
3086
- children: [message2.text, index.o("svg", {
3087
- class: "flex-none",
3088
- width: "15",
3089
- height: "15",
3090
- viewBox: "0 0 15 15",
3091
- fill: "none",
3092
- xmlns: "http://www.w3.org/2000/svg",
3093
- children: index.o("path", {
3094
- d: "M3.64645 11.3536C3.45118 11.1583 3.45118 10.8417 3.64645 10.6465L10.2929 4L6 4C5.72386 4 5.5 3.77614 5.5 3.5C5.5 3.22386 5.72386 3 6 3L11.5 3C11.6326 3 11.7598 3.05268 11.8536 3.14645C11.9473 3.24022 12 3.36739 12 3.5L12 9.00001C12 9.27615 11.7761 9.50001 11.5 9.50001C11.2239 9.50001 11 9.27615 11 9.00001V4.70711L4.35355 11.3536C4.15829 11.5488 3.84171 11.5488 3.64645 11.3536Z",
3095
- fill: "currentColor",
3096
- "fill-rule": "evenodd",
3097
- "clip-rule": "evenodd"
3192
+ children: messages.map((message, i2) => {
3193
+ return index.o(index.k, {
3194
+ children: index.o("li", {
3195
+ class: "flex",
3196
+ children: index.N(message).with({
3197
+ type: "system"
3198
+ }, (message2) => index.o("p", {
3199
+ class: systemMessageStyle({
3200
+ variant: message2.variant
3201
+ }),
3202
+ children: message2.text
3203
+ })).with({
3204
+ type: "text",
3205
+ author: index._.union("bot", "user")
3206
+ }, (message2) => {
3207
+ const isLastSentMessage = message2.author === "user" && !messages.slice(i2 + 1).some((m) => m.type === "text" && m.author === "user");
3208
+ return index.o(ChatBubble, {
3209
+ side: authorToSide[message2.author],
3210
+ children: [message2.text, isLastSentMessage ? lastSentMessageFooter : null]
3211
+ }, i2);
3212
+ }).with({
3213
+ type: "link"
3214
+ }, (message2) => {
3215
+ return index.o("div", {
3216
+ class: "bg-accent-3 flex w-full items-center justify-center overflow-hidden rounded-xl px-2 py-2",
3217
+ children: index.o("a", {
3218
+ class: "bg-lowest shadow-surface-sm ring-accent-6 hover:ring-accent-8 active:bg-accent-2 active:text-accent-10 text-accent-9 focus-visible:ring-accent-7/50 text-wrap-balance flex items-center gap-1.5 rounded-full py-2 pl-4 pr-2.5 text-center no-underline ring-1 transition-all focus:outline-none focus-visible:ring-4 focus-visible:ring-offset-2",
3219
+ target: "_blank",
3220
+ href: message2.href,
3221
+ children: [message2.text, index.o("svg", {
3222
+ class: "flex-none",
3223
+ width: "15",
3224
+ height: "15",
3225
+ viewBox: "0 0 15 15",
3226
+ fill: "none",
3227
+ xmlns: "http://www.w3.org/2000/svg",
3228
+ children: index.o("path", {
3229
+ d: "M3.64645 11.3536C3.45118 11.1583 3.45118 10.8417 3.64645 10.6465L10.2929 4L6 4C5.72386 4 5.5 3.77614 5.5 3.5C5.5 3.22386 5.72386 3 6 3L11.5 3C11.6326 3 11.7598 3.05268 11.8536 3.14645C11.9473 3.24022 12 3.36739 12 3.5L12 9.00001C12 9.27615 11.7761 9.50001 11.5 9.50001C11.2239 9.50001 11 9.27615 11 9.00001V4.70711L4.35355 11.3536C4.15829 11.5488 3.84171 11.5488 3.64645 11.3536Z",
3230
+ fill: "currentColor",
3231
+ "fill-rule": "evenodd",
3232
+ "clip-rule": "evenodd"
3233
+ })
3234
+ })]
3098
3235
  })
3099
- })]
3100
- })
3101
- });
3102
- }).with({
3103
- type: "image"
3104
- }, (image) => index.o("img", {
3105
- class: "shadow-surface-md w-full max-w-[min(100%,24rem)] rounded-2xl",
3106
- src: image.url,
3107
- style: {
3108
- aspectRatio: image.width / image.height
3109
- }
3110
- })).with({
3111
- type: "file"
3112
- }, (file) => {
3113
- return index.o(FileThumbnail, {
3114
- class: file.author === "bot" ? "" : "ml-auto",
3115
- file: {
3116
- name: file.fileName,
3117
- sizeKb: file.fileSizeKb
3118
- }
3119
- });
3120
- }).exhaustive()
3121
- }, i2))
3122
- }), index.o("aside", {
3123
- "aria-hidden": true,
3124
- children: isBotTyping && index.o(TypingIndicator, {})
3125
- })]
3236
+ });
3237
+ }).with({
3238
+ type: "image"
3239
+ }, (image) => index.o("img", {
3240
+ class: "shadow-surface-md w-full max-w-[min(100%,24rem)] rounded-2xl",
3241
+ src: image.url,
3242
+ style: {
3243
+ aspectRatio: image.width / image.height
3244
+ }
3245
+ })).with({
3246
+ type: "file"
3247
+ }, (file) => {
3248
+ return index.o(FileThumbnail, {
3249
+ class: file.author === "bot" ? "" : "ml-auto",
3250
+ file: {
3251
+ name: file.fileName,
3252
+ sizeKb: file.fileSizeKb
3253
+ }
3254
+ });
3255
+ }).exhaustive()
3256
+ })
3257
+ }, i2);
3258
+ })
3259
+ }), index.o(TypingIndicator, {}, "typing")]
3126
3260
  });
3127
3261
  };
3128
- const TYPING_SPEED_MS_PER_CHARACTER = 25;
3129
- const useChatService = () => {
3130
- const chatRef = index._$1(null);
3131
- const [isBotTyping, setIsBotTyping] = index.h(false);
3132
- const [onSubmitSuccessFn, setOnSubmitSuccessFn] = index.h(() => () => {
3133
- });
3134
- const scrollToEnd = index.F(() => (options2) => {
3135
- var _a;
3136
- return (_a = chatRef.current) == null ? void 0 : _a.scrollTo({
3137
- top: chatRef.current.scrollHeight,
3138
- ...options2
3139
- });
3140
- }, [chatRef]);
3141
- const chatService = index.F(() => ({
3142
- send: async ({
3143
- message,
3144
- signal,
3145
- groupId
3146
- }) => {
3147
- await index.N(message).with({
3148
- author: "bot",
3149
- type: "text"
3150
- }, async (message2) => {
3151
- if (signal == null ? void 0 : signal.aborted)
3152
- throw new AbortedError();
3153
- setIsBotTyping(true);
3154
- const typingTime = Math.min(Math.max(20, message2.text.length), 100) * TYPING_SPEED_MS_PER_CHARACTER;
3155
- await new Promise((resolve) => {
3156
- return setTimeout(resolve, typingTime, {
3157
- signal
3158
- });
3159
- });
3160
- setIsBotTyping(false);
3161
- }).otherwise(async () => void 0);
3162
- if (signal == null ? void 0 : signal.aborted)
3163
- throw new AbortedError();
3164
- index.application.addMessage(message, groupId);
3165
- },
3166
- input: async ({
3167
- input,
3168
- signal
3169
- }) => {
3170
- if (signal == null ? void 0 : signal.aborted)
3171
- throw new AbortedError();
3172
- index.application.setInput(input);
3173
- return await new Promise((resolve) => {
3174
- const submitFunction = (submission) => {
3175
- if (signal == null ? void 0 : signal.aborted)
3176
- throw new AbortedError();
3177
- index.application.setInput(void 0);
3178
- if (input.key) {
3179
- index.application.setSubmission;
3180
- index.application.setSubmission(input.key, submission);
3181
- }
3182
- resolve(submission);
3183
- };
3184
- setOnSubmitSuccessFn(() => submitFunction);
3185
- });
3186
- }
3187
- }), []);
3188
- return {
3189
- chatRef,
3190
- chatService,
3191
- isBotTyping,
3192
- onSubmitSuccessFn,
3193
- scrollToEnd
3194
- };
3195
- };
3196
- const JobApplicationContent = ({
3197
- currentApplication,
3262
+ const ChatbotBody = ({
3198
3263
  logger,
3199
3264
  apiClient,
3200
3265
  analytics
3201
3266
  }) => {
3267
+ const {
3268
+ flow
3269
+ } = index.store.current$.value;
3270
+ index.invariant(flow, "Flow is required to exist to show chatbot body");
3271
+ const view = index.store.viewState$.value;
3202
3272
  const {
3203
3273
  chatRef,
3204
- chatService,
3205
- isBotTyping,
3206
- onSubmitSuccessFn,
3207
- scrollToEnd
3274
+ chatService
3208
3275
  } = useChatService();
3209
- const view = index.viewState.value;
3210
- const flow = currentApplication.flow;
3211
- const job = currentApplication.job;
3276
+ const [undoFn, setUndoFn] = index.h();
3212
3277
  index.y(() => {
3213
3278
  if (view === "maximised")
3214
- scrollToEnd({
3215
- behavior: "instant"
3216
- });
3217
- }, [scrollToEnd, view]);
3218
- index.p(() => {
3219
- scrollToEnd({
3220
- behavior: "smooth"
3221
- });
3222
- }, [currentApplication.data.messages, scrollToEnd]);
3279
+ chatStore.scrollToEnd.instant();
3280
+ }, [view]);
3223
3281
  index.y(() => {
3224
3282
  const {
3225
3283
  state,
3226
- application: currentApplication2
3227
- } = index.application.current$.peek();
3284
+ flow: currentApplication
3285
+ } = index.store.current$.peek();
3228
3286
  if (state !== "loaded")
3229
3287
  throw new Error(index.ERROR_MESSAGES.invalid_state);
3230
- let fromNodeId = currentApplication2.data.currentNodeId;
3231
- scrollToEnd({
3232
- behavior: "instant"
3233
- });
3234
- index.application.setInput(void 0);
3235
- if (currentApplication2.data.isFinished)
3288
+ let fromNodeId = currentApplication.data.nodeHistory.at(-1);
3289
+ chatStore.scrollToEnd.instant();
3290
+ index.store.setInput(void 0);
3291
+ if (currentApplication.data.isFinished)
3236
3292
  return;
3237
- if (fromNodeId === null) {
3238
- fromNodeId = getHeadOrThrow(flow.nodes).id;
3239
- index.application.setCurrentNodeId(fromNodeId);
3240
- analytics.log({
3241
- event: "APPLY_START",
3242
- attributionKey: `job_${job.id}`,
3243
- properties: {
3244
- job_id: job.id,
3245
- flow_id: flow.id
3246
- }
3247
- });
3293
+ if (fromNodeId === void 0) {
3294
+ fromNodeId = index.getHeadOrThrow(flow.nodes).id;
3295
+ index.store.setCurrentNodeId(fromNodeId);
3248
3296
  } else {
3249
- index.application.removeLastGroupMessagesById(fromNodeId);
3297
+ index.store.removeMessagesSentByNodeIds([fromNodeId]);
3250
3298
  }
3251
3299
  const {
3252
3300
  interpret: interpret2,
3253
- abort
3301
+ abort,
3302
+ undo
3254
3303
  } = createFlowInterpreter({
3255
- context: {
3256
- jobId: job.id,
3257
- flowId: flow.id
3258
- },
3304
+ context: flow.context,
3259
3305
  analytics,
3260
3306
  apiClient,
3261
3307
  logger,
@@ -3264,57 +3310,87 @@ const JobApplicationContent = ({
3264
3310
  // We need to get fresh submissions, that’s why we call `peek` here.
3265
3311
  getSubmissions: () => {
3266
3312
  var _a;
3267
- return (_a = index.application.current$.peek().application) == null ? void 0 : _a.data.submissions;
3313
+ return (_a = index.store.current$.peek().flow) == null ? void 0 : _a.data.submissions;
3268
3314
  },
3269
3315
  onInterpret: (node, prevNode) => {
3270
- const currentState = index.application.current$.peek().application;
3316
+ const currentState = index.store.current$.peek().flow;
3271
3317
  index.invariant(currentState);
3272
3318
  if (prevNode) {
3273
3319
  currentState.data.sequence = currentState.data.sequence + 1;
3274
3320
  analytics.log({
3275
3321
  event: "FLOW_NODE",
3276
- attributionKey: `job_${job.id}`,
3277
3322
  properties: {
3278
3323
  flow_id: flow.id,
3279
3324
  flow_version: flow.version,
3280
- job_id: job.id,
3281
3325
  from_node_id: prevNode.id,
3282
3326
  to_node_id: node.id,
3283
3327
  sequence: currentState.data.sequence,
3284
3328
  flow_session_id: currentState.data.flowSessionId
3285
- }
3329
+ },
3330
+ customProperties: flow.context
3286
3331
  });
3287
3332
  }
3288
- index.application.setCurrentNodeId(node.id);
3333
+ index.store.setCurrentNodeId(node.id);
3289
3334
  },
3290
3335
  onFlowEnd: async () => {
3291
- index.application.markAsFinished();
3336
+ index.store.markAsFinished();
3292
3337
  }
3293
3338
  });
3339
+ setUndoFn(() => undo);
3294
3340
  interpret2(fromNodeId);
3295
3341
  return abort;
3296
- }, [analytics, apiClient, chatService, logger, scrollToEnd, flow, job.id, currentApplication.startedAt]);
3342
+ }, [analytics, apiClient, chatService, logger, flow]);
3297
3343
  return index.o(index.k, {
3298
3344
  children: [index.o("div", {
3299
3345
  ref: chatRef,
3300
- className: "hide-scrollbars relative flex max-w-full flex-grow flex-col overflow-y-scroll",
3346
+ className: "hide-scrollbars relative flex w-full max-w-full flex-grow flex-col overflow-y-scroll",
3301
3347
  style: {
3302
3348
  WebkitOverflowScrolling: "touch",
3303
- paddingBottom: index.inputHeight.value
3349
+ paddingBottom: index.store.inputHeight$.value
3304
3350
  },
3305
- children: index.o(index.AnimatePresence, {
3306
- children: index.o(Conversation, {
3307
- isBotTyping,
3308
- messages: currentApplication.data.messages
3351
+ children: index.o(Conversation, {
3352
+ lastSentMessageFooter: flow.data.isFinished || !undoFn ? null : index.o(UndoButton, {
3353
+ undoFn
3309
3354
  })
3310
3355
  })
3311
- }), index.o(ChatInput, {
3312
- input: currentApplication.data.currentInput,
3313
- onInputChange: () => scrollToEnd({
3314
- behavior: "smooth"
3315
- }),
3316
- onSubmit: onSubmitSuccessFn
3317
- })]
3356
+ }), index.o(ChatInput, {})]
3357
+ });
3358
+ };
3359
+ const getNodeKeys = (node) => {
3360
+ if ("key" in node.data && node.data.key)
3361
+ return [node.data.key];
3362
+ if ("keys" in node.data)
3363
+ return Object.values(node.data.keys).filter((key) => typeof key === "string");
3364
+ return [];
3365
+ };
3366
+ const UndoButton = ({
3367
+ undoFn
3368
+ }) => {
3369
+ return index.o("div", {
3370
+ class: "absolute bottom-0 right-0 flex w-full translate-y-full justify-end",
3371
+ children: index.o("button", {
3372
+ class: "fr touch-hitbox text-neutral-9 hover:text-neutral-12 rounded-full p-1 text-right text-xs transition-all",
3373
+ onClick: async () => {
3374
+ const {
3375
+ flow
3376
+ } = index.store.current$.peek();
3377
+ index.invariant(flow);
3378
+ const {
3379
+ removed
3380
+ } = undoFn(flow.data.nodeHistory);
3381
+ const removedNodeIds = flow.data.nodeHistory.splice(-removed);
3382
+ if (removedNodeIds.length === 0)
3383
+ return;
3384
+ index.store.removeMessagesSentByNodeIds(removedNodeIds);
3385
+ index.store.setInput(void 0);
3386
+ removedNodeIds.pop();
3387
+ removedNodeIds.map((nodeId) => flow.nodes.find((node) => node.id === nodeId)).filter(Boolean).flatMap(getNodeKeys).forEach((key) => delete flow.data.submissions[key]);
3388
+ index.store.current$.value = {
3389
+ ...index.store.current$.value
3390
+ };
3391
+ },
3392
+ children: "Undo"
3393
+ })
3318
3394
  });
3319
3395
  };
3320
- exports.JobApplicationContent = JobApplicationContent;
3396
+ exports.ChatbotBody = ChatbotBody;