@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 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">&times;</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
- this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
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">&times;</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
- this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
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.0.0",
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": {