@informedai/react 0.2.6 → 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 +65 -5
- package/dist/index.d.ts +65 -5
- package/dist/index.js +309 -32
- package/dist/index.mjs +309 -32
- 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;
|
|
@@ -76,7 +77,24 @@ interface InformedAssistantConfig {
|
|
|
76
77
|
documentTypeId: string;
|
|
77
78
|
/** API base URL (defaults to https://api.informedassistant.ai/api/v1) */
|
|
78
79
|
apiUrl?: string;
|
|
79
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* External ID for idempotency - your object's ID in your system.
|
|
82
|
+
* If provided, the widget will find or create a document linked to this ID.
|
|
83
|
+
* This allows resuming work on the same document across sessions.
|
|
84
|
+
*/
|
|
85
|
+
externalId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Your current field values to sync before starting.
|
|
88
|
+
* Since you're the source of truth, this data will be written to our document.
|
|
89
|
+
* Use this to ensure the assistant sees your latest data.
|
|
90
|
+
*/
|
|
91
|
+
initialData?: Record<string, unknown>;
|
|
92
|
+
/**
|
|
93
|
+
* Whether to persist sessions in localStorage for resumption.
|
|
94
|
+
* Defaults to true when externalId is provided, false otherwise.
|
|
95
|
+
*/
|
|
96
|
+
persistSession?: boolean;
|
|
97
|
+
/** Optional: Existing session ID to resume (overrides localStorage) */
|
|
80
98
|
sessionId?: string;
|
|
81
99
|
/** Callback when widget is ready with document type schema */
|
|
82
100
|
onReady?: (context: WidgetReadyContext) => void;
|
|
@@ -116,6 +134,20 @@ interface SSEEvent {
|
|
|
116
134
|
session?: Session;
|
|
117
135
|
error?: string;
|
|
118
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
|
+
}
|
|
119
151
|
interface UseSessionReturn {
|
|
120
152
|
session: Session | null;
|
|
121
153
|
isLoading: boolean;
|
|
@@ -156,6 +188,8 @@ interface InformedAIContextValue {
|
|
|
156
188
|
sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
|
|
157
189
|
applyPendingValue: () => Promise<void>;
|
|
158
190
|
skipTask: () => Promise<void>;
|
|
191
|
+
startNewSession: () => Promise<void>;
|
|
192
|
+
endSession: () => Promise<void>;
|
|
159
193
|
clearError: () => void;
|
|
160
194
|
}
|
|
161
195
|
declare function useInformedAI(): InformedAIContextValue;
|
|
@@ -213,13 +247,19 @@ declare class InformedAIClient {
|
|
|
213
247
|
private request;
|
|
214
248
|
/**
|
|
215
249
|
* Create a new session for a document type.
|
|
216
|
-
*
|
|
250
|
+
*
|
|
251
|
+
* @param documentTypeId - The document type to create a session for
|
|
252
|
+
* @param options.externalId - Your object's ID for idempotency (finds or creates document)
|
|
253
|
+
* @param options.initialData - Your current field values to sync (you're the source of truth)
|
|
217
254
|
*/
|
|
218
|
-
createSession(documentTypeId: string
|
|
255
|
+
createSession(documentTypeId: string, options?: {
|
|
256
|
+
externalId?: string;
|
|
257
|
+
initialData?: Record<string, unknown>;
|
|
258
|
+
}): Promise<CreateSessionResponse>;
|
|
219
259
|
/**
|
|
220
|
-
* Get an existing session.
|
|
260
|
+
* Get an existing session with its document and documentType.
|
|
221
261
|
*/
|
|
222
|
-
getSession(id: string): Promise<
|
|
262
|
+
getSession(id: string): Promise<GetSessionResponse>;
|
|
223
263
|
/**
|
|
224
264
|
* Send a message to the session with SSE streaming.
|
|
225
265
|
*/
|
|
@@ -238,6 +278,26 @@ declare class InformedAIClient {
|
|
|
238
278
|
* Skip the active task.
|
|
239
279
|
*/
|
|
240
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;
|
|
241
301
|
private processSSEStream;
|
|
242
302
|
}
|
|
243
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;
|
|
@@ -76,7 +77,24 @@ interface InformedAssistantConfig {
|
|
|
76
77
|
documentTypeId: string;
|
|
77
78
|
/** API base URL (defaults to https://api.informedassistant.ai/api/v1) */
|
|
78
79
|
apiUrl?: string;
|
|
79
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* External ID for idempotency - your object's ID in your system.
|
|
82
|
+
* If provided, the widget will find or create a document linked to this ID.
|
|
83
|
+
* This allows resuming work on the same document across sessions.
|
|
84
|
+
*/
|
|
85
|
+
externalId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Your current field values to sync before starting.
|
|
88
|
+
* Since you're the source of truth, this data will be written to our document.
|
|
89
|
+
* Use this to ensure the assistant sees your latest data.
|
|
90
|
+
*/
|
|
91
|
+
initialData?: Record<string, unknown>;
|
|
92
|
+
/**
|
|
93
|
+
* Whether to persist sessions in localStorage for resumption.
|
|
94
|
+
* Defaults to true when externalId is provided, false otherwise.
|
|
95
|
+
*/
|
|
96
|
+
persistSession?: boolean;
|
|
97
|
+
/** Optional: Existing session ID to resume (overrides localStorage) */
|
|
80
98
|
sessionId?: string;
|
|
81
99
|
/** Callback when widget is ready with document type schema */
|
|
82
100
|
onReady?: (context: WidgetReadyContext) => void;
|
|
@@ -116,6 +134,20 @@ interface SSEEvent {
|
|
|
116
134
|
session?: Session;
|
|
117
135
|
error?: string;
|
|
118
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
|
+
}
|
|
119
151
|
interface UseSessionReturn {
|
|
120
152
|
session: Session | null;
|
|
121
153
|
isLoading: boolean;
|
|
@@ -156,6 +188,8 @@ interface InformedAIContextValue {
|
|
|
156
188
|
sendQuickAction: (action: string, payload?: Record<string, unknown>) => Promise<void>;
|
|
157
189
|
applyPendingValue: () => Promise<void>;
|
|
158
190
|
skipTask: () => Promise<void>;
|
|
191
|
+
startNewSession: () => Promise<void>;
|
|
192
|
+
endSession: () => Promise<void>;
|
|
159
193
|
clearError: () => void;
|
|
160
194
|
}
|
|
161
195
|
declare function useInformedAI(): InformedAIContextValue;
|
|
@@ -213,13 +247,19 @@ declare class InformedAIClient {
|
|
|
213
247
|
private request;
|
|
214
248
|
/**
|
|
215
249
|
* Create a new session for a document type.
|
|
216
|
-
*
|
|
250
|
+
*
|
|
251
|
+
* @param documentTypeId - The document type to create a session for
|
|
252
|
+
* @param options.externalId - Your object's ID for idempotency (finds or creates document)
|
|
253
|
+
* @param options.initialData - Your current field values to sync (you're the source of truth)
|
|
217
254
|
*/
|
|
218
|
-
createSession(documentTypeId: string
|
|
255
|
+
createSession(documentTypeId: string, options?: {
|
|
256
|
+
externalId?: string;
|
|
257
|
+
initialData?: Record<string, unknown>;
|
|
258
|
+
}): Promise<CreateSessionResponse>;
|
|
219
259
|
/**
|
|
220
|
-
* Get an existing session.
|
|
260
|
+
* Get an existing session with its document and documentType.
|
|
221
261
|
*/
|
|
222
|
-
getSession(id: string): Promise<
|
|
262
|
+
getSession(id: string): Promise<GetSessionResponse>;
|
|
223
263
|
/**
|
|
224
264
|
* Send a message to the session with SSE streaming.
|
|
225
265
|
*/
|
|
@@ -238,6 +278,26 @@ declare class InformedAIClient {
|
|
|
238
278
|
* Skip the active task.
|
|
239
279
|
*/
|
|
240
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;
|
|
241
301
|
private processSSEStream;
|
|
242
302
|
}
|
|
243
303
|
|
package/dist/index.js
CHANGED
|
@@ -67,16 +67,23 @@ var InformedAIClient = class {
|
|
|
67
67
|
// ========================================================================
|
|
68
68
|
/**
|
|
69
69
|
* Create a new session for a document type.
|
|
70
|
-
*
|
|
70
|
+
*
|
|
71
|
+
* @param documentTypeId - The document type to create a session for
|
|
72
|
+
* @param options.externalId - Your object's ID for idempotency (finds or creates document)
|
|
73
|
+
* @param options.initialData - Your current field values to sync (you're the source of truth)
|
|
71
74
|
*/
|
|
72
|
-
async createSession(documentTypeId) {
|
|
75
|
+
async createSession(documentTypeId, options) {
|
|
73
76
|
return this.request("/widget/sessions", {
|
|
74
77
|
method: "POST",
|
|
75
|
-
body: JSON.stringify({
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
documentTypeId,
|
|
80
|
+
externalId: options?.externalId,
|
|
81
|
+
initialData: options?.initialData
|
|
82
|
+
})
|
|
76
83
|
});
|
|
77
84
|
}
|
|
78
85
|
/**
|
|
79
|
-
* Get an existing session.
|
|
86
|
+
* Get an existing session with its document and documentType.
|
|
80
87
|
*/
|
|
81
88
|
async getSession(id) {
|
|
82
89
|
return this.request(`/widget/sessions/${id}`);
|
|
@@ -144,6 +151,51 @@ var InformedAIClient = class {
|
|
|
144
151
|
method: "POST"
|
|
145
152
|
});
|
|
146
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
|
+
}
|
|
147
199
|
// ========================================================================
|
|
148
200
|
// SSE Stream Processing
|
|
149
201
|
// ========================================================================
|
|
@@ -186,6 +238,15 @@ var InformedAIClient = class {
|
|
|
186
238
|
|
|
187
239
|
// src/context/InformedAIContext.tsx
|
|
188
240
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
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
|
+
}
|
|
189
250
|
var InformedAIContext = (0, import_react.createContext)(null);
|
|
190
251
|
function useInformedAI() {
|
|
191
252
|
const context = (0, import_react.useContext)(InformedAIContext);
|
|
@@ -203,41 +264,140 @@ function InformedAIProvider({ config, children }) {
|
|
|
203
264
|
const [error, setError] = (0, import_react.useState)(null);
|
|
204
265
|
const [streamingContent, setStreamingContent] = (0, import_react.useState)("");
|
|
205
266
|
const clientRef = (0, import_react.useRef)(null);
|
|
267
|
+
const initRef = (0, import_react.useRef)(false);
|
|
268
|
+
const heartbeatIntervalRef = (0, import_react.useRef)(null);
|
|
269
|
+
const sessionIdRef = (0, import_react.useRef)(null);
|
|
270
|
+
const shouldPersist = config.persistSession ?? !!config.externalId;
|
|
271
|
+
const storageKey = getStorageKey(config.documentTypeId, config.externalId);
|
|
206
272
|
(0, import_react.useEffect)(() => {
|
|
207
273
|
clientRef.current = new InformedAIClient(config.apiUrl);
|
|
208
274
|
}, [config.apiUrl]);
|
|
275
|
+
const createNewSession = (0, import_react.useCallback)(async () => {
|
|
276
|
+
if (!clientRef.current) return null;
|
|
277
|
+
const result = await clientRef.current.createSession(
|
|
278
|
+
config.documentTypeId,
|
|
279
|
+
{
|
|
280
|
+
externalId: config.externalId,
|
|
281
|
+
initialData: config.initialData
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
const dt = {
|
|
285
|
+
id: result.documentType.id,
|
|
286
|
+
name: result.documentType.name,
|
|
287
|
+
displayName: result.documentType.displayName,
|
|
288
|
+
schema: result.documentType.schema,
|
|
289
|
+
workspaceId: "",
|
|
290
|
+
taskConfigs: {},
|
|
291
|
+
createdAt: "",
|
|
292
|
+
updatedAt: ""
|
|
293
|
+
};
|
|
294
|
+
if (shouldPersist && typeof window !== "undefined") {
|
|
295
|
+
try {
|
|
296
|
+
localStorage.setItem(storageKey, result.session.id);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return { session: result.session, document: result.document, documentType: dt };
|
|
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]);
|
|
209
341
|
(0, import_react.useEffect)(() => {
|
|
342
|
+
if (initRef.current) return;
|
|
343
|
+
initRef.current = true;
|
|
210
344
|
async function initialize() {
|
|
211
345
|
if (!clientRef.current) return;
|
|
212
346
|
try {
|
|
213
347
|
setIsLoading(true);
|
|
214
348
|
setError(null);
|
|
215
|
-
let sess;
|
|
349
|
+
let sess = null;
|
|
216
350
|
let doc = null;
|
|
217
351
|
let dt = null;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
dt = {
|
|
225
|
-
id: result.documentType.id,
|
|
226
|
-
name: result.documentType.name,
|
|
227
|
-
displayName: result.documentType.displayName,
|
|
228
|
-
schema: result.documentType.schema,
|
|
229
|
-
workspaceId: "",
|
|
230
|
-
taskConfigs: {},
|
|
231
|
-
createdAt: "",
|
|
232
|
-
updatedAt: ""
|
|
233
|
-
};
|
|
352
|
+
let sessionIdToResume = config.sessionId;
|
|
353
|
+
if (!sessionIdToResume && shouldPersist && typeof window !== "undefined") {
|
|
354
|
+
try {
|
|
355
|
+
sessionIdToResume = localStorage.getItem(storageKey) || void 0;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
}
|
|
234
358
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
359
|
+
if (sessionIdToResume) {
|
|
360
|
+
try {
|
|
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
|
+
};
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.warn("Failed to resume session, creating new one");
|
|
377
|
+
if (shouldPersist && typeof window !== "undefined") {
|
|
378
|
+
try {
|
|
379
|
+
localStorage.removeItem(storageKey);
|
|
380
|
+
} catch (e2) {
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!sess) {
|
|
386
|
+
const result = await createNewSession();
|
|
387
|
+
if (result) {
|
|
388
|
+
sess = result.session;
|
|
389
|
+
doc = result.document;
|
|
390
|
+
dt = result.documentType;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (sess) {
|
|
394
|
+
setSession(sess);
|
|
395
|
+
if (doc) setDocument(doc);
|
|
396
|
+
if (dt) setDocumentType(dt);
|
|
397
|
+
config.onSessionChange?.(sess);
|
|
398
|
+
if (doc && dt) {
|
|
399
|
+
config.onReady?.({ session: sess, document: doc, documentType: dt });
|
|
400
|
+
}
|
|
241
401
|
}
|
|
242
402
|
} catch (err) {
|
|
243
403
|
const error2 = err instanceof Error ? err : new Error("Initialization failed");
|
|
@@ -248,7 +408,64 @@ function InformedAIProvider({ config, children }) {
|
|
|
248
408
|
}
|
|
249
409
|
}
|
|
250
410
|
initialize();
|
|
251
|
-
}, [config.documentTypeId, config.sessionId]);
|
|
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
|
+
}, []);
|
|
252
469
|
const handleSSEEvent = (0, import_react.useCallback)((event) => {
|
|
253
470
|
if (event.type === "content" && event.content) {
|
|
254
471
|
setStreamingContent((prev) => prev + event.content);
|
|
@@ -276,12 +493,17 @@ function InformedAIProvider({ config, children }) {
|
|
|
276
493
|
setError(null);
|
|
277
494
|
await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
|
|
278
495
|
} catch (err) {
|
|
496
|
+
if (isSessionNotFoundError(err)) {
|
|
497
|
+
setIsStreaming(false);
|
|
498
|
+
await handleSessionDeleted();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
279
501
|
const error2 = err instanceof Error ? err : new Error("Failed to send message");
|
|
280
502
|
setError(error2);
|
|
281
503
|
config.onError?.(error2);
|
|
282
504
|
setIsStreaming(false);
|
|
283
505
|
}
|
|
284
|
-
}, [session, handleSSEEvent, config]);
|
|
506
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
285
507
|
const sendQuickAction = (0, import_react.useCallback)(async (action) => {
|
|
286
508
|
if (!clientRef.current || !session) return;
|
|
287
509
|
try {
|
|
@@ -296,13 +518,18 @@ function InformedAIProvider({ config, children }) {
|
|
|
296
518
|
setSession(newSession);
|
|
297
519
|
config.onSessionChange?.(newSession);
|
|
298
520
|
} catch (err) {
|
|
521
|
+
if (isSessionNotFoundError(err)) {
|
|
522
|
+
setIsStreaming(false);
|
|
523
|
+
await handleSessionDeleted();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
299
526
|
const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
|
|
300
527
|
setError(error2);
|
|
301
528
|
config.onError?.(error2);
|
|
302
529
|
} finally {
|
|
303
530
|
setIsStreaming(false);
|
|
304
531
|
}
|
|
305
|
-
}, [session, handleSSEEvent, config]);
|
|
532
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
306
533
|
const applyPendingValue = (0, import_react.useCallback)(async () => {
|
|
307
534
|
if (!clientRef.current || !session) return;
|
|
308
535
|
try {
|
|
@@ -312,11 +539,15 @@ function InformedAIProvider({ config, children }) {
|
|
|
312
539
|
config.onSessionChange?.(result.session);
|
|
313
540
|
config.onFieldApply?.(result.appliedField, result.appliedValue);
|
|
314
541
|
} catch (err) {
|
|
542
|
+
if (isSessionNotFoundError(err)) {
|
|
543
|
+
await handleSessionDeleted();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
315
546
|
const error2 = err instanceof Error ? err : new Error("Failed to apply value");
|
|
316
547
|
setError(error2);
|
|
317
548
|
config.onError?.(error2);
|
|
318
549
|
}
|
|
319
|
-
}, [session, config]);
|
|
550
|
+
}, [session, handleSessionDeleted, config]);
|
|
320
551
|
const skipTask = (0, import_react.useCallback)(async () => {
|
|
321
552
|
if (!clientRef.current || !session) return;
|
|
322
553
|
try {
|
|
@@ -325,11 +556,55 @@ function InformedAIProvider({ config, children }) {
|
|
|
325
556
|
setSession(newSession);
|
|
326
557
|
config.onSessionChange?.(newSession);
|
|
327
558
|
} catch (err) {
|
|
559
|
+
if (isSessionNotFoundError(err)) {
|
|
560
|
+
await handleSessionDeleted();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
328
563
|
const error2 = err instanceof Error ? err : new Error("Failed to skip task");
|
|
329
564
|
setError(error2);
|
|
330
565
|
config.onError?.(error2);
|
|
331
566
|
}
|
|
332
|
-
}, [session, config]);
|
|
567
|
+
}, [session, handleSessionDeleted, config]);
|
|
568
|
+
const startNewSession = (0, import_react.useCallback)(async () => {
|
|
569
|
+
if (!clientRef.current) return;
|
|
570
|
+
try {
|
|
571
|
+
setIsLoading(true);
|
|
572
|
+
setError(null);
|
|
573
|
+
clearPersistedSession();
|
|
574
|
+
const result = await createNewSession();
|
|
575
|
+
if (result) {
|
|
576
|
+
setSession(result.session);
|
|
577
|
+
setDocument(result.document);
|
|
578
|
+
setDocumentType(result.documentType);
|
|
579
|
+
config.onSessionChange?.(result.session);
|
|
580
|
+
config.onReady?.({
|
|
581
|
+
session: result.session,
|
|
582
|
+
document: result.document,
|
|
583
|
+
documentType: result.documentType
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} catch (err) {
|
|
587
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start new session");
|
|
588
|
+
setError(error2);
|
|
589
|
+
config.onError?.(error2);
|
|
590
|
+
} finally {
|
|
591
|
+
setIsLoading(false);
|
|
592
|
+
}
|
|
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]);
|
|
333
608
|
const clearError = (0, import_react.useCallback)(() => {
|
|
334
609
|
setError(null);
|
|
335
610
|
}, []);
|
|
@@ -345,6 +620,8 @@ function InformedAIProvider({ config, children }) {
|
|
|
345
620
|
sendQuickAction,
|
|
346
621
|
applyPendingValue,
|
|
347
622
|
skipTask,
|
|
623
|
+
startNewSession,
|
|
624
|
+
endSession,
|
|
348
625
|
clearError
|
|
349
626
|
};
|
|
350
627
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InformedAIContext.Provider, { value, children });
|
package/dist/index.mjs
CHANGED
|
@@ -37,16 +37,23 @@ var InformedAIClient = class {
|
|
|
37
37
|
// ========================================================================
|
|
38
38
|
/**
|
|
39
39
|
* Create a new session for a document type.
|
|
40
|
-
*
|
|
40
|
+
*
|
|
41
|
+
* @param documentTypeId - The document type to create a session for
|
|
42
|
+
* @param options.externalId - Your object's ID for idempotency (finds or creates document)
|
|
43
|
+
* @param options.initialData - Your current field values to sync (you're the source of truth)
|
|
41
44
|
*/
|
|
42
|
-
async createSession(documentTypeId) {
|
|
45
|
+
async createSession(documentTypeId, options) {
|
|
43
46
|
return this.request("/widget/sessions", {
|
|
44
47
|
method: "POST",
|
|
45
|
-
body: JSON.stringify({
|
|
48
|
+
body: JSON.stringify({
|
|
49
|
+
documentTypeId,
|
|
50
|
+
externalId: options?.externalId,
|
|
51
|
+
initialData: options?.initialData
|
|
52
|
+
})
|
|
46
53
|
});
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
|
-
* Get an existing session.
|
|
56
|
+
* Get an existing session with its document and documentType.
|
|
50
57
|
*/
|
|
51
58
|
async getSession(id) {
|
|
52
59
|
return this.request(`/widget/sessions/${id}`);
|
|
@@ -114,6 +121,51 @@ var InformedAIClient = class {
|
|
|
114
121
|
method: "POST"
|
|
115
122
|
});
|
|
116
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
|
+
}
|
|
117
169
|
// ========================================================================
|
|
118
170
|
// SSE Stream Processing
|
|
119
171
|
// ========================================================================
|
|
@@ -156,6 +208,15 @@ var InformedAIClient = class {
|
|
|
156
208
|
|
|
157
209
|
// src/context/InformedAIContext.tsx
|
|
158
210
|
import { jsx } from "react/jsx-runtime";
|
|
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
|
+
}
|
|
159
220
|
var InformedAIContext = createContext(null);
|
|
160
221
|
function useInformedAI() {
|
|
161
222
|
const context = useContext(InformedAIContext);
|
|
@@ -173,41 +234,140 @@ function InformedAIProvider({ config, children }) {
|
|
|
173
234
|
const [error, setError] = useState(null);
|
|
174
235
|
const [streamingContent, setStreamingContent] = useState("");
|
|
175
236
|
const clientRef = useRef(null);
|
|
237
|
+
const initRef = useRef(false);
|
|
238
|
+
const heartbeatIntervalRef = useRef(null);
|
|
239
|
+
const sessionIdRef = useRef(null);
|
|
240
|
+
const shouldPersist = config.persistSession ?? !!config.externalId;
|
|
241
|
+
const storageKey = getStorageKey(config.documentTypeId, config.externalId);
|
|
176
242
|
useEffect(() => {
|
|
177
243
|
clientRef.current = new InformedAIClient(config.apiUrl);
|
|
178
244
|
}, [config.apiUrl]);
|
|
245
|
+
const createNewSession = useCallback(async () => {
|
|
246
|
+
if (!clientRef.current) return null;
|
|
247
|
+
const result = await clientRef.current.createSession(
|
|
248
|
+
config.documentTypeId,
|
|
249
|
+
{
|
|
250
|
+
externalId: config.externalId,
|
|
251
|
+
initialData: config.initialData
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
const dt = {
|
|
255
|
+
id: result.documentType.id,
|
|
256
|
+
name: result.documentType.name,
|
|
257
|
+
displayName: result.documentType.displayName,
|
|
258
|
+
schema: result.documentType.schema,
|
|
259
|
+
workspaceId: "",
|
|
260
|
+
taskConfigs: {},
|
|
261
|
+
createdAt: "",
|
|
262
|
+
updatedAt: ""
|
|
263
|
+
};
|
|
264
|
+
if (shouldPersist && typeof window !== "undefined") {
|
|
265
|
+
try {
|
|
266
|
+
localStorage.setItem(storageKey, result.session.id);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { session: result.session, document: result.document, documentType: dt };
|
|
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]);
|
|
179
311
|
useEffect(() => {
|
|
312
|
+
if (initRef.current) return;
|
|
313
|
+
initRef.current = true;
|
|
180
314
|
async function initialize() {
|
|
181
315
|
if (!clientRef.current) return;
|
|
182
316
|
try {
|
|
183
317
|
setIsLoading(true);
|
|
184
318
|
setError(null);
|
|
185
|
-
let sess;
|
|
319
|
+
let sess = null;
|
|
186
320
|
let doc = null;
|
|
187
321
|
let dt = null;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
dt = {
|
|
195
|
-
id: result.documentType.id,
|
|
196
|
-
name: result.documentType.name,
|
|
197
|
-
displayName: result.documentType.displayName,
|
|
198
|
-
schema: result.documentType.schema,
|
|
199
|
-
workspaceId: "",
|
|
200
|
-
taskConfigs: {},
|
|
201
|
-
createdAt: "",
|
|
202
|
-
updatedAt: ""
|
|
203
|
-
};
|
|
322
|
+
let sessionIdToResume = config.sessionId;
|
|
323
|
+
if (!sessionIdToResume && shouldPersist && typeof window !== "undefined") {
|
|
324
|
+
try {
|
|
325
|
+
sessionIdToResume = localStorage.getItem(storageKey) || void 0;
|
|
326
|
+
} catch (e) {
|
|
327
|
+
}
|
|
204
328
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
329
|
+
if (sessionIdToResume) {
|
|
330
|
+
try {
|
|
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
|
+
};
|
|
345
|
+
} catch (e) {
|
|
346
|
+
console.warn("Failed to resume session, creating new one");
|
|
347
|
+
if (shouldPersist && typeof window !== "undefined") {
|
|
348
|
+
try {
|
|
349
|
+
localStorage.removeItem(storageKey);
|
|
350
|
+
} catch (e2) {
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (!sess) {
|
|
356
|
+
const result = await createNewSession();
|
|
357
|
+
if (result) {
|
|
358
|
+
sess = result.session;
|
|
359
|
+
doc = result.document;
|
|
360
|
+
dt = result.documentType;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (sess) {
|
|
364
|
+
setSession(sess);
|
|
365
|
+
if (doc) setDocument(doc);
|
|
366
|
+
if (dt) setDocumentType(dt);
|
|
367
|
+
config.onSessionChange?.(sess);
|
|
368
|
+
if (doc && dt) {
|
|
369
|
+
config.onReady?.({ session: sess, document: doc, documentType: dt });
|
|
370
|
+
}
|
|
211
371
|
}
|
|
212
372
|
} catch (err) {
|
|
213
373
|
const error2 = err instanceof Error ? err : new Error("Initialization failed");
|
|
@@ -218,7 +378,64 @@ function InformedAIProvider({ config, children }) {
|
|
|
218
378
|
}
|
|
219
379
|
}
|
|
220
380
|
initialize();
|
|
221
|
-
}, [config.documentTypeId, config.sessionId]);
|
|
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
|
+
}, []);
|
|
222
439
|
const handleSSEEvent = useCallback((event) => {
|
|
223
440
|
if (event.type === "content" && event.content) {
|
|
224
441
|
setStreamingContent((prev) => prev + event.content);
|
|
@@ -246,12 +463,17 @@ function InformedAIProvider({ config, children }) {
|
|
|
246
463
|
setError(null);
|
|
247
464
|
await clientRef.current.sendMessage(session.id, message, handleSSEEvent);
|
|
248
465
|
} catch (err) {
|
|
466
|
+
if (isSessionNotFoundError(err)) {
|
|
467
|
+
setIsStreaming(false);
|
|
468
|
+
await handleSessionDeleted();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
249
471
|
const error2 = err instanceof Error ? err : new Error("Failed to send message");
|
|
250
472
|
setError(error2);
|
|
251
473
|
config.onError?.(error2);
|
|
252
474
|
setIsStreaming(false);
|
|
253
475
|
}
|
|
254
|
-
}, [session, handleSSEEvent, config]);
|
|
476
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
255
477
|
const sendQuickAction = useCallback(async (action) => {
|
|
256
478
|
if (!clientRef.current || !session) return;
|
|
257
479
|
try {
|
|
@@ -266,13 +488,18 @@ function InformedAIProvider({ config, children }) {
|
|
|
266
488
|
setSession(newSession);
|
|
267
489
|
config.onSessionChange?.(newSession);
|
|
268
490
|
} catch (err) {
|
|
491
|
+
if (isSessionNotFoundError(err)) {
|
|
492
|
+
setIsStreaming(false);
|
|
493
|
+
await handleSessionDeleted();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
269
496
|
const error2 = err instanceof Error ? err : new Error("Failed to send quick action");
|
|
270
497
|
setError(error2);
|
|
271
498
|
config.onError?.(error2);
|
|
272
499
|
} finally {
|
|
273
500
|
setIsStreaming(false);
|
|
274
501
|
}
|
|
275
|
-
}, [session, handleSSEEvent, config]);
|
|
502
|
+
}, [session, handleSSEEvent, handleSessionDeleted, config]);
|
|
276
503
|
const applyPendingValue = useCallback(async () => {
|
|
277
504
|
if (!clientRef.current || !session) return;
|
|
278
505
|
try {
|
|
@@ -282,11 +509,15 @@ function InformedAIProvider({ config, children }) {
|
|
|
282
509
|
config.onSessionChange?.(result.session);
|
|
283
510
|
config.onFieldApply?.(result.appliedField, result.appliedValue);
|
|
284
511
|
} catch (err) {
|
|
512
|
+
if (isSessionNotFoundError(err)) {
|
|
513
|
+
await handleSessionDeleted();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
285
516
|
const error2 = err instanceof Error ? err : new Error("Failed to apply value");
|
|
286
517
|
setError(error2);
|
|
287
518
|
config.onError?.(error2);
|
|
288
519
|
}
|
|
289
|
-
}, [session, config]);
|
|
520
|
+
}, [session, handleSessionDeleted, config]);
|
|
290
521
|
const skipTask = useCallback(async () => {
|
|
291
522
|
if (!clientRef.current || !session) return;
|
|
292
523
|
try {
|
|
@@ -295,11 +526,55 @@ function InformedAIProvider({ config, children }) {
|
|
|
295
526
|
setSession(newSession);
|
|
296
527
|
config.onSessionChange?.(newSession);
|
|
297
528
|
} catch (err) {
|
|
529
|
+
if (isSessionNotFoundError(err)) {
|
|
530
|
+
await handleSessionDeleted();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
298
533
|
const error2 = err instanceof Error ? err : new Error("Failed to skip task");
|
|
299
534
|
setError(error2);
|
|
300
535
|
config.onError?.(error2);
|
|
301
536
|
}
|
|
302
|
-
}, [session, config]);
|
|
537
|
+
}, [session, handleSessionDeleted, config]);
|
|
538
|
+
const startNewSession = useCallback(async () => {
|
|
539
|
+
if (!clientRef.current) return;
|
|
540
|
+
try {
|
|
541
|
+
setIsLoading(true);
|
|
542
|
+
setError(null);
|
|
543
|
+
clearPersistedSession();
|
|
544
|
+
const result = await createNewSession();
|
|
545
|
+
if (result) {
|
|
546
|
+
setSession(result.session);
|
|
547
|
+
setDocument(result.document);
|
|
548
|
+
setDocumentType(result.documentType);
|
|
549
|
+
config.onSessionChange?.(result.session);
|
|
550
|
+
config.onReady?.({
|
|
551
|
+
session: result.session,
|
|
552
|
+
document: result.document,
|
|
553
|
+
documentType: result.documentType
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start new session");
|
|
558
|
+
setError(error2);
|
|
559
|
+
config.onError?.(error2);
|
|
560
|
+
} finally {
|
|
561
|
+
setIsLoading(false);
|
|
562
|
+
}
|
|
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]);
|
|
303
578
|
const clearError = useCallback(() => {
|
|
304
579
|
setError(null);
|
|
305
580
|
}, []);
|
|
@@ -315,6 +590,8 @@ function InformedAIProvider({ config, children }) {
|
|
|
315
590
|
sendQuickAction,
|
|
316
591
|
applyPendingValue,
|
|
317
592
|
skipTask,
|
|
593
|
+
startNewSession,
|
|
594
|
+
endSession,
|
|
318
595
|
clearError
|
|
319
596
|
};
|
|
320
597
|
return /* @__PURE__ */ jsx(InformedAIContext.Provider, { value, children });
|