@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 +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +205 -20
- package/dist/index.mjs +205 -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,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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 });
|