@ram_28/kf-ai-sdk 1.0.9 → 1.0.11

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.
@@ -49,15 +49,16 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
49
49
  mode = "onBlur", // Validation mode - controls when errors are shown (see types.ts for details)
50
50
  enabled = true,
51
51
  userRole,
52
- onSuccess,
53
- onError,
54
52
  onSchemaError,
55
- onSubmitError,
56
53
  skipSchemaFetch = false,
57
54
  schema: manualSchema,
58
55
  draftOnEveryChange = false,
56
+ interactionMode = "interactive",
59
57
  } = options;
60
58
 
59
+ // Derived interaction mode flags
60
+ const isInteractiveMode = interactionMode === "interactive";
61
+
61
62
  // ============================================================
62
63
  // STATE MANAGEMENT
63
64
  // ============================================================
@@ -65,10 +66,14 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
65
66
  const [schemaConfig, setSchemaConfig] =
66
67
  useState<FormSchemaConfig | null>(null);
67
68
  const [referenceData, setReferenceData] = useState<Record<string, any[]>>({});
68
- const [submitError, setSubmitError] = useState<Error | null>(null);
69
69
  const [isSubmitting, setIsSubmitting] = useState(false);
70
70
  const [lastFormValues] = useState<Partial<T>>({});
71
71
 
72
+ // Interactive mode state
73
+ const [draftId, setDraftId] = useState<string | null>(null);
74
+ const [isCreatingDraft, setIsCreatingDraft] = useState(false);
75
+ const [draftError, setDraftError] = useState<Error | null>(null);
76
+
72
77
  // Prevent infinite loop in API calls
73
78
  const isComputingRef = useRef(false);
74
79
  const computeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -77,25 +82,10 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
77
82
  // This allows us to detect changes since the last draft, not since form init
78
83
  const lastSyncedValuesRef = useRef<Partial<T> | null>(null);
79
84
 
80
- // Stable callback refs to prevent dependency loops
81
- const onSuccessRef = useRef(onSuccess);
82
- const onErrorRef = useRef(onError);
83
- const onSubmitErrorRef = useRef(onSubmitError);
85
+ // Stable callback ref to prevent dependency loops
84
86
  const onSchemaErrorRef = useRef(onSchemaError);
85
87
 
86
- // Update refs when callbacks change
87
- useEffect(() => {
88
- onSuccessRef.current = onSuccess;
89
- }, [onSuccess]);
90
-
91
- useEffect(() => {
92
- onErrorRef.current = onError;
93
- }, [onError]);
94
-
95
- useEffect(() => {
96
- onSubmitErrorRef.current = onSubmitError;
97
- }, [onSubmitError]);
98
-
88
+ // Update ref when callback changes
99
89
  useEffect(() => {
100
90
  onSchemaErrorRef.current = onSchemaError;
101
91
  }, [onSchemaError]);
@@ -201,18 +191,67 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
201
191
  }
202
192
  }, [schema, userRole]); // Removed onSchemaError - using ref instead
203
193
 
204
- // Handle schema and record errors
194
+ // Handle schema error
205
195
  useEffect(() => {
206
196
  if (schemaError) {
207
197
  onSchemaErrorRef.current?.(schemaError);
208
198
  }
209
199
  }, [schemaError]);
210
200
 
201
+ // ============================================================
202
+ // INTERACTIVE MODE - INITIAL DRAFT CREATION
203
+ // ============================================================
204
+
205
+ // Create initial draft for interactive create mode
211
206
  useEffect(() => {
212
- if (recordError) {
213
- onErrorRef.current?.(recordError);
207
+ // Only run for interactive mode + create operation + schema loaded + no draft yet
208
+ if (
209
+ !isInteractiveMode ||
210
+ operation !== "create" ||
211
+ !schemaConfig ||
212
+ !enabled ||
213
+ draftId
214
+ ) {
215
+ return;
214
216
  }
215
- }, [recordError]);
217
+
218
+ const createInitialDraft = async () => {
219
+ setIsCreatingDraft(true);
220
+ setDraftError(null);
221
+
222
+ try {
223
+ const client = api<T>(source);
224
+ // Call PATCH /{bdo_id}/draft with empty payload to get draft ID
225
+ const response = await client.draftInteraction({});
226
+
227
+ // Store the draft ID
228
+ setDraftId(response._id);
229
+
230
+ // Apply any computed fields returned from API
231
+ if (response && typeof response === "object") {
232
+ Object.entries(response).forEach(([fieldName, value]) => {
233
+ // Skip _id as it's the draft ID, not a form field
234
+ if (fieldName === "_id") return;
235
+
236
+ const currentValue = rhfForm.getValues(fieldName as any);
237
+ if (currentValue !== value) {
238
+ rhfForm.setValue(fieldName as any, value as any, {
239
+ shouldDirty: false,
240
+ shouldValidate: false,
241
+ });
242
+ }
243
+ });
244
+ }
245
+ } catch (error) {
246
+ console.error("Failed to create initial draft:", error);
247
+ setDraftError(error as Error);
248
+ } finally {
249
+ setIsCreatingDraft(false);
250
+ }
251
+ };
252
+
253
+ createInitialDraft();
254
+ }, [isInteractiveMode, operation, schemaConfig, enabled, draftId, source, rhfForm]);
216
255
 
217
256
  // ============================================================
218
257
  // COMPUTED FIELD DEPENDENCY TRACKING AND OPTIMIZATION
@@ -278,21 +317,29 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
278
317
  // Manual computation trigger - called on blur after validation passes
279
318
  const triggerComputationAfterValidation = useCallback(
280
319
  async (fieldName: string) => {
281
- if (!schemaConfig || computedFieldDependencies.length === 0) {
320
+ if (!schemaConfig) {
282
321
  return;
283
322
  }
284
323
 
285
- // Check if this field is a dependency for any computed fields
286
- // If draftOnEveryChange is true, trigger for all fields
287
- // If false (default), only trigger for computed field dependencies
288
- const shouldTrigger =
289
- draftOnEveryChange ||
290
- computedFieldDependencies.includes(fieldName as Path<T>);
324
+ // Determine if draft should be triggered based on interaction mode
325
+ // Interactive mode: Always trigger draft API on blur
326
+ // Non-interactive mode: Only trigger for computed field dependencies (legacy behavior)
327
+ const shouldTrigger = isInteractiveMode
328
+ ? true // Interactive mode: always trigger
329
+ : (computedFieldDependencies.length > 0 &&
330
+ (draftOnEveryChange ||
331
+ computedFieldDependencies.includes(fieldName as Path<T>)));
291
332
 
292
333
  if (!shouldTrigger) {
293
334
  return;
294
335
  }
295
336
 
337
+ // For interactive create, check that we have a draftId (except for initial draft creation)
338
+ if (isInteractiveMode && operation === "create" && !draftId) {
339
+ console.warn("Interactive create mode: waiting for draft ID");
340
+ return;
341
+ }
342
+
296
343
  // Prevent concurrent API calls
297
344
  if (isComputingRef.current) {
298
345
  return;
@@ -330,6 +377,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
330
377
  (changedFields as any)._id = (currentValues as any)._id;
331
378
  }
332
379
 
380
+ // For interactive create mode, include draft _id
381
+ if (isInteractiveMode && operation === "create" && draftId) {
382
+ (changedFields as any)._id = draftId;
383
+ }
384
+
333
385
  // Use lastSyncedValues if available, otherwise use recordData (for update) or empty object (for create)
334
386
  const baseline =
335
387
  lastSyncedValuesRef.current ??
@@ -382,10 +434,18 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
382
434
 
383
435
  lastSyncedValuesRef.current = baselineBeforeApiCall;
384
436
 
385
- const computedFieldsResponse =
386
- operation === "update" && recordId
387
- ? await client.draftPatch(recordId, payload)
388
- : await client.draft(payload);
437
+ // Choose API method based on operation and interaction mode
438
+ let computedFieldsResponse;
439
+ if (operation === "update" && recordId) {
440
+ // Update mode: use draftPatch (both interactive and non-interactive)
441
+ computedFieldsResponse = await client.draftPatch(recordId, payload);
442
+ } else if (isInteractiveMode && draftId) {
443
+ // Interactive create: use draftInteraction with _id
444
+ computedFieldsResponse = await client.draftInteraction(payload);
445
+ } else {
446
+ // Non-interactive create: use draft (POST)
447
+ computedFieldsResponse = await client.draft(payload);
448
+ }
389
449
 
390
450
  // Apply computed fields returned from API
391
451
  if (
@@ -438,6 +498,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
438
498
  rhfForm,
439
499
  computedFieldDependencies,
440
500
  draftOnEveryChange,
501
+ isInteractiveMode,
502
+ draftId,
441
503
  ]
442
504
  );
443
505
 
@@ -495,76 +557,149 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
495
557
  ]);
496
558
 
497
559
  // ============================================================
498
- // FORM SUBMISSION
560
+ // HANDLE SUBMIT - RHF-style API with internal submission
499
561
  // ============================================================
500
562
 
501
- const submit = useCallback(async (): Promise<void> => {
502
- if (!schemaConfig) {
503
- throw new Error("Schema not loaded");
504
- }
563
+ /**
564
+ * handleSubmit follows React Hook Form's signature pattern:
565
+ *
566
+ * handleSubmit(onSuccess?, onError?) => (e?) => Promise<void>
567
+ *
568
+ * Internal flow:
569
+ * 1. RHF validation + Cross-field validation → FAILS → onError(fieldErrors)
570
+ * 2. Clean data & call API → FAILS → onError(apiError)
571
+ * 3. SUCCESS → onSuccess(responseData)
572
+ */
573
+ const handleSubmit = useCallback(
574
+ (
575
+ onSuccess?: (data: T, e?: React.BaseSyntheticEvent) => void | Promise<void>,
576
+ onError?: (
577
+ error: import("react-hook-form").FieldErrors<T> | Error,
578
+ e?: React.BaseSyntheticEvent
579
+ ) => void | Promise<void>
580
+ ) => {
581
+ return rhfForm.handleSubmit(
582
+ // RHF onValid handler - validation passed, now do cross-field + API
583
+ async (data, event) => {
584
+ if (!schemaConfig) {
585
+ const error = new Error("Schema not loaded");
586
+ onError?.(error, event);
587
+ return;
588
+ }
505
589
 
506
- setIsSubmitting(true);
507
- setSubmitError(null);
590
+ setIsSubmitting(true);
508
591
 
509
- try {
510
- // Validate form including cross-field validation
511
- const isValid = await validateForm();
512
- if (!isValid) {
513
- throw new Error("Form validation failed");
514
- }
592
+ try {
593
+ // Cross-field validation
594
+ const transformedRules = schemaConfig.crossFieldValidation.map(
595
+ (rule) => ({
596
+ Id: rule.Id,
597
+ Condition: { ExpressionTree: rule.ExpressionTree },
598
+ Message: rule.Message || `Validation failed for ${rule.Name}`,
599
+ })
600
+ );
515
601
 
516
- const formValues = rhfForm.getValues();
602
+ const crossFieldErrors = validateCrossField(
603
+ transformedRules,
604
+ data as any,
605
+ referenceData
606
+ );
517
607
 
518
- // Clean data for submission
519
- // - For create: includes all non-computed fields
520
- // - For update: includes only fields that changed from recordData
521
- const cleanedData = cleanFormData(
522
- formValues as any,
523
- schemaConfig.computedFields,
524
- operation,
525
- recordData as Partial<T> | undefined
526
- );
608
+ if (crossFieldErrors.length > 0) {
609
+ // Set cross-field errors in form state
610
+ crossFieldErrors.forEach((error, index) => {
611
+ rhfForm.setError(`root.crossField${index}` as any, {
612
+ type: "validate",
613
+ message: error.message,
614
+ });
615
+ });
616
+ // Call onError with current form errors
617
+ onError?.(rhfForm.formState.errors, event);
618
+ return;
619
+ }
527
620
 
528
- // Submit data
529
- const result = await submitFormData<T>(
530
- source,
531
- operation,
532
- cleanedData,
533
- recordId
534
- );
621
+ // Clean data for submission
622
+ const cleanedData = cleanFormData(
623
+ data as any,
624
+ schemaConfig.computedFields,
625
+ operation,
626
+ recordData as Partial<T> | undefined
627
+ );
535
628
 
536
- if (!result.success) {
537
- throw result.error || new Error("Submission failed");
538
- }
629
+ let result;
539
630
 
540
- // Success callback
541
- onSuccessRef.current?.(result.data || formValues);
631
+ if (isInteractiveMode) {
632
+ // Interactive mode submission
633
+ const client = api<T>(source);
542
634
 
543
- // Reset form for create operations
544
- if (operation === "create") {
545
- rhfForm.reset();
546
- }
547
- } catch (error) {
548
- const submitError = error as Error;
549
- setSubmitError(submitError);
550
- onSubmitErrorRef.current?.(submitError);
551
- onErrorRef.current?.(submitError);
552
- throw error;
553
- } finally {
554
- setIsSubmitting(false);
555
- }
556
- }, [schemaConfig, validateForm, rhfForm, source, operation, recordId, recordData]);
635
+ if (operation === "create") {
636
+ // Interactive create: must have draftId
637
+ if (!draftId) {
638
+ throw new Error(
639
+ "Interactive create mode requires a draft ID. Draft creation may have failed."
640
+ );
641
+ }
642
+ // POST /{bdo_id}/draft with _id in payload
643
+ const response = await client.draft({
644
+ ...cleanedData,
645
+ _id: draftId,
646
+ } as any);
647
+ result = { success: true, data: response };
648
+ } else {
649
+ // Interactive update: POST /{bdo_id}/{id}/draft
650
+ const response = await client.draftUpdate(recordId!, cleanedData);
651
+ result = { success: true, data: response };
652
+ }
653
+ } else {
654
+ // Non-interactive mode: use existing submitFormData
655
+ result = await submitFormData<T>(
656
+ source,
657
+ operation,
658
+ cleanedData,
659
+ recordId
660
+ );
557
661
 
558
- // ============================================================
559
- // HANDLE SUBMIT - Simplified API
560
- // ============================================================
662
+ if (!result.success) {
663
+ throw result.error || new Error("Submission failed");
664
+ }
665
+ }
561
666
 
562
- // Simplified handleSubmit that always uses SDK's submit function
563
- const handleSubmit = useCallback(() => {
564
- return rhfForm.handleSubmit(async () => {
565
- await submit();
566
- });
567
- }, [rhfForm, submit]);
667
+ // Reset form for create operations
668
+ if (operation === "create") {
669
+ rhfForm.reset();
670
+ // Clear draft state for interactive mode
671
+ if (isInteractiveMode) {
672
+ setDraftId(null);
673
+ }
674
+ }
675
+
676
+ // Success callback with response data
677
+ await onSuccess?.(result.data || data, event);
678
+ } catch (error) {
679
+ // API error - call onError with Error object
680
+ onError?.(error as Error, event);
681
+ } finally {
682
+ setIsSubmitting(false);
683
+ }
684
+ },
685
+ // RHF onInvalid handler - validation failed
686
+ (errors, event) => {
687
+ onError?.(errors, event);
688
+ }
689
+ );
690
+ },
691
+ [
692
+ rhfForm,
693
+ schemaConfig,
694
+ referenceData,
695
+ source,
696
+ operation,
697
+ recordId,
698
+ recordData,
699
+ isInteractiveMode,
700
+ draftId,
701
+ ]
702
+ );
568
703
 
569
704
  // ============================================================
570
705
  // FIELD HELPERS
@@ -623,18 +758,20 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
623
758
 
624
759
  const clearErrors = useCallback((): void => {
625
760
  rhfForm.clearErrors();
626
- setSubmitError(null);
627
761
  }, [rhfForm]);
628
762
 
629
763
  // ============================================================
630
764
  // COMPUTED PROPERTIES
631
765
  // ============================================================
632
766
 
767
+ // Loading state includes interactive mode draft creation
633
768
  const isLoadingInitialData =
634
- isLoadingSchema || (operation === "update" && isLoadingRecord);
769
+ isLoadingSchema ||
770
+ (operation === "update" && isLoadingRecord) ||
771
+ (isInteractiveMode && operation === "create" && isCreatingDraft);
635
772
  const isLoading = isLoadingInitialData || isSubmitting;
636
- const loadError = schemaError || recordError;
637
- const hasError = !!(loadError || submitError);
773
+ const loadError = schemaError || recordError || draftError;
774
+ const hasError = !!loadError;
638
775
 
639
776
  const computedFields = useMemo<Array<keyof T>>(
640
777
  () => (schemaConfig?.computedFields as Array<keyof T>) || [],
@@ -794,9 +931,12 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
794
931
  isLoadingRecord,
795
932
  isLoading,
796
933
 
934
+ // Interactive mode state
935
+ draftId,
936
+ isCreatingDraft,
937
+
797
938
  // Error handling
798
939
  loadError: loadError as Error | null,
799
- submitError,
800
940
  hasError,
801
941
 
802
942
  // Schema information
@@ -813,7 +953,6 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
813
953
  isFieldComputed,
814
954
 
815
955
  // Operations
816
- submit,
817
956
  refreshSchema,
818
957
  validateForm,
819
958
  clearErrors,