@informedai/react 0.3.0 → 0.4.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
@@ -11,6 +11,7 @@ interface Session {
11
11
  taskStates: Record<string, TaskState>;
12
12
  activeTask: string | null;
13
13
  aiConversations: Record<string, ChatMessage[]>;
14
+ status: 'active' | 'ended' | 'abandoned';
14
15
  }
15
16
  interface WidgetMessage {
16
17
  id: string;
@@ -133,6 +134,20 @@ interface SSEEvent {
133
134
  session?: Session;
134
135
  error?: string;
135
136
  }
137
+ /**
138
+ * Response from GET /widget/sessions/:id
139
+ * Includes the session with embedded document and documentType
140
+ */
141
+ interface GetSessionResponse extends Session {
142
+ document: Document & {
143
+ documentType: {
144
+ id: string;
145
+ name: string;
146
+ displayName: string;
147
+ schema: DocumentTypeSchema;
148
+ };
149
+ };
150
+ }
136
151
  interface UseSessionReturn {
137
152
  session: Session | null;
138
153
  isLoading: boolean;
@@ -174,6 +189,7 @@ interface InformedAIContextValue {
174
189
  applyPendingValue: () => Promise<void>;
175
190
  skipTask: () => Promise<void>;
176
191
  startNewSession: () => Promise<void>;
192
+ endSession: () => Promise<void>;
177
193
  clearError: () => void;
178
194
  }
179
195
  declare function useInformedAI(): InformedAIContextValue;
@@ -241,9 +257,9 @@ declare class InformedAIClient {
241
257
  initialData?: Record<string, unknown>;
242
258
  }): Promise<CreateSessionResponse>;
243
259
  /**
244
- * Get an existing session.
260
+ * Get an existing session with its document and documentType.
245
261
  */
246
- getSession(id: string): Promise<Session>;
262
+ getSession(id: string): Promise<GetSessionResponse>;
247
263
  /**
248
264
  * Send a message to the session with SSE streaming.
249
265
  */
@@ -262,6 +278,26 @@ declare class InformedAIClient {
262
278
  * Skip the active task.
263
279
  */
264
280
  skipTask(sessionId: string): Promise<Session>;
281
+ /**
282
+ * Send a heartbeat to keep the session active.
283
+ * Call this every 30 seconds while the session is in use.
284
+ */
285
+ sendHeartbeat(sessionId: string): Promise<void>;
286
+ /**
287
+ * Resume an abandoned session with a new chatbot.
288
+ * Preserves conversation history.
289
+ */
290
+ resumeSession(sessionId: string): Promise<Session>;
291
+ /**
292
+ * Explicitly end a session and release resources.
293
+ * Conversation history is preserved but session cannot be resumed.
294
+ */
295
+ endSession(sessionId: string): Promise<void>;
296
+ /**
297
+ * Send end session via sendBeacon (for beforeunload).
298
+ * Returns true if sendBeacon was used, false if fetch fallback was attempted.
299
+ */
300
+ endSessionBeacon(sessionId: string): boolean;
265
301
  private processSSEStream;
266
302
  }
267
303
 
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ interface Session {
11
11
  taskStates: Record<string, TaskState>;
12
12
  activeTask: string | null;
13
13
  aiConversations: Record<string, ChatMessage[]>;
14
+ status: 'active' | 'ended' | 'abandoned';
14
15
  }
15
16
  interface WidgetMessage {
16
17
  id: string;
@@ -133,6 +134,20 @@ interface SSEEvent {
133
134
  session?: Session;
134
135
  error?: string;
135
136
  }
137
+ /**
138
+ * Response from GET /widget/sessions/:id
139
+ * Includes the session with embedded document and documentType
140
+ */
141
+ interface GetSessionResponse extends Session {
142
+ document: Document & {
143
+ documentType: {
144
+ id: string;
145
+ name: string;
146
+ displayName: string;
147
+ schema: DocumentTypeSchema;
148
+ };
149
+ };
150
+ }
136
151
  interface UseSessionReturn {
137
152
  session: Session | null;
138
153
  isLoading: boolean;
@@ -174,6 +189,7 @@ interface InformedAIContextValue {
174
189
  applyPendingValue: () => Promise<void>;
175
190
  skipTask: () => Promise<void>;
176
191
  startNewSession: () => Promise<void>;
192
+ endSession: () => Promise<void>;
177
193
  clearError: () => void;
178
194
  }
179
195
  declare function useInformedAI(): InformedAIContextValue;
@@ -241,9 +257,9 @@ declare class InformedAIClient {
241
257
  initialData?: Record<string, unknown>;
242
258
  }): Promise<CreateSessionResponse>;
243
259
  /**
244
- * Get an existing session.
260
+ * Get an existing session with its document and documentType.
245
261
  */
246
- getSession(id: string): Promise<Session>;
262
+ getSession(id: string): Promise<GetSessionResponse>;
247
263
  /**
248
264
  * Send a message to the session with SSE streaming.
249
265
  */
@@ -262,6 +278,26 @@ declare class InformedAIClient {
262
278
  * Skip the active task.
263
279
  */
264
280
  skipTask(sessionId: string): Promise<Session>;
281
+ /**
282
+ * Send a heartbeat to keep the session active.
283
+ * Call this every 30 seconds while the session is in use.
284
+ */
285
+ sendHeartbeat(sessionId: string): Promise<void>;
286
+ /**
287
+ * Resume an abandoned session with a new chatbot.
288
+ * Preserves conversation history.
289
+ */
290
+ resumeSession(sessionId: string): Promise<Session>;
291
+ /**
292
+ * Explicitly end a session and release resources.
293
+ * Conversation history is preserved but session cannot be resumed.
294
+ */
295
+ endSession(sessionId: string): Promise<void>;
296
+ /**
297
+ * Send end session via sendBeacon (for beforeunload).
298
+ * Returns true if sendBeacon was used, false if fetch fallback was attempted.
299
+ */
300
+ endSessionBeacon(sessionId: string): boolean;
265
301
  private processSSEStream;
266
302
  }
267
303
 
package/dist/index.js CHANGED
@@ -83,7 +83,7 @@ var InformedAIClient = class {
83
83
  });
84
84
  }
85
85
  /**
86
- * Get an existing session.
86
+ * Get an existing session with its document and documentType.
87
87
  */
88
88
  async getSession(id) {
89
89
  return this.request(`/widget/sessions/${id}`);
@@ -151,6 +151,51 @@ var InformedAIClient = class {
151
151
  method: "POST"
152
152
  });
153
153
  }
154
+ /**
155
+ * Send a heartbeat to keep the session active.
156
+ * Call this every 30 seconds while the session is in use.
157
+ */
158
+ async sendHeartbeat(sessionId) {
159
+ await this.request(`/widget/sessions/${sessionId}/heartbeat`, {
160
+ method: "POST"
161
+ });
162
+ }
163
+ /**
164
+ * Resume an abandoned session with a new chatbot.
165
+ * Preserves conversation history.
166
+ */
167
+ async resumeSession(sessionId) {
168
+ return this.request(`/widget/sessions/${sessionId}/resume`, {
169
+ method: "POST"
170
+ });
171
+ }
172
+ /**
173
+ * Explicitly end a session and release resources.
174
+ * Conversation history is preserved but session cannot be resumed.
175
+ */
176
+ async endSession(sessionId) {
177
+ await this.request(`/widget/sessions/${sessionId}/end`, {
178
+ method: "POST"
179
+ });
180
+ }
181
+ /**
182
+ * Send end session via sendBeacon (for beforeunload).
183
+ * Returns true if sendBeacon was used, false if fetch fallback was attempted.
184
+ */
185
+ endSessionBeacon(sessionId) {
186
+ const url = `${this.apiUrl}/widget/sessions/${sessionId}/end`;
187
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
188
+ const blob = new Blob([JSON.stringify({})], { type: "application/json" });
189
+ return navigator.sendBeacon(url, blob);
190
+ }
191
+ fetch(url, {
192
+ method: "POST",
193
+ headers: this.getHeaders(),
194
+ keepalive: true
195
+ }).catch(() => {
196
+ });
197
+ return false;
198
+ }
154
199
  // ========================================================================
155
200
  // SSE Stream Processing
156
201
  // ========================================================================
@@ -194,6 +239,14 @@ var InformedAIClient = class {
194
239
  // src/context/InformedAIContext.tsx
195
240
  var import_jsx_runtime = require("react/jsx-runtime");
196
241
  var getStorageKey = (documentTypeId, externalId) => `informedai_session_${documentTypeId}${externalId ? `_${externalId}` : ""}`;
242
+ var HEARTBEAT_INTERVAL = 30 * 1e3;
243
+ function isSessionNotFoundError(error) {
244
+ if (error instanceof Error) {
245
+ const message = error.message.toLowerCase();
246
+ return message.includes("session not found") || message.includes("404") || message.includes("not found");
247
+ }
248
+ return false;
249
+ }
197
250
  var InformedAIContext = (0, import_react.createContext)(null);
198
251
  function useInformedAI() {
199
252
  const context = (0, import_react.useContext)(InformedAIContext);
@@ -212,6 +265,8 @@ function InformedAIProvider({ config, children }) {
212
265
  const [streamingContent, setStreamingContent] = (0, import_react.useState)("");
213
266
  const clientRef = (0, import_react.useRef)(null);
214
267
  const initRef = (0, import_react.useRef)(false);
268
+ const heartbeatIntervalRef = (0, import_react.useRef)(null);
269
+ const sessionIdRef = (0, import_react.useRef)(null);
215
270
  const shouldPersist = config.persistSession ?? !!config.externalId;
216
271
  const storageKey = getStorageKey(config.documentTypeId, config.externalId);
217
272
  (0, import_react.useEffect)(() => {
@@ -244,6 +299,45 @@ function InformedAIProvider({ config, children }) {
244
299
  }
245
300
  return { session: result.session, document: result.document, documentType: dt };
246
301
  }, [config.documentTypeId, config.externalId, config.initialData, shouldPersist, storageKey]);
302
+ const clearPersistedSession = (0, import_react.useCallback)(() => {
303
+ if (shouldPersist && typeof window !== "undefined") {
304
+ try {
305
+ localStorage.removeItem(storageKey);
306
+ } catch (e) {
307
+ }
308
+ }
309
+ }, [shouldPersist, storageKey]);
310
+ const handleSessionDeleted = (0, import_react.useCallback)(async () => {
311
+ console.warn("[InformedAI] Session was deleted, starting new session...");
312
+ clearPersistedSession();
313
+ if (heartbeatIntervalRef.current) {
314
+ clearInterval(heartbeatIntervalRef.current);
315
+ heartbeatIntervalRef.current = null;
316
+ }
317
+ sessionIdRef.current = null;
318
+ try {
319
+ const result = await createNewSession();
320
+ if (result) {
321
+ setSession(result.session);
322
+ setDocument(result.document);
323
+ setDocumentType(result.documentType);
324
+ setError(null);
325
+ config.onSessionChange?.(result.session);
326
+ config.onReady?.({
327
+ session: result.session,
328
+ document: result.document,
329
+ documentType: result.documentType
330
+ });
331
+ return true;
332
+ }
333
+ } catch (err) {
334
+ console.error("[InformedAI] Failed to create new session after deletion:", err);
335
+ const error2 = err instanceof Error ? err : new Error("Failed to recover session");
336
+ setError(error2);
337
+ config.onError?.(error2);
338
+ }
339
+ return false;
340
+ }, [createNewSession, clearPersistedSession, config]);
247
341
  (0, import_react.useEffect)(() => {
248
342
  if (initRef.current) return;
249
343
  initRef.current = true;
@@ -264,7 +358,20 @@ function InformedAIProvider({ config, children }) {
264
358
  }
265
359
  if (sessionIdToResume) {
266
360
  try {
267
- sess = await clientRef.current.getSession(sessionIdToResume);
361
+ const resumedSession = await clientRef.current.getSession(sessionIdToResume);
362
+ sess = resumedSession;
363
+ doc = resumedSession.document;
364
+ const embeddedDt = resumedSession.document.documentType;
365
+ dt = {
366
+ id: embeddedDt.id,
367
+ name: embeddedDt.name,
368
+ displayName: embeddedDt.displayName,
369
+ schema: embeddedDt.schema,
370
+ workspaceId: "",
371
+ taskConfigs: {},
372
+ createdAt: "",
373
+ updatedAt: ""
374
+ };
268
375
  } catch (e) {
269
376
  console.warn("Failed to resume session, creating new one");
270
377
  if (shouldPersist && typeof window !== "undefined") {
@@ -282,13 +389,6 @@ function InformedAIProvider({ config, children }) {
282
389
  doc = result.document;
283
390
  dt = result.documentType;
284
391
  }
285
- } else {
286
- const result = await createNewSession();
287
- if (result) {
288
- sess = result.session;
289
- doc = result.document;
290
- dt = result.documentType;
291
- }
292
392
  }
293
393
  if (sess) {
294
394
  setSession(sess);
@@ -309,6 +409,63 @@ function InformedAIProvider({ config, children }) {
309
409
  }
310
410
  initialize();
311
411
  }, [config.documentTypeId, config.sessionId, config.externalId]);
412
+ const startHeartbeat = (0, import_react.useCallback)((sessionId) => {
413
+ if (heartbeatIntervalRef.current) {
414
+ clearInterval(heartbeatIntervalRef.current);
415
+ }
416
+ sessionIdRef.current = sessionId;
417
+ clientRef.current?.sendHeartbeat(sessionId).catch((err) => {
418
+ console.warn("Heartbeat failed:", err);
419
+ });
420
+ heartbeatIntervalRef.current = setInterval(() => {
421
+ if (clientRef.current && sessionIdRef.current) {
422
+ clientRef.current.sendHeartbeat(sessionIdRef.current).catch((err) => {
423
+ console.warn("Heartbeat failed:", err);
424
+ });
425
+ }
426
+ }, HEARTBEAT_INTERVAL);
427
+ }, []);
428
+ const stopHeartbeat = (0, import_react.useCallback)(() => {
429
+ if (heartbeatIntervalRef.current) {
430
+ clearInterval(heartbeatIntervalRef.current);
431
+ heartbeatIntervalRef.current = null;
432
+ }
433
+ }, []);
434
+ (0, import_react.useEffect)(() => {
435
+ if (session?.id && session.status === "active") {
436
+ startHeartbeat(session.id);
437
+ } else {
438
+ stopHeartbeat();
439
+ }
440
+ return () => {
441
+ stopHeartbeat();
442
+ };
443
+ }, [session?.id, session?.status, startHeartbeat, stopHeartbeat]);
444
+ (0, import_react.useEffect)(() => {
445
+ if (typeof window === "undefined") return;
446
+ const handleVisibilityChange = () => {
447
+ if (window.document.hidden) {
448
+ stopHeartbeat();
449
+ } else if (session?.id && session.status === "active") {
450
+ startHeartbeat(session.id);
451
+ }
452
+ };
453
+ window.document.addEventListener("visibilitychange", handleVisibilityChange);
454
+ return () => {
455
+ window.document.removeEventListener("visibilitychange", handleVisibilityChange);
456
+ };
457
+ }, [session?.id, session?.status, startHeartbeat, stopHeartbeat]);
458
+ (0, import_react.useEffect)(() => {
459
+ const handleBeforeUnload = () => {
460
+ if (clientRef.current && sessionIdRef.current) {
461
+ clientRef.current.endSessionBeacon(sessionIdRef.current);
462
+ }
463
+ };
464
+ window.addEventListener("beforeunload", handleBeforeUnload);
465
+ return () => {
466
+ window.removeEventListener("beforeunload", handleBeforeUnload);
467
+ };
468
+ }, []);
312
469
  const handleSSEEvent = (0, import_react.useCallback)((event) => {
313
470
  if (event.type === "content" && event.content) {
314
471
  setStreamingContent((prev) => prev + event.content);
@@ -336,12 +493,17 @@ function InformedAIProvider({ config, children }) {
336
493
  setError(null);
337
494
  await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
338
495
  } catch (err) {
496
+ if (isSessionNotFoundError(err)) {
497
+ setIsStreaming(false);
498
+ await handleSessionDeleted();
499
+ return;
500
+ }
339
501
  const error2 = err instanceof Error ? err : new Error("Failed to send message");
340
502
  setError(error2);
341
503
  config.onError?.(error2);
342
504
  setIsStreaming(false);
343
505
  }
344
- }, [session, handleSSEEvent, config]);
506
+ }, [session, handleSSEEvent, handleSessionDeleted, config]);
345
507
  const sendQuickAction = (0, import_react.useCallback)(async (action) => {
346
508
  if (!clientRef.current || !session) return;
347
509
  try {
@@ -356,13 +518,18 @@ function InformedAIProvider({ config, children }) {
356
518
  setSession(newSession);
357
519
  config.onSessionChange?.(newSession);
358
520
  } catch (err) {
521
+ if (isSessionNotFoundError(err)) {
522
+ setIsStreaming(false);
523
+ await handleSessionDeleted();
524
+ return;
525
+ }
359
526
  const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
360
527
  setError(error2);
361
528
  config.onError?.(error2);
362
529
  } finally {
363
530
  setIsStreaming(false);
364
531
  }
365
- }, [session, handleSSEEvent, config]);
532
+ }, [session, handleSSEEvent, handleSessionDeleted, config]);
366
533
  const applyPendingValue = (0, import_react.useCallback)(async () => {
367
534
  if (!clientRef.current || !session) return;
368
535
  try {
@@ -372,11 +539,15 @@ function InformedAIProvider({ config, children }) {
372
539
  config.onSessionChange?.(result.session);
373
540
  config.onFieldApply?.(result.appliedField, result.appliedValue);
374
541
  } catch (err) {
542
+ if (isSessionNotFoundError(err)) {
543
+ await handleSessionDeleted();
544
+ return;
545
+ }
375
546
  const error2 = err instanceof Error ? err : new Error("Failed to apply value");
376
547
  setError(error2);
377
548
  config.onError?.(error2);
378
549
  }
379
- }, [session, config]);
550
+ }, [session, handleSessionDeleted, config]);
380
551
  const skipTask = (0, import_react.useCallback)(async () => {
381
552
  if (!clientRef.current || !session) return;
382
553
  try {
@@ -385,22 +556,21 @@ function InformedAIProvider({ config, children }) {
385
556
  setSession(newSession);
386
557
  config.onSessionChange?.(newSession);
387
558
  } catch (err) {
559
+ if (isSessionNotFoundError(err)) {
560
+ await handleSessionDeleted();
561
+ return;
562
+ }
388
563
  const error2 = err instanceof Error ? err : new Error("Failed to skip task");
389
564
  setError(error2);
390
565
  config.onError?.(error2);
391
566
  }
392
- }, [session, config]);
567
+ }, [session, handleSessionDeleted, config]);
393
568
  const startNewSession = (0, import_react.useCallback)(async () => {
394
569
  if (!clientRef.current) return;
395
570
  try {
396
571
  setIsLoading(true);
397
572
  setError(null);
398
- if (shouldPersist && typeof window !== "undefined") {
399
- try {
400
- localStorage.removeItem(storageKey);
401
- } catch (e) {
402
- }
403
- }
573
+ clearPersistedSession();
404
574
  const result = await createNewSession();
405
575
  if (result) {
406
576
  setSession(result.session);
@@ -420,7 +590,21 @@ function InformedAIProvider({ config, children }) {
420
590
  } finally {
421
591
  setIsLoading(false);
422
592
  }
423
- }, [createNewSession, shouldPersist, storageKey, config]);
593
+ }, [createNewSession, clearPersistedSession, config]);
594
+ const endSession = (0, import_react.useCallback)(async () => {
595
+ if (!clientRef.current || !session) return;
596
+ try {
597
+ setError(null);
598
+ stopHeartbeat();
599
+ await clientRef.current.endSession(session.id);
600
+ clearPersistedSession();
601
+ setSession((prev) => prev ? { ...prev, status: "ended" } : null);
602
+ } catch (err) {
603
+ const error2 = err instanceof Error ? err : new Error("Failed to end session");
604
+ setError(error2);
605
+ config.onError?.(error2);
606
+ }
607
+ }, [session, clearPersistedSession, stopHeartbeat, config]);
424
608
  const clearError = (0, import_react.useCallback)(() => {
425
609
  setError(null);
426
610
  }, []);
@@ -437,6 +621,7 @@ function InformedAIProvider({ config, children }) {
437
621
  applyPendingValue,
438
622
  skipTask,
439
623
  startNewSession,
624
+ endSession,
440
625
  clearError
441
626
  };
442
627
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InformedAIContext.Provider, { value, children });
package/dist/index.mjs CHANGED
@@ -53,7 +53,7 @@ var InformedAIClient = class {
53
53
  });
54
54
  }
55
55
  /**
56
- * Get an existing session.
56
+ * Get an existing session with its document and documentType.
57
57
  */
58
58
  async getSession(id) {
59
59
  return this.request(`/widget/sessions/${id}`);
@@ -121,6 +121,51 @@ var InformedAIClient = class {
121
121
  method: "POST"
122
122
  });
123
123
  }
124
+ /**
125
+ * Send a heartbeat to keep the session active.
126
+ * Call this every 30 seconds while the session is in use.
127
+ */
128
+ async sendHeartbeat(sessionId) {
129
+ await this.request(`/widget/sessions/${sessionId}/heartbeat`, {
130
+ method: "POST"
131
+ });
132
+ }
133
+ /**
134
+ * Resume an abandoned session with a new chatbot.
135
+ * Preserves conversation history.
136
+ */
137
+ async resumeSession(sessionId) {
138
+ return this.request(`/widget/sessions/${sessionId}/resume`, {
139
+ method: "POST"
140
+ });
141
+ }
142
+ /**
143
+ * Explicitly end a session and release resources.
144
+ * Conversation history is preserved but session cannot be resumed.
145
+ */
146
+ async endSession(sessionId) {
147
+ await this.request(`/widget/sessions/${sessionId}/end`, {
148
+ method: "POST"
149
+ });
150
+ }
151
+ /**
152
+ * Send end session via sendBeacon (for beforeunload).
153
+ * Returns true if sendBeacon was used, false if fetch fallback was attempted.
154
+ */
155
+ endSessionBeacon(sessionId) {
156
+ const url = `${this.apiUrl}/widget/sessions/${sessionId}/end`;
157
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
158
+ const blob = new Blob([JSON.stringify({})], { type: "application/json" });
159
+ return navigator.sendBeacon(url, blob);
160
+ }
161
+ fetch(url, {
162
+ method: "POST",
163
+ headers: this.getHeaders(),
164
+ keepalive: true
165
+ }).catch(() => {
166
+ });
167
+ return false;
168
+ }
124
169
  // ========================================================================
125
170
  // SSE Stream Processing
126
171
  // ========================================================================
@@ -164,6 +209,14 @@ var InformedAIClient = class {
164
209
  // src/context/InformedAIContext.tsx
165
210
  import { jsx } from "react/jsx-runtime";
166
211
  var getStorageKey = (documentTypeId, externalId) => `informedai_session_${documentTypeId}${externalId ? `_${externalId}` : ""}`;
212
+ var HEARTBEAT_INTERVAL = 30 * 1e3;
213
+ function isSessionNotFoundError(error) {
214
+ if (error instanceof Error) {
215
+ const message = error.message.toLowerCase();
216
+ return message.includes("session not found") || message.includes("404") || message.includes("not found");
217
+ }
218
+ return false;
219
+ }
167
220
  var InformedAIContext = createContext(null);
168
221
  function useInformedAI() {
169
222
  const context = useContext(InformedAIContext);
@@ -182,6 +235,8 @@ function InformedAIProvider({ config, children }) {
182
235
  const [streamingContent, setStreamingContent] = useState("");
183
236
  const clientRef = useRef(null);
184
237
  const initRef = useRef(false);
238
+ const heartbeatIntervalRef = useRef(null);
239
+ const sessionIdRef = useRef(null);
185
240
  const shouldPersist = config.persistSession ?? !!config.externalId;
186
241
  const storageKey = getStorageKey(config.documentTypeId, config.externalId);
187
242
  useEffect(() => {
@@ -214,6 +269,45 @@ function InformedAIProvider({ config, children }) {
214
269
  }
215
270
  return { session: result.session, document: result.document, documentType: dt };
216
271
  }, [config.documentTypeId, config.externalId, config.initialData, shouldPersist, storageKey]);
272
+ const clearPersistedSession = useCallback(() => {
273
+ if (shouldPersist && typeof window !== "undefined") {
274
+ try {
275
+ localStorage.removeItem(storageKey);
276
+ } catch (e) {
277
+ }
278
+ }
279
+ }, [shouldPersist, storageKey]);
280
+ const handleSessionDeleted = useCallback(async () => {
281
+ console.warn("[InformedAI] Session was deleted, starting new session...");
282
+ clearPersistedSession();
283
+ if (heartbeatIntervalRef.current) {
284
+ clearInterval(heartbeatIntervalRef.current);
285
+ heartbeatIntervalRef.current = null;
286
+ }
287
+ sessionIdRef.current = null;
288
+ try {
289
+ const result = await createNewSession();
290
+ if (result) {
291
+ setSession(result.session);
292
+ setDocument(result.document);
293
+ setDocumentType(result.documentType);
294
+ setError(null);
295
+ config.onSessionChange?.(result.session);
296
+ config.onReady?.({
297
+ session: result.session,
298
+ document: result.document,
299
+ documentType: result.documentType
300
+ });
301
+ return true;
302
+ }
303
+ } catch (err) {
304
+ console.error("[InformedAI] Failed to create new session after deletion:", err);
305
+ const error2 = err instanceof Error ? err : new Error("Failed to recover session");
306
+ setError(error2);
307
+ config.onError?.(error2);
308
+ }
309
+ return false;
310
+ }, [createNewSession, clearPersistedSession, config]);
217
311
  useEffect(() => {
218
312
  if (initRef.current) return;
219
313
  initRef.current = true;
@@ -234,7 +328,20 @@ function InformedAIProvider({ config, children }) {
234
328
  }
235
329
  if (sessionIdToResume) {
236
330
  try {
237
- sess = await clientRef.current.getSession(sessionIdToResume);
331
+ const resumedSession = await clientRef.current.getSession(sessionIdToResume);
332
+ sess = resumedSession;
333
+ doc = resumedSession.document;
334
+ const embeddedDt = resumedSession.document.documentType;
335
+ dt = {
336
+ id: embeddedDt.id,
337
+ name: embeddedDt.name,
338
+ displayName: embeddedDt.displayName,
339
+ schema: embeddedDt.schema,
340
+ workspaceId: "",
341
+ taskConfigs: {},
342
+ createdAt: "",
343
+ updatedAt: ""
344
+ };
238
345
  } catch (e) {
239
346
  console.warn("Failed to resume session, creating new one");
240
347
  if (shouldPersist && typeof window !== "undefined") {
@@ -252,13 +359,6 @@ function InformedAIProvider({ config, children }) {
252
359
  doc = result.document;
253
360
  dt = result.documentType;
254
361
  }
255
- } else {
256
- const result = await createNewSession();
257
- if (result) {
258
- sess = result.session;
259
- doc = result.document;
260
- dt = result.documentType;
261
- }
262
362
  }
263
363
  if (sess) {
264
364
  setSession(sess);
@@ -279,6 +379,63 @@ function InformedAIProvider({ config, children }) {
279
379
  }
280
380
  initialize();
281
381
  }, [config.documentTypeId, config.sessionId, config.externalId]);
382
+ const startHeartbeat = useCallback((sessionId) => {
383
+ if (heartbeatIntervalRef.current) {
384
+ clearInterval(heartbeatIntervalRef.current);
385
+ }
386
+ sessionIdRef.current = sessionId;
387
+ clientRef.current?.sendHeartbeat(sessionId).catch((err) => {
388
+ console.warn("Heartbeat failed:", err);
389
+ });
390
+ heartbeatIntervalRef.current = setInterval(() => {
391
+ if (clientRef.current && sessionIdRef.current) {
392
+ clientRef.current.sendHeartbeat(sessionIdRef.current).catch((err) => {
393
+ console.warn("Heartbeat failed:", err);
394
+ });
395
+ }
396
+ }, HEARTBEAT_INTERVAL);
397
+ }, []);
398
+ const stopHeartbeat = useCallback(() => {
399
+ if (heartbeatIntervalRef.current) {
400
+ clearInterval(heartbeatIntervalRef.current);
401
+ heartbeatIntervalRef.current = null;
402
+ }
403
+ }, []);
404
+ useEffect(() => {
405
+ if (session?.id && session.status === "active") {
406
+ startHeartbeat(session.id);
407
+ } else {
408
+ stopHeartbeat();
409
+ }
410
+ return () => {
411
+ stopHeartbeat();
412
+ };
413
+ }, [session?.id, session?.status, startHeartbeat, stopHeartbeat]);
414
+ useEffect(() => {
415
+ if (typeof window === "undefined") return;
416
+ const handleVisibilityChange = () => {
417
+ if (window.document.hidden) {
418
+ stopHeartbeat();
419
+ } else if (session?.id && session.status === "active") {
420
+ startHeartbeat(session.id);
421
+ }
422
+ };
423
+ window.document.addEventListener("visibilitychange", handleVisibilityChange);
424
+ return () => {
425
+ window.document.removeEventListener("visibilitychange", handleVisibilityChange);
426
+ };
427
+ }, [session?.id, session?.status, startHeartbeat, stopHeartbeat]);
428
+ useEffect(() => {
429
+ const handleBeforeUnload = () => {
430
+ if (clientRef.current && sessionIdRef.current) {
431
+ clientRef.current.endSessionBeacon(sessionIdRef.current);
432
+ }
433
+ };
434
+ window.addEventListener("beforeunload", handleBeforeUnload);
435
+ return () => {
436
+ window.removeEventListener("beforeunload", handleBeforeUnload);
437
+ };
438
+ }, []);
282
439
  const handleSSEEvent = useCallback((event) => {
283
440
  if (event.type === "content" && event.content) {
284
441
  setStreamingContent((prev) => prev + event.content);
@@ -306,12 +463,17 @@ function InformedAIProvider({ config, children }) {
306
463
  setError(null);
307
464
  await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
308
465
  } catch (err) {
466
+ if (isSessionNotFoundError(err)) {
467
+ setIsStreaming(false);
468
+ await handleSessionDeleted();
469
+ return;
470
+ }
309
471
  const error2 = err instanceof Error ? err : new Error("Failed to send message");
310
472
  setError(error2);
311
473
  config.onError?.(error2);
312
474
  setIsStreaming(false);
313
475
  }
314
- }, [session, handleSSEEvent, config]);
476
+ }, [session, handleSSEEvent, handleSessionDeleted, config]);
315
477
  const sendQuickAction = useCallback(async (action) => {
316
478
  if (!clientRef.current || !session) return;
317
479
  try {
@@ -326,13 +488,18 @@ function InformedAIProvider({ config, children }) {
326
488
  setSession(newSession);
327
489
  config.onSessionChange?.(newSession);
328
490
  } catch (err) {
491
+ if (isSessionNotFoundError(err)) {
492
+ setIsStreaming(false);
493
+ await handleSessionDeleted();
494
+ return;
495
+ }
329
496
  const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
330
497
  setError(error2);
331
498
  config.onError?.(error2);
332
499
  } finally {
333
500
  setIsStreaming(false);
334
501
  }
335
- }, [session, handleSSEEvent, config]);
502
+ }, [session, handleSSEEvent, handleSessionDeleted, config]);
336
503
  const applyPendingValue = useCallback(async () => {
337
504
  if (!clientRef.current || !session) return;
338
505
  try {
@@ -342,11 +509,15 @@ function InformedAIProvider({ config, children }) {
342
509
  config.onSessionChange?.(result.session);
343
510
  config.onFieldApply?.(result.appliedField, result.appliedValue);
344
511
  } catch (err) {
512
+ if (isSessionNotFoundError(err)) {
513
+ await handleSessionDeleted();
514
+ return;
515
+ }
345
516
  const error2 = err instanceof Error ? err : new Error("Failed to apply value");
346
517
  setError(error2);
347
518
  config.onError?.(error2);
348
519
  }
349
- }, [session, config]);
520
+ }, [session, handleSessionDeleted, config]);
350
521
  const skipTask = useCallback(async () => {
351
522
  if (!clientRef.current || !session) return;
352
523
  try {
@@ -355,22 +526,21 @@ function InformedAIProvider({ config, children }) {
355
526
  setSession(newSession);
356
527
  config.onSessionChange?.(newSession);
357
528
  } catch (err) {
529
+ if (isSessionNotFoundError(err)) {
530
+ await handleSessionDeleted();
531
+ return;
532
+ }
358
533
  const error2 = err instanceof Error ? err : new Error("Failed to skip task");
359
534
  setError(error2);
360
535
  config.onError?.(error2);
361
536
  }
362
- }, [session, config]);
537
+ }, [session, handleSessionDeleted, config]);
363
538
  const startNewSession = useCallback(async () => {
364
539
  if (!clientRef.current) return;
365
540
  try {
366
541
  setIsLoading(true);
367
542
  setError(null);
368
- if (shouldPersist && typeof window !== "undefined") {
369
- try {
370
- localStorage.removeItem(storageKey);
371
- } catch (e) {
372
- }
373
- }
543
+ clearPersistedSession();
374
544
  const result = await createNewSession();
375
545
  if (result) {
376
546
  setSession(result.session);
@@ -390,7 +560,21 @@ function InformedAIProvider({ config, children }) {
390
560
  } finally {
391
561
  setIsLoading(false);
392
562
  }
393
- }, [createNewSession, shouldPersist, storageKey, config]);
563
+ }, [createNewSession, clearPersistedSession, config]);
564
+ const endSession = useCallback(async () => {
565
+ if (!clientRef.current || !session) return;
566
+ try {
567
+ setError(null);
568
+ stopHeartbeat();
569
+ await clientRef.current.endSession(session.id);
570
+ clearPersistedSession();
571
+ setSession((prev) => prev ? { ...prev, status: "ended" } : null);
572
+ } catch (err) {
573
+ const error2 = err instanceof Error ? err : new Error("Failed to end session");
574
+ setError(error2);
575
+ config.onError?.(error2);
576
+ }
577
+ }, [session, clearPersistedSession, stopHeartbeat, config]);
394
578
  const clearError = useCallback(() => {
395
579
  setError(null);
396
580
  }, []);
@@ -407,6 +591,7 @@ function InformedAIProvider({ config, children }) {
407
591
  applyPendingValue,
408
592
  skipTask,
409
593
  startNewSession,
594
+ endSession,
410
595
  clearError
411
596
  };
412
597
  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.3.0",
3
+ "version": "0.4.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",