@spektra-cloudevents/application-copilot-widget 1.0.0 → 1.1.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/dist/index.cjs +186 -2
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +186 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -146,10 +146,15 @@ var PROGRESS_LABELS = {
|
|
|
146
146
|
};
|
|
147
147
|
var CopilotWidget = class {
|
|
148
148
|
constructor() {
|
|
149
|
+
this.lastQuestion = "";
|
|
149
150
|
this.mode = "explain";
|
|
150
151
|
}
|
|
151
152
|
init(config) {
|
|
152
153
|
this.config = config;
|
|
154
|
+
try {
|
|
155
|
+
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
153
158
|
this.mount();
|
|
154
159
|
this.connectSignalR().catch(() => {
|
|
155
160
|
});
|
|
@@ -161,6 +166,17 @@ var CopilotWidget = class {
|
|
|
161
166
|
close() {
|
|
162
167
|
this.panel?.classList.remove("acw-open");
|
|
163
168
|
}
|
|
169
|
+
/** Show the launcher (widget host). No-op before init(). */
|
|
170
|
+
show() {
|
|
171
|
+
if (this.host) this.host.style.display = "";
|
|
172
|
+
}
|
|
173
|
+
/** Hide the launcher and close the panel. Used to gate the widget by route. */
|
|
174
|
+
hide() {
|
|
175
|
+
if (this.host) {
|
|
176
|
+
this.host.style.display = "none";
|
|
177
|
+
}
|
|
178
|
+
this.close();
|
|
179
|
+
}
|
|
164
180
|
setContext(context) {
|
|
165
181
|
this.contextOverride = context;
|
|
166
182
|
}
|
|
@@ -168,6 +184,25 @@ var CopilotWidget = class {
|
|
|
168
184
|
this.recentError = errorContext;
|
|
169
185
|
this.open();
|
|
170
186
|
}
|
|
187
|
+
/** Proactive nudge: show + open the panel with a message and an optional one-click suggested question. */
|
|
188
|
+
notify(text, suggestion) {
|
|
189
|
+
this.show();
|
|
190
|
+
this.open();
|
|
191
|
+
if (!this.body) return;
|
|
192
|
+
const el = document.createElement("div");
|
|
193
|
+
el.className = "acw-msg acw-assistant acw-nudge";
|
|
194
|
+
el.innerHTML = `<div class="acw-reason">${escapeHtml(text)}</div>`;
|
|
195
|
+
if (suggestion) {
|
|
196
|
+
const btn = document.createElement("button");
|
|
197
|
+
btn.type = "button";
|
|
198
|
+
btn.className = "acw-chip";
|
|
199
|
+
btn.textContent = suggestion;
|
|
200
|
+
btn.addEventListener("click", () => this.ask(suggestion));
|
|
201
|
+
el.appendChild(btn);
|
|
202
|
+
}
|
|
203
|
+
this.body.appendChild(el);
|
|
204
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
205
|
+
}
|
|
171
206
|
ask(question) {
|
|
172
207
|
if (this.mode === "intake") {
|
|
173
208
|
void this.runIntakeAsk(question);
|
|
@@ -194,27 +229,79 @@ var CopilotWidget = class {
|
|
|
194
229
|
wrap.querySelector(".acw-launch").addEventListener("click", () => this.panel?.classList.contains("acw-open") ? this.close() : this.open());
|
|
195
230
|
const panel = document.createElement("div");
|
|
196
231
|
panel.className = "acw-panel";
|
|
232
|
+
const uploadIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
|
|
197
233
|
panel.innerHTML = `
|
|
198
234
|
<div class="acw-header">
|
|
199
235
|
${sparkle}
|
|
200
236
|
<span class="acw-title">Application Copilot</span>
|
|
237
|
+
<button class="acw-kb" type="button" aria-label="Add a document to the knowledge base" title="Add to knowledge base (admins)" style="background:none;border:none;color:inherit;cursor:pointer;padding:2px 6px;display:inline-flex;align-items:center;">${uploadIcon}</button>
|
|
201
238
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
202
239
|
</div>
|
|
203
240
|
<div class="acw-body"></div>
|
|
204
241
|
<div class="acw-footer">
|
|
205
242
|
<input class="acw-input" type="text" placeholder="Ask a question\u2026" />
|
|
206
243
|
<button class="acw-send" type="button">Send</button>
|
|
207
|
-
</div
|
|
244
|
+
</div>
|
|
245
|
+
<input class="acw-file" type="file" accept=".pdf,.docx,.xlsx,.xlsm,.txt,.md,.markdown,.csv" style="display:none" />`;
|
|
208
246
|
this.root.appendChild(panel);
|
|
209
247
|
this.panel = panel;
|
|
210
248
|
this.body = panel.querySelector(".acw-body");
|
|
211
249
|
this.input = panel.querySelector(".acw-input");
|
|
250
|
+
this.fileInput = panel.querySelector(".acw-file");
|
|
212
251
|
panel.querySelector(".acw-close").addEventListener("click", () => this.close());
|
|
213
252
|
panel.querySelector(".acw-send").addEventListener("click", () => this.submitFromInput());
|
|
253
|
+
panel.querySelector(".acw-kb").addEventListener("click", () => this.promptUpload());
|
|
254
|
+
this.fileInput.addEventListener("change", () => {
|
|
255
|
+
const f = this.fileInput?.files?.[0];
|
|
256
|
+
if (f) {
|
|
257
|
+
void this.uploadDocument(f);
|
|
258
|
+
if (this.fileInput) this.fileInput.value = "";
|
|
259
|
+
}
|
|
260
|
+
});
|
|
214
261
|
this.input.addEventListener("keydown", (e) => {
|
|
215
262
|
if (e.key === "Enter") this.submitFromInput();
|
|
216
263
|
});
|
|
217
264
|
}
|
|
265
|
+
/** Open the file picker to add a document to the knowledge base (server enforces admin-only). */
|
|
266
|
+
promptUpload() {
|
|
267
|
+
this.open();
|
|
268
|
+
this.fileInput?.click();
|
|
269
|
+
}
|
|
270
|
+
async uploadDocument(file) {
|
|
271
|
+
this.open();
|
|
272
|
+
this.addMessage(`Uploading \u201C${file.name}\u201D to the knowledge base\u2026`, "user");
|
|
273
|
+
this.setProgress("Extracting and indexing\u2026");
|
|
274
|
+
try {
|
|
275
|
+
const form = new FormData();
|
|
276
|
+
form.append("file", file, file.name);
|
|
277
|
+
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
278
|
+
form.append("environment", this.config.environment);
|
|
279
|
+
const h = {};
|
|
280
|
+
const token = this.config.getAuthToken?.();
|
|
281
|
+
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
282
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
283
|
+
else if (email) h["X-User-Email"] = email;
|
|
284
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
285
|
+
this.setProgress("");
|
|
286
|
+
if (res.status === 403) {
|
|
287
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Only Spektra Admins can add to the knowledge base." });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `Upload failed (${res.status}).` });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const data = await res.json();
|
|
295
|
+
if (data?.status === "Indexed") {
|
|
296
|
+
this.renderAnswer({ status: "Answered", disabled: false, primaryReason: `Added \u201C${data.title}\u201D to the knowledge base (${data.chunks} chunk${data.chunks === 1 ? "" : "s"}). I can now use it to answer questions.` });
|
|
297
|
+
} else {
|
|
298
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
this.setProgress("");
|
|
302
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
218
305
|
submitFromInput() {
|
|
219
306
|
const q = this.input?.value.trim();
|
|
220
307
|
if (q) {
|
|
@@ -341,8 +428,99 @@ var CopilotWidget = class {
|
|
|
341
428
|
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>` : "");
|
|
342
429
|
}
|
|
343
430
|
this.body.appendChild(el);
|
|
431
|
+
const role = (answer.verifiedRoleLabel || "").toLowerCase();
|
|
432
|
+
const eligibleRole = role === "requestor" || role === "approver";
|
|
433
|
+
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 || "");
|
|
434
|
+
const offerTicket = answer.supportTicketEligible === true || (answer.disabled === true || looksBlocked) && eligibleRole;
|
|
435
|
+
if (!isFailure && offerTicket) {
|
|
436
|
+
const btn = document.createElement("button");
|
|
437
|
+
btn.type = "button";
|
|
438
|
+
btn.className = "acw-chip";
|
|
439
|
+
btn.textContent = "Raise a support ticket";
|
|
440
|
+
btn.addEventListener("click", () => this.openTicketDraft(answer));
|
|
441
|
+
el.appendChild(btn);
|
|
442
|
+
}
|
|
344
443
|
this.body.scrollTop = this.body.scrollHeight;
|
|
345
444
|
}
|
|
445
|
+
currentRequestId() {
|
|
446
|
+
const ctx = this.currentContext();
|
|
447
|
+
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
448
|
+
if (explicit) return String(explicit);
|
|
449
|
+
const route = ctx.page?.route || (typeof window !== "undefined" ? window.location.pathname : "");
|
|
450
|
+
const m = /MS[A-Z0-9]{6,}/i.exec(route || "");
|
|
451
|
+
return m ? m[0] : void 0;
|
|
452
|
+
}
|
|
453
|
+
/** Build a pre-filled, editable ticket draft and let the user review + Send (they trigger the API call). */
|
|
454
|
+
openTicketDraft(answer) {
|
|
455
|
+
const requestId = this.currentRequestId();
|
|
456
|
+
const role = answer.verifiedRoleLabel || "";
|
|
457
|
+
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
458
|
+
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
459
|
+
const description = `Raised from the Application Copilot.
|
|
460
|
+
|
|
461
|
+
` + (role ? `Role: ${role}
|
|
462
|
+
` : "") + `Request Id: ${requestId || "(not on a specific request)"}
|
|
463
|
+
` + (this.lastQuestion ? `Question: ${this.lastQuestion}
|
|
464
|
+
` : "") + `
|
|
465
|
+
What the copilot reported:
|
|
466
|
+
${reason}`;
|
|
467
|
+
const card = document.createElement("div");
|
|
468
|
+
card.className = "acw-answer";
|
|
469
|
+
const lbl = "display:block;font-size:11px;color:var(--acw-muted);margin:8px 0 2px;";
|
|
470
|
+
const fld = "width:100%;box-sizing:border-box;font:inherit;font-size:13px;padding:6px 8px;border:1px solid var(--acw-border);border-radius:8px;background:var(--acw-bg);color:var(--acw-fg);";
|
|
471
|
+
card.innerHTML = `<div class="acw-reason"><strong>Review your support ticket</strong></div><label style="${lbl}">Subject</label><input class="acw-ticket-subject" type="text" style="${fld}" /><label style="${lbl}">Description</label><textarea class="acw-ticket-desc" rows="6" style="${fld}resize:vertical;"></textarea><div style="display:flex;gap:8px;margin-top:10px;"><button class="acw-ticket-send" type="button" style="border:none;border-radius:8px;padding:6px 14px;cursor:pointer;background:var(--acw-accent);color:var(--acw-accent-fg);font-size:13px;">Send ticket</button><button class="acw-ticket-cancel" type="button" style="border:1px solid var(--acw-border);border-radius:8px;padding:6px 12px;cursor:pointer;background:var(--acw-bg);color:var(--acw-fg);font-size:13px;">Cancel</button></div>`;
|
|
472
|
+
card.querySelector(".acw-ticket-subject").value = subject;
|
|
473
|
+
card.querySelector(".acw-ticket-desc").value = description;
|
|
474
|
+
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
475
|
+
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
476
|
+
const subj = card.querySelector(".acw-ticket-subject").value.trim();
|
|
477
|
+
const desc = card.querySelector(".acw-ticket-desc").value.trim();
|
|
478
|
+
card.remove();
|
|
479
|
+
void this.submitTicket(subj, desc, requestId);
|
|
480
|
+
});
|
|
481
|
+
this.body.appendChild(card);
|
|
482
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
483
|
+
}
|
|
484
|
+
async submitTicket(subject, description, requestId) {
|
|
485
|
+
this.setProgress("Sending your support ticket\u2026");
|
|
486
|
+
try {
|
|
487
|
+
const sessionId = await this.ensureSession();
|
|
488
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/support-ticket`, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: this.headers(),
|
|
491
|
+
body: JSON.stringify({
|
|
492
|
+
tenantUniqueName: this.config.tenantUniqueName,
|
|
493
|
+
environment: this.config.environment,
|
|
494
|
+
requestId,
|
|
495
|
+
subject,
|
|
496
|
+
description
|
|
497
|
+
})
|
|
498
|
+
});
|
|
499
|
+
this.setProgress("");
|
|
500
|
+
if (res.status === 403) {
|
|
501
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Support tickets are available for Requestor/Approver roles." });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!res.ok) {
|
|
505
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `Could not send the ticket (${res.status}).` });
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const data = await res.json();
|
|
509
|
+
if (data?.status === "Created") {
|
|
510
|
+
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
511
|
+
const el = document.createElement("div");
|
|
512
|
+
el.className = "acw-answer acw-okstate";
|
|
513
|
+
el.innerHTML = `<div class="acw-reason">Support ticket ${escapeHtml(num)} has been created. The support team will follow up.</div>` + (data.webUrl ? `<div class="acw-role"><a href="${escapeHtml(data.webUrl)}" target="_blank" rel="noopener">Open the ticket</a></div>` : "");
|
|
514
|
+
this.body.appendChild(el);
|
|
515
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
516
|
+
} else {
|
|
517
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
this.setProgress("");
|
|
521
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
346
524
|
// ---- networking ----
|
|
347
525
|
headers() {
|
|
348
526
|
const h = { "Content-Type": "application/json" };
|
|
@@ -372,6 +550,7 @@ var CopilotWidget = class {
|
|
|
372
550
|
}
|
|
373
551
|
async runAsk(question) {
|
|
374
552
|
this.open();
|
|
553
|
+
this.lastQuestion = question;
|
|
375
554
|
this.addMessage(question, "user");
|
|
376
555
|
this.setProgress("Reading your question\u2026");
|
|
377
556
|
try {
|
|
@@ -389,7 +568,8 @@ var CopilotWidget = class {
|
|
|
389
568
|
this.renderAnswer(await res.json());
|
|
390
569
|
} catch (err) {
|
|
391
570
|
this.setProgress("");
|
|
392
|
-
|
|
571
|
+
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
572
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
393
573
|
}
|
|
394
574
|
}
|
|
395
575
|
async connectSignalR() {
|
|
@@ -415,6 +595,10 @@ var ApplicationCopilotWidget = {
|
|
|
415
595
|
init: (config) => instance.init(config),
|
|
416
596
|
open: () => instance.open(),
|
|
417
597
|
close: () => instance.close(),
|
|
598
|
+
show: () => instance.show(),
|
|
599
|
+
hide: () => instance.hide(),
|
|
600
|
+
notify: (text, suggestion) => instance.notify(text, suggestion),
|
|
601
|
+
promptUpload: () => instance.promptUpload(),
|
|
418
602
|
ask: (question) => instance.ask(question),
|
|
419
603
|
setContext: (context) => instance.setContext(context),
|
|
420
604
|
captureError: (errorContext) => instance.captureError(errorContext)
|
package/dist/index.d.cts
CHANGED
|
@@ -70,6 +70,10 @@ declare const ApplicationCopilotWidget: {
|
|
|
70
70
|
init: (config: WidgetConfig) => void;
|
|
71
71
|
open: () => void;
|
|
72
72
|
close: () => void;
|
|
73
|
+
show: () => void;
|
|
74
|
+
hide: () => void;
|
|
75
|
+
notify: (text: string, suggestion?: string) => void;
|
|
76
|
+
promptUpload: () => void;
|
|
73
77
|
ask: (question: string) => void;
|
|
74
78
|
setContext: (context: ApplicationContext) => void;
|
|
75
79
|
captureError: (errorContext: ApplicationContext["recentError"]) => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,10 @@ declare const ApplicationCopilotWidget: {
|
|
|
70
70
|
init: (config: WidgetConfig) => void;
|
|
71
71
|
open: () => void;
|
|
72
72
|
close: () => void;
|
|
73
|
+
show: () => void;
|
|
74
|
+
hide: () => void;
|
|
75
|
+
notify: (text: string, suggestion?: string) => void;
|
|
76
|
+
promptUpload: () => void;
|
|
73
77
|
ask: (question: string) => void;
|
|
74
78
|
setContext: (context: ApplicationContext) => void;
|
|
75
79
|
captureError: (errorContext: ApplicationContext["recentError"]) => void;
|
package/dist/index.js
CHANGED
|
@@ -109,10 +109,15 @@ var PROGRESS_LABELS = {
|
|
|
109
109
|
};
|
|
110
110
|
var CopilotWidget = class {
|
|
111
111
|
constructor() {
|
|
112
|
+
this.lastQuestion = "";
|
|
112
113
|
this.mode = "explain";
|
|
113
114
|
}
|
|
114
115
|
init(config) {
|
|
115
116
|
this.config = config;
|
|
117
|
+
try {
|
|
118
|
+
console.log("[ApplicationCopilot] widget loaded \u2014 support-ticket build v2");
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
116
121
|
this.mount();
|
|
117
122
|
this.connectSignalR().catch(() => {
|
|
118
123
|
});
|
|
@@ -124,6 +129,17 @@ var CopilotWidget = class {
|
|
|
124
129
|
close() {
|
|
125
130
|
this.panel?.classList.remove("acw-open");
|
|
126
131
|
}
|
|
132
|
+
/** Show the launcher (widget host). No-op before init(). */
|
|
133
|
+
show() {
|
|
134
|
+
if (this.host) this.host.style.display = "";
|
|
135
|
+
}
|
|
136
|
+
/** Hide the launcher and close the panel. Used to gate the widget by route. */
|
|
137
|
+
hide() {
|
|
138
|
+
if (this.host) {
|
|
139
|
+
this.host.style.display = "none";
|
|
140
|
+
}
|
|
141
|
+
this.close();
|
|
142
|
+
}
|
|
127
143
|
setContext(context) {
|
|
128
144
|
this.contextOverride = context;
|
|
129
145
|
}
|
|
@@ -131,6 +147,25 @@ var CopilotWidget = class {
|
|
|
131
147
|
this.recentError = errorContext;
|
|
132
148
|
this.open();
|
|
133
149
|
}
|
|
150
|
+
/** Proactive nudge: show + open the panel with a message and an optional one-click suggested question. */
|
|
151
|
+
notify(text, suggestion) {
|
|
152
|
+
this.show();
|
|
153
|
+
this.open();
|
|
154
|
+
if (!this.body) return;
|
|
155
|
+
const el = document.createElement("div");
|
|
156
|
+
el.className = "acw-msg acw-assistant acw-nudge";
|
|
157
|
+
el.innerHTML = `<div class="acw-reason">${escapeHtml(text)}</div>`;
|
|
158
|
+
if (suggestion) {
|
|
159
|
+
const btn = document.createElement("button");
|
|
160
|
+
btn.type = "button";
|
|
161
|
+
btn.className = "acw-chip";
|
|
162
|
+
btn.textContent = suggestion;
|
|
163
|
+
btn.addEventListener("click", () => this.ask(suggestion));
|
|
164
|
+
el.appendChild(btn);
|
|
165
|
+
}
|
|
166
|
+
this.body.appendChild(el);
|
|
167
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
168
|
+
}
|
|
134
169
|
ask(question) {
|
|
135
170
|
if (this.mode === "intake") {
|
|
136
171
|
void this.runIntakeAsk(question);
|
|
@@ -157,27 +192,79 @@ var CopilotWidget = class {
|
|
|
157
192
|
wrap.querySelector(".acw-launch").addEventListener("click", () => this.panel?.classList.contains("acw-open") ? this.close() : this.open());
|
|
158
193
|
const panel = document.createElement("div");
|
|
159
194
|
panel.className = "acw-panel";
|
|
195
|
+
const uploadIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
|
|
160
196
|
panel.innerHTML = `
|
|
161
197
|
<div class="acw-header">
|
|
162
198
|
${sparkle}
|
|
163
199
|
<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;display:inline-flex;align-items:center;">${uploadIcon}</button>
|
|
164
201
|
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
165
202
|
</div>
|
|
166
203
|
<div class="acw-body"></div>
|
|
167
204
|
<div class="acw-footer">
|
|
168
205
|
<input class="acw-input" type="text" placeholder="Ask a question\u2026" />
|
|
169
206
|
<button class="acw-send" type="button">Send</button>
|
|
170
|
-
</div
|
|
207
|
+
</div>
|
|
208
|
+
<input class="acw-file" type="file" accept=".pdf,.docx,.xlsx,.xlsm,.txt,.md,.markdown,.csv" style="display:none" />`;
|
|
171
209
|
this.root.appendChild(panel);
|
|
172
210
|
this.panel = panel;
|
|
173
211
|
this.body = panel.querySelector(".acw-body");
|
|
174
212
|
this.input = panel.querySelector(".acw-input");
|
|
213
|
+
this.fileInput = panel.querySelector(".acw-file");
|
|
175
214
|
panel.querySelector(".acw-close").addEventListener("click", () => this.close());
|
|
176
215
|
panel.querySelector(".acw-send").addEventListener("click", () => this.submitFromInput());
|
|
216
|
+
panel.querySelector(".acw-kb").addEventListener("click", () => this.promptUpload());
|
|
217
|
+
this.fileInput.addEventListener("change", () => {
|
|
218
|
+
const f = this.fileInput?.files?.[0];
|
|
219
|
+
if (f) {
|
|
220
|
+
void this.uploadDocument(f);
|
|
221
|
+
if (this.fileInput) this.fileInput.value = "";
|
|
222
|
+
}
|
|
223
|
+
});
|
|
177
224
|
this.input.addEventListener("keydown", (e) => {
|
|
178
225
|
if (e.key === "Enter") this.submitFromInput();
|
|
179
226
|
});
|
|
180
227
|
}
|
|
228
|
+
/** Open the file picker to add a document to the knowledge base (server enforces admin-only). */
|
|
229
|
+
promptUpload() {
|
|
230
|
+
this.open();
|
|
231
|
+
this.fileInput?.click();
|
|
232
|
+
}
|
|
233
|
+
async uploadDocument(file) {
|
|
234
|
+
this.open();
|
|
235
|
+
this.addMessage(`Uploading \u201C${file.name}\u201D to the knowledge base\u2026`, "user");
|
|
236
|
+
this.setProgress("Extracting and indexing\u2026");
|
|
237
|
+
try {
|
|
238
|
+
const form = new FormData();
|
|
239
|
+
form.append("file", file, file.name);
|
|
240
|
+
form.append("tenantUniqueName", this.config.tenantUniqueName);
|
|
241
|
+
form.append("environment", this.config.environment);
|
|
242
|
+
const h = {};
|
|
243
|
+
const token = this.config.getAuthToken?.();
|
|
244
|
+
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
245
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
246
|
+
else if (email) h["X-User-Email"] = email;
|
|
247
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/knowledge/upload`, { method: "POST", headers: h, body: form });
|
|
248
|
+
this.setProgress("");
|
|
249
|
+
if (res.status === 403) {
|
|
250
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Only Spektra Admins can add to the knowledge base." });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (!res.ok) {
|
|
254
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `Upload failed (${res.status}).` });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const data = await res.json();
|
|
258
|
+
if (data?.status === "Indexed") {
|
|
259
|
+
this.renderAnswer({ status: "Answered", disabled: false, primaryReason: `Added \u201C${data.title}\u201D to the knowledge base (${data.chunks} chunk${data.chunks === 1 ? "" : "s"}). I can now use it to answer questions.` });
|
|
260
|
+
} else {
|
|
261
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not add the document." });
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
this.setProgress("");
|
|
265
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
181
268
|
submitFromInput() {
|
|
182
269
|
const q = this.input?.value.trim();
|
|
183
270
|
if (q) {
|
|
@@ -304,8 +391,99 @@ var CopilotWidget = class {
|
|
|
304
391
|
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>` : "");
|
|
305
392
|
}
|
|
306
393
|
this.body.appendChild(el);
|
|
394
|
+
const role = (answer.verifiedRoleLabel || "").toLowerCase();
|
|
395
|
+
const eligibleRole = role === "requestor" || role === "approver";
|
|
396
|
+
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 || "");
|
|
397
|
+
const offerTicket = answer.supportTicketEligible === true || (answer.disabled === true || looksBlocked) && eligibleRole;
|
|
398
|
+
if (!isFailure && offerTicket) {
|
|
399
|
+
const btn = document.createElement("button");
|
|
400
|
+
btn.type = "button";
|
|
401
|
+
btn.className = "acw-chip";
|
|
402
|
+
btn.textContent = "Raise a support ticket";
|
|
403
|
+
btn.addEventListener("click", () => this.openTicketDraft(answer));
|
|
404
|
+
el.appendChild(btn);
|
|
405
|
+
}
|
|
307
406
|
this.body.scrollTop = this.body.scrollHeight;
|
|
308
407
|
}
|
|
408
|
+
currentRequestId() {
|
|
409
|
+
const ctx = this.currentContext();
|
|
410
|
+
const explicit = ctx.page?.requestId || ctx.page?.RequestId;
|
|
411
|
+
if (explicit) return String(explicit);
|
|
412
|
+
const route = ctx.page?.route || (typeof window !== "undefined" ? window.location.pathname : "");
|
|
413
|
+
const m = /MS[A-Z0-9]{6,}/i.exec(route || "");
|
|
414
|
+
return m ? m[0] : void 0;
|
|
415
|
+
}
|
|
416
|
+
/** Build a pre-filled, editable ticket draft and let the user review + Send (they trigger the API call). */
|
|
417
|
+
openTicketDraft(answer) {
|
|
418
|
+
const requestId = this.currentRequestId();
|
|
419
|
+
const role = answer.verifiedRoleLabel || "";
|
|
420
|
+
const reason = answer.primaryReason || "I am blocked from completing an action.";
|
|
421
|
+
const subject = requestId ? `[Cloud Events] Help needed on request ${requestId}` : "[Cloud Events] Help needed with an event request";
|
|
422
|
+
const description = `Raised from the Application Copilot.
|
|
423
|
+
|
|
424
|
+
` + (role ? `Role: ${role}
|
|
425
|
+
` : "") + `Request Id: ${requestId || "(not on a specific request)"}
|
|
426
|
+
` + (this.lastQuestion ? `Question: ${this.lastQuestion}
|
|
427
|
+
` : "") + `
|
|
428
|
+
What the copilot reported:
|
|
429
|
+
${reason}`;
|
|
430
|
+
const card = document.createElement("div");
|
|
431
|
+
card.className = "acw-answer";
|
|
432
|
+
const lbl = "display:block;font-size:11px;color:var(--acw-muted);margin:8px 0 2px;";
|
|
433
|
+
const fld = "width:100%;box-sizing:border-box;font:inherit;font-size:13px;padding:6px 8px;border:1px solid var(--acw-border);border-radius:8px;background:var(--acw-bg);color:var(--acw-fg);";
|
|
434
|
+
card.innerHTML = `<div class="acw-reason"><strong>Review your support ticket</strong></div><label style="${lbl}">Subject</label><input class="acw-ticket-subject" type="text" style="${fld}" /><label style="${lbl}">Description</label><textarea class="acw-ticket-desc" rows="6" style="${fld}resize:vertical;"></textarea><div style="display:flex;gap:8px;margin-top:10px;"><button class="acw-ticket-send" type="button" style="border:none;border-radius:8px;padding:6px 14px;cursor:pointer;background:var(--acw-accent);color:var(--acw-accent-fg);font-size:13px;">Send ticket</button><button class="acw-ticket-cancel" type="button" style="border:1px solid var(--acw-border);border-radius:8px;padding:6px 12px;cursor:pointer;background:var(--acw-bg);color:var(--acw-fg);font-size:13px;">Cancel</button></div>`;
|
|
435
|
+
card.querySelector(".acw-ticket-subject").value = subject;
|
|
436
|
+
card.querySelector(".acw-ticket-desc").value = description;
|
|
437
|
+
card.querySelector(".acw-ticket-cancel").addEventListener("click", () => card.remove());
|
|
438
|
+
card.querySelector(".acw-ticket-send").addEventListener("click", () => {
|
|
439
|
+
const subj = card.querySelector(".acw-ticket-subject").value.trim();
|
|
440
|
+
const desc = card.querySelector(".acw-ticket-desc").value.trim();
|
|
441
|
+
card.remove();
|
|
442
|
+
void this.submitTicket(subj, desc, requestId);
|
|
443
|
+
});
|
|
444
|
+
this.body.appendChild(card);
|
|
445
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
446
|
+
}
|
|
447
|
+
async submitTicket(subject, description, requestId) {
|
|
448
|
+
this.setProgress("Sending your support ticket\u2026");
|
|
449
|
+
try {
|
|
450
|
+
const sessionId = await this.ensureSession();
|
|
451
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/support-ticket`, {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: this.headers(),
|
|
454
|
+
body: JSON.stringify({
|
|
455
|
+
tenantUniqueName: this.config.tenantUniqueName,
|
|
456
|
+
environment: this.config.environment,
|
|
457
|
+
requestId,
|
|
458
|
+
subject,
|
|
459
|
+
description
|
|
460
|
+
})
|
|
461
|
+
});
|
|
462
|
+
this.setProgress("");
|
|
463
|
+
if (res.status === 403) {
|
|
464
|
+
this.renderAnswer({ status: "Forbidden", primaryReason: "Support tickets are available for Requestor/Approver roles." });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (!res.ok) {
|
|
468
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `Could not send the ticket (${res.status}).` });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
if (data?.status === "Created") {
|
|
473
|
+
const num = data.ticketNumber ? `#${data.ticketNumber}` : "";
|
|
474
|
+
const el = document.createElement("div");
|
|
475
|
+
el.className = "acw-answer acw-okstate";
|
|
476
|
+
el.innerHTML = `<div class="acw-reason">Support ticket ${escapeHtml(num)} has been created. The support team will follow up.</div>` + (data.webUrl ? `<div class="acw-role"><a href="${escapeHtml(data.webUrl)}" target="_blank" rel="noopener">Open the ticket</a></div>` : "");
|
|
477
|
+
this.body.appendChild(el);
|
|
478
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
479
|
+
} else {
|
|
480
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: data?.reason || "Could not create the ticket." });
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
this.setProgress("");
|
|
484
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
309
487
|
// ---- networking ----
|
|
310
488
|
headers() {
|
|
311
489
|
const h = { "Content-Type": "application/json" };
|
|
@@ -335,6 +513,7 @@ var CopilotWidget = class {
|
|
|
335
513
|
}
|
|
336
514
|
async runAsk(question) {
|
|
337
515
|
this.open();
|
|
516
|
+
this.lastQuestion = question;
|
|
338
517
|
this.addMessage(question, "user");
|
|
339
518
|
this.setProgress("Reading your question\u2026");
|
|
340
519
|
try {
|
|
@@ -352,7 +531,8 @@ var CopilotWidget = class {
|
|
|
352
531
|
this.renderAnswer(await res.json());
|
|
353
532
|
} catch (err) {
|
|
354
533
|
this.setProgress("");
|
|
355
|
-
|
|
534
|
+
const msg = err?.message ? String(err.message) : "Could not reach the copilot service.";
|
|
535
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: msg });
|
|
356
536
|
}
|
|
357
537
|
}
|
|
358
538
|
async connectSignalR() {
|
|
@@ -378,6 +558,10 @@ var ApplicationCopilotWidget = {
|
|
|
378
558
|
init: (config) => instance.init(config),
|
|
379
559
|
open: () => instance.open(),
|
|
380
560
|
close: () => instance.close(),
|
|
561
|
+
show: () => instance.show(),
|
|
562
|
+
hide: () => instance.hide(),
|
|
563
|
+
notify: (text, suggestion) => instance.notify(text, suggestion),
|
|
564
|
+
promptUpload: () => instance.promptUpload(),
|
|
381
565
|
ask: (question) => instance.ask(question),
|
|
382
566
|
setContext: (context) => instance.setContext(context),
|
|
383
567
|
captureError: (errorContext) => instance.captureError(errorContext)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spektra-cloudevents/application-copilot-widget",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Spektra Application Copilot widget — framework-agnostic and CSS-library agnostic via Shadow DOM isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|