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.d.cts CHANGED
@@ -186,7 +186,38 @@ interface FollowupValue {
186
186
  interface FollowupFieldDef extends BaseFieldDef {
187
187
  type: 'followup';
188
188
  }
189
- type FieldDef = TextFieldDef | NumberFieldDef | SelectFieldDef | RadioFieldDef | CheckboxFieldDef | RepeatableFieldDef | ImageUploadFieldDef | SignatureFieldDef | EditableTableFieldDef | StaticTextFieldDef | MedicationsFieldDef | InvestigationsFieldDef | ProceduresFieldDef | DifferentialDiagnosisFieldDef | VitalsFieldDef | ReferralFieldDef | FollowupFieldDef | BaseFieldDef;
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
- 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 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 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 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, validateField, validateForm };
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
- type FieldDef = TextFieldDef | NumberFieldDef | SelectFieldDef | RadioFieldDef | CheckboxFieldDef | RepeatableFieldDef | ImageUploadFieldDef | SignatureFieldDef | EditableTableFieldDef | StaticTextFieldDef | MedicationsFieldDef | InvestigationsFieldDef | ProceduresFieldDef | DifferentialDiagnosisFieldDef | VitalsFieldDef | ReferralFieldDef | FollowupFieldDef | BaseFieldDef;
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
- 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 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 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 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, validateField, validateForm };
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