cap-copilot-widget 0.1.0 → 0.1.2
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/btp-copilot.esm.js +675 -275
- package/dist/btp-copilot.esm.js.map +1 -1
- package/dist/btp-copilot.js +15 -10
- package/dist/btp-copilot.js.map +1 -1
- package/package.json +3 -3
package/dist/btp-copilot.esm.js
CHANGED
|
@@ -1,108 +1,114 @@
|
|
|
1
|
-
const
|
|
2
|
-
static resolve(
|
|
3
|
-
if (t) return t.replace(/^Bearer\s+/i, "");
|
|
4
|
-
const e = window.__BTP_COPILOT_TOKEN__;
|
|
1
|
+
const A = class A {
|
|
2
|
+
static resolve(e) {
|
|
5
3
|
if (e) return e.replace(/^Bearer\s+/i, "");
|
|
6
|
-
const
|
|
4
|
+
const t = window.__BTP_COPILOT_TOKEN__;
|
|
5
|
+
if (t) return t.replace(/^Bearer\s+/i, "");
|
|
6
|
+
const n = A._fromCookie();
|
|
7
7
|
if (n) return n;
|
|
8
8
|
try {
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
9
|
+
const r = localStorage.getItem("access_token");
|
|
10
|
+
if (r) return r;
|
|
11
11
|
} catch {
|
|
12
12
|
}
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
|
-
static getAuthHeader(
|
|
16
|
-
return
|
|
15
|
+
static getAuthHeader(e) {
|
|
16
|
+
return e ? { Authorization: `Bearer ${e}` } : {};
|
|
17
17
|
}
|
|
18
18
|
static _fromCookie() {
|
|
19
19
|
try {
|
|
20
|
-
const
|
|
21
|
-
for (const
|
|
22
|
-
const [n, ...
|
|
23
|
-
if (
|
|
24
|
-
return decodeURIComponent(
|
|
20
|
+
const e = document.cookie.split(";");
|
|
21
|
+
for (const t of e) {
|
|
22
|
+
const [n, ...r] = t.trim().split("=");
|
|
23
|
+
if (A.XSUAA_COOKIE_NAMES.includes(n.trim()))
|
|
24
|
+
return decodeURIComponent(r.join("=").trim());
|
|
25
25
|
}
|
|
26
26
|
} catch {
|
|
27
27
|
}
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
A.XSUAA_COOKIE_NAMES = [
|
|
32
32
|
"x-uaa-token",
|
|
33
33
|
"xsuaa_token",
|
|
34
34
|
"access_token",
|
|
35
35
|
"X-Authorization"
|
|
36
36
|
];
|
|
37
|
-
let
|
|
38
|
-
class
|
|
37
|
+
let S = A;
|
|
38
|
+
class C {
|
|
39
39
|
static getHash() {
|
|
40
|
-
var
|
|
40
|
+
var e, t, n, r;
|
|
41
41
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
42
|
+
const a = (n = (t = (e = sap == null ? void 0 : sap.ushell) == null ? void 0 : e.Container) == null ? void 0 : t.getService) == null ? void 0 : n.call(
|
|
43
|
+
t,
|
|
44
44
|
"CrossApplicationNavigation"
|
|
45
45
|
);
|
|
46
|
-
if (
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
46
|
+
if (a) {
|
|
47
|
+
const i = (r = a.hrefForExternal) == null ? void 0 : r.call(a);
|
|
48
|
+
if (i) return i;
|
|
49
49
|
}
|
|
50
|
-
} catch (
|
|
51
|
-
console.log(
|
|
50
|
+
} catch (a) {
|
|
51
|
+
console.log(a);
|
|
52
52
|
}
|
|
53
53
|
return window.location.hash || "";
|
|
54
54
|
}
|
|
55
55
|
static getCurrentAppId() {
|
|
56
56
|
try {
|
|
57
|
-
const
|
|
58
|
-
return
|
|
57
|
+
const t = C.getHash().match(/^#([^&?/]+)/);
|
|
58
|
+
return t ? t[1] : null;
|
|
59
59
|
} catch {
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
static getUserLocale() {
|
|
64
|
-
var
|
|
64
|
+
var e, t, n, r, a, i;
|
|
65
65
|
try {
|
|
66
|
-
return ((
|
|
66
|
+
return ((i = (a = (r = (n = (t = (e = sap == null ? void 0 : sap.ui) == null ? void 0 : e.getCore) == null ? void 0 : t.call(e)) == null ? void 0 : n.getConfiguration) == null ? void 0 : r.call(n)) == null ? void 0 : a.getLanguageTag) == null ? void 0 : i.call(a)) ?? navigator.language ?? "en";
|
|
67
67
|
} catch {
|
|
68
68
|
return navigator.language ?? "en";
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
class
|
|
72
|
+
class _ {
|
|
73
73
|
static getCurrentEntity() {
|
|
74
|
-
var
|
|
74
|
+
var e, t, n, r, a;
|
|
75
75
|
try {
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
let
|
|
79
|
-
for (;
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
return
|
|
76
|
+
const i = (t = (e = sap == null ? void 0 : sap.ui) == null ? void 0 : e.getCore) == null ? void 0 : t.call(e);
|
|
77
|
+
if (!i) return null;
|
|
78
|
+
let o = document.activeElement;
|
|
79
|
+
for (; o; ) {
|
|
80
|
+
const l = o.getAttribute("id") || o.getAttribute("data-sap-ui");
|
|
81
|
+
if (l) {
|
|
82
|
+
const s = i.byId(l), c = (n = s == null ? void 0 : s.getBindingContext) == null ? void 0 : n.call(s);
|
|
83
|
+
if (c) {
|
|
84
|
+
const h = (r = c.getObject) == null ? void 0 : r.call(c);
|
|
85
|
+
if (h && typeof h == "object")
|
|
86
|
+
return _._withEntityName(
|
|
87
|
+
_._sanitize(h),
|
|
88
|
+
((a = c.getPath) == null ? void 0 : a.call(c)) ?? ""
|
|
89
|
+
);
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
|
-
|
|
92
|
+
o = o.parentElement;
|
|
90
93
|
}
|
|
91
|
-
return
|
|
94
|
+
return _._scanAllControls(i);
|
|
92
95
|
} catch {
|
|
93
96
|
return null;
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
|
-
static _scanAllControls(
|
|
97
|
-
var
|
|
99
|
+
static _scanAllControls(e) {
|
|
100
|
+
var t, n, r;
|
|
98
101
|
try {
|
|
99
|
-
const
|
|
100
|
-
for (const
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
103
|
-
const
|
|
104
|
-
if (
|
|
105
|
-
return
|
|
102
|
+
const a = e.mElements ?? {};
|
|
103
|
+
for (const i of Object.keys(a)) {
|
|
104
|
+
const o = a[i], l = (t = o == null ? void 0 : o.getBindingContext) == null ? void 0 : t.call(o);
|
|
105
|
+
if (l) {
|
|
106
|
+
const s = (n = l.getObject) == null ? void 0 : n.call(l);
|
|
107
|
+
if (s && typeof s == "object" && Object.keys(s).length > 0)
|
|
108
|
+
return _._withEntityName(
|
|
109
|
+
_._sanitize(s),
|
|
110
|
+
((r = l.getPath) == null ? void 0 : r.call(l)) ?? ""
|
|
111
|
+
);
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
} catch {
|
|
@@ -110,234 +116,588 @@ class u {
|
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
118
|
static getServiceUrl() {
|
|
113
|
-
var
|
|
119
|
+
var e, t;
|
|
114
120
|
try {
|
|
115
|
-
const n = (
|
|
121
|
+
const n = (t = (e = sap == null ? void 0 : sap.ui) == null ? void 0 : e.getCore) == null ? void 0 : t.call(e);
|
|
116
122
|
if (!n) return null;
|
|
117
|
-
const
|
|
118
|
-
for (const
|
|
119
|
-
const
|
|
120
|
-
if (!(
|
|
121
|
-
const
|
|
122
|
-
if (!
|
|
123
|
-
const
|
|
124
|
-
if (typeof
|
|
125
|
-
return
|
|
123
|
+
const r = n.mElements ?? {};
|
|
124
|
+
for (const a of Object.keys(r)) {
|
|
125
|
+
const i = r[a];
|
|
126
|
+
if (!(i != null && i.getModel)) continue;
|
|
127
|
+
const o = i.getModel();
|
|
128
|
+
if (!o) continue;
|
|
129
|
+
const l = o.sServiceUrl ?? o._sServiceUrl;
|
|
130
|
+
if (typeof l == "string" && l.length > 4)
|
|
131
|
+
return l.replace(/\/$/, "");
|
|
126
132
|
}
|
|
127
133
|
} catch {
|
|
128
134
|
}
|
|
129
135
|
return null;
|
|
130
136
|
}
|
|
131
|
-
static _sanitize(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
static _sanitize(e) {
|
|
138
|
+
var n;
|
|
139
|
+
const t = {};
|
|
140
|
+
for (const [r, a] of Object.entries(e))
|
|
141
|
+
if (!r.startsWith("__"))
|
|
142
|
+
if (a === null || typeof a != "object")
|
|
143
|
+
t[r] = a;
|
|
144
|
+
else if (a.__deferred) {
|
|
145
|
+
const i = (n = a.__deferred) == null ? void 0 : n.uri;
|
|
146
|
+
typeof i == "string" && i && (t[`${r}_navUri`] = i);
|
|
147
|
+
} else
|
|
148
|
+
t[r] = a;
|
|
149
|
+
return t;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Inject `_entity` into a sanitized binding context object by parsing the
|
|
153
|
+
* OData context path. The path looks like `/FertilizerBlend(orderID='2466',...)`
|
|
154
|
+
* or `/Farms('abc-123')` — the entity set name is the first path segment.
|
|
155
|
+
*/
|
|
156
|
+
static _withEntityName(e, t) {
|
|
157
|
+
if (e._entity) return e;
|
|
158
|
+
try {
|
|
159
|
+
const n = t.match(/^\/([A-Za-z_][A-Za-z0-9_]*)(?:\(|$|\/)/);
|
|
160
|
+
if (n != null && n[1]) return { _entity: n[1], ...e };
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
135
163
|
return e;
|
|
136
164
|
}
|
|
137
165
|
}
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
const m = class m {
|
|
167
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* Fetch and fully parse $metadata for a service URL.
|
|
170
|
+
* Result is memory-cached for the page lifetime and persisted to sessionStorage.
|
|
171
|
+
* Returns null on any network / parse failure.
|
|
172
|
+
*/
|
|
173
|
+
static async fetchMetadata(e, t) {
|
|
174
|
+
const n = `btp-copilot-meta-v2-${e}`;
|
|
175
|
+
if (m._metaCache.has(n))
|
|
176
|
+
return m._metaCache.get(n);
|
|
177
|
+
const r = sessionStorage.getItem(n);
|
|
178
|
+
if (r)
|
|
145
179
|
try {
|
|
146
|
-
const
|
|
147
|
-
return
|
|
180
|
+
const i = { ...JSON.parse(r), rawXml: "" };
|
|
181
|
+
return m._metaCache.set(n, i), i;
|
|
148
182
|
} catch {
|
|
149
183
|
}
|
|
150
184
|
try {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
if (!
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const i = new DOMParser().parseFromString(t, "text/xml").querySelectorAll("EntityType");
|
|
164
|
-
return Array.from(i).map((r) => {
|
|
165
|
-
const s = r.getAttribute("Name") ?? "", a = r.querySelectorAll("Key > PropertyRef"), o = new Set(
|
|
166
|
-
Array.from(a).map((h) => h.getAttribute("Name") ?? "")
|
|
167
|
-
), l = Array.from(
|
|
168
|
-
r.querySelectorAll("Property")
|
|
169
|
-
).map((h) => ({
|
|
170
|
-
name: h.getAttribute("Name") ?? "",
|
|
171
|
-
type: c._shortType(h.getAttribute("Type") ?? ""),
|
|
172
|
-
isKey: o.has(h.getAttribute("Name") ?? ""),
|
|
173
|
-
nullable: h.getAttribute("Nullable") !== "false"
|
|
174
|
-
})), p = Array.from(
|
|
175
|
-
r.querySelectorAll("NavigationProperty")
|
|
176
|
-
).map((h) => ({
|
|
177
|
-
name: h.getAttribute("Name") ?? "",
|
|
178
|
-
type: c._shortType(
|
|
179
|
-
h.getAttribute("Type") ?? h.getAttribute("ToRole") ?? ""
|
|
180
|
-
)
|
|
181
|
-
}));
|
|
182
|
-
return { name: s, properties: l, navProperties: p };
|
|
183
|
-
});
|
|
185
|
+
const a = `${e.replace(/\/$/, "")}/$metadata`, i = { Accept: "application/xml" };
|
|
186
|
+
t && (i.Authorization = `Bearer ${t}`);
|
|
187
|
+
const o = await fetch(a, { headers: i });
|
|
188
|
+
if (!o.ok) return null;
|
|
189
|
+
const l = await o.text(), s = m._parseEDMX(l, e);
|
|
190
|
+
m._metaCache.set(n, s);
|
|
191
|
+
try {
|
|
192
|
+
const { rawXml: c, ...h } = s;
|
|
193
|
+
sessionStorage.setItem(n, JSON.stringify(h));
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
return s;
|
|
184
197
|
} catch {
|
|
185
|
-
return
|
|
198
|
+
return null;
|
|
186
199
|
}
|
|
187
200
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Build schema hint documents from parsed metadata.
|
|
203
|
+
* Used to populate fiori_context.extra.schema_hint for each chat message.
|
|
204
|
+
*/
|
|
205
|
+
static buildSchemaDocuments(e, t) {
|
|
192
206
|
const n = [];
|
|
193
|
-
for (const
|
|
194
|
-
if (!
|
|
195
|
-
const r =
|
|
196
|
-
`Entity: ${
|
|
197
|
-
`
|
|
198
|
-
`
|
|
199
|
-
`
|
|
207
|
+
for (const r of e.entities) {
|
|
208
|
+
if (!r.name) continue;
|
|
209
|
+
const a = r.entitySetName ?? r.name, i = r.properties.filter((s) => s.isKey), o = r.properties.filter((s) => !s.isKey), l = [
|
|
210
|
+
`Entity: ${r.name}`,
|
|
211
|
+
`Entity set: ${a}`,
|
|
212
|
+
`OData service: ${t}`,
|
|
213
|
+
`Entity set path: ${t}/${a}`,
|
|
214
|
+
`Count path: ${t}/${a}/$count`
|
|
200
215
|
];
|
|
201
|
-
|
|
202
|
-
`Key fields: ${
|
|
203
|
-
),
|
|
204
|
-
`Fields: ${
|
|
205
|
-
|
|
206
|
-
).join(", ")}`
|
|
207
|
-
),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
216
|
+
i.length && l.push(
|
|
217
|
+
`Key fields: ${i.map((s) => `${s.name} (${s.type})`).join(", ")}`
|
|
218
|
+
), o.length && l.push(
|
|
219
|
+
`Fields: ${o.map((s) => `${s.name} (${s.type}${s.nullable ? "" : ", required"})`).join(", ")}`
|
|
220
|
+
), r.navProperties.length && l.push(
|
|
221
|
+
`Navigation: ${r.navProperties.map((s) => `${s.name} → ${s.type}${s.cardinality === "*" ? "[]" : ""}`).join(", ")}`
|
|
222
|
+
), n.push({ title: `${r.name} entity schema`, content: l.join(`
|
|
223
|
+
`) });
|
|
224
|
+
}
|
|
225
|
+
if (e.associations.length) {
|
|
226
|
+
const r = e.associations.map(
|
|
227
|
+
(a) => `${a.sourceEntity}.${a.navigationProperty} → ${a.targetEntity} (${a.cardinality === "*" ? "many" : "one"})`
|
|
228
|
+
);
|
|
229
|
+
n.push({
|
|
230
|
+
title: "Entity associations and navigation",
|
|
231
|
+
content: ["Associations:", ...r].join(`
|
|
232
|
+
`)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (e.actions.length) {
|
|
236
|
+
const r = e.actions.map((a) => {
|
|
237
|
+
const i = a.parameters.map((o) => `${o.name}: ${o.type}`).join(", ");
|
|
238
|
+
return `${a.name}(${i})${a.bound && a.entity ? ` [bound to ${a.entity}]` : ""}`;
|
|
239
|
+
});
|
|
240
|
+
n.push({
|
|
241
|
+
title: "OData actions and functions",
|
|
242
|
+
content: ["Actions/Functions:", ...r].join(`
|
|
212
243
|
`)
|
|
213
244
|
});
|
|
214
245
|
}
|
|
215
|
-
return
|
|
246
|
+
return e.entities.length > 0 && n.push({
|
|
216
247
|
title: "Available OData entity sets",
|
|
217
248
|
content: [
|
|
218
|
-
`OData service: ${
|
|
219
|
-
`Entities: ${
|
|
220
|
-
"GET {
|
|
249
|
+
`OData service: ${t}`,
|
|
250
|
+
`Entities: ${e.entities.map((r) => r.entitySetName ?? r.name).join(", ")}`,
|
|
251
|
+
"Patterns: GET {entitySet}/$count | ?$filter=field eq 'value' | ?$top=10&$skip=0 | ?$orderby=field desc"
|
|
221
252
|
].join(`
|
|
222
253
|
`)
|
|
223
254
|
}), n;
|
|
224
255
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Legacy helper kept for backward compatibility.
|
|
258
|
+
* Prefer fetchMetadata() + buildSchemaDocuments() for new code.
|
|
259
|
+
*/
|
|
260
|
+
static async fetchSchemaDocuments(e, t) {
|
|
261
|
+
const n = await m.fetchMetadata(e, t);
|
|
262
|
+
return n ? m.buildSchemaDocuments(n, e) : [];
|
|
263
|
+
}
|
|
264
|
+
/** Invalidate cached metadata for a service (call when metadata may have changed). */
|
|
265
|
+
static invalidate(e) {
|
|
266
|
+
const t = `btp-copilot-meta-v2-${e}`;
|
|
267
|
+
m._metaCache.delete(t), sessionStorage.removeItem(t);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Fetch live entity data from an OData service with full error capture.
|
|
271
|
+
*
|
|
272
|
+
* Unlike a raw fetch(), this method:
|
|
273
|
+
* - Returns { data, error } — never throws
|
|
274
|
+
* - Captures the HTTP status and error body on failure
|
|
275
|
+
* - Builds a human-readable error context string the chatbot can use to
|
|
276
|
+
* explain to the user why data is unavailable, instead of returning
|
|
277
|
+
* an empty result or crashing silently
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* const { data, error, errorContext } = await ODataProbe.fetchEntityData(
|
|
281
|
+
* '/odata/v4/fertilizer-blend/Farms', token
|
|
282
|
+
* );
|
|
283
|
+
* if (error) ctx.extra.fetch_errors = [...(ctx.extra.fetch_errors ?? []), errorContext];
|
|
284
|
+
*/
|
|
285
|
+
static async fetchEntityData(e, t, n) {
|
|
286
|
+
try {
|
|
287
|
+
const r = { Accept: "application/json" };
|
|
288
|
+
t && (r.Authorization = `Bearer ${t}`);
|
|
289
|
+
const a = await fetch(e, { ...n, headers: { ...r, ...(n == null ? void 0 : n.headers) ?? {} } });
|
|
290
|
+
if (!a.ok) {
|
|
291
|
+
let o = "";
|
|
292
|
+
try {
|
|
293
|
+
o = await a.text();
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
const l = `HTTP ${a.status} ${a.statusText}`, s = [
|
|
297
|
+
`OData fetch failed: ${e}`,
|
|
298
|
+
`Status: ${l}`,
|
|
299
|
+
o ? `Response: ${o.slice(0, 500)}` : "",
|
|
300
|
+
"This entity's data cannot be displayed to the user right now.",
|
|
301
|
+
"The underlying service may have an API mismatch (e.g. invalid $expand)."
|
|
302
|
+
].filter(Boolean).join(`
|
|
303
|
+
`);
|
|
304
|
+
return { data: null, error: l, errorContext: s };
|
|
305
|
+
}
|
|
306
|
+
return { data: await a.json(), error: null, errorContext: null };
|
|
307
|
+
} catch (r) {
|
|
308
|
+
const a = r instanceof Error ? r.message : String(r), i = [
|
|
309
|
+
`OData fetch failed: ${e}`,
|
|
310
|
+
`Error: ${a}`,
|
|
311
|
+
"This entity's data cannot be displayed to the user right now."
|
|
312
|
+
].join(`
|
|
313
|
+
`);
|
|
314
|
+
return { data: null, error: a, errorContext: i };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Track an OData fetch error in sessionStorage so the ContextBridge can
|
|
319
|
+
* include it in the next context push to the chatbot iframe.
|
|
320
|
+
* Call this whenever a live data fetch in the host app fails.
|
|
321
|
+
*/
|
|
322
|
+
static recordFetchError(e, t, n) {
|
|
323
|
+
try {
|
|
324
|
+
const r = "btp-copilot-fetch-errors", i = [
|
|
325
|
+
...JSON.parse(sessionStorage.getItem(r) ?? "[]").filter((o) => o.entity !== e),
|
|
326
|
+
{ entity: e, url: t, reason: n, at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
327
|
+
].slice(-10);
|
|
328
|
+
sessionStorage.setItem(r, JSON.stringify(i));
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/** Read all recorded fetch errors as a schema_hint-style string. */
|
|
333
|
+
static getFetchErrorContext() {
|
|
334
|
+
try {
|
|
335
|
+
const t = JSON.parse(sessionStorage.getItem("btp-copilot-fetch-errors") ?? "[]");
|
|
336
|
+
return t.length === 0 ? null : [
|
|
337
|
+
"The following entities currently have backend errors and may not show live data:",
|
|
338
|
+
...t.map(
|
|
339
|
+
(r) => `- ${r.entity}: ${r.reason} (${r.url}) [at ${r.at}]`
|
|
340
|
+
)
|
|
341
|
+
].join(`
|
|
342
|
+
`);
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Find all entities in the parsed metadata that link TO the given entity type.
|
|
349
|
+
* These are the "reverse" associations — i.e. other entities that have a FK
|
|
350
|
+
* pointing at `entityName`.
|
|
351
|
+
*
|
|
352
|
+
* For each such entity, returns the entity set name, the navigation property
|
|
353
|
+
* name, and the FK / referenced-key pair so the caller can build an OData
|
|
354
|
+
* $filter query like:
|
|
355
|
+
* `GET /Farms?$filter=to_FertilizerBlend_orderID eq '2466'`
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* // User is viewing FertilizerBlend(orderID='2466')
|
|
359
|
+
* const links = ODataProbe.findLinkedEntities(parsed, "FertilizerBlend");
|
|
360
|
+
* // → [{ entitySetName: "Farms", navProperty: "to_FertilizerBlend",
|
|
361
|
+
* // foreignKey: "to_FertilizerBlend_orderID", referencedKey: "orderID" }]
|
|
362
|
+
*/
|
|
363
|
+
static findLinkedEntities(e, t) {
|
|
364
|
+
const n = [];
|
|
365
|
+
for (const r of e.associations)
|
|
366
|
+
if (r.targetEntity === t && r.foreignKey && r.referencedKey) {
|
|
367
|
+
const a = e.entities.find(
|
|
368
|
+
(i) => i.name === r.sourceEntity
|
|
369
|
+
);
|
|
370
|
+
a && n.push({
|
|
371
|
+
entitySetName: a.entitySetName ?? a.name,
|
|
372
|
+
navProperty: r.navigationProperty,
|
|
373
|
+
foreignKey: r.foreignKey,
|
|
374
|
+
referencedKey: r.referencedKey
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return n;
|
|
378
|
+
}
|
|
379
|
+
// ── EDMX Parser ─────────────────────────────────────────────────────────────
|
|
380
|
+
static _parseEDMX(e, t) {
|
|
381
|
+
const n = [], r = [], a = [];
|
|
382
|
+
try {
|
|
383
|
+
const i = new DOMParser().parseFromString(e, "text/xml"), o = /* @__PURE__ */ new Map();
|
|
384
|
+
i.querySelectorAll("EntitySet").forEach((s) => {
|
|
385
|
+
const c = s.getAttribute("EntityType") ?? "", h = c.split(".").pop() ?? c, p = s.getAttribute("Name") ?? h;
|
|
386
|
+
o.set(h, p);
|
|
387
|
+
}), i.querySelectorAll("EntityType").forEach((s) => {
|
|
388
|
+
const c = s.getAttribute("Name") ?? "";
|
|
389
|
+
if (!c) return;
|
|
390
|
+
const h = new Set(
|
|
391
|
+
Array.from(s.querySelectorAll("Key > PropertyRef")).map(
|
|
392
|
+
(d) => d.getAttribute("Name") ?? ""
|
|
393
|
+
)
|
|
394
|
+
), p = Array.from(
|
|
395
|
+
s.querySelectorAll("Property")
|
|
396
|
+
).map((d) => ({
|
|
397
|
+
name: d.getAttribute("Name") ?? "",
|
|
398
|
+
type: m._shortType(d.getAttribute("Type") ?? ""),
|
|
399
|
+
isKey: h.has(d.getAttribute("Name") ?? ""),
|
|
400
|
+
nullable: d.getAttribute("Nullable") !== "false"
|
|
401
|
+
})), b = Array.from(
|
|
402
|
+
s.querySelectorAll("NavigationProperty")
|
|
403
|
+
).map((d) => {
|
|
404
|
+
const v = d.getAttribute("Type") ?? d.getAttribute("ToRole") ?? "", w = v.startsWith("Collection("), u = d.querySelector("ReferentialConstraint");
|
|
405
|
+
return {
|
|
406
|
+
name: d.getAttribute("Name") ?? "",
|
|
407
|
+
type: m._shortType(v),
|
|
408
|
+
cardinality: w ? "*" : "1",
|
|
409
|
+
partner: d.getAttribute("Partner") ?? void 0,
|
|
410
|
+
foreignKey: (u == null ? void 0 : u.getAttribute("Property")) ?? void 0,
|
|
411
|
+
referencedKey: (u == null ? void 0 : u.getAttribute("ReferencedProperty")) ?? void 0
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
n.push({
|
|
415
|
+
name: c,
|
|
416
|
+
entitySetName: o.get(c) ?? c,
|
|
417
|
+
properties: p,
|
|
418
|
+
navProperties: b
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
for (const s of n)
|
|
422
|
+
for (const c of s.navProperties) {
|
|
423
|
+
const h = c.type.replace(/\[\]$/, "");
|
|
424
|
+
r.push({
|
|
425
|
+
sourceEntity: s.name,
|
|
426
|
+
navigationProperty: c.name,
|
|
427
|
+
targetEntity: h,
|
|
428
|
+
cardinality: c.cardinality === "*" ? "*" : "1",
|
|
429
|
+
foreignKey: c.foreignKey,
|
|
430
|
+
referencedKey: c.referencedKey
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const l = (s, c) => {
|
|
434
|
+
const h = s.getAttribute("Name") ?? "", p = [];
|
|
435
|
+
let b;
|
|
436
|
+
s.querySelectorAll("Parameter").forEach((w, u) => {
|
|
437
|
+
const x = w.getAttribute("Name") ?? "", g = m._shortType(w.getAttribute("Type") ?? "");
|
|
438
|
+
c && u === 0 ? b = g.replace(/\[\]$/, "") : p.push({ name: x, type: g });
|
|
439
|
+
});
|
|
440
|
+
const d = s.querySelector("ReturnType"), v = d ? m._shortType(d.getAttribute("Type") ?? "") : void 0;
|
|
441
|
+
return { name: h, bound: c, entity: b, parameters: p, returnType: v };
|
|
442
|
+
};
|
|
443
|
+
i.querySelectorAll("Action").forEach((s) => {
|
|
444
|
+
const c = s.getAttribute("IsBound") === "true";
|
|
445
|
+
a.push(l(s, c));
|
|
446
|
+
}), i.querySelectorAll("Function").forEach((s) => {
|
|
447
|
+
const c = s.getAttribute("IsBound") === "true";
|
|
448
|
+
a.push(l(s, c));
|
|
449
|
+
}), i.querySelectorAll("FunctionImport").forEach((s) => {
|
|
450
|
+
const c = s.getAttribute("Name") ?? "", h = [];
|
|
451
|
+
s.querySelectorAll("Parameter").forEach((p) => {
|
|
452
|
+
h.push({
|
|
453
|
+
name: p.getAttribute("Name") ?? "",
|
|
454
|
+
type: m._shortType(p.getAttribute("Type") ?? "")
|
|
455
|
+
});
|
|
456
|
+
}), a.push({ name: c, bound: !1, parameters: h });
|
|
457
|
+
});
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
return { entities: n, associations: r, actions: a, rawXml: e };
|
|
461
|
+
}
|
|
462
|
+
static _shortType(e) {
|
|
463
|
+
return e.startsWith("Collection(") ? `${e.slice(11, -1).split(".").pop()}[]` : e.split(".").pop() ?? e;
|
|
228
464
|
}
|
|
229
465
|
};
|
|
230
|
-
|
|
231
|
-
let
|
|
232
|
-
class
|
|
233
|
-
static capture(
|
|
234
|
-
|
|
466
|
+
m._metaCache = /* @__PURE__ */ new Map();
|
|
467
|
+
let y = m;
|
|
468
|
+
const f = class f {
|
|
469
|
+
static capture(e) {
|
|
470
|
+
var n, r, a;
|
|
471
|
+
const t = { ...e };
|
|
235
472
|
try {
|
|
236
|
-
const
|
|
237
|
-
|
|
473
|
+
const i = C.getHash();
|
|
474
|
+
i && (t.current_view = i);
|
|
238
475
|
} catch {
|
|
239
476
|
}
|
|
240
477
|
try {
|
|
241
|
-
const
|
|
242
|
-
|
|
478
|
+
const i = _.getCurrentEntity();
|
|
479
|
+
i && Object.keys(i).length > 0 && (t.entity_data = i);
|
|
243
480
|
} catch {
|
|
244
481
|
}
|
|
245
482
|
try {
|
|
246
|
-
|
|
483
|
+
t.user_locale || (t.user_locale = C.getUserLocale());
|
|
247
484
|
} catch {
|
|
248
485
|
}
|
|
249
486
|
try {
|
|
250
|
-
const
|
|
251
|
-
|
|
487
|
+
const i = window.location.pathname + window.location.search + window.location.hash;
|
|
488
|
+
(!t.current_view || t.current_view === "") && (t.current_view = i);
|
|
252
489
|
} catch {
|
|
253
490
|
}
|
|
254
|
-
|
|
491
|
+
try {
|
|
492
|
+
t.user_locale || (t.user_locale = navigator.language || ((n = navigator.languages) == null ? void 0 : n[0]) || "en");
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
t.extra = {
|
|
497
|
+
...t.extra ?? {},
|
|
498
|
+
page_title: ((r = t.extra) == null ? void 0 : r.page_title) ?? document.title ?? "",
|
|
499
|
+
page_url: ((a = t.extra) == null ? void 0 : a.page_url) ?? window.location.href ?? ""
|
|
500
|
+
};
|
|
501
|
+
} catch {
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const i = window.__APP_CONTEXT__;
|
|
505
|
+
i && (i.entity_data && (t.entity_data = f._safeMerge(t.entity_data ?? {}, i.entity_data)), i.current_view && !t.current_view && (t.current_view = i.current_view), i.extra && (t.extra = f._safeMerge(i.extra, t.extra)), i.service_url && !t.service_url && (t.service_url = i.service_url));
|
|
506
|
+
} catch {
|
|
507
|
+
}
|
|
508
|
+
return t;
|
|
255
509
|
}
|
|
256
|
-
static async captureAsync(
|
|
257
|
-
var n;
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
510
|
+
static async captureAsync(e) {
|
|
511
|
+
var n, r, a;
|
|
512
|
+
const t = f.capture(e);
|
|
513
|
+
if (t.service_url || (t.service_url = f._discoverServiceUrl() ?? void 0), t.service_url)
|
|
260
514
|
try {
|
|
261
|
-
const i = ((n =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
${a.content}`).join(`
|
|
515
|
+
const i = ((n = t.extra) == null ? void 0 : n.auth_token) ?? null, o = await y.fetchMetadata(t.service_url, i);
|
|
516
|
+
if (o) {
|
|
517
|
+
const l = y.buildSchemaDocuments(o, t.service_url);
|
|
518
|
+
if (l.length > 0) {
|
|
519
|
+
const h = l.map((p) => `## ${p.title}
|
|
520
|
+
${p.content}`).join(`
|
|
268
521
|
|
|
269
522
|
`);
|
|
270
|
-
|
|
523
|
+
t.extra = { ...t.extra ?? {}, schema_hint: h };
|
|
524
|
+
}
|
|
525
|
+
const s = (r = t.extra) == null ? void 0 : r.backend_url;
|
|
526
|
+
if (s && o.rawXml) {
|
|
527
|
+
const h = t.app_id, p = t.service_url;
|
|
528
|
+
f._registerMetadata(s, h, p, o.rawXml, i);
|
|
529
|
+
}
|
|
530
|
+
const c = (a = t.entity_data) == null ? void 0 : a._entity;
|
|
531
|
+
if (c && t.service_url)
|
|
532
|
+
try {
|
|
533
|
+
const h = await f._fetchLinkedEntities(
|
|
534
|
+
t.service_url,
|
|
535
|
+
c,
|
|
536
|
+
t.entity_data,
|
|
537
|
+
o,
|
|
538
|
+
i
|
|
539
|
+
);
|
|
540
|
+
Object.keys(h).length > 0 && (t.entity_data = { ...t.entity_data ?? {}, ...h });
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
271
543
|
}
|
|
272
544
|
} catch {
|
|
273
545
|
}
|
|
274
|
-
|
|
546
|
+
try {
|
|
547
|
+
const i = y.getFetchErrorContext();
|
|
548
|
+
i && (t.extra = { ...t.extra ?? {}, fetch_errors: i });
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
return t;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Safe shallow merge that blocks prototype pollution.
|
|
555
|
+
* Skips __proto__, constructor, and prototype keys from the incoming object.
|
|
556
|
+
*/
|
|
557
|
+
static _safeMerge(e, t) {
|
|
558
|
+
if (!t || typeof t != "object" || Array.isArray(t)) return e;
|
|
559
|
+
const n = { ...e };
|
|
560
|
+
for (const [r, a] of Object.entries(t))
|
|
561
|
+
r === "__proto__" || r === "constructor" || r === "prototype" || (n[r] = a);
|
|
562
|
+
return n;
|
|
563
|
+
}
|
|
564
|
+
static async _fetchLinkedEntities(e, t, n, r, a) {
|
|
565
|
+
const i = r.entities.find(
|
|
566
|
+
(u) => u.name === t || u.entitySetName === t
|
|
567
|
+
);
|
|
568
|
+
if (!i) return {};
|
|
569
|
+
const o = i.properties.find((u) => u.isKey);
|
|
570
|
+
if (!o) return {};
|
|
571
|
+
const l = n[o.name];
|
|
572
|
+
if (l == null) return {};
|
|
573
|
+
const s = `${t}::${l}`, c = Date.now(), h = f._linkedEntityCache.get(s);
|
|
574
|
+
if (h && c - h.fetchedAt < f._LINKED_CACHE_TTL)
|
|
575
|
+
return h.data;
|
|
576
|
+
const p = {}, b = typeof l == "number" ? String(l) : `'${l}'`, d = y.findLinkedEntities(r, i.name);
|
|
577
|
+
await Promise.all(
|
|
578
|
+
d.map(async (u) => {
|
|
579
|
+
try {
|
|
580
|
+
const x = `${e}/${u.entitySetName}?$filter=${encodeURIComponent(`${u.foreignKey} eq ${b}`)}&$top=50`, { data: g, error: E, errorContext: $ } = await y.fetchEntityData(x, a);
|
|
581
|
+
if (E)
|
|
582
|
+
y.recordFetchError(u.entitySetName, x, E), $ && (p[`linked_${u.entitySetName}_error`] = $);
|
|
583
|
+
else if (g) {
|
|
584
|
+
const I = g.value ?? (Array.isArray(g) ? g : [g]);
|
|
585
|
+
I.length > 0 && (p[`linked_${u.entitySetName}`] = I);
|
|
586
|
+
}
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
);
|
|
591
|
+
const v = i.entitySetName ?? i.name, w = i.navProperties.filter(
|
|
592
|
+
(u) => u.cardinality !== "*"
|
|
593
|
+
);
|
|
594
|
+
return await Promise.all(
|
|
595
|
+
w.map(async (u) => {
|
|
596
|
+
if (p[`linked_${u.name}`] == null)
|
|
597
|
+
try {
|
|
598
|
+
const x = `${e}/${v}(${b})/${u.name}`, { data: g, error: E } = await y.fetchEntityData(x, a);
|
|
599
|
+
if (!E && g) {
|
|
600
|
+
const $ = g.value ?? g;
|
|
601
|
+
$ != null && (p[`linked_${u.name}`] = $);
|
|
602
|
+
}
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
})
|
|
606
|
+
), f._linkedEntityCache.set(s, {
|
|
607
|
+
data: p,
|
|
608
|
+
fetchedAt: c
|
|
609
|
+
}), p;
|
|
610
|
+
}
|
|
611
|
+
static async _registerMetadata(e, t, n, r, a) {
|
|
612
|
+
const i = `${t}::${n}`;
|
|
613
|
+
if (!f._registeredKeys.has(i)) {
|
|
614
|
+
f._registeredKeys.add(i);
|
|
615
|
+
try {
|
|
616
|
+
const o = { "Content-Type": "application/json" };
|
|
617
|
+
a && (o.Authorization = `Bearer ${a}`), await fetch(`${e}/api/apps/register-metadata`, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: o,
|
|
620
|
+
body: JSON.stringify({ app_id: t, service_url: n, raw_xml: r })
|
|
621
|
+
});
|
|
622
|
+
} catch {
|
|
623
|
+
f._registeredKeys.delete(i);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
275
626
|
}
|
|
276
627
|
static _discoverServiceUrl() {
|
|
277
628
|
try {
|
|
278
|
-
const
|
|
279
|
-
if (
|
|
629
|
+
const e = _.getServiceUrl();
|
|
630
|
+
if (e) return e;
|
|
280
631
|
} catch {
|
|
281
632
|
}
|
|
282
633
|
try {
|
|
283
|
-
const
|
|
634
|
+
const e = performance.getEntriesByType(
|
|
284
635
|
"resource"
|
|
285
636
|
);
|
|
286
|
-
for (const
|
|
287
|
-
const n =
|
|
637
|
+
for (const t of e) {
|
|
638
|
+
const n = t.name.match(/^(https?:\/\/[^?#]+)\/\$metadata/);
|
|
288
639
|
if (n) return n[1];
|
|
289
640
|
}
|
|
290
641
|
} catch {
|
|
291
642
|
}
|
|
292
643
|
try {
|
|
293
|
-
const
|
|
644
|
+
const e = window.location.href.match(
|
|
294
645
|
/(https?:\/\/[^/]+(?:\/odata\/v[24]\/[^/?#]+|\/sap\/opu\/odata\/[^/?#]+))/
|
|
295
646
|
);
|
|
296
|
-
if (
|
|
647
|
+
if (e) return e[1];
|
|
297
648
|
} catch {
|
|
298
649
|
}
|
|
299
650
|
return null;
|
|
300
651
|
}
|
|
301
652
|
static async discoverFromManifest() {
|
|
302
|
-
var
|
|
653
|
+
var e;
|
|
303
654
|
try {
|
|
304
|
-
const
|
|
305
|
-
if (!
|
|
306
|
-
const n = await
|
|
307
|
-
for (const
|
|
308
|
-
const
|
|
309
|
-
if ((
|
|
310
|
-
const
|
|
311
|
-
if (typeof
|
|
312
|
-
return new URL(
|
|
655
|
+
const t = await fetch("/manifest.json");
|
|
656
|
+
if (!t.ok) return null;
|
|
657
|
+
const n = await t.json(), r = ((e = n == null ? void 0 : n["sap.app"]) == null ? void 0 : e.dataSources) ?? {};
|
|
658
|
+
for (const a of Object.keys(r)) {
|
|
659
|
+
const i = r[a];
|
|
660
|
+
if ((i == null ? void 0 : i.type) === "ODataAnnotation") continue;
|
|
661
|
+
const o = i == null ? void 0 : i.uri;
|
|
662
|
+
if (typeof o == "string" && o.length > 0)
|
|
663
|
+
return new URL(o, window.location.href).toString().replace(/\/$/, "");
|
|
313
664
|
}
|
|
314
665
|
} catch {
|
|
315
666
|
}
|
|
316
667
|
return null;
|
|
317
668
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Listen for postMessage events from the BTP Copilot iframe.
|
|
671
|
+
*
|
|
672
|
+
* @param allowedOrigin The iframe's origin (e.g. "https://your-chatbot.cfapps.eu10.hana.ondemand.com").
|
|
673
|
+
* Messages from any other origin are silently ignored.
|
|
674
|
+
* Pass "*" only in trusted, same-origin deployments.
|
|
675
|
+
*/
|
|
676
|
+
static listenForUpdates(e, t, n, r, a) {
|
|
677
|
+
const i = (o) => {
|
|
678
|
+
if (!o.data || typeof o.data != "object" || a && a !== "*" && o.origin !== a) return;
|
|
679
|
+
const { type: l, payload: s, text: c } = o.data;
|
|
680
|
+
switch (l) {
|
|
323
681
|
case "btp-copilot:set-context":
|
|
324
|
-
|
|
682
|
+
s && e(s);
|
|
325
683
|
break;
|
|
326
684
|
case "btp-copilot:send-message":
|
|
327
|
-
|
|
685
|
+
c && t(c);
|
|
328
686
|
break;
|
|
329
687
|
case "btp-copilot:open":
|
|
330
688
|
n();
|
|
331
689
|
break;
|
|
332
690
|
case "btp-copilot:close":
|
|
333
|
-
|
|
691
|
+
r();
|
|
334
692
|
break;
|
|
335
693
|
}
|
|
336
694
|
};
|
|
337
|
-
return window.addEventListener("message",
|
|
695
|
+
return window.addEventListener("message", i), () => window.removeEventListener("message", i);
|
|
338
696
|
}
|
|
339
|
-
}
|
|
340
|
-
|
|
697
|
+
};
|
|
698
|
+
f._registeredKeys = /* @__PURE__ */ new Set(), f._linkedEntityCache = /* @__PURE__ */ new Map(), f._LINKED_CACHE_TTL = 3e4;
|
|
699
|
+
let k = f;
|
|
700
|
+
const j = `/* ── Host element ─────────────────────────────────────────────────────────── */
|
|
341
701
|
:host {
|
|
342
702
|
position: fixed;
|
|
343
703
|
z-index: 2147483647;
|
|
@@ -464,77 +824,113 @@ const v = `/* ── Host element ───────────────
|
|
|
464
824
|
}
|
|
465
825
|
.chat-iframe { border-radius: 1rem 1rem 0 0; }
|
|
466
826
|
}
|
|
467
|
-
`,
|
|
827
|
+
`, O = [
|
|
468
828
|
"iframe-url",
|
|
469
829
|
"app-id",
|
|
470
830
|
"app-name",
|
|
471
831
|
"service-url",
|
|
832
|
+
"backend-url",
|
|
472
833
|
"auto-context",
|
|
473
834
|
"position",
|
|
474
835
|
"theme",
|
|
475
836
|
"token",
|
|
476
837
|
"context-interval"
|
|
477
|
-
],
|
|
838
|
+
], N = class N extends HTMLElement {
|
|
478
839
|
constructor() {
|
|
479
|
-
super(...arguments), this._open = !1, this._isFullscreen = !1, this._iframeEl = null, this._contextTimer = null;
|
|
840
|
+
super(...arguments), this._open = !1, this._isFullscreen = !1, this._iframeEl = null, this._contextTimer = null, this._manualRecord = null;
|
|
480
841
|
}
|
|
481
842
|
connectedCallback() {
|
|
482
843
|
this._config = this._readConfig(), this._buildShadow(), this._startContextPolling();
|
|
483
|
-
const
|
|
484
|
-
if (!
|
|
485
|
-
const
|
|
486
|
-
|
|
844
|
+
const e = (t) => {
|
|
845
|
+
if (!t.data || typeof t.data != "object") return;
|
|
846
|
+
const n = this._iframeOrigin();
|
|
847
|
+
if (n !== "*" && t.origin !== n) return;
|
|
848
|
+
const { type: r } = t.data;
|
|
849
|
+
r === "btp-copilot:close" && this.close(), r === "btp-copilot:open" && this.open(), r === "btp-copilot:fullscreen" && this._toggleFullscreen(), r === "btp-copilot:minimize" && this.close();
|
|
487
850
|
};
|
|
488
|
-
window.addEventListener("message",
|
|
851
|
+
window.addEventListener("message", e), this._removeMessageListener = () => window.removeEventListener("message", e);
|
|
489
852
|
}
|
|
490
853
|
disconnectedCallback() {
|
|
491
|
-
var
|
|
492
|
-
(
|
|
854
|
+
var e;
|
|
855
|
+
(e = this._removeMessageListener) == null || e.call(this), this._stopContextPolling();
|
|
493
856
|
}
|
|
494
|
-
attributeChangedCallback(
|
|
495
|
-
|
|
857
|
+
attributeChangedCallback(e, t, n) {
|
|
858
|
+
t === n || !this.isConnected || (this._config = this._readConfig(), this._updateAfterAttributeChange());
|
|
496
859
|
}
|
|
497
860
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
498
861
|
open() {
|
|
499
862
|
this._open = !0, this.shadowRoot.querySelector(".panel").classList.remove("closed"), this.shadowRoot.querySelector(".toggle-btn").setAttribute("aria-expanded", "true"), this._clearBadge(), this._pushContextToIframe();
|
|
500
863
|
}
|
|
501
864
|
close() {
|
|
502
|
-
this._open = !1;
|
|
503
|
-
const
|
|
504
|
-
|
|
865
|
+
this._open = !1, this._isFullscreen = !1;
|
|
866
|
+
const e = this.shadowRoot.querySelector(".panel");
|
|
867
|
+
e.classList.add("closed"), e.classList.remove("fullscreen");
|
|
868
|
+
const t = this.shadowRoot.querySelector(".toggle-btn");
|
|
869
|
+
t.style.display = "", t.setAttribute("aria-expanded", "false");
|
|
505
870
|
}
|
|
506
871
|
_toggleFullscreen() {
|
|
507
872
|
this._isFullscreen = !this._isFullscreen;
|
|
508
|
-
const
|
|
509
|
-
this._isFullscreen ? (
|
|
873
|
+
const e = this.shadowRoot.querySelector(".panel"), t = this.shadowRoot.querySelector(".toggle-btn");
|
|
874
|
+
this._isFullscreen ? (e.classList.add("fullscreen"), t.style.display = "none") : (e.classList.remove("fullscreen"), t.style.display = "");
|
|
510
875
|
}
|
|
511
|
-
sendMessage(
|
|
876
|
+
sendMessage(e) {
|
|
512
877
|
this._open || this.open(), setTimeout(() => {
|
|
513
|
-
var
|
|
514
|
-
(n = (
|
|
515
|
-
{ type: "btp-copilot:send-message", text:
|
|
516
|
-
|
|
878
|
+
var t, n;
|
|
879
|
+
(n = (t = this._iframeEl) == null ? void 0 : t.contentWindow) == null || n.postMessage(
|
|
880
|
+
{ type: "btp-copilot:send-message", text: e },
|
|
881
|
+
this._iframeOrigin()
|
|
517
882
|
);
|
|
518
883
|
}, 100);
|
|
519
884
|
}
|
|
885
|
+
/**
|
|
886
|
+
* Push the currently-viewed record into the chatbot context.
|
|
887
|
+
*
|
|
888
|
+
* Call this from your Fiori controller or CAP handler after a record is loaded.
|
|
889
|
+
* The chatbot will have all field values and can answer questions like
|
|
890
|
+
* "what is the applicator?" or "what status is this order?" immediately.
|
|
891
|
+
*
|
|
892
|
+
* @param entityName The OData entity set name, e.g. "FertilizerBlend"
|
|
893
|
+
* @param data Plain object of field → value pairs (the record)
|
|
894
|
+
*
|
|
895
|
+
* @example // Fiori UI5 controller (1 line):
|
|
896
|
+
* document.querySelector('btp-copilot')
|
|
897
|
+
* .setRecord('FertilizerBlend', this.getView().getBindingContext().getObject());
|
|
898
|
+
*/
|
|
899
|
+
setRecord(e, t) {
|
|
900
|
+
this._manualRecord = { _entity: e, ...t }, this._pushContextToIframe();
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Push arbitrary key/value pairs into the chatbot context (without an entity name).
|
|
904
|
+
* Useful for custom metadata, computed values, or non-OData context.
|
|
905
|
+
*
|
|
906
|
+
* @example
|
|
907
|
+
* widget.pushData({ currentUser: 'John', selectedRegion: 'EU' });
|
|
908
|
+
*/
|
|
909
|
+
pushData(e) {
|
|
910
|
+
this._manualRecord = { ...this._manualRecord, ...e }, this._pushContextToIframe();
|
|
911
|
+
}
|
|
912
|
+
/** Remove the manually-set record so auto-context takes over again. */
|
|
913
|
+
clearRecord() {
|
|
914
|
+
this._manualRecord = null, this._pushContextToIframe();
|
|
915
|
+
}
|
|
520
916
|
// ── Shadow DOM ─────────────────────────────────────────────────────────────
|
|
521
917
|
_buildShadow() {
|
|
522
|
-
var
|
|
523
|
-
const
|
|
524
|
-
|
|
918
|
+
var r;
|
|
919
|
+
const e = this.attachShadow({ mode: "open" }), t = document.createElement("style");
|
|
920
|
+
t.textContent = j, e.appendChild(t);
|
|
525
921
|
const n = document.createElement("div");
|
|
526
|
-
for (n.innerHTML = this._template(); n.firstChild; )
|
|
527
|
-
|
|
922
|
+
for (n.innerHTML = this._template(); n.firstChild; ) e.appendChild(n.firstChild);
|
|
923
|
+
e.querySelector(".toggle-btn").addEventListener("click", () => {
|
|
528
924
|
this._open ? this.close() : this.open();
|
|
529
|
-
}), this._iframeEl =
|
|
925
|
+
}), this._iframeEl = e.querySelector(".chat-iframe"), (r = this._iframeEl) == null || r.addEventListener("load", () => {
|
|
530
926
|
this._pushAuthToIframe(), this._pushContextToIframe();
|
|
531
927
|
});
|
|
532
928
|
}
|
|
533
929
|
_template() {
|
|
534
930
|
var n;
|
|
535
|
-
const
|
|
931
|
+
const e = ((n = this._config) == null ? void 0 : n.position) ?? "bottom-right", t = this._esc(this._buildIframeUrl());
|
|
536
932
|
return `
|
|
537
|
-
<button class="toggle-btn ${
|
|
933
|
+
<button class="toggle-btn ${e}" aria-label="Open AI assistant" aria-expanded="false" aria-haspopup="dialog">
|
|
538
934
|
<svg class="icon-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
539
935
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
540
936
|
</svg>
|
|
@@ -544,10 +940,10 @@ const v = `/* ── Host element ───────────────
|
|
|
544
940
|
<span class="badge" aria-hidden="true"></span>
|
|
545
941
|
</button>
|
|
546
942
|
|
|
547
|
-
<div class="panel closed ${
|
|
943
|
+
<div class="panel closed ${e}" role="dialog" aria-label="AI Assistant" aria-modal="true">
|
|
548
944
|
<iframe
|
|
549
945
|
class="chat-iframe"
|
|
550
|
-
src="${
|
|
946
|
+
src="${t}"
|
|
551
947
|
title="AI Assistant"
|
|
552
948
|
allow="clipboard-read; clipboard-write"
|
|
553
949
|
loading="lazy">
|
|
@@ -556,27 +952,30 @@ const v = `/* ── Host element ───────────────
|
|
|
556
952
|
}
|
|
557
953
|
// ── Context + Auth ─────────────────────────────────────────────────────────
|
|
558
954
|
_buildBaseContext() {
|
|
559
|
-
const
|
|
955
|
+
const e = S.resolve(this._config.token), t = this._config.serviceUrl ?? "", n = t ? t.startsWith("http") ? t : `${window.location.origin}${t}` : void 0;
|
|
560
956
|
return {
|
|
561
957
|
app_id: this._config.appId,
|
|
562
958
|
app_name: this._config.appName,
|
|
563
|
-
service_url:
|
|
959
|
+
service_url: n,
|
|
960
|
+
// Manually set record (via setRecord / pushData) — overrides auto-context entity_data
|
|
961
|
+
...this._manualRecord ? { entity_data: this._manualRecord } : {},
|
|
564
962
|
// Include the resolved auth token in extra so the backend can
|
|
565
963
|
// proxy OData calls on behalf of the user to fetch real data
|
|
566
|
-
|
|
964
|
+
extra: {
|
|
965
|
+
...e ? { auth_token: e } : {},
|
|
966
|
+
...this._config.backendUrl ? { backend_url: this._config.backendUrl } : {}
|
|
967
|
+
}
|
|
567
968
|
};
|
|
568
969
|
}
|
|
569
970
|
_buildIframeUrl() {
|
|
570
|
-
var
|
|
571
|
-
const
|
|
572
|
-
if (!
|
|
971
|
+
var t;
|
|
972
|
+
const e = ((t = this._config) == null ? void 0 : t.iframeUrl) ?? "";
|
|
973
|
+
if (!e) return "";
|
|
573
974
|
try {
|
|
574
|
-
const n = new URL(
|
|
575
|
-
|
|
576
|
-
const a = d.resolve(this._config.token);
|
|
577
|
-
return a && n.searchParams.set("token", a), n.toString();
|
|
975
|
+
const n = new URL(e), { appId: r, appName: a, serviceUrl: i } = this._config;
|
|
976
|
+
return r && n.searchParams.set("appId", r), a && n.searchParams.set("appName", a), i && n.searchParams.set("serviceUrl", i), n.toString();
|
|
578
977
|
} catch {
|
|
579
|
-
return
|
|
978
|
+
return e;
|
|
580
979
|
}
|
|
581
980
|
}
|
|
582
981
|
/** Derive the allowed postMessage target origin from the configured iframe URL.
|
|
@@ -589,31 +988,31 @@ const v = `/* ── Host element ───────────────
|
|
|
589
988
|
}
|
|
590
989
|
}
|
|
591
990
|
_pushContextToIframe() {
|
|
592
|
-
var
|
|
593
|
-
if (!((
|
|
594
|
-
const
|
|
595
|
-
this._config.autoContext ?
|
|
596
|
-
var
|
|
597
|
-
(
|
|
991
|
+
var t;
|
|
992
|
+
if (!((t = this._iframeEl) != null && t.contentWindow)) return;
|
|
993
|
+
const e = this._iframeOrigin();
|
|
994
|
+
this._config.autoContext ? k.captureAsync(this._buildBaseContext()).then((n) => {
|
|
995
|
+
var r, a;
|
|
996
|
+
(a = (r = this._iframeEl) == null ? void 0 : r.contentWindow) == null || a.postMessage(
|
|
598
997
|
{ type: "btp-copilot:set-context", payload: n },
|
|
599
|
-
|
|
998
|
+
e
|
|
600
999
|
), n.service_url && !this._config.serviceUrl && (this._config = { ...this._config, serviceUrl: n.service_url });
|
|
601
1000
|
}).catch(() => {
|
|
602
|
-
var n,
|
|
603
|
-
(
|
|
1001
|
+
var n, r;
|
|
1002
|
+
(r = (n = this._iframeEl) == null ? void 0 : n.contentWindow) == null || r.postMessage(
|
|
604
1003
|
{ type: "btp-copilot:set-context", payload: this._buildBaseContext() },
|
|
605
|
-
|
|
1004
|
+
e
|
|
606
1005
|
);
|
|
607
1006
|
}) : this._iframeEl.contentWindow.postMessage(
|
|
608
1007
|
{ type: "btp-copilot:set-context", payload: this._buildBaseContext() },
|
|
609
|
-
|
|
1008
|
+
e
|
|
610
1009
|
);
|
|
611
1010
|
}
|
|
612
1011
|
_pushAuthToIframe() {
|
|
613
|
-
var
|
|
614
|
-
const
|
|
615
|
-
!
|
|
616
|
-
{ type: "btp-copilot:auth", token:
|
|
1012
|
+
var t;
|
|
1013
|
+
const e = S.resolve(this._config.token);
|
|
1014
|
+
!e || !((t = this._iframeEl) != null && t.contentWindow) || this._iframeEl.contentWindow.postMessage(
|
|
1015
|
+
{ type: "btp-copilot:auth", token: e },
|
|
617
1016
|
this._iframeOrigin()
|
|
618
1017
|
);
|
|
619
1018
|
}
|
|
@@ -627,39 +1026,40 @@ const v = `/* ── Host element ───────────────
|
|
|
627
1026
|
}
|
|
628
1027
|
// ── Config ─────────────────────────────────────────────────────────────────
|
|
629
1028
|
_readConfig() {
|
|
630
|
-
const
|
|
1029
|
+
const e = (t) => this.getAttribute(t);
|
|
631
1030
|
return {
|
|
632
|
-
iframeUrl:
|
|
633
|
-
appId:
|
|
634
|
-
appName:
|
|
635
|
-
serviceUrl:
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1031
|
+
iframeUrl: e("iframe-url") ?? "",
|
|
1032
|
+
appId: e("app-id") ?? "default",
|
|
1033
|
+
appName: e("app-name") ?? void 0,
|
|
1034
|
+
serviceUrl: e("service-url") ?? void 0,
|
|
1035
|
+
backendUrl: e("backend-url") ?? void 0,
|
|
1036
|
+
autoContext: e("auto-context") !== "false",
|
|
1037
|
+
position: e("position") ?? "bottom-right",
|
|
1038
|
+
theme: e("theme") ?? "auto",
|
|
1039
|
+
token: e("token") ?? void 0,
|
|
1040
|
+
contextInterval: Math.max(500, Number(e("context-interval")) || 3e3)
|
|
641
1041
|
};
|
|
642
1042
|
}
|
|
643
1043
|
_updateAfterAttributeChange() {
|
|
644
1044
|
var n;
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
this._iframeEl && this._iframeEl.src !==
|
|
1045
|
+
const e = (n = this.shadowRoot) == null ? void 0 : n.querySelector("style");
|
|
1046
|
+
e && (e.textContent = j);
|
|
1047
|
+
const t = this._buildIframeUrl();
|
|
1048
|
+
this._iframeEl && this._iframeEl.src !== t && t && (this._iframeEl.src = t), this._stopContextPolling(), this._startContextPolling();
|
|
649
1049
|
}
|
|
650
1050
|
_clearBadge() {
|
|
651
|
-
var
|
|
652
|
-
(
|
|
1051
|
+
var e, t;
|
|
1052
|
+
(t = (e = this.shadowRoot) == null ? void 0 : e.querySelector(".badge")) == null || t.classList.remove("visible");
|
|
653
1053
|
}
|
|
654
|
-
_esc(
|
|
655
|
-
return
|
|
1054
|
+
_esc(e) {
|
|
1055
|
+
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
656
1056
|
}
|
|
657
1057
|
};
|
|
658
|
-
|
|
659
|
-
let
|
|
660
|
-
customElements.get("btp-copilot") || customElements.define("btp-copilot",
|
|
1058
|
+
N.observedAttributes = [...O];
|
|
1059
|
+
let T = N;
|
|
1060
|
+
customElements.get("btp-copilot") || customElements.define("btp-copilot", T);
|
|
661
1061
|
export {
|
|
662
|
-
|
|
663
|
-
|
|
1062
|
+
T as BtpCopilotElement,
|
|
1063
|
+
k as ContextBridge
|
|
664
1064
|
};
|
|
665
1065
|
//# sourceMappingURL=btp-copilot.esm.js.map
|