@informedai/react 0.3.0 → 0.4.1
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 +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +227 -20
- package/dist/index.mjs +227 -20
- package/package.json +1 -1
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<
|
|
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<
|
|
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
|
-
|
|
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,85 @@ 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 = async () => {
|
|
447
|
+
if (window.document.hidden) {
|
|
448
|
+
stopHeartbeat();
|
|
449
|
+
} else if (session?.id && clientRef.current) {
|
|
450
|
+
try {
|
|
451
|
+
const refreshedSession = await clientRef.current.getSession(session.id);
|
|
452
|
+
if (refreshedSession.status === "abandoned") {
|
|
453
|
+
console.log("[InformedAI] Session was abandoned, resuming...");
|
|
454
|
+
try {
|
|
455
|
+
const resumedSession = await clientRef.current.resumeSession(session.id);
|
|
456
|
+
setSession(resumedSession);
|
|
457
|
+
config.onSessionChange?.(resumedSession);
|
|
458
|
+
startHeartbeat(session.id);
|
|
459
|
+
} catch (resumeErr) {
|
|
460
|
+
console.error("[InformedAI] Failed to resume session:", resumeErr);
|
|
461
|
+
await handleSessionDeleted();
|
|
462
|
+
}
|
|
463
|
+
} else if (refreshedSession.status === "active") {
|
|
464
|
+
setSession(refreshedSession);
|
|
465
|
+
startHeartbeat(session.id);
|
|
466
|
+
} else {
|
|
467
|
+
setSession(refreshedSession);
|
|
468
|
+
}
|
|
469
|
+
} catch (err) {
|
|
470
|
+
console.warn("[InformedAI] Session not found on return, creating new...");
|
|
471
|
+
await handleSessionDeleted();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
window.document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
476
|
+
return () => {
|
|
477
|
+
window.document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
478
|
+
};
|
|
479
|
+
}, [session?.id, startHeartbeat, stopHeartbeat, handleSessionDeleted, config]);
|
|
480
|
+
(0, import_react.useEffect)(() => {
|
|
481
|
+
const handleBeforeUnload = () => {
|
|
482
|
+
if (clientRef.current && sessionIdRef.current) {
|
|
483
|
+
clientRef.current.endSessionBeacon(sessionIdRef.current);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
487
|
+
return () => {
|
|
488
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
489
|
+
};
|
|
490
|
+
}, []);
|
|
312
491
|
const handleSSEEvent = (0, import_react.useCallback)((event) => {
|
|
313
492
|
if (event.type === "content" && event.content) {
|
|
314
493
|
setStreamingContent((prev) => prev + event.content);
|
|
@@ -336,12 +515,17 @@ function InformedAIProvider({ config, children }) {
|
|
|
336
515
|
setError(null);
|
|
337
516
|
await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
|
|
338
517
|
} catch (err) {
|
|
518
|
+
if (isSessionNotFoundError(err)) {
|
|
519
|
+
setIsStreaming(false);
|
|
520
|
+
await handleSessionDeleted();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
339
523
|
const error2 = err instanceof Error ? err : new Error("Failed to send message");
|
|
340
524
|
setError(error2);
|
|
341
525
|
config.onError?.(error2);
|
|
342
526
|
setIsStreaming(false);
|
|
343
527
|
}
|
|
344
|
-
}, [session, handleSSEEvent, config]);
|
|
528
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
345
529
|
const sendQuickAction = (0, import_react.useCallback)(async (action) => {
|
|
346
530
|
if (!clientRef.current || !session) return;
|
|
347
531
|
try {
|
|
@@ -356,13 +540,18 @@ function InformedAIProvider({ config, children }) {
|
|
|
356
540
|
setSession(newSession);
|
|
357
541
|
config.onSessionChange?.(newSession);
|
|
358
542
|
} catch (err) {
|
|
543
|
+
if (isSessionNotFoundError(err)) {
|
|
544
|
+
setIsStreaming(false);
|
|
545
|
+
await handleSessionDeleted();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
359
548
|
const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
|
|
360
549
|
setError(error2);
|
|
361
550
|
config.onError?.(error2);
|
|
362
551
|
} finally {
|
|
363
552
|
setIsStreaming(false);
|
|
364
553
|
}
|
|
365
|
-
}, [session, handleSSEEvent, config]);
|
|
554
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
366
555
|
const applyPendingValue = (0, import_react.useCallback)(async () => {
|
|
367
556
|
if (!clientRef.current || !session) return;
|
|
368
557
|
try {
|
|
@@ -372,11 +561,15 @@ function InformedAIProvider({ config, children }) {
|
|
|
372
561
|
config.onSessionChange?.(result.session);
|
|
373
562
|
config.onFieldApply?.(result.appliedField, result.appliedValue);
|
|
374
563
|
} catch (err) {
|
|
564
|
+
if (isSessionNotFoundError(err)) {
|
|
565
|
+
await handleSessionDeleted();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
375
568
|
const error2 = err instanceof Error ? err : new Error("Failed to apply value");
|
|
376
569
|
setError(error2);
|
|
377
570
|
config.onError?.(error2);
|
|
378
571
|
}
|
|
379
|
-
}, [session, config]);
|
|
572
|
+
}, [session, handleSessionDeleted, config]);
|
|
380
573
|
const skipTask = (0, import_react.useCallback)(async () => {
|
|
381
574
|
if (!clientRef.current || !session) return;
|
|
382
575
|
try {
|
|
@@ -385,22 +578,21 @@ function InformedAIProvider({ config, children }) {
|
|
|
385
578
|
setSession(newSession);
|
|
386
579
|
config.onSessionChange?.(newSession);
|
|
387
580
|
} catch (err) {
|
|
581
|
+
if (isSessionNotFoundError(err)) {
|
|
582
|
+
await handleSessionDeleted();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
388
585
|
const error2 = err instanceof Error ? err : new Error("Failed to skip task");
|
|
389
586
|
setError(error2);
|
|
390
587
|
config.onError?.(error2);
|
|
391
588
|
}
|
|
392
|
-
}, [session, config]);
|
|
589
|
+
}, [session, handleSessionDeleted, config]);
|
|
393
590
|
const startNewSession = (0, import_react.useCallback)(async () => {
|
|
394
591
|
if (!clientRef.current) return;
|
|
395
592
|
try {
|
|
396
593
|
setIsLoading(true);
|
|
397
594
|
setError(null);
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
localStorage.removeItem(storageKey);
|
|
401
|
-
} catch (e) {
|
|
402
|
-
}
|
|
403
|
-
}
|
|
595
|
+
clearPersistedSession();
|
|
404
596
|
const result = await createNewSession();
|
|
405
597
|
if (result) {
|
|
406
598
|
setSession(result.session);
|
|
@@ -420,7 +612,21 @@ function InformedAIProvider({ config, children }) {
|
|
|
420
612
|
} finally {
|
|
421
613
|
setIsLoading(false);
|
|
422
614
|
}
|
|
423
|
-
}, [createNewSession,
|
|
615
|
+
}, [createNewSession, clearPersistedSession, config]);
|
|
616
|
+
const endSession = (0, import_react.useCallback)(async () => {
|
|
617
|
+
if (!clientRef.current || !session) return;
|
|
618
|
+
try {
|
|
619
|
+
setError(null);
|
|
620
|
+
stopHeartbeat();
|
|
621
|
+
await clientRef.current.endSession(session.id);
|
|
622
|
+
clearPersistedSession();
|
|
623
|
+
setSession((prev) => prev ? { ...prev, status: "ended" } : null);
|
|
624
|
+
} catch (err) {
|
|
625
|
+
const error2 = err instanceof Error ? err : new Error("Failed to end session");
|
|
626
|
+
setError(error2);
|
|
627
|
+
config.onError?.(error2);
|
|
628
|
+
}
|
|
629
|
+
}, [session, clearPersistedSession, stopHeartbeat, config]);
|
|
424
630
|
const clearError = (0, import_react.useCallback)(() => {
|
|
425
631
|
setError(null);
|
|
426
632
|
}, []);
|
|
@@ -437,6 +643,7 @@ function InformedAIProvider({ config, children }) {
|
|
|
437
643
|
applyPendingValue,
|
|
438
644
|
skipTask,
|
|
439
645
|
startNewSession,
|
|
646
|
+
endSession,
|
|
440
647
|
clearError
|
|
441
648
|
};
|
|
442
649
|
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
|
-
|
|
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,85 @@ 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 = async () => {
|
|
417
|
+
if (window.document.hidden) {
|
|
418
|
+
stopHeartbeat();
|
|
419
|
+
} else if (session?.id && clientRef.current) {
|
|
420
|
+
try {
|
|
421
|
+
const refreshedSession = await clientRef.current.getSession(session.id);
|
|
422
|
+
if (refreshedSession.status === "abandoned") {
|
|
423
|
+
console.log("[InformedAI] Session was abandoned, resuming...");
|
|
424
|
+
try {
|
|
425
|
+
const resumedSession = await clientRef.current.resumeSession(session.id);
|
|
426
|
+
setSession(resumedSession);
|
|
427
|
+
config.onSessionChange?.(resumedSession);
|
|
428
|
+
startHeartbeat(session.id);
|
|
429
|
+
} catch (resumeErr) {
|
|
430
|
+
console.error("[InformedAI] Failed to resume session:", resumeErr);
|
|
431
|
+
await handleSessionDeleted();
|
|
432
|
+
}
|
|
433
|
+
} else if (refreshedSession.status === "active") {
|
|
434
|
+
setSession(refreshedSession);
|
|
435
|
+
startHeartbeat(session.id);
|
|
436
|
+
} else {
|
|
437
|
+
setSession(refreshedSession);
|
|
438
|
+
}
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.warn("[InformedAI] Session not found on return, creating new...");
|
|
441
|
+
await handleSessionDeleted();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
window.document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
446
|
+
return () => {
|
|
447
|
+
window.document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
448
|
+
};
|
|
449
|
+
}, [session?.id, startHeartbeat, stopHeartbeat, handleSessionDeleted, config]);
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
const handleBeforeUnload = () => {
|
|
452
|
+
if (clientRef.current && sessionIdRef.current) {
|
|
453
|
+
clientRef.current.endSessionBeacon(sessionIdRef.current);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
457
|
+
return () => {
|
|
458
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
459
|
+
};
|
|
460
|
+
}, []);
|
|
282
461
|
const handleSSEEvent = useCallback((event) => {
|
|
283
462
|
if (event.type === "content" && event.content) {
|
|
284
463
|
setStreamingContent((prev) => prev + event.content);
|
|
@@ -306,12 +485,17 @@ function InformedAIProvider({ config, children }) {
|
|
|
306
485
|
setError(null);
|
|
307
486
|
await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
|
|
308
487
|
} catch (err) {
|
|
488
|
+
if (isSessionNotFoundError(err)) {
|
|
489
|
+
setIsStreaming(false);
|
|
490
|
+
await handleSessionDeleted();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
309
493
|
const error2 = err instanceof Error ? err : new Error("Failed to send message");
|
|
310
494
|
setError(error2);
|
|
311
495
|
config.onError?.(error2);
|
|
312
496
|
setIsStreaming(false);
|
|
313
497
|
}
|
|
314
|
-
}, [session, handleSSEEvent, config]);
|
|
498
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
315
499
|
const sendQuickAction = useCallback(async (action) => {
|
|
316
500
|
if (!clientRef.current || !session) return;
|
|
317
501
|
try {
|
|
@@ -326,13 +510,18 @@ function InformedAIProvider({ config, children }) {
|
|
|
326
510
|
setSession(newSession);
|
|
327
511
|
config.onSessionChange?.(newSession);
|
|
328
512
|
} catch (err) {
|
|
513
|
+
if (isSessionNotFoundError(err)) {
|
|
514
|
+
setIsStreaming(false);
|
|
515
|
+
await handleSessionDeleted();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
329
518
|
const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
|
|
330
519
|
setError(error2);
|
|
331
520
|
config.onError?.(error2);
|
|
332
521
|
} finally {
|
|
333
522
|
setIsStreaming(false);
|
|
334
523
|
}
|
|
335
|
-
}, [session, handleSSEEvent, config]);
|
|
524
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
336
525
|
const applyPendingValue = useCallback(async () => {
|
|
337
526
|
if (!clientRef.current || !session) return;
|
|
338
527
|
try {
|
|
@@ -342,11 +531,15 @@ function InformedAIProvider({ config, children }) {
|
|
|
342
531
|
config.onSessionChange?.(result.session);
|
|
343
532
|
config.onFieldApply?.(result.appliedField, result.appliedValue);
|
|
344
533
|
} catch (err) {
|
|
534
|
+
if (isSessionNotFoundError(err)) {
|
|
535
|
+
await handleSessionDeleted();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
345
538
|
const error2 = err instanceof Error ? err : new Error("Failed to apply value");
|
|
346
539
|
setError(error2);
|
|
347
540
|
config.onError?.(error2);
|
|
348
541
|
}
|
|
349
|
-
}, [session, config]);
|
|
542
|
+
}, [session, handleSessionDeleted, config]);
|
|
350
543
|
const skipTask = useCallback(async () => {
|
|
351
544
|
if (!clientRef.current || !session) return;
|
|
352
545
|
try {
|
|
@@ -355,22 +548,21 @@ function InformedAIProvider({ config, children }) {
|
|
|
355
548
|
setSession(newSession);
|
|
356
549
|
config.onSessionChange?.(newSession);
|
|
357
550
|
} catch (err) {
|
|
551
|
+
if (isSessionNotFoundError(err)) {
|
|
552
|
+
await handleSessionDeleted();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
358
555
|
const error2 = err instanceof Error ? err : new Error("Failed to skip task");
|
|
359
556
|
setError(error2);
|
|
360
557
|
config.onError?.(error2);
|
|
361
558
|
}
|
|
362
|
-
}, [session, config]);
|
|
559
|
+
}, [session, handleSessionDeleted, config]);
|
|
363
560
|
const startNewSession = useCallback(async () => {
|
|
364
561
|
if (!clientRef.current) return;
|
|
365
562
|
try {
|
|
366
563
|
setIsLoading(true);
|
|
367
564
|
setError(null);
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
localStorage.removeItem(storageKey);
|
|
371
|
-
} catch (e) {
|
|
372
|
-
}
|
|
373
|
-
}
|
|
565
|
+
clearPersistedSession();
|
|
374
566
|
const result = await createNewSession();
|
|
375
567
|
if (result) {
|
|
376
568
|
setSession(result.session);
|
|
@@ -390,7 +582,21 @@ function InformedAIProvider({ config, children }) {
|
|
|
390
582
|
} finally {
|
|
391
583
|
setIsLoading(false);
|
|
392
584
|
}
|
|
393
|
-
}, [createNewSession,
|
|
585
|
+
}, [createNewSession, clearPersistedSession, config]);
|
|
586
|
+
const endSession = useCallback(async () => {
|
|
587
|
+
if (!clientRef.current || !session) return;
|
|
588
|
+
try {
|
|
589
|
+
setError(null);
|
|
590
|
+
stopHeartbeat();
|
|
591
|
+
await clientRef.current.endSession(session.id);
|
|
592
|
+
clearPersistedSession();
|
|
593
|
+
setSession((prev) => prev ? { ...prev, status: "ended" } : null);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
const error2 = err instanceof Error ? err : new Error("Failed to end session");
|
|
596
|
+
setError(error2);
|
|
597
|
+
config.onError?.(error2);
|
|
598
|
+
}
|
|
599
|
+
}, [session, clearPersistedSession, stopHeartbeat, config]);
|
|
394
600
|
const clearError = useCallback(() => {
|
|
395
601
|
setError(null);
|
|
396
602
|
}, []);
|
|
@@ -407,6 +613,7 @@ function InformedAIProvider({ config, children }) {
|
|
|
407
613
|
applyPendingValue,
|
|
408
614
|
skipTask,
|
|
409
615
|
startNewSession,
|
|
616
|
+
endSession,
|
|
410
617
|
clearError
|
|
411
618
|
};
|
|
412
619
|
return /* @__PURE__ */ jsx(InformedAIContext.Provider, { value, children });
|