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.d.cts
CHANGED
|
@@ -186,7 +186,38 @@ interface FollowupValue {
|
|
|
186
186
|
interface FollowupFieldDef extends BaseFieldDef {
|
|
187
187
|
type: 'followup';
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
/** A matched master record returned from a backend search API. */
|
|
190
|
+
interface MatchedCondition {
|
|
191
|
+
id: number | string;
|
|
192
|
+
name: string;
|
|
193
|
+
created_at?: string;
|
|
194
|
+
updated_at?: string;
|
|
195
|
+
}
|
|
196
|
+
/** A single keyword-to-master-record match produced by NLP extraction + search. */
|
|
197
|
+
interface ExtractedKeywordMatch {
|
|
198
|
+
extractedKeyword: string;
|
|
199
|
+
matchedCondition: MatchedCondition;
|
|
200
|
+
}
|
|
201
|
+
/** Structured value stored in form state for smart_textarea fields. */
|
|
202
|
+
interface SmartTextareaValue {
|
|
203
|
+
text: string;
|
|
204
|
+
extractedKeywords: ExtractedKeywordMatch[];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Smart textarea that performs client-side NLP keyword extraction and searches
|
|
208
|
+
* matched terms against backend master tables via `fieldHandlers[fieldId].onSearch`.
|
|
209
|
+
*
|
|
210
|
+
* Value shape: `SmartTextareaValue` — `{ text, extractedKeywords }`.
|
|
211
|
+
*/
|
|
212
|
+
interface SmartTextareaFieldDef extends BaseFieldDef {
|
|
213
|
+
type: 'smart_textarea';
|
|
214
|
+
height?: number;
|
|
215
|
+
/** Semantic label for the kind of content (e.g. 'allergies', 'chiefComplaints', 'symptoms'). */
|
|
216
|
+
componentType: string;
|
|
217
|
+
/** Debounce delay in ms before NLP + search triggers. Default: 1000. */
|
|
218
|
+
debounceDelay?: number;
|
|
219
|
+
}
|
|
220
|
+
type FieldDef = TextFieldDef | NumberFieldDef | SelectFieldDef | RadioFieldDef | CheckboxFieldDef | RepeatableFieldDef | ImageUploadFieldDef | SignatureFieldDef | EditableTableFieldDef | StaticTextFieldDef | MedicationsFieldDef | InvestigationsFieldDef | ProceduresFieldDef | DifferentialDiagnosisFieldDef | VitalsFieldDef | ReferralFieldDef | FollowupFieldDef | SmartTextareaFieldDef | BaseFieldDef;
|
|
190
221
|
interface Condition {
|
|
191
222
|
field?: string;
|
|
192
223
|
op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'in' | 'contains';
|
|
@@ -392,6 +423,13 @@ declare class FormStore {
|
|
|
392
423
|
* }
|
|
393
424
|
*/
|
|
394
425
|
getSerializedValuesForSubmission(): FormValues;
|
|
426
|
+
/**
|
|
427
|
+
* Raw form values as stored internally, without any serialization or
|
|
428
|
+
* transformation. Useful when consumers need access to richer structures
|
|
429
|
+
* (e.g. smart_textarea keyword metadata) alongside the flattened values
|
|
430
|
+
* returned by getSerializedValuesForSubmission.
|
|
431
|
+
*/
|
|
432
|
+
getRawValues(): FormValues;
|
|
395
433
|
private serializeValuesForSubmission;
|
|
396
434
|
private evaluate;
|
|
397
435
|
getAvailableTransitions(): WorkflowTransition[];
|
|
@@ -472,6 +510,7 @@ declare function useForm(): {
|
|
|
472
510
|
availableTransitions: WorkflowTransition[];
|
|
473
511
|
hasPersistence: boolean;
|
|
474
512
|
getSerializedValues: () => FormValues;
|
|
513
|
+
getRawValues: () => FormValues;
|
|
475
514
|
markSubmitAttempted: () => void;
|
|
476
515
|
flushPendingUploads: () => Promise<void>;
|
|
477
516
|
};
|
|
@@ -704,4 +743,23 @@ type DifferentialDiagnosisProps = {
|
|
|
704
743
|
*/
|
|
705
744
|
declare const DifferentialDiagnosis: React.FC<DifferentialDiagnosisProps>;
|
|
706
745
|
|
|
707
|
-
|
|
746
|
+
interface SmartKeywordsResult {
|
|
747
|
+
extractedNouns: string[];
|
|
748
|
+
matchedConditions: ExtractedKeywordMatch[];
|
|
749
|
+
isProcessing: boolean;
|
|
750
|
+
error: string | null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Extracts noun phrases from `text` using compromise, deduplicates, then
|
|
754
|
+
* searches each noun against a backend master table via `onSearch`. Returns
|
|
755
|
+
* the flattened list of keyword-to-master-record matches.
|
|
756
|
+
*
|
|
757
|
+
* All NLP runs client-side; the backend only receives simple query strings.
|
|
758
|
+
*/
|
|
759
|
+
declare function useSmartKeywords(text: string, onSearch: (query: string) => Promise<any[]>, debounceDelay?: number): SmartKeywordsResult;
|
|
760
|
+
|
|
761
|
+
declare const SmartTextareaWidget: React.FC<{
|
|
762
|
+
fieldId: string;
|
|
763
|
+
}>;
|
|
764
|
+
|
|
765
|
+
export { type AIDifferentialDiagnosisItem, AddInvestigationField, type AddInvestigationFieldProps, AddMedicationField, type AddMedicationFieldProps, type BaseFieldDef, type CheckboxFieldDef, type ColumnLayout, type Computation, type Condition, type CreateUploadHandlerOptions, type DataSource, DifferentialDiagnosis, type DifferentialDiagnosisFieldDef, type DifferentialDiagnosisItem, type DifferentialDiagnosisProps, DynamicForm, EMAIL_REGEX, type EditableTableColumnDef, type EditableTableFieldDef, type EventDefinition, type ExtractedKeywordMatch, type FieldDef, type FieldHandler, type FieldHandlersMap, type FieldPrefill, FieldRenderer, type FieldState, type FieldType, type FollowupFieldDef, type FollowupValue, FormControls, type FormControlsProps, type FormDef, type FormErrors, FormProvider, type FormState, FormStore, type FormValues, type FormulaComputation, type FormulaExpr, type ImageUploadFieldDef, ImageUploadWidget, type Investigation, type InvestigationSearchResult, type InvestigationsFieldDef, type LayoutNode, type MatchedCondition, type Medication, type MedicationSearchResult, type MedicationsFieldDef, type NumberFieldDef, type Permission, type PresignedUploadResponse, type ProceduresFieldDef, type RadioFieldDef, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlySignature, ReadOnlyTable, type ReferralFieldDef, type ReferralItem, type RepeatableFieldDef, type Rule, type RuleAction, type RuleResult, type ScoreComputation, type ScoreRange, type Section, type SectionChild, type SelectFieldDef, type SelectOption, type SignatureFieldDef, SignatureUploadWidget, SmartForm, type SmartFormProps, type SmartKeywordsResult, type SmartTextareaFieldDef, type SmartTextareaValue, SmartTextareaWidget, type StaticTextFieldDef, type StoreConfig, type SubmissionData, type SumComputation, type TextFieldDef, type ValidationRule, type VitalsFieldDef, type WebhookDef, type Workflow, type WorkflowTransition, createUploadHandler, evaluateRules, useField, useFieldHandlers, useForm, useFormStore, useSmartKeywords, validateField, validateForm };
|
package/dist/index.d.ts
CHANGED
|
@@ -186,7 +186,38 @@ interface FollowupValue {
|
|
|
186
186
|
interface FollowupFieldDef extends BaseFieldDef {
|
|
187
187
|
type: 'followup';
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
/** A matched master record returned from a backend search API. */
|
|
190
|
+
interface MatchedCondition {
|
|
191
|
+
id: number | string;
|
|
192
|
+
name: string;
|
|
193
|
+
created_at?: string;
|
|
194
|
+
updated_at?: string;
|
|
195
|
+
}
|
|
196
|
+
/** A single keyword-to-master-record match produced by NLP extraction + search. */
|
|
197
|
+
interface ExtractedKeywordMatch {
|
|
198
|
+
extractedKeyword: string;
|
|
199
|
+
matchedCondition: MatchedCondition;
|
|
200
|
+
}
|
|
201
|
+
/** Structured value stored in form state for smart_textarea fields. */
|
|
202
|
+
interface SmartTextareaValue {
|
|
203
|
+
text: string;
|
|
204
|
+
extractedKeywords: ExtractedKeywordMatch[];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Smart textarea that performs client-side NLP keyword extraction and searches
|
|
208
|
+
* matched terms against backend master tables via `fieldHandlers[fieldId].onSearch`.
|
|
209
|
+
*
|
|
210
|
+
* Value shape: `SmartTextareaValue` — `{ text, extractedKeywords }`.
|
|
211
|
+
*/
|
|
212
|
+
interface SmartTextareaFieldDef extends BaseFieldDef {
|
|
213
|
+
type: 'smart_textarea';
|
|
214
|
+
height?: number;
|
|
215
|
+
/** Semantic label for the kind of content (e.g. 'allergies', 'chiefComplaints', 'symptoms'). */
|
|
216
|
+
componentType: string;
|
|
217
|
+
/** Debounce delay in ms before NLP + search triggers. Default: 1000. */
|
|
218
|
+
debounceDelay?: number;
|
|
219
|
+
}
|
|
220
|
+
type FieldDef = TextFieldDef | NumberFieldDef | SelectFieldDef | RadioFieldDef | CheckboxFieldDef | RepeatableFieldDef | ImageUploadFieldDef | SignatureFieldDef | EditableTableFieldDef | StaticTextFieldDef | MedicationsFieldDef | InvestigationsFieldDef | ProceduresFieldDef | DifferentialDiagnosisFieldDef | VitalsFieldDef | ReferralFieldDef | FollowupFieldDef | SmartTextareaFieldDef | BaseFieldDef;
|
|
190
221
|
interface Condition {
|
|
191
222
|
field?: string;
|
|
192
223
|
op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'in' | 'contains';
|
|
@@ -392,6 +423,13 @@ declare class FormStore {
|
|
|
392
423
|
* }
|
|
393
424
|
*/
|
|
394
425
|
getSerializedValuesForSubmission(): FormValues;
|
|
426
|
+
/**
|
|
427
|
+
* Raw form values as stored internally, without any serialization or
|
|
428
|
+
* transformation. Useful when consumers need access to richer structures
|
|
429
|
+
* (e.g. smart_textarea keyword metadata) alongside the flattened values
|
|
430
|
+
* returned by getSerializedValuesForSubmission.
|
|
431
|
+
*/
|
|
432
|
+
getRawValues(): FormValues;
|
|
395
433
|
private serializeValuesForSubmission;
|
|
396
434
|
private evaluate;
|
|
397
435
|
getAvailableTransitions(): WorkflowTransition[];
|
|
@@ -472,6 +510,7 @@ declare function useForm(): {
|
|
|
472
510
|
availableTransitions: WorkflowTransition[];
|
|
473
511
|
hasPersistence: boolean;
|
|
474
512
|
getSerializedValues: () => FormValues;
|
|
513
|
+
getRawValues: () => FormValues;
|
|
475
514
|
markSubmitAttempted: () => void;
|
|
476
515
|
flushPendingUploads: () => Promise<void>;
|
|
477
516
|
};
|
|
@@ -704,4 +743,23 @@ type DifferentialDiagnosisProps = {
|
|
|
704
743
|
*/
|
|
705
744
|
declare const DifferentialDiagnosis: React.FC<DifferentialDiagnosisProps>;
|
|
706
745
|
|
|
707
|
-
|
|
746
|
+
interface SmartKeywordsResult {
|
|
747
|
+
extractedNouns: string[];
|
|
748
|
+
matchedConditions: ExtractedKeywordMatch[];
|
|
749
|
+
isProcessing: boolean;
|
|
750
|
+
error: string | null;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Extracts noun phrases from `text` using compromise, deduplicates, then
|
|
754
|
+
* searches each noun against a backend master table via `onSearch`. Returns
|
|
755
|
+
* the flattened list of keyword-to-master-record matches.
|
|
756
|
+
*
|
|
757
|
+
* All NLP runs client-side; the backend only receives simple query strings.
|
|
758
|
+
*/
|
|
759
|
+
declare function useSmartKeywords(text: string, onSearch: (query: string) => Promise<any[]>, debounceDelay?: number): SmartKeywordsResult;
|
|
760
|
+
|
|
761
|
+
declare const SmartTextareaWidget: React.FC<{
|
|
762
|
+
fieldId: string;
|
|
763
|
+
}>;
|
|
764
|
+
|
|
765
|
+
export { type AIDifferentialDiagnosisItem, AddInvestigationField, type AddInvestigationFieldProps, AddMedicationField, type AddMedicationFieldProps, type BaseFieldDef, type CheckboxFieldDef, type ColumnLayout, type Computation, type Condition, type CreateUploadHandlerOptions, type DataSource, DifferentialDiagnosis, type DifferentialDiagnosisFieldDef, type DifferentialDiagnosisItem, type DifferentialDiagnosisProps, DynamicForm, EMAIL_REGEX, type EditableTableColumnDef, type EditableTableFieldDef, type EventDefinition, type ExtractedKeywordMatch, type FieldDef, type FieldHandler, type FieldHandlersMap, type FieldPrefill, FieldRenderer, type FieldState, type FieldType, type FollowupFieldDef, type FollowupValue, FormControls, type FormControlsProps, type FormDef, type FormErrors, FormProvider, type FormState, FormStore, type FormValues, type FormulaComputation, type FormulaExpr, type ImageUploadFieldDef, ImageUploadWidget, type Investigation, type InvestigationSearchResult, type InvestigationsFieldDef, type LayoutNode, type MatchedCondition, type Medication, type MedicationSearchResult, type MedicationsFieldDef, type NumberFieldDef, type Permission, type PresignedUploadResponse, type ProceduresFieldDef, type RadioFieldDef, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlySignature, ReadOnlyTable, type ReferralFieldDef, type ReferralItem, type RepeatableFieldDef, type Rule, type RuleAction, type RuleResult, type ScoreComputation, type ScoreRange, type Section, type SectionChild, type SelectFieldDef, type SelectOption, type SignatureFieldDef, SignatureUploadWidget, SmartForm, type SmartFormProps, type SmartKeywordsResult, type SmartTextareaFieldDef, type SmartTextareaValue, SmartTextareaWidget, type StaticTextFieldDef, type StoreConfig, type SubmissionData, type SumComputation, type TextFieldDef, type ValidationRule, type VitalsFieldDef, type WebhookDef, type Workflow, type WorkflowTransition, createUploadHandler, evaluateRules, useField, useFieldHandlers, useForm, useFormStore, useSmartKeywords, validateField, validateForm };
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { twMerge } from 'tailwind-merge';
|
|
|
6
6
|
import * as LabelPrimitive from '@radix-ui/react-label';
|
|
7
7
|
import { cva } from 'class-variance-authority';
|
|
8
8
|
import * as SelectPrimitive from '@radix-ui/react-select';
|
|
9
|
-
import { ChevronDown, ChevronUp, Check, Circle, X, Image, Upload, Trash2, Plus, PenTool, ChevronRight, Calendar as Calendar$1, ChevronLeft, RefreshCw, Clock } from 'lucide-react';
|
|
9
|
+
import { ChevronDown, ChevronUp, Check, Circle, X, Image, Upload, Trash2, Plus, Loader2, PenTool, ChevronRight, Calendar as Calendar$1, ChevronLeft, RefreshCw, Clock } from 'lucide-react';
|
|
10
10
|
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
11
11
|
import { Slot } from '@radix-ui/react-slot';
|
|
12
12
|
import { format, addDays, addWeeks, addMonths, differenceInCalendarDays, parseISO } from 'date-fns';
|
|
@@ -15,6 +15,7 @@ import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
|
15
15
|
import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
|
|
16
16
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
|
17
17
|
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
18
|
+
import nlp from 'compromise';
|
|
18
19
|
|
|
19
20
|
// src/core/validate.ts
|
|
20
21
|
var EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -27,6 +28,11 @@ function validateField(value, field) {
|
|
|
27
28
|
if (Array.isArray(value) && value.length === 0) {
|
|
28
29
|
return "This field is required";
|
|
29
30
|
}
|
|
31
|
+
if (field.type === "smart_textarea" && typeof value === "object" && !Array.isArray(value)) {
|
|
32
|
+
if (!value.text || value.text.trim() === "") {
|
|
33
|
+
return "This field is required";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
30
36
|
}
|
|
31
37
|
if (value === null || value === void 0 || value === "") {
|
|
32
38
|
return null;
|
|
@@ -214,6 +220,7 @@ var FormStore = class {
|
|
|
214
220
|
} else {
|
|
215
221
|
if (field.type === "repeatable") values[field.id] = [];
|
|
216
222
|
else if (field.type === "checkbox") values[field.id] = [];
|
|
223
|
+
else if (field.type === "smart_textarea") values[field.id] = { text: "", extractedKeywords: [] };
|
|
217
224
|
else values[field.id] = null;
|
|
218
225
|
}
|
|
219
226
|
});
|
|
@@ -361,6 +368,15 @@ var FormStore = class {
|
|
|
361
368
|
getSerializedValuesForSubmission() {
|
|
362
369
|
return this.serializeValuesForSubmission(this.state.values);
|
|
363
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Raw form values as stored internally, without any serialization or
|
|
373
|
+
* transformation. Useful when consumers need access to richer structures
|
|
374
|
+
* (e.g. smart_textarea keyword metadata) alongside the flattened values
|
|
375
|
+
* returned by getSerializedValuesForSubmission.
|
|
376
|
+
*/
|
|
377
|
+
getRawValues() {
|
|
378
|
+
return { ...this.state.values };
|
|
379
|
+
}
|
|
364
380
|
serializeValuesForSubmission(values) {
|
|
365
381
|
const result = { ...values };
|
|
366
382
|
this.schema.fields.forEach((field) => {
|
|
@@ -368,6 +384,11 @@ var FormStore = class {
|
|
|
368
384
|
delete result[field.id];
|
|
369
385
|
return;
|
|
370
386
|
}
|
|
387
|
+
if (field.type === "smart_textarea") {
|
|
388
|
+
const raw = values[field.id];
|
|
389
|
+
result[field.id] = raw?.text ?? null;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
371
392
|
if (field.type === "image_upload" || field.type === "signature") {
|
|
372
393
|
const raw = values[field.id];
|
|
373
394
|
if (raw !== void 0 && raw !== null && raw !== "") {
|
|
@@ -658,6 +679,7 @@ function useForm() {
|
|
|
658
679
|
availableTransitions: store.getAvailableTransitions(),
|
|
659
680
|
hasPersistence: store.hasPersistence(),
|
|
660
681
|
getSerializedValues: store.getSerializedValuesForSubmission.bind(store),
|
|
682
|
+
getRawValues: store.getRawValues.bind(store),
|
|
661
683
|
markSubmitAttempted: store.markSubmitAttempted.bind(store),
|
|
662
684
|
flushPendingUploads: store.flushPendingUploads.bind(store)
|
|
663
685
|
};
|
|
@@ -3992,6 +4014,206 @@ var FollowupWidget = ({ fieldId }) => {
|
|
|
3992
4014
|
error && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: error })
|
|
3993
4015
|
] }) });
|
|
3994
4016
|
};
|
|
4017
|
+
function useSmartKeywords(text, onSearch, debounceDelay = 1e3) {
|
|
4018
|
+
console.log("[useSmartKeywords] render", {
|
|
4019
|
+
textLength: text?.length ?? 0,
|
|
4020
|
+
hasOnSearch: typeof onSearch === "function",
|
|
4021
|
+
debounceDelay
|
|
4022
|
+
});
|
|
4023
|
+
const [extractedNouns, setExtractedNouns] = useState([]);
|
|
4024
|
+
const [matchedConditions, setMatchedConditions] = useState([]);
|
|
4025
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
4026
|
+
const [error, setError] = useState(null);
|
|
4027
|
+
const timerRef = useRef(null);
|
|
4028
|
+
const abortRef = useRef(0);
|
|
4029
|
+
const processText = useCallback(
|
|
4030
|
+
async (input, runId) => {
|
|
4031
|
+
if (!input || input.trim().length === 0) {
|
|
4032
|
+
console.log("[useSmartKeywords] skip empty input", {
|
|
4033
|
+
runId,
|
|
4034
|
+
inputLength: input?.length ?? 0
|
|
4035
|
+
});
|
|
4036
|
+
setExtractedNouns([]);
|
|
4037
|
+
setMatchedConditions([]);
|
|
4038
|
+
setIsProcessing(false);
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
console.log("[useSmartKeywords] processText start", {
|
|
4042
|
+
runId,
|
|
4043
|
+
inputLength: input.length
|
|
4044
|
+
});
|
|
4045
|
+
setIsProcessing(true);
|
|
4046
|
+
setError(null);
|
|
4047
|
+
try {
|
|
4048
|
+
const nouns = extractNouns(input);
|
|
4049
|
+
const uniqueNouns = Array.from(new Set(nouns));
|
|
4050
|
+
console.log("[useSmartKeywords] extracted nouns", {
|
|
4051
|
+
runId,
|
|
4052
|
+
nouns,
|
|
4053
|
+
uniqueNouns
|
|
4054
|
+
});
|
|
4055
|
+
if (runId !== abortRef.current) return;
|
|
4056
|
+
setExtractedNouns(uniqueNouns);
|
|
4057
|
+
if (uniqueNouns.length === 0) {
|
|
4058
|
+
console.log("[useSmartKeywords] no nouns, skipping search", { runId });
|
|
4059
|
+
setMatchedConditions([]);
|
|
4060
|
+
setIsProcessing(false);
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
const searchResults = await Promise.all(
|
|
4064
|
+
uniqueNouns.map(async (noun) => {
|
|
4065
|
+
const normalised = noun.replace(/\s+/g, " ").replace(/^[\s-]+/, "").trim();
|
|
4066
|
+
try {
|
|
4067
|
+
console.log("[useSmartKeywords] onSearch call", { runId, noun, normalised });
|
|
4068
|
+
const results = await onSearch(normalised);
|
|
4069
|
+
console.log("[useSmartKeywords] onSearch result", {
|
|
4070
|
+
runId,
|
|
4071
|
+
noun,
|
|
4072
|
+
normalised,
|
|
4073
|
+
count: Array.isArray(results) ? results.length : 0
|
|
4074
|
+
});
|
|
4075
|
+
const first = Array.isArray(results) && results.length > 0 ? [results[0]] : [];
|
|
4076
|
+
return { keyword: normalised, matches: first };
|
|
4077
|
+
} catch (err) {
|
|
4078
|
+
console.error("[useSmartKeywords] onSearch error", { runId, noun, err });
|
|
4079
|
+
return { keyword: normalised || noun, matches: [] };
|
|
4080
|
+
}
|
|
4081
|
+
})
|
|
4082
|
+
);
|
|
4083
|
+
if (runId !== abortRef.current) return;
|
|
4084
|
+
const allMatches = searchResults.flatMap(
|
|
4085
|
+
({ keyword, matches }) => matches.map((record) => ({
|
|
4086
|
+
extractedKeyword: keyword,
|
|
4087
|
+
matchedCondition: {
|
|
4088
|
+
id: record.id,
|
|
4089
|
+
name: record.name,
|
|
4090
|
+
created_at: record.created_at,
|
|
4091
|
+
updated_at: record.updated_at
|
|
4092
|
+
}
|
|
4093
|
+
}))
|
|
4094
|
+
);
|
|
4095
|
+
console.log("[useSmartKeywords] allMatches computed", {
|
|
4096
|
+
runId,
|
|
4097
|
+
matchesCount: allMatches.length
|
|
4098
|
+
});
|
|
4099
|
+
setMatchedConditions(allMatches);
|
|
4100
|
+
} catch (err) {
|
|
4101
|
+
if (runId !== abortRef.current) return;
|
|
4102
|
+
console.error("[useSmartKeywords] processText error", { runId, err });
|
|
4103
|
+
setError(err instanceof Error ? err.message : "Keyword extraction failed");
|
|
4104
|
+
} finally {
|
|
4105
|
+
if (runId === abortRef.current) {
|
|
4106
|
+
console.log("[useSmartKeywords] processText end", { runId });
|
|
4107
|
+
setIsProcessing(false);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
},
|
|
4111
|
+
[onSearch]
|
|
4112
|
+
);
|
|
4113
|
+
useEffect(() => {
|
|
4114
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
4115
|
+
const runId = ++abortRef.current;
|
|
4116
|
+
console.log("[useSmartKeywords] schedule debounce", {
|
|
4117
|
+
runId,
|
|
4118
|
+
debounceDelay,
|
|
4119
|
+
textLength: text?.length ?? 0
|
|
4120
|
+
});
|
|
4121
|
+
timerRef.current = setTimeout(() => {
|
|
4122
|
+
console.log("[useSmartKeywords] debounce fired", { runId });
|
|
4123
|
+
processText(text, runId);
|
|
4124
|
+
}, debounceDelay);
|
|
4125
|
+
return () => {
|
|
4126
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
4127
|
+
};
|
|
4128
|
+
}, [text, debounceDelay, processText]);
|
|
4129
|
+
return { extractedNouns, matchedConditions, isProcessing, error };
|
|
4130
|
+
}
|
|
4131
|
+
function extractNouns(text) {
|
|
4132
|
+
const doc = nlp(text);
|
|
4133
|
+
const rawNouns = doc.nouns().out("array");
|
|
4134
|
+
const cleaned = rawNouns.map((n) => n.replace(/[^\w\s-]/g, "").trim().toLowerCase()).filter((n) => n.length > 2);
|
|
4135
|
+
console.log("[useSmartKeywords] extractNouns", {
|
|
4136
|
+
inputLength: text?.length ?? 0,
|
|
4137
|
+
rawNouns,
|
|
4138
|
+
cleaned
|
|
4139
|
+
});
|
|
4140
|
+
return cleaned;
|
|
4141
|
+
}
|
|
4142
|
+
var SmartTextareaWidget = ({ fieldId }) => {
|
|
4143
|
+
const { fieldDef, value, setValue, setTouched, error, disabled, touched } = useField(fieldId);
|
|
4144
|
+
const { state } = useForm();
|
|
4145
|
+
const handler = useFieldHandlers(fieldId);
|
|
4146
|
+
const def = fieldDef;
|
|
4147
|
+
const structured = value;
|
|
4148
|
+
const textValue = structured?.text ?? "";
|
|
4149
|
+
const showError = !!error && (touched || state.submitAttempted);
|
|
4150
|
+
const onSearch = useCallback(
|
|
4151
|
+
(query) => handler.onSearch(query),
|
|
4152
|
+
[handler]
|
|
4153
|
+
);
|
|
4154
|
+
const { matchedConditions, isProcessing } = useSmartKeywords(
|
|
4155
|
+
textValue,
|
|
4156
|
+
onSearch,
|
|
4157
|
+
def?.debounceDelay ?? 1e3
|
|
4158
|
+
);
|
|
4159
|
+
const currentKeywords = structured?.extractedKeywords ?? [];
|
|
4160
|
+
const nextKeywords = useMemo(() => {
|
|
4161
|
+
if (matchedConditions.length === 0 && currentKeywords.length === 0) return currentKeywords;
|
|
4162
|
+
if (matchedConditions.length === currentKeywords.length && matchedConditions.every(
|
|
4163
|
+
(m, i) => m.matchedCondition.id === currentKeywords[i]?.matchedCondition.id && m.extractedKeyword === currentKeywords[i]?.extractedKeyword
|
|
4164
|
+
)) {
|
|
4165
|
+
return currentKeywords;
|
|
4166
|
+
}
|
|
4167
|
+
return matchedConditions;
|
|
4168
|
+
}, [matchedConditions, currentKeywords]);
|
|
4169
|
+
const writeValue = useCallback(
|
|
4170
|
+
(text, keywords) => {
|
|
4171
|
+
const next = { text, extractedKeywords: keywords };
|
|
4172
|
+
setValue(next);
|
|
4173
|
+
},
|
|
4174
|
+
[setValue]
|
|
4175
|
+
);
|
|
4176
|
+
React11__default.useEffect(() => {
|
|
4177
|
+
if (nextKeywords !== currentKeywords) {
|
|
4178
|
+
writeValue(textValue, nextKeywords);
|
|
4179
|
+
}
|
|
4180
|
+
}, [nextKeywords]);
|
|
4181
|
+
const handleTextChange = useCallback(
|
|
4182
|
+
(e) => {
|
|
4183
|
+
writeValue(e.target.value, currentKeywords);
|
|
4184
|
+
},
|
|
4185
|
+
[writeValue, currentKeywords]
|
|
4186
|
+
);
|
|
4187
|
+
if (!def) return null;
|
|
4188
|
+
const height = def.height;
|
|
4189
|
+
const theme = getThemeConfig("textarea", def.theme);
|
|
4190
|
+
const wrapperClass = theme?.wrapperClassName ?? "space-y-2";
|
|
4191
|
+
const labelClass = theme?.labelClassName;
|
|
4192
|
+
const inputClass = cn(theme?.inputClassName, showError && "border-red-500");
|
|
4193
|
+
const labelContent = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4194
|
+
def.label,
|
|
4195
|
+
" ",
|
|
4196
|
+
def.required && /* @__PURE__ */ jsx("span", { className: "text-red-500", children: "*" }),
|
|
4197
|
+
isProcessing && /* @__PURE__ */ jsx(Loader2, { className: "inline-block ml-1.5 h-3 w-3 animate-spin text-muted-foreground" })
|
|
4198
|
+
] });
|
|
4199
|
+
return /* @__PURE__ */ jsxs("div", { className: wrapperClass, children: [
|
|
4200
|
+
theme ? /* @__PURE__ */ jsx(Label, { htmlFor: fieldId, className: labelClass, children: labelContent }) : /* @__PURE__ */ jsx(Label, { htmlFor: fieldId, children: labelContent }),
|
|
4201
|
+
/* @__PURE__ */ jsx(
|
|
4202
|
+
Textarea,
|
|
4203
|
+
{
|
|
4204
|
+
id: fieldId,
|
|
4205
|
+
value: textValue,
|
|
4206
|
+
onChange: handleTextChange,
|
|
4207
|
+
onBlur: setTouched,
|
|
4208
|
+
disabled,
|
|
4209
|
+
placeholder: def.placeholder,
|
|
4210
|
+
className: inputClass,
|
|
4211
|
+
...height != null ? { height } : {}
|
|
4212
|
+
}
|
|
4213
|
+
),
|
|
4214
|
+
showError && /* @__PURE__ */ jsx("p", { className: "text-xs text-red-500", children: error })
|
|
4215
|
+
] });
|
|
4216
|
+
};
|
|
3995
4217
|
var FieldRenderer = ({ fieldId }) => {
|
|
3996
4218
|
const { fieldDef, visible } = useField(fieldId);
|
|
3997
4219
|
if (!visible || !fieldDef) {
|
|
@@ -4038,6 +4260,8 @@ var FieldRenderer = ({ fieldId }) => {
|
|
|
4038
4260
|
return /* @__PURE__ */ jsx(ReferralWidget, { fieldId });
|
|
4039
4261
|
case "followup":
|
|
4040
4262
|
return /* @__PURE__ */ jsx(FollowupWidget, { fieldId });
|
|
4263
|
+
case "smart_textarea":
|
|
4264
|
+
return /* @__PURE__ */ jsx(SmartTextareaWidget, { fieldId });
|
|
4041
4265
|
case "static_text": {
|
|
4042
4266
|
const def = fieldDef;
|
|
4043
4267
|
const size = def.size ?? "regular";
|
|
@@ -4786,6 +5010,6 @@ function createUploadHandler(options) {
|
|
|
4786
5010
|
};
|
|
4787
5011
|
}
|
|
4788
5012
|
|
|
4789
|
-
export { AddInvestigationField, AddMedicationField, DifferentialDiagnosis, DynamicForm, EMAIL_REGEX, FieldRenderer, FormControls, FormProvider, FormStore, ImageUploadWidget, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlySignature, ReadOnlyTable, SignatureUploadWidget, SmartForm, createUploadHandler, evaluateRules, useField, useFieldHandlers, useForm, useFormStore, validateField, validateForm };
|
|
5013
|
+
export { AddInvestigationField, AddMedicationField, DifferentialDiagnosis, DynamicForm, EMAIL_REGEX, FieldRenderer, FormControls, FormProvider, FormStore, ImageUploadWidget, ReadOnlyForm, ReadOnlyImageUpload, ReadOnlySignature, ReadOnlyTable, SignatureUploadWidget, SmartForm, SmartTextareaWidget, createUploadHandler, evaluateRules, useField, useFieldHandlers, useForm, useFormStore, useSmartKeywords, validateField, validateForm };
|
|
4790
5014
|
//# sourceMappingURL=index.mjs.map
|
|
4791
5015
|
//# sourceMappingURL=index.mjs.map
|