@informedai/react 0.2.6 → 0.3.0

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.mts CHANGED
@@ -76,7 +76,24 @@ interface InformedAssistantConfig {
76
76
  documentTypeId: string;
77
77
  /** API base URL (defaults to https://api.informedassistant.ai/api/v1) */
78
78
  apiUrl?: string;
79
- /** Optional: Existing session ID to resume */
79
+ /**
80
+ * External ID for idempotency - your object's ID in your system.
81
+ * If provided, the widget will find or create a document linked to this ID.
82
+ * This allows resuming work on the same document across sessions.
83
+ */
84
+ externalId?: string;
85
+ /**
86
+ * Your current field values to sync before starting.
87
+ * Since you're the source of truth, this data will be written to our document.
88
+ * Use this to ensure the assistant sees your latest data.
89
+ */
90
+ initialData?: Record<string, unknown>;
91
+ /**
92
+ * Whether to persist sessions in localStorage for resumption.
93
+ * Defaults to true when externalId is provided, false otherwise.
94
+ */
95
+ persistSession?: boolean;
96
+ /** Optional: Existing session ID to resume (overrides localStorage) */
80
97
  sessionId?: string;
81
98
  /** Callback when widget is ready with document type schema */
82
99
  onReady?: (context: WidgetReadyContext) => void;
@@ -156,6 +173,7 @@ interface InformedAIContextValue {
156
173
  sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
157
174
  applyPendingValue: () => Promise<void>;
158
175
  skipTask: () => Promise<void>;
176
+ startNewSession: () => Promise<void>;
159
177
  clearError: () => void;
160
178
  }
161
179
  declare function useInformedAI(): InformedAIContextValue;
@@ -213,9 +231,15 @@ declare class InformedAIClient {
213
231
  private request;
214
232
  /**
215
233
  * Create a new session for a document type.
216
- * This automatically creates a document with default values.
234
+ *
235
+ * @param documentTypeId - The document type to create a session for
236
+ * @param options.externalId - Your object's ID for idempotency (finds or creates document)
237
+ * @param options.initialData - Your current field values to sync (you're the source of truth)
217
238
  */
218
- createSession(documentTypeId: string): Promise<CreateSessionResponse>;
239
+ createSession(documentTypeId: string, options?: {
240
+ externalId?: string;
241
+ initialData?: Record<string, unknown>;
242
+ }): Promise<CreateSessionResponse>;
219
243
  /**
220
244
  * Get an existing session.
221
245
  */
package/dist/index.d.ts CHANGED
@@ -76,7 +76,24 @@ interface InformedAssistantConfig {
76
76
  documentTypeId: string;
77
77
  /** API base URL (defaults to https://api.informedassistant.ai/api/v1) */
78
78
  apiUrl?: string;
79
- /** Optional: Existing session ID to resume */
79
+ /**
80
+ * External ID for idempotency - your object's ID in your system.
81
+ * If provided, the widget will find or create a document linked to this ID.
82
+ * This allows resuming work on the same document across sessions.
83
+ */
84
+ externalId?: string;
85
+ /**
86
+ * Your current field values to sync before starting.
87
+ * Since you're the source of truth, this data will be written to our document.
88
+ * Use this to ensure the assistant sees your latest data.
89
+ */
90
+ initialData?: Record<string, unknown>;
91
+ /**
92
+ * Whether to persist sessions in localStorage for resumption.
93
+ * Defaults to true when externalId is provided, false otherwise.
94
+ */
95
+ persistSession?: boolean;
96
+ /** Optional: Existing session ID to resume (overrides localStorage) */
80
97
  sessionId?: string;
81
98
  /** Callback when widget is ready with document type schema */
82
99
  onReady?: (context: WidgetReadyContext) => void;
@@ -156,6 +173,7 @@ interface InformedAIContextValue {
156
173
  sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
157
174
  applyPendingValue: () => Promise<void>;
158
175
  skipTask: () => Promise<void>;
176
+ startNewSession: () => Promise<void>;
159
177
  clearError: () => void;
160
178
  }
161
179
  declare function useInformedAI(): InformedAIContextValue;
@@ -213,9 +231,15 @@ declare class InformedAIClient {
213
231
  private request;
214
232
  /**
215
233
  * Create a new session for a document type.
216
- * This automatically creates a document with default values.
234
+ *
235
+ * @param documentTypeId - The document type to create a session for
236
+ * @param options.externalId - Your object's ID for idempotency (finds or creates document)
237
+ * @param options.initialData - Your current field values to sync (you're the source of truth)
217
238
  */
218
- createSession(documentTypeId: string): Promise<CreateSessionResponse>;
239
+ createSession(documentTypeId: string, options?: {
240
+ externalId?: string;
241
+ initialData?: Record<string, unknown>;
242
+ }): Promise<CreateSessionResponse>;
219
243
  /**
220
244
  * Get an existing session.
221
245
  */
package/dist/index.js CHANGED
@@ -67,12 +67,19 @@ var InformedAIClient = class {
67
67
  // ========================================================================
68
68
  /**
69
69
  * Create a new session for a document type.
70
- * This automatically creates a document with default values.
70
+ *
71
+ * @param documentTypeId - The document type to create a session for
72
+ * @param options.externalId - Your object's ID for idempotency (finds or creates document)
73
+ * @param options.initialData - Your current field values to sync (you're the source of truth)
71
74
  */
72
- async createSession(documentTypeId) {
75
+ async createSession(documentTypeId, options) {
73
76
  return this.request("/widget/sessions", {
74
77
  method: "POST",
75
- body: JSON.stringify({ documentTypeId })
78
+ body: JSON.stringify({
79
+ documentTypeId,
80
+ externalId: options?.externalId,
81
+ initialData: options?.initialData
82
+ })
76
83
  });
77
84
  }
78
85
  /**
@@ -186,6 +193,7 @@ var InformedAIClient = class {
186
193
 
187
194
  // src/context/InformedAIContext.tsx
188
195
  var import_jsx_runtime = require("react/jsx-runtime");
196
+ var getStorageKey = (documentTypeId, externalId) => `informedai_session_${documentTypeId}${externalId ? `_${externalId}` : ""}`;
189
197
  var InformedAIContext = (0, import_react.createContext)(null);
190
198
  function useInformedAI() {
191
199
  const context = (0, import_react.useContext)(InformedAIContext);
@@ -203,41 +211,93 @@ function InformedAIProvider({ config, children }) {
203
211
  const [error, setError] = (0, import_react.useState)(null);
204
212
  const [streamingContent, setStreamingContent] = (0, import_react.useState)("");
205
213
  const clientRef = (0, import_react.useRef)(null);
214
+ const initRef = (0, import_react.useRef)(false);
215
+ const shouldPersist = config.persistSession ?? !!config.externalId;
216
+ const storageKey = getStorageKey(config.documentTypeId, config.externalId);
206
217
  (0, import_react.useEffect)(() => {
207
218
  clientRef.current = new InformedAIClient(config.apiUrl);
208
219
  }, [config.apiUrl]);
220
+ const createNewSession = (0, import_react.useCallback)(async () => {
221
+ if (!clientRef.current) return null;
222
+ const result = await clientRef.current.createSession(
223
+ config.documentTypeId,
224
+ {
225
+ externalId: config.externalId,
226
+ initialData: config.initialData
227
+ }
228
+ );
229
+ const dt = {
230
+ id: result.documentType.id,
231
+ name: result.documentType.name,
232
+ displayName: result.documentType.displayName,
233
+ schema: result.documentType.schema,
234
+ workspaceId: "",
235
+ taskConfigs: {},
236
+ createdAt: "",
237
+ updatedAt: ""
238
+ };
239
+ if (shouldPersist && typeof window !== "undefined") {
240
+ try {
241
+ localStorage.setItem(storageKey, result.session.id);
242
+ } catch (e) {
243
+ }
244
+ }
245
+ return { session: result.session, document: result.document, documentType: dt };
246
+ }, [config.documentTypeId, config.externalId, config.initialData, shouldPersist, storageKey]);
209
247
  (0, import_react.useEffect)(() => {
248
+ if (initRef.current) return;
249
+ initRef.current = true;
210
250
  async function initialize() {
211
251
  if (!clientRef.current) return;
212
252
  try {
213
253
  setIsLoading(true);
214
254
  setError(null);
215
- let sess;
255
+ let sess = null;
216
256
  let doc = null;
217
257
  let dt = null;
218
- if (config.sessionId) {
219
- sess = await clientRef.current.getSession(config.sessionId);
258
+ let sessionIdToResume = config.sessionId;
259
+ if (!sessionIdToResume && shouldPersist && typeof window !== "undefined") {
260
+ try {
261
+ sessionIdToResume = localStorage.getItem(storageKey) || void 0;
262
+ } catch (e) {
263
+ }
264
+ }
265
+ if (sessionIdToResume) {
266
+ try {
267
+ sess = await clientRef.current.getSession(sessionIdToResume);
268
+ } catch (e) {
269
+ console.warn("Failed to resume session, creating new one");
270
+ if (shouldPersist && typeof window !== "undefined") {
271
+ try {
272
+ localStorage.removeItem(storageKey);
273
+ } catch (e2) {
274
+ }
275
+ }
276
+ }
277
+ }
278
+ if (!sess) {
279
+ const result = await createNewSession();
280
+ if (result) {
281
+ sess = result.session;
282
+ doc = result.document;
283
+ dt = result.documentType;
284
+ }
220
285
  } else {
221
- const result = await clientRef.current.createSession(config.documentTypeId);
222
- sess = result.session;
223
- doc = result.document;
224
- dt = {
225
- id: result.documentType.id,
226
- name: result.documentType.name,
227
- displayName: result.documentType.displayName,
228
- schema: result.documentType.schema,
229
- workspaceId: "",
230
- taskConfigs: {},
231
- createdAt: "",
232
- updatedAt: ""
233
- };
286
+ const result = await createNewSession();
287
+ if (result) {
288
+ sess = result.session;
289
+ doc = result.document;
290
+ dt = result.documentType;
291
+ }
234
292
  }
235
- setSession(sess);
236
- if (doc) setDocument(doc);
237
- if (dt) setDocumentType(dt);
238
- config.onSessionChange?.(sess);
239
- if (doc && dt) {
240
- config.onReady?.({ session: sess, document: doc, documentType: dt });
293
+ if (sess) {
294
+ setSession(sess);
295
+ if (doc) setDocument(doc);
296
+ if (dt) setDocumentType(dt);
297
+ config.onSessionChange?.(sess);
298
+ if (doc && dt) {
299
+ config.onReady?.({ session: sess, document: doc, documentType: dt });
300
+ }
241
301
  }
242
302
  } catch (err) {
243
303
  const error2 = err instanceof Error ? err : new Error("Initialization failed");
@@ -248,7 +308,7 @@ function InformedAIProvider({ config, children }) {
248
308
  }
249
309
  }
250
310
  initialize();
251
- }, [config.documentTypeId, config.sessionId]);
311
+ }, [config.documentTypeId, config.sessionId, config.externalId]);
252
312
  const handleSSEEvent = (0, import_react.useCallback)((event) => {
253
313
  if (event.type === "content" && event.content) {
254
314
  setStreamingContent((prev) => prev + event.content);
@@ -330,6 +390,37 @@ function InformedAIProvider({ config, children }) {
330
390
  config.onError?.(error2);
331
391
  }
332
392
  }, [session, config]);
393
+ const startNewSession = (0, import_react.useCallback)(async () => {
394
+ if (!clientRef.current) return;
395
+ try {
396
+ setIsLoading(true);
397
+ setError(null);
398
+ if (shouldPersist && typeof window !== "undefined") {
399
+ try {
400
+ localStorage.removeItem(storageKey);
401
+ } catch (e) {
402
+ }
403
+ }
404
+ const result = await createNewSession();
405
+ if (result) {
406
+ setSession(result.session);
407
+ setDocument(result.document);
408
+ setDocumentType(result.documentType);
409
+ config.onSessionChange?.(result.session);
410
+ config.onReady?.({
411
+ session: result.session,
412
+ document: result.document,
413
+ documentType: result.documentType
414
+ });
415
+ }
416
+ } catch (err) {
417
+ const error2 = err instanceof Error ? err : new Error("Failed to start new session");
418
+ setError(error2);
419
+ config.onError?.(error2);
420
+ } finally {
421
+ setIsLoading(false);
422
+ }
423
+ }, [createNewSession, shouldPersist, storageKey, config]);
333
424
  const clearError = (0, import_react.useCallback)(() => {
334
425
  setError(null);
335
426
  }, []);
@@ -345,6 +436,7 @@ function InformedAIProvider({ config, children }) {
345
436
  sendQuickAction,
346
437
  applyPendingValue,
347
438
  skipTask,
439
+ startNewSession,
348
440
  clearError
349
441
  };
350
442
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InformedAIContext.Provider, { value, children });
package/dist/index.mjs CHANGED
@@ -37,12 +37,19 @@ var InformedAIClient = class {
37
37
  // ========================================================================
38
38
  /**
39
39
  * Create a new session for a document type.
40
- * This automatically creates a document with default values.
40
+ *
41
+ * @param documentTypeId - The document type to create a session for
42
+ * @param options.externalId - Your object's ID for idempotency (finds or creates document)
43
+ * @param options.initialData - Your current field values to sync (you're the source of truth)
41
44
  */
42
- async createSession(documentTypeId) {
45
+ async createSession(documentTypeId, options) {
43
46
  return this.request("/widget/sessions", {
44
47
  method: "POST",
45
- body: JSON.stringify({ documentTypeId })
48
+ body: JSON.stringify({
49
+ documentTypeId,
50
+ externalId: options?.externalId,
51
+ initialData: options?.initialData
52
+ })
46
53
  });
47
54
  }
48
55
  /**
@@ -156,6 +163,7 @@ var InformedAIClient = class {
156
163
 
157
164
  // src/context/InformedAIContext.tsx
158
165
  import { jsx } from "react/jsx-runtime";
166
+ var getStorageKey = (documentTypeId, externalId) => `informedai_session_${documentTypeId}${externalId ? `_${externalId}` : ""}`;
159
167
  var InformedAIContext = createContext(null);
160
168
  function useInformedAI() {
161
169
  const context = useContext(InformedAIContext);
@@ -173,41 +181,93 @@ function InformedAIProvider({ config, children }) {
173
181
  const [error, setError] = useState(null);
174
182
  const [streamingContent, setStreamingContent] = useState("");
175
183
  const clientRef = useRef(null);
184
+ const initRef = useRef(false);
185
+ const shouldPersist = config.persistSession ?? !!config.externalId;
186
+ const storageKey = getStorageKey(config.documentTypeId, config.externalId);
176
187
  useEffect(() => {
177
188
  clientRef.current = new InformedAIClient(config.apiUrl);
178
189
  }, [config.apiUrl]);
190
+ const createNewSession = useCallback(async () => {
191
+ if (!clientRef.current) return null;
192
+ const result = await clientRef.current.createSession(
193
+ config.documentTypeId,
194
+ {
195
+ externalId: config.externalId,
196
+ initialData: config.initialData
197
+ }
198
+ );
199
+ const dt = {
200
+ id: result.documentType.id,
201
+ name: result.documentType.name,
202
+ displayName: result.documentType.displayName,
203
+ schema: result.documentType.schema,
204
+ workspaceId: "",
205
+ taskConfigs: {},
206
+ createdAt: "",
207
+ updatedAt: ""
208
+ };
209
+ if (shouldPersist && typeof window !== "undefined") {
210
+ try {
211
+ localStorage.setItem(storageKey, result.session.id);
212
+ } catch (e) {
213
+ }
214
+ }
215
+ return { session: result.session, document: result.document, documentType: dt };
216
+ }, [config.documentTypeId, config.externalId, config.initialData, shouldPersist, storageKey]);
179
217
  useEffect(() => {
218
+ if (initRef.current) return;
219
+ initRef.current = true;
180
220
  async function initialize() {
181
221
  if (!clientRef.current) return;
182
222
  try {
183
223
  setIsLoading(true);
184
224
  setError(null);
185
- let sess;
225
+ let sess = null;
186
226
  let doc = null;
187
227
  let dt = null;
188
- if (config.sessionId) {
189
- sess = await clientRef.current.getSession(config.sessionId);
228
+ let sessionIdToResume = config.sessionId;
229
+ if (!sessionIdToResume && shouldPersist && typeof window !== "undefined") {
230
+ try {
231
+ sessionIdToResume = localStorage.getItem(storageKey) || void 0;
232
+ } catch (e) {
233
+ }
234
+ }
235
+ if (sessionIdToResume) {
236
+ try {
237
+ sess = await clientRef.current.getSession(sessionIdToResume);
238
+ } catch (e) {
239
+ console.warn("Failed to resume session, creating new one");
240
+ if (shouldPersist && typeof window !== "undefined") {
241
+ try {
242
+ localStorage.removeItem(storageKey);
243
+ } catch (e2) {
244
+ }
245
+ }
246
+ }
247
+ }
248
+ if (!sess) {
249
+ const result = await createNewSession();
250
+ if (result) {
251
+ sess = result.session;
252
+ doc = result.document;
253
+ dt = result.documentType;
254
+ }
190
255
  } else {
191
- const result = await clientRef.current.createSession(config.documentTypeId);
192
- sess = result.session;
193
- doc = result.document;
194
- dt = {
195
- id: result.documentType.id,
196
- name: result.documentType.name,
197
- displayName: result.documentType.displayName,
198
- schema: result.documentType.schema,
199
- workspaceId: "",
200
- taskConfigs: {},
201
- createdAt: "",
202
- updatedAt: ""
203
- };
256
+ const result = await createNewSession();
257
+ if (result) {
258
+ sess = result.session;
259
+ doc = result.document;
260
+ dt = result.documentType;
261
+ }
204
262
  }
205
- setSession(sess);
206
- if (doc) setDocument(doc);
207
- if (dt) setDocumentType(dt);
208
- config.onSessionChange?.(sess);
209
- if (doc && dt) {
210
- config.onReady?.({ session: sess, document: doc, documentType: dt });
263
+ if (sess) {
264
+ setSession(sess);
265
+ if (doc) setDocument(doc);
266
+ if (dt) setDocumentType(dt);
267
+ config.onSessionChange?.(sess);
268
+ if (doc && dt) {
269
+ config.onReady?.({ session: sess, document: doc, documentType: dt });
270
+ }
211
271
  }
212
272
  } catch (err) {
213
273
  const error2 = err instanceof Error ? err : new Error("Initialization failed");
@@ -218,7 +278,7 @@ function InformedAIProvider({ config, children }) {
218
278
  }
219
279
  }
220
280
  initialize();
221
- }, [config.documentTypeId, config.sessionId]);
281
+ }, [config.documentTypeId, config.sessionId, config.externalId]);
222
282
  const handleSSEEvent = useCallback((event) => {
223
283
  if (event.type === "content" && event.content) {
224
284
  setStreamingContent((prev) => prev + event.content);
@@ -300,6 +360,37 @@ function InformedAIProvider({ config, children }) {
300
360
  config.onError?.(error2);
301
361
  }
302
362
  }, [session, config]);
363
+ const startNewSession = useCallback(async () => {
364
+ if (!clientRef.current) return;
365
+ try {
366
+ setIsLoading(true);
367
+ setError(null);
368
+ if (shouldPersist && typeof window !== "undefined") {
369
+ try {
370
+ localStorage.removeItem(storageKey);
371
+ } catch (e) {
372
+ }
373
+ }
374
+ const result = await createNewSession();
375
+ if (result) {
376
+ setSession(result.session);
377
+ setDocument(result.document);
378
+ setDocumentType(result.documentType);
379
+ config.onSessionChange?.(result.session);
380
+ config.onReady?.({
381
+ session: result.session,
382
+ document: result.document,
383
+ documentType: result.documentType
384
+ });
385
+ }
386
+ } catch (err) {
387
+ const error2 = err instanceof Error ? err : new Error("Failed to start new session");
388
+ setError(error2);
389
+ config.onError?.(error2);
390
+ } finally {
391
+ setIsLoading(false);
392
+ }
393
+ }, [createNewSession, shouldPersist, storageKey, config]);
303
394
  const clearError = useCallback(() => {
304
395
  setError(null);
305
396
  }, []);
@@ -315,6 +406,7 @@ function InformedAIProvider({ config, children }) {
315
406
  sendQuickAction,
316
407
  applyPendingValue,
317
408
  skipTask,
409
+ startNewSession,
318
410
  clearError
319
411
  };
320
412
  return /* @__PURE__ */ jsx(InformedAIContext.Provider, { value, children });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@informedai/react",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "React SDK for InformedAI Assistant - AI-powered content creation widget",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",