@case-framework/survey-assistant 0.5.0 → 0.7.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.
Files changed (114) hide show
  1. package/README.md +9 -4
  2. package/dist/attachments-B0hg3vpT.mjs +11 -0
  3. package/dist/attachments-B0hg3vpT.mjs.map +1 -0
  4. package/dist/authoring-references-BJQ_1nMX.mjs +115 -0
  5. package/dist/authoring-references-BJQ_1nMX.mjs.map +1 -0
  6. package/dist/capabilities-1KNnNO1_.mjs +73 -0
  7. package/dist/capabilities-1KNnNO1_.mjs.map +1 -0
  8. package/dist/{capabilities-CnsVuJ1k.d.mts → capabilities-DVTfogjv.d.mts} +1 -1
  9. package/dist/{capabilities-CnsVuJ1k.d.mts.map → capabilities-DVTfogjv.d.mts.map} +1 -1
  10. package/dist/capabilities-default.d.mts +1 -1
  11. package/dist/capabilities-default.d.mts.map +1 -1
  12. package/dist/capabilities-default.mjs +47 -20
  13. package/dist/capabilities-default.mjs.map +1 -1
  14. package/dist/{constants-CFUWrRtl.mjs → constants-0UHGFcRJ.mjs} +1 -1
  15. package/dist/{constants-CFUWrRtl.mjs.map → constants-0UHGFcRJ.mjs.map} +1 -1
  16. package/dist/{constants-DbrJxxOh.d.mts → constants-Be91Gay9.d.mts} +4 -4
  17. package/dist/{constants-DbrJxxOh.d.mts.map → constants-Be91Gay9.d.mts.map} +1 -1
  18. package/dist/{controller-proxy-jWIPOZAv.mjs → controller-proxy-BH05e-C2.mjs} +21 -8
  19. package/dist/controller-proxy-BH05e-C2.mjs.map +1 -0
  20. package/dist/{controller-proxy-BdKF2yrq.d.mts → controller-proxy-Bd0ZHBPF.d.mts} +11 -11
  21. package/dist/controller-proxy-Bd0ZHBPF.d.mts.map +1 -0
  22. package/dist/{digest-Dyjch_sC.d.mts → digest-SJmoYVaK.d.mts} +2 -2
  23. package/dist/{digest-Dyjch_sC.d.mts.map → digest-SJmoYVaK.d.mts.map} +1 -1
  24. package/dist/digest.d.mts +1 -1
  25. package/dist/{engine-DcSdLRle.mjs → engine-Bo85Oyj0.mjs} +3129 -2435
  26. package/dist/engine-Bo85Oyj0.mjs.map +1 -0
  27. package/dist/engine.d.mts +3 -3
  28. package/dist/engine.mjs +3 -3
  29. package/dist/{fair-guidance-CEdJYccK.mjs → fair-guidance-CRYCRxQm.mjs} +1 -1
  30. package/dist/{fair-guidance-CEdJYccK.mjs.map → fair-guidance-CRYCRxQm.mjs.map} +1 -1
  31. package/dist/{index-Da_Hb9-1.d.mts → index-Tnqv-yKW.d.mts} +73 -67
  32. package/dist/index-Tnqv-yKW.d.mts.map +1 -0
  33. package/dist/index-l_EK4yK7.d.mts +404 -0
  34. package/dist/index-l_EK4yK7.d.mts.map +1 -0
  35. package/dist/{index-BApnIiCI.d.mts → index-nhrgbgO5.d.mts} +24 -5
  36. package/dist/index-nhrgbgO5.d.mts.map +1 -0
  37. package/dist/{index-DhNQKOwr.d.mts → index-x0qprHgK.d.mts} +19 -7
  38. package/dist/index-x0qprHgK.d.mts.map +1 -0
  39. package/dist/{lifecycle-DWv5CiSu.d.mts → lifecycle-Bx0aq_4S.d.mts} +8 -2
  40. package/dist/{lifecycle-DWv5CiSu.d.mts.map → lifecycle-Bx0aq_4S.d.mts.map} +1 -1
  41. package/dist/{memory-thread-repository-CvYZwni6.mjs → memory-thread-repository-KWHQWFEW.mjs} +45 -14
  42. package/dist/memory-thread-repository-KWHQWFEW.mjs.map +1 -0
  43. package/dist/{protocol-DJUP4gc0.mjs → protocol-BhzQbx81.mjs} +66 -16
  44. package/dist/protocol-BhzQbx81.mjs.map +1 -0
  45. package/dist/protocol.d.mts +4 -4
  46. package/dist/protocol.mjs +3 -2
  47. package/dist/react-B3ybjJ-X.mjs +1620 -0
  48. package/dist/react-B3ybjJ-X.mjs.map +1 -0
  49. package/dist/react-integration.d.mts +1 -1
  50. package/dist/react-integration.mjs +1 -1
  51. package/dist/react.d.mts +3 -3
  52. package/dist/react.mjs +3 -3
  53. package/dist/references/assistant-operations.md +45 -39
  54. package/dist/references/expressions.md +28 -0
  55. package/dist/references/item-types.md +1 -1
  56. package/dist/references/localization.md +5 -4
  57. package/dist/references/rich-text-content.md +60 -27
  58. package/dist/references/source-material-surveys.md +9 -2
  59. package/dist/references/survey-data-model.md +9 -1
  60. package/dist/{request-context-D1sC54ZA.mjs → request-context-CG4SWijt.mjs} +3 -3
  61. package/dist/{request-context-D1sC54ZA.mjs.map → request-context-CG4SWijt.mjs.map} +1 -1
  62. package/dist/{request-guardrails-h4t6u8mP.mjs → request-guardrails-TmgQtqp4.mjs} +5 -12
  63. package/dist/request-guardrails-TmgQtqp4.mjs.map +1 -0
  64. package/dist/server-agent.d.mts +19 -8
  65. package/dist/server-agent.d.mts.map +1 -1
  66. package/dist/server-agent.mjs +11 -13
  67. package/dist/server-agent.mjs.map +1 -1
  68. package/dist/server-runtime.d.mts +12 -6
  69. package/dist/server-runtime.d.mts.map +1 -1
  70. package/dist/server-runtime.mjs +9 -4
  71. package/dist/server-runtime.mjs.map +1 -1
  72. package/dist/server-tasks.d.mts +1 -1
  73. package/dist/server-tasks.mjs +5 -5
  74. package/dist/server-tools.d.mts +1 -1
  75. package/dist/server-tools.mjs +1 -1
  76. package/dist/server.d.mts +7 -7
  77. package/dist/server.mjs +6 -6
  78. package/dist/storage-postgres.d.mts +1 -1
  79. package/dist/storage-postgres.mjs +1 -1
  80. package/dist/{tasks-CCtxjhwh.d.mts → tasks-CYqrcaRO.d.mts} +16 -4
  81. package/dist/tasks-CYqrcaRO.d.mts.map +1 -0
  82. package/dist/{thread-documents-Dvo6XS7o.d.mts → thread-documents-rmd6nyX9.d.mts} +1 -1
  83. package/dist/{thread-documents-Dvo6XS7o.d.mts.map → thread-documents-rmd6nyX9.d.mts.map} +1 -1
  84. package/dist/{thread-handlers-Bt9cDo-F.d.mts → thread-handlers-DBAaU7BA.d.mts} +7 -5
  85. package/dist/thread-handlers-DBAaU7BA.d.mts.map +1 -0
  86. package/dist/{tools-3tq-XqI0.mjs → tools-BvSovehT.mjs} +433 -495
  87. package/dist/tools-BvSovehT.mjs.map +1 -0
  88. package/dist/ui.css +1 -1
  89. package/dist/ui.d.mts +5 -4
  90. package/dist/ui.d.mts.map +1 -1
  91. package/dist/ui.mjs +744 -131
  92. package/dist/ui.mjs.map +1 -1
  93. package/docs/integration.md +43 -3
  94. package/package.json +21 -19
  95. package/dist/authoring-references-yyYC4Ydi.mjs +0 -98
  96. package/dist/authoring-references-yyYC4Ydi.mjs.map +0 -1
  97. package/dist/controller-proxy-BdKF2yrq.d.mts.map +0 -1
  98. package/dist/controller-proxy-jWIPOZAv.mjs.map +0 -1
  99. package/dist/engine-DcSdLRle.mjs.map +0 -1
  100. package/dist/form-authoring-BQh40yDb.mjs +0 -155
  101. package/dist/form-authoring-BQh40yDb.mjs.map +0 -1
  102. package/dist/index-BApnIiCI.d.mts.map +0 -1
  103. package/dist/index-Da_Hb9-1.d.mts.map +0 -1
  104. package/dist/index-DhNQKOwr.d.mts.map +0 -1
  105. package/dist/index-dV6df0W3.d.mts +0 -224
  106. package/dist/index-dV6df0W3.d.mts.map +0 -1
  107. package/dist/memory-thread-repository-CvYZwni6.mjs.map +0 -1
  108. package/dist/protocol-DJUP4gc0.mjs.map +0 -1
  109. package/dist/react-CY6N7lVi.mjs +0 -768
  110. package/dist/react-CY6N7lVi.mjs.map +0 -1
  111. package/dist/request-guardrails-h4t6u8mP.mjs.map +0 -1
  112. package/dist/tasks-CCtxjhwh.d.mts.map +0 -1
  113. package/dist/thread-handlers-Bt9cDo-F.d.mts.map +0 -1
  114. package/dist/tools-3tq-XqI0.mjs.map +0 -1
@@ -0,0 +1,1620 @@
1
+ import { H as surveyDocumentVersionSchema, Lt as getSurveyAssistantEmbeddedBinaryMarker, M as surveyAssistantThreadSchema, Mt as parseSurveyAssistantError, N as surveyAssistantThreadTranscriptSchema, Rt as isSurveyAssistantSensitiveMetadataKey, f as surveyChangeSetSchema, j as surveyAssistantThreadListSchema, l as surveyAssistantTurnFinalSchema, n as SURVEY_ASSISTANT_TURN_FINAL_PART_TYPE, o as surveyAssistantApplyOutcomeSchema, p as surveyAssistantModelCatalogSchema, r as applyReceiptSchema, zt as redactSurveyAssistantDiagnosticSecrets } from "./protocol-BhzQbx81.mjs";
2
+ import { hashString } from "./digest.mjs";
3
+ import { a as applySurveyAssistantLaunchUIContext, l as readSurveyAssistantJson } from "./controller-proxy-BH05e-C2.mjs";
4
+ import { DefaultChatTransport } from "ai";
5
+ import { Zip, ZipPassThrough } from "fflate";
6
+ import { useCallback, useEffect, useEffectEvent, useMemo, useReducer, useRef, useState } from "react";
7
+ import { useChat } from "@ai-sdk/react";
8
+ //#region src/react/session-state.ts
9
+ const isSurveyAssistantTurnActive = ({ isStreaming, state }) => state !== "aborted" && (isStreaming || state === "submitting" || state === "streaming" || state === "applying");
10
+ const reduceSurveyAssistantSessionState = (_state, event) => {
11
+ switch (event.type) {
12
+ case "submit": return "submitting";
13
+ case "stream-started": return "streaming";
14
+ case "completed": return "completed";
15
+ case "apply-started": return "applying";
16
+ case "applied": return "applied";
17
+ case "failed": return "failed";
18
+ case "aborted": return "aborted";
19
+ case "reset": return event.hasTranscript ? "completed" : "idle";
20
+ }
21
+ };
22
+ //#endregion
23
+ //#region src/react/turn-authorization.ts
24
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25
+ const isCompletedToolPart = (part) => (part.type === "dynamic-tool" || part.type.startsWith("tool-")) && "state" in part && part.state === "output-available" && "output" in part;
26
+ const unwrapSurveyAssistantToolOutput = (output) => {
27
+ if (isRecord(output) && "output" in output && output.output !== void 0) return output.output;
28
+ return output;
29
+ };
30
+ const parseSurveyChangeSet = (output) => {
31
+ const parsed = surveyChangeSetSchema.safeParse(unwrapSurveyAssistantToolOutput(output));
32
+ return parsed.success ? parsed.data : null;
33
+ };
34
+ const collectSurveyChangeSets = (messages) => {
35
+ const changes = [];
36
+ for (const message of messages) for (const part of message.parts) {
37
+ if (!isCompletedToolPart(part)) continue;
38
+ const change = parseSurveyChangeSet(part.output);
39
+ if (change) changes.push(change);
40
+ }
41
+ return changes;
42
+ };
43
+ const findLastUserMessageIndex$1 = (messages) => {
44
+ for (let index = messages.length - 1; index >= 0; index -= 1) if (messages[index]?.role === "user") return index;
45
+ return -1;
46
+ };
47
+ const resolveLatestSurveyTurnAuthorization = (messages) => {
48
+ const lastUserIndex = findLastUserMessageIndex$1(messages);
49
+ if (lastUserIndex < 0) return { status: "pending" };
50
+ const turnMessages = messages.slice(lastUserIndex + 1);
51
+ const finalParts = turnMessages.flatMap((message) => message.parts).filter((part) => part.type === SURVEY_ASSISTANT_TURN_FINAL_PART_TYPE);
52
+ if (finalParts.length === 0) return { status: "pending" };
53
+ if (finalParts.length !== 1) return {
54
+ status: "invalid",
55
+ reason: "The assistant returned multiple turn finals."
56
+ };
57
+ const finalPart = finalParts[0];
58
+ const finalParse = surveyAssistantTurnFinalSchema.safeParse(finalPart && "data" in finalPart ? finalPart.data : void 0);
59
+ if (!finalParse.success) return {
60
+ status: "invalid",
61
+ reason: "The assistant returned an invalid turn final."
62
+ };
63
+ const turnFinal = finalParse.data;
64
+ if (turnFinal.status !== "completed") return {
65
+ status: turnFinal.status,
66
+ turnFinal,
67
+ changeSets: []
68
+ };
69
+ const candidates = collectSurveyChangeSets(turnMessages).filter((change) => change.turnId === turnFinal.turnId);
70
+ const candidatesById = /* @__PURE__ */ new Map();
71
+ for (const candidate of candidates) {
72
+ if (candidatesById.has(candidate.id)) return {
73
+ status: "invalid",
74
+ turnId: turnFinal.turnId,
75
+ reason: `The assistant returned duplicate change set "${candidate.id}".`
76
+ };
77
+ candidatesById.set(candidate.id, candidate);
78
+ }
79
+ for (const blockedChangeSetId of turnFinal.blockedChangeSetIds ?? []) {
80
+ const candidate = candidatesById.get(blockedChangeSetId);
81
+ if (!candidate || candidate.status !== "rejected" || candidate.validation.valid) return {
82
+ status: "invalid",
83
+ turnId: turnFinal.turnId,
84
+ reason: `The assistant did not provide blocked change set "${blockedChangeSetId}".`
85
+ };
86
+ }
87
+ const changeSets = [];
88
+ for (const changeSetId of turnFinal.executableChangeSetIds) {
89
+ const candidate = candidatesById.get(changeSetId);
90
+ if (!candidate || candidate.status !== "validated" || !candidate.validation.valid) return {
91
+ status: "invalid",
92
+ turnId: turnFinal.turnId,
93
+ reason: `The assistant did not provide authorized change set "${changeSetId}".`
94
+ };
95
+ changeSets.push(candidate);
96
+ }
97
+ return {
98
+ status: "completed",
99
+ turnFinal,
100
+ changeSets
101
+ };
102
+ };
103
+ const resolveSurveyTurnOutcome = ({ applyResultsByChangeId, authorization }) => {
104
+ if (authorization.status === "failed" || authorization.status === "aborted") return {
105
+ status: authorization.status,
106
+ terminationReason: authorization.turnFinal.terminationReason
107
+ };
108
+ if (authorization.status !== "completed") return null;
109
+ const blockedChangeSetCount = authorization.turnFinal.blockedChangeSetIds?.length ?? 0;
110
+ if (blockedChangeSetCount > 0) return {
111
+ status: "blocked",
112
+ blockedChangeSetCount
113
+ };
114
+ if (authorization.changeSets.length === 0) return null;
115
+ const results = authorization.changeSets.map((change) => applyResultsByChangeId[change.id]);
116
+ if (results.some((result) => result === void 0)) return null;
117
+ const failedChangeSetCount = results.filter((result) => result.status !== "applied").length;
118
+ if (failedChangeSetCount === 0) return null;
119
+ return {
120
+ status: "apply-failed",
121
+ appliedChangeSetCount: results.length - failedChangeSetCount,
122
+ failedChangeSetCount
123
+ };
124
+ };
125
+ const getSurveyDocumentVersionFromSnapshot = (snapshot) => ({ ...snapshot.version });
126
+ const createApplyReceipts = ({ afterSnapshot, beforeSnapshot, changeSets, results, turnFinal, createdAt = (/* @__PURE__ */ new Date()).toISOString() }) => {
127
+ const beforeVersion = getSurveyDocumentVersionFromSnapshot(beforeSnapshot);
128
+ const afterVersion = afterSnapshot ? getSurveyDocumentVersionFromSnapshot(afterSnapshot) : void 0;
129
+ const resultsByChangeId = new Map(results.map((result) => [result.changeId, result]));
130
+ return changeSets.map((changeSet) => {
131
+ const result = resultsByChangeId.get(changeSet.id);
132
+ const status = result?.status ?? "failed";
133
+ return applyReceiptSchema.parse({
134
+ protocolVersion: 1,
135
+ turnId: turnFinal.turnId,
136
+ changeSetId: changeSet.id,
137
+ beforeVersion,
138
+ ...status === "applied" && afterVersion ? { afterVersion } : {},
139
+ status,
140
+ operationCount: changeSet.operations.length,
141
+ ...status === "applied" && afterVersion?.historyIndex !== void 0 ? { commit: { index: afterVersion.historyIndex } } : {},
142
+ ...result?.issues ? { issues: result.issues } : {},
143
+ createdAt
144
+ });
145
+ });
146
+ };
147
+ //#endregion
148
+ //#region src/react/transport.ts
149
+ const findLastUserMessage = (messages) => {
150
+ for (let index = messages.length - 1; index >= 0; index -= 1) if (messages[index]?.role === "user") return messages[index];
151
+ };
152
+ const fetchSurveyAssistantChat = async (input, init) => {
153
+ const response = await globalThis.fetch(input, init);
154
+ if (!response.ok) await readSurveyAssistantJson(response);
155
+ return response;
156
+ };
157
+ const createSurveyAssistantTransport = ({ api = "/api/survey-assistant/chat", resourceId, surveyId, threadId }) => new DefaultChatTransport({
158
+ api,
159
+ body: {
160
+ resourceId,
161
+ ...surveyId ? { surveyId } : {},
162
+ threadId
163
+ },
164
+ fetch: fetchSurveyAssistantChat,
165
+ prepareSendMessagesRequest: ({ api, body, credentials, headers, id, messageId, messages, trigger }) => {
166
+ const latestUserMessage = findLastUserMessage(messages);
167
+ return {
168
+ api,
169
+ credentials,
170
+ headers,
171
+ body: {
172
+ ...body,
173
+ id,
174
+ messages: latestUserMessage ? [latestUserMessage] : [],
175
+ trigger,
176
+ messageId
177
+ }
178
+ };
179
+ }
180
+ });
181
+ //#endregion
182
+ //#region src/react/thread-client.ts
183
+ const createSurveyAssistantThreadClient = (api = "/api/survey-assistant/threads") => ({
184
+ async delete(threadId) {
185
+ const response = await fetch(`${api}/${encodeURIComponent(threadId)}`, { method: "DELETE" });
186
+ if (!response.ok) await readSurveyAssistantJson(response);
187
+ },
188
+ async create(title) {
189
+ const body = await readSurveyAssistantJson(await fetch(api, {
190
+ body: JSON.stringify(title ? { title } : {}),
191
+ headers: { "content-type": "application/json" },
192
+ method: "POST"
193
+ }));
194
+ return surveyAssistantThreadSchema.parse(body.thread);
195
+ },
196
+ async list() {
197
+ const body = await readSurveyAssistantJson(await fetch(api, { cache: "no-store" }));
198
+ return surveyAssistantThreadListSchema.parse(body).threads;
199
+ },
200
+ async listPage(options) {
201
+ const search = new URLSearchParams();
202
+ if (options?.cursor) search.set("cursor", options.cursor);
203
+ const body = await readSurveyAssistantJson(await fetch(`${api}${search.size ? `?${search}` : ""}`, { cache: "no-store" }));
204
+ return surveyAssistantThreadListSchema.parse(body);
205
+ },
206
+ async read(threadId, cursor, options) {
207
+ const search = cursor ? `?cursor=${encodeURIComponent(cursor)}` : "";
208
+ const body = await readSurveyAssistantJson(await fetch(`${api}/${encodeURIComponent(threadId)}${search}`, {
209
+ cache: "no-store",
210
+ signal: options?.signal
211
+ }));
212
+ return surveyAssistantThreadTranscriptSchema.parse(body);
213
+ },
214
+ async rename(threadId, title) {
215
+ const body = await readSurveyAssistantJson(await fetch(`${api}/${encodeURIComponent(threadId)}`, {
216
+ body: JSON.stringify({
217
+ mode: "manual",
218
+ title
219
+ }),
220
+ headers: { "content-type": "application/json" },
221
+ method: "PATCH"
222
+ }));
223
+ return surveyAssistantThreadSchema.parse(body.thread);
224
+ },
225
+ async setAutomaticTitle(threadId, title) {
226
+ const body = await readSurveyAssistantJson(await fetch(`${api}/${encodeURIComponent(threadId)}`, {
227
+ body: JSON.stringify({
228
+ mode: "automatic",
229
+ ...title ? { title } : {}
230
+ }),
231
+ headers: { "content-type": "application/json" },
232
+ method: "PATCH"
233
+ }));
234
+ return surveyAssistantThreadSchema.parse(body.thread);
235
+ }
236
+ });
237
+ //#endregion
238
+ //#region src/react/support-bundle-limits.ts
239
+ const MEBIBYTE = 1024 * 1024;
240
+ const SURVEY_ASSISTANT_DEFAULT_SUPPORT_BUNDLE_LIMITS = {
241
+ attemptEvents: 200,
242
+ attemptStoreBytes: 48 * MEBIBYTE,
243
+ bundleBytes: 64 * MEBIBYTE,
244
+ emailWarningBytes: 15 * MEBIBYTE,
245
+ retainedAttempts: 5,
246
+ transcriptBytes: 24 * MEBIBYTE,
247
+ transcriptMessages: 2e3,
248
+ transcriptPages: 20,
249
+ zipEntryBytes: 32 * MEBIBYTE
250
+ };
251
+ const resolveSurveyAssistantSupportBundleLimits = (overrides) => {
252
+ const limits = {
253
+ ...SURVEY_ASSISTANT_DEFAULT_SUPPORT_BUNDLE_LIMITS,
254
+ ...overrides
255
+ };
256
+ for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`Survey assistant support-bundle limit "${name}" must be a positive integer.`);
257
+ if (limits.transcriptBytes > limits.zipEntryBytes) throw new Error("The transcript limit cannot exceed the individual ZIP-entry limit.");
258
+ if (limits.zipEntryBytes > limits.bundleBytes) throw new Error("The individual ZIP-entry limit cannot exceed the complete bundle limit.");
259
+ return limits;
260
+ };
261
+ //#endregion
262
+ //#region src/react/diagnostic-store.ts
263
+ const createSurveyAssistantDiagnosticId = (prefix = "attempt") => {
264
+ const randomId = globalThis.crypto?.randomUUID?.();
265
+ if (randomId) return `${prefix}-${randomId}`.slice(0, 64);
266
+ const segment = () => Math.random().toString(36).slice(2).padEnd(12, "0");
267
+ return `${prefix}-${segment()}${segment()}`.slice(0, 64);
268
+ };
269
+ const cloneDiagnosticValue = (value) => {
270
+ if (typeof structuredClone === "function") return structuredClone(value);
271
+ return JSON.parse(JSON.stringify(value));
272
+ };
273
+ const getDataUrlBytes = (value) => {
274
+ const comma = value.indexOf(",");
275
+ if (comma < 0) return new TextEncoder().encode(value).byteLength;
276
+ const payload = value.slice(comma + 1).replace(/\s/g, "");
277
+ if (!/;base64,/i.test(value.slice(0, comma + 1))) try {
278
+ return new TextEncoder().encode(decodeURIComponent(payload)).byteLength;
279
+ } catch {
280
+ return new TextEncoder().encode(payload).byteLength;
281
+ }
282
+ const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
283
+ return Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
284
+ };
285
+ const stripMessageAttachmentBytes = (messages) => {
286
+ const cloned = cloneDiagnosticValue(messages);
287
+ if (!Array.isArray(cloned)) return [];
288
+ for (const message of cloned) {
289
+ if (!message || typeof message !== "object" || !("parts" in message)) continue;
290
+ const parts = message.parts;
291
+ if (!Array.isArray(parts)) continue;
292
+ for (const part of parts) if (part && typeof part === "object" && "type" in part && part.type === "file" && "url" in part && typeof part.url === "string" && part.url.startsWith("data:")) part.url = "[attachment bytes retained separately when available]";
293
+ }
294
+ return cloned;
295
+ };
296
+ const getApproximateBytes = (value) => {
297
+ try {
298
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
299
+ } catch {
300
+ return 0;
301
+ }
302
+ };
303
+ const toAttachmentReference = (attachment) => ({
304
+ bytes: attachment.bytes,
305
+ filename: attachment.filename,
306
+ mediaType: attachment.mediaType,
307
+ sessionContentDigest: attachment.sessionContentDigest,
308
+ storeKey: attachment.storeKey
309
+ });
310
+ const toPublicAttempt = ({ retainedBytes: _retainedBytes, ...attempt }) => cloneDiagnosticValue(attempt);
311
+ const safeEventFields = (fields) => {
312
+ if (!fields) return void 0;
313
+ const allowed = {};
314
+ for (const [key, value] of Object.entries(fields)) {
315
+ if (!/^(?:aborted|applied|changeSetId|code|count|durationMs|httpStatus|operationCount|retryable|status|turnId)$/.test(key)) continue;
316
+ if (value === null || value === void 0 || typeof value === "boolean" || typeof value === "number" || typeof value === "string") allowed[key] = typeof value === "string" ? value.slice(0, 240) : value;
317
+ }
318
+ return Object.keys(allowed).length > 0 ? allowed : void 0;
319
+ };
320
+ const createSurveyAssistantDiagnosticStore = (overrides) => {
321
+ const limits = resolveSurveyAssistantSupportBundleLimits(overrides);
322
+ const attempts = [];
323
+ const attachments = /* @__PURE__ */ new Map();
324
+ let attachmentSequence = 0;
325
+ const latest = (threadId) => [...attempts].reverse().find((attempt) => attempt.threadId === threadId);
326
+ const exportAttempt = (threadId) => latest(threadId);
327
+ const updateAttemptBytes = (attempt) => {
328
+ const { retainedBytes: _retainedBytes, ...value } = attempt;
329
+ attempt.retainedBytes = getApproximateBytes(value);
330
+ };
331
+ const retainedBytes = () => attempts.reduce((total, attempt) => total + attempt.retainedBytes, 0) + [...attachments.values()].reduce((total, attachment) => total + attachment.retainedBytes, 0);
332
+ const removeUnreferencedAttachments = () => {
333
+ const retainedKeys = new Set(attempts.flatMap((attempt) => attempt.attachments.map((item) => item.storeKey)));
334
+ for (const key of attachments.keys()) if (!retainedKeys.has(key)) attachments.delete(key);
335
+ };
336
+ const enforceLimits = () => {
337
+ if (attempts.length > limits.retainedAttempts) {
338
+ const newest = attempts.at(-1);
339
+ const newestFirst = [...attempts].reverse();
340
+ const retained = new Set([
341
+ newest,
342
+ ...newestFirst.filter((attempt) => attempt !== newest && (attempt.error !== void 0 || attempt.captureFailure !== void 0)),
343
+ ...newestFirst.filter((attempt) => attempt !== newest && attempt.error === void 0 && attempt.captureFailure === void 0)
344
+ ].slice(0, limits.retainedAttempts));
345
+ for (let index = attempts.length - 1; index >= 0; index -= 1) if (!retained.has(attempts[index])) attempts.splice(index, 1);
346
+ }
347
+ removeUnreferencedAttachments();
348
+ if (retainedBytes() <= limits.attemptStoreBytes) return;
349
+ const protectedAttempts = new Set([...new Set(attempts.map((attempt) => attempt.threadId))].map(exportAttempt).filter((attempt) => attempt !== void 0));
350
+ for (const attempt of attempts) {
351
+ if (protectedAttempts.has(attempt)) continue;
352
+ attempt.messages = void 0;
353
+ attempt.submittedSurvey = void 0;
354
+ updateAttemptBytes(attempt);
355
+ if (retainedBytes() <= limits.attemptStoreBytes) return;
356
+ }
357
+ const protectedAttachmentKeys = new Set([...protectedAttempts].flatMap((attempt) => attempt.attachments.map((attachment) => attachment.storeKey)));
358
+ for (const attachment of attachments.values()) {
359
+ if (protectedAttachmentKeys.has(attachment.storeKey)) continue;
360
+ attachment.dataUrl = void 0;
361
+ attachment.retainedBytes = 0;
362
+ if (retainedBytes() <= limits.attemptStoreBytes) return;
363
+ }
364
+ for (const attachment of attachments.values()) {
365
+ attachment.dataUrl = void 0;
366
+ attachment.retainedBytes = 0;
367
+ if (retainedBytes() <= limits.attemptStoreBytes) return;
368
+ }
369
+ for (const protectedAttempt of protectedAttempts) {
370
+ protectedAttempt.messages = void 0;
371
+ protectedAttempt.submittedSurvey = void 0;
372
+ protectedAttempt.captureFailure ??= "Diagnostic content was evicted because the browser limit was reached.";
373
+ updateAttemptBytes(protectedAttempt);
374
+ if (retainedBytes() <= limits.attemptStoreBytes) return;
375
+ }
376
+ };
377
+ const retainAttachment = (file) => {
378
+ const dataUrl = typeof file.url === "string" && file.url.startsWith("data:") ? file.url : void 0;
379
+ const mediaType = file.mediaType || "application/octet-stream";
380
+ const filename = file.filename?.trim() || "Attached document";
381
+ const bytes = dataUrl ? getDataUrlBytes(dataUrl) : 0;
382
+ const sessionContentDigest = hashString(dataUrl ?? `${mediaType}\u0000${filename}\u0000${bytes}`);
383
+ if (dataUrl) {
384
+ for (const candidate of attachments.values()) if (candidate.sessionContentDigest === sessionContentDigest && candidate.mediaType === mediaType && candidate.bytes === bytes && candidate.dataUrl === dataUrl) return toAttachmentReference(candidate);
385
+ }
386
+ attachmentSequence += 1;
387
+ const stored = {
388
+ bytes,
389
+ filename,
390
+ mediaType,
391
+ sessionContentDigest,
392
+ storeKey: `${sessionContentDigest}-${attachmentSequence}`,
393
+ retainedBytes: dataUrl?.length ?? 0,
394
+ ...dataUrl ? { dataUrl } : {}
395
+ };
396
+ attachments.set(stored.storeKey, stored);
397
+ return toAttachmentReference(stored);
398
+ };
399
+ const retainAttachmentReference = (reference) => {
400
+ const filename = reference.filename.trim() || "Attached document";
401
+ const mediaType = reference.mediaType || "application/octet-stream";
402
+ const sessionContentDigest = hashString(`${reference.documentId ?? "restored"}\u0000${mediaType}\u0000${filename}\u0000${reference.bytes}`);
403
+ attachmentSequence += 1;
404
+ const stored = {
405
+ bytes: reference.bytes,
406
+ filename,
407
+ mediaType,
408
+ retainedBytes: 0,
409
+ sessionContentDigest,
410
+ storeKey: `${sessionContentDigest}-${attachmentSequence}`
411
+ };
412
+ attachments.set(stored.storeKey, stored);
413
+ return toAttachmentReference(stored);
414
+ };
415
+ const recordEvent = (threadId, event, fields) => {
416
+ const attempt = latest(threadId);
417
+ if (!attempt) return;
418
+ attempt.events.push({
419
+ at: (/* @__PURE__ */ new Date()).toISOString(),
420
+ event: event.slice(0, 80),
421
+ ...safeEventFields(fields) ? { fields: safeEventFields(fields) } : {}
422
+ });
423
+ if (attempt.events.length > limits.attemptEvents) attempt.events.splice(0, attempt.events.length - limits.attemptEvents);
424
+ updateAttemptBytes(attempt);
425
+ };
426
+ return {
427
+ beginAttempt(input) {
428
+ const requestId = input.requestId ?? createSurveyAssistantDiagnosticId();
429
+ const attempt = {
430
+ attachments: [...input.files.map(retainAttachment), ...(input.attachmentReferences ?? []).map(retainAttachmentReference)],
431
+ events: [],
432
+ ...input.modelId ? { modelId: input.modelId } : {},
433
+ requestId,
434
+ resourceId: input.resourceId,
435
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
436
+ submittedSurvey: cloneDiagnosticValue(input.submittedSurvey),
437
+ threadId: input.threadId,
438
+ retainedBytes: 0
439
+ };
440
+ updateAttemptBytes(attempt);
441
+ attempts.push(attempt);
442
+ recordEvent(input.threadId, "submission");
443
+ enforceLimits();
444
+ return requestId;
445
+ },
446
+ deleteThread(threadId) {
447
+ for (let index = attempts.length - 1; index >= 0; index -= 1) if (attempts[index]?.threadId === threadId) attempts.splice(index, 1);
448
+ removeUnreferencedAttachments();
449
+ },
450
+ getAttachment(storeKey) {
451
+ return attachments.get(storeKey);
452
+ },
453
+ getExportAttempt(threadId) {
454
+ const attempt = exportAttempt(threadId);
455
+ return attempt ? toPublicAttempt(attempt) : void 0;
456
+ },
457
+ recordApplyOutcome(threadId, outcome) {
458
+ const attempt = latest(threadId);
459
+ if (!attempt) return;
460
+ attempt.applyOutcome = cloneDiagnosticValue(outcome);
461
+ recordEvent(threadId, "apply-result", { status: outcome.status });
462
+ enforceLimits();
463
+ },
464
+ recordApplyReceipts(threadId, receipts) {
465
+ const attempt = latest(threadId);
466
+ if (!attempt) return;
467
+ attempt.applyReceipts = cloneDiagnosticValue(receipts);
468
+ recordEvent(threadId, "apply-receipts", {
469
+ count: receipts.length,
470
+ operationCount: receipts.reduce((sum, receipt) => sum + receipt.operationCount, 0)
471
+ });
472
+ enforceLimits();
473
+ },
474
+ recordEvent,
475
+ recordFailure(threadId, messages, error) {
476
+ const attempt = latest(threadId);
477
+ if (!attempt) return;
478
+ try {
479
+ attempt.messages = stripMessageAttachmentBytes(messages);
480
+ attempt.error = cloneDiagnosticValue(error);
481
+ recordEvent(threadId, "failure", {
482
+ code: error.code,
483
+ httpStatus: error.upstreamStatus,
484
+ retryable: error.retryable
485
+ });
486
+ enforceLimits();
487
+ } catch {
488
+ attempt.messages = void 0;
489
+ attempt.captureFailure = "The failed assistant messages could not be captured.";
490
+ recordEvent(threadId, "capture-failed");
491
+ }
492
+ },
493
+ recordTurnFinal(threadId, turnFinal) {
494
+ const attempt = latest(threadId);
495
+ if (!attempt) return;
496
+ attempt.turnFinal = cloneDiagnosticValue(turnFinal);
497
+ recordEvent(threadId, "turn-final", {
498
+ durationMs: turnFinal.durationMs,
499
+ status: turnFinal.status,
500
+ turnId: turnFinal.turnId
501
+ });
502
+ enforceLimits();
503
+ }
504
+ };
505
+ };
506
+ //#endregion
507
+ //#region package.json
508
+ var version = "0.7.0";
509
+ //#endregion
510
+ //#region src/react/support-bundle.ts
511
+ const SURVEY_ASSISTANT_DEFAULT_SUPPORT_BUNDLE_SELECTION = {
512
+ attachments: true,
513
+ conversation: true,
514
+ survey: true
515
+ };
516
+ const isSurveyAssistantSupportBundleEnabled = (config) => config?.enabled !== false;
517
+ const assertSurveyAssistantSupportBundleEnabled = (config) => {
518
+ if (!isSurveyAssistantSupportBundleEnabled(config)) throw new Error("Support bundle export is disabled by host configuration.");
519
+ };
520
+ const encoder = new TextEncoder();
521
+ const encodeJson = (value) => encoder.encode(JSON.stringify(value));
522
+ const abortIfNeeded = (signal) => {
523
+ if (signal?.aborted) throw new DOMException("Support bundle preparation was cancelled.", "AbortError");
524
+ };
525
+ const sanitizeValue = (value, state, parent, key, depth = 0) => {
526
+ state.nodes += 1;
527
+ if (state.nodes > 1e5) {
528
+ state.redactions += 1;
529
+ state.truncated = true;
530
+ return { redacted: "node-limit" };
531
+ }
532
+ if (depth > 32) {
533
+ state.redactions += 1;
534
+ state.truncated = true;
535
+ return { redacted: "depth-limit" };
536
+ }
537
+ const binaryMarker = getSurveyAssistantEmbeddedBinaryMarker(value, parent, key);
538
+ if (binaryMarker) {
539
+ state.redactions += 1;
540
+ return binaryMarker;
541
+ }
542
+ if (key && isSurveyAssistantSensitiveMetadataKey(key)) {
543
+ state.redactions += 1;
544
+ return { redacted: "sensitive-field" };
545
+ }
546
+ if (typeof value === "string") {
547
+ const redacted = redactSurveyAssistantDiagnosticSecrets(value);
548
+ if (redacted !== value) state.redactions += 1;
549
+ return redacted.length > 2e5 ? `${redacted.slice(0, 199999)}…` : redacted;
550
+ }
551
+ if (value === null || typeof value === "number" || typeof value === "boolean") return value;
552
+ if (value === void 0) return void 0;
553
+ if (Array.isArray(value)) {
554
+ if (value.length > 2e4) state.truncated = true;
555
+ return value.slice(0, 2e4).map((item) => sanitizeValue(item, state, void 0, void 0, depth + 1));
556
+ }
557
+ if (typeof value !== "object") return String(value);
558
+ const record = value;
559
+ const sanitized = {};
560
+ const entries = Object.entries(record);
561
+ if (entries.length > 2e4) state.truncated = true;
562
+ for (const [childKey, childValue] of entries.slice(0, 2e4)) sanitized[childKey] = sanitizeValue(childValue, state, record, childKey, depth + 1);
563
+ return sanitized;
564
+ };
565
+ const sanitizeSurveyAssistantSupportValue = (value) => {
566
+ const state = {
567
+ nodes: 0,
568
+ redactions: 0,
569
+ truncated: false
570
+ };
571
+ const sanitized = sanitizeValue(value, state);
572
+ return {
573
+ redactions: state.redactions,
574
+ truncated: state.truncated,
575
+ value: sanitized
576
+ };
577
+ };
578
+ const createJsonEntry = (path, category, source, value) => {
579
+ const state = {
580
+ nodes: 0,
581
+ redactions: 0,
582
+ truncated: false
583
+ };
584
+ const sanitized = sanitizeValue(value, state);
585
+ return {
586
+ bytes: encodeJson({
587
+ redactions: state.redactions,
588
+ truncated: state.truncated,
589
+ value: sanitized
590
+ }),
591
+ category,
592
+ path,
593
+ source
594
+ };
595
+ };
596
+ const snapshotVersion = (snapshot) => snapshot?.version;
597
+ const extensionForMediaType = (mediaType) => {
598
+ switch (mediaType.toLocaleLowerCase()) {
599
+ case "application/pdf": return ".pdf";
600
+ case "image/jpeg": return ".jpg";
601
+ case "image/png": return ".png";
602
+ default: return ".bin";
603
+ }
604
+ };
605
+ const sanitizeFilename = (filename, mediaType) => {
606
+ const extension = extensionForMediaType(mediaType);
607
+ return `${filename.replace(/\.[a-z0-9]{1,10}$/i, "").normalize("NFKD").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "").slice(0, 100) || "attachment"}${extension}`;
608
+ };
609
+ const disambiguateFilenames = (files) => {
610
+ const used = /* @__PURE__ */ new Set();
611
+ return files.map((file) => {
612
+ const sanitized = sanitizeFilename(file.filename, file.mediaType);
613
+ const dot = sanitized.lastIndexOf(".");
614
+ const stem = dot > 0 ? sanitized.slice(0, dot) : sanitized;
615
+ const extension = dot > 0 ? sanitized.slice(dot) : "";
616
+ let candidate = sanitized;
617
+ let index = 2;
618
+ while (used.has(candidate.toLocaleLowerCase())) {
619
+ candidate = `${stem}-${index}${extension}`;
620
+ index += 1;
621
+ }
622
+ used.add(candidate.toLocaleLowerCase());
623
+ return candidate;
624
+ });
625
+ };
626
+ const dataUrlToBytes = (dataUrl) => {
627
+ const comma = dataUrl.indexOf(",");
628
+ if (comma < 0) return encoder.encode(dataUrl);
629
+ const metadata = dataUrl.slice(0, comma);
630
+ const payload = dataUrl.slice(comma + 1);
631
+ if (!/;base64$/i.test(metadata)) return encoder.encode(decodeURIComponent(payload));
632
+ const decoded = globalThis.atob(payload);
633
+ const bytes = new Uint8Array(decoded.length);
634
+ for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
635
+ return bytes;
636
+ };
637
+ const pushPreparedEntry = (entries, unavailable, entry, limits) => {
638
+ if (entry.bytes.byteLength > limits.zipEntryBytes) {
639
+ unavailable.push({
640
+ bytes: entry.bytes.byteLength,
641
+ category: entry.category,
642
+ path: entry.path,
643
+ reason: "too-large"
644
+ });
645
+ return;
646
+ }
647
+ entries.push(entry);
648
+ };
649
+ const prepareSurveyAssistantSupportBundle = async ({ config, controller, diagnosticStore, resourceId, signal, threadClient, threadId }) => {
650
+ assertSurveyAssistantSupportBundleEnabled(config);
651
+ const limits = resolveSurveyAssistantSupportBundleLimits(config?.limits);
652
+ const entries = [];
653
+ const unavailable = [];
654
+ const attempt = diagnosticStore.getExportAttempt(threadId);
655
+ const currentSurveyCapturedAt = (/* @__PURE__ */ new Date()).toISOString();
656
+ let currentSurvey = null;
657
+ try {
658
+ currentSurvey = controller?.getSnapshot() ?? null;
659
+ if (currentSurvey) currentSurvey = typeof structuredClone === "function" ? structuredClone(currentSurvey) : JSON.parse(JSON.stringify(currentSurvey));
660
+ } catch {
661
+ unavailable.push({
662
+ category: "survey",
663
+ reason: "capture-failed",
664
+ path: "survey/current-survey.json"
665
+ });
666
+ }
667
+ abortIfNeeded(signal);
668
+ if (!attempt) unavailable.push({
669
+ category: "conversation",
670
+ path: "conversation/attempt-messages.json",
671
+ reason: "no-attempt-retained"
672
+ }, {
673
+ category: "conversation",
674
+ path: "diagnostics/client-apply-outcome.json",
675
+ reason: "no-attempt-retained"
676
+ }, {
677
+ category: "conversation",
678
+ path: "diagnostics/client-apply-receipts.json",
679
+ reason: "no-attempt-retained"
680
+ }, {
681
+ category: "attachments",
682
+ reason: "no-attempt-retained"
683
+ });
684
+ if (attempt?.messages) pushPreparedEntry(entries, unavailable, createJsonEntry("conversation/attempt-messages.json", "conversation", "browser-session", attempt.messages), limits);
685
+ else if (attempt?.captureFailure) unavailable.push({
686
+ category: "conversation",
687
+ reason: "capture-failed",
688
+ path: "conversation/attempt-messages.json"
689
+ });
690
+ else if (attempt) unavailable.push({
691
+ category: "conversation",
692
+ reason: "not-retained",
693
+ path: "conversation/attempt-messages.json"
694
+ });
695
+ if (attempt?.applyOutcome) pushPreparedEntry(entries, unavailable, createJsonEntry("diagnostics/client-apply-outcome.json", "conversation", "browser-session", attempt.applyOutcome), limits);
696
+ if (attempt?.applyReceipts) pushPreparedEntry(entries, unavailable, createJsonEntry("diagnostics/client-apply-receipts.json", "conversation", "browser-session", attempt.applyReceipts), limits);
697
+ const transcriptMessages = [];
698
+ let transcriptBytes = 0;
699
+ let transcriptCursor;
700
+ let transcriptRedactions = 0;
701
+ let transcriptTruncated = false;
702
+ try {
703
+ for (let page = 0; page < limits.transcriptPages; page += 1) {
704
+ abortIfNeeded(signal);
705
+ const transcript = await threadClient.read(threadId, transcriptCursor, { signal });
706
+ abortIfNeeded(signal);
707
+ const pageState = {
708
+ nodes: 0,
709
+ redactions: 0,
710
+ truncated: false
711
+ };
712
+ const sanitizedPage = sanitizeValue(transcript.messages, pageState);
713
+ transcriptRedactions += pageState.redactions;
714
+ transcriptTruncated ||= pageState.truncated;
715
+ const pageMessages = Array.isArray(sanitizedPage) ? sanitizedPage : [];
716
+ const pageBytes = encodeJson(pageMessages).byteLength;
717
+ if (transcriptMessages.length + pageMessages.length > limits.transcriptMessages || transcriptBytes + pageBytes > limits.transcriptBytes) {
718
+ transcriptTruncated = true;
719
+ break;
720
+ }
721
+ transcriptMessages.unshift(...pageMessages);
722
+ transcriptBytes += pageBytes;
723
+ transcriptCursor = transcript.nextCursor;
724
+ if (!transcriptCursor) break;
725
+ if (page === limits.transcriptPages - 1) transcriptTruncated = true;
726
+ }
727
+ pushPreparedEntry(entries, unavailable, {
728
+ bytes: encodeJson({
729
+ messages: transcriptMessages,
730
+ redactions: transcriptRedactions,
731
+ truncated: transcriptTruncated
732
+ }),
733
+ category: "conversation",
734
+ path: "conversation/transcript.json",
735
+ source: "authorized-transcript"
736
+ }, limits);
737
+ } catch (error) {
738
+ if (signal?.aborted) throw error;
739
+ unavailable.push({
740
+ category: "conversation",
741
+ reason: "unavailable",
742
+ path: "conversation/transcript.json"
743
+ });
744
+ }
745
+ if (attempt?.submittedSurvey !== void 0) pushPreparedEntry(entries, unavailable, createJsonEntry("survey/submitted-survey.json", "survey", "browser-session", {
746
+ snapshot: attempt.submittedSurvey,
747
+ version: snapshotVersion(attempt.submittedSurvey)
748
+ }), limits);
749
+ else unavailable.push({
750
+ category: "survey",
751
+ reason: attempt ? "not-retained" : "no-attempt-retained",
752
+ path: "survey/submitted-survey.json"
753
+ });
754
+ if (currentSurvey) pushPreparedEntry(entries, unavailable, createJsonEntry("survey/current-survey.json", "survey", "browser-session", {
755
+ capturedAt: currentSurveyCapturedAt,
756
+ snapshot: currentSurvey,
757
+ version: snapshotVersion(currentSurvey)
758
+ }), limits);
759
+ if (attempt) {
760
+ const availableAttachments = attempt.attachments.map((reference) => ({
761
+ reference,
762
+ stored: diagnosticStore.getAttachment(reference.storeKey)
763
+ })).filter((item) => Boolean(item.stored?.dataUrl));
764
+ const filenames = disambiguateFilenames(availableAttachments.map(({ reference }) => reference));
765
+ for (const [index, { reference, stored }] of availableAttachments.entries()) {
766
+ abortIfNeeded(signal);
767
+ pushPreparedEntry(entries, unavailable, {
768
+ bytes: dataUrlToBytes(stored.dataUrl),
769
+ category: "attachments",
770
+ path: `attachments/${filenames[index]}`,
771
+ source: "browser-session"
772
+ }, limits);
773
+ }
774
+ for (const reference of attempt.attachments) if (!diagnosticStore.getAttachment(reference.storeKey)?.dataUrl) unavailable.push({
775
+ bytes: reference.bytes,
776
+ category: "attachments",
777
+ filename: reference.filename,
778
+ mediaType: reference.mediaType,
779
+ reason: "not-retained"
780
+ });
781
+ }
782
+ const categoryBytes = {
783
+ attachments: 0,
784
+ conversation: 0,
785
+ survey: 0
786
+ };
787
+ for (const entry of entries) categoryBytes[entry.category] += entry.bytes.byteLength;
788
+ return {
789
+ attempt,
790
+ categoryBytes,
791
+ currentSurveyCapturedAt,
792
+ entries,
793
+ limits,
794
+ resourceId,
795
+ threadId,
796
+ unavailable
797
+ };
798
+ };
799
+ const validateZipPath = (path) => {
800
+ if (!path || path.startsWith("/") || path.includes("\\") || path.split("/").includes("..")) throw new Error(`Unsafe support-bundle ZIP path: ${path}`);
801
+ };
802
+ const toBlobPart = (bytes) => {
803
+ if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) return bytes.buffer;
804
+ return bytes.slice().buffer;
805
+ };
806
+ const createZipBlob = (entries) => {
807
+ const paths = /* @__PURE__ */ new Set();
808
+ for (const entry of entries) {
809
+ validateZipPath(entry.path);
810
+ if (paths.has(entry.path)) throw new Error(`Duplicate support-bundle ZIP path: ${entry.path}`);
811
+ paths.add(entry.path);
812
+ }
813
+ return new Promise((resolve, reject) => {
814
+ const chunks = [];
815
+ const zip = new Zip((error, data, final) => {
816
+ if (error) {
817
+ reject(error);
818
+ return;
819
+ }
820
+ chunks.push(toBlobPart(data));
821
+ if (final) resolve(new Blob(chunks, { type: "application/zip" }));
822
+ });
823
+ try {
824
+ for (const entry of entries) {
825
+ const file = new ZipPassThrough(entry.path);
826
+ zip.add(file);
827
+ file.push(entry.bytes, true);
828
+ }
829
+ zip.end();
830
+ } catch (error) {
831
+ zip.terminate();
832
+ reject(error);
833
+ }
834
+ });
835
+ };
836
+ const categoryDescriptors = (prepared, selection) => Object.keys(selection).map((category) => {
837
+ const entries = prepared.entries.filter((entry) => entry.category === category);
838
+ return selection[category] ? {
839
+ category,
840
+ included: true,
841
+ itemCount: entries.length,
842
+ totalBytes: entries.reduce((total, entry) => total + entry.bytes.byteLength, 0),
843
+ entries: entries.map((entry) => ({
844
+ bytes: entry.bytes.byteLength,
845
+ path: entry.path,
846
+ source: entry.source
847
+ }))
848
+ } : {
849
+ category,
850
+ included: false,
851
+ itemCount: entries.length,
852
+ totalBytes: entries.reduce((total, entry) => total + entry.bytes.byteLength, 0)
853
+ };
854
+ });
855
+ const createAttemptMetadata = (prepared, selection) => {
856
+ const attempt = prepared.attempt;
857
+ return {
858
+ attemptAvailable: Boolean(attempt),
859
+ ...attempt ? {
860
+ captureFailure: attempt.captureFailure,
861
+ error: attempt.error,
862
+ events: attempt.events,
863
+ modelId: attempt.modelId,
864
+ requestId: attempt.requestId,
865
+ resourceId: attempt.resourceId,
866
+ submittedAt: attempt.submittedAt,
867
+ threadId: attempt.threadId,
868
+ turnFinal: attempt.turnFinal
869
+ } : { threadId: prepared.threadId },
870
+ categories: categoryDescriptors(prepared, selection)
871
+ };
872
+ };
873
+ const encodeSanitizedJson = (value) => encodeJson(sanitizeSurveyAssistantSupportValue(value).value);
874
+ const createRequiredEntries = (prepared, selection, config, generatedAt) => {
875
+ const descriptors = categoryDescriptors(prepared, selection);
876
+ const unavailable = Object.keys(selection).flatMap((category) => {
877
+ const categoryUnavailable = prepared.unavailable.filter((item) => item.category === category);
878
+ if (categoryUnavailable.length === 0) return [];
879
+ if (selection[category]) return categoryUnavailable;
880
+ return [{
881
+ category,
882
+ itemCount: categoryUnavailable.length,
883
+ totalBytes: categoryUnavailable.reduce((total, item) => total + (item.bytes ?? 0), 0)
884
+ }];
885
+ });
886
+ const manifest = {
887
+ schemaVersion: 1,
888
+ generatedAt,
889
+ packageVersion: config?.packageVersion ?? version,
890
+ hostBuildVersion: config?.hostBuildVersion,
891
+ resourceId: prepared.resourceId,
892
+ threadId: prepared.threadId,
893
+ requestId: prepared.attempt?.requestId,
894
+ turnId: prepared.attempt?.turnFinal?.turnId,
895
+ modelId: prepared.attempt?.modelId,
896
+ categories: descriptors,
897
+ unavailable,
898
+ currentSurveyCapturedAt: prepared.currentSurveyCapturedAt
899
+ };
900
+ const readme = [
901
+ "CASE Survey Assistant support bundle",
902
+ "",
903
+ `Generated: ${generatedAt}`,
904
+ "This archive contains diagnostic data selected by the user, including visible model reasoning when conversation data is included.",
905
+ "Nothing was uploaded or emailed automatically.",
906
+ "See manifest.json for the authoritative inventory and availability information.",
907
+ "Treat the archive as potentially sensitive."
908
+ ].join("\n");
909
+ return [
910
+ {
911
+ bytes: encodeSanitizedJson(manifest),
912
+ path: "manifest.json"
913
+ },
914
+ {
915
+ bytes: encoder.encode(readme),
916
+ path: "README.txt"
917
+ },
918
+ {
919
+ bytes: encodeSanitizedJson(createAttemptMetadata(prepared, selection)),
920
+ path: "diagnostics/client-attempt.json"
921
+ }
922
+ ];
923
+ };
924
+ const estimateArchiveBytes = (entries) => entries.reduce((total, entry) => total + entry.bytes.byteLength + encoder.encode(entry.path).byteLength * 2 + 110, 22);
925
+ const estimateSurveyAssistantSupportBundleBytes = (prepared, selection, config) => {
926
+ if (!isSurveyAssistantSupportBundleEnabled(config)) return 0;
927
+ return estimateArchiveBytes([...createRequiredEntries(prepared, selection, config, (/* @__PURE__ */ new Date()).toISOString()), ...prepared.entries.filter((entry) => selection[entry.category]).map(({ bytes, path }) => ({
928
+ bytes,
929
+ path
930
+ }))]);
931
+ };
932
+ const generateSurveyAssistantSupportBundle = async (prepared, selection, config) => {
933
+ assertSurveyAssistantSupportBundleEnabled(config);
934
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
935
+ const selectedEntries = prepared.entries.filter((entry) => selection[entry.category]);
936
+ const allEntries = [...createRequiredEntries(prepared, selection, config, generatedAt), ...selectedEntries.map(({ bytes, path }) => ({
937
+ bytes,
938
+ path
939
+ }))];
940
+ if (estimateArchiveBytes(allEntries) > prepared.limits.bundleBytes) throw new Error("The selected support-bundle data exceeds the configured bundle limit.");
941
+ const blob = await createZipBlob(allEntries);
942
+ if (blob.size > prepared.limits.bundleBytes) throw new Error("The selected support-bundle data exceeds the configured bundle limit.");
943
+ return {
944
+ blob,
945
+ filename: `case-survey-assistant-support-${(prepared.attempt?.requestId ?? prepared.attempt?.turnFinal?.turnId ?? "unidentified").replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64) || "unidentified"}-${generatedAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z")}.zip`,
946
+ generatedAt,
947
+ includedCategories: Object.keys(selection).filter((category) => selection[category]),
948
+ size: blob.size
949
+ };
950
+ };
951
+ const downloadSurveyAssistantSupportBundle = (bundle) => {
952
+ const url = URL.createObjectURL(bundle.blob);
953
+ const anchor = document.createElement("a");
954
+ anchor.href = url;
955
+ anchor.download = bundle.filename;
956
+ anchor.style.display = "none";
957
+ document.body.append(anchor);
958
+ anchor.click();
959
+ anchor.remove();
960
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
961
+ };
962
+ const createSurveyAssistantSupportDetails = ({ bundle, prepared }) => [
963
+ `File: ${bundle.filename}`,
964
+ `Request: ${prepared.attempt?.requestId ?? "unavailable"}`,
965
+ `Model: ${prepared.attempt?.modelId ?? "unavailable"}`,
966
+ `Error category: ${prepared.attempt?.error?.code ?? "unavailable"}`,
967
+ `Generated: ${bundle.generatedAt}`
968
+ ].join("\n");
969
+ const createSurveyAssistantSupportMailto = ({ bundle, config, prepared }) => {
970
+ if (!isSurveyAssistantSupportBundleEnabled(config)) return void 0;
971
+ if (!config.supportEmail?.trim()) return void 0;
972
+ const subject = `${config.subjectPrefix?.trim() || "CASE Survey Assistant support"} ${prepared.attempt?.requestId ?? ""}`.trim();
973
+ const body = [
974
+ "IMPORTANT: ATTACH THE DOWNLOADED ZIP FILE TO THIS EMAIL BEFORE SENDING.",
975
+ "",
976
+ "A Survey Assistant support bundle has been downloaded for this issue.",
977
+ "",
978
+ createSurveyAssistantSupportDetails({
979
+ bundle,
980
+ prepared
981
+ })
982
+ ].join("\n");
983
+ return `mailto:${encodeURIComponent(config.supportEmail.trim())}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
984
+ };
985
+ //#endregion
986
+ //#region src/react/model-catalog.ts
987
+ const DEFAULT_MODEL_STORAGE_KEY = "survey-assistant-selected-model";
988
+ const createSurveyAssistantModelCatalogClient = (api = "/api/survey-assistant/models") => ({ async get() {
989
+ const body = await readSurveyAssistantJson(await fetch(api, { cache: "no-store" }), "The assistant model catalog could not be loaded.");
990
+ return surveyAssistantModelCatalogSchema.parse(body);
991
+ } });
992
+ const useSurveyAssistantModelCatalog = ({ api, client: providedClient, onModelChange, storageKey = DEFAULT_MODEL_STORAGE_KEY } = {}) => {
993
+ const [models, setModels] = useState([]);
994
+ const [modelId, setModelId] = useState();
995
+ const [error, setError] = useState(null);
996
+ const [loading, setLoading] = useState(true);
997
+ const [reloadToken, setReloadToken] = useState(0);
998
+ useEffect(() => {
999
+ let cancelled = false;
1000
+ const client = providedClient ?? createSurveyAssistantModelCatalogClient(api);
1001
+ setLoading(true);
1002
+ setError(null);
1003
+ client.get().then((catalog) => {
1004
+ if (cancelled) return;
1005
+ const rememberedId = storageKey && typeof window !== "undefined" ? window.localStorage.getItem(storageKey) ?? void 0 : void 0;
1006
+ const selectedId = rememberedId && catalog.models.some((model) => model.id === rememberedId) ? rememberedId : catalog.defaultModelId;
1007
+ setModels(catalog.models);
1008
+ setModelId(selectedId);
1009
+ onModelChange?.(selectedId);
1010
+ }).catch((reason) => {
1011
+ if (!cancelled) setError(reason instanceof Error ? reason : new Error(String(reason)));
1012
+ }).finally(() => {
1013
+ if (!cancelled) setLoading(false);
1014
+ });
1015
+ return () => {
1016
+ cancelled = true;
1017
+ };
1018
+ }, [
1019
+ api,
1020
+ onModelChange,
1021
+ providedClient,
1022
+ reloadToken,
1023
+ storageKey
1024
+ ]);
1025
+ const selectModel = useCallback((nextModelId) => {
1026
+ if (!models.some((model) => model.id === nextModelId)) return;
1027
+ setModelId(nextModelId);
1028
+ if (storageKey && typeof window !== "undefined") window.localStorage.setItem(storageKey, nextModelId);
1029
+ onModelChange?.(nextModelId);
1030
+ }, [
1031
+ models,
1032
+ onModelChange,
1033
+ storageKey
1034
+ ]);
1035
+ return {
1036
+ error,
1037
+ loading,
1038
+ modelId,
1039
+ models,
1040
+ reload: useCallback(() => setReloadToken((current) => current + 1), []),
1041
+ selectModel
1042
+ };
1043
+ };
1044
+ //#endregion
1045
+ //#region src/react/document-baseline.ts
1046
+ const STORAGE_PREFIX = "case-survey-assistant:settled-document:";
1047
+ const memoryBaselines = /* @__PURE__ */ new Map();
1048
+ const getStorageKey = (resourceId, threadId) => `${STORAGE_PREFIX}${encodeURIComponent(resourceId)}:${encodeURIComponent(threadId)}`;
1049
+ const getSessionStorage = () => {
1050
+ if (typeof document === "undefined") return void 0;
1051
+ try {
1052
+ return window.sessionStorage;
1053
+ } catch {
1054
+ return;
1055
+ }
1056
+ };
1057
+ const readSettledDocumentVersion = ({ resourceId, threadId }) => {
1058
+ const storageKey = getStorageKey(resourceId, threadId);
1059
+ const storedInMemory = memoryBaselines.get(storageKey);
1060
+ if (storedInMemory) return { ...storedInMemory };
1061
+ const serialized = getSessionStorage()?.getItem(storageKey);
1062
+ if (!serialized) return void 0;
1063
+ try {
1064
+ const parsed = surveyDocumentVersionSchema.safeParse(JSON.parse(serialized));
1065
+ if (!parsed.success) return void 0;
1066
+ memoryBaselines.set(storageKey, parsed.data);
1067
+ return { ...parsed.data };
1068
+ } catch {
1069
+ return;
1070
+ }
1071
+ };
1072
+ const writeSettledDocumentVersion = ({ resourceId, snapshot, threadId }) => {
1073
+ const storageKey = getStorageKey(resourceId, threadId);
1074
+ const version = surveyDocumentVersionSchema.parse(snapshot.version);
1075
+ memoryBaselines.set(storageKey, version);
1076
+ try {
1077
+ getSessionStorage()?.setItem(storageKey, JSON.stringify(version));
1078
+ } catch {}
1079
+ };
1080
+ //#endregion
1081
+ //#region src/react/use-survey-assistant-session.ts
1082
+ const createLocalId = (prefix) => {
1083
+ return createSurveyAssistantDiagnosticId(prefix);
1084
+ };
1085
+ const compactOutcomeText = (value, maximum, fallback) => {
1086
+ const normalized = value.trim() || fallback;
1087
+ return normalized.length > maximum ? `${normalized.slice(0, maximum - 1)}…` : normalized;
1088
+ };
1089
+ const createApplyOutcome = ({ message, preparedChanges, results, status, turnId }) => surveyAssistantApplyOutcomeSchema.parse({
1090
+ protocolVersion: 1,
1091
+ turnId,
1092
+ status,
1093
+ message: compactOutcomeText(message, 2e3, "No lifecycle detail was reported."),
1094
+ ...results && results.length > 0 ? { results: results.slice(0, 100).map((result) => ({
1095
+ status: result.status,
1096
+ changeId: compactOutcomeText(result.changeId, 240, "unknown-change"),
1097
+ summary: compactOutcomeText(result.summary, 500, "Survey change"),
1098
+ message: compactOutcomeText(result.message, 2e3, "No apply detail was reported."),
1099
+ appliedOperations: result.appliedOperations
1100
+ })) } : {},
1101
+ ...preparedChanges && preparedChanges.length > 0 ? { preparedChanges: preparedChanges.slice(-24).map((change) => ({
1102
+ changeId: compactOutcomeText(change.id, 240, "unknown-change"),
1103
+ status: change.status,
1104
+ summary: compactOutcomeText(change.summary, 500, "Survey change"),
1105
+ operationCount: change.operations.length
1106
+ })) } : {},
1107
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1108
+ });
1109
+ const getAssistantTextParts = (message) => message.parts.filter((part) => part.type === "text" && part.text.trim().length > 0);
1110
+ const findLastAssistantTextMessageIndex = (messages, endExclusive = messages.length) => {
1111
+ for (let index = Math.min(messages.length, endExclusive) - 1; index >= 0; index -= 1) {
1112
+ const message = messages[index];
1113
+ if (message?.role === "assistant" && getAssistantTextParts(message).length > 0) return index;
1114
+ }
1115
+ return -1;
1116
+ };
1117
+ const findLastUserMessageIndex = (messages) => {
1118
+ for (let index = messages.length - 1; index >= 0; index -= 1) if (messages[index]?.role === "user") return index;
1119
+ return -1;
1120
+ };
1121
+ const pruneFailedMessagesForDisplay = (messages) => {
1122
+ const lastUserIndex = findLastUserMessageIndex(messages);
1123
+ const lastCompletedAssistantIndex = findLastAssistantTextMessageIndex(messages);
1124
+ if (lastUserIndex > lastCompletedAssistantIndex) return messages.slice(0, lastUserIndex + 1);
1125
+ return lastCompletedAssistantIndex >= 0 ? messages.slice(0, lastCompletedAssistantIndex + 1) : [];
1126
+ };
1127
+ const hasCompletedAssistantResponseAfterLastUser = (messages) => {
1128
+ const lastUserIndex = findLastUserMessageIndex(messages);
1129
+ return lastUserIndex >= 0 && findLastAssistantTextMessageIndex(messages) > lastUserIndex;
1130
+ };
1131
+ const collectChangeSetsAfterLastUser = (messages) => {
1132
+ const lastUserIndex = findLastUserMessageIndex(messages);
1133
+ return lastUserIndex < 0 ? [] : collectSurveyChangeSets(messages.slice(lastUserIndex + 1));
1134
+ };
1135
+ const getInitialSessionState = (messages) => {
1136
+ if (messages.length === 0) return "idle";
1137
+ const authorization = resolveLatestSurveyTurnAuthorization(messages);
1138
+ if (authorization.status === "failed") return "failed";
1139
+ if (authorization.status === "aborted") return "aborted";
1140
+ return "completed";
1141
+ };
1142
+ const resolveSurveyAssistantReviewDecision = async ({ changeSets, onReviewChangeSets, turnFinal }) => {
1143
+ if (!onReviewChangeSets) throw new Error("No survey assistant review adapter is available.");
1144
+ return onReviewChangeSets(changeSets, turnFinal);
1145
+ };
1146
+ const useSurveyAssistantSession = ({ api, applyPolicy, controller, diagnosticStore, initialMessages = [], modelId, onApplyReceipts, onSettleApply, onReviewChangeSets, onTurnActiveChange, resourceId: controlledResourceId, surveyId, threadId: controlledThreadId }) => {
1147
+ const [localResourceId] = useState(() => createLocalId("survey-assistant-local-user"));
1148
+ const [localThreadId] = useState(() => createLocalId("survey-assistant-local-thread"));
1149
+ const resourceId = controlledResourceId ?? localResourceId;
1150
+ const threadId = controlledThreadId ?? localThreadId;
1151
+ const applyOutcomeScopeKey = `${resourceId}\u0000${threadId}`;
1152
+ const [state, dispatch] = useReducer(reduceSurveyAssistantSessionState, initialMessages, getInitialSessionState);
1153
+ const [applyResultsByChangeId, setApplyResultsByChangeId] = useState({});
1154
+ const [authorizationError, setAuthorizationError] = useState(null);
1155
+ const handledTurnIdsRef = useRef(/* @__PURE__ */ new Set());
1156
+ const localTurnAwaitingApplyRef = useRef(false);
1157
+ const localTurnStreamStartedRef = useRef(false);
1158
+ const captureSettledVersionRef = useRef(false);
1159
+ const lastSubmittedUIContextRef = useRef(void 0);
1160
+ const lastSubmittedModelIdRef = useRef(void 0);
1161
+ const previousApplyOutcomesRef = useRef(/* @__PURE__ */ new Map());
1162
+ const capturedChatErrorRef = useRef(void 0);
1163
+ const { messages, setMessages, sendMessage, status, stop: stopChat, error, clearError, regenerate } = useChat({
1164
+ id: threadId,
1165
+ messages: initialMessages,
1166
+ transport: useMemo(() => createSurveyAssistantTransport({
1167
+ api,
1168
+ resourceId,
1169
+ surveyId,
1170
+ threadId
1171
+ }), [
1172
+ api,
1173
+ resourceId,
1174
+ surveyId,
1175
+ threadId
1176
+ ])
1177
+ });
1178
+ const isStreaming = status === "submitted" || status === "streaming";
1179
+ const messagesRef = useRef(messages);
1180
+ messagesRef.current = messages;
1181
+ const isTurnActive = isSurveyAssistantTurnActive({
1182
+ isStreaming,
1183
+ state
1184
+ });
1185
+ const notifyTurnActiveChange = useEffectEvent((active) => {
1186
+ onTurnActiveChange?.(active);
1187
+ });
1188
+ const prependMessages = useCallback((olderMessages) => {
1189
+ setMessages((current) => {
1190
+ const currentIds = new Set(current.map((message) => message.id));
1191
+ return [...olderMessages.filter((message) => !currentIds.has(message.id)), ...current];
1192
+ });
1193
+ }, [setMessages]);
1194
+ const turnAuthorization = useMemo(() => resolveLatestSurveyTurnAuthorization(messages), [messages]);
1195
+ const latestTurnOutcome = useMemo(() => resolveSurveyTurnOutcome({
1196
+ applyResultsByChangeId,
1197
+ authorization: turnAuthorization
1198
+ }), [applyResultsByChangeId, turnAuthorization]);
1199
+ const hasUsefulLatestAssistantResponse = useMemo(() => hasCompletedAssistantResponseAfterLastUser(messages) || collectChangeSetsAfterLastUser(messages).length > 0, [messages]);
1200
+ const reportAuthorizationError = useCallback((message) => {
1201
+ try {
1202
+ diagnosticStore?.recordFailure(threadId, messagesRef.current, {
1203
+ code: "request_failed",
1204
+ detail: message,
1205
+ retryable: false
1206
+ });
1207
+ } catch {}
1208
+ queueMicrotask(() => {
1209
+ setAuthorizationError(message);
1210
+ dispatch({ type: "failed" });
1211
+ });
1212
+ }, [diagnosticStore, threadId]);
1213
+ const rememberApplyOutcome = useCallback((outcome) => {
1214
+ previousApplyOutcomesRef.current.set(applyOutcomeScopeKey, outcome);
1215
+ try {
1216
+ diagnosticStore?.recordApplyOutcome(threadId, outcome);
1217
+ } catch {}
1218
+ }, [
1219
+ applyOutcomeScopeKey,
1220
+ diagnosticStore,
1221
+ threadId
1222
+ ]);
1223
+ useEffect(() => {
1224
+ if (status === "submitted") dispatch({ type: "submit" });
1225
+ if (status === "streaming") dispatch({ type: "stream-started" });
1226
+ }, [status]);
1227
+ useEffect(() => {
1228
+ notifyTurnActiveChange(isTurnActive);
1229
+ return () => {
1230
+ if (isTurnActive) notifyTurnActiveChange(false);
1231
+ };
1232
+ }, [isTurnActive]);
1233
+ useEffect(() => {
1234
+ if (!captureSettledVersionRef.current || isStreaming || state !== "completed" && state !== "failed" && state !== "aborted" && state !== "applied") return;
1235
+ captureSettledVersionRef.current = false;
1236
+ try {
1237
+ const snapshot = controller?.getSnapshot();
1238
+ if (snapshot) writeSettledDocumentVersion({
1239
+ resourceId,
1240
+ snapshot,
1241
+ threadId
1242
+ });
1243
+ } catch {}
1244
+ }, [
1245
+ controller,
1246
+ isStreaming,
1247
+ resourceId,
1248
+ state,
1249
+ threadId
1250
+ ]);
1251
+ useEffect(() => {
1252
+ if (isStreaming) {
1253
+ localTurnStreamStartedRef.current = true;
1254
+ return;
1255
+ }
1256
+ if (!localTurnAwaitingApplyRef.current) return;
1257
+ if (turnAuthorization.status === "pending") {
1258
+ if (!localTurnStreamStartedRef.current) return;
1259
+ const timeoutId = window.setTimeout(() => {
1260
+ if (capturedChatErrorRef.current) return;
1261
+ localTurnAwaitingApplyRef.current = false;
1262
+ localTurnStreamStartedRef.current = false;
1263
+ reportAuthorizationError("The assistant response ended without a valid turn final.");
1264
+ }, 100);
1265
+ return () => window.clearTimeout(timeoutId);
1266
+ }
1267
+ localTurnAwaitingApplyRef.current = false;
1268
+ localTurnStreamStartedRef.current = false;
1269
+ if (turnAuthorization.status === "invalid") {
1270
+ reportAuthorizationError(turnAuthorization.reason);
1271
+ return;
1272
+ }
1273
+ const { turnFinal } = turnAuthorization;
1274
+ try {
1275
+ diagnosticStore?.recordTurnFinal(threadId, turnFinal);
1276
+ } catch {}
1277
+ if (handledTurnIdsRef.current.has(turnFinal.turnId)) return;
1278
+ handledTurnIdsRef.current.add(turnFinal.turnId);
1279
+ const preparedChanges = collectChangeSetsAfterLastUser(messages).filter((change) => change.turnId === turnFinal.turnId);
1280
+ if (turnAuthorization.status === "failed") {
1281
+ rememberApplyOutcome(createApplyOutcome({
1282
+ turnId: turnFinal.turnId,
1283
+ status: "not-applied",
1284
+ message: "The previous assistant turn failed before editor authorization. Every request-scoped draft change from that turn was discarded and was not applied.",
1285
+ preparedChanges
1286
+ }));
1287
+ dispatch({ type: "failed" });
1288
+ return;
1289
+ }
1290
+ if (turnAuthorization.status === "aborted") {
1291
+ rememberApplyOutcome(createApplyOutcome({
1292
+ turnId: turnFinal.turnId,
1293
+ status: "not-applied",
1294
+ message: "The previous assistant turn was interrupted before editor authorization. Every request-scoped draft change from that turn was discarded and was not applied.",
1295
+ preparedChanges
1296
+ }));
1297
+ dispatch({ type: "aborted" });
1298
+ return;
1299
+ }
1300
+ if (turnAuthorization.changeSets.length === 0) {
1301
+ if ((turnFinal.blockedChangeSetIds?.length ?? 0) > 0) rememberApplyOutcome(createApplyOutcome({
1302
+ turnId: turnFinal.turnId,
1303
+ status: "not-applied",
1304
+ message: "The previous assistant turn ended with unresolved rejected changes. No request-scoped draft change from that turn was authorized or applied.",
1305
+ preparedChanges
1306
+ }));
1307
+ dispatch({ type: "completed" });
1308
+ return;
1309
+ }
1310
+ if (applyPolicy === "deny") {
1311
+ rememberApplyOutcome(createApplyOutcome({
1312
+ turnId: turnFinal.turnId,
1313
+ status: "not-applied",
1314
+ message: "The host apply policy did not authorize editor mutations for this turn.",
1315
+ preparedChanges
1316
+ }));
1317
+ dispatch({ type: "completed" });
1318
+ return;
1319
+ }
1320
+ dispatch({ type: "apply-started" });
1321
+ (async () => {
1322
+ if (applyPolicy === "review") {
1323
+ let approved;
1324
+ try {
1325
+ approved = await resolveSurveyAssistantReviewDecision({
1326
+ changeSets: turnAuthorization.changeSets,
1327
+ onReviewChangeSets,
1328
+ turnFinal
1329
+ });
1330
+ } catch (error) {
1331
+ const message = error instanceof Error && error.message === "No survey assistant review adapter is available." ? error.message : "The survey assistant review could not be completed.";
1332
+ rememberApplyOutcome(createApplyOutcome({
1333
+ turnId: turnFinal.turnId,
1334
+ status: "not-applied",
1335
+ message,
1336
+ preparedChanges
1337
+ }));
1338
+ reportAuthorizationError(message);
1339
+ return;
1340
+ }
1341
+ if (!approved) {
1342
+ rememberApplyOutcome(createApplyOutcome({
1343
+ turnId: turnFinal.turnId,
1344
+ status: "not-applied",
1345
+ message: "The proposed editor changes were not approved during review.",
1346
+ preparedChanges
1347
+ }));
1348
+ dispatch({ type: "completed" });
1349
+ return;
1350
+ }
1351
+ }
1352
+ if (!controller?.applyAssistantChanges) {
1353
+ const message = "No survey editor apply adapter is available.";
1354
+ rememberApplyOutcome(createApplyOutcome({
1355
+ turnId: turnFinal.turnId,
1356
+ status: "apply-failed",
1357
+ message,
1358
+ preparedChanges
1359
+ }));
1360
+ reportAuthorizationError(message);
1361
+ return;
1362
+ }
1363
+ const results = [];
1364
+ let applyStarted = false;
1365
+ const hasCompleteApplyResults = () => {
1366
+ const ids = new Set(results.map((result) => result.changeId));
1367
+ return results.length === turnAuthorization.changeSets.length && ids.size === results.length && turnAuthorization.changeSets.every((change) => ids.has(change.id));
1368
+ };
1369
+ const allChangesApplied = () => hasCompleteApplyResults() && results.every((result) => result.status === "applied");
1370
+ const retainApplyResults = () => {
1371
+ const entries = results.map((result) => [result.changeId, result]);
1372
+ queueMicrotask(() => setApplyResultsByChangeId((current) => ({
1373
+ ...current,
1374
+ ...Object.fromEntries(entries)
1375
+ })));
1376
+ };
1377
+ try {
1378
+ const beforeSnapshot = controller.getSnapshot();
1379
+ if (!beforeSnapshot) throw new Error("Editor snapshot unavailable.");
1380
+ applyStarted = true;
1381
+ results.push(...controller.applyAssistantChanges(turnAuthorization.changeSets).results);
1382
+ retainApplyResults();
1383
+ const receipts = createApplyReceipts({
1384
+ beforeSnapshot,
1385
+ afterSnapshot: controller.getSnapshot(),
1386
+ changeSets: turnAuthorization.changeSets,
1387
+ results,
1388
+ turnFinal
1389
+ });
1390
+ try {
1391
+ diagnosticStore?.recordApplyReceipts(threadId, receipts);
1392
+ } catch {}
1393
+ onApplyReceipts?.(receipts);
1394
+ if (!hasCompleteApplyResults()) throw new Error("The host returned mismatched apply results.");
1395
+ if (!allChangesApplied()) {
1396
+ rememberApplyOutcome(createApplyOutcome({
1397
+ turnId: turnFinal.turnId,
1398
+ status: "apply-failed",
1399
+ message: "One or more authorized survey changes could not be applied to the editor.",
1400
+ preparedChanges,
1401
+ results
1402
+ }));
1403
+ dispatch({ type: "completed" });
1404
+ return;
1405
+ }
1406
+ await onSettleApply?.({
1407
+ changeSets: turnAuthorization.changeSets,
1408
+ receipts,
1409
+ turnFinal
1410
+ });
1411
+ previousApplyOutcomesRef.current.delete(applyOutcomeScopeKey);
1412
+ dispatch({ type: "applied" });
1413
+ } catch {
1414
+ retainApplyResults();
1415
+ const applied = allChangesApplied();
1416
+ const message = applied ? "The assistant changes were applied locally but could not be settled by the host." : applyStarted ? "The host could not complete the assistant apply lifecycle. Some changes may already have been applied." : "The editor state was unavailable before applying the assistant changes. No changes were applied.";
1417
+ rememberApplyOutcome(createApplyOutcome({
1418
+ turnId: turnFinal.turnId,
1419
+ status: applied ? "settle-failed" : "apply-failed",
1420
+ message,
1421
+ preparedChanges,
1422
+ results
1423
+ }));
1424
+ reportAuthorizationError(message);
1425
+ }
1426
+ })();
1427
+ }, [
1428
+ applyPolicy,
1429
+ applyOutcomeScopeKey,
1430
+ controller,
1431
+ diagnosticStore,
1432
+ isStreaming,
1433
+ onApplyReceipts,
1434
+ onReviewChangeSets,
1435
+ onSettleApply,
1436
+ reportAuthorizationError,
1437
+ rememberApplyOutcome,
1438
+ messages,
1439
+ threadId,
1440
+ turnAuthorization
1441
+ ]);
1442
+ useEffect(() => {
1443
+ if (!error) {
1444
+ capturedChatErrorRef.current = void 0;
1445
+ return;
1446
+ }
1447
+ if (capturedChatErrorRef.current === error) return;
1448
+ capturedChatErrorRef.current = error;
1449
+ localTurnAwaitingApplyRef.current = false;
1450
+ localTurnStreamStartedRef.current = false;
1451
+ dispatch({ type: "failed" });
1452
+ try {
1453
+ diagnosticStore?.recordFailure(threadId, messagesRef.current, parseSurveyAssistantError(error));
1454
+ } catch {}
1455
+ if (!hasUsefulLatestAssistantResponse) setMessages(pruneFailedMessagesForDisplay);
1456
+ }, [
1457
+ diagnosticStore,
1458
+ error,
1459
+ hasUsefulLatestAssistantResponse,
1460
+ setMessages,
1461
+ threadId
1462
+ ]);
1463
+ const submit = useCallback(({ files, modelId: submittedModelId, text: rawText, uiContext }) => {
1464
+ const text = rawText.trim();
1465
+ if (!text && files.length === 0) return false;
1466
+ if (isStreaming || localTurnAwaitingApplyRef.current) return false;
1467
+ let snapshot;
1468
+ try {
1469
+ snapshot = applySurveyAssistantLaunchUIContext(controller?.getSnapshot() ?? null, uiContext);
1470
+ } catch {
1471
+ reportAuthorizationError("The editor state was unavailable before sending the assistant request. No request was sent.");
1472
+ return false;
1473
+ }
1474
+ localTurnAwaitingApplyRef.current = true;
1475
+ localTurnStreamStartedRef.current = false;
1476
+ setAuthorizationError(null);
1477
+ if ((authorizationError || error) && !hasUsefulLatestAssistantResponse) setMessages(pruneFailedMessagesForDisplay);
1478
+ clearError();
1479
+ dispatch({ type: "submit" });
1480
+ captureSettledVersionRef.current = true;
1481
+ lastSubmittedUIContextRef.current = uiContext;
1482
+ lastSubmittedModelIdRef.current = submittedModelId ?? modelId;
1483
+ const requestId = createSurveyAssistantDiagnosticId();
1484
+ try {
1485
+ diagnosticStore?.beginAttempt({
1486
+ files,
1487
+ modelId: lastSubmittedModelIdRef.current,
1488
+ requestId,
1489
+ resourceId,
1490
+ submittedSurvey: snapshot,
1491
+ threadId
1492
+ });
1493
+ } catch {}
1494
+ sendMessage(files.length > 0 ? text ? {
1495
+ text,
1496
+ files
1497
+ } : { files } : { text }, { body: {
1498
+ requestId,
1499
+ snapshot,
1500
+ previousSettledVersion: readSettledDocumentVersion({
1501
+ resourceId,
1502
+ threadId
1503
+ }),
1504
+ previousApplyOutcome: previousApplyOutcomesRef.current.get(applyOutcomeScopeKey),
1505
+ ...lastSubmittedModelIdRef.current ? { modelId: lastSubmittedModelIdRef.current } : {}
1506
+ } });
1507
+ return true;
1508
+ }, [
1509
+ authorizationError,
1510
+ applyOutcomeScopeKey,
1511
+ clearError,
1512
+ controller,
1513
+ diagnosticStore,
1514
+ error,
1515
+ hasUsefulLatestAssistantResponse,
1516
+ isStreaming,
1517
+ modelId,
1518
+ resourceId,
1519
+ reportAuthorizationError,
1520
+ sendMessage,
1521
+ setMessages,
1522
+ threadId
1523
+ ]);
1524
+ const stop = useCallback(() => {
1525
+ localTurnAwaitingApplyRef.current = false;
1526
+ localTurnStreamStartedRef.current = false;
1527
+ dispatch({ type: "aborted" });
1528
+ stopChat();
1529
+ }, [stopChat]);
1530
+ const retry = useCallback(() => {
1531
+ let snapshot;
1532
+ try {
1533
+ snapshot = applySurveyAssistantLaunchUIContext(controller?.getSnapshot() ?? null, lastSubmittedUIContextRef.current);
1534
+ } catch {
1535
+ reportAuthorizationError("The editor state was unavailable before retrying the assistant request. No request was sent.");
1536
+ return;
1537
+ }
1538
+ localTurnAwaitingApplyRef.current = true;
1539
+ localTurnStreamStartedRef.current = false;
1540
+ setAuthorizationError(null);
1541
+ clearError();
1542
+ dispatch({ type: "submit" });
1543
+ captureSettledVersionRef.current = true;
1544
+ const latestUserMessage = [...messagesRef.current].reverse().find((message) => message.role === "user");
1545
+ const files = (latestUserMessage?.parts ?? []).filter((part) => part.type === "file");
1546
+ const attachmentReferences = (latestUserMessage?.parts ?? []).flatMap((part) => part.type === "data-survey-assistant-attachment" && typeof part.data === "object" && part.data !== null ? [part.data] : []);
1547
+ const requestId = createSurveyAssistantDiagnosticId();
1548
+ try {
1549
+ diagnosticStore?.beginAttempt({
1550
+ attachmentReferences,
1551
+ files,
1552
+ modelId: lastSubmittedModelIdRef.current ?? modelId,
1553
+ requestId,
1554
+ resourceId,
1555
+ submittedSurvey: snapshot,
1556
+ threadId
1557
+ });
1558
+ diagnosticStore?.recordEvent(threadId, "retry");
1559
+ } catch {}
1560
+ regenerate({ body: {
1561
+ requestId,
1562
+ snapshot,
1563
+ previousSettledVersion: readSettledDocumentVersion({
1564
+ resourceId,
1565
+ threadId
1566
+ }),
1567
+ previousApplyOutcome: previousApplyOutcomesRef.current.get(applyOutcomeScopeKey),
1568
+ ...lastSubmittedModelIdRef.current ? { modelId: lastSubmittedModelIdRef.current } : {}
1569
+ } });
1570
+ }, [
1571
+ applyOutcomeScopeKey,
1572
+ clearError,
1573
+ controller,
1574
+ diagnosticStore,
1575
+ modelId,
1576
+ regenerate,
1577
+ resourceId,
1578
+ reportAuthorizationError,
1579
+ threadId
1580
+ ]);
1581
+ return {
1582
+ applyResultsByChangeId,
1583
+ authorizationError,
1584
+ dismissError: useCallback(() => {
1585
+ localTurnAwaitingApplyRef.current = false;
1586
+ localTurnStreamStartedRef.current = false;
1587
+ if (!hasUsefulLatestAssistantResponse) setMessages(pruneFailedMessagesForDisplay);
1588
+ setAuthorizationError(null);
1589
+ clearError();
1590
+ dispatch({
1591
+ type: "reset",
1592
+ hasTranscript: messages.length > 0
1593
+ });
1594
+ try {
1595
+ diagnosticStore?.recordEvent(threadId, "dismiss");
1596
+ } catch {}
1597
+ }, [
1598
+ clearError,
1599
+ diagnosticStore,
1600
+ hasUsefulLatestAssistantResponse,
1601
+ messages.length,
1602
+ setMessages,
1603
+ threadId
1604
+ ]),
1605
+ error,
1606
+ isStreaming,
1607
+ latestTurnOutcome,
1608
+ messages,
1609
+ prependMessages,
1610
+ state,
1611
+ status,
1612
+ stop,
1613
+ submit,
1614
+ retry
1615
+ };
1616
+ };
1617
+ //#endregion
1618
+ export { resolveLatestSurveyTurnAuthorization as C, reduceSurveyAssistantSessionState as D, isSurveyAssistantTurnActive as E, parseSurveyChangeSet as S, unwrapSurveyAssistantToolOutput as T, createSurveyAssistantThreadClient as _, createSurveyAssistantSupportDetails as a, createApplyReceipts as b, estimateSurveyAssistantSupportBundleBytes as c, prepareSurveyAssistantSupportBundle as d, sanitizeSurveyAssistantSupportValue as f, resolveSurveyAssistantSupportBundleLimits as g, SURVEY_ASSISTANT_DEFAULT_SUPPORT_BUNDLE_LIMITS as h, SURVEY_ASSISTANT_DEFAULT_SUPPORT_BUNDLE_SELECTION as i, generateSurveyAssistantSupportBundle as l, createSurveyAssistantDiagnosticStore as m, createSurveyAssistantModelCatalogClient as n, createSurveyAssistantSupportMailto as o, createSurveyAssistantDiagnosticId as p, useSurveyAssistantModelCatalog as r, downloadSurveyAssistantSupportBundle as s, useSurveyAssistantSession as t, isSurveyAssistantSupportBundleEnabled as u, createSurveyAssistantTransport as v, resolveSurveyTurnOutcome as w, getSurveyDocumentVersionFromSnapshot as x, collectSurveyChangeSets as y };
1619
+
1620
+ //# sourceMappingURL=react-B3ybjJ-X.mjs.map