@informedai/react 0.2.5 → 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,8 +76,27 @@ 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;
98
+ /** Callback when widget is ready with document type schema */
99
+ onReady?: (context: WidgetReadyContext) => void;
81
100
  /** Callback when a field value is applied */
82
101
  onFieldApply?: (fieldName: string, value: unknown) => void;
83
102
  /** Callback when session state changes */
@@ -91,6 +110,11 @@ interface InformedAssistantConfig {
91
110
  /** Initial collapsed state */
92
111
  defaultCollapsed?: boolean;
93
112
  }
113
+ interface WidgetReadyContext {
114
+ session: Session;
115
+ document: Document;
116
+ documentType: DocumentType;
117
+ }
94
118
  interface WidgetTheme {
95
119
  /** Primary accent color */
96
120
  primaryColor: string;
@@ -149,6 +173,7 @@ interface InformedAIContextValue {
149
173
  sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
150
174
  applyPendingValue: () => Promise<void>;
151
175
  skipTask: () => Promise<void>;
176
+ startNewSession: () => Promise<void>;
152
177
  clearError: () => void;
153
178
  }
154
179
  declare function useInformedAI(): InformedAIContextValue;
@@ -206,9 +231,15 @@ declare class InformedAIClient {
206
231
  private request;
207
232
  /**
208
233
  * Create a new session for a document type.
209
- * 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)
210
238
  */
211
- createSession(documentTypeId: string): Promise<CreateSessionResponse>;
239
+ createSession(documentTypeId: string, options?: {
240
+ externalId?: string;
241
+ initialData?: Record<string, unknown>;
242
+ }): Promise<CreateSessionResponse>;
212
243
  /**
213
244
  * Get an existing session.
214
245
  */
@@ -234,4 +265,4 @@ declare class InformedAIClient {
234
265
  private processSSEStream;
235
266
  }
236
267
 
237
- export { type ChatMessage, type Document, type DocumentType, type DocumentTypeSchema, type FieldDefinition, InformedAIClient, InformedAIProvider, InformedAssistant, type InformedAssistantConfig, type QuickAction, type SSEEvent, type Session, type TaskConfig, type TaskState, type UseSessionReturn, type WidgetMessage, type WidgetTheme, useInformedAI, useSession };
268
+ export { type ChatMessage, type Document, type DocumentType, type DocumentTypeSchema, type FieldDefinition, InformedAIClient, InformedAIProvider, InformedAssistant, type InformedAssistantConfig, type QuickAction, type SSEEvent, type Session, type TaskConfig, type TaskState, type UseSessionReturn, type WidgetMessage, type WidgetReadyContext, type WidgetTheme, useInformedAI, useSession };
package/dist/index.d.ts CHANGED
@@ -76,8 +76,27 @@ 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;
98
+ /** Callback when widget is ready with document type schema */
99
+ onReady?: (context: WidgetReadyContext) => void;
81
100
  /** Callback when a field value is applied */
82
101
  onFieldApply?: (fieldName: string, value: unknown) => void;
83
102
  /** Callback when session state changes */
@@ -91,6 +110,11 @@ interface InformedAssistantConfig {
91
110
  /** Initial collapsed state */
92
111
  defaultCollapsed?: boolean;
93
112
  }
113
+ interface WidgetReadyContext {
114
+ session: Session;
115
+ document: Document;
116
+ documentType: DocumentType;
117
+ }
94
118
  interface WidgetTheme {
95
119
  /** Primary accent color */
96
120
  primaryColor: string;
@@ -149,6 +173,7 @@ interface InformedAIContextValue {
149
173
  sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
150
174
  applyPendingValue: () => Promise<void>;
151
175
  skipTask: () => Promise<void>;
176
+ startNewSession: () => Promise<void>;
152
177
  clearError: () => void;
153
178
  }
154
179
  declare function useInformedAI(): InformedAIContextValue;
@@ -206,9 +231,15 @@ declare class InformedAIClient {
206
231
  private request;
207
232
  /**
208
233
  * Create a new session for a document type.
209
- * 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)
210
238
  */
211
- createSession(documentTypeId: string): Promise<CreateSessionResponse>;
239
+ createSession(documentTypeId: string, options?: {
240
+ externalId?: string;
241
+ initialData?: Record<string, unknown>;
242
+ }): Promise<CreateSessionResponse>;
212
243
  /**
213
244
  * Get an existing session.
214
245
  */
@@ -234,4 +265,4 @@ declare class InformedAIClient {
234
265
  private processSSEStream;
235
266
  }
236
267
 
237
- export { type ChatMessage, type Document, type DocumentType, type DocumentTypeSchema, type FieldDefinition, InformedAIClient, InformedAIProvider, InformedAssistant, type InformedAssistantConfig, type QuickAction, type SSEEvent, type Session, type TaskConfig, type TaskState, type UseSessionReturn, type WidgetMessage, type WidgetTheme, useInformedAI, useSession };
268
+ export { type ChatMessage, type Document, type DocumentType, type DocumentTypeSchema, type FieldDefinition, InformedAIClient, InformedAIProvider, InformedAssistant, type InformedAssistantConfig, type QuickAction, type SSEEvent, type Session, type TaskConfig, type TaskState, type UseSessionReturn, type WidgetMessage, type WidgetReadyContext, type WidgetTheme, useInformedAI, useSession };
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,39 +211,94 @@ 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
+ }
292
+ }
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
+ }
234
301
  }
235
- setSession(sess);
236
- if (doc) setDocument(doc);
237
- if (dt) setDocumentType(dt);
238
- config.onSessionChange?.(sess);
239
302
  } catch (err) {
240
303
  const error2 = err instanceof Error ? err : new Error("Initialization failed");
241
304
  setError(error2);
@@ -245,7 +308,7 @@ function InformedAIProvider({ config, children }) {
245
308
  }
246
309
  }
247
310
  initialize();
248
- }, [config.documentTypeId, config.sessionId]);
311
+ }, [config.documentTypeId, config.sessionId, config.externalId]);
249
312
  const handleSSEEvent = (0, import_react.useCallback)((event) => {
250
313
  if (event.type === "content" && event.content) {
251
314
  setStreamingContent((prev) => prev + event.content);
@@ -327,6 +390,37 @@ function InformedAIProvider({ config, children }) {
327
390
  config.onError?.(error2);
328
391
  }
329
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]);
330
424
  const clearError = (0, import_react.useCallback)(() => {
331
425
  setError(null);
332
426
  }, []);
@@ -342,6 +436,7 @@ function InformedAIProvider({ config, children }) {
342
436
  sendQuickAction,
343
437
  applyPendingValue,
344
438
  skipTask,
439
+ startNewSession,
345
440
  clearError
346
441
  };
347
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,39 +181,94 @@ 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
+ }
262
+ }
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
+ }
204
271
  }
205
- setSession(sess);
206
- if (doc) setDocument(doc);
207
- if (dt) setDocumentType(dt);
208
- config.onSessionChange?.(sess);
209
272
  } catch (err) {
210
273
  const error2 = err instanceof Error ? err : new Error("Initialization failed");
211
274
  setError(error2);
@@ -215,7 +278,7 @@ function InformedAIProvider({ config, children }) {
215
278
  }
216
279
  }
217
280
  initialize();
218
- }, [config.documentTypeId, config.sessionId]);
281
+ }, [config.documentTypeId, config.sessionId, config.externalId]);
219
282
  const handleSSEEvent = useCallback((event) => {
220
283
  if (event.type === "content" && event.content) {
221
284
  setStreamingContent((prev) => prev + event.content);
@@ -297,6 +360,37 @@ function InformedAIProvider({ config, children }) {
297
360
  config.onError?.(error2);
298
361
  }
299
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]);
300
394
  const clearError = useCallback(() => {
301
395
  setError(null);
302
396
  }, []);
@@ -312,6 +406,7 @@ function InformedAIProvider({ config, children }) {
312
406
  sendQuickAction,
313
407
  applyPendingValue,
314
408
  skipTask,
409
+ startNewSession,
315
410
  clearError
316
411
  };
317
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.5",
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",