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 +229 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.mjs +226 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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
|