@oyehello/chat 0.3.0 → 0.4.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 +2 -0
- package/dist/index.cjs +108 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -3
- package/dist/index.d.ts +29 -3
- package/dist/index.global.js +116 -34
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +108 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -37,6 +37,15 @@ async function openSession(opts) {
|
|
|
37
37
|
if (!res.ok) throw await asError(res);
|
|
38
38
|
return await res.json();
|
|
39
39
|
}
|
|
40
|
+
async function getWidgetConfig(opts) {
|
|
41
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/widget-config?agentId=${encodeURIComponent(opts.agentId)}`, {
|
|
42
|
+
method: "GET",
|
|
43
|
+
credentials: "omit",
|
|
44
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) throw await asError(res);
|
|
47
|
+
return await res.json();
|
|
48
|
+
}
|
|
40
49
|
async function postMessage(opts) {
|
|
41
50
|
const body = { text: opts.text };
|
|
42
51
|
if (opts.attachments && opts.attachments.length > 0) body.attachments = opts.attachments;
|
|
@@ -214,6 +223,8 @@ function createChatClient(options) {
|
|
|
214
223
|
}
|
|
215
224
|
} else if (ev.type === "agent_typing") {
|
|
216
225
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
226
|
+
} else if (ev.type === "suggestions") {
|
|
227
|
+
emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
|
|
217
228
|
}
|
|
218
229
|
};
|
|
219
230
|
const openStream = async () => {
|
|
@@ -281,6 +292,9 @@ function createChatClient(options) {
|
|
|
281
292
|
if (!session) throw new Error("call start() before getAttachmentUrl");
|
|
282
293
|
return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
|
|
283
294
|
},
|
|
295
|
+
getWidgetConfig() {
|
|
296
|
+
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
297
|
+
},
|
|
284
298
|
async start() {
|
|
285
299
|
if (state === "opening" || state === "ready") return;
|
|
286
300
|
closed = false;
|
|
@@ -527,8 +541,11 @@ var STYLES = `
|
|
|
527
541
|
.i-expand { opacity: 1; } .i-shrink { opacity: 0; }
|
|
528
542
|
.oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
|
|
529
543
|
|
|
530
|
-
|
|
531
|
-
.err
|
|
544
|
+
/* Inline connection error, shown inside the chat window (no banner) */
|
|
545
|
+
.oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
|
|
546
|
+
.oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
547
|
+
.oc-err-retry { padding: 7px 15px; font-size: 12.5px; font-weight: 500; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-2); color: var(--oc-fg); cursor: pointer; transition: background .18s var(--oc-ease); }
|
|
548
|
+
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
532
549
|
|
|
533
550
|
/* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
534
551
|
.body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 16px 26px; background: var(--oc-bg); scroll-behavior: smooth; }
|
|
@@ -796,6 +813,13 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
796
813
|
renderedCount = 0;
|
|
797
814
|
renderedBotFinal = 0;
|
|
798
815
|
timers = [];
|
|
816
|
+
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
817
|
+
// widget prefers these over the static `suggestions` attribute.
|
|
818
|
+
serverSuggestions = null;
|
|
819
|
+
teaserText = "Need a hand? Ask me anything.";
|
|
820
|
+
teaserShownOnce = false;
|
|
821
|
+
idleTimer = null;
|
|
822
|
+
idleReset = null;
|
|
799
823
|
// ── shadow refs ──
|
|
800
824
|
oc;
|
|
801
825
|
bodyEl;
|
|
@@ -803,7 +827,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
803
827
|
fileEl;
|
|
804
828
|
titleEl;
|
|
805
829
|
statusEl;
|
|
806
|
-
errEl;
|
|
807
830
|
emojiListEl;
|
|
808
831
|
emojiSearchEl;
|
|
809
832
|
connectedCallback() {
|
|
@@ -815,6 +838,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
815
838
|
this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
|
|
816
839
|
return;
|
|
817
840
|
}
|
|
841
|
+
this.teaserText = this.getAttribute("teaser") || this.teaserText;
|
|
818
842
|
this.client = createChatClient({ apiKey, agentId, baseUrl });
|
|
819
843
|
if (Object.keys(this.pendingTools).length > 0) {
|
|
820
844
|
this.client.registerTools(this.pendingTools);
|
|
@@ -826,6 +850,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
826
850
|
this.updateHeader();
|
|
827
851
|
this.updateStatus();
|
|
828
852
|
this.updateClasses();
|
|
853
|
+
void this.loadWidgetConfig();
|
|
829
854
|
let wasOpen = false;
|
|
830
855
|
try {
|
|
831
856
|
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
@@ -839,24 +864,81 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
839
864
|
this.timers.push(setTimeout(() => {
|
|
840
865
|
this.oc?.classList.remove("ring");
|
|
841
866
|
}, 6e3));
|
|
842
|
-
|
|
867
|
+
this.armIdleTeaser();
|
|
868
|
+
}
|
|
869
|
+
// Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
|
|
870
|
+
// is already open or the teaser was dismissed this session. Fires at most once.
|
|
871
|
+
armIdleTeaser() {
|
|
843
872
|
let dismissed = false;
|
|
844
873
|
try {
|
|
845
874
|
dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
|
|
846
875
|
} catch {
|
|
847
876
|
}
|
|
848
|
-
if (
|
|
849
|
-
|
|
850
|
-
|
|
877
|
+
if (dismissed) return;
|
|
878
|
+
const reset = () => {
|
|
879
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
880
|
+
this.idleTimer = setTimeout(() => {
|
|
881
|
+
if (!this.uiOpen && !this.teaserShownOnce) {
|
|
851
882
|
this.teaserActive = true;
|
|
883
|
+
this.teaserShownOnce = true;
|
|
852
884
|
this.updateClasses();
|
|
885
|
+
this.teardownIdle();
|
|
886
|
+
}
|
|
887
|
+
}, 1e4);
|
|
888
|
+
};
|
|
889
|
+
this.idleReset = reset;
|
|
890
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
|
|
891
|
+
reset();
|
|
892
|
+
}
|
|
893
|
+
teardownIdle() {
|
|
894
|
+
if (this.idleTimer) {
|
|
895
|
+
clearTimeout(this.idleTimer);
|
|
896
|
+
this.idleTimer = null;
|
|
897
|
+
}
|
|
898
|
+
if (this.idleReset) {
|
|
899
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
|
|
900
|
+
this.idleReset = null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async loadWidgetConfig() {
|
|
904
|
+
try {
|
|
905
|
+
const cfg = await this.client.getWidgetConfig();
|
|
906
|
+
if (!this.getAttribute("teaser") && cfg.teaser) {
|
|
907
|
+
this.teaserText = cfg.teaser;
|
|
908
|
+
this.applyTeaserText();
|
|
909
|
+
}
|
|
910
|
+
if (cfg.brand) {
|
|
911
|
+
if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
|
|
912
|
+
if (cfg.brand.name && !this.getAttribute("title")) {
|
|
913
|
+
this.agentName = cfg.brand.name;
|
|
914
|
+
this.updateHeader();
|
|
853
915
|
}
|
|
854
|
-
|
|
916
|
+
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
855
919
|
}
|
|
856
920
|
}
|
|
921
|
+
applyTeaserText() {
|
|
922
|
+
const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
|
|
923
|
+
if (el) el.textContent = this.teaserText;
|
|
924
|
+
}
|
|
925
|
+
applyBrandLogo(url) {
|
|
926
|
+
const set = (host) => {
|
|
927
|
+
if (!host) return;
|
|
928
|
+
host.textContent = "";
|
|
929
|
+
const img = document.createElement("img");
|
|
930
|
+
img.src = url;
|
|
931
|
+
img.alt = "";
|
|
932
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
|
933
|
+
host.appendChild(img);
|
|
934
|
+
};
|
|
935
|
+
set(this.shadowRoot?.querySelector(".hd .logo"));
|
|
936
|
+
set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
|
|
937
|
+
}
|
|
857
938
|
disconnectedCallback() {
|
|
858
939
|
this.off?.();
|
|
859
940
|
void this.client?.end();
|
|
941
|
+
this.teardownIdle();
|
|
860
942
|
for (const t of this.timers) clearTimeout(t);
|
|
861
943
|
for (const url of this.imagePreviewCache.values()) {
|
|
862
944
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -870,18 +952,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
870
952
|
key(k) {
|
|
871
953
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
872
954
|
}
|
|
873
|
-
|
|
955
|
+
// Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
|
|
956
|
+
get attrSuggestions() {
|
|
874
957
|
return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
875
958
|
}
|
|
959
|
+
// Effective chips: server (flow) suggestions once received, else the static attribute fallback.
|
|
960
|
+
get chipLabels() {
|
|
961
|
+
return this.serverSuggestions ?? this.attrSuggestions;
|
|
962
|
+
}
|
|
876
963
|
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
877
964
|
renderShell() {
|
|
878
965
|
const root = this.shadowRoot;
|
|
879
|
-
const hasTeaser = !!this.getAttribute("teaser");
|
|
880
966
|
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
881
|
-
const teaserText = this.
|
|
967
|
+
const teaserText = this.teaserText;
|
|
882
968
|
root.innerHTML = `
|
|
883
969
|
<style>${STYLES}</style>
|
|
884
|
-
<div class="oc
|
|
970
|
+
<div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
|
|
885
971
|
<div class="panel" role="dialog" aria-label="Chat">
|
|
886
972
|
<div class="hd">
|
|
887
973
|
<span class="hd-grid"></span>
|
|
@@ -890,7 +976,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
890
976
|
<button type="button" class="hd-btn max" aria-label="Expand"><span class="i i-expand">${ICON.expand}</span><span class="i i-shrink">${ICON.shrink}</span></button>
|
|
891
977
|
<button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
|
|
892
978
|
</div>
|
|
893
|
-
<div class="err" hidden></div>
|
|
894
979
|
<div class="body"></div>
|
|
895
980
|
<span class="emoji-scrim"></span>
|
|
896
981
|
<div class="emoji-panel">
|
|
@@ -934,7 +1019,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
934
1019
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
935
1020
|
this.titleEl = root.querySelector(".hd-title");
|
|
936
1021
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
937
|
-
this.errEl = root.querySelector(".err");
|
|
938
1022
|
this.emojiListEl = root.querySelector(".emoji-list");
|
|
939
1023
|
this.emojiSearchEl = root.querySelector(".emoji-search input");
|
|
940
1024
|
root.querySelector(".launcher").addEventListener("click", () => this.toggle());
|
|
@@ -1015,6 +1099,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1015
1099
|
this.uiOpen = true;
|
|
1016
1100
|
this.teaserActive = false;
|
|
1017
1101
|
this.unseen = 0;
|
|
1102
|
+
this.teardownIdle();
|
|
1018
1103
|
try {
|
|
1019
1104
|
sessionStorage.setItem(this.key("open"), "1");
|
|
1020
1105
|
} catch {
|
|
@@ -1057,6 +1142,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1057
1142
|
this.updateHeader();
|
|
1058
1143
|
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
1059
1144
|
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
1145
|
+
else if (e.type === "suggestions") this.serverSuggestions = e.items;
|
|
1060
1146
|
else if (e.type === "error") this.lastError = e.error.message;
|
|
1061
1147
|
this.updateStatus();
|
|
1062
1148
|
this.renderMessages();
|
|
@@ -1071,16 +1157,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1071
1157
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1072
1158
|
const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1073
1159
|
if (this.statusEl) this.statusEl.textContent = text;
|
|
1074
|
-
if (this.state === "error" && this.lastError) {
|
|
1075
|
-
this.errEl.hidden = false;
|
|
1076
|
-
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
1077
|
-
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
1078
|
-
void this.client?.start();
|
|
1079
|
-
});
|
|
1080
|
-
} else {
|
|
1081
|
-
this.errEl.hidden = true;
|
|
1082
|
-
this.errEl.innerHTML = "";
|
|
1083
|
-
}
|
|
1084
1160
|
}
|
|
1085
1161
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
1086
1162
|
renderMessages() {
|
|
@@ -1098,10 +1174,13 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1098
1174
|
})();
|
|
1099
1175
|
let html = this.messages.map((m, i) => this.turnHtml(m, i === lastBot && !this.agentTyping)).join("");
|
|
1100
1176
|
if (this.agentTyping) html += '<span class="typing"><span></span><span></span><span></span></span>';
|
|
1101
|
-
const sugg = this.
|
|
1177
|
+
const sugg = this.chipLabels;
|
|
1102
1178
|
if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
|
|
1103
1179
|
html += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
|
|
1104
1180
|
}
|
|
1181
|
+
if (this.state === "error" && this.lastError) {
|
|
1182
|
+
html += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
|
|
1183
|
+
}
|
|
1105
1184
|
body.innerHTML = html;
|
|
1106
1185
|
this.hydrateAttachmentImages();
|
|
1107
1186
|
this.bindChips();
|
|
@@ -1179,12 +1258,15 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1179
1258
|
this.updateClasses();
|
|
1180
1259
|
}
|
|
1181
1260
|
bindChips() {
|
|
1182
|
-
const sugg = this.
|
|
1261
|
+
const sugg = this.chipLabels;
|
|
1183
1262
|
this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
|
|
1184
1263
|
const i = Number(btn.getAttribute("data-chip"));
|
|
1185
1264
|
const text = sugg[i];
|
|
1186
1265
|
if (text) btn.addEventListener("click", () => this.sendChip(text));
|
|
1187
1266
|
});
|
|
1267
|
+
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
1268
|
+
void this.client?.start();
|
|
1269
|
+
});
|
|
1188
1270
|
}
|
|
1189
1271
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
1190
1272
|
toggleEmoji() {
|