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