@spektra-cloudevents/application-copilot-widget 1.1.0 → 2.0.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 +126 -75
- package/dist/index.cjs +439 -66
- package/dist/index.d.cts +70 -8
- package/dist/index.d.ts +70 -8
- package/dist/index.js +439 -66
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -77,7 +77,7 @@ var widgetStyles = `
|
|
|
77
77
|
/* Panel */
|
|
78
78
|
.acw-panel {
|
|
79
79
|
position: fixed; right: 24px; bottom: 96px; z-index: 2147483000;
|
|
80
|
-
width:
|
|
80
|
+
width: 480px; max-width: calc(100vw - 48px); height: 660px; max-height: calc(100vh - 120px);
|
|
81
81
|
display: none; flex-direction: column;
|
|
82
82
|
background: var(--acw-bg); color: var(--acw-fg);
|
|
83
83
|
border: 1px solid var(--acw-border); border-radius: 16px; overflow: hidden;
|
|
@@ -96,7 +96,7 @@ var widgetStyles = `
|
|
|
96
96
|
.acw-close { margin-left: auto; border: none; background: transparent; cursor: pointer; font-size: 22px; color: rgba(255,255,255,0.85); line-height: 1; }
|
|
97
97
|
.acw-close:hover { color: #fff; }
|
|
98
98
|
|
|
99
|
-
.acw-body { flex: 1; overflow-y: auto; padding: 16px; background: var(--acw-surface); }
|
|
99
|
+
.acw-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 16px; background: var(--acw-surface); }
|
|
100
100
|
.acw-hint { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
101
101
|
|
|
102
102
|
.acw-chip {
|
|
@@ -116,28 +116,140 @@ var widgetStyles = `
|
|
|
116
116
|
.acw-msg.acw-user span, .acw-msg.acw-user {
|
|
117
117
|
display: inline-block;
|
|
118
118
|
}
|
|
119
|
+
.acw-msg.acw-user { display: block; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
119
120
|
.acw-progress { font-size: 13px; color: var(--acw-muted); margin: 10px 0; }
|
|
120
121
|
|
|
121
122
|
.acw-answer { background: var(--acw-bg); border: 1px solid var(--acw-border); border-radius: 12px; padding: 12px 14px; margin: 12px 0; }
|
|
122
|
-
.acw-answer .acw-reason { font-size: 14px; line-height: 1.5; }
|
|
123
|
+
.acw-answer .acw-reason, .acw-msg .acw-reason { font-size: 14px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
123
124
|
.acw-answer.acw-blocked { border-left: 4px solid var(--acw-danger); }
|
|
124
125
|
.acw-answer.acw-okstate { border-left: 4px solid var(--acw-ok); }
|
|
125
126
|
.acw-role { font-size: 12px; color: var(--acw-muted); margin-top: 8px; }
|
|
126
127
|
.acw-evidence { font-size: 12px; color: var(--acw-muted); margin-top: 10px; word-break: break-word; }
|
|
127
|
-
.acw-error { font-size: 14px; color: var(--acw-danger); }
|
|
128
|
+
.acw-error { font-size: 14px; color: var(--acw-danger); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
129
|
+
|
|
130
|
+
/* Knowledge-base references */
|
|
131
|
+
.acw-refs { margin-top: 10px; font-size: 12px; }
|
|
132
|
+
.acw-refs-label { color: var(--acw-muted); font-weight: 600; margin-bottom: 4px; }
|
|
133
|
+
.acw-refs-list { margin: 0; padding-left: 18px; }
|
|
134
|
+
.acw-refs-list li { margin: 2px 0; }
|
|
135
|
+
.acw-ref { color: var(--acw-accent); text-decoration: underline; overflow-wrap: anywhere; }
|
|
136
|
+
.acw-ref:hover { color: var(--acw-accent-dark); }
|
|
137
|
+
|
|
138
|
+
/* Support-ticket offer */
|
|
139
|
+
.acw-offer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--acw-border); }
|
|
140
|
+
.acw-offer-text { font-size: 13px; line-height: 1.5; color: var(--acw-fg); white-space: pre-wrap; overflow-wrap: anywhere; margin-bottom: 8px; }
|
|
141
|
+
.acw-offer-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
|
|
142
|
+
.acw-offer-actions .acw-chip { width: auto; margin-bottom: 0; }
|
|
143
|
+
.acw-offer-actions .acw-link-btn { margin-top: 0; }
|
|
144
|
+
.acw-chip:disabled, .acw-link-btn:disabled { opacity: .6; cursor: default; }
|
|
145
|
+
.acw-nothelpful { display: block; margin-top: 10px; color: var(--acw-muted); }
|
|
128
146
|
|
|
129
147
|
.acw-footer { display: flex; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--acw-border); background: var(--acw-bg); }
|
|
130
148
|
.acw-input {
|
|
131
|
-
flex: 1; padding: 10px 12px; font-size: 14px;
|
|
149
|
+
flex: 1; min-width: 0; padding: 10px 12px; font-size: 14px;
|
|
132
150
|
border: 1px solid var(--acw-border); border-radius: 10px; color: var(--acw-fg); background: var(--acw-bg);
|
|
133
151
|
font-family: inherit;
|
|
134
152
|
}
|
|
135
153
|
.acw-input:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
136
154
|
.acw-send { border: none; border-radius: 10px; padding: 0 16px; background: var(--acw-accent); color: #fff; cursor: pointer; font-size: 14px; font-family: inherit; }
|
|
137
155
|
.acw-send:hover { background: var(--acw-accent-dark); }
|
|
156
|
+
|
|
157
|
+
/* Support-ticket draft card */
|
|
158
|
+
.acw-ticket { background: var(--acw-bg); border: 1px solid var(--acw-border); border-radius: 12px; padding: 14px; margin: 12px 0; border-left: 4px solid var(--acw-accent); }
|
|
159
|
+
.acw-ticket-title { font-size: 15px; font-weight: 600; color: var(--acw-fg); margin: 0 0 4px; }
|
|
160
|
+
.acw-ticket-sub { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
161
|
+
.acw-ticket-label { display: block; font-size: 12px; font-weight: 600; color: var(--acw-muted); margin: 12px 0 5px; }
|
|
162
|
+
.acw-ticket-input, .acw-ticket-desc {
|
|
163
|
+
width: 100%; box-sizing: border-box; font-family: inherit; font-size: 13px;
|
|
164
|
+
padding: 10px 12px; border: 1px solid var(--acw-border); border-radius: 10px;
|
|
165
|
+
color: var(--acw-fg); background: var(--acw-bg);
|
|
166
|
+
}
|
|
167
|
+
.acw-ticket-desc { resize: vertical; min-height: 170px; line-height: 1.5; }
|
|
168
|
+
.acw-ticket-input:focus, .acw-ticket-desc:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
169
|
+
.acw-ticket-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
|
|
170
|
+
.acw-btn-primary { border: none; border-radius: 10px; padding: 10px 20px; cursor: pointer; background: var(--acw-accent); color: #fff; font-size: 14px; font-weight: 600; font-family: inherit; }
|
|
171
|
+
.acw-btn-primary:hover { background: var(--acw-accent-dark); }
|
|
172
|
+
.acw-btn-ghost { border: 1px solid var(--acw-border); border-radius: 10px; padding: 10px 16px; cursor: pointer; background: var(--acw-bg); color: var(--acw-fg); font-size: 14px; font-family: inherit; }
|
|
173
|
+
.acw-btn-ghost:hover { background: #f0eefe; }
|
|
174
|
+
.acw-link-btn { margin-top: 8px; border: none; background: transparent; color: var(--acw-accent); font-size: 12px; font-family: inherit; cursor: pointer; padding: 0; text-decoration: underline; }
|
|
175
|
+
.acw-link-btn:hover { color: var(--acw-accent-dark); }
|
|
176
|
+
|
|
177
|
+
/* Responsive: full-width panel below 480px; never taller than the viewport; no horizontal scroll. */
|
|
178
|
+
@media (max-width: 479.98px) {
|
|
179
|
+
.acw-launch-wrap { right: 16px; bottom: 16px; }
|
|
180
|
+
.acw-tip { display: none; }
|
|
181
|
+
.acw-panel {
|
|
182
|
+
left: 0; right: 0; bottom: 0;
|
|
183
|
+
width: 100%; max-width: 100%;
|
|
184
|
+
height: calc(100vh - 32px); max-height: calc(100vh - 32px);
|
|
185
|
+
border-radius: 16px 16px 0 0; border-left: none; border-right: none; border-bottom: none;
|
|
186
|
+
}
|
|
187
|
+
.acw-ticket-desc { min-height: 120px; }
|
|
188
|
+
}
|
|
189
|
+
/* Short desktop/tablet viewports: keep the panel inside the viewport (beside the launcher). */
|
|
190
|
+
@media (max-height: 700px) and (min-width: 480px) {
|
|
191
|
+
.acw-panel { bottom: 16px; right: 96px; max-width: calc(100vw - 112px); max-height: calc(100vh - 32px); }
|
|
192
|
+
}
|
|
138
193
|
`;
|
|
139
194
|
|
|
195
|
+
// src/internal.ts
|
|
196
|
+
function escapeHtml(value) {
|
|
197
|
+
const s = value === null || value === void 0 ? "" : String(value);
|
|
198
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
199
|
+
}
|
|
200
|
+
function safeHref(raw) {
|
|
201
|
+
if (typeof raw !== "string") return null;
|
|
202
|
+
const trimmed = raw.trim();
|
|
203
|
+
if (!trimmed) return null;
|
|
204
|
+
let url;
|
|
205
|
+
try {
|
|
206
|
+
url = new URL(trimmed);
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
if (url.protocol !== "https:") return null;
|
|
211
|
+
if (url.username || url.password) return null;
|
|
212
|
+
if (!url.hostname) return null;
|
|
213
|
+
return url.href;
|
|
214
|
+
}
|
|
215
|
+
function normalizeReferences(refs, max = 10) {
|
|
216
|
+
if (!Array.isArray(refs)) return [];
|
|
217
|
+
const out = [];
|
|
218
|
+
const seen = /* @__PURE__ */ new Set();
|
|
219
|
+
for (const r of refs) {
|
|
220
|
+
if (!r || typeof r !== "object") continue;
|
|
221
|
+
const href = safeHref(r.url);
|
|
222
|
+
if (!href || seen.has(href)) continue;
|
|
223
|
+
seen.add(href);
|
|
224
|
+
const rawTitle = r.title;
|
|
225
|
+
const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : new URL(href).hostname;
|
|
226
|
+
out.push({ title, href });
|
|
227
|
+
if (out.length >= max) break;
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
function isSessionInvalidStatus(status) {
|
|
232
|
+
return status === 403 || status === 404;
|
|
233
|
+
}
|
|
234
|
+
function tokenIdentity(token) {
|
|
235
|
+
if (!token || typeof token !== "string") return null;
|
|
236
|
+
const parts = token.split(".");
|
|
237
|
+
if (parts.length < 2 || !parts[1]) return null;
|
|
238
|
+
try {
|
|
239
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
240
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
241
|
+
const payload = JSON.parse(atob(padded));
|
|
242
|
+
const emails = Array.isArray(payload.emails) ? payload.emails[0] : void 0;
|
|
243
|
+
const id = payload.oid ?? payload.sub ?? emails ?? payload.email ?? payload.preferred_username;
|
|
244
|
+
return typeof id === "string" && id ? id.toLowerCase() : null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
140
250
|
// src/widget.ts
|
|
251
|
+
var TICKET_OFFER_TEXT = "I couldn\u2019t answer this confidently. Would you like to raise a support ticket? I\u2019ll pre-fill it with this conversation.";
|
|
252
|
+
var SESSION_RESET_TEXT = "Your Copilot session has ended or is no longer valid, so a new one will be started. Please ask your question again.";
|
|
141
253
|
var PROGRESS_LABELS = {
|
|
142
254
|
QuestionAccepted: "Reading your question\u2026",
|
|
143
255
|
PermissionsVerified: "Verifying your permissions\u2026",
|
|
@@ -148,18 +260,50 @@ var CopilotWidget = class {
|
|
|
148
260
|
constructor() {
|
|
149
261
|
this.lastQuestion = "";
|
|
150
262
|
this.mode = "explain";
|
|
263
|
+
this.connecting = false;
|
|
264
|
+
/** Bumped by resetSession(); responses that belong to an older epoch are dropped. */
|
|
265
|
+
this.epoch = 0;
|
|
151
266
|
}
|
|
152
267
|
init(config) {
|
|
268
|
+
const previousTenant = this.config?.tenantUniqueName;
|
|
153
269
|
this.config = config;
|
|
154
270
|
try {
|
|
155
271
|
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
156
272
|
} catch {
|
|
157
273
|
}
|
|
274
|
+
if (previousTenant !== void 0 && previousTenant !== config.tenantUniqueName) this.resetSession();
|
|
158
275
|
this.mount();
|
|
159
|
-
this.
|
|
160
|
-
|
|
276
|
+
this.applyKnowledgeUploadVisibility();
|
|
277
|
+
if (!this.connection && !this.connecting) {
|
|
278
|
+
this.connecting = true;
|
|
279
|
+
this.connectSignalR().catch(() => {
|
|
280
|
+
}).finally(() => {
|
|
281
|
+
this.connecting = false;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Drop the current Copilot session and clear the conversation. The next question opens a new session
|
|
287
|
+
* for the current `tenantUniqueName` and signed-in user. Also called automatically when the tenant or the
|
|
288
|
+
* token's user changes and when the API answers 403/404 for the session.
|
|
289
|
+
*/
|
|
290
|
+
resetSession() {
|
|
291
|
+
this.epoch++;
|
|
292
|
+
this.sessionId = void 0;
|
|
293
|
+
this.sessionTenant = void 0;
|
|
294
|
+
this.sessionIdentity = void 0;
|
|
295
|
+
this.lastQuestion = "";
|
|
296
|
+
this.contextOverride = void 0;
|
|
297
|
+
this.recentError = void 0;
|
|
298
|
+
this.mode = "explain";
|
|
299
|
+
if (this.input) this.input.placeholder = "Ask a question\u2026";
|
|
300
|
+
if (this.body) {
|
|
301
|
+
this.body.replaceChildren();
|
|
302
|
+
if (this.panel?.classList.contains("acw-open")) this.refreshChips();
|
|
303
|
+
}
|
|
161
304
|
}
|
|
162
305
|
open() {
|
|
306
|
+
this.syncSessionScope();
|
|
163
307
|
this.panel?.classList.add("acw-open");
|
|
164
308
|
this.refreshChips();
|
|
165
309
|
}
|
|
@@ -204,6 +348,7 @@ var CopilotWidget = class {
|
|
|
204
348
|
this.body.scrollTop = this.body.scrollHeight;
|
|
205
349
|
}
|
|
206
350
|
ask(question) {
|
|
351
|
+
this.syncSessionScope();
|
|
207
352
|
if (this.mode === "intake") {
|
|
208
353
|
void this.runIntakeAsk(question);
|
|
209
354
|
} else {
|
|
@@ -221,7 +366,7 @@ var CopilotWidget = class {
|
|
|
221
366
|
style.textContent = widgetStyles;
|
|
222
367
|
this.root.appendChild(style);
|
|
223
368
|
const sparkle = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l1.7 4.6L18.3 8l-4.6 1.4L12 14l-1.7-4.6L5.7 8l4.6-1.4L12 2zm6.5 9l1 2.6 2.6 1-2.6 1-1 2.6-1-2.6-2.6-1 2.6-1 1-2.6zM6 13.5l.9 2.3 2.3.9-2.3.9L6 20l-.9-2.4L2.7 16.7l2.4-.9.9-2.3z"/></svg>';
|
|
224
|
-
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
369
|
+
const tip = escapeHtml(this.config.buttonText ?? "Ask Application Copilot");
|
|
225
370
|
const wrap = document.createElement("div");
|
|
226
371
|
wrap.className = "acw-launch-wrap";
|
|
227
372
|
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
@@ -234,7 +379,7 @@ var CopilotWidget = class {
|
|
|
234
379
|
<div class="acw-header">
|
|
235
380
|
${sparkle}
|
|
236
381
|
<span class="acw-title">Application Copilot</span>
|
|
237
|
-
<button class="acw-kb" type="button" aria-label="Add a document to the knowledge base" title="Add to knowledge base (admins)" style="background:none;border:none;color:inherit;cursor:pointer;padding:2px 6px;
|
|
382
|
+
<button class="acw-kb" type="button" aria-label="Add a document to the knowledge base" title="Add to knowledge base (admins)" hidden style="background:none;border:none;color:inherit;cursor:pointer;padding:2px 6px;align-items:center;">${uploadIcon}</button>
|
|
238
383
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
239
384
|
</div>
|
|
240
385
|
<div class="acw-body"></div>
|
|
@@ -262,10 +407,21 @@ var CopilotWidget = class {
|
|
|
262
407
|
if (e.key === "Enter") this.submitFromInput();
|
|
263
408
|
});
|
|
264
409
|
}
|
|
265
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Open the file picker to add a document to the knowledge base. Only when the host set `showKnowledgeUpload`;
|
|
412
|
+
* otherwise it just opens the panel. The Copilot API authorizes every upload either way.
|
|
413
|
+
*/
|
|
266
414
|
promptUpload() {
|
|
267
415
|
this.open();
|
|
268
|
-
this.fileInput?.click();
|
|
416
|
+
if (this.config?.showKnowledgeUpload === true) this.fileInput?.click();
|
|
417
|
+
}
|
|
418
|
+
/** B5: the upload button is hidden unless the host opts in (re-applied on every init, e.g. after a tenant switch). */
|
|
419
|
+
applyKnowledgeUploadVisibility() {
|
|
420
|
+
const btn = this.panel?.querySelector(".acw-kb");
|
|
421
|
+
if (!btn) return;
|
|
422
|
+
const show = this.config?.showKnowledgeUpload === true;
|
|
423
|
+
btn.hidden = !show;
|
|
424
|
+
btn.style.display = show ? "inline-flex" : "none";
|
|
269
425
|
}
|
|
270
426
|
async uploadDocument(file) {
|
|
271
427
|
this.open();
|
|
@@ -275,16 +431,14 @@ var CopilotWidget = class {
|
|
|
275
431
|
const form = new FormData();
|
|
276
432
|
form.append("file", file, file.name);
|
|
277
433
|
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
278
|
-
form.append("environment", this.config.environment);
|
|
279
434
|
const h = {};
|
|
280
435
|
const token = this.config.getAuthToken?.();
|
|
281
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
282
436
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
283
|
-
else if (email) h["X-User-Email"] = email;
|
|
284
437
|
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
285
438
|
this.setProgress("");
|
|
286
439
|
if (res.status === 403) {
|
|
287
|
-
this.
|
|
440
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
441
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Only System Admins and Partner Admins can add to the knowledge base." });
|
|
288
442
|
return;
|
|
289
443
|
}
|
|
290
444
|
if (!res.ok) {
|
|
@@ -293,7 +447,7 @@ var CopilotWidget = class {
|
|
|
293
447
|
}
|
|
294
448
|
const data = await res.json();
|
|
295
449
|
if (data?.status === "Indexed") {
|
|
296
|
-
this.renderAnswer({ status: "Answered", disabled: false, primaryReason: `
|
|
450
|
+
this.renderAnswer({ status: "Answered", disabled: false, primaryReason: `Uploaded \u201C${data.title}\u201D as a Draft (${data.chunks} chunk${data.chunks === 1 ? "" : "s"}). A System Admin or Partner Admin must publish it in Admin \u2192 Settings \u2192 Copilot Knowledge Base before I use it in answers.` });
|
|
297
451
|
} else {
|
|
298
452
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
299
453
|
}
|
|
@@ -342,20 +496,30 @@ var CopilotWidget = class {
|
|
|
342
496
|
this.open();
|
|
343
497
|
this.addMessage(question, "user");
|
|
344
498
|
this.setProgress("Finding the right offering\u2026");
|
|
499
|
+
const epoch = this.epoch;
|
|
345
500
|
try {
|
|
346
501
|
const sessionId = await this.ensureSession();
|
|
347
|
-
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/intake/ask`, {
|
|
502
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/intake/ask`, {
|
|
348
503
|
method: "POST",
|
|
349
504
|
headers: this.headers(),
|
|
350
505
|
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
351
506
|
});
|
|
507
|
+
if (epoch !== this.epoch) return;
|
|
508
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
509
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
510
|
+
this.handleInvalidSession();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
352
513
|
if (!res.ok) {
|
|
353
514
|
this.setProgress("");
|
|
354
515
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
355
516
|
return;
|
|
356
517
|
}
|
|
357
|
-
|
|
518
|
+
const data = await res.json();
|
|
519
|
+
if (epoch !== this.epoch) return;
|
|
520
|
+
this.renderIntakeAnswer(data);
|
|
358
521
|
} catch {
|
|
522
|
+
if (epoch !== this.epoch) return;
|
|
359
523
|
this.setProgress("");
|
|
360
524
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
361
525
|
}
|
|
@@ -414,10 +578,10 @@ var CopilotWidget = class {
|
|
|
414
578
|
p.textContent = text;
|
|
415
579
|
this.body.scrollTop = this.body.scrollHeight;
|
|
416
580
|
}
|
|
417
|
-
renderAnswer(answer) {
|
|
581
|
+
renderAnswer(answer, question, wasNotHelpful = false) {
|
|
418
582
|
this.setProgress("");
|
|
419
583
|
const el = document.createElement("div");
|
|
420
|
-
const isFailure = answer.status && answer.status !== "Answered";
|
|
584
|
+
const isFailure = !!answer.status && answer.status !== "Answered";
|
|
421
585
|
if (isFailure) {
|
|
422
586
|
el.className = "acw-answer";
|
|
423
587
|
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
@@ -427,32 +591,148 @@ var CopilotWidget = class {
|
|
|
427
591
|
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
428
592
|
el.innerHTML = `<div class="acw-reason">${escapeHtml(answer.primaryReason || "")}</div>` + (answer.verifiedRoleLabel ? `<div class="acw-role">Verified role: ${escapeHtml(answer.verifiedRoleLabel)}</div>` : "") + (sourceLabel ? `<div class="acw-role">Source: ${sourceLabel}</div>` : "") + (evidence ? `<div class="acw-evidence">Evidence: ${evidence}</div>` : "");
|
|
429
593
|
}
|
|
594
|
+
this.appendReferences(el, answer);
|
|
430
595
|
this.body.appendChild(el);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
596
|
+
let offerTicket;
|
|
597
|
+
if (typeof answer.supportTicketEligible === "boolean") {
|
|
598
|
+
offerTicket = answer.supportTicketEligible;
|
|
599
|
+
} else {
|
|
600
|
+
const role = (answer.verifiedRoleLabel || "").toLowerCase();
|
|
601
|
+
const eligibleRole = role === "requestor" || role === "approver";
|
|
602
|
+
const looksBlocked = /lock(ed)?|can only be (changed|edited)|not editable|isn'?t permitted|not permitted|can'?t (change|edit|update)|cannot (change|edit|update)|read-only|don'?t have access|not allowed/i.test(answer.primaryReason || "");
|
|
603
|
+
offerTicket = !isFailure && (answer.disabled === true || looksBlocked) && eligibleRole;
|
|
604
|
+
}
|
|
605
|
+
if (offerTicket) {
|
|
606
|
+
this.appendTicketOffer(el, answer);
|
|
607
|
+
} else if (!isFailure && question && !wasNotHelpful) {
|
|
608
|
+
const nh = document.createElement("button");
|
|
609
|
+
nh.type = "button";
|
|
610
|
+
nh.className = "acw-link-btn acw-nothelpful";
|
|
611
|
+
nh.textContent = "This didn\u2019t help";
|
|
612
|
+
nh.addEventListener("click", () => {
|
|
613
|
+
nh.remove();
|
|
614
|
+
void this.runAsk(question, true);
|
|
615
|
+
});
|
|
616
|
+
el.appendChild(nh);
|
|
442
617
|
}
|
|
443
618
|
this.body.scrollTop = this.body.scrollHeight;
|
|
444
619
|
}
|
|
620
|
+
/** KB references as safe links: https only, built with DOM APIs (no innerHTML), new tab, no opener/referrer. */
|
|
621
|
+
appendReferences(container, answer) {
|
|
622
|
+
const refs = normalizeReferences(answer.references);
|
|
623
|
+
if (refs.length === 0) return;
|
|
624
|
+
const wrap = document.createElement("div");
|
|
625
|
+
wrap.className = "acw-refs";
|
|
626
|
+
const label = document.createElement("div");
|
|
627
|
+
label.className = "acw-refs-label";
|
|
628
|
+
label.textContent = "Learn more:";
|
|
629
|
+
wrap.appendChild(label);
|
|
630
|
+
const list = document.createElement("ul");
|
|
631
|
+
list.className = "acw-refs-list";
|
|
632
|
+
for (const ref of refs) {
|
|
633
|
+
const li = document.createElement("li");
|
|
634
|
+
const a = document.createElement("a");
|
|
635
|
+
a.className = "acw-ref";
|
|
636
|
+
a.setAttribute("href", ref.href);
|
|
637
|
+
a.setAttribute("target", "_blank");
|
|
638
|
+
a.setAttribute("rel", "noopener noreferrer");
|
|
639
|
+
a.textContent = ref.title;
|
|
640
|
+
li.appendChild(a);
|
|
641
|
+
list.appendChild(li);
|
|
642
|
+
}
|
|
643
|
+
wrap.appendChild(list);
|
|
644
|
+
container.appendChild(wrap);
|
|
645
|
+
}
|
|
646
|
+
appendTicketOffer(container, answer) {
|
|
647
|
+
const offer = document.createElement("div");
|
|
648
|
+
offer.className = "acw-offer";
|
|
649
|
+
const text = document.createElement("div");
|
|
650
|
+
text.className = "acw-offer-text";
|
|
651
|
+
text.textContent = answer.supportTicketPrompt?.trim() || TICKET_OFFER_TEXT;
|
|
652
|
+
offer.appendChild(text);
|
|
653
|
+
const actions = document.createElement("div");
|
|
654
|
+
actions.className = "acw-offer-actions";
|
|
655
|
+
const btn = document.createElement("button");
|
|
656
|
+
btn.type = "button";
|
|
657
|
+
btn.className = "acw-chip";
|
|
658
|
+
btn.textContent = "Raise a support ticket";
|
|
659
|
+
btn.addEventListener("click", () => {
|
|
660
|
+
btn.disabled = true;
|
|
661
|
+
void this.openTicketDraft(answer).finally(() => {
|
|
662
|
+
btn.disabled = false;
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
actions.appendChild(btn);
|
|
666
|
+
this.appendOpenSupport(actions);
|
|
667
|
+
offer.appendChild(actions);
|
|
668
|
+
container.appendChild(offer);
|
|
669
|
+
}
|
|
670
|
+
/** Adds an "Open support" action when the host supplied `onOpenSupport`. */
|
|
671
|
+
appendOpenSupport(container) {
|
|
672
|
+
if (!this.config.onOpenSupport) return;
|
|
673
|
+
const b = document.createElement("button");
|
|
674
|
+
b.type = "button";
|
|
675
|
+
b.className = "acw-link-btn acw-open-support";
|
|
676
|
+
b.textContent = "Open support";
|
|
677
|
+
b.addEventListener("click", () => this.openSupport());
|
|
678
|
+
container.appendChild(b);
|
|
679
|
+
}
|
|
680
|
+
/** Close the panel first (synchronously), then hand over to the host so the two overlays never stack. */
|
|
681
|
+
openSupport() {
|
|
682
|
+
const cb = this.config.onOpenSupport;
|
|
683
|
+
if (!cb) return;
|
|
684
|
+
this.close();
|
|
685
|
+
cb();
|
|
686
|
+
}
|
|
445
687
|
currentRequestId() {
|
|
446
688
|
const ctx = this.currentContext();
|
|
447
689
|
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
448
690
|
if (explicit) return String(explicit);
|
|
449
691
|
const route = ctx.page?.route || (typeof window !== "undefined" ? window.location.pathname : "");
|
|
450
|
-
const
|
|
451
|
-
|
|
692
|
+
const ms = /MS[A-Z0-9]{6,}/i.exec(route || "");
|
|
693
|
+
if (ms) return ms[0];
|
|
694
|
+
const guid = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/.exec(route || "");
|
|
695
|
+
return guid ? guid[0] : void 0;
|
|
696
|
+
}
|
|
697
|
+
/** Fetch the server-built draft, then show it as an editable form; the user triggers Send. */
|
|
698
|
+
async openTicketDraft(answer) {
|
|
699
|
+
const fromAnswer = /MS[A-Z0-9]{6,}/i.exec(answer.primaryReason || "");
|
|
700
|
+
const requestId = fromAnswer ? fromAnswer[0] : this.currentRequestId();
|
|
701
|
+
const epoch = this.epoch;
|
|
702
|
+
this.setProgress("Preparing your support ticket\u2026");
|
|
703
|
+
let draft;
|
|
704
|
+
try {
|
|
705
|
+
const sessionId = await this.ensureSession();
|
|
706
|
+
const qs = requestId ? `?requestId=${encodeURIComponent(requestId)}` : "";
|
|
707
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/support-ticket/draft${qs}`, {
|
|
708
|
+
method: "GET",
|
|
709
|
+
headers: this.headers()
|
|
710
|
+
});
|
|
711
|
+
if (epoch !== this.epoch) return;
|
|
712
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
713
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
714
|
+
this.handleInvalidSession();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (res.ok) {
|
|
718
|
+
const data = await res.json();
|
|
719
|
+
if (epoch !== this.epoch) return;
|
|
720
|
+
if (data && typeof data.subject === "string" && typeof data.description === "string") draft = data;
|
|
721
|
+
}
|
|
722
|
+
} catch {
|
|
723
|
+
}
|
|
724
|
+
if (epoch !== this.epoch) return;
|
|
725
|
+
this.setProgress("");
|
|
726
|
+
const fallback = this.localDraft(answer, requestId);
|
|
727
|
+
this.showTicketForm(
|
|
728
|
+
draft?.subject || fallback.subject,
|
|
729
|
+
draft?.description || fallback.description,
|
|
730
|
+
draft?.fields?.requestId || requestId,
|
|
731
|
+
answer
|
|
732
|
+
);
|
|
452
733
|
}
|
|
453
|
-
/**
|
|
454
|
-
|
|
455
|
-
const requestId = this.currentRequestId();
|
|
734
|
+
/** Used only when the draft endpoint is unavailable (older server or a transient error). */
|
|
735
|
+
localDraft(answer, requestId) {
|
|
456
736
|
const role = answer.verifiedRoleLabel || "";
|
|
457
737
|
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
458
738
|
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
@@ -464,41 +744,57 @@ var CopilotWidget = class {
|
|
|
464
744
|
` : "") + `
|
|
465
745
|
What the copilot reported:
|
|
466
746
|
${reason}`;
|
|
747
|
+
return { subject, description };
|
|
748
|
+
}
|
|
749
|
+
showTicketForm(subject, description, requestId, answer) {
|
|
467
750
|
const card = document.createElement("div");
|
|
468
|
-
card.className = "acw-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
751
|
+
card.className = "acw-ticket";
|
|
752
|
+
card.innerHTML = `<div class="acw-ticket-title">Raise a support ticket</div><div class="acw-ticket-sub">Review and edit the details below, then click Send. It goes to the support team as a Zoho Desk ticket.</div><label class="acw-ticket-label">Subject</label><input class="acw-ticket-subject acw-ticket-input" type="text" /><label class="acw-ticket-label">Description</label><textarea class="acw-ticket-desc" rows="8"></textarea><div class="acw-ticket-actions"><button class="acw-ticket-send acw-btn-primary" type="button">Send ticket</button><button class="acw-ticket-cancel acw-btn-ghost" type="button">Cancel</button></div>`;
|
|
753
|
+
const subjectInput = card.querySelector(".acw-ticket-subject");
|
|
754
|
+
const descInput = card.querySelector(".acw-ticket-desc");
|
|
755
|
+
subjectInput.value = subject;
|
|
756
|
+
descInput.value = description;
|
|
474
757
|
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
475
758
|
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
476
|
-
const subj =
|
|
477
|
-
const desc =
|
|
759
|
+
const subj = subjectInput.value.trim();
|
|
760
|
+
const desc = descInput.value.trim();
|
|
761
|
+
if (!subj) {
|
|
762
|
+
subjectInput.focus();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (!desc) {
|
|
766
|
+
descInput.focus();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
478
769
|
card.remove();
|
|
479
|
-
void this.submitTicket(subj, desc, requestId);
|
|
770
|
+
void this.submitTicket(subj, desc, requestId, answer.primaryReason);
|
|
480
771
|
});
|
|
481
772
|
this.body.appendChild(card);
|
|
482
773
|
this.body.scrollTop = this.body.scrollHeight;
|
|
774
|
+
subjectInput.focus();
|
|
483
775
|
}
|
|
484
|
-
async submitTicket(subject, description, requestId) {
|
|
776
|
+
async submitTicket(subject, description, requestId, reason) {
|
|
485
777
|
this.setProgress("Sending your support ticket\u2026");
|
|
778
|
+
const epoch = this.epoch;
|
|
486
779
|
try {
|
|
487
780
|
const sessionId = await this.ensureSession();
|
|
488
|
-
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/support-ticket`, {
|
|
781
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/support-ticket`, {
|
|
489
782
|
method: "POST",
|
|
490
783
|
headers: this.headers(),
|
|
491
784
|
body: JSON.stringify({
|
|
492
785
|
tenantUniqueName: this.config.tenantUniqueName,
|
|
493
|
-
environment: this.config.environment,
|
|
494
786
|
requestId,
|
|
495
787
|
subject,
|
|
496
|
-
description
|
|
788
|
+
description,
|
|
789
|
+
reason,
|
|
790
|
+
question: this.lastQuestion || void 0
|
|
497
791
|
})
|
|
498
792
|
});
|
|
793
|
+
if (epoch !== this.epoch) return;
|
|
499
794
|
this.setProgress("");
|
|
500
|
-
if (res.status
|
|
501
|
-
this.
|
|
795
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
796
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
797
|
+
this.handleInvalidSession();
|
|
502
798
|
return;
|
|
503
799
|
}
|
|
504
800
|
if (!res.ok) {
|
|
@@ -506,17 +802,42 @@ ${reason}`;
|
|
|
506
802
|
return;
|
|
507
803
|
}
|
|
508
804
|
const data = await res.json();
|
|
805
|
+
if (epoch !== this.epoch) return;
|
|
509
806
|
if (data?.status === "Created") {
|
|
510
|
-
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
511
807
|
const el = document.createElement("div");
|
|
512
808
|
el.className = "acw-answer acw-okstate";
|
|
513
|
-
|
|
809
|
+
const msg = document.createElement("div");
|
|
810
|
+
msg.className = "acw-reason";
|
|
811
|
+
msg.textContent = data.message?.trim() || (data.ticketNumber ? `Your support ticket ${data.ticketNumber} has been created.` : "Your support ticket has been created.");
|
|
812
|
+
el.appendChild(msg);
|
|
813
|
+
const actions = document.createElement("div");
|
|
814
|
+
actions.className = "acw-offer-actions";
|
|
815
|
+
if (data.webUrl || data.id || this.config.onOpenTicket) {
|
|
816
|
+
const open = document.createElement("button");
|
|
817
|
+
open.type = "button";
|
|
818
|
+
open.className = "acw-link-btn";
|
|
819
|
+
open.textContent = "Open the ticket";
|
|
820
|
+
open.addEventListener("click", () => {
|
|
821
|
+
const ticket = { id: data.id, number: data.ticketNumber, webUrl: data.webUrl };
|
|
822
|
+
if (this.config.onOpenTicket) {
|
|
823
|
+
this.close();
|
|
824
|
+
this.config.onOpenTicket(ticket);
|
|
825
|
+
} else {
|
|
826
|
+
const href = safeHref(data.webUrl);
|
|
827
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
actions.appendChild(open);
|
|
831
|
+
}
|
|
832
|
+
this.appendOpenSupport(actions);
|
|
833
|
+
if (actions.childElementCount > 0) el.appendChild(actions);
|
|
514
834
|
this.body.appendChild(el);
|
|
515
835
|
this.body.scrollTop = this.body.scrollHeight;
|
|
516
836
|
} else {
|
|
517
837
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
518
838
|
}
|
|
519
839
|
} catch {
|
|
840
|
+
if (epoch !== this.epoch) return;
|
|
520
841
|
this.setProgress("");
|
|
521
842
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
522
843
|
}
|
|
@@ -525,21 +846,62 @@ ${reason}`;
|
|
|
525
846
|
headers() {
|
|
526
847
|
const h = { "Content-Type": "application/json" };
|
|
527
848
|
const token = this.config.getAuthToken?.();
|
|
528
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
529
849
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
530
|
-
else if (email) h["X-User-Email"] = email;
|
|
531
850
|
return h;
|
|
532
851
|
}
|
|
852
|
+
/** Reset the session when the tenant or the token's user no longer match the ones it was opened for. */
|
|
853
|
+
syncSessionScope() {
|
|
854
|
+
if (!this.config || !this.sessionId) return;
|
|
855
|
+
const tenantChanged = this.sessionTenant !== this.config.tenantUniqueName;
|
|
856
|
+
const identity = tokenIdentity(this.config.getAuthToken?.());
|
|
857
|
+
const userChanged = !!identity && !!this.sessionIdentity && identity !== this.sessionIdentity;
|
|
858
|
+
if (tenantChanged || userChanged) this.resetSession();
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* B5 contract check (R1): a 403 {status:"FeatureDisabled"} means a capability is switched off, not that the session is
|
|
862
|
+
* invalid - keep the session and show the server's reason instead of resetting.
|
|
863
|
+
*/
|
|
864
|
+
async renderIfFeatureDisabled(res) {
|
|
865
|
+
if (res.status !== 403) return false;
|
|
866
|
+
try {
|
|
867
|
+
const body = await res.clone().json();
|
|
868
|
+
if (body?.status !== "FeatureDisabled") return false;
|
|
869
|
+
this.setProgress("");
|
|
870
|
+
this.renderAnswer({ status: "FeatureDisabled", primaryReason: typeof body.reason === "string" ? body.reason : "That part of the Copilot is not enabled yet." });
|
|
871
|
+
return true;
|
|
872
|
+
} catch {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/** 403/404 on a session-scoped call: drop the session and tell the user to ask again. */
|
|
877
|
+
handleInvalidSession() {
|
|
878
|
+
this.setProgress("");
|
|
879
|
+
this.resetSession();
|
|
880
|
+
if (!this.body) return;
|
|
881
|
+
const el = document.createElement("div");
|
|
882
|
+
el.className = "acw-answer";
|
|
883
|
+
el.innerHTML = `<div class="acw-error">${escapeHtml(SESSION_RESET_TEXT)}</div>`;
|
|
884
|
+
this.body.appendChild(el);
|
|
885
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
886
|
+
}
|
|
533
887
|
async ensureSession() {
|
|
888
|
+
this.syncSessionScope();
|
|
534
889
|
if (this.sessionId) return this.sessionId;
|
|
890
|
+
const tenant = this.config.tenantUniqueName;
|
|
891
|
+
const identity = tokenIdentity(this.config.getAuthToken?.());
|
|
892
|
+
const epoch = this.epoch;
|
|
535
893
|
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
536
894
|
method: "POST",
|
|
537
895
|
headers: this.headers(),
|
|
538
|
-
|
|
896
|
+
// The session is bound to the signed-in user and this tenant (verified server-side).
|
|
897
|
+
body: JSON.stringify({ projectKey: this.config.projectKey, tenantUniqueName: tenant })
|
|
539
898
|
});
|
|
540
899
|
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
541
900
|
const session = await res.json();
|
|
901
|
+
if (epoch !== this.epoch) throw new Error("The Copilot session was reset.");
|
|
542
902
|
this.sessionId = session.sessionId;
|
|
903
|
+
this.sessionTenant = tenant;
|
|
904
|
+
this.sessionIdentity = identity;
|
|
543
905
|
if (this.connection && this.sessionId) {
|
|
544
906
|
try {
|
|
545
907
|
await this.connection.invoke("JoinSession", this.sessionId);
|
|
@@ -548,25 +910,37 @@ ${reason}`;
|
|
|
548
910
|
}
|
|
549
911
|
return this.sessionId;
|
|
550
912
|
}
|
|
551
|
-
async runAsk(question) {
|
|
913
|
+
async runAsk(question, notHelpful = false) {
|
|
552
914
|
this.open();
|
|
553
915
|
this.lastQuestion = question;
|
|
554
|
-
this.addMessage(question, "user");
|
|
916
|
+
this.addMessage(notHelpful ? "This didn\u2019t help." : question, "user");
|
|
555
917
|
this.setProgress("Reading your question\u2026");
|
|
918
|
+
const epoch = this.epoch;
|
|
556
919
|
try {
|
|
557
920
|
const sessionId = await this.ensureSession();
|
|
558
|
-
const
|
|
921
|
+
const body = { question, tenantUniqueName: this.config.tenantUniqueName, context: this.currentContext() };
|
|
922
|
+
if (notHelpful) body.notHelpful = true;
|
|
923
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
|
559
924
|
method: "POST",
|
|
560
925
|
headers: this.headers(),
|
|
561
|
-
body: JSON.stringify(
|
|
926
|
+
body: JSON.stringify(body)
|
|
562
927
|
});
|
|
928
|
+
if (epoch !== this.epoch) return;
|
|
929
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
930
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
931
|
+
this.handleInvalidSession();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
563
934
|
if (!res.ok) {
|
|
564
935
|
this.setProgress("");
|
|
565
936
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
566
937
|
return;
|
|
567
938
|
}
|
|
568
|
-
|
|
939
|
+
const answer = await res.json();
|
|
940
|
+
if (epoch !== this.epoch) return;
|
|
941
|
+
this.renderAnswer(answer, question, notHelpful);
|
|
569
942
|
} catch (err) {
|
|
943
|
+
if (epoch !== this.epoch) return;
|
|
570
944
|
this.setProgress("");
|
|
571
945
|
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
572
946
|
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
@@ -575,7 +949,7 @@ ${reason}`;
|
|
|
575
949
|
async connectSignalR() {
|
|
576
950
|
if (!this.config.signalRHubUrl) return;
|
|
577
951
|
const signalR = await import("@microsoft/signalr");
|
|
578
|
-
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl).withAutomaticReconnect().build();
|
|
952
|
+
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl, { accessTokenFactory: () => this.config.getAuthToken?.() ?? "" }).withAutomaticReconnect().build();
|
|
579
953
|
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
580
954
|
connection.on(event, () => {
|
|
581
955
|
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
@@ -585,9 +959,6 @@ ${reason}`;
|
|
|
585
959
|
this.connection = connection;
|
|
586
960
|
}
|
|
587
961
|
};
|
|
588
|
-
function escapeHtml(s) {
|
|
589
|
-
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
590
|
-
}
|
|
591
962
|
|
|
592
963
|
// src/index.ts
|
|
593
964
|
var instance = new CopilotWidget();
|
|
@@ -601,7 +972,9 @@ var ApplicationCopilotWidget = {
|
|
|
601
972
|
promptUpload: () => instance.promptUpload(),
|
|
602
973
|
ask: (question) => instance.ask(question),
|
|
603
974
|
setContext: (context) => instance.setContext(context),
|
|
604
|
-
captureError: (errorContext) => instance.captureError(errorContext)
|
|
975
|
+
captureError: (errorContext) => instance.captureError(errorContext),
|
|
976
|
+
/** Drop the current Copilot session and conversation (e.g. after a tenant switch or sign-out). */
|
|
977
|
+
resetSession: () => instance.resetSession()
|
|
605
978
|
};
|
|
606
979
|
if (typeof window !== "undefined") {
|
|
607
980
|
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|