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