@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.js
CHANGED
|
@@ -40,7 +40,7 @@ var widgetStyles = `
|
|
|
40
40
|
/* Panel */
|
|
41
41
|
.acw-panel {
|
|
42
42
|
position: fixed; right: 24px; bottom: 96px; z-index: 2147483000;
|
|
43
|
-
width:
|
|
43
|
+
width: 480px; max-width: calc(100vw - 48px); height: 660px; max-height: calc(100vh - 120px);
|
|
44
44
|
display: none; flex-direction: column;
|
|
45
45
|
background: var(--acw-bg); color: var(--acw-fg);
|
|
46
46
|
border: 1px solid var(--acw-border); border-radius: 16px; overflow: hidden;
|
|
@@ -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,28 +79,140 @@ 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
|
}
|
|
98
116
|
.acw-input:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
99
117
|
.acw-send { border: none; border-radius: 10px; padding: 0 16px; background: var(--acw-accent); color: #fff; cursor: pointer; font-size: 14px; font-family: inherit; }
|
|
100
118
|
.acw-send:hover { background: var(--acw-accent-dark); }
|
|
119
|
+
|
|
120
|
+
/* Support-ticket draft card */
|
|
121
|
+
.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); }
|
|
122
|
+
.acw-ticket-title { font-size: 15px; font-weight: 600; color: var(--acw-fg); margin: 0 0 4px; }
|
|
123
|
+
.acw-ticket-sub { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
124
|
+
.acw-ticket-label { display: block; font-size: 12px; font-weight: 600; color: var(--acw-muted); margin: 12px 0 5px; }
|
|
125
|
+
.acw-ticket-input, .acw-ticket-desc {
|
|
126
|
+
width: 100%; box-sizing: border-box; font-family: inherit; font-size: 13px;
|
|
127
|
+
padding: 10px 12px; border: 1px solid var(--acw-border); border-radius: 10px;
|
|
128
|
+
color: var(--acw-fg); background: var(--acw-bg);
|
|
129
|
+
}
|
|
130
|
+
.acw-ticket-desc { resize: vertical; min-height: 170px; line-height: 1.5; }
|
|
131
|
+
.acw-ticket-input:focus, .acw-ticket-desc:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
132
|
+
.acw-ticket-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
|
|
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; }
|
|
134
|
+
.acw-btn-primary:hover { background: var(--acw-accent-dark); }
|
|
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; }
|
|
136
|
+
.acw-btn-ghost:hover { background: #f0eefe; }
|
|
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; }
|
|
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
|
+
}
|
|
101
156
|
`;
|
|
102
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
|
+
|
|
103
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.";
|
|
104
216
|
var PROGRESS_LABELS = {
|
|
105
217
|
QuestionAccepted: "Reading your question\u2026",
|
|
106
218
|
PermissionsVerified: "Verifying your permissions\u2026",
|
|
@@ -111,18 +223,50 @@ var CopilotWidget = class {
|
|
|
111
223
|
constructor() {
|
|
112
224
|
this.lastQuestion = "";
|
|
113
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;
|
|
114
229
|
}
|
|
115
230
|
init(config) {
|
|
231
|
+
const previousTenant = this.config?.tenantUniqueName;
|
|
116
232
|
this.config = config;
|
|
117
233
|
try {
|
|
118
234
|
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
119
235
|
} catch {
|
|
120
236
|
}
|
|
237
|
+
if (previousTenant !== void 0 && previousTenant !== config.tenantUniqueName) this.resetSession();
|
|
121
238
|
this.mount();
|
|
122
|
-
this.
|
|
123
|
-
|
|
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
|
+
}
|
|
124
267
|
}
|
|
125
268
|
open() {
|
|
269
|
+
this.syncSessionScope();
|
|
126
270
|
this.panel?.classList.add("acw-open");
|
|
127
271
|
this.refreshChips();
|
|
128
272
|
}
|
|
@@ -167,6 +311,7 @@ var CopilotWidget = class {
|
|
|
167
311
|
this.body.scrollTop = this.body.scrollHeight;
|
|
168
312
|
}
|
|
169
313
|
ask(question) {
|
|
314
|
+
this.syncSessionScope();
|
|
170
315
|
if (this.mode === "intake") {
|
|
171
316
|
void this.runIntakeAsk(question);
|
|
172
317
|
} else {
|
|
@@ -184,7 +329,7 @@ var CopilotWidget = class {
|
|
|
184
329
|
style.textContent = widgetStyles;
|
|
185
330
|
this.root.appendChild(style);
|
|
186
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>';
|
|
187
|
-
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
332
|
+
const tip = escapeHtml(this.config.buttonText ?? "Ask Application Copilot");
|
|
188
333
|
const wrap = document.createElement("div");
|
|
189
334
|
wrap.className = "acw-launch-wrap";
|
|
190
335
|
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
@@ -197,7 +342,7 @@ var CopilotWidget = class {
|
|
|
197
342
|
<div class="acw-header">
|
|
198
343
|
${sparkle}
|
|
199
344
|
<span class="acw-title">Application Copilot</span>
|
|
200
|
-
<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>
|
|
201
346
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
202
347
|
</div>
|
|
203
348
|
<div class="acw-body"></div>
|
|
@@ -225,10 +370,21 @@ var CopilotWidget = class {
|
|
|
225
370
|
if (e.key === "Enter") this.submitFromInput();
|
|
226
371
|
});
|
|
227
372
|
}
|
|
228
|
-
/**
|
|
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
|
+
*/
|
|
229
377
|
promptUpload() {
|
|
230
378
|
this.open();
|
|
231
|
-
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";
|
|
232
388
|
}
|
|
233
389
|
async uploadDocument(file) {
|
|
234
390
|
this.open();
|
|
@@ -238,16 +394,14 @@ var CopilotWidget = class {
|
|
|
238
394
|
const form = new FormData();
|
|
239
395
|
form.append("file", file, file.name);
|
|
240
396
|
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
241
|
-
form.append("environment", this.config.environment);
|
|
242
397
|
const h = {};
|
|
243
398
|
const token = this.config.getAuthToken?.();
|
|
244
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
245
399
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
246
|
-
else if (email) h["X-User-Email"] = email;
|
|
247
400
|
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
248
401
|
this.setProgress("");
|
|
249
402
|
if (res.status === 403) {
|
|
250
|
-
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." });
|
|
251
405
|
return;
|
|
252
406
|
}
|
|
253
407
|
if (!res.ok) {
|
|
@@ -256,7 +410,7 @@ var CopilotWidget = class {
|
|
|
256
410
|
}
|
|
257
411
|
const data = await res.json();
|
|
258
412
|
if (data?.status === "Indexed") {
|
|
259
|
-
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.` });
|
|
260
414
|
} else {
|
|
261
415
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
262
416
|
}
|
|
@@ -305,20 +459,30 @@ var CopilotWidget = class {
|
|
|
305
459
|
this.open();
|
|
306
460
|
this.addMessage(question, "user");
|
|
307
461
|
this.setProgress("Finding the right offering\u2026");
|
|
462
|
+
const epoch = this.epoch;
|
|
308
463
|
try {
|
|
309
464
|
const sessionId = await this.ensureSession();
|
|
310
|
-
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`, {
|
|
311
466
|
method: "POST",
|
|
312
467
|
headers: this.headers(),
|
|
313
468
|
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
314
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
|
+
}
|
|
315
476
|
if (!res.ok) {
|
|
316
477
|
this.setProgress("");
|
|
317
478
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
318
479
|
return;
|
|
319
480
|
}
|
|
320
|
-
|
|
481
|
+
const data = await res.json();
|
|
482
|
+
if (epoch !== this.epoch) return;
|
|
483
|
+
this.renderIntakeAnswer(data);
|
|
321
484
|
} catch {
|
|
485
|
+
if (epoch !== this.epoch) return;
|
|
322
486
|
this.setProgress("");
|
|
323
487
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
324
488
|
}
|
|
@@ -377,10 +541,10 @@ var CopilotWidget = class {
|
|
|
377
541
|
p.textContent = text;
|
|
378
542
|
this.body.scrollTop = this.body.scrollHeight;
|
|
379
543
|
}
|
|
380
|
-
renderAnswer(answer) {
|
|
544
|
+
renderAnswer(answer, question, wasNotHelpful = false) {
|
|
381
545
|
this.setProgress("");
|
|
382
546
|
const el = document.createElement("div");
|
|
383
|
-
const isFailure = answer.status && answer.status !== "Answered";
|
|
547
|
+
const isFailure = !!answer.status && answer.status !== "Answered";
|
|
384
548
|
if (isFailure) {
|
|
385
549
|
el.className = "acw-answer";
|
|
386
550
|
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
@@ -390,32 +554,148 @@ var CopilotWidget = class {
|
|
|
390
554
|
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
391
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>` : "");
|
|
392
556
|
}
|
|
557
|
+
this.appendReferences(el, answer);
|
|
393
558
|
this.body.appendChild(el);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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);
|
|
405
580
|
}
|
|
406
581
|
this.body.scrollTop = this.body.scrollHeight;
|
|
407
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
|
+
}
|
|
408
650
|
currentRequestId() {
|
|
409
651
|
const ctx = this.currentContext();
|
|
410
652
|
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
411
653
|
if (explicit) return String(explicit);
|
|
412
654
|
const route = ctx.page?.route || (typeof window !== "undefined" ? window.location.pathname : "");
|
|
413
|
-
const
|
|
414
|
-
|
|
655
|
+
const ms = /MS[A-Z0-9]{6,}/i.exec(route || "");
|
|
656
|
+
if (ms) return ms[0];
|
|
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 || "");
|
|
658
|
+
return guid ? guid[0] : void 0;
|
|
659
|
+
}
|
|
660
|
+
/** Fetch the server-built draft, then show it as an editable form; the user triggers Send. */
|
|
661
|
+
async openTicketDraft(answer) {
|
|
662
|
+
const fromAnswer = /MS[A-Z0-9]{6,}/i.exec(answer.primaryReason || "");
|
|
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
|
+
);
|
|
415
696
|
}
|
|
416
|
-
/**
|
|
417
|
-
|
|
418
|
-
const requestId = this.currentRequestId();
|
|
697
|
+
/** Used only when the draft endpoint is unavailable (older server or a transient error). */
|
|
698
|
+
localDraft(answer, requestId) {
|
|
419
699
|
const role = answer.verifiedRoleLabel || "";
|
|
420
700
|
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
421
701
|
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
@@ -427,41 +707,57 @@ var CopilotWidget = class {
|
|
|
427
707
|
` : "") + `
|
|
428
708
|
What the copilot reported:
|
|
429
709
|
${reason}`;
|
|
710
|
+
return { subject, description };
|
|
711
|
+
}
|
|
712
|
+
showTicketForm(subject, description, requestId, answer) {
|
|
430
713
|
const card = document.createElement("div");
|
|
431
|
-
card.className = "acw-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
714
|
+
card.className = "acw-ticket";
|
|
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;
|
|
437
720
|
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
438
721
|
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
439
|
-
const subj =
|
|
440
|
-
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
|
+
}
|
|
441
732
|
card.remove();
|
|
442
|
-
void this.submitTicket(subj, desc, requestId);
|
|
733
|
+
void this.submitTicket(subj, desc, requestId, answer.primaryReason);
|
|
443
734
|
});
|
|
444
735
|
this.body.appendChild(card);
|
|
445
736
|
this.body.scrollTop = this.body.scrollHeight;
|
|
737
|
+
subjectInput.focus();
|
|
446
738
|
}
|
|
447
|
-
async submitTicket(subject, description, requestId) {
|
|
739
|
+
async submitTicket(subject, description, requestId, reason) {
|
|
448
740
|
this.setProgress("Sending your support ticket\u2026");
|
|
741
|
+
const epoch = this.epoch;
|
|
449
742
|
try {
|
|
450
743
|
const sessionId = await this.ensureSession();
|
|
451
|
-
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`, {
|
|
452
745
|
method: "POST",
|
|
453
746
|
headers: this.headers(),
|
|
454
747
|
body: JSON.stringify({
|
|
455
748
|
tenantUniqueName: this.config.tenantUniqueName,
|
|
456
|
-
environment: this.config.environment,
|
|
457
749
|
requestId,
|
|
458
750
|
subject,
|
|
459
|
-
description
|
|
751
|
+
description,
|
|
752
|
+
reason,
|
|
753
|
+
question: this.lastQuestion || void 0
|
|
460
754
|
})
|
|
461
755
|
});
|
|
756
|
+
if (epoch !== this.epoch) return;
|
|
462
757
|
this.setProgress("");
|
|
463
|
-
if (res.status
|
|
464
|
-
this.
|
|
758
|
+
if (isSessionInvalidStatus(res.status)) {
|
|
759
|
+
if (await this.renderIfFeatureDisabled(res)) return;
|
|
760
|
+
this.handleInvalidSession();
|
|
465
761
|
return;
|
|
466
762
|
}
|
|
467
763
|
if (!res.ok) {
|
|
@@ -469,17 +765,42 @@ ${reason}`;
|
|
|
469
765
|
return;
|
|
470
766
|
}
|
|
471
767
|
const data = await res.json();
|
|
768
|
+
if (epoch !== this.epoch) return;
|
|
472
769
|
if (data?.status === "Created") {
|
|
473
|
-
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
474
770
|
const el = document.createElement("div");
|
|
475
771
|
el.className = "acw-answer acw-okstate";
|
|
476
|
-
|
|
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";
|
|
778
|
+
if (data.webUrl || data.id || this.config.onOpenTicket) {
|
|
779
|
+
const open = document.createElement("button");
|
|
780
|
+
open.type = "button";
|
|
781
|
+
open.className = "acw-link-btn";
|
|
782
|
+
open.textContent = "Open the ticket";
|
|
783
|
+
open.addEventListener("click", () => {
|
|
784
|
+
const ticket = { id: data.id, number: data.ticketNumber, webUrl: data.webUrl };
|
|
785
|
+
if (this.config.onOpenTicket) {
|
|
786
|
+
this.close();
|
|
787
|
+
this.config.onOpenTicket(ticket);
|
|
788
|
+
} else {
|
|
789
|
+
const href = safeHref(data.webUrl);
|
|
790
|
+
if (href) window.open(href, "_blank", "noopener,noreferrer");
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
actions.appendChild(open);
|
|
794
|
+
}
|
|
795
|
+
this.appendOpenSupport(actions);
|
|
796
|
+
if (actions.childElementCount > 0) el.appendChild(actions);
|
|
477
797
|
this.body.appendChild(el);
|
|
478
798
|
this.body.scrollTop = this.body.scrollHeight;
|
|
479
799
|
} else {
|
|
480
800
|
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
481
801
|
}
|
|
482
802
|
} catch {
|
|
803
|
+
if (epoch !== this.epoch) return;
|
|
483
804
|
this.setProgress("");
|
|
484
805
|
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
485
806
|
}
|
|
@@ -488,21 +809,62 @@ ${reason}`;
|
|
|
488
809
|
headers() {
|
|
489
810
|
const h = { "Content-Type": "application/json" };
|
|
490
811
|
const token = this.config.getAuthToken?.();
|
|
491
|
-
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
492
812
|
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
493
|
-
else if (email) h["X-User-Email"] = email;
|
|
494
813
|
return h;
|
|
495
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
|
+
}
|
|
496
850
|
async ensureSession() {
|
|
851
|
+
this.syncSessionScope();
|
|
497
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;
|
|
498
856
|
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
499
857
|
method: "POST",
|
|
500
858
|
headers: this.headers(),
|
|
501
|
-
|
|
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 })
|
|
502
861
|
});
|
|
503
862
|
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
504
863
|
const session = await res.json();
|
|
864
|
+
if (epoch !== this.epoch) throw new Error("The Copilot session was reset.");
|
|
505
865
|
this.sessionId = session.sessionId;
|
|
866
|
+
this.sessionTenant = tenant;
|
|
867
|
+
this.sessionIdentity = identity;
|
|
506
868
|
if (this.connection && this.sessionId) {
|
|
507
869
|
try {
|
|
508
870
|
await this.connection.invoke("JoinSession", this.sessionId);
|
|
@@ -511,25 +873,37 @@ ${reason}`;
|
|
|
511
873
|
}
|
|
512
874
|
return this.sessionId;
|
|
513
875
|
}
|
|
514
|
-
async runAsk(question) {
|
|
876
|
+
async runAsk(question, notHelpful = false) {
|
|
515
877
|
this.open();
|
|
516
878
|
this.lastQuestion = question;
|
|
517
|
-
this.addMessage(question, "user");
|
|
879
|
+
this.addMessage(notHelpful ? "This didn\u2019t help." : question, "user");
|
|
518
880
|
this.setProgress("Reading your question\u2026");
|
|
881
|
+
const epoch = this.epoch;
|
|
519
882
|
try {
|
|
520
883
|
const sessionId = await this.ensureSession();
|
|
521
|
-
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`, {
|
|
522
887
|
method: "POST",
|
|
523
888
|
headers: this.headers(),
|
|
524
|
-
body: JSON.stringify(
|
|
889
|
+
body: JSON.stringify(body)
|
|
525
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
|
+
}
|
|
526
897
|
if (!res.ok) {
|
|
527
898
|
this.setProgress("");
|
|
528
899
|
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
529
900
|
return;
|
|
530
901
|
}
|
|
531
|
-
|
|
902
|
+
const answer = await res.json();
|
|
903
|
+
if (epoch !== this.epoch) return;
|
|
904
|
+
this.renderAnswer(answer, question, notHelpful);
|
|
532
905
|
} catch (err) {
|
|
906
|
+
if (epoch !== this.epoch) return;
|
|
533
907
|
this.setProgress("");
|
|
534
908
|
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
535
909
|
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
@@ -538,7 +912,7 @@ ${reason}`;
|
|
|
538
912
|
async connectSignalR() {
|
|
539
913
|
if (!this.config.signalRHubUrl) return;
|
|
540
914
|
const signalR = await import("@microsoft/signalr");
|
|
541
|
-
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();
|
|
542
916
|
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
543
917
|
connection.on(event, () => {
|
|
544
918
|
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
@@ -548,9 +922,6 @@ ${reason}`;
|
|
|
548
922
|
this.connection = connection;
|
|
549
923
|
}
|
|
550
924
|
};
|
|
551
|
-
function escapeHtml(s) {
|
|
552
|
-
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
553
|
-
}
|
|
554
925
|
|
|
555
926
|
// src/index.ts
|
|
556
927
|
var instance = new CopilotWidget();
|
|
@@ -564,7 +935,9 @@ var ApplicationCopilotWidget = {
|
|
|
564
935
|
promptUpload: () => instance.promptUpload(),
|
|
565
936
|
ask: (question) => instance.ask(question),
|
|
566
937
|
setContext: (context) => instance.setContext(context),
|
|
567
|
-
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()
|
|
568
941
|
};
|
|
569
942
|
if (typeof window !== "undefined") {
|
|
570
943
|
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|