@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.js
CHANGED
|
@@ -59,7 +59,7 @@ var widgetStyles = `
|
|
|
59
59
|
.acw-close { margin-left: auto; border: none; background: transparent; cursor: pointer; font-size: 22px; color: rgba(255,255,255,0.85); line-height: 1; }
|
|
60
60
|
.acw-close:hover { color: #fff; }
|
|
61
61
|
|
|
62
|
-
.acw-body { flex: 1; overflow-y: auto; padding: 16px; background: var(--acw-surface); }
|
|
62
|
+
.acw-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 16px; background: var(--acw-surface); }
|
|
63
63
|
.acw-hint { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
64
64
|
|
|
65
65
|
.acw-chip {
|
|
@@ -79,19 +79,37 @@ var widgetStyles = `
|
|
|
79
79
|
.acw-msg.acw-user span, .acw-msg.acw-user {
|
|
80
80
|
display: inline-block;
|
|
81
81
|
}
|
|
82
|
+
.acw-msg.acw-user { display: block; max-width: 100%; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
82
83
|
.acw-progress { font-size: 13px; color: var(--acw-muted); margin: 10px 0; }
|
|
83
84
|
|
|
84
85
|
.acw-answer { background: var(--acw-bg); border: 1px solid var(--acw-border); border-radius: 12px; padding: 12px 14px; margin: 12px 0; }
|
|
85
|
-
.acw-answer .acw-reason { font-size: 14px; line-height: 1.5; }
|
|
86
|
+
.acw-answer .acw-reason, .acw-msg .acw-reason { font-size: 14px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
86
87
|
.acw-answer.acw-blocked { border-left: 4px solid var(--acw-danger); }
|
|
87
88
|
.acw-answer.acw-okstate { border-left: 4px solid var(--acw-ok); }
|
|
88
89
|
.acw-role { font-size: 12px; color: var(--acw-muted); margin-top: 8px; }
|
|
89
90
|
.acw-evidence { font-size: 12px; color: var(--acw-muted); margin-top: 10px; word-break: break-word; }
|
|
90
|
-
.acw-error { font-size: 14px; color: var(--acw-danger); }
|
|
91
|
+
.acw-error { font-size: 14px; color: var(--acw-danger); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
92
|
+
|
|
93
|
+
/* Knowledge-base references */
|
|
94
|
+
.acw-refs { margin-top: 10px; font-size: 12px; }
|
|
95
|
+
.acw-refs-label { color: var(--acw-muted); font-weight: 600; margin-bottom: 4px; }
|
|
96
|
+
.acw-refs-list { margin: 0; padding-left: 18px; }
|
|
97
|
+
.acw-refs-list li { margin: 2px 0; }
|
|
98
|
+
.acw-ref { color: var(--acw-accent); text-decoration: underline; overflow-wrap: anywhere; }
|
|
99
|
+
.acw-ref:hover { color: var(--acw-accent-dark); }
|
|
100
|
+
|
|
101
|
+
/* Support-ticket offer */
|
|
102
|
+
.acw-offer { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--acw-border); }
|
|
103
|
+
.acw-offer-text { font-size: 13px; line-height: 1.5; color: var(--acw-fg); white-space: pre-wrap; overflow-wrap: anywhere; margin-bottom: 8px; }
|
|
104
|
+
.acw-offer-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; }
|
|
105
|
+
.acw-offer-actions .acw-chip { width: auto; margin-bottom: 0; }
|
|
106
|
+
.acw-offer-actions .acw-link-btn { margin-top: 0; }
|
|
107
|
+
.acw-chip:disabled, .acw-link-btn:disabled { opacity: .6; cursor: default; }
|
|
108
|
+
.acw-nothelpful { display: block; margin-top: 10px; color: var(--acw-muted); }
|
|
91
109
|
|
|
92
110
|
.acw-footer { display: flex; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--acw-border); background: var(--acw-bg); }
|
|
93
111
|
.acw-input {
|
|
94
|
-
flex: 1; padding: 10px 12px; font-size: 14px;
|
|
112
|
+
flex: 1; min-width: 0; padding: 10px 12px; font-size: 14px;
|
|
95
113
|
border: 1px solid var(--acw-border); border-radius: 10px; color: var(--acw-fg); background: var(--acw-bg);
|
|
96
114
|
font-family: inherit;
|
|
97
115
|
}
|
|
@@ -111,16 +129,90 @@ var widgetStyles = `
|
|
|
111
129
|
}
|
|
112
130
|
.acw-ticket-desc { resize: vertical; min-height: 170px; line-height: 1.5; }
|
|
113
131
|
.acw-ticket-input:focus, .acw-ticket-desc:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
114
|
-
.acw-ticket-actions { display: flex; gap: 10px; margin-top: 14px; }
|
|
132
|
+
.acw-ticket-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
|
|
115
133
|
.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; }
|
|
116
134
|
.acw-btn-primary:hover { background: var(--acw-accent-dark); }
|
|
117
135
|
.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; }
|
|
118
136
|
.acw-btn-ghost:hover { background: #f0eefe; }
|
|
119
137
|
.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; }
|
|
120
138
|
.acw-link-btn:hover { color: var(--acw-accent-dark); }
|
|
139
|
+
|
|
140
|
+
/* Responsive: full-width panel below 480px; never taller than the viewport; no horizontal scroll. */
|
|
141
|
+
@media (max-width: 479.98px) {
|
|
142
|
+
.acw-launch-wrap { right: 16px; bottom: 16px; }
|
|
143
|
+
.acw-tip { display: none; }
|
|
144
|
+
.acw-panel {
|
|
145
|
+
left: 0; right: 0; bottom: 0;
|
|
146
|
+
width: 100%; max-width: 100%;
|
|
147
|
+
height: calc(100vh - 32px); max-height: calc(100vh - 32px);
|
|
148
|
+
border-radius: 16px 16px 0 0; border-left: none; border-right: none; border-bottom: none;
|
|
149
|
+
}
|
|
150
|
+
.acw-ticket-desc { min-height: 120px; }
|
|
151
|
+
}
|
|
152
|
+
/* Short desktop/tablet viewports: keep the panel inside the viewport (beside the launcher). */
|
|
153
|
+
@media (max-height: 700px) and (min-width: 480px) {
|
|
154
|
+
.acw-panel { bottom: 16px; right: 96px; max-width: calc(100vw - 112px); max-height: calc(100vh - 32px); }
|
|
155
|
+
}
|
|
121
156
|
`;
|
|
122
157
|
|
|
158
|
+
// src/internal.ts
|
|
159
|
+
function escapeHtml(value) {
|
|
160
|
+
const s = value === null || value === void 0 ? "" : String(value);
|
|
161
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
162
|
+
}
|
|
163
|
+
function safeHref(raw) {
|
|
164
|
+
if (typeof raw !== "string") return null;
|
|
165
|
+
const trimmed = raw.trim();
|
|
166
|
+
if (!trimmed) return null;
|
|
167
|
+
let url;
|
|
168
|
+
try {
|
|
169
|
+
url = new URL(trimmed);
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
if (url.protocol !== "https:") return null;
|
|
174
|
+
if (url.username || url.password) return null;
|
|
175
|
+
if (!url.hostname) return null;
|
|
176
|
+
return url.href;
|
|
177
|
+
}
|
|
178
|
+
function normalizeReferences(refs, max = 10) {
|
|
179
|
+
if (!Array.isArray(refs)) return [];
|
|
180
|
+
const out = [];
|
|
181
|
+
const seen = /* @__PURE__ */ new Set();
|
|
182
|
+
for (const r of refs) {
|
|
183
|
+
if (!r || typeof r !== "object") continue;
|
|
184
|
+
const href = safeHref(r.url);
|
|
185
|
+
if (!href || seen.has(href)) continue;
|
|
186
|
+
seen.add(href);
|
|
187
|
+
const rawTitle = r.title;
|
|
188
|
+
const title = typeof rawTitle === "string" && rawTitle.trim() ? rawTitle.trim() : new URL(href).hostname;
|
|
189
|
+
out.push({ title, href });
|
|
190
|
+
if (out.length >= max) break;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function isSessionInvalidStatus(status) {
|
|
195
|
+
return status === 403 || status === 404;
|
|
196
|
+
}
|
|
197
|
+
function tokenIdentity(token) {
|
|
198
|
+
if (!token || typeof token !== "string") return null;
|
|
199
|
+
const parts = token.split(".");
|
|
200
|
+
if (parts.length < 2 || !parts[1]) return null;
|
|
201
|
+
try {
|
|
202
|
+
const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
203
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
204
|
+
const payload = JSON.parse(atob(padded));
|
|
205
|
+
const emails = Array.isArray(payload.emails) ? payload.emails[0] : void 0;
|
|
206
|
+
const id = payload.oid ?? payload.sub ?? emails ?? payload.email ?? payload.preferred_username;
|
|
207
|
+
return typeof id === "string" && id ? id.toLowerCase() : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
123
213
|
// src/widget.ts
|
|
214
|
+
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.";
|
|
215
|
+
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.";
|
|
124
216
|
var PROGRESS_LABELS = {
|
|
125
217
|
QuestionAccepted: "Reading your question\u2026",
|
|
126
218
|
PermissionsVerified: "Verifying your permissions\u2026",
|
|
@@ -131,18 +223,50 @@ var CopilotWidget = class {
|
|
|
131
223
|
constructor() {
|
|
132
224
|
this.lastQuestion = "";
|
|
133
225
|
this.mode = "explain";
|
|
226
|
+
this.connecting = false;
|
|
227
|
+
/** Bumped by resetSession(); responses that belong to an older epoch are dropped. */
|
|
228
|
+
this.epoch = 0;
|
|
134
229
|
}
|
|
135
230
|
init(config) {
|
|
231
|
+
const previousTenant = this.config?.tenantUniqueName;
|
|
136
232
|
this.config = config;
|
|
137
233
|
try {
|
|
138
234
|
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
139
235
|
} catch {
|
|
140
236
|
}
|
|
237
|
+
if (previousTenant !== void 0 && previousTenant !== config.tenantUniqueName) this.resetSession();
|
|
141
238
|
this.mount();
|
|
142
|
-
this.
|
|
143
|
-
|
|
239
|
+
this.applyKnowledgeUploadVisibility();
|
|
240
|
+
if (!this.connection && !this.connecting) {
|
|
241
|
+
this.connecting = true;
|
|
242
|
+
this.connectSignalR().catch(() => {
|
|
243
|
+
}).finally(() => {
|
|
244
|
+
this.connecting = false;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Drop the current Copilot session and clear the conversation. The next question opens a new session
|
|
250
|
+
* for the current `tenantUniqueName` and signed-in user. Also called automatically when the tenant or the
|
|
251
|
+
* token's user changes and when the API answers 403/404 for the session.
|
|
252
|
+
*/
|
|
253
|
+
resetSession() {
|
|
254
|
+
this.epoch++;
|
|
255
|
+
this.sessionId = void 0;
|
|
256
|
+
this.sessionTenant = void 0;
|
|
257
|
+
this.sessionIdentity = void 0;
|
|
258
|
+
this.lastQuestion = "";
|
|
259
|
+
this.contextOverride = void 0;
|
|
260
|
+
this.recentError = void 0;
|
|
261
|
+
this.mode = "explain";
|
|
262
|
+
if (this.input) this.input.placeholder = "Ask a question\u2026";
|
|
263
|
+
if (this.body) {
|
|
264
|
+
this.body.replaceChildren();
|
|
265
|
+
if (this.panel?.classList.contains("acw-open")) this.refreshChips();
|
|
266
|
+
}
|
|
144
267
|
}
|
|
145
268
|
open() {
|
|
269
|
+
this.syncSessionScope();
|
|
146
270
|
this.panel?.classList.add("acw-open");
|
|
147
271
|
this.refreshChips();
|
|
148
272
|
}
|
|
@@ -187,6 +311,7 @@ var CopilotWidget = class {
|
|
|
187
311
|
this.body.scrollTop = this.body.scrollHeight;
|
|
188
312
|
}
|
|
189
313
|
ask(question) {
|
|
314
|
+
this.syncSessionScope();
|
|
190
315
|
if (this.mode === "intake") {
|
|
191
316
|
void this.runIntakeAsk(question);
|
|
192
317
|
} else {
|
|
@@ -204,7 +329,7 @@ var CopilotWidget = class {
|
|
|
204
329
|
style.textContent = widgetStyles;
|
|
205
330
|
this.root.appendChild(style);
|
|
206
331
|
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>';
|
|
207
|
-
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
332
|
+
const tip = escapeHtml(this.config.buttonText ?? "Ask Application Copilot");
|
|
208
333
|
const wrap = document.createElement("div");
|
|
209
334
|
wrap.className = "acw-launch-wrap";
|
|
210
335
|
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
@@ -217,7 +342,7 @@ var CopilotWidget = class {
|
|
|
217
342
|
<div class="acw-header">
|
|
218
343
|
${sparkle}
|
|
219
344
|
<span class="acw-title">Application Copilot</span>
|
|
220
|
-
<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;
|
|
345
|
+
<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>
|
|
221
346
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
222
347
|
</div>
|
|
223
348
|
<div class="acw-body"></div>
|
|
@@ -245,10 +370,21 @@ var CopilotWidget = class {
|
|
|
245
370
|
if (e.key === "Enter") this.submitFromInput();
|
|
246
371
|
});
|
|
247
372
|
}
|
|
248
|
-
/**
|
|
373
|
+
/**
|
|
374
|
+
* Open the file picker to add a document to the knowledge base. Only when the host set `showKnowledgeUpload`;
|
|
375
|
+
* otherwise it just opens the panel. The Copilot API authorizes every upload either way.
|
|
376
|
+
*/
|
|
249
377
|
promptUpload() {
|
|
250
378
|
this.open();
|
|
251
|
-
this.fileInput?.click();
|
|
379
|
+
if (this.config?.showKnowledgeUpload === true) this.fileInput?.click();
|
|
380
|
+
}
|
|
381
|
+
/** B5: the upload button is hidden unless the host opts in (re-applied on every init, e.g. after a tenant switch). */
|
|
382
|
+
applyKnowledgeUploadVisibility() {
|
|
383
|
+
const btn = this.panel?.querySelector(".acw-kb");
|
|
384
|
+
if (!btn) return;
|
|
385
|
+
const show = this.config?.showKnowledgeUpload === true;
|
|
386
|
+
btn.hidden = !show;
|
|
387
|
+
btn.style.display = show ? "inline-flex" : "none";
|
|
252
388
|
}
|
|
253
389
|
async uploadDocument(file) {
|
|
254
390
|
this.open();
|
|
@@ -258,16 +394,14 @@ var CopilotWidget = class {
|
|
|
258
394
|
const form = new FormData();
|
|
259
395
|
form.append("file", file, file.name);
|
|
260
396
|
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
261
|
-
form.append("environment", this.config.environment);
|
|
262
397
|
const h = {};
|
|
263
398
|
const token = this.config.getAuthToken?.();
|
|
264
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
265
399
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
266
|
-
else if (email) h["X-User-Email"] = email;
|
|
267
400
|
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
268
401
|
this.setProgress("");
|
|
269
402
|
if (res.status === 403) {
|
|
270
|
-
this.
|
|
403
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
404
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Only System Admins and Partner Admins can add to the knowledge base." });
|
|
271
405
|
return;
|
|
272
406
|
}
|
|
273
407
|
if (!res.ok) {
|
|
@@ -276,7 +410,7 @@ var CopilotWidget = class {
|
|
|
276
410
|
}
|
|
277
411
|
const data = await res.json();
|
|
278
412
|
if (data?.status === "Indexed") {
|
|
279
|
-
this.renderAnswer({ status: "Answered", disabled: false, primaryReason: `
|
|
413
|
+
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.` });
|
|
280
414
|
} else {
|
|
281
415
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
282
416
|
}
|
|
@@ -325,20 +459,30 @@ var CopilotWidget = class {
|
|
|
325
459
|
this.open();
|
|
326
460
|
this.addMessage(question, "user");
|
|
327
461
|
this.setProgress("Finding the right offering\u2026");
|
|
462
|
+
const epoch = this.epoch;
|
|
328
463
|
try {
|
|
329
464
|
const sessionId = await this.ensureSession();
|
|
330
|
-
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/intake/ask`, {
|
|
465
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/intake/ask`, {
|
|
331
466
|
method: "POST",
|
|
332
467
|
headers: this.headers(),
|
|
333
468
|
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
334
469
|
});
|
|
470
|
+
if (epoch !== this.epoch) return;
|
|
471
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
472
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
473
|
+
this.handleInvalidSession();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
335
476
|
if (!res.ok) {
|
|
336
477
|
this.setProgress("");
|
|
337
478
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
338
479
|
return;
|
|
339
480
|
}
|
|
340
|
-
|
|
481
|
+
const data = await res.json();
|
|
482
|
+
if (epoch !== this.epoch) return;
|
|
483
|
+
this.renderIntakeAnswer(data);
|
|
341
484
|
} catch {
|
|
485
|
+
if (epoch !== this.epoch) return;
|
|
342
486
|
this.setProgress("");
|
|
343
487
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
344
488
|
}
|
|
@@ -397,10 +541,10 @@ var CopilotWidget = class {
|
|
|
397
541
|
p.textContent = text;
|
|
398
542
|
this.body.scrollTop = this.body.scrollHeight;
|
|
399
543
|
}
|
|
400
|
-
renderAnswer(answer) {
|
|
544
|
+
renderAnswer(answer, question, wasNotHelpful = false) {
|
|
401
545
|
this.setProgress("");
|
|
402
546
|
const el = document.createElement("div");
|
|
403
|
-
const isFailure = answer.status && answer.status !== "Answered";
|
|
547
|
+
const isFailure = !!answer.status && answer.status !== "Answered";
|
|
404
548
|
if (isFailure) {
|
|
405
549
|
el.className = "acw-answer";
|
|
406
550
|
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
@@ -410,21 +554,99 @@ var CopilotWidget = class {
|
|
|
410
554
|
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
411
555
|
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>` : "");
|
|
412
556
|
}
|
|
557
|
+
this.appendReferences(el, answer);
|
|
413
558
|
this.body.appendChild(el);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
559
|
+
let offerTicket;
|
|
560
|
+
if (typeof answer.supportTicketEligible === "boolean") {
|
|
561
|
+
offerTicket = answer.supportTicketEligible;
|
|
562
|
+
} else {
|
|
563
|
+
const role = (answer.verifiedRoleLabel || "").toLowerCase();
|
|
564
|
+
const eligibleRole = role === "requestor" || role === "approver";
|
|
565
|
+
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 || "");
|
|
566
|
+
offerTicket = !isFailure && (answer.disabled === true || looksBlocked) && eligibleRole;
|
|
567
|
+
}
|
|
568
|
+
if (offerTicket) {
|
|
569
|
+
this.appendTicketOffer(el, answer);
|
|
570
|
+
} else if (!isFailure && question && !wasNotHelpful) {
|
|
571
|
+
const nh = document.createElement("button");
|
|
572
|
+
nh.type = "button";
|
|
573
|
+
nh.className = "acw-link-btn acw-nothelpful";
|
|
574
|
+
nh.textContent = "This didn\u2019t help";
|
|
575
|
+
nh.addEventListener("click", () => {
|
|
576
|
+
nh.remove();
|
|
577
|
+
void this.runAsk(question, true);
|
|
578
|
+
});
|
|
579
|
+
el.appendChild(nh);
|
|
425
580
|
}
|
|
426
581
|
this.body.scrollTop = this.body.scrollHeight;
|
|
427
582
|
}
|
|
583
|
+
/** KB references as safe links: https only, built with DOM APIs (no innerHTML), new tab, no opener/referrer. */
|
|
584
|
+
appendReferences(container, answer) {
|
|
585
|
+
const refs = normalizeReferences(answer.references);
|
|
586
|
+
if (refs.length === 0) return;
|
|
587
|
+
const wrap = document.createElement("div");
|
|
588
|
+
wrap.className = "acw-refs";
|
|
589
|
+
const label = document.createElement("div");
|
|
590
|
+
label.className = "acw-refs-label";
|
|
591
|
+
label.textContent = "Learn more:";
|
|
592
|
+
wrap.appendChild(label);
|
|
593
|
+
const list = document.createElement("ul");
|
|
594
|
+
list.className = "acw-refs-list";
|
|
595
|
+
for (const ref of refs) {
|
|
596
|
+
const li = document.createElement("li");
|
|
597
|
+
const a = document.createElement("a");
|
|
598
|
+
a.className = "acw-ref";
|
|
599
|
+
a.setAttribute("href", ref.href);
|
|
600
|
+
a.setAttribute("target", "_blank");
|
|
601
|
+
a.setAttribute("rel", "noopener noreferrer");
|
|
602
|
+
a.textContent = ref.title;
|
|
603
|
+
li.appendChild(a);
|
|
604
|
+
list.appendChild(li);
|
|
605
|
+
}
|
|
606
|
+
wrap.appendChild(list);
|
|
607
|
+
container.appendChild(wrap);
|
|
608
|
+
}
|
|
609
|
+
appendTicketOffer(container, answer) {
|
|
610
|
+
const offer = document.createElement("div");
|
|
611
|
+
offer.className = "acw-offer";
|
|
612
|
+
const text = document.createElement("div");
|
|
613
|
+
text.className = "acw-offer-text";
|
|
614
|
+
text.textContent = answer.supportTicketPrompt?.trim() || TICKET_OFFER_TEXT;
|
|
615
|
+
offer.appendChild(text);
|
|
616
|
+
const actions = document.createElement("div");
|
|
617
|
+
actions.className = "acw-offer-actions";
|
|
618
|
+
const btn = document.createElement("button");
|
|
619
|
+
btn.type = "button";
|
|
620
|
+
btn.className = "acw-chip";
|
|
621
|
+
btn.textContent = "Raise a support ticket";
|
|
622
|
+
btn.addEventListener("click", () => {
|
|
623
|
+
btn.disabled = true;
|
|
624
|
+
void this.openTicketDraft(answer).finally(() => {
|
|
625
|
+
btn.disabled = false;
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
actions.appendChild(btn);
|
|
629
|
+
this.appendOpenSupport(actions);
|
|
630
|
+
offer.appendChild(actions);
|
|
631
|
+
container.appendChild(offer);
|
|
632
|
+
}
|
|
633
|
+
/** Adds an "Open support" action when the host supplied `onOpenSupport`. */
|
|
634
|
+
appendOpenSupport(container) {
|
|
635
|
+
if (!this.config.onOpenSupport) return;
|
|
636
|
+
const b = document.createElement("button");
|
|
637
|
+
b.type = "button";
|
|
638
|
+
b.className = "acw-link-btn acw-open-support";
|
|
639
|
+
b.textContent = "Open support";
|
|
640
|
+
b.addEventListener("click", () => this.openSupport());
|
|
641
|
+
container.appendChild(b);
|
|
642
|
+
}
|
|
643
|
+
/** Close the panel first (synchronously), then hand over to the host so the two overlays never stack. */
|
|
644
|
+
openSupport() {
|
|
645
|
+
const cb = this.config.onOpenSupport;
|
|
646
|
+
if (!cb) return;
|
|
647
|
+
this.close();
|
|
648
|
+
cb();
|
|
649
|
+
}
|
|
428
650
|
currentRequestId() {
|
|
429
651
|
const ctx = this.currentContext();
|
|
430
652
|
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
@@ -435,10 +657,45 @@ var CopilotWidget = class {
|
|
|
435
657
|
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 || "");
|
|
436
658
|
return guid ? guid[0] : void 0;
|
|
437
659
|
}
|
|
438
|
-
/**
|
|
439
|
-
openTicketDraft(answer) {
|
|
660
|
+
/** Fetch the server-built draft, then show it as an editable form; the user triggers Send. */
|
|
661
|
+
async openTicketDraft(answer) {
|
|
440
662
|
const fromAnswer = /MS[A-Z0-9]{6,}/i.exec(answer.primaryReason || "");
|
|
441
663
|
const requestId = fromAnswer ? fromAnswer[0] : this.currentRequestId();
|
|
664
|
+
const epoch = this.epoch;
|
|
665
|
+
this.setProgress("Preparing your support ticket\u2026");
|
|
666
|
+
let draft;
|
|
667
|
+
try {
|
|
668
|
+
const sessionId = await this.ensureSession();
|
|
669
|
+
const qs = requestId ? `?requestId=${encodeURIComponent(requestId)}` : "";
|
|
670
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/support-ticket/draft${qs}`, {
|
|
671
|
+
method: "GET",
|
|
672
|
+
headers: this.headers()
|
|
673
|
+
});
|
|
674
|
+
if (epoch !== this.epoch) return;
|
|
675
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
676
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
677
|
+
this.handleInvalidSession();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (res.ok) {
|
|
681
|
+
const data = await res.json();
|
|
682
|
+
if (epoch !== this.epoch) return;
|
|
683
|
+
if (data && typeof data.subject === "string" && typeof data.description === "string") draft = data;
|
|
684
|
+
}
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
if (epoch !== this.epoch) return;
|
|
688
|
+
this.setProgress("");
|
|
689
|
+
const fallback = this.localDraft(answer, requestId);
|
|
690
|
+
this.showTicketForm(
|
|
691
|
+
draft?.subject || fallback.subject,
|
|
692
|
+
draft?.description || fallback.description,
|
|
693
|
+
draft?.fields?.requestId || requestId,
|
|
694
|
+
answer
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
/** Used only when the draft endpoint is unavailable (older server or a transient error). */
|
|
698
|
+
localDraft(answer, requestId) {
|
|
442
699
|
const role = answer.verifiedRoleLabel || "";
|
|
443
700
|
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
444
701
|
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
@@ -450,39 +707,57 @@ var CopilotWidget = class {
|
|
|
450
707
|
` : "") + `
|
|
451
708
|
What the copilot reported:
|
|
452
709
|
${reason}`;
|
|
710
|
+
return { subject, description };
|
|
711
|
+
}
|
|
712
|
+
showTicketForm(subject, description, requestId, answer) {
|
|
453
713
|
const card = document.createElement("div");
|
|
454
714
|
card.className = "acw-ticket";
|
|
455
|
-
card.innerHTML = `<div class="acw-ticket-title">Raise a support ticket</div><div class="acw-ticket-sub">Review the details below
|
|
456
|
-
card.querySelector(".acw-ticket-subject")
|
|
457
|
-
card.querySelector(".acw-ticket-desc")
|
|
715
|
+
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>`;
|
|
716
|
+
const subjectInput = card.querySelector(".acw-ticket-subject");
|
|
717
|
+
const descInput = card.querySelector(".acw-ticket-desc");
|
|
718
|
+
subjectInput.value = subject;
|
|
719
|
+
descInput.value = description;
|
|
458
720
|
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
459
721
|
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
460
|
-
const subj =
|
|
461
|
-
const desc =
|
|
722
|
+
const subj = subjectInput.value.trim();
|
|
723
|
+
const desc = descInput.value.trim();
|
|
724
|
+
if (!subj) {
|
|
725
|
+
subjectInput.focus();
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!desc) {
|
|
729
|
+
descInput.focus();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
462
732
|
card.remove();
|
|
463
|
-
void this.submitTicket(subj, desc, requestId);
|
|
733
|
+
void this.submitTicket(subj, desc, requestId, answer.primaryReason);
|
|
464
734
|
});
|
|
465
735
|
this.body.appendChild(card);
|
|
466
736
|
this.body.scrollTop = this.body.scrollHeight;
|
|
737
|
+
subjectInput.focus();
|
|
467
738
|
}
|
|
468
|
-
async submitTicket(subject, description, requestId) {
|
|
739
|
+
async submitTicket(subject, description, requestId, reason) {
|
|
469
740
|
this.setProgress("Sending your support ticket\u2026");
|
|
741
|
+
const epoch = this.epoch;
|
|
470
742
|
try {
|
|
471
743
|
const sessionId = await this.ensureSession();
|
|
472
|
-
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/support-ticket`, {
|
|
744
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/support-ticket`, {
|
|
473
745
|
method: "POST",
|
|
474
746
|
headers: this.headers(),
|
|
475
747
|
body: JSON.stringify({
|
|
476
748
|
tenantUniqueName: this.config.tenantUniqueName,
|
|
477
|
-
environment: this.config.environment,
|
|
478
749
|
requestId,
|
|
479
750
|
subject,
|
|
480
|
-
description
|
|
751
|
+
description,
|
|
752
|
+
reason,
|
|
753
|
+
question: this.lastQuestion || void 0
|
|
481
754
|
})
|
|
482
755
|
});
|
|
756
|
+
if (epoch !== this.epoch) return;
|
|
483
757
|
this.setProgress("");
|
|
484
|
-
if (res.status
|
|
485
|
-
this.
|
|
758
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
759
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
760
|
+
this.handleInvalidSession();
|
|
486
761
|
return;
|
|
487
762
|
}
|
|
488
763
|
if (!res.ok) {
|
|
@@ -490,11 +765,16 @@ ${reason}`;
|
|
|
490
765
|
return;
|
|
491
766
|
}
|
|
492
767
|
const data = await res.json();
|
|
768
|
+
if (epoch !== this.epoch) return;
|
|
493
769
|
if (data?.status === "Created") {
|
|
494
|
-
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
495
770
|
const el = document.createElement("div");
|
|
496
771
|
el.className = "acw-answer acw-okstate";
|
|
497
|
-
|
|
772
|
+
const msg = document.createElement("div");
|
|
773
|
+
msg.className = "acw-reason";
|
|
774
|
+
msg.textContent = data.message?.trim() || (data.ticketNumber ? `Your support ticket ${data.ticketNumber} has been created.` : "Your support ticket has been created.");
|
|
775
|
+
el.appendChild(msg);
|
|
776
|
+
const actions = document.createElement("div");
|
|
777
|
+
actions.className = "acw-offer-actions";
|
|
498
778
|
if (data.webUrl || data.id || this.config.onOpenTicket) {
|
|
499
779
|
const open = document.createElement("button");
|
|
500
780
|
open.type = "button";
|
|
@@ -503,19 +783,24 @@ ${reason}`;
|
|
|
503
783
|
open.addEventListener("click", () => {
|
|
504
784
|
const ticket = { id: data.id, number: data.ticketNumber, webUrl: data.webUrl };
|
|
505
785
|
if (this.config.onOpenTicket) {
|
|
786
|
+
this.close();
|
|
506
787
|
this.config.onOpenTicket(ticket);
|
|
507
|
-
} else
|
|
508
|
-
|
|
788
|
+
} else {
|
|
789
|
+
const href = safeHref(data.webUrl);
|
|
790
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
509
791
|
}
|
|
510
792
|
});
|
|
511
|
-
|
|
793
|
+
actions.appendChild(open);
|
|
512
794
|
}
|
|
795
|
+
this.appendOpenSupport(actions);
|
|
796
|
+
if (actions.childElementCount > 0) el.appendChild(actions);
|
|
513
797
|
this.body.appendChild(el);
|
|
514
798
|
this.body.scrollTop = this.body.scrollHeight;
|
|
515
799
|
} else {
|
|
516
800
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
517
801
|
}
|
|
518
802
|
} catch {
|
|
803
|
+
if (epoch !== this.epoch) return;
|
|
519
804
|
this.setProgress("");
|
|
520
805
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
521
806
|
}
|
|
@@ -524,21 +809,62 @@ ${reason}`;
|
|
|
524
809
|
headers() {
|
|
525
810
|
const h = { "Content-Type": "application/json" };
|
|
526
811
|
const token = this.config.getAuthToken?.();
|
|
527
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
528
812
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
529
|
-
else if (email) h["X-User-Email"] = email;
|
|
530
813
|
return h;
|
|
531
814
|
}
|
|
815
|
+
/** Reset the session when the tenant or the token's user no longer match the ones it was opened for. */
|
|
816
|
+
syncSessionScope() {
|
|
817
|
+
if (!this.config || !this.sessionId) return;
|
|
818
|
+
const tenantChanged = this.sessionTenant !== this.config.tenantUniqueName;
|
|
819
|
+
const identity = tokenIdentity(this.config.getAuthToken?.());
|
|
820
|
+
const userChanged = !!identity && !!this.sessionIdentity && identity !== this.sessionIdentity;
|
|
821
|
+
if (tenantChanged || userChanged) this.resetSession();
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* B5 contract check (R1): a 403 {status:"FeatureDisabled"} means a capability is switched off, not that the session is
|
|
825
|
+
* invalid - keep the session and show the server's reason instead of resetting.
|
|
826
|
+
*/
|
|
827
|
+
async renderIfFeatureDisabled(res) {
|
|
828
|
+
if (res.status !== 403) return false;
|
|
829
|
+
try {
|
|
830
|
+
const body = await res.clone().json();
|
|
831
|
+
if (body?.status !== "FeatureDisabled") return false;
|
|
832
|
+
this.setProgress("");
|
|
833
|
+
this.renderAnswer({ status: "FeatureDisabled", primaryReason: typeof body.reason === "string" ? body.reason : "That part of the Copilot is not enabled yet." });
|
|
834
|
+
return true;
|
|
835
|
+
} catch {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/** 403/404 on a session-scoped call: drop the session and tell the user to ask again. */
|
|
840
|
+
handleInvalidSession() {
|
|
841
|
+
this.setProgress("");
|
|
842
|
+
this.resetSession();
|
|
843
|
+
if (!this.body) return;
|
|
844
|
+
const el = document.createElement("div");
|
|
845
|
+
el.className = "acw-answer";
|
|
846
|
+
el.innerHTML = `<div class="acw-error">${escapeHtml(SESSION_RESET_TEXT)}</div>`;
|
|
847
|
+
this.body.appendChild(el);
|
|
848
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
849
|
+
}
|
|
532
850
|
async ensureSession() {
|
|
851
|
+
this.syncSessionScope();
|
|
533
852
|
if (this.sessionId) return this.sessionId;
|
|
853
|
+
const tenant = this.config.tenantUniqueName;
|
|
854
|
+
const identity = tokenIdentity(this.config.getAuthToken?.());
|
|
855
|
+
const epoch = this.epoch;
|
|
534
856
|
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
535
857
|
method: "POST",
|
|
536
858
|
headers: this.headers(),
|
|
537
|
-
|
|
859
|
+
// The session is bound to the signed-in user and this tenant (verified server-side).
|
|
860
|
+
body: JSON.stringify({ projectKey: this.config.projectKey, tenantUniqueName: tenant })
|
|
538
861
|
});
|
|
539
862
|
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
540
863
|
const session = await res.json();
|
|
864
|
+
if (epoch !== this.epoch) throw new Error("The Copilot session was reset.");
|
|
541
865
|
this.sessionId = session.sessionId;
|
|
866
|
+
this.sessionTenant = tenant;
|
|
867
|
+
this.sessionIdentity = identity;
|
|
542
868
|
if (this.connection && this.sessionId) {
|
|
543
869
|
try {
|
|
544
870
|
await this.connection.invoke("JoinSession", this.sessionId);
|
|
@@ -547,25 +873,37 @@ ${reason}`;
|
|
|
547
873
|
}
|
|
548
874
|
return this.sessionId;
|
|
549
875
|
}
|
|
550
|
-
async runAsk(question) {
|
|
876
|
+
async runAsk(question, notHelpful = false) {
|
|
551
877
|
this.open();
|
|
552
878
|
this.lastQuestion = question;
|
|
553
|
-
this.addMessage(question, "user");
|
|
879
|
+
this.addMessage(notHelpful ? "This didn\u2019t help." : question, "user");
|
|
554
880
|
this.setProgress("Reading your question\u2026");
|
|
881
|
+
const epoch = this.epoch;
|
|
555
882
|
try {
|
|
556
883
|
const sessionId = await this.ensureSession();
|
|
557
|
-
const
|
|
884
|
+
const body = { question, tenantUniqueName: this.config.tenantUniqueName, context: this.currentContext() };
|
|
885
|
+
if (notHelpful) body.notHelpful = true;
|
|
886
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/messages`, {
|
|
558
887
|
method: "POST",
|
|
559
888
|
headers: this.headers(),
|
|
560
|
-
body: JSON.stringify(
|
|
889
|
+
body: JSON.stringify(body)
|
|
561
890
|
});
|
|
891
|
+
if (epoch !== this.epoch) return;
|
|
892
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
893
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
894
|
+
this.handleInvalidSession();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
562
897
|
if (!res.ok) {
|
|
563
898
|
this.setProgress("");
|
|
564
899
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
565
900
|
return;
|
|
566
901
|
}
|
|
567
|
-
|
|
902
|
+
const answer = await res.json();
|
|
903
|
+
if (epoch !== this.epoch) return;
|
|
904
|
+
this.renderAnswer(answer, question, notHelpful);
|
|
568
905
|
} catch (err) {
|
|
906
|
+
if (epoch !== this.epoch) return;
|
|
569
907
|
this.setProgress("");
|
|
570
908
|
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
571
909
|
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
@@ -574,7 +912,7 @@ ${reason}`;
|
|
|
574
912
|
async connectSignalR() {
|
|
575
913
|
if (!this.config.signalRHubUrl) return;
|
|
576
914
|
const signalR = await import("@microsoft/signalr");
|
|
577
|
-
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl).withAutomaticReconnect().build();
|
|
915
|
+
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl, { accessTokenFactory: () => this.config.getAuthToken?.() ?? "" }).withAutomaticReconnect().build();
|
|
578
916
|
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
579
917
|
connection.on(event, () => {
|
|
580
918
|
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
@@ -584,9 +922,6 @@ ${reason}`;
|
|
|
584
922
|
this.connection = connection;
|
|
585
923
|
}
|
|
586
924
|
};
|
|
587
|
-
function escapeHtml(s) {
|
|
588
|
-
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
589
|
-
}
|
|
590
925
|
|
|
591
926
|
// src/index.ts
|
|
592
927
|
var instance = new CopilotWidget();
|
|
@@ -600,7 +935,9 @@ var ApplicationCopilotWidget = {
|
|
|
600
935
|
promptUpload: () => instance.promptUpload(),
|
|
601
936
|
ask: (question) => instance.ask(question),
|
|
602
937
|
setContext: (context) => instance.setContext(context),
|
|
603
|
-
captureError: (errorContext) => instance.captureError(errorContext)
|
|
938
|
+
captureError: (errorContext) => instance.captureError(errorContext),
|
|
939
|
+
/** Drop the current Copilot session and conversation (e.g. after a tenant switch or sign-out). */
|
|
940
|
+
resetSession: () => instance.resetSession()
|
|
604
941
|
};
|
|
605
942
|
if (typeof window !== "undefined") {
|
|
606
943
|
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|