formanitor 0.0.12 → 0.0.14

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/index.cjs CHANGED
@@ -16,6 +16,9 @@ var PopoverPrimitive = require('@radix-ui/react-popover');
16
16
  var reactTable = require('@tanstack/react-table');
17
17
  var RadioGroupPrimitive = require('@radix-ui/react-radio-group');
18
18
  var DialogPrimitive = require('@radix-ui/react-dialog');
19
+ var nlp = require('compromise');
20
+
21
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
22
 
20
23
  function _interopNamespace(e) {
21
24
  if (e && e.__esModule) return e;
@@ -42,6 +45,7 @@ var CheckboxPrimitive__namespace = /*#__PURE__*/_interopNamespace(CheckboxPrimit
42
45
  var PopoverPrimitive__namespace = /*#__PURE__*/_interopNamespace(PopoverPrimitive);
43
46
  var RadioGroupPrimitive__namespace = /*#__PURE__*/_interopNamespace(RadioGroupPrimitive);
44
47
  var DialogPrimitive__namespace = /*#__PURE__*/_interopNamespace(DialogPrimitive);
48
+ var nlp__default = /*#__PURE__*/_interopDefault(nlp);
45
49
 
46
50
  // src/core/validate.ts
47
51
  var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -54,6 +58,11 @@ function validateField(value, field) {
54
58
  if (Array.isArray(value) && value.length === 0) {
55
59
  return "This field is required";
56
60
  }
61
+ if (field.type === "smart_textarea" && typeof value === "object" && !Array.isArray(value)) {
62
+ if (!value.text || value.text.trim() === "") {
63
+ return "This field is required";
64
+ }
65
+ }
57
66
  }
58
67
  if (value === null || value === void 0 || value === "") {
59
68
  return null;
@@ -241,6 +250,7 @@ var FormStore = class {
241
250
  } else {
242
251
  if (field.type === "repeatable") values[field.id] = [];
243
252
  else if (field.type === "checkbox") values[field.id] = [];
253
+ else if (field.type === "smart_textarea") values[field.id] = { text: "", extractedKeywords: [] };
244
254
  else values[field.id] = null;
245
255
  }
246
256
  });
@@ -388,6 +398,15 @@ var FormStore = class {
388
398
  getSerializedValuesForSubmission() {
389
399
  return this.serializeValuesForSubmission(this.state.values);
390
400
  }
401
+ /**
402
+ * Raw form values as stored internally, without any serialization or
403
+ * transformation. Useful when consumers need access to richer structures
404
+ * (e.g. smart_textarea keyword metadata) alongside the flattened values
405
+ * returned by getSerializedValuesForSubmission.
406
+ */
407
+ getRawValues() {
408
+ return { ...this.state.values };
409
+ }
391
410
  serializeValuesForSubmission(values) {
392
411
  const result = { ...values };
393
412
  this.schema.fields.forEach((field) => {
@@ -395,6 +414,11 @@ var FormStore = class {
395
414
  delete result[field.id];
396
415
  return;
397
416
  }
417
+ if (field.type === "smart_textarea") {
418
+ const raw = values[field.id];
419
+ result[field.id] = raw?.text ?? null;
420
+ return;
421
+ }
398
422
  if (field.type === "image_upload" || field.type === "signature") {
399
423
  const raw = values[field.id];
400
424
  if (raw !== void 0 && raw !== null && raw !== "") {
@@ -685,6 +709,7 @@ function useForm() {
685
709
  availableTransitions: store.getAvailableTransitions(),
686
710
  hasPersistence: store.hasPersistence(),
687
711
  getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
712
+ getRawValues: store.getRawValues.bind(store),
688
713
  markSubmitAttempted: store.markSubmitAttempted.bind(store),
689
714
  flushPendingUploads: store.flushPendingUploads.bind(store)
690
715
  };
@@ -4019,6 +4044,206 @@ var FollowupWidget = ({ fieldId }) => {
4019
4044
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: error })
4020
4045
  ] }) });
4021
4046
  };
4047
+ function useSmartKeywords(text, onSearch, debounceDelay = 1e3) {
4048
+ console.log("[useSmartKeywords] render", {
4049
+ textLength: text?.length ?? 0,
4050
+ hasOnSearch: typeof onSearch === "function",
4051
+ debounceDelay
4052
+ });
4053
+ const [extractedNouns, setExtractedNouns] = React11.useState([]);
4054
+ const [matchedConditions, setMatchedConditions] = React11.useState([]);
4055
+ const [isProcessing, setIsProcessing] = React11.useState(false);
4056
+ const [error, setError] = React11.useState(null);
4057
+ const timerRef = React11.useRef(null);
4058
+ const abortRef = React11.useRef(0);
4059
+ const processText = React11.useCallback(
4060
+ async (input, runId) => {
4061
+ if (!input || input.trim().length === 0) {
4062
+ console.log("[useSmartKeywords] skip empty input", {
4063
+ runId,
4064
+ inputLength: input?.length ?? 0
4065
+ });
4066
+ setExtractedNouns([]);
4067
+ setMatchedConditions([]);
4068
+ setIsProcessing(false);
4069
+ return;
4070
+ }
4071
+ console.log("[useSmartKeywords] processText start", {
4072
+ runId,
4073
+ inputLength: input.length
4074
+ });
4075
+ setIsProcessing(true);
4076
+ setError(null);
4077
+ try {
4078
+ const nouns = extractNouns(input);
4079
+ const uniqueNouns = Array.from(new Set(nouns));
4080
+ console.log("[useSmartKeywords] extracted nouns", {
4081
+ runId,
4082
+ nouns,
4083
+ uniqueNouns
4084
+ });
4085
+ if (runId !== abortRef.current) return;
4086
+ setExtractedNouns(uniqueNouns);
4087
+ if (uniqueNouns.length === 0) {
4088
+ console.log("[useSmartKeywords] no nouns, skipping search", { runId });
4089
+ setMatchedConditions([]);
4090
+ setIsProcessing(false);
4091
+ return;
4092
+ }
4093
+ const searchResults = await Promise.all(
4094
+ uniqueNouns.map(async (noun) => {
4095
+ const normalised = noun.replace(/\s+/g, " ").replace(/^[\s-]+/, "").trim();
4096
+ try {
4097
+ console.log("[useSmartKeywords] onSearch call", { runId, noun, normalised });
4098
+ const results = await onSearch(normalised);
4099
+ console.log("[useSmartKeywords] onSearch result", {
4100
+ runId,
4101
+ noun,
4102
+ normalised,
4103
+ count: Array.isArray(results) ? results.length : 0
4104
+ });
4105
+ const first = Array.isArray(results) && results.length > 0 ? [results[0]] : [];
4106
+ return { keyword: normalised, matches: first };
4107
+ } catch (err) {
4108
+ console.error("[useSmartKeywords] onSearch error", { runId, noun, err });
4109
+ return { keyword: normalised || noun, matches: [] };
4110
+ }
4111
+ })
4112
+ );
4113
+ if (runId !== abortRef.current) return;
4114
+ const allMatches = searchResults.flatMap(
4115
+ ({ keyword, matches }) => matches.map((record) => ({
4116
+ extractedKeyword: keyword,
4117
+ matchedCondition: {
4118
+ id: record.id,
4119
+ name: record.name,
4120
+ created_at: record.created_at,
4121
+ updated_at: record.updated_at
4122
+ }
4123
+ }))
4124
+ );
4125
+ console.log("[useSmartKeywords] allMatches computed", {
4126
+ runId,
4127
+ matchesCount: allMatches.length
4128
+ });
4129
+ setMatchedConditions(allMatches);
4130
+ } catch (err) {
4131
+ if (runId !== abortRef.current) return;
4132
+ console.error("[useSmartKeywords] processText error", { runId, err });
4133
+ setError(err instanceof Error ? err.message : "Keyword extraction failed");
4134
+ } finally {
4135
+ if (runId === abortRef.current) {
4136
+ console.log("[useSmartKeywords] processText end", { runId });
4137
+ setIsProcessing(false);
4138
+ }
4139
+ }
4140
+ },
4141
+ [onSearch]
4142
+ );
4143
+ React11.useEffect(() => {
4144
+ if (timerRef.current) clearTimeout(timerRef.current);
4145
+ const runId = ++abortRef.current;
4146
+ console.log("[useSmartKeywords] schedule debounce", {
4147
+ runId,
4148
+ debounceDelay,
4149
+ textLength: text?.length ?? 0
4150
+ });
4151
+ timerRef.current = setTimeout(() => {
4152
+ console.log("[useSmartKeywords] debounce fired", { runId });
4153
+ processText(text, runId);
4154
+ }, debounceDelay);
4155
+ return () => {
4156
+ if (timerRef.current) clearTimeout(timerRef.current);
4157
+ };
4158
+ }, [text, debounceDelay, processText]);
4159
+ return { extractedNouns, matchedConditions, isProcessing, error };
4160
+ }
4161
+ function extractNouns(text) {
4162
+ const doc = nlp__default.default(text);
4163
+ const rawNouns = doc.nouns().out("array");
4164
+ const cleaned = rawNouns.map((n) => n.replace(/[^\w\s-]/g, "").trim().toLowerCase()).filter((n) => n.length > 2);
4165
+ console.log("[useSmartKeywords] extractNouns", {
4166
+ inputLength: text?.length ?? 0,
4167
+ rawNouns,
4168
+ cleaned
4169
+ });
4170
+ return cleaned;
4171
+ }
4172
+ var SmartTextareaWidget = ({ fieldId }) => {
4173
+ const { fieldDef, value, setValue, setTouched, error, disabled, touched } = useField(fieldId);
4174
+ const { state } = useForm();
4175
+ const handler = useFieldHandlers(fieldId);
4176
+ const def = fieldDef;
4177
+ const structured = value;
4178
+ const textValue = structured?.text ?? "";
4179
+ const showError = !!error && (touched || state.submitAttempted);
4180
+ const onSearch = React11.useCallback(
4181
+ (query) => handler.onSearch(query),
4182
+ [handler]
4183
+ );
4184
+ const { matchedConditions, isProcessing } = useSmartKeywords(
4185
+ textValue,
4186
+ onSearch,
4187
+ def?.debounceDelay ?? 1e3
4188
+ );
4189
+ const currentKeywords = structured?.extractedKeywords ?? [];
4190
+ const nextKeywords = React11.useMemo(() => {
4191
+ if (matchedConditions.length === 0 && currentKeywords.length === 0) return currentKeywords;
4192
+ if (matchedConditions.length === currentKeywords.length && matchedConditions.every(
4193
+ (m, i) => m.matchedCondition.id === currentKeywords[i]?.matchedCondition.id && m.extractedKeyword === currentKeywords[i]?.extractedKeyword
4194
+ )) {
4195
+ return currentKeywords;
4196
+ }
4197
+ return matchedConditions;
4198
+ }, [matchedConditions, currentKeywords]);
4199
+ const writeValue = React11.useCallback(
4200
+ (text, keywords) => {
4201
+ const next = { text, extractedKeywords: keywords };
4202
+ setValue(next);
4203
+ },
4204
+ [setValue]
4205
+ );
4206
+ React11__namespace.default.useEffect(() => {
4207
+ if (nextKeywords !== currentKeywords) {
4208
+ writeValue(textValue, nextKeywords);
4209
+ }
4210
+ }, [nextKeywords]);
4211
+ const handleTextChange = React11.useCallback(
4212
+ (e) => {
4213
+ writeValue(e.target.value, currentKeywords);
4214
+ },
4215
+ [writeValue, currentKeywords]
4216
+ );
4217
+ if (!def) return null;
4218
+ const height = def.height;
4219
+ const theme = getThemeConfig("textarea", def.theme);
4220
+ const wrapperClass = theme?.wrapperClassName ?? "space-y-2";
4221
+ const labelClass = theme?.labelClassName;
4222
+ const inputClass = cn(theme?.inputClassName, showError && "border-red-500");
4223
+ const labelContent = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4224
+ def.label,
4225
+ " ",
4226
+ def.required && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-red-500", children: "*" }),
4227
+ isProcessing && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "inline-block ml-1.5 h-3 w-3 animate-spin text-muted-foreground" })
4228
+ ] });
4229
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: wrapperClass, children: [
4230
+ theme ? /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: fieldId, className: labelClass, children: labelContent }) : /* @__PURE__ */ jsxRuntime.jsx(Label, { htmlFor: fieldId, children: labelContent }),
4231
+ /* @__PURE__ */ jsxRuntime.jsx(
4232
+ Textarea,
4233
+ {
4234
+ id: fieldId,
4235
+ value: textValue,
4236
+ onChange: handleTextChange,
4237
+ onBlur: setTouched,
4238
+ disabled,
4239
+ placeholder: def.placeholder,
4240
+ className: inputClass,
4241
+ ...height != null ? { height } : {}
4242
+ }
4243
+ ),
4244
+ showError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-red-500", children: error })
4245
+ ] });
4246
+ };
4022
4247
  var FieldRenderer = ({ fieldId }) => {
4023
4248
  const { fieldDef, visible } = useField(fieldId);
4024
4249
  if (!visible || !fieldDef) {
@@ -4065,6 +4290,8 @@ var FieldRenderer = ({ fieldId }) => {
4065
4290
  return /* @__PURE__ */ jsxRuntime.jsx(ReferralWidget, { fieldId });
4066
4291
  case "followup":
4067
4292
  return /* @__PURE__ */ jsxRuntime.jsx(FollowupWidget, { fieldId });
4293
+ case "smart_textarea":
4294
+ return /* @__PURE__ */ jsxRuntime.jsx(SmartTextareaWidget, { fieldId });
4068
4295
  case "static_text": {
4069
4296
  const def = fieldDef;
4070
4297
  const size = def.size ?? "regular";
@@ -4829,12 +5056,14 @@ exports.ReadOnlySignature = ReadOnlySignature;
4829
5056
  exports.ReadOnlyTable = ReadOnlyTable;
4830
5057
  exports.SignatureUploadWidget = SignatureUploadWidget;
4831
5058
  exports.SmartForm = SmartForm;
5059
+ exports.SmartTextareaWidget = SmartTextareaWidget;
4832
5060
  exports.createUploadHandler = createUploadHandler;
4833
5061
  exports.evaluateRules = evaluateRules;
4834
5062
  exports.useField = useField;
4835
5063
  exports.useFieldHandlers = useFieldHandlers;
4836
5064
  exports.useForm = useForm;
4837
5065
  exports.useFormStore = useFormStore;
5066
+ exports.useSmartKeywords = useSmartKeywords;
4838
5067
  exports.validateField = validateField;
4839
5068
  exports.validateForm = validateForm;
4840
5069
  //# sourceMappingURL=index.cjs.map