@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.
- package/README.md +9 -4
- package/dist/attachments-B0hg3vpT.mjs +11 -0
- package/dist/attachments-B0hg3vpT.mjs.map +1 -0
- package/dist/authoring-references-BJQ_1nMX.mjs +115 -0
- package/dist/authoring-references-BJQ_1nMX.mjs.map +1 -0
- package/dist/capabilities-1KNnNO1_.mjs +73 -0
- package/dist/capabilities-1KNnNO1_.mjs.map +1 -0
- package/dist/{capabilities-CnsVuJ1k.d.mts → capabilities-DVTfogjv.d.mts} +1 -1
- package/dist/{capabilities-CnsVuJ1k.d.mts.map → capabilities-DVTfogjv.d.mts.map} +1 -1
- package/dist/capabilities-default.d.mts +1 -1
- package/dist/capabilities-default.d.mts.map +1 -1
- package/dist/capabilities-default.mjs +47 -20
- package/dist/capabilities-default.mjs.map +1 -1
- package/dist/{constants-CFUWrRtl.mjs → constants-0UHGFcRJ.mjs} +1 -1
- package/dist/{constants-CFUWrRtl.mjs.map → constants-0UHGFcRJ.mjs.map} +1 -1
- package/dist/{constants-DbrJxxOh.d.mts → constants-Be91Gay9.d.mts} +4 -4
- package/dist/{constants-DbrJxxOh.d.mts.map → constants-Be91Gay9.d.mts.map} +1 -1
- package/dist/{controller-proxy-jWIPOZAv.mjs → controller-proxy-BH05e-C2.mjs} +21 -8
- package/dist/controller-proxy-BH05e-C2.mjs.map +1 -0
- package/dist/{controller-proxy-BdKF2yrq.d.mts → controller-proxy-Bd0ZHBPF.d.mts} +11 -11
- package/dist/controller-proxy-Bd0ZHBPF.d.mts.map +1 -0
- package/dist/{digest-Dyjch_sC.d.mts → digest-SJmoYVaK.d.mts} +2 -2
- package/dist/{digest-Dyjch_sC.d.mts.map → digest-SJmoYVaK.d.mts.map} +1 -1
- package/dist/digest.d.mts +1 -1
- package/dist/{engine-DcSdLRle.mjs → engine-Bo85Oyj0.mjs} +3129 -2435
- package/dist/engine-Bo85Oyj0.mjs.map +1 -0
- package/dist/engine.d.mts +3 -3
- package/dist/engine.mjs +3 -3
- package/dist/{fair-guidance-CEdJYccK.mjs → fair-guidance-CRYCRxQm.mjs} +1 -1
- package/dist/{fair-guidance-CEdJYccK.mjs.map → fair-guidance-CRYCRxQm.mjs.map} +1 -1
- package/dist/{index-Da_Hb9-1.d.mts → index-Tnqv-yKW.d.mts} +73 -67
- package/dist/index-Tnqv-yKW.d.mts.map +1 -0
- package/dist/index-l_EK4yK7.d.mts +404 -0
- package/dist/index-l_EK4yK7.d.mts.map +1 -0
- package/dist/{index-BApnIiCI.d.mts → index-nhrgbgO5.d.mts} +24 -5
- package/dist/index-nhrgbgO5.d.mts.map +1 -0
- package/dist/{index-DhNQKOwr.d.mts → index-x0qprHgK.d.mts} +19 -7
- package/dist/index-x0qprHgK.d.mts.map +1 -0
- package/dist/{lifecycle-DWv5CiSu.d.mts → lifecycle-Bx0aq_4S.d.mts} +8 -2
- package/dist/{lifecycle-DWv5CiSu.d.mts.map → lifecycle-Bx0aq_4S.d.mts.map} +1 -1
- package/dist/{memory-thread-repository-CvYZwni6.mjs → memory-thread-repository-KWHQWFEW.mjs} +45 -14
- package/dist/memory-thread-repository-KWHQWFEW.mjs.map +1 -0
- package/dist/{protocol-DJUP4gc0.mjs → protocol-BhzQbx81.mjs} +66 -16
- package/dist/protocol-BhzQbx81.mjs.map +1 -0
- package/dist/protocol.d.mts +4 -4
- package/dist/protocol.mjs +3 -2
- package/dist/react-B3ybjJ-X.mjs +1620 -0
- package/dist/react-B3ybjJ-X.mjs.map +1 -0
- package/dist/react-integration.d.mts +1 -1
- package/dist/react-integration.mjs +1 -1
- package/dist/react.d.mts +3 -3
- package/dist/react.mjs +3 -3
- package/dist/references/assistant-operations.md +45 -39
- package/dist/references/expressions.md +28 -0
- package/dist/references/item-types.md +1 -1
- package/dist/references/localization.md +5 -4
- package/dist/references/rich-text-content.md +60 -27
- package/dist/references/source-material-surveys.md +9 -2
- package/dist/references/survey-data-model.md +9 -1
- package/dist/{request-context-D1sC54ZA.mjs → request-context-CG4SWijt.mjs} +3 -3
- package/dist/{request-context-D1sC54ZA.mjs.map → request-context-CG4SWijt.mjs.map} +1 -1
- package/dist/{request-guardrails-h4t6u8mP.mjs → request-guardrails-TmgQtqp4.mjs} +5 -12
- package/dist/request-guardrails-TmgQtqp4.mjs.map +1 -0
- package/dist/server-agent.d.mts +19 -8
- package/dist/server-agent.d.mts.map +1 -1
- package/dist/server-agent.mjs +11 -13
- package/dist/server-agent.mjs.map +1 -1
- package/dist/server-runtime.d.mts +12 -6
- package/dist/server-runtime.d.mts.map +1 -1
- package/dist/server-runtime.mjs +9 -4
- package/dist/server-runtime.mjs.map +1 -1
- package/dist/server-tasks.d.mts +1 -1
- package/dist/server-tasks.mjs +5 -5
- package/dist/server-tools.d.mts +1 -1
- package/dist/server-tools.mjs +1 -1
- package/dist/server.d.mts +7 -7
- package/dist/server.mjs +6 -6
- package/dist/storage-postgres.d.mts +1 -1
- package/dist/storage-postgres.mjs +1 -1
- package/dist/{tasks-CCtxjhwh.d.mts → tasks-CYqrcaRO.d.mts} +16 -4
- package/dist/tasks-CYqrcaRO.d.mts.map +1 -0
- package/dist/{thread-documents-Dvo6XS7o.d.mts → thread-documents-rmd6nyX9.d.mts} +1 -1
- package/dist/{thread-documents-Dvo6XS7o.d.mts.map → thread-documents-rmd6nyX9.d.mts.map} +1 -1
- package/dist/{thread-handlers-Bt9cDo-F.d.mts → thread-handlers-DBAaU7BA.d.mts} +7 -5
- package/dist/thread-handlers-DBAaU7BA.d.mts.map +1 -0
- package/dist/{tools-3tq-XqI0.mjs → tools-BvSovehT.mjs} +433 -495
- package/dist/tools-BvSovehT.mjs.map +1 -0
- package/dist/ui.css +1 -1
- package/dist/ui.d.mts +5 -4
- package/dist/ui.d.mts.map +1 -1
- package/dist/ui.mjs +744 -131
- package/dist/ui.mjs.map +1 -1
- package/docs/integration.md +43 -3
- package/package.json +21 -19
- package/dist/authoring-references-yyYC4Ydi.mjs +0 -98
- package/dist/authoring-references-yyYC4Ydi.mjs.map +0 -1
- package/dist/controller-proxy-BdKF2yrq.d.mts.map +0 -1
- package/dist/controller-proxy-jWIPOZAv.mjs.map +0 -1
- package/dist/engine-DcSdLRle.mjs.map +0 -1
- package/dist/form-authoring-BQh40yDb.mjs +0 -155
- package/dist/form-authoring-BQh40yDb.mjs.map +0 -1
- package/dist/index-BApnIiCI.d.mts.map +0 -1
- package/dist/index-Da_Hb9-1.d.mts.map +0 -1
- package/dist/index-DhNQKOwr.d.mts.map +0 -1
- package/dist/index-dV6df0W3.d.mts +0 -224
- package/dist/index-dV6df0W3.d.mts.map +0 -1
- package/dist/memory-thread-repository-CvYZwni6.mjs.map +0 -1
- package/dist/protocol-DJUP4gc0.mjs.map +0 -1
- package/dist/react-CY6N7lVi.mjs +0 -768
- package/dist/react-CY6N7lVi.mjs.map +0 -1
- package/dist/request-guardrails-h4t6u8mP.mjs.map +0 -1
- package/dist/tasks-CCtxjhwh.d.mts.map +0 -1
- package/dist/thread-handlers-Bt9cDo-F.d.mts.map +0 -1
- 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
|