@spektra-cloudevents/application-copilot-widget 1.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 +75 -0
- package/dist/index.cjs +429 -0
- package/dist/index.d.cts +83 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.js +392 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @spektra/application-copilot-widget
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, **CSS-library agnostic** copilot widget. It renders entirely
|
|
4
|
+
inside a **Shadow DOM**, so the host app's styles (Bootstrap, Angular Material,
|
|
5
|
+
Tailwind, etc.) cannot affect the widget and the widget's styles cannot leak out.
|
|
6
|
+
It works in Angular, React, Vue, or plain HTML.
|
|
7
|
+
|
|
8
|
+
## Build
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cd packages/application-copilot-widget
|
|
12
|
+
npm install
|
|
13
|
+
npm run build # outputs dist/ (ESM + CJS + types) via tsup
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Use (Angular example)
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { ApplicationCopilotWidget } from '@spektra/application-copilot-widget';
|
|
20
|
+
|
|
21
|
+
ApplicationCopilotWidget.init({
|
|
22
|
+
projectKey: 'cloud-events',
|
|
23
|
+
environment: 'stage',
|
|
24
|
+
apiBaseUrl: environment.applicationCopilotApi,
|
|
25
|
+
signalRHubUrl: `${environment.applicationCopilotApi}/hubs/application-copilot`,
|
|
26
|
+
tenantUniqueName: this.tenantUniqueName,
|
|
27
|
+
getAuthToken: () => this.authService.accessToken, // sent as Authorization: Bearer
|
|
28
|
+
// devUserEmail: 'someone@partner.com', // local testing only
|
|
29
|
+
|
|
30
|
+
getContext: () => ({
|
|
31
|
+
projectKey: 'cloud-events',
|
|
32
|
+
environment: 'stage',
|
|
33
|
+
page: { pageKey: 'create-update-event', activeTab: this.activeTab, componentHint: 'ScheduleComponent' },
|
|
34
|
+
entities: { eventRequestId: this.eventRequestId },
|
|
35
|
+
uiState: {
|
|
36
|
+
disabledFields: [
|
|
37
|
+
...(this.scheduleForm.get('startDateTime')?.disabled ? ['eventStartDate'] : []),
|
|
38
|
+
...(this.scheduleForm.get('endDateTime')?.disabled ? ['eventEndDate'] : [])
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
// Hint values for the Schedule rule (backend re-verifies the role):
|
|
42
|
+
clientSnapshot: {
|
|
43
|
+
sectionMode: this.sectionMode,
|
|
44
|
+
isDraftMode: this.isDraftMode,
|
|
45
|
+
isBudgetStatusClosed: this.isBudgetStatusClosed,
|
|
46
|
+
showPendingRegistrationBanner: this.showPendingRegistrationBanner,
|
|
47
|
+
eventPriorDays: this.editEventObj?.EventPriorDays,
|
|
48
|
+
controlSetting: {
|
|
49
|
+
canEdit: this.startDateControlSetting?.CanEdit,
|
|
50
|
+
priorDays: this.startDateControlSetting?.PriorDays
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
contextVersion: this.contextVersion,
|
|
54
|
+
capturedAt: new Date().toISOString()
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Public API
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
ApplicationCopilotWidget.init(config);
|
|
63
|
+
ApplicationCopilotWidget.open();
|
|
64
|
+
ApplicationCopilotWidget.ask('Why are the dates disabled?');
|
|
65
|
+
ApplicationCopilotWidget.setContext(context);
|
|
66
|
+
ApplicationCopilotWidget.captureError(errorContext);
|
|
67
|
+
ApplicationCopilotWidget.close();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Why Shadow DOM
|
|
71
|
+
|
|
72
|
+
The widget mounts a single host element and attaches `attachShadow({ mode: 'open' })`.
|
|
73
|
+
All markup and CSS live inside that shadow root, and `:host { all: initial }` neutralises
|
|
74
|
+
inherited styles. The result: pixel-identical rendering regardless of the host's CSS
|
|
75
|
+
framework, with zero risk of class-name collisions in either direction.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ApplicationCopilotWidget: () => ApplicationCopilotWidget,
|
|
34
|
+
default: () => index_default
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/styles.ts
|
|
39
|
+
var widgetStyles = `
|
|
40
|
+
:host {
|
|
41
|
+
all: initial;
|
|
42
|
+
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
|
43
|
+
--acw-bg: #ffffff;
|
|
44
|
+
--acw-surface: #f4f5fb;
|
|
45
|
+
--acw-fg: #1b1f3b;
|
|
46
|
+
--acw-muted: #5a6072;
|
|
47
|
+
--acw-border: #e3e6f0;
|
|
48
|
+
--acw-accent: #5b3df5;
|
|
49
|
+
--acw-accent-dark: #4a2fe0;
|
|
50
|
+
--acw-accent-2: #8b5cf6;
|
|
51
|
+
--acw-accent-fg: #ffffff;
|
|
52
|
+
--acw-danger: #c0362c;
|
|
53
|
+
--acw-ok: #1f9d6b;
|
|
54
|
+
}
|
|
55
|
+
* { box-sizing: border-box; }
|
|
56
|
+
|
|
57
|
+
/* Launcher: AI icon + hover tooltip */
|
|
58
|
+
.acw-launch-wrap { position: fixed; right: 24px; bottom: 24px; z-index: 2147483000; }
|
|
59
|
+
.acw-launch {
|
|
60
|
+
width: 60px; height: 60px; border: none; border-radius: 50%;
|
|
61
|
+
background: linear-gradient(135deg, var(--acw-accent), var(--acw-accent-2));
|
|
62
|
+
color: var(--acw-accent-fg); cursor: pointer;
|
|
63
|
+
display: flex; align-items: center; justify-content: center;
|
|
64
|
+
box-shadow: 0 8px 24px rgba(91,61,245,0.45);
|
|
65
|
+
transition: transform .15s ease, box-shadow .15s ease;
|
|
66
|
+
}
|
|
67
|
+
.acw-launch:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 12px 30px rgba(91,61,245,0.55); }
|
|
68
|
+
.acw-launch svg { width: 30px; height: 30px; }
|
|
69
|
+
.acw-tip {
|
|
70
|
+
position: absolute; right: 72px; bottom: 17px; white-space: nowrap;
|
|
71
|
+
background: #1b1f3b; color: #fff; font-size: 13px; padding: 7px 12px;
|
|
72
|
+
border-radius: 8px; opacity: 0; pointer-events: none; transition: opacity .15s ease;
|
|
73
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
|
|
74
|
+
}
|
|
75
|
+
.acw-launch-wrap:hover .acw-tip { opacity: 1; }
|
|
76
|
+
|
|
77
|
+
/* Panel */
|
|
78
|
+
.acw-panel {
|
|
79
|
+
position: fixed; right: 24px; bottom: 96px; z-index: 2147483000;
|
|
80
|
+
width: 410px; max-width: calc(100vw - 48px); height: 580px; max-height: calc(100vh - 130px);
|
|
81
|
+
display: none; flex-direction: column;
|
|
82
|
+
background: var(--acw-bg); color: var(--acw-fg);
|
|
83
|
+
border: 1px solid var(--acw-border); border-radius: 16px; overflow: hidden;
|
|
84
|
+
box-shadow: 0 18px 50px rgba(27,31,59,0.28);
|
|
85
|
+
}
|
|
86
|
+
.acw-panel.acw-open { display: flex; }
|
|
87
|
+
|
|
88
|
+
.acw-header {
|
|
89
|
+
display: flex; align-items: center; gap: 10px;
|
|
90
|
+
padding: 14px 16px;
|
|
91
|
+
background: linear-gradient(135deg, var(--acw-accent), var(--acw-accent-2));
|
|
92
|
+
color: #fff;
|
|
93
|
+
}
|
|
94
|
+
.acw-header svg { width: 22px; height: 22px; flex: none; }
|
|
95
|
+
.acw-title { font-size: 16px; font-weight: 600; }
|
|
96
|
+
.acw-close { margin-left: auto; border: none; background: transparent; cursor: pointer; font-size: 22px; color: rgba(255,255,255,0.85); line-height: 1; }
|
|
97
|
+
.acw-close:hover { color: #fff; }
|
|
98
|
+
|
|
99
|
+
.acw-body { flex: 1; overflow-y: auto; padding: 16px; background: var(--acw-surface); }
|
|
100
|
+
.acw-hint { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
101
|
+
|
|
102
|
+
.acw-chip {
|
|
103
|
+
display: block; width: 100%; text-align: left;
|
|
104
|
+
padding: 11px 13px; margin-bottom: 8px;
|
|
105
|
+
border: 1px solid var(--acw-border); border-radius: 10px;
|
|
106
|
+
background: var(--acw-bg); color: var(--acw-fg); font-size: 14px; cursor: pointer;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
transition: border-color .15s ease, background .15s ease;
|
|
109
|
+
}
|
|
110
|
+
.acw-chip:hover { background: #eceafe; border-color: var(--acw-accent); }
|
|
111
|
+
|
|
112
|
+
.acw-msg { font-size: 14px; margin: 12px 0; }
|
|
113
|
+
.acw-msg.acw-user {
|
|
114
|
+
text-align: right; color: var(--acw-fg);
|
|
115
|
+
}
|
|
116
|
+
.acw-msg.acw-user span, .acw-msg.acw-user {
|
|
117
|
+
display: inline-block;
|
|
118
|
+
}
|
|
119
|
+
.acw-progress { font-size: 13px; color: var(--acw-muted); margin: 10px 0; }
|
|
120
|
+
|
|
121
|
+
.acw-answer { background: var(--acw-bg); border: 1px solid var(--acw-border); border-radius: 12px; padding: 12px 14px; margin: 12px 0; }
|
|
122
|
+
.acw-answer .acw-reason { font-size: 14px; line-height: 1.5; }
|
|
123
|
+
.acw-answer.acw-blocked { border-left: 4px solid var(--acw-danger); }
|
|
124
|
+
.acw-answer.acw-okstate { border-left: 4px solid var(--acw-ok); }
|
|
125
|
+
.acw-role { font-size: 12px; color: var(--acw-muted); margin-top: 8px; }
|
|
126
|
+
.acw-evidence { font-size: 12px; color: var(--acw-muted); margin-top: 10px; word-break: break-word; }
|
|
127
|
+
.acw-error { font-size: 14px; color: var(--acw-danger); }
|
|
128
|
+
|
|
129
|
+
.acw-footer { display: flex; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--acw-border); background: var(--acw-bg); }
|
|
130
|
+
.acw-input {
|
|
131
|
+
flex: 1; padding: 10px 12px; font-size: 14px;
|
|
132
|
+
border: 1px solid var(--acw-border); border-radius: 10px; color: var(--acw-fg); background: var(--acw-bg);
|
|
133
|
+
font-family: inherit;
|
|
134
|
+
}
|
|
135
|
+
.acw-input:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
136
|
+
.acw-send { border: none; border-radius: 10px; padding: 0 16px; background: var(--acw-accent); color: #fff; cursor: pointer; font-size: 14px; font-family: inherit; }
|
|
137
|
+
.acw-send:hover { background: var(--acw-accent-dark); }
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
// src/widget.ts
|
|
141
|
+
var PROGRESS_LABELS = {
|
|
142
|
+
QuestionAccepted: "Reading your question\u2026",
|
|
143
|
+
PermissionsVerified: "Verifying your permissions\u2026",
|
|
144
|
+
RuleEvaluated: "Evaluating the rule\u2026",
|
|
145
|
+
AnswerCompleted: ""
|
|
146
|
+
};
|
|
147
|
+
var CopilotWidget = class {
|
|
148
|
+
constructor() {
|
|
149
|
+
this.mode = "explain";
|
|
150
|
+
}
|
|
151
|
+
init(config) {
|
|
152
|
+
this.config = config;
|
|
153
|
+
this.mount();
|
|
154
|
+
this.connectSignalR().catch(() => {
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
open() {
|
|
158
|
+
this.panel?.classList.add("acw-open");
|
|
159
|
+
this.refreshChips();
|
|
160
|
+
}
|
|
161
|
+
close() {
|
|
162
|
+
this.panel?.classList.remove("acw-open");
|
|
163
|
+
}
|
|
164
|
+
setContext(context) {
|
|
165
|
+
this.contextOverride = context;
|
|
166
|
+
}
|
|
167
|
+
captureError(errorContext) {
|
|
168
|
+
this.recentError = errorContext;
|
|
169
|
+
this.open();
|
|
170
|
+
}
|
|
171
|
+
ask(question) {
|
|
172
|
+
if (this.mode === "intake") {
|
|
173
|
+
void this.runIntakeAsk(question);
|
|
174
|
+
} else {
|
|
175
|
+
void this.runAsk(question);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ---- rendering (all inside Shadow DOM) ----
|
|
179
|
+
mount() {
|
|
180
|
+
if (this.host) return;
|
|
181
|
+
this.host = document.createElement("div");
|
|
182
|
+
this.host.setAttribute("data-spektra-copilot", "");
|
|
183
|
+
document.body.appendChild(this.host);
|
|
184
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
185
|
+
const style = document.createElement("style");
|
|
186
|
+
style.textContent = widgetStyles;
|
|
187
|
+
this.root.appendChild(style);
|
|
188
|
+
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>';
|
|
189
|
+
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
190
|
+
const wrap = document.createElement("div");
|
|
191
|
+
wrap.className = "acw-launch-wrap";
|
|
192
|
+
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
193
|
+
this.root.appendChild(wrap);
|
|
194
|
+
wrap.querySelector(".acw-launch").addEventListener("click", () => this.panel?.classList.contains("acw-open") ? this.close() : this.open());
|
|
195
|
+
const panel = document.createElement("div");
|
|
196
|
+
panel.className = "acw-panel";
|
|
197
|
+
panel.innerHTML = `
|
|
198
|
+
<div class="acw-header">
|
|
199
|
+
${sparkle}
|
|
200
|
+
<span class="acw-title">Application Copilot</span>
|
|
201
|
+
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="acw-body"></div>
|
|
204
|
+
<div class="acw-footer">
|
|
205
|
+
<input class="acw-input" type="text" placeholder="Ask a question\u2026" />
|
|
206
|
+
<button class="acw-send" type="button">Send</button>
|
|
207
|
+
</div>`;
|
|
208
|
+
this.root.appendChild(panel);
|
|
209
|
+
this.panel = panel;
|
|
210
|
+
this.body = panel.querySelector(".acw-body");
|
|
211
|
+
this.input = panel.querySelector(".acw-input");
|
|
212
|
+
panel.querySelector(".acw-close").addEventListener("click", () => this.close());
|
|
213
|
+
panel.querySelector(".acw-send").addEventListener("click", () => this.submitFromInput());
|
|
214
|
+
this.input.addEventListener("keydown", (e) => {
|
|
215
|
+
if (e.key === "Enter") this.submitFromInput();
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
submitFromInput() {
|
|
219
|
+
const q = this.input?.value.trim();
|
|
220
|
+
if (q) {
|
|
221
|
+
this.input.value = "";
|
|
222
|
+
this.ask(q);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
navigate(path) {
|
|
226
|
+
if (this.config.onNavigate) {
|
|
227
|
+
this.config.onNavigate(path);
|
|
228
|
+
this.close();
|
|
229
|
+
} else {
|
|
230
|
+
window.location.href = path;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
currentContext() {
|
|
234
|
+
const ctx = this.contextOverride ?? this.config.getContext();
|
|
235
|
+
if (this.recentError) ctx.recentError = this.recentError;
|
|
236
|
+
return ctx;
|
|
237
|
+
}
|
|
238
|
+
refreshChips() {
|
|
239
|
+
if (!this.body || this.body.childElementCount > 0) return;
|
|
240
|
+
const greeting = document.createElement("p");
|
|
241
|
+
greeting.className = "acw-hint";
|
|
242
|
+
greeting.textContent = "Ask me anything about this page \u2014 for example, why a field is disabled, or why you can\u2019t update, reschedule, or submit something.";
|
|
243
|
+
this.body.appendChild(greeting);
|
|
244
|
+
}
|
|
245
|
+
enterIntakeMode() {
|
|
246
|
+
this.mode = "intake";
|
|
247
|
+
if (this.input) this.input.placeholder = "Describe the event you want to run\u2026";
|
|
248
|
+
const hint = document.createElement("div");
|
|
249
|
+
hint.className = "acw-progress";
|
|
250
|
+
hint.textContent = "Guided intake \u2014 tell me the topic, attendee count, format, and rough date.";
|
|
251
|
+
this.body.appendChild(hint);
|
|
252
|
+
this.input?.focus();
|
|
253
|
+
}
|
|
254
|
+
async runIntakeAsk(question) {
|
|
255
|
+
this.open();
|
|
256
|
+
this.addMessage(question, "user");
|
|
257
|
+
this.setProgress("Finding the right offering\u2026");
|
|
258
|
+
try {
|
|
259
|
+
const sessionId = await this.ensureSession();
|
|
260
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/intake/ask`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: this.headers(),
|
|
263
|
+
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
264
|
+
});
|
|
265
|
+
if (!res.ok) {
|
|
266
|
+
this.setProgress("");
|
|
267
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
this.renderIntakeAnswer(await res.json());
|
|
271
|
+
} catch {
|
|
272
|
+
this.setProgress("");
|
|
273
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
renderIntakeAnswer(data) {
|
|
277
|
+
this.setProgress("");
|
|
278
|
+
const rec = data?.recommendation;
|
|
279
|
+
const understood = data?.understood;
|
|
280
|
+
const el = document.createElement("div");
|
|
281
|
+
if (!rec) {
|
|
282
|
+
el.className = "acw-answer";
|
|
283
|
+
el.innerHTML = `<div class="acw-reason">${escapeHtml(data?.note || "Could not match an offering to your request.")}</div>`;
|
|
284
|
+
this.body.appendChild(el);
|
|
285
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const standard = rec.classification === "Standard";
|
|
289
|
+
const deepLink = data?.deepLink;
|
|
290
|
+
el.className = "acw-answer " + (standard ? "acw-okstate" : "acw-blocked");
|
|
291
|
+
const reasons = (rec.reasons || []).map((r) => `<li>${escapeHtml(r)}</li>`).join("");
|
|
292
|
+
const routeLabel = standard ? "Open this track to submit" : "Talk to a PM";
|
|
293
|
+
el.innerHTML = `<div class="acw-reason"><strong>${escapeHtml(rec.track || rec.program || understood?.matchedTrackName || "Matched track")}</strong></div><div class="acw-role">${escapeHtml(rec.classification)} · ${escapeHtml(rec.eventType || "")} · ${escapeHtml(rec.format || "")}</div>` + (reasons ? `<ul style="margin:6px 0;padding-left:18px;font-size:12px;">${reasons}</ul>` : "") + `<button class="acw-route" type="button" style="margin-top:8px;border:none;border-radius:8px;padding:6px 12px;cursor:pointer;background:var(--acw-accent);color:var(--acw-accent-fg);font-size:13px;">${routeLabel}</button>`;
|
|
294
|
+
this.body.appendChild(el);
|
|
295
|
+
const btn = el.querySelector(".acw-route");
|
|
296
|
+
btn?.addEventListener("click", () => {
|
|
297
|
+
if (standard && deepLink) {
|
|
298
|
+
this.navigate(deepLink);
|
|
299
|
+
} else {
|
|
300
|
+
const m = document.createElement("div");
|
|
301
|
+
m.className = "acw-progress";
|
|
302
|
+
m.textContent = "Connecting you with a PM for qualification\u2026";
|
|
303
|
+
this.body.appendChild(m);
|
|
304
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
308
|
+
}
|
|
309
|
+
addMessage(text, who) {
|
|
310
|
+
const el = document.createElement("div");
|
|
311
|
+
el.className = `acw-msg acw-${who}`;
|
|
312
|
+
el.textContent = text;
|
|
313
|
+
this.body.appendChild(el);
|
|
314
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
315
|
+
}
|
|
316
|
+
setProgress(text) {
|
|
317
|
+
let p = this.body.querySelector(".acw-progress");
|
|
318
|
+
if (!text) {
|
|
319
|
+
p?.remove();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!p) {
|
|
323
|
+
p = document.createElement("div");
|
|
324
|
+
p.className = "acw-progress";
|
|
325
|
+
this.body.appendChild(p);
|
|
326
|
+
}
|
|
327
|
+
p.textContent = text;
|
|
328
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
329
|
+
}
|
|
330
|
+
renderAnswer(answer) {
|
|
331
|
+
this.setProgress("");
|
|
332
|
+
const el = document.createElement("div");
|
|
333
|
+
const isFailure = answer.status && answer.status !== "Answered";
|
|
334
|
+
if (isFailure) {
|
|
335
|
+
el.className = "acw-answer";
|
|
336
|
+
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
337
|
+
} else {
|
|
338
|
+
el.className = "acw-answer " + (answer.disabled ? "acw-blocked" : "acw-okstate");
|
|
339
|
+
const evidence = (answer.evidenceRefs ?? []).map(escapeHtml).join("<br>");
|
|
340
|
+
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
341
|
+
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
|
+
}
|
|
343
|
+
this.body.appendChild(el);
|
|
344
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
345
|
+
}
|
|
346
|
+
// ---- networking ----
|
|
347
|
+
headers() {
|
|
348
|
+
const h = { "Content-Type": "application/json" };
|
|
349
|
+
const token = this.config.getAuthToken?.();
|
|
350
|
+
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
351
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
352
|
+
else if (email) h["X-User-Email"] = email;
|
|
353
|
+
return h;
|
|
354
|
+
}
|
|
355
|
+
async ensureSession() {
|
|
356
|
+
if (this.sessionId) return this.sessionId;
|
|
357
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: this.headers(),
|
|
360
|
+
body: JSON.stringify({ projectKey: this.config.projectKey, environment: this.config.environment })
|
|
361
|
+
});
|
|
362
|
+
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
363
|
+
const session = await res.json();
|
|
364
|
+
this.sessionId = session.sessionId;
|
|
365
|
+
if (this.connection && this.sessionId) {
|
|
366
|
+
try {
|
|
367
|
+
await this.connection.invoke("JoinSession", this.sessionId);
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return this.sessionId;
|
|
372
|
+
}
|
|
373
|
+
async runAsk(question) {
|
|
374
|
+
this.open();
|
|
375
|
+
this.addMessage(question, "user");
|
|
376
|
+
this.setProgress("Reading your question\u2026");
|
|
377
|
+
try {
|
|
378
|
+
const sessionId = await this.ensureSession();
|
|
379
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/messages`, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: this.headers(),
|
|
382
|
+
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName, context: this.currentContext() })
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok) {
|
|
385
|
+
this.setProgress("");
|
|
386
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
this.renderAnswer(await res.json());
|
|
390
|
+
} catch (err) {
|
|
391
|
+
this.setProgress("");
|
|
392
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async connectSignalR() {
|
|
396
|
+
if (!this.config.signalRHubUrl) return;
|
|
397
|
+
const signalR = await import("@microsoft/signalr");
|
|
398
|
+
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl).withAutomaticReconnect().build();
|
|
399
|
+
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
400
|
+
connection.on(event, () => {
|
|
401
|
+
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
await connection.start();
|
|
405
|
+
this.connection = connection;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
function escapeHtml(s) {
|
|
409
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/index.ts
|
|
413
|
+
var instance = new CopilotWidget();
|
|
414
|
+
var ApplicationCopilotWidget = {
|
|
415
|
+
init: (config) => instance.init(config),
|
|
416
|
+
open: () => instance.open(),
|
|
417
|
+
close: () => instance.close(),
|
|
418
|
+
ask: (question) => instance.ask(question),
|
|
419
|
+
setContext: (context) => instance.setContext(context),
|
|
420
|
+
captureError: (errorContext) => instance.captureError(errorContext)
|
|
421
|
+
};
|
|
422
|
+
if (typeof window !== "undefined") {
|
|
423
|
+
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|
|
424
|
+
}
|
|
425
|
+
var index_default = ApplicationCopilotWidget;
|
|
426
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
427
|
+
0 && (module.exports = {
|
|
428
|
+
ApplicationCopilotWidget
|
|
429
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type Environment = 'qa' | 'stage';
|
|
2
|
+
interface ApplicationContext {
|
|
3
|
+
projectKey: string;
|
|
4
|
+
environment: Environment;
|
|
5
|
+
page?: {
|
|
6
|
+
pageKey?: string;
|
|
7
|
+
route?: string;
|
|
8
|
+
activeTab?: string;
|
|
9
|
+
componentHint?: string;
|
|
10
|
+
};
|
|
11
|
+
entities?: Record<string, string | number | boolean | null>;
|
|
12
|
+
uiState?: {
|
|
13
|
+
disabledFields?: string[];
|
|
14
|
+
hiddenControls?: string[];
|
|
15
|
+
visibleControls?: string[];
|
|
16
|
+
};
|
|
17
|
+
/** Hint only — the backend verifies security-relevant facts (e.g. role). */
|
|
18
|
+
clientSnapshot?: Record<string, unknown>;
|
|
19
|
+
recentError?: {
|
|
20
|
+
endpoint: string;
|
|
21
|
+
method: string;
|
|
22
|
+
statusCode: number;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
correlationId?: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
};
|
|
27
|
+
contextVersion?: number;
|
|
28
|
+
capturedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
interface WidgetConfig {
|
|
31
|
+
projectKey: string;
|
|
32
|
+
environment: Environment;
|
|
33
|
+
/** Base URL of Spektra.ApplicationCopilot.Api, e.g. https://localhost:5001 */
|
|
34
|
+
apiBaseUrl: string;
|
|
35
|
+
/** Optional SignalR hub URL for live progress, e.g. `${apiBaseUrl}/hubs/application-copilot` */
|
|
36
|
+
signalRHubUrl?: string;
|
|
37
|
+
/** Partner/tenant unique name — required to resolve the user's role server-side. */
|
|
38
|
+
tenantUniqueName: string;
|
|
39
|
+
/** Executed on every question to capture the current page context. */
|
|
40
|
+
getContext: () => ApplicationContext;
|
|
41
|
+
/** Returns the app access token; sent as `Authorization: Bearer <token>`. */
|
|
42
|
+
getAuthToken?: () => string | undefined;
|
|
43
|
+
/** Local-testing fallback only — sent as `X-User-Email`. May be a value or a getter (read per request). Do not use in production. */
|
|
44
|
+
devUserEmail?: string | (() => string | undefined);
|
|
45
|
+
/** Launcher button label. Defaults to "Ask Application Copilot". */
|
|
46
|
+
buttonText?: string;
|
|
47
|
+
/** Host navigation hook (e.g. Angular router). If omitted, the widget falls back to window.location. */
|
|
48
|
+
onNavigate?: (path: string) => void;
|
|
49
|
+
}
|
|
50
|
+
interface CopilotAnswer {
|
|
51
|
+
status: string;
|
|
52
|
+
ruleId?: string;
|
|
53
|
+
disabled?: boolean | null;
|
|
54
|
+
primaryReason?: string;
|
|
55
|
+
blockingConditions?: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
passed: boolean;
|
|
58
|
+
detail?: string;
|
|
59
|
+
}>;
|
|
60
|
+
verifiedRoleLabel?: string | null;
|
|
61
|
+
evidenceRefs?: string[];
|
|
62
|
+
source?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Public API. Framework-agnostic and CSS-library agnostic (Shadow DOM isolated),
|
|
67
|
+
* so it renders identically inside Bootstrap, Material, Tailwind, or unstyled hosts.
|
|
68
|
+
*/
|
|
69
|
+
declare const ApplicationCopilotWidget: {
|
|
70
|
+
init: (config: WidgetConfig) => void;
|
|
71
|
+
open: () => void;
|
|
72
|
+
close: () => void;
|
|
73
|
+
ask: (question: string) => void;
|
|
74
|
+
setContext: (context: ApplicationContext) => void;
|
|
75
|
+
captureError: (errorContext: ApplicationContext["recentError"]) => void;
|
|
76
|
+
};
|
|
77
|
+
declare global {
|
|
78
|
+
interface Window {
|
|
79
|
+
ApplicationCopilotWidget?: typeof ApplicationCopilotWidget;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { type ApplicationContext, ApplicationCopilotWidget, type CopilotAnswer, type Environment, type WidgetConfig, ApplicationCopilotWidget as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type Environment = 'qa' | 'stage';
|
|
2
|
+
interface ApplicationContext {
|
|
3
|
+
projectKey: string;
|
|
4
|
+
environment: Environment;
|
|
5
|
+
page?: {
|
|
6
|
+
pageKey?: string;
|
|
7
|
+
route?: string;
|
|
8
|
+
activeTab?: string;
|
|
9
|
+
componentHint?: string;
|
|
10
|
+
};
|
|
11
|
+
entities?: Record<string, string | number | boolean | null>;
|
|
12
|
+
uiState?: {
|
|
13
|
+
disabledFields?: string[];
|
|
14
|
+
hiddenControls?: string[];
|
|
15
|
+
visibleControls?: string[];
|
|
16
|
+
};
|
|
17
|
+
/** Hint only — the backend verifies security-relevant facts (e.g. role). */
|
|
18
|
+
clientSnapshot?: Record<string, unknown>;
|
|
19
|
+
recentError?: {
|
|
20
|
+
endpoint: string;
|
|
21
|
+
method: string;
|
|
22
|
+
statusCode: number;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
correlationId?: string;
|
|
25
|
+
timestamp: string;
|
|
26
|
+
};
|
|
27
|
+
contextVersion?: number;
|
|
28
|
+
capturedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
interface WidgetConfig {
|
|
31
|
+
projectKey: string;
|
|
32
|
+
environment: Environment;
|
|
33
|
+
/** Base URL of Spektra.ApplicationCopilot.Api, e.g. https://localhost:5001 */
|
|
34
|
+
apiBaseUrl: string;
|
|
35
|
+
/** Optional SignalR hub URL for live progress, e.g. `${apiBaseUrl}/hubs/application-copilot` */
|
|
36
|
+
signalRHubUrl?: string;
|
|
37
|
+
/** Partner/tenant unique name — required to resolve the user's role server-side. */
|
|
38
|
+
tenantUniqueName: string;
|
|
39
|
+
/** Executed on every question to capture the current page context. */
|
|
40
|
+
getContext: () => ApplicationContext;
|
|
41
|
+
/** Returns the app access token; sent as `Authorization: Bearer <token>`. */
|
|
42
|
+
getAuthToken?: () => string | undefined;
|
|
43
|
+
/** Local-testing fallback only — sent as `X-User-Email`. May be a value or a getter (read per request). Do not use in production. */
|
|
44
|
+
devUserEmail?: string | (() => string | undefined);
|
|
45
|
+
/** Launcher button label. Defaults to "Ask Application Copilot". */
|
|
46
|
+
buttonText?: string;
|
|
47
|
+
/** Host navigation hook (e.g. Angular router). If omitted, the widget falls back to window.location. */
|
|
48
|
+
onNavigate?: (path: string) => void;
|
|
49
|
+
}
|
|
50
|
+
interface CopilotAnswer {
|
|
51
|
+
status: string;
|
|
52
|
+
ruleId?: string;
|
|
53
|
+
disabled?: boolean | null;
|
|
54
|
+
primaryReason?: string;
|
|
55
|
+
blockingConditions?: Array<{
|
|
56
|
+
id: string;
|
|
57
|
+
passed: boolean;
|
|
58
|
+
detail?: string;
|
|
59
|
+
}>;
|
|
60
|
+
verifiedRoleLabel?: string | null;
|
|
61
|
+
evidenceRefs?: string[];
|
|
62
|
+
source?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Public API. Framework-agnostic and CSS-library agnostic (Shadow DOM isolated),
|
|
67
|
+
* so it renders identically inside Bootstrap, Material, Tailwind, or unstyled hosts.
|
|
68
|
+
*/
|
|
69
|
+
declare const ApplicationCopilotWidget: {
|
|
70
|
+
init: (config: WidgetConfig) => void;
|
|
71
|
+
open: () => void;
|
|
72
|
+
close: () => void;
|
|
73
|
+
ask: (question: string) => void;
|
|
74
|
+
setContext: (context: ApplicationContext) => void;
|
|
75
|
+
captureError: (errorContext: ApplicationContext["recentError"]) => void;
|
|
76
|
+
};
|
|
77
|
+
declare global {
|
|
78
|
+
interface Window {
|
|
79
|
+
ApplicationCopilotWidget?: typeof ApplicationCopilotWidget;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { type ApplicationContext, ApplicationCopilotWidget, type CopilotAnswer, type Environment, type WidgetConfig, ApplicationCopilotWidget as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// src/styles.ts
|
|
2
|
+
var widgetStyles = `
|
|
3
|
+
:host {
|
|
4
|
+
all: initial;
|
|
5
|
+
font-family: "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif;
|
|
6
|
+
--acw-bg: #ffffff;
|
|
7
|
+
--acw-surface: #f4f5fb;
|
|
8
|
+
--acw-fg: #1b1f3b;
|
|
9
|
+
--acw-muted: #5a6072;
|
|
10
|
+
--acw-border: #e3e6f0;
|
|
11
|
+
--acw-accent: #5b3df5;
|
|
12
|
+
--acw-accent-dark: #4a2fe0;
|
|
13
|
+
--acw-accent-2: #8b5cf6;
|
|
14
|
+
--acw-accent-fg: #ffffff;
|
|
15
|
+
--acw-danger: #c0362c;
|
|
16
|
+
--acw-ok: #1f9d6b;
|
|
17
|
+
}
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
|
|
20
|
+
/* Launcher: AI icon + hover tooltip */
|
|
21
|
+
.acw-launch-wrap { position: fixed; right: 24px; bottom: 24px; z-index: 2147483000; }
|
|
22
|
+
.acw-launch {
|
|
23
|
+
width: 60px; height: 60px; border: none; border-radius: 50%;
|
|
24
|
+
background: linear-gradient(135deg, var(--acw-accent), var(--acw-accent-2));
|
|
25
|
+
color: var(--acw-accent-fg); cursor: pointer;
|
|
26
|
+
display: flex; align-items: center; justify-content: center;
|
|
27
|
+
box-shadow: 0 8px 24px rgba(91,61,245,0.45);
|
|
28
|
+
transition: transform .15s ease, box-shadow .15s ease;
|
|
29
|
+
}
|
|
30
|
+
.acw-launch:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 12px 30px rgba(91,61,245,0.55); }
|
|
31
|
+
.acw-launch svg { width: 30px; height: 30px; }
|
|
32
|
+
.acw-tip {
|
|
33
|
+
position: absolute; right: 72px; bottom: 17px; white-space: nowrap;
|
|
34
|
+
background: #1b1f3b; color: #fff; font-size: 13px; padding: 7px 12px;
|
|
35
|
+
border-radius: 8px; opacity: 0; pointer-events: none; transition: opacity .15s ease;
|
|
36
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.25);
|
|
37
|
+
}
|
|
38
|
+
.acw-launch-wrap:hover .acw-tip { opacity: 1; }
|
|
39
|
+
|
|
40
|
+
/* Panel */
|
|
41
|
+
.acw-panel {
|
|
42
|
+
position: fixed; right: 24px; bottom: 96px; z-index: 2147483000;
|
|
43
|
+
width: 410px; max-width: calc(100vw - 48px); height: 580px; max-height: calc(100vh - 130px);
|
|
44
|
+
display: none; flex-direction: column;
|
|
45
|
+
background: var(--acw-bg); color: var(--acw-fg);
|
|
46
|
+
border: 1px solid var(--acw-border); border-radius: 16px; overflow: hidden;
|
|
47
|
+
box-shadow: 0 18px 50px rgba(27,31,59,0.28);
|
|
48
|
+
}
|
|
49
|
+
.acw-panel.acw-open { display: flex; }
|
|
50
|
+
|
|
51
|
+
.acw-header {
|
|
52
|
+
display: flex; align-items: center; gap: 10px;
|
|
53
|
+
padding: 14px 16px;
|
|
54
|
+
background: linear-gradient(135deg, var(--acw-accent), var(--acw-accent-2));
|
|
55
|
+
color: #fff;
|
|
56
|
+
}
|
|
57
|
+
.acw-header svg { width: 22px; height: 22px; flex: none; }
|
|
58
|
+
.acw-title { font-size: 16px; font-weight: 600; }
|
|
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
|
+
.acw-close:hover { color: #fff; }
|
|
61
|
+
|
|
62
|
+
.acw-body { flex: 1; overflow-y: auto; padding: 16px; background: var(--acw-surface); }
|
|
63
|
+
.acw-hint { font-size: 12px; color: var(--acw-muted); margin: 0 0 10px; }
|
|
64
|
+
|
|
65
|
+
.acw-chip {
|
|
66
|
+
display: block; width: 100%; text-align: left;
|
|
67
|
+
padding: 11px 13px; margin-bottom: 8px;
|
|
68
|
+
border: 1px solid var(--acw-border); border-radius: 10px;
|
|
69
|
+
background: var(--acw-bg); color: var(--acw-fg); font-size: 14px; cursor: pointer;
|
|
70
|
+
font-family: inherit;
|
|
71
|
+
transition: border-color .15s ease, background .15s ease;
|
|
72
|
+
}
|
|
73
|
+
.acw-chip:hover { background: #eceafe; border-color: var(--acw-accent); }
|
|
74
|
+
|
|
75
|
+
.acw-msg { font-size: 14px; margin: 12px 0; }
|
|
76
|
+
.acw-msg.acw-user {
|
|
77
|
+
text-align: right; color: var(--acw-fg);
|
|
78
|
+
}
|
|
79
|
+
.acw-msg.acw-user span, .acw-msg.acw-user {
|
|
80
|
+
display: inline-block;
|
|
81
|
+
}
|
|
82
|
+
.acw-progress { font-size: 13px; color: var(--acw-muted); margin: 10px 0; }
|
|
83
|
+
|
|
84
|
+
.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-blocked { border-left: 4px solid var(--acw-danger); }
|
|
87
|
+
.acw-answer.acw-okstate { border-left: 4px solid var(--acw-ok); }
|
|
88
|
+
.acw-role { font-size: 12px; color: var(--acw-muted); margin-top: 8px; }
|
|
89
|
+
.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
|
+
|
|
92
|
+
.acw-footer { display: flex; gap: 8px; padding: 12px 14px; border-top: 1px solid var(--acw-border); background: var(--acw-bg); }
|
|
93
|
+
.acw-input {
|
|
94
|
+
flex: 1; padding: 10px 12px; font-size: 14px;
|
|
95
|
+
border: 1px solid var(--acw-border); border-radius: 10px; color: var(--acw-fg); background: var(--acw-bg);
|
|
96
|
+
font-family: inherit;
|
|
97
|
+
}
|
|
98
|
+
.acw-input:focus { outline: 2px solid var(--acw-accent); outline-offset: 0; }
|
|
99
|
+
.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
|
+
.acw-send:hover { background: var(--acw-accent-dark); }
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
// src/widget.ts
|
|
104
|
+
var PROGRESS_LABELS = {
|
|
105
|
+
QuestionAccepted: "Reading your question\u2026",
|
|
106
|
+
PermissionsVerified: "Verifying your permissions\u2026",
|
|
107
|
+
RuleEvaluated: "Evaluating the rule\u2026",
|
|
108
|
+
AnswerCompleted: ""
|
|
109
|
+
};
|
|
110
|
+
var CopilotWidget = class {
|
|
111
|
+
constructor() {
|
|
112
|
+
this.mode = "explain";
|
|
113
|
+
}
|
|
114
|
+
init(config) {
|
|
115
|
+
this.config = config;
|
|
116
|
+
this.mount();
|
|
117
|
+
this.connectSignalR().catch(() => {
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
open() {
|
|
121
|
+
this.panel?.classList.add("acw-open");
|
|
122
|
+
this.refreshChips();
|
|
123
|
+
}
|
|
124
|
+
close() {
|
|
125
|
+
this.panel?.classList.remove("acw-open");
|
|
126
|
+
}
|
|
127
|
+
setContext(context) {
|
|
128
|
+
this.contextOverride = context;
|
|
129
|
+
}
|
|
130
|
+
captureError(errorContext) {
|
|
131
|
+
this.recentError = errorContext;
|
|
132
|
+
this.open();
|
|
133
|
+
}
|
|
134
|
+
ask(question) {
|
|
135
|
+
if (this.mode === "intake") {
|
|
136
|
+
void this.runIntakeAsk(question);
|
|
137
|
+
} else {
|
|
138
|
+
void this.runAsk(question);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---- rendering (all inside Shadow DOM) ----
|
|
142
|
+
mount() {
|
|
143
|
+
if (this.host) return;
|
|
144
|
+
this.host = document.createElement("div");
|
|
145
|
+
this.host.setAttribute("data-spektra-copilot", "");
|
|
146
|
+
document.body.appendChild(this.host);
|
|
147
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
148
|
+
const style = document.createElement("style");
|
|
149
|
+
style.textContent = widgetStyles;
|
|
150
|
+
this.root.appendChild(style);
|
|
151
|
+
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>';
|
|
152
|
+
const tip = this.config.buttonText ?? "Ask Application Copilot";
|
|
153
|
+
const wrap = document.createElement("div");
|
|
154
|
+
wrap.className = "acw-launch-wrap";
|
|
155
|
+
wrap.innerHTML = `<span class="acw-tip">${tip}</span><button class="acw-launch" type="button" aria-label="${tip}">${sparkle}</button>`;
|
|
156
|
+
this.root.appendChild(wrap);
|
|
157
|
+
wrap.querySelector(".acw-launch").addEventListener("click", () => this.panel?.classList.contains("acw-open") ? this.close() : this.open());
|
|
158
|
+
const panel = document.createElement("div");
|
|
159
|
+
panel.className = "acw-panel";
|
|
160
|
+
panel.innerHTML = `
|
|
161
|
+
<div class="acw-header">
|
|
162
|
+
${sparkle}
|
|
163
|
+
<span class="acw-title">Application Copilot</span>
|
|
164
|
+
<button class="acw-close" type="button" aria-label="Close">×</button>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="acw-body"></div>
|
|
167
|
+
<div class="acw-footer">
|
|
168
|
+
<input class="acw-input" type="text" placeholder="Ask a question\u2026" />
|
|
169
|
+
<button class="acw-send" type="button">Send</button>
|
|
170
|
+
</div>`;
|
|
171
|
+
this.root.appendChild(panel);
|
|
172
|
+
this.panel = panel;
|
|
173
|
+
this.body = panel.querySelector(".acw-body");
|
|
174
|
+
this.input = panel.querySelector(".acw-input");
|
|
175
|
+
panel.querySelector(".acw-close").addEventListener("click", () => this.close());
|
|
176
|
+
panel.querySelector(".acw-send").addEventListener("click", () => this.submitFromInput());
|
|
177
|
+
this.input.addEventListener("keydown", (e) => {
|
|
178
|
+
if (e.key === "Enter") this.submitFromInput();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
submitFromInput() {
|
|
182
|
+
const q = this.input?.value.trim();
|
|
183
|
+
if (q) {
|
|
184
|
+
this.input.value = "";
|
|
185
|
+
this.ask(q);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
navigate(path) {
|
|
189
|
+
if (this.config.onNavigate) {
|
|
190
|
+
this.config.onNavigate(path);
|
|
191
|
+
this.close();
|
|
192
|
+
} else {
|
|
193
|
+
window.location.href = path;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
currentContext() {
|
|
197
|
+
const ctx = this.contextOverride ?? this.config.getContext();
|
|
198
|
+
if (this.recentError) ctx.recentError = this.recentError;
|
|
199
|
+
return ctx;
|
|
200
|
+
}
|
|
201
|
+
refreshChips() {
|
|
202
|
+
if (!this.body || this.body.childElementCount > 0) return;
|
|
203
|
+
const greeting = document.createElement("p");
|
|
204
|
+
greeting.className = "acw-hint";
|
|
205
|
+
greeting.textContent = "Ask me anything about this page \u2014 for example, why a field is disabled, or why you can\u2019t update, reschedule, or submit something.";
|
|
206
|
+
this.body.appendChild(greeting);
|
|
207
|
+
}
|
|
208
|
+
enterIntakeMode() {
|
|
209
|
+
this.mode = "intake";
|
|
210
|
+
if (this.input) this.input.placeholder = "Describe the event you want to run\u2026";
|
|
211
|
+
const hint = document.createElement("div");
|
|
212
|
+
hint.className = "acw-progress";
|
|
213
|
+
hint.textContent = "Guided intake \u2014 tell me the topic, attendee count, format, and rough date.";
|
|
214
|
+
this.body.appendChild(hint);
|
|
215
|
+
this.input?.focus();
|
|
216
|
+
}
|
|
217
|
+
async runIntakeAsk(question) {
|
|
218
|
+
this.open();
|
|
219
|
+
this.addMessage(question, "user");
|
|
220
|
+
this.setProgress("Finding the right offering\u2026");
|
|
221
|
+
try {
|
|
222
|
+
const sessionId = await this.ensureSession();
|
|
223
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/intake/ask`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: this.headers(),
|
|
226
|
+
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName })
|
|
227
|
+
});
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
this.setProgress("");
|
|
230
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.renderIntakeAnswer(await res.json());
|
|
234
|
+
} catch {
|
|
235
|
+
this.setProgress("");
|
|
236
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
renderIntakeAnswer(data) {
|
|
240
|
+
this.setProgress("");
|
|
241
|
+
const rec = data?.recommendation;
|
|
242
|
+
const understood = data?.understood;
|
|
243
|
+
const el = document.createElement("div");
|
|
244
|
+
if (!rec) {
|
|
245
|
+
el.className = "acw-answer";
|
|
246
|
+
el.innerHTML = `<div class="acw-reason">${escapeHtml(data?.note || "Could not match an offering to your request.")}</div>`;
|
|
247
|
+
this.body.appendChild(el);
|
|
248
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const standard = rec.classification === "Standard";
|
|
252
|
+
const deepLink = data?.deepLink;
|
|
253
|
+
el.className = "acw-answer " + (standard ? "acw-okstate" : "acw-blocked");
|
|
254
|
+
const reasons = (rec.reasons || []).map((r) => `<li>${escapeHtml(r)}</li>`).join("");
|
|
255
|
+
const routeLabel = standard ? "Open this track to submit" : "Talk to a PM";
|
|
256
|
+
el.innerHTML = `<div class="acw-reason"><strong>${escapeHtml(rec.track || rec.program || understood?.matchedTrackName || "Matched track")}</strong></div><div class="acw-role">${escapeHtml(rec.classification)} · ${escapeHtml(rec.eventType || "")} · ${escapeHtml(rec.format || "")}</div>` + (reasons ? `<ul style="margin:6px 0;padding-left:18px;font-size:12px;">${reasons}</ul>` : "") + `<button class="acw-route" type="button" style="margin-top:8px;border:none;border-radius:8px;padding:6px 12px;cursor:pointer;background:var(--acw-accent);color:var(--acw-accent-fg);font-size:13px;">${routeLabel}</button>`;
|
|
257
|
+
this.body.appendChild(el);
|
|
258
|
+
const btn = el.querySelector(".acw-route");
|
|
259
|
+
btn?.addEventListener("click", () => {
|
|
260
|
+
if (standard && deepLink) {
|
|
261
|
+
this.navigate(deepLink);
|
|
262
|
+
} else {
|
|
263
|
+
const m = document.createElement("div");
|
|
264
|
+
m.className = "acw-progress";
|
|
265
|
+
m.textContent = "Connecting you with a PM for qualification\u2026";
|
|
266
|
+
this.body.appendChild(m);
|
|
267
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
271
|
+
}
|
|
272
|
+
addMessage(text, who) {
|
|
273
|
+
const el = document.createElement("div");
|
|
274
|
+
el.className = `acw-msg acw-${who}`;
|
|
275
|
+
el.textContent = text;
|
|
276
|
+
this.body.appendChild(el);
|
|
277
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
278
|
+
}
|
|
279
|
+
setProgress(text) {
|
|
280
|
+
let p = this.body.querySelector(".acw-progress");
|
|
281
|
+
if (!text) {
|
|
282
|
+
p?.remove();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!p) {
|
|
286
|
+
p = document.createElement("div");
|
|
287
|
+
p.className = "acw-progress";
|
|
288
|
+
this.body.appendChild(p);
|
|
289
|
+
}
|
|
290
|
+
p.textContent = text;
|
|
291
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
292
|
+
}
|
|
293
|
+
renderAnswer(answer) {
|
|
294
|
+
this.setProgress("");
|
|
295
|
+
const el = document.createElement("div");
|
|
296
|
+
const isFailure = answer.status && answer.status !== "Answered";
|
|
297
|
+
if (isFailure) {
|
|
298
|
+
el.className = "acw-answer";
|
|
299
|
+
el.innerHTML = `<div class="acw-error">${escapeHtml(answer.primaryReason || answer.status)}</div>`;
|
|
300
|
+
} else {
|
|
301
|
+
el.className = "acw-answer " + (answer.disabled ? "acw-blocked" : "acw-okstate");
|
|
302
|
+
const evidence = (answer.evidenceRefs ?? []).map(escapeHtml).join("<br>");
|
|
303
|
+
const sourceLabel = answer.source === "code" ? "App rules" : answer.source === "database" ? "Live data" : answer.source === "policy" ? "SharePoint docs" : "";
|
|
304
|
+
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
|
+
}
|
|
306
|
+
this.body.appendChild(el);
|
|
307
|
+
this.body.scrollTop = this.body.scrollHeight;
|
|
308
|
+
}
|
|
309
|
+
// ---- networking ----
|
|
310
|
+
headers() {
|
|
311
|
+
const h = { "Content-Type": "application/json" };
|
|
312
|
+
const token = this.config.getAuthToken?.();
|
|
313
|
+
const email = typeof this.config.devUserEmail === "function" ? this.config.devUserEmail() : this.config.devUserEmail;
|
|
314
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
315
|
+
else if (email) h["X-User-Email"] = email;
|
|
316
|
+
return h;
|
|
317
|
+
}
|
|
318
|
+
async ensureSession() {
|
|
319
|
+
if (this.sessionId) return this.sessionId;
|
|
320
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions`, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: this.headers(),
|
|
323
|
+
body: JSON.stringify({ projectKey: this.config.projectKey, environment: this.config.environment })
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok) throw new Error(`Session creation failed (${res.status})`);
|
|
326
|
+
const session = await res.json();
|
|
327
|
+
this.sessionId = session.sessionId;
|
|
328
|
+
if (this.connection && this.sessionId) {
|
|
329
|
+
try {
|
|
330
|
+
await this.connection.invoke("JoinSession", this.sessionId);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return this.sessionId;
|
|
335
|
+
}
|
|
336
|
+
async runAsk(question) {
|
|
337
|
+
this.open();
|
|
338
|
+
this.addMessage(question, "user");
|
|
339
|
+
this.setProgress("Reading your question\u2026");
|
|
340
|
+
try {
|
|
341
|
+
const sessionId = await this.ensureSession();
|
|
342
|
+
const res = await fetch(`${this.config.apiBaseUrl}/api/sessions/${sessionId}/messages`, {
|
|
343
|
+
method: "POST",
|
|
344
|
+
headers: this.headers(),
|
|
345
|
+
body: JSON.stringify({ question, tenantUniqueName: this.config.tenantUniqueName, context: this.currentContext() })
|
|
346
|
+
});
|
|
347
|
+
if (!res.ok) {
|
|
348
|
+
this.setProgress("");
|
|
349
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: `The copilot service returned ${res.status}.` });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.renderAnswer(await res.json());
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this.setProgress("");
|
|
355
|
+
this.renderAnswer({ status: "RequestFailed", primaryReason: "Could not reach the copilot service." });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async connectSignalR() {
|
|
359
|
+
if (!this.config.signalRHubUrl) return;
|
|
360
|
+
const signalR = await import("@microsoft/signalr");
|
|
361
|
+
const connection = new signalR.HubConnectionBuilder().withUrl(this.config.signalRHubUrl).withAutomaticReconnect().build();
|
|
362
|
+
for (const event of Object.keys(PROGRESS_LABELS)) {
|
|
363
|
+
connection.on(event, () => {
|
|
364
|
+
if (event !== "AnswerCompleted") this.setProgress(PROGRESS_LABELS[event]);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
await connection.start();
|
|
368
|
+
this.connection = connection;
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
function escapeHtml(s) {
|
|
372
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/index.ts
|
|
376
|
+
var instance = new CopilotWidget();
|
|
377
|
+
var ApplicationCopilotWidget = {
|
|
378
|
+
init: (config) => instance.init(config),
|
|
379
|
+
open: () => instance.open(),
|
|
380
|
+
close: () => instance.close(),
|
|
381
|
+
ask: (question) => instance.ask(question),
|
|
382
|
+
setContext: (context) => instance.setContext(context),
|
|
383
|
+
captureError: (errorContext) => instance.captureError(errorContext)
|
|
384
|
+
};
|
|
385
|
+
if (typeof window !== "undefined") {
|
|
386
|
+
window.ApplicationCopilotWidget = ApplicationCopilotWidget;
|
|
387
|
+
}
|
|
388
|
+
var index_default = ApplicationCopilotWidget;
|
|
389
|
+
export {
|
|
390
|
+
ApplicationCopilotWidget,
|
|
391
|
+
index_default as default
|
|
392
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spektra-cloudevents/application-copilot-widget",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Spektra Application Copilot widget — framework-agnostic and CSS-library agnostic via Shadow DOM isolation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
24
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@microsoft/signalr": "^8.0.7"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"tsup": "^8.3.0",
|
|
32
|
+
"typescript": "^5.5.4"
|
|
33
|
+
}
|
|
34
|
+
}
|