@ram_28/kf-ai-sdk 1.0.10 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ram_28/kf-ai-sdk",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Type-safe, AI-driven SDK for building modern web applications with role-based access control",
5
5
  "author": "Ramprasad",
6
6
  "license": "MIT",
package/sdk/api/client.ts CHANGED
@@ -69,6 +69,15 @@ export interface ResourceClient<T = any> {
69
69
  */
70
70
  draftPatch(id: string, data: Partial<T>): Promise<DraftResponse>;
71
71
 
72
+ /**
73
+ * Interactive draft - create/update draft without instance ID
74
+ * PATCH /{bo_id}/draft
75
+ * Used in interactive mode for create operations
76
+ */
77
+ draftInteraction(
78
+ data: Partial<T> & { _id?: string }
79
+ ): Promise<DraftResponse & { _id: string }>;
80
+
72
81
  // ============================================================
73
82
  // QUERY OPERATIONS
74
83
  // ============================================================
@@ -355,6 +364,24 @@ export function api<T = any>(bo_id: string): ResourceClient<T> {
355
364
  return response.json();
356
365
  },
357
366
 
367
+ async draftInteraction(
368
+ data: Partial<T> & { _id?: string }
369
+ ): Promise<DraftResponse & { _id: string }> {
370
+ const response = await fetch(`${baseUrl}/api/app/${bo_id}/draft`, {
371
+ method: "PATCH",
372
+ headers: defaultHeaders,
373
+ body: JSON.stringify(data),
374
+ });
375
+
376
+ if (!response.ok) {
377
+ throw new Error(
378
+ `Failed to create interactive draft for ${bo_id}: ${response.statusText}`
379
+ );
380
+ }
381
+
382
+ return response.json();
383
+ },
384
+
358
385
  // ============================================================
359
386
  // QUERY OPERATIONS
360
387
  // ============================================================
@@ -229,6 +229,13 @@ export interface BDOSchema {
229
229
  */
230
230
  export type FormOperation = "create" | "update";
231
231
 
232
+ /**
233
+ * Form interaction mode
234
+ * - "interactive" (default): Real-time server-side validation and computation on every field blur
235
+ * - "non-interactive": Draft only for computed field dependencies (legacy behavior)
236
+ */
237
+ export type InteractionMode = "interactive" | "non-interactive";
238
+
232
239
  /**
233
240
  * Form validation mode (from react-hook-form)
234
241
  */
@@ -295,8 +302,17 @@ export interface UseFormOptions<
295
302
  * Trigger draft API call on every field change (after validation passes)
296
303
  * If true: draft API is called for any field change
297
304
  * If false (default): draft API is called only when computed field dependencies change
305
+ * @deprecated Use interactionMode: "interactive" instead
298
306
  */
299
307
  draftOnEveryChange?: boolean;
308
+
309
+ /**
310
+ * Form interaction mode
311
+ * - "interactive" (default): Real-time server-side validation and computation on every field blur
312
+ * - "non-interactive": Draft only for computed field dependencies (legacy behavior)
313
+ * @default "interactive"
314
+ */
315
+ interactionMode?: InteractionMode;
300
316
  }
301
317
 
302
318
  // ============================================================
@@ -536,6 +552,16 @@ export interface UseFormReturn<
536
552
  /** Any loading state active */
537
553
  isLoading: boolean;
538
554
 
555
+ // ============================================================
556
+ // INTERACTIVE MODE STATE
557
+ // ============================================================
558
+
559
+ /** Draft ID for interactive create mode */
560
+ draftId: string | null;
561
+
562
+ /** Whether draft is being created (interactive create only) */
563
+ isCreatingDraft: boolean;
564
+
539
565
  // ============================================================
540
566
  // ERROR HANDLING
541
567
  // ============================================================
@@ -53,8 +53,12 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
53
53
  skipSchemaFetch = false,
54
54
  schema: manualSchema,
55
55
  draftOnEveryChange = false,
56
+ interactionMode = "interactive",
56
57
  } = options;
57
58
 
59
+ // Derived interaction mode flags
60
+ const isInteractiveMode = interactionMode === "interactive";
61
+
58
62
  // ============================================================
59
63
  // STATE MANAGEMENT
60
64
  // ============================================================
@@ -65,6 +69,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
65
69
  const [isSubmitting, setIsSubmitting] = useState(false);
66
70
  const [lastFormValues] = useState<Partial<T>>({});
67
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
+
68
77
  // Prevent infinite loop in API calls
69
78
  const isComputingRef = useRef(false);
70
79
  const computeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -189,6 +198,61 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
189
198
  }
190
199
  }, [schemaError]);
191
200
 
201
+ // ============================================================
202
+ // INTERACTIVE MODE - INITIAL DRAFT CREATION
203
+ // ============================================================
204
+
205
+ // Create initial draft for interactive create mode
206
+ useEffect(() => {
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;
216
+ }
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]);
255
+
192
256
  // ============================================================
193
257
  // COMPUTED FIELD DEPENDENCY TRACKING AND OPTIMIZATION
194
258
  // ============================================================
@@ -253,21 +317,29 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
253
317
  // Manual computation trigger - called on blur after validation passes
254
318
  const triggerComputationAfterValidation = useCallback(
255
319
  async (fieldName: string) => {
256
- if (!schemaConfig || computedFieldDependencies.length === 0) {
320
+ if (!schemaConfig) {
257
321
  return;
258
322
  }
259
323
 
260
- // Check if this field is a dependency for any computed fields
261
- // If draftOnEveryChange is true, trigger for all fields
262
- // If false (default), only trigger for computed field dependencies
263
- const shouldTrigger =
264
- draftOnEveryChange ||
265
- 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>)));
266
332
 
267
333
  if (!shouldTrigger) {
268
334
  return;
269
335
  }
270
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
+
271
343
  // Prevent concurrent API calls
272
344
  if (isComputingRef.current) {
273
345
  return;
@@ -305,6 +377,11 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
305
377
  (changedFields as any)._id = (currentValues as any)._id;
306
378
  }
307
379
 
380
+ // For interactive create mode, include draft _id
381
+ if (isInteractiveMode && operation === "create" && draftId) {
382
+ (changedFields as any)._id = draftId;
383
+ }
384
+
308
385
  // Use lastSyncedValues if available, otherwise use recordData (for update) or empty object (for create)
309
386
  const baseline =
310
387
  lastSyncedValuesRef.current ??
@@ -357,10 +434,18 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
357
434
 
358
435
  lastSyncedValuesRef.current = baselineBeforeApiCall;
359
436
 
360
- const computedFieldsResponse =
361
- operation === "update" && recordId
362
- ? await client.draftPatch(recordId, payload)
363
- : 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
+ }
364
449
 
365
450
  // Apply computed fields returned from API
366
451
  if (
@@ -413,6 +498,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
413
498
  rhfForm,
414
499
  computedFieldDependencies,
415
500
  draftOnEveryChange,
501
+ isInteractiveMode,
502
+ draftId,
416
503
  ]
417
504
  );
418
505
 
@@ -539,21 +626,51 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
539
626
  recordData as Partial<T> | undefined
540
627
  );
541
628
 
542
- // Submit data to API
543
- const result = await submitFormData<T>(
544
- source,
545
- operation,
546
- cleanedData,
547
- recordId
548
- );
629
+ let result;
549
630
 
550
- if (!result.success) {
551
- throw result.error || new Error("Submission failed");
631
+ if (isInteractiveMode) {
632
+ // Interactive mode submission
633
+ const client = api<T>(source);
634
+
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
+ );
661
+
662
+ if (!result.success) {
663
+ throw result.error || new Error("Submission failed");
664
+ }
552
665
  }
553
666
 
554
667
  // Reset form for create operations
555
668
  if (operation === "create") {
556
669
  rhfForm.reset();
670
+ // Clear draft state for interactive mode
671
+ if (isInteractiveMode) {
672
+ setDraftId(null);
673
+ }
557
674
  }
558
675
 
559
676
  // Success callback with response data
@@ -579,6 +696,8 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
579
696
  operation,
580
697
  recordId,
581
698
  recordData,
699
+ isInteractiveMode,
700
+ draftId,
582
701
  ]
583
702
  );
584
703
 
@@ -645,10 +764,13 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
645
764
  // COMPUTED PROPERTIES
646
765
  // ============================================================
647
766
 
767
+ // Loading state includes interactive mode draft creation
648
768
  const isLoadingInitialData =
649
- isLoadingSchema || (operation === "update" && isLoadingRecord);
769
+ isLoadingSchema ||
770
+ (operation === "update" && isLoadingRecord) ||
771
+ (isInteractiveMode && operation === "create" && isCreatingDraft);
650
772
  const isLoading = isLoadingInitialData || isSubmitting;
651
- const loadError = schemaError || recordError;
773
+ const loadError = schemaError || recordError || draftError;
652
774
  const hasError = !!loadError;
653
775
 
654
776
  const computedFields = useMemo<Array<keyof T>>(
@@ -809,6 +931,10 @@ export function useForm<T extends Record<string, any> = Record<string, any>>(
809
931
  isLoadingRecord,
810
932
  isLoading,
811
933
 
934
+ // Interactive mode state
935
+ draftId,
936
+ isCreatingDraft,
937
+
812
938
  // Error handling
813
939
  loadError: loadError as Error | null,
814
940
  hasError,