@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,29 +1,5 @@
1
- import { N, _, h as hasProp, i as invariant, o as o$1, c as clsx, a as _$1, p, b as parse, d as picklist, y, e as application, f as h, k, C as Cn, g as parseAsync, V as ValiError, j as object, t as transform, m as maxLength, l as minLength, r as record, n as boolean, s as string, q as email, u as url, v as regex, T, w as inputHeight, x as m, A as AnimatePresence, F, z as viewState, E as ERROR_MESSAGES } from "./index-5333c591.js";
1
+ import { _, g as getHeadOrThrow, i as invariant, A as AbortedError, N, a as getFlowSubmissionsPayload, h as hasProp, k as kbToReadableSize, o as o$1, c as clsx, b as a$2, d as _$1, y, F, s as store, p, e as parse, f as picklist, j as h, l as k, m as isSubmissionOfType, C as Cn, n as parseAsync, V as ValiError, q as object, t as transform, r as maxLength, u as minLength, v as record, w as boolean, x as string, z as email, B as url, D as regex, T, E as m, G as AnimatePresence, H as ERROR_MESSAGES } from "./index-fc00a362.js";
2
2
  import "@inploi/sdk";
3
- const kbToReadableSize = (kb) => N(kb).with(_.number.lte(1e3), () => `${Math.round(kb)}KB`).with(_.number.lt(1e3 * 10), () => `${(kb / 1e3).toFixed(1)}MB`).otherwise(() => `${Math.round(kb / 1e3)}MB`);
4
- const getHeadOrThrow = (nodes) => {
5
- const head = nodes.find((n2) => n2.isHead);
6
- if (!head)
7
- throw new Error("No head node found");
8
- return head;
9
- };
10
- const getApplicationSubmissionsPayload = (submissions) => {
11
- const payload = Object.entries(submissions).reduce((acc, [key, submission]) => {
12
- acc[key] = submission.value;
13
- return acc;
14
- }, {});
15
- return payload;
16
- };
17
- const isSubmissionOfType = (type) => (submission) => {
18
- if (!submission)
19
- return false;
20
- return submission.type === type;
21
- };
22
- class AbortedError extends Error {
23
- constructor() {
24
- super("Aborted");
25
- }
26
- }
27
3
  const followNodes = ({
28
4
  node,
29
5
  nodes,
@@ -75,7 +51,7 @@ const createFlowInterpreter = ({
75
51
  onFlowEnd,
76
52
  onInterpret
77
53
  }) => {
78
- const controller = new AbortController();
54
+ let controller = new AbortController();
79
55
  const interpretNode = async (node, prevNode) => {
80
56
  const submissions = getSubmissions();
81
57
  onInterpret == null ? void 0 : onInterpret(node, prevNode);
@@ -120,6 +96,27 @@ const createFlowInterpreter = ({
120
96
  const startNode = flow.find((node) => node.id === startFromNodeId) ?? getHeadOrThrow(flow);
121
97
  return interpretNode(startNode);
122
98
  },
99
+ undo: (nodeHistory) => {
100
+ let removed = 1;
101
+ const formerLastNode = flow.find((n2) => n2.id === nodeHistory[nodeHistory.length - 1]);
102
+ for (let i2 = nodeHistory.length - 2; i2 > 0; i2--) {
103
+ const nodeId = nodeHistory[i2];
104
+ const node = flow.find((n2) => n2.id === nodeId);
105
+ if (!node)
106
+ break;
107
+ removed++;
108
+ if (node.type.startsWith("question-"))
109
+ break;
110
+ }
111
+ controller.abort();
112
+ controller = new AbortController();
113
+ const newStartNode = flow.find((node) => node.id === nodeHistory[nodeHistory.length - removed]);
114
+ invariant(newStartNode, "Undo failed: new start node not found");
115
+ interpretNode(newStartNode, formerLastNode);
116
+ return {
117
+ removed
118
+ };
119
+ },
123
120
  abort: () => {
124
121
  controller.abort();
125
122
  }
@@ -198,22 +195,17 @@ async function interpretSubmitNode({
198
195
  analytics
199
196
  }) {
200
197
  await chat.userInput({
201
- type: "multiple-choice",
198
+ type: "submit",
202
199
  key: void 0,
203
200
  config: {
204
- options: [{
205
- label: "Submit my application",
206
- value: "submit"
207
- }],
208
- maxSelected: 1,
209
- minSelected: 1
201
+ label: "Submit my application"
210
202
  }
211
203
  });
212
204
  const logApplyComplete = () => {
213
- const contextJobId = typeof context.jobId === "string" ? context.jobId : void 0;
214
- const contextFlowId = typeof context.flowId === "string" ? context.flowId : void 0;
215
- invariant(contextJobId);
216
- invariant(contextFlowId);
205
+ const contextJobId = typeof context.job_id === "string" ? context.job_id : void 0;
206
+ const contextFlowId = typeof context.flow_id === "string" ? context.flow_id : void 0;
207
+ if (!contextJobId || !contextFlowId)
208
+ return;
217
209
  if (contextJobId) {
218
210
  analytics.log({
219
211
  event: "APPLY_COMPLETE",
@@ -241,7 +233,7 @@ async function interpretSubmitNode({
241
233
  integration_id: node.data.integrationId,
242
234
  anonymous_id,
243
235
  session_id,
244
- submissions: getApplicationSubmissionsPayload(submissions || {})
236
+ submissions: getFlowSubmissionsPayload(submissions || {})
245
237
  })
246
238
  }).catch((e) => e);
247
239
  await N(response).with({
@@ -812,6 +804,96 @@ const SendButton = ({
812
804
  })]
813
805
  })
814
806
  });
807
+ const TYPING_SPEED_MS_PER_CHARACTER = 25;
808
+ const scrollToEndFn$ = a$2({
809
+ instant: () => {
810
+ },
811
+ smooth: () => {
812
+ }
813
+ });
814
+ const chatStore = {
815
+ onSubmitSuccessFn$: a$2(() => {
816
+ }),
817
+ isBotTyping$: a$2(false),
818
+ scrollToEnd: {
819
+ instant: () => scrollToEndFn$.value.instant(),
820
+ smooth: () => scrollToEndFn$.value.smooth()
821
+ }
822
+ };
823
+ const useChatService = () => {
824
+ const chatRef = _$1(null);
825
+ y(() => {
826
+ scrollToEndFn$.value = {
827
+ instant: () => {
828
+ var _a;
829
+ return (_a = chatRef.current) == null ? void 0 : _a.scrollTo({
830
+ top: chatRef.current.scrollHeight,
831
+ behavior: "instant"
832
+ });
833
+ },
834
+ smooth: () => {
835
+ if (!chatRef.current)
836
+ return;
837
+ if (chatRef.current.scrollHeight - chatRef.current.scrollTop <= chatRef.current.clientHeight * 1.5) {
838
+ chatRef.current.scrollTo({
839
+ top: chatRef.current.scrollHeight,
840
+ behavior: "smooth"
841
+ });
842
+ }
843
+ }
844
+ };
845
+ }, [chatRef]);
846
+ const chatService = F(() => ({
847
+ send: async ({
848
+ message,
849
+ signal: signal2,
850
+ groupId
851
+ }) => {
852
+ await N(message).with({
853
+ author: "bot",
854
+ type: "text"
855
+ }, async (message2) => {
856
+ if (signal2 == null ? void 0 : signal2.aborted)
857
+ throw new AbortedError();
858
+ chatStore.isBotTyping$.value = true;
859
+ const typingTime = Math.min(Math.max(20, message2.text.length), 100) * TYPING_SPEED_MS_PER_CHARACTER;
860
+ await new Promise((resolve) => {
861
+ return setTimeout(resolve, typingTime, {
862
+ signal: signal2
863
+ });
864
+ });
865
+ chatStore.isBotTyping$.value = false;
866
+ }).otherwise(async () => void 0);
867
+ if (signal2 == null ? void 0 : signal2.aborted)
868
+ throw new AbortedError();
869
+ store.addMessage(message, groupId);
870
+ },
871
+ input: async ({
872
+ input,
873
+ signal: signal2
874
+ }) => {
875
+ if (signal2 == null ? void 0 : signal2.aborted)
876
+ throw new AbortedError();
877
+ store.setInput(input);
878
+ return await new Promise((resolve) => {
879
+ const submitFunction = (submission) => {
880
+ if (signal2 == null ? void 0 : signal2.aborted)
881
+ throw new AbortedError();
882
+ store.setInput(void 0);
883
+ if (input.key) {
884
+ store.setSubmission(input.key, submission);
885
+ }
886
+ resolve(submission);
887
+ };
888
+ chatStore.onSubmitSuccessFn$.value = submitFunction;
889
+ });
890
+ }
891
+ }), []);
892
+ return {
893
+ chatRef,
894
+ chatService
895
+ };
896
+ };
815
897
  const SkipButton = ({
816
898
  class: className,
817
899
  ...props
@@ -1006,7 +1088,7 @@ const ChatInputFile = ({
1006
1088
  onHeightChange
1007
1089
  }) => {
1008
1090
  var _a;
1009
- const submission = (_a = application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key];
1091
+ const submission = (_a = store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key];
1010
1092
  const [files, setFiles] = h(isFileSubmission(submission) && submission.value !== null ? submission.value : []);
1011
1093
  const [error, setError] = h();
1012
1094
  const hiddenFileCount = files.length - FILENAMES_TO_SHOW_QTY;
@@ -2643,7 +2725,8 @@ const ChatInputMultipleChoice = ({
2643
2725
  onHeightChange
2644
2726
  }) => {
2645
2727
  var _a, _b;
2646
- const submission = input.key ? (_a = application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2728
+ const submission = input.key ? (_a = store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2729
+ const isSingleChoice = (input.config.minSelected === 1 || input.config.minSelected === void 0) && input.config.maxSelected === 1;
2647
2730
  const {
2648
2731
  register,
2649
2732
  handleSubmit,
@@ -2652,12 +2735,11 @@ const ChatInputMultipleChoice = ({
2652
2735
  }
2653
2736
  } = useForm({
2654
2737
  defaultValues: {
2655
- checked: isMultipleChoiceSubmission(submission) ? Object.fromEntries(submission.value.map((key) => [key, true])) : {}
2738
+ checked: isSingleChoice ? {} : isMultipleChoiceSubmission(submission) ? Object.fromEntries(submission.value.map((key) => [key, true])) : {}
2656
2739
  },
2657
2740
  resolver: getResolver$1(input.config)
2658
2741
  });
2659
2742
  const focusRef = useFocusOnMount();
2660
- const isSingleChoice = (input.config.minSelected === 1 || input.config.minSelected === void 0) && input.config.maxSelected === 1;
2661
2743
  return o$1("form", {
2662
2744
  noValidate: true,
2663
2745
  class: "flex flex-col gap-1 pr-2.5",
@@ -2722,6 +2804,41 @@ const ChatInputMultipleChoice = ({
2722
2804
  })]
2723
2805
  });
2724
2806
  };
2807
+ const ChatInputSubmit = ({
2808
+ input,
2809
+ onSubmitSuccess
2810
+ }) => {
2811
+ return o$1("div", {
2812
+ class: "flex flex-col items-center py-3",
2813
+ children: o$1("button", {
2814
+ 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",
2815
+ name: input.key,
2816
+ onClick: () => {
2817
+ onSubmitSuccess(null);
2818
+ },
2819
+ children: o$1("span", {
2820
+ class: "relative bottom-[2px] top-[-2px] flex items-center gap-1.5 transition-all duration-300 group-active:bottom-0 group-active:top-0",
2821
+ children: [o$1("span", {
2822
+ class: "inline-flex items-center text-sm font-medium",
2823
+ children: input.config.label
2824
+ }), o$1("svg", {
2825
+ stroke: "currentColor",
2826
+ "stroke-width": "1.5",
2827
+ width: "16",
2828
+ height: "16",
2829
+ viewBox: "0 0 16 16",
2830
+ fill: "none",
2831
+ xmlns: "http://www.w3.org/2000/svg",
2832
+ children: [o$1("path", {
2833
+ d: "M4 8L8 4L12 8"
2834
+ }), o$1("path", {
2835
+ d: "M8 4V13"
2836
+ })]
2837
+ })]
2838
+ })
2839
+ })
2840
+ });
2841
+ };
2725
2842
  const errors = {
2726
2843
  empty: "Please enter some text",
2727
2844
  email: "That doesn’t look like a valid email address",
@@ -2764,7 +2881,7 @@ const ChatInputText = ({
2764
2881
  onHeightChange
2765
2882
  }) => {
2766
2883
  var _a;
2767
- const submission = input.key ? (_a = application.current$.value.application) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2884
+ const submission = input.key ? (_a = store.current$.value.flow) == null ? void 0 : _a.data.submissions[input.key] : void 0;
2768
2885
  const defaultValue = input.config.defaultValue;
2769
2886
  const {
2770
2887
  register,
@@ -2830,22 +2947,19 @@ const ChatInputText = ({
2830
2947
  })
2831
2948
  });
2832
2949
  };
2833
- const ChatInput = ({
2834
- onSubmit,
2835
- onInputChange,
2836
- input
2837
- }) => {
2950
+ const ChatInput = () => {
2951
+ var _a;
2952
+ const input = (_a = store.current$.value.flow) == null ? void 0 : _a.data.currentInput;
2838
2953
  const inputWrapperRef = _$1(null);
2839
2954
  const updateHeight = T(() => {
2840
2955
  if (inputWrapperRef.current) {
2841
- inputHeight.value = inputWrapperRef.current.getBoundingClientRect().height;
2956
+ store.inputHeight$.value = inputWrapperRef.current.getBoundingClientRect().height;
2842
2957
  }
2843
2958
  }, []);
2844
2959
  p(() => {
2845
2960
  updateHeight();
2846
- onInputChange(input == null ? void 0 : input.type);
2847
- }, [input == null ? void 0 : input.type, onInputChange, updateHeight]);
2848
- const handleSubmitSuccess = (type) => (value) => onSubmit({
2961
+ }, [input == null ? void 0 : input.type, updateHeight]);
2962
+ const handleSubmitSuccess = (type) => (value) => chatStore.onSubmitSuccessFn$.value({
2849
2963
  type,
2850
2964
  value
2851
2965
  });
@@ -2854,18 +2968,19 @@ const ChatInput = ({
2854
2968
  height: 0
2855
2969
  },
2856
2970
  animate: {
2857
- height: inputHeight.value
2971
+ height: store.inputHeight$.value
2858
2972
  },
2859
2973
  exit: {
2860
2974
  height: 0,
2861
2975
  opacity: 0
2862
2976
  },
2977
+ onAnimationStart: chatStore.scrollToEnd.smooth,
2978
+ onAnimationComplete: chatStore.scrollToEnd.smooth,
2863
2979
  class: "bg-statusbar absolute bottom-0 w-full overflow-hidden rounded-b-3xl backdrop-blur-md backdrop-saturate-150",
2864
2980
  children: o$1("div", {
2865
2981
  ref: inputWrapperRef,
2866
2982
  class: "border-divider border-t",
2867
2983
  children: N({
2868
- application,
2869
2984
  input,
2870
2985
  onHeightChange: updateHeight
2871
2986
  }).with({
@@ -2910,6 +3025,13 @@ const ChatInput = ({
2910
3025
  }, (props) => o$1(ChatInputFile, {
2911
3026
  onSubmitSuccess: handleSubmitSuccess(props.input.type),
2912
3027
  ...props
3028
+ })).with({
3029
+ input: {
3030
+ type: "submit"
3031
+ }
3032
+ }, (props) => o$1(ChatInputSubmit, {
3033
+ onSubmitSuccess: handleSubmitSuccess(props.input.type),
3034
+ ...props
2913
3035
  })).exhaustive()
2914
3036
  })
2915
3037
  });
@@ -2958,7 +3080,7 @@ const cva = (base, config) => {
2958
3080
  return cx(base, getVariantClassNames, getCompoundVariantClassNames, props === null || props === void 0 ? void 0 : props.class, props === null || props === void 0 ? void 0 : props.className);
2959
3081
  };
2960
3082
  };
2961
- 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", {
3083
+ 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", {
2962
3084
  variants: {
2963
3085
  side: {
2964
3086
  left: "bg-bubble-weak-bg text-neutral-12 shadow-surface-sm outline outline-1 outline-bubble-weak rounded-bl-md",
@@ -3018,18 +3140,24 @@ const TypingIndicator = ({
3018
3140
  className,
3019
3141
  ...props
3020
3142
  }) => {
3143
+ p(() => {
3144
+ chatStore.scrollToEnd.smooth();
3145
+ });
3021
3146
  return o$1("div", {
3022
- "aria-label": "Typing…",
3023
- class: clsx("flex gap-1 p-4", className),
3024
- ...props,
3025
- children: Array.from({
3026
- length: 3
3027
- }, (_2, i2) => o$1("div", {
3028
- class: "bg-accent-9 h-1.5 w-1.5 animate-bounce rounded-full",
3029
- style: {
3030
- animationDelay: `${-i2 * 200}ms`
3031
- }
3032
- }))
3147
+ "aria-hidden": true,
3148
+ children: chatStore.isBotTyping$.value === true ? o$1("div", {
3149
+ "aria-label": "Typing…",
3150
+ class: clsx("flex gap-1 p-4", className),
3151
+ ...props,
3152
+ children: Array.from({
3153
+ length: 3
3154
+ }, (_2, i2) => o$1("div", {
3155
+ class: "bg-accent-9 h-1.5 w-1.5 animate-bounce rounded-full",
3156
+ style: {
3157
+ animationDelay: `${-i2 * 200}ms`
3158
+ }
3159
+ }))
3160
+ }) : void 0
3033
3161
  });
3034
3162
  };
3035
3163
  const authorToSide = {
@@ -3047,213 +3175,131 @@ const systemMessageStyle = cva("w-full select-none py-2 text-wrap-balance text-c
3047
3175
  }
3048
3176
  });
3049
3177
  const Conversation = ({
3050
- messages,
3051
- isBotTyping
3178
+ lastSentMessageFooter
3052
3179
  }) => {
3180
+ var _a;
3181
+ const messages = ((_a = store.current$.value.flow) == null ? void 0 : _a.data.messages) ?? [];
3182
+ p(() => {
3183
+ chatStore.scrollToEnd.smooth();
3184
+ }, [messages.length]);
3053
3185
  return o$1("ol", {
3054
3186
  "aria-label": "Chat messages",
3055
3187
  class: "flex flex-col justify-end gap-2 p-2 pt-[calc(var(--header-height)+1rem)]",
3056
3188
  children: [o$1(AnimatePresence, {
3057
3189
  initial: false,
3058
- children: messages.map((message, i2) => o$1("li", {
3059
- class: "flex",
3060
- children: N(message).with({
3061
- type: "system"
3062
- }, (message2) => o$1("p", {
3063
- class: systemMessageStyle({
3064
- variant: message2.variant
3065
- }),
3066
- children: message2.text
3067
- })).with({
3068
- type: "text",
3069
- author: _.union("bot", "user")
3070
- }, (message2) => {
3071
- return o$1(ChatBubble, {
3072
- side: authorToSide[message2.author],
3073
- children: message2.text
3074
- }, i2);
3075
- }).with({
3076
- type: "link"
3077
- }, (message2) => {
3078
- return o$1("div", {
3079
- class: "bg-accent-3 flex w-full items-center justify-center overflow-hidden rounded-xl px-2 py-2",
3080
- children: o$1("a", {
3081
- 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",
3082
- target: "_blank",
3083
- href: message2.href,
3084
- children: [message2.text, o$1("svg", {
3085
- class: "flex-none",
3086
- width: "15",
3087
- height: "15",
3088
- viewBox: "0 0 15 15",
3089
- fill: "none",
3090
- xmlns: "http://www.w3.org/2000/svg",
3091
- children: o$1("path", {
3092
- 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",
3093
- fill: "currentColor",
3094
- "fill-rule": "evenodd",
3095
- "clip-rule": "evenodd"
3190
+ children: messages.map((message, i2) => {
3191
+ return o$1(k, {
3192
+ children: o$1("li", {
3193
+ class: "flex",
3194
+ children: N(message).with({
3195
+ type: "system"
3196
+ }, (message2) => o$1("p", {
3197
+ class: systemMessageStyle({
3198
+ variant: message2.variant
3199
+ }),
3200
+ children: message2.text
3201
+ })).with({
3202
+ type: "text",
3203
+ author: _.union("bot", "user")
3204
+ }, (message2) => {
3205
+ const isLastSentMessage = message2.author === "user" && !messages.slice(i2 + 1).some((m2) => m2.type === "text" && m2.author === "user");
3206
+ return o$1(ChatBubble, {
3207
+ side: authorToSide[message2.author],
3208
+ children: [message2.text, isLastSentMessage ? lastSentMessageFooter : null]
3209
+ }, i2);
3210
+ }).with({
3211
+ type: "link"
3212
+ }, (message2) => {
3213
+ return o$1("div", {
3214
+ class: "bg-accent-3 flex w-full items-center justify-center overflow-hidden rounded-xl px-2 py-2",
3215
+ children: o$1("a", {
3216
+ 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",
3217
+ target: "_blank",
3218
+ href: message2.href,
3219
+ children: [message2.text, o$1("svg", {
3220
+ class: "flex-none",
3221
+ width: "15",
3222
+ height: "15",
3223
+ viewBox: "0 0 15 15",
3224
+ fill: "none",
3225
+ xmlns: "http://www.w3.org/2000/svg",
3226
+ children: o$1("path", {
3227
+ 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",
3228
+ fill: "currentColor",
3229
+ "fill-rule": "evenodd",
3230
+ "clip-rule": "evenodd"
3231
+ })
3232
+ })]
3096
3233
  })
3097
- })]
3098
- })
3099
- });
3100
- }).with({
3101
- type: "image"
3102
- }, (image) => o$1("img", {
3103
- class: "shadow-surface-md w-full max-w-[min(100%,24rem)] rounded-2xl",
3104
- src: image.url,
3105
- style: {
3106
- aspectRatio: image.width / image.height
3107
- }
3108
- })).with({
3109
- type: "file"
3110
- }, (file) => {
3111
- return o$1(FileThumbnail, {
3112
- class: file.author === "bot" ? "" : "ml-auto",
3113
- file: {
3114
- name: file.fileName,
3115
- sizeKb: file.fileSizeKb
3116
- }
3117
- });
3118
- }).exhaustive()
3119
- }, i2))
3120
- }), o$1("aside", {
3121
- "aria-hidden": true,
3122
- children: isBotTyping && o$1(TypingIndicator, {})
3123
- })]
3234
+ });
3235
+ }).with({
3236
+ type: "image"
3237
+ }, (image) => o$1("img", {
3238
+ class: "shadow-surface-md w-full max-w-[min(100%,24rem)] rounded-2xl",
3239
+ src: image.url,
3240
+ style: {
3241
+ aspectRatio: image.width / image.height
3242
+ }
3243
+ })).with({
3244
+ type: "file"
3245
+ }, (file) => {
3246
+ return o$1(FileThumbnail, {
3247
+ class: file.author === "bot" ? "" : "ml-auto",
3248
+ file: {
3249
+ name: file.fileName,
3250
+ sizeKb: file.fileSizeKb
3251
+ }
3252
+ });
3253
+ }).exhaustive()
3254
+ })
3255
+ }, i2);
3256
+ })
3257
+ }), o$1(TypingIndicator, {}, "typing")]
3124
3258
  });
3125
3259
  };
3126
- const TYPING_SPEED_MS_PER_CHARACTER = 25;
3127
- const useChatService = () => {
3128
- const chatRef = _$1(null);
3129
- const [isBotTyping, setIsBotTyping] = h(false);
3130
- const [onSubmitSuccessFn, setOnSubmitSuccessFn] = h(() => () => {
3131
- });
3132
- const scrollToEnd = F(() => (options2) => {
3133
- var _a;
3134
- return (_a = chatRef.current) == null ? void 0 : _a.scrollTo({
3135
- top: chatRef.current.scrollHeight,
3136
- ...options2
3137
- });
3138
- }, [chatRef]);
3139
- const chatService = F(() => ({
3140
- send: async ({
3141
- message,
3142
- signal,
3143
- groupId
3144
- }) => {
3145
- await N(message).with({
3146
- author: "bot",
3147
- type: "text"
3148
- }, async (message2) => {
3149
- if (signal == null ? void 0 : signal.aborted)
3150
- throw new AbortedError();
3151
- setIsBotTyping(true);
3152
- const typingTime = Math.min(Math.max(20, message2.text.length), 100) * TYPING_SPEED_MS_PER_CHARACTER;
3153
- await new Promise((resolve) => {
3154
- return setTimeout(resolve, typingTime, {
3155
- signal
3156
- });
3157
- });
3158
- setIsBotTyping(false);
3159
- }).otherwise(async () => void 0);
3160
- if (signal == null ? void 0 : signal.aborted)
3161
- throw new AbortedError();
3162
- application.addMessage(message, groupId);
3163
- },
3164
- input: async ({
3165
- input,
3166
- signal
3167
- }) => {
3168
- if (signal == null ? void 0 : signal.aborted)
3169
- throw new AbortedError();
3170
- application.setInput(input);
3171
- return await new Promise((resolve) => {
3172
- const submitFunction = (submission) => {
3173
- if (signal == null ? void 0 : signal.aborted)
3174
- throw new AbortedError();
3175
- application.setInput(void 0);
3176
- if (input.key) {
3177
- application.setSubmission;
3178
- application.setSubmission(input.key, submission);
3179
- }
3180
- resolve(submission);
3181
- };
3182
- setOnSubmitSuccessFn(() => submitFunction);
3183
- });
3184
- }
3185
- }), []);
3186
- return {
3187
- chatRef,
3188
- chatService,
3189
- isBotTyping,
3190
- onSubmitSuccessFn,
3191
- scrollToEnd
3192
- };
3193
- };
3194
- const JobApplicationContent = ({
3195
- currentApplication,
3260
+ const ChatbotBody = ({
3196
3261
  logger,
3197
3262
  apiClient,
3198
3263
  analytics
3199
3264
  }) => {
3265
+ const {
3266
+ flow
3267
+ } = store.current$.value;
3268
+ invariant(flow, "Flow is required to exist to show chatbot body");
3269
+ const view = store.viewState$.value;
3200
3270
  const {
3201
3271
  chatRef,
3202
- chatService,
3203
- isBotTyping,
3204
- onSubmitSuccessFn,
3205
- scrollToEnd
3272
+ chatService
3206
3273
  } = useChatService();
3207
- const view = viewState.value;
3208
- const flow = currentApplication.flow;
3209
- const job = currentApplication.job;
3274
+ const [undoFn, setUndoFn] = h();
3210
3275
  y(() => {
3211
3276
  if (view === "maximised")
3212
- scrollToEnd({
3213
- behavior: "instant"
3214
- });
3215
- }, [scrollToEnd, view]);
3216
- p(() => {
3217
- scrollToEnd({
3218
- behavior: "smooth"
3219
- });
3220
- }, [currentApplication.data.messages, scrollToEnd]);
3277
+ chatStore.scrollToEnd.instant();
3278
+ }, [view]);
3221
3279
  y(() => {
3222
3280
  const {
3223
3281
  state,
3224
- application: currentApplication2
3225
- } = application.current$.peek();
3282
+ flow: currentApplication
3283
+ } = store.current$.peek();
3226
3284
  if (state !== "loaded")
3227
3285
  throw new Error(ERROR_MESSAGES.invalid_state);
3228
- let fromNodeId = currentApplication2.data.currentNodeId;
3229
- scrollToEnd({
3230
- behavior: "instant"
3231
- });
3232
- application.setInput(void 0);
3233
- if (currentApplication2.data.isFinished)
3286
+ let fromNodeId = currentApplication.data.nodeHistory.at(-1);
3287
+ chatStore.scrollToEnd.instant();
3288
+ store.setInput(void 0);
3289
+ if (currentApplication.data.isFinished)
3234
3290
  return;
3235
- if (fromNodeId === null) {
3291
+ if (fromNodeId === void 0) {
3236
3292
  fromNodeId = getHeadOrThrow(flow.nodes).id;
3237
- application.setCurrentNodeId(fromNodeId);
3238
- analytics.log({
3239
- event: "APPLY_START",
3240
- attributionKey: `job_${job.id}`,
3241
- properties: {
3242
- job_id: job.id,
3243
- flow_id: flow.id
3244
- }
3245
- });
3293
+ store.setCurrentNodeId(fromNodeId);
3246
3294
  } else {
3247
- application.removeLastGroupMessagesById(fromNodeId);
3295
+ store.removeMessagesSentByNodeIds([fromNodeId]);
3248
3296
  }
3249
3297
  const {
3250
3298
  interpret: interpret2,
3251
- abort
3299
+ abort,
3300
+ undo
3252
3301
  } = createFlowInterpreter({
3253
- context: {
3254
- jobId: job.id,
3255
- flowId: flow.id
3256
- },
3302
+ context: flow.context,
3257
3303
  analytics,
3258
3304
  apiClient,
3259
3305
  logger,
@@ -3262,59 +3308,89 @@ const JobApplicationContent = ({
3262
3308
  // We need to get fresh submissions, that’s why we call `peek` here.
3263
3309
  getSubmissions: () => {
3264
3310
  var _a;
3265
- return (_a = application.current$.peek().application) == null ? void 0 : _a.data.submissions;
3311
+ return (_a = store.current$.peek().flow) == null ? void 0 : _a.data.submissions;
3266
3312
  },
3267
3313
  onInterpret: (node, prevNode) => {
3268
- const currentState = application.current$.peek().application;
3314
+ const currentState = store.current$.peek().flow;
3269
3315
  invariant(currentState);
3270
3316
  if (prevNode) {
3271
3317
  currentState.data.sequence = currentState.data.sequence + 1;
3272
3318
  analytics.log({
3273
3319
  event: "FLOW_NODE",
3274
- attributionKey: `job_${job.id}`,
3275
3320
  properties: {
3276
3321
  flow_id: flow.id,
3277
3322
  flow_version: flow.version,
3278
- job_id: job.id,
3279
3323
  from_node_id: prevNode.id,
3280
3324
  to_node_id: node.id,
3281
3325
  sequence: currentState.data.sequence,
3282
3326
  flow_session_id: currentState.data.flowSessionId
3283
- }
3327
+ },
3328
+ customProperties: flow.context
3284
3329
  });
3285
3330
  }
3286
- application.setCurrentNodeId(node.id);
3331
+ store.setCurrentNodeId(node.id);
3287
3332
  },
3288
3333
  onFlowEnd: async () => {
3289
- application.markAsFinished();
3334
+ store.markAsFinished();
3290
3335
  }
3291
3336
  });
3337
+ setUndoFn(() => undo);
3292
3338
  interpret2(fromNodeId);
3293
3339
  return abort;
3294
- }, [analytics, apiClient, chatService, logger, scrollToEnd, flow, job.id, currentApplication.startedAt]);
3340
+ }, [analytics, apiClient, chatService, logger, flow]);
3295
3341
  return o$1(k, {
3296
3342
  children: [o$1("div", {
3297
3343
  ref: chatRef,
3298
- className: "hide-scrollbars relative flex max-w-full flex-grow flex-col overflow-y-scroll",
3344
+ className: "hide-scrollbars relative flex w-full max-w-full flex-grow flex-col overflow-y-scroll",
3299
3345
  style: {
3300
3346
  WebkitOverflowScrolling: "touch",
3301
- paddingBottom: inputHeight.value
3347
+ paddingBottom: store.inputHeight$.value
3302
3348
  },
3303
- children: o$1(AnimatePresence, {
3304
- children: o$1(Conversation, {
3305
- isBotTyping,
3306
- messages: currentApplication.data.messages
3349
+ children: o$1(Conversation, {
3350
+ lastSentMessageFooter: flow.data.isFinished || !undoFn ? null : o$1(UndoButton, {
3351
+ undoFn
3307
3352
  })
3308
3353
  })
3309
- }), o$1(ChatInput, {
3310
- input: currentApplication.data.currentInput,
3311
- onInputChange: () => scrollToEnd({
3312
- behavior: "smooth"
3313
- }),
3314
- onSubmit: onSubmitSuccessFn
3315
- })]
3354
+ }), o$1(ChatInput, {})]
3355
+ });
3356
+ };
3357
+ const getNodeKeys = (node) => {
3358
+ if ("key" in node.data && node.data.key)
3359
+ return [node.data.key];
3360
+ if ("keys" in node.data)
3361
+ return Object.values(node.data.keys).filter((key) => typeof key === "string");
3362
+ return [];
3363
+ };
3364
+ const UndoButton = ({
3365
+ undoFn
3366
+ }) => {
3367
+ return o$1("div", {
3368
+ class: "absolute bottom-0 right-0 flex w-full translate-y-full justify-end",
3369
+ children: o$1("button", {
3370
+ class: "fr touch-hitbox text-neutral-9 hover:text-neutral-12 rounded-full p-1 text-right text-xs transition-all",
3371
+ onClick: async () => {
3372
+ const {
3373
+ flow
3374
+ } = store.current$.peek();
3375
+ invariant(flow);
3376
+ const {
3377
+ removed
3378
+ } = undoFn(flow.data.nodeHistory);
3379
+ const removedNodeIds = flow.data.nodeHistory.splice(-removed);
3380
+ if (removedNodeIds.length === 0)
3381
+ return;
3382
+ store.removeMessagesSentByNodeIds(removedNodeIds);
3383
+ store.setInput(void 0);
3384
+ removedNodeIds.pop();
3385
+ removedNodeIds.map((nodeId) => flow.nodes.find((node) => node.id === nodeId)).filter(Boolean).flatMap(getNodeKeys).forEach((key) => delete flow.data.submissions[key]);
3386
+ store.current$.value = {
3387
+ ...store.current$.value
3388
+ };
3389
+ },
3390
+ children: "Undo"
3391
+ })
3316
3392
  });
3317
3393
  };
3318
3394
  export {
3319
- JobApplicationContent
3395
+ ChatbotBody
3320
3396
  };