@spektra-cloudevents/application-copilot-widget 1.2.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 +400 -63
- package/dist/index.d.cts +64 -8
- package/dist/index.d.ts +64 -8
- package/dist/index.js +400 -63
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -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,19 +116,37 @@ 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
|
}
|
|
@@ -148,16 +166,90 @@ var widgetStyles = `
|
|
|
148
166
|
}
|
|
149
167
|
.acw-ticket-desc { resize: vertical; min-height: 170px; line-height: 1.5; }
|
|
150
168
|
.acw-ticket-input:focus, .acw-ticket-desc:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
151
|
-
.acw-ticket-actions { display: flex; gap: 10px; margin-top: 14px; }
|
|
169
|
+
.acw-ticket-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
|
|
152
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; }
|
|
153
171
|
.acw-btn-primary:hover { background: var(--acw-accent-dark); }
|
|
154
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; }
|
|
155
173
|
.acw-btn-ghost:hover { background: #f0eefe; }
|
|
156
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; }
|
|
157
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
|
+
}
|
|
158
193
|
`;
|
|
159
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
|
+
|
|
160
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.";
|
|
161
253
|
var PROGRESS_LABELS = {
|
|
162
254
|
QuestionAccepted: "Reading your question\u2026",
|
|
163
255
|
PermissionsVerified: "Verifying your permissions\u2026",
|
|
@@ -168,18 +260,50 @@ var CopilotWidget = class {
|
|
|
168
260
|
constructor() {
|
|
169
261
|
this.lastQuestion = "";
|
|
170
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;
|
|
171
266
|
}
|
|
172
267
|
init(config) {
|
|
268
|
+
const previousTenant = this.config?.tenantUniqueName;
|
|
173
269
|
this.config = config;
|
|
174
270
|
try {
|
|
175
271
|
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
176
272
|
} catch {
|
|
177
273
|
}
|
|
274
|
+
if (previousTenant !== void 0 && previousTenant !== config.tenantUniqueName) this.resetSession();
|
|
178
275
|
this.mount();
|
|
179
|
-
this.
|
|
180
|
-
|
|
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
|
+
}
|
|
181
304
|
}
|
|
182
305
|
open() {
|
|
306
|
+
this.syncSessionScope();
|
|
183
307
|
this.panel?.classList.add("acw-open");
|
|
184
308
|
this.refreshChips();
|
|
185
309
|
}
|
|
@@ -224,6 +348,7 @@ var CopilotWidget = class {
|
|
|
224
348
|
this.body.scrollTop = this.body.scrollHeight;
|
|
225
349
|
}
|
|
226
350
|
ask(question) {
|
|
351
|
+
this.syncSessionScope();
|
|
227
352
|
if (this.mode === "intake") {
|
|
228
353
|
void this.runIntakeAsk(question);
|
|
229
354
|
} else {
|
|
@@ -241,7 +366,7 @@ var CopilotWidget = class {
|
|
|
241
366
|
style.textContent = widgetStyles;
|
|
242
367
|
this.root.appendChild(style);
|
|
243
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>';
|
|
244
|
-
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
369
|
+
const tip = escapeHtml(this.config.buttonText ?? "Ask Application Copilot");
|
|
245
370
|
const wrap = document.createElement("div");
|
|
246
371
|
wrap.className = "acw-launch-wrap";
|
|
247
372
|
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
@@ -254,7 +379,7 @@ var CopilotWidget = class {
|
|
|
254
379
|
<div class="acw-header">
|
|
255
380
|
${sparkle}
|
|
256
381
|
<span class="acw-title">Application Copilot</span>
|
|
257
|
-
<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>
|
|
258
383
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
259
384
|
</div>
|
|
260
385
|
<div class="acw-body"></div>
|
|
@@ -282,10 +407,21 @@ var CopilotWidget = class {
|
|
|
282
407
|
if (e.key === "Enter") this.submitFromInput();
|
|
283
408
|
});
|
|
284
409
|
}
|
|
285
|
-
/**
|
|
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
|
+
*/
|
|
286
414
|
promptUpload() {
|
|
287
415
|
this.open();
|
|
288
|
-
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";
|
|
289
425
|
}
|
|
290
426
|
async uploadDocument(file) {
|
|
291
427
|
this.open();
|
|
@@ -295,16 +431,14 @@ var CopilotWidget = class {
|
|
|
295
431
|
const form = new FormData();
|
|
296
432
|
form.append("file", file, file.name);
|
|
297
433
|
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
298
|
-
form.append("environment", this.config.environment);
|
|
299
434
|
const h = {};
|
|
300
435
|
const token = this.config.getAuthToken?.();
|
|
301
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
302
436
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
303
|
-
else if (email) h["X-User-Email"] = email;
|
|
304
437
|
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
305
438
|
this.setProgress("");
|
|
306
439
|
if (res.status === 403) {
|
|
307
|
-
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." });
|
|
308
442
|
return;
|
|
309
443
|
}
|
|
310
444
|
if (!res.ok) {
|
|
@@ -313,7 +447,7 @@ var CopilotWidget = class {
|
|
|
313
447
|
}
|
|
314
448
|
const data = await res.json();
|
|
315
449
|
if (data?.status === "Indexed") {
|
|
316
|
-
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.` });
|
|
317
451
|
} else {
|
|
318
452
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
319
453
|
}
|
|
@@ -362,20 +496,30 @@ var CopilotWidget = class {
|
|
|
362
496
|
this.open();
|
|
363
497
|
this.addMessage(question, "user");
|
|
364
498
|
this.setProgress("Finding the right offering\u2026");
|
|
499
|
+
const epoch = this.epoch;
|
|
365
500
|
try {
|
|
366
501
|
const sessionId = await this.ensureSession();
|
|
367
|
-
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`, {
|
|
368
503
|
method: "POST",
|
|
369
504
|
headers: this.headers(),
|
|
370
505
|
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
371
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
|
+
}
|
|
372
513
|
if (!res.ok) {
|
|
373
514
|
this.setProgress("");
|
|
374
515
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
375
516
|
return;
|
|
376
517
|
}
|
|
377
|
-
|
|
518
|
+
const data = await res.json();
|
|
519
|
+
if (epoch !== this.epoch) return;
|
|
520
|
+
this.renderIntakeAnswer(data);
|
|
378
521
|
} catch {
|
|
522
|
+
if (epoch !== this.epoch) return;
|
|
379
523
|
this.setProgress("");
|
|
380
524
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
381
525
|
}
|
|
@@ -434,10 +578,10 @@ var CopilotWidget = class {
|
|
|
434
578
|
p.textContent = text;
|
|
435
579
|
this.body.scrollTop = this.body.scrollHeight;
|
|
436
580
|
}
|
|
437
|
-
renderAnswer(answer) {
|
|
581
|
+
renderAnswer(answer, question, wasNotHelpful = false) {
|
|
438
582
|
this.setProgress("");
|
|
439
583
|
const el = document.createElement("div");
|
|
440
|
-
const isFailure = answer.status && answer.status !== "Answered";
|
|
584
|
+
const isFailure = !!answer.status && answer.status !== "Answered";
|
|
441
585
|
if (isFailure) {
|
|
442
586
|
el.className = "acw-answer";
|
|
443
587
|
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
@@ -447,21 +591,99 @@ var CopilotWidget = class {
|
|
|
447
591
|
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
448
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>` : "");
|
|
449
593
|
}
|
|
594
|
+
this.appendReferences(el, answer);
|
|
450
595
|
this.body.appendChild(el);
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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);
|
|
462
617
|
}
|
|
463
618
|
this.body.scrollTop = this.body.scrollHeight;
|
|
464
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
|
+
}
|
|
465
687
|
currentRequestId() {
|
|
466
688
|
const ctx = this.currentContext();
|
|
467
689
|
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
@@ -472,10 +694,45 @@ var CopilotWidget = class {
|
|
|
472
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 || "");
|
|
473
695
|
return guid ? guid[0] : void 0;
|
|
474
696
|
}
|
|
475
|
-
/**
|
|
476
|
-
openTicketDraft(answer) {
|
|
697
|
+
/** Fetch the server-built draft, then show it as an editable form; the user triggers Send. */
|
|
698
|
+
async openTicketDraft(answer) {
|
|
477
699
|
const fromAnswer = /MS[A-Z0-9]{6,}/i.exec(answer.primaryReason || "");
|
|
478
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
|
+
);
|
|
733
|
+
}
|
|
734
|
+
/** Used only when the draft endpoint is unavailable (older server or a transient error). */
|
|
735
|
+
localDraft(answer, requestId) {
|
|
479
736
|
const role = answer.verifiedRoleLabel || "";
|
|
480
737
|
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
481
738
|
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
@@ -487,39 +744,57 @@ var CopilotWidget = class {
|
|
|
487
744
|
` : "") + `
|
|
488
745
|
What the copilot reported:
|
|
489
746
|
${reason}`;
|
|
747
|
+
return { subject, description };
|
|
748
|
+
}
|
|
749
|
+
showTicketForm(subject, description, requestId, answer) {
|
|
490
750
|
const card = document.createElement("div");
|
|
491
751
|
card.className = "acw-ticket";
|
|
492
|
-
card.innerHTML = `<div class="acw-ticket-title">Raise a support ticket</div><div class="acw-ticket-sub">Review the details below
|
|
493
|
-
card.querySelector(".acw-ticket-subject")
|
|
494
|
-
card.querySelector(".acw-ticket-desc")
|
|
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;
|
|
495
757
|
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
496
758
|
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
497
|
-
const subj =
|
|
498
|
-
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
|
+
}
|
|
499
769
|
card.remove();
|
|
500
|
-
void this.submitTicket(subj, desc, requestId);
|
|
770
|
+
void this.submitTicket(subj, desc, requestId, answer.primaryReason);
|
|
501
771
|
});
|
|
502
772
|
this.body.appendChild(card);
|
|
503
773
|
this.body.scrollTop = this.body.scrollHeight;
|
|
774
|
+
subjectInput.focus();
|
|
504
775
|
}
|
|
505
|
-
async submitTicket(subject, description, requestId) {
|
|
776
|
+
async submitTicket(subject, description, requestId, reason) {
|
|
506
777
|
this.setProgress("Sending your support ticket\u2026");
|
|
778
|
+
const epoch = this.epoch;
|
|
507
779
|
try {
|
|
508
780
|
const sessionId = await this.ensureSession();
|
|
509
|
-
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`, {
|
|
510
782
|
method: "POST",
|
|
511
783
|
headers: this.headers(),
|
|
512
784
|
body: JSON.stringify({
|
|
513
785
|
tenantUniqueName: this.config.tenantUniqueName,
|
|
514
|
-
environment: this.config.environment,
|
|
515
786
|
requestId,
|
|
516
787
|
subject,
|
|
517
|
-
description
|
|
788
|
+
description,
|
|
789
|
+
reason,
|
|
790
|
+
question: this.lastQuestion || void 0
|
|
518
791
|
})
|
|
519
792
|
});
|
|
793
|
+
if (epoch !== this.epoch) return;
|
|
520
794
|
this.setProgress("");
|
|
521
|
-
if (res.status
|
|
522
|
-
this.
|
|
795
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
796
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
797
|
+
this.handleInvalidSession();
|
|
523
798
|
return;
|
|
524
799
|
}
|
|
525
800
|
if (!res.ok) {
|
|
@@ -527,11 +802,16 @@ ${reason}`;
|
|
|
527
802
|
return;
|
|
528
803
|
}
|
|
529
804
|
const data = await res.json();
|
|
805
|
+
if (epoch !== this.epoch) return;
|
|
530
806
|
if (data?.status === "Created") {
|
|
531
|
-
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
532
807
|
const el = document.createElement("div");
|
|
533
808
|
el.className = "acw-answer acw-okstate";
|
|
534
|
-
|
|
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";
|
|
535
815
|
if (data.webUrl || data.id || this.config.onOpenTicket) {
|
|
536
816
|
const open = document.createElement("button");
|
|
537
817
|
open.type = "button";
|
|
@@ -540,19 +820,24 @@ ${reason}`;
|
|
|
540
820
|
open.addEventListener("click", () => {
|
|
541
821
|
const ticket = { id: data.id, number: data.ticketNumber, webUrl: data.webUrl };
|
|
542
822
|
if (this.config.onOpenTicket) {
|
|
823
|
+
this.close();
|
|
543
824
|
this.config.onOpenTicket(ticket);
|
|
544
|
-
} else
|
|
545
|
-
|
|
825
|
+
} else {
|
|
826
|
+
const href = safeHref(data.webUrl);
|
|
827
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
546
828
|
}
|
|
547
829
|
});
|
|
548
|
-
|
|
830
|
+
actions.appendChild(open);
|
|
549
831
|
}
|
|
832
|
+
this.appendOpenSupport(actions);
|
|
833
|
+
if (actions.childElementCount > 0) el.appendChild(actions);
|
|
550
834
|
this.body.appendChild(el);
|
|
551
835
|
this.body.scrollTop = this.body.scrollHeight;
|
|
552
836
|
} else {
|
|
553
837
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
554
838
|
}
|
|
555
839
|
} catch {
|
|
840
|
+
if (epoch !== this.epoch) return;
|
|
556
841
|
this.setProgress("");
|
|
557
842
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
558
843
|
}
|
|
@@ -561,21 +846,62 @@ ${reason}`;
|
|
|
561
846
|
headers() {
|
|
562
847
|
const h = { "Content-Type": "application/json" };
|
|
563
848
|
const token = this.config.getAuthToken?.();
|
|
564
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
565
849
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
566
|
-
else if (email) h["X-User-Email"] = email;
|
|
567
850
|
return h;
|
|
568
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
|
+
}
|
|
569
887
|
async ensureSession() {
|
|
888
|
+
this.syncSessionScope();
|
|
570
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;
|
|
571
893
|
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
572
894
|
method: "POST",
|
|
573
895
|
headers: this.headers(),
|
|
574
|
-
|
|
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 })
|
|
575
898
|
});
|
|
576
899
|
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
577
900
|
const session = await res.json();
|
|
901
|
+
if (epoch !== this.epoch) throw new Error("The Copilot session was reset.");
|
|
578
902
|
this.sessionId = session.sessionId;
|
|
903
|
+
this.sessionTenant = tenant;
|
|
904
|
+
this.sessionIdentity = identity;
|
|
579
905
|
if (this.connection && this.sessionId) {
|
|
580
906
|
try {
|
|
581
907
|
await this.connection.invoke("JoinSession", this.sessionId);
|
|
@@ -584,25 +910,37 @@ ${reason}`;
|
|
|
584
910
|
}
|
|
585
911
|
return this.sessionId;
|
|
586
912
|
}
|
|
587
|
-
async runAsk(question) {
|
|
913
|
+
async runAsk(question, notHelpful = false) {
|
|
588
914
|
this.open();
|
|
589
915
|
this.lastQuestion = question;
|
|
590
|
-
this.addMessage(question, "user");
|
|
916
|
+
this.addMessage(notHelpful ? "This didn\u2019t help." : question, "user");
|
|
591
917
|
this.setProgress("Reading your question\u2026");
|
|
918
|
+
const epoch = this.epoch;
|
|
592
919
|
try {
|
|
593
920
|
const sessionId = await this.ensureSession();
|
|
594
|
-
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`, {
|
|
595
924
|
method: "POST",
|
|
596
925
|
headers: this.headers(),
|
|
597
|
-
body: JSON.stringify(
|
|
926
|
+
body: JSON.stringify(body)
|
|
598
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
|
+
}
|
|
599
934
|
if (!res.ok) {
|
|
600
935
|
this.setProgress("");
|
|
601
936
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
602
937
|
return;
|
|
603
938
|
}
|
|
604
|
-
|
|
939
|
+
const answer = await res.json();
|
|
940
|
+
if (epoch !== this.epoch) return;
|
|
941
|
+
this.renderAnswer(answer, question, notHelpful);
|
|
605
942
|
} catch (err) {
|
|
943
|
+
if (epoch !== this.epoch) return;
|
|
606
944
|
this.setProgress("");
|
|
607
945
|
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
608
946
|
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
@@ -611,7 +949,7 @@ ${reason}`;
|
|
|
611
949
|
async connectSignalR() {
|
|
612
950
|
if (!this.config.signalRHubUrl) return;
|
|
613
951
|
const signalR = await import("@microsoft/signalr");
|
|
614
|
-
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();
|
|
615
953
|
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
616
954
|
connection.on(event, () => {
|
|
617
955
|
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
@@ -621,9 +959,6 @@ ${reason}`;
|
|
|
621
959
|
this.connection = connection;
|
|
622
960
|
}
|
|
623
961
|
};
|
|
624
|
-
function escapeHtml(s) {
|
|
625
|
-
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
626
|
-
}
|
|
627
962
|
|
|
628
963
|
// src/index.ts
|
|
629
964
|
var instance = new CopilotWidget();
|
|
@@ -637,7 +972,9 @@ var ApplicationCopilotWidget = {
|
|
|
637
972
|
promptUpload: () => instance.promptUpload(),
|
|
638
973
|
ask: (question) => instance.ask(question),
|
|
639
974
|
setContext: (context) => instance.setContext(context),
|
|
640
|
-
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()
|
|
641
978
|
};
|
|
642
979
|
if (typeof window !== "undefined") {
|
|
643
980
|
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|