@webless/agent 0.7.2 → 0.7.3

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/embed.cjs CHANGED
@@ -3804,42 +3804,72 @@ function emptyValues(form) {
3804
3804
  for (const field of form?.fields ?? []) values[field.id] = "";
3805
3805
  return values;
3806
3806
  }
3807
+ function createDraft(form) {
3808
+ return {
3809
+ form,
3810
+ values: emptyValues(form),
3811
+ expanded: Boolean(form),
3812
+ submitted: false,
3813
+ dismissalSent: false,
3814
+ attempted: false
3815
+ };
3816
+ }
3807
3817
  function Composer({
3808
3818
  disabled = false,
3809
3819
  placeholder = "Ask anything\u2026",
3810
3820
  variant = "default",
3811
3821
  form = null,
3822
+ allowFormResume = true,
3812
3823
  onSubmit
3813
3824
  }) {
3814
3825
  const [value, setValue] = (0, import_react8.useState)("");
3815
- const [values, setValues] = (0, import_react8.useState)(
3816
- () => emptyValues(form)
3817
- );
3818
- const [blurred, setBlurred] = (0, import_react8.useState)({});
3826
+ const [draft, setDraft] = (0, import_react8.useState)(() => createDraft(form));
3819
3827
  const inputRef = (0, import_react8.useRef)(null);
3820
3828
  const firstFieldRef = (0, import_react8.useRef)(null);
3829
+ const formRef = (0, import_react8.useRef)(null);
3821
3830
  const formId = (0, import_react8.useId)();
3822
- const activeForm = form;
3823
- const canSendForm = activeForm ? isComposerFormComplete(activeForm, values) : Boolean(value.trim());
3824
- (0, import_react8.useEffect)(() => {
3825
- setValues(emptyValues(form));
3826
- setBlurred({});
3827
- }, [form?.id]);
3831
+ if (form && form.id !== draft.form?.id) setDraft(createDraft(form));
3832
+ const savedForm = allowFormResume && !draft.submitted ? draft.form : null;
3833
+ const activeForm = !disabled && draft.expanded ? savedForm : null;
3834
+ const hasDraft = Object.values(draft.values).some((value2) => value2.trim());
3828
3835
  (0, import_react8.useEffect)(() => {
3829
3836
  if (activeForm) firstFieldRef.current?.focus();
3830
- }, [activeForm?.id]);
3837
+ else if ((savedForm || draft.submitted) && !disabled)
3838
+ inputRef.current?.focus();
3839
+ }, [activeForm?.id, disabled, savedForm?.id, draft.submitted]);
3831
3840
  function submitChat() {
3832
3841
  const trimmed = value.trim();
3833
3842
  if (!trimmed || disabled) return;
3834
- onSubmit?.(trimmed);
3843
+ const shareDismissal = savedForm && !draft.dismissalSent;
3844
+ onSubmit?.(
3845
+ shareDismissal ? `I'd like to keep chatting without sharing the requested details for now. Please don't ask for them again unless I choose to proceed with something that needs them.
3846
+
3847
+ ${trimmed}` : trimmed
3848
+ );
3849
+ if (shareDismissal)
3850
+ setDraft((current) => ({ ...current, dismissalSent: true }));
3835
3851
  setValue("");
3836
3852
  inputRef.current?.focus();
3837
3853
  }
3838
3854
  function submitForm() {
3839
- if (!activeForm || disabled || !canSendForm) return;
3840
- onSubmit?.(formatComposerFormMessage(activeForm, values));
3841
- setValues(emptyValues(activeForm));
3842
- setBlurred({});
3855
+ if (!activeForm || disabled) return;
3856
+ if (!isComposerFormComplete(activeForm, draft.values)) {
3857
+ setDraft((current) => ({ ...current, attempted: true }));
3858
+ const invalidField = activeForm.fields.find(
3859
+ (field) => !isValidComposerFieldValue(field, draft.values[field.id] ?? "")
3860
+ );
3861
+ const control = invalidField && formRef.current?.elements.namedItem(invalidField.id);
3862
+ if (control instanceof HTMLElement) control.focus();
3863
+ return;
3864
+ }
3865
+ onSubmit?.(formatComposerFormMessage(activeForm, draft.values));
3866
+ setDraft((current) => ({
3867
+ ...current,
3868
+ values: emptyValues(activeForm),
3869
+ expanded: false,
3870
+ submitted: true,
3871
+ attempted: false
3872
+ }));
3843
3873
  }
3844
3874
  function handleSubmit(event) {
3845
3875
  event.preventDefault();
@@ -3863,112 +3893,151 @@ function Composer({
3863
3893
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3864
3894
  "form",
3865
3895
  {
3896
+ ref: formRef,
3897
+ noValidate: true,
3866
3898
  className: [
3867
3899
  "composer",
3868
3900
  variant === "dock" ? "composer--dock" : "",
3869
3901
  activeForm ? "composer--form" : ""
3870
3902
  ].filter(Boolean).join(" "),
3871
3903
  onSubmit: handleSubmit,
3872
- children: activeForm ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__sheet", role: "group", "aria-label": "Required details", children: [
3873
- activeForm.fields.map((field, index) => {
3874
- const fieldId = `${formId}-${field.id}`;
3875
- const invalid = Boolean(blurred[field.id]) && !isValidComposerFieldValue(field, values[field.id] ?? "");
3876
- const controlProps = {
3877
- id: fieldId,
3878
- name: field.id,
3879
- disabled,
3880
- required: field.required,
3881
- autoComplete: field.autocomplete,
3882
- placeholder: field.placeholder,
3883
- spellCheck: false,
3884
- value: values[field.id] ?? "",
3885
- "aria-invalid": invalid || void 0,
3886
- "aria-describedby": invalid ? `${fieldId}-error` : void 0,
3887
- onBlur: () => setBlurred((current) => ({ ...current, [field.id]: true })),
3888
- onChange: (event) => {
3889
- const next = readComposerControlValue(event);
3890
- setValues((current) => ({
3891
- ...current,
3892
- [field.id]: next
3893
- }));
3894
- },
3895
- onKeyDown: handleFormKeyDown
3896
- };
3897
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3898
- "div",
3904
+ children: activeForm ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3905
+ "div",
3906
+ {
3907
+ className: "composer__sheet",
3908
+ role: "group",
3909
+ "aria-label": "Contact details",
3910
+ children: [
3911
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__form-heading", children: [
3912
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Share your details" }),
3913
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Optional" }),
3914
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: "You can also keep chatting." })
3915
+ ] }),
3916
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "composer__fields", children: activeForm.fields.map((field, index) => {
3917
+ const fieldId = `${formId}-${field.id}`;
3918
+ const invalid = draft.attempted && !isValidComposerFieldValue(field, draft.values[field.id] ?? "");
3919
+ const controlProps = {
3920
+ id: fieldId,
3921
+ name: field.id,
3922
+ disabled,
3923
+ required: field.required,
3924
+ autoComplete: field.autocomplete,
3925
+ placeholder: field.placeholder,
3926
+ spellCheck: false,
3927
+ value: draft.values[field.id] ?? "",
3928
+ "aria-invalid": invalid || void 0,
3929
+ "aria-describedby": invalid ? `${fieldId}-error` : void 0,
3930
+ onChange: (event) => {
3931
+ const next = readComposerControlValue(event);
3932
+ setDraft((current) => ({
3933
+ ...current,
3934
+ values: { ...current.values, [field.id]: next }
3935
+ }));
3936
+ },
3937
+ onKeyDown: handleFormKeyDown
3938
+ };
3939
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
3940
+ "div",
3941
+ {
3942
+ className: [
3943
+ "composer__row",
3944
+ field.kind === "textarea" ? "composer__row--grow" : ""
3945
+ ].filter(Boolean).join(" "),
3946
+ children: [
3947
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "composer__label", htmlFor: fieldId, children: [
3948
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "composer__field-label", children: field.label }),
3949
+ field.kind === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3950
+ "textarea",
3951
+ {
3952
+ ...controlProps,
3953
+ ref: index === 0 ? (node) => {
3954
+ firstFieldRef.current = node;
3955
+ } : void 0,
3956
+ className: "composer__control composer__control--area",
3957
+ rows: 3
3958
+ }
3959
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3960
+ "input",
3961
+ {
3962
+ ...controlProps,
3963
+ ref: index === 0 ? (node) => {
3964
+ firstFieldRef.current = node;
3965
+ } : void 0,
3966
+ className: "composer__control",
3967
+ type: field.kind,
3968
+ inputMode: field.kind === "tel" ? "tel" : void 0
3969
+ }
3970
+ )
3971
+ ] }),
3972
+ invalid ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "composer__error", id: `${fieldId}-error`, children: field.kind === "email" ? "Enter a valid email to share your details." : `Add your ${field.label.toLowerCase()} to share your details.` }) : null
3973
+ ]
3974
+ },
3975
+ field.id
3976
+ );
3977
+ }) }),
3978
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__toolbar", children: [
3979
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3980
+ "button",
3981
+ {
3982
+ type: "submit",
3983
+ className: "composer__action composer__action--primary",
3984
+ disabled,
3985
+ children: "Share details"
3986
+ }
3987
+ ),
3988
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3989
+ "button",
3990
+ {
3991
+ type: "button",
3992
+ className: "composer__action",
3993
+ disabled,
3994
+ onClick: () => setDraft((current) => ({ ...current, expanded: false })),
3995
+ children: "Keep chatting"
3996
+ }
3997
+ )
3998
+ ] })
3999
+ ]
4000
+ }
4001
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
4002
+ savedForm ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__resume", children: [
4003
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: hasDraft ? "Your details aren\u2019t shared yet" : "Contact details are optional" }),
4004
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4005
+ "button",
3899
4006
  {
3900
- className: [
3901
- "composer__row",
3902
- field.kind === "textarea" ? "composer__row--grow" : ""
3903
- ].filter(Boolean).join(" "),
3904
- children: [
3905
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "composer__label", htmlFor: fieldId, children: [
3906
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "composer__sr-only", children: field.label }),
3907
- field.kind === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3908
- "textarea",
3909
- {
3910
- ...controlProps,
3911
- ref: index === 0 ? (node) => {
3912
- firstFieldRef.current = node;
3913
- } : void 0,
3914
- className: "composer__control composer__control--area",
3915
- rows: 3
3916
- }
3917
- ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3918
- "input",
3919
- {
3920
- ...controlProps,
3921
- ref: index === 0 ? (node) => {
3922
- firstFieldRef.current = node;
3923
- } : void 0,
3924
- className: "composer__control",
3925
- type: field.kind,
3926
- inputMode: field.kind === "tel" ? "tel" : void 0
3927
- }
3928
- )
3929
- ] }),
3930
- invalid ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "composer__error", id: `${fieldId}-error`, children: field.kind === "email" ? "Enter a valid email to continue." : `Add your ${field.label.toLowerCase()} to continue.` }) : null
3931
- ]
3932
- },
3933
- field.id
3934
- );
3935
- }),
3936
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "composer__toolbar", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3937
- "button",
3938
- {
3939
- type: "submit",
3940
- className: "composer__send",
3941
- disabled: disabled || !canSendForm,
3942
- "aria-label": "Send details",
3943
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SendIcon, {})
3944
- }
3945
- ) })
3946
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__field", children: [
3947
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3948
- "textarea",
3949
- {
3950
- ref: inputRef,
3951
- className: "composer__input",
3952
- rows: 1,
3953
- value,
3954
- placeholder,
3955
- disabled,
3956
- spellCheck: false,
3957
- "aria-label": "Message",
3958
- onChange: (event) => setValue(event.target.value),
3959
- onKeyDown: handleChatKeyDown
3960
- }
3961
- ),
3962
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
3963
- "button",
3964
- {
3965
- type: "submit",
3966
- className: "composer__send",
3967
- disabled: disabled || !value.trim(),
3968
- "aria-label": "Send message",
3969
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SendIcon, {})
3970
- }
3971
- )
4007
+ type: "button",
4008
+ disabled,
4009
+ onClick: () => setDraft((current) => ({ ...current, expanded: true })),
4010
+ children: hasDraft ? "Resume details" : "Add details"
4011
+ }
4012
+ )
4013
+ ] }) : null,
4014
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "composer__field", children: [
4015
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4016
+ "textarea",
4017
+ {
4018
+ ref: inputRef,
4019
+ className: "composer__input",
4020
+ rows: 1,
4021
+ value,
4022
+ placeholder,
4023
+ disabled,
4024
+ spellCheck: false,
4025
+ "aria-label": "Message",
4026
+ onChange: (event) => setValue(event.target.value),
4027
+ onKeyDown: handleChatKeyDown
4028
+ }
4029
+ ),
4030
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
4031
+ "button",
4032
+ {
4033
+ type: "submit",
4034
+ className: "composer__send",
4035
+ disabled: disabled || !value.trim(),
4036
+ "aria-label": "Send message",
4037
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SendIcon, {})
4038
+ }
4039
+ )
4040
+ ] })
3972
4041
  ] })
3973
4042
  }
3974
4043
  );
@@ -6339,10 +6408,12 @@ function AgentRail({
6339
6408
  {
6340
6409
  variant: expanded || mobileFullscreen ? "dock" : "default",
6341
6410
  disabled: isBusy,
6342
- form: composerForm,
6411
+ form: composerForm && lastMessage ? { ...composerForm, id: `${lastMessage.id}:${composerForm.id}` } : null,
6412
+ allowFormResume: !state.pendingOffer && !waitingForBooking && !hasPendingConfirmation,
6343
6413
  placeholder: composerPlaceholder,
6344
6414
  onSubmit: handleSubmit
6345
- }
6415
+ },
6416
+ state.messages.find((message) => message.role === "visitor")?.id ?? "empty"
6346
6417
  ),
6347
6418
  poweredByLabel ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "agent-rail__footer", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: poweredByLabel }) }) }) : null
6348
6419
  ] })