@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/README.md
CHANGED
|
@@ -6,6 +6,8 @@ The widget connects lazily (a session opens the first time the visitor opens the
|
|
|
6
6
|
|
|
7
7
|
### Attributes
|
|
8
8
|
|
|
9
|
+
The **accent/logo/name** (workspace brand), the **teaser** (a chat-agent setting), and the **quick-reply chips** (generated from the agent's flow after each reply) are served automatically by the API — the attributes below just override them.
|
|
10
|
+
|
|
9
11
|
| Attribute | Required | What it is |
|
|
10
12
|
| --- | --- | --- |
|
|
11
13
|
| `api-key` | yes | Publishable key (`ohk_pub_…`) |
|
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,15 @@ async function openSession(opts) {
|
|
|
43
43
|
if (!res.ok) throw await asError(res);
|
|
44
44
|
return await res.json();
|
|
45
45
|
}
|
|
46
|
+
async function getWidgetConfig(opts) {
|
|
47
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/widget-config?agentId=${encodeURIComponent(opts.agentId)}`, {
|
|
48
|
+
method: "GET",
|
|
49
|
+
credentials: "omit",
|
|
50
|
+
headers: { Authorization: `Bearer ${opts.apiKey}` }
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw await asError(res);
|
|
53
|
+
return await res.json();
|
|
54
|
+
}
|
|
46
55
|
async function postMessage(opts) {
|
|
47
56
|
const body = { text: opts.text };
|
|
48
57
|
if (opts.attachments && opts.attachments.length > 0) body.attachments = opts.attachments;
|
|
@@ -220,6 +229,8 @@ function createChatClient(options) {
|
|
|
220
229
|
}
|
|
221
230
|
} else if (ev.type === "agent_typing") {
|
|
222
231
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
232
|
+
} else if (ev.type === "suggestions") {
|
|
233
|
+
emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
|
|
223
234
|
}
|
|
224
235
|
};
|
|
225
236
|
const openStream = async () => {
|
|
@@ -287,6 +298,9 @@ function createChatClient(options) {
|
|
|
287
298
|
if (!session) throw new Error("call start() before getAttachmentUrl");
|
|
288
299
|
return getAttachmentUrl({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, storageRef, fetchImpl });
|
|
289
300
|
},
|
|
301
|
+
getWidgetConfig() {
|
|
302
|
+
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
303
|
+
},
|
|
290
304
|
async start() {
|
|
291
305
|
if (state === "opening" || state === "ready") return;
|
|
292
306
|
closed = false;
|
|
@@ -533,8 +547,11 @@ var STYLES = `
|
|
|
533
547
|
.i-expand { opacity: 1; } .i-shrink { opacity: 0; }
|
|
534
548
|
.oc.max .i-expand { opacity: 0; } .oc.max .i-shrink { opacity: 1; }
|
|
535
549
|
|
|
536
|
-
|
|
537
|
-
.err
|
|
550
|
+
/* Inline connection error, shown inside the chat window (no banner) */
|
|
551
|
+
.oc-err { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 24px 16px; text-align: center; }
|
|
552
|
+
.oc-err-t { font-size: 13px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
553
|
+
.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); }
|
|
554
|
+
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
538
555
|
|
|
539
556
|
/* \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 */
|
|
540
557
|
.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; }
|
|
@@ -802,6 +819,13 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
802
819
|
renderedCount = 0;
|
|
803
820
|
renderedBotFinal = 0;
|
|
804
821
|
timers = [];
|
|
822
|
+
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
823
|
+
// widget prefers these over the static `suggestions` attribute.
|
|
824
|
+
serverSuggestions = null;
|
|
825
|
+
teaserText = "Need a hand? Ask me anything.";
|
|
826
|
+
teaserShownOnce = false;
|
|
827
|
+
idleTimer = null;
|
|
828
|
+
idleReset = null;
|
|
805
829
|
// ── shadow refs ──
|
|
806
830
|
oc;
|
|
807
831
|
bodyEl;
|
|
@@ -809,7 +833,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
809
833
|
fileEl;
|
|
810
834
|
titleEl;
|
|
811
835
|
statusEl;
|
|
812
|
-
errEl;
|
|
813
836
|
emojiListEl;
|
|
814
837
|
emojiSearchEl;
|
|
815
838
|
connectedCallback() {
|
|
@@ -821,6 +844,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
821
844
|
this.renderError("oyehello-chat: api-key, agent-id and base-url are required");
|
|
822
845
|
return;
|
|
823
846
|
}
|
|
847
|
+
this.teaserText = this.getAttribute("teaser") || this.teaserText;
|
|
824
848
|
this.client = createChatClient({ apiKey, agentId, baseUrl });
|
|
825
849
|
if (Object.keys(this.pendingTools).length > 0) {
|
|
826
850
|
this.client.registerTools(this.pendingTools);
|
|
@@ -832,6 +856,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
832
856
|
this.updateHeader();
|
|
833
857
|
this.updateStatus();
|
|
834
858
|
this.updateClasses();
|
|
859
|
+
void this.loadWidgetConfig();
|
|
835
860
|
let wasOpen = false;
|
|
836
861
|
try {
|
|
837
862
|
wasOpen = sessionStorage.getItem(this.key("open")) === "1";
|
|
@@ -845,24 +870,81 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
845
870
|
this.timers.push(setTimeout(() => {
|
|
846
871
|
this.oc?.classList.remove("ring");
|
|
847
872
|
}, 6e3));
|
|
848
|
-
|
|
873
|
+
this.armIdleTeaser();
|
|
874
|
+
}
|
|
875
|
+
// Show the teaser once the visitor has been idle (no page interaction) for 10s — unless the panel
|
|
876
|
+
// is already open or the teaser was dismissed this session. Fires at most once.
|
|
877
|
+
armIdleTeaser() {
|
|
849
878
|
let dismissed = false;
|
|
850
879
|
try {
|
|
851
880
|
dismissed = sessionStorage.getItem(this.key("teaser")) === "0";
|
|
852
881
|
} catch {
|
|
853
882
|
}
|
|
854
|
-
if (
|
|
855
|
-
|
|
856
|
-
|
|
883
|
+
if (dismissed) return;
|
|
884
|
+
const reset = () => {
|
|
885
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
886
|
+
this.idleTimer = setTimeout(() => {
|
|
887
|
+
if (!this.uiOpen && !this.teaserShownOnce) {
|
|
857
888
|
this.teaserActive = true;
|
|
889
|
+
this.teaserShownOnce = true;
|
|
858
890
|
this.updateClasses();
|
|
891
|
+
this.teardownIdle();
|
|
892
|
+
}
|
|
893
|
+
}, 1e4);
|
|
894
|
+
};
|
|
895
|
+
this.idleReset = reset;
|
|
896
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.addEventListener(ev, reset, { passive: true });
|
|
897
|
+
reset();
|
|
898
|
+
}
|
|
899
|
+
teardownIdle() {
|
|
900
|
+
if (this.idleTimer) {
|
|
901
|
+
clearTimeout(this.idleTimer);
|
|
902
|
+
this.idleTimer = null;
|
|
903
|
+
}
|
|
904
|
+
if (this.idleReset) {
|
|
905
|
+
for (const ev of ["mousemove", "keydown", "scroll", "touchstart", "click"]) document.removeEventListener(ev, this.idleReset);
|
|
906
|
+
this.idleReset = null;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async loadWidgetConfig() {
|
|
910
|
+
try {
|
|
911
|
+
const cfg = await this.client.getWidgetConfig();
|
|
912
|
+
if (!this.getAttribute("teaser") && cfg.teaser) {
|
|
913
|
+
this.teaserText = cfg.teaser;
|
|
914
|
+
this.applyTeaserText();
|
|
915
|
+
}
|
|
916
|
+
if (cfg.brand) {
|
|
917
|
+
if (cfg.brand.accentColor) this.style.setProperty("--oc-accent", cfg.brand.accentColor);
|
|
918
|
+
if (cfg.brand.name && !this.getAttribute("title")) {
|
|
919
|
+
this.agentName = cfg.brand.name;
|
|
920
|
+
this.updateHeader();
|
|
859
921
|
}
|
|
860
|
-
|
|
922
|
+
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
923
|
+
}
|
|
924
|
+
} catch {
|
|
861
925
|
}
|
|
862
926
|
}
|
|
927
|
+
applyTeaserText() {
|
|
928
|
+
const el = this.shadowRoot?.querySelector(".teaser-btn span:last-child");
|
|
929
|
+
if (el) el.textContent = this.teaserText;
|
|
930
|
+
}
|
|
931
|
+
applyBrandLogo(url) {
|
|
932
|
+
const set = (host) => {
|
|
933
|
+
if (!host) return;
|
|
934
|
+
host.textContent = "";
|
|
935
|
+
const img = document.createElement("img");
|
|
936
|
+
img.src = url;
|
|
937
|
+
img.alt = "";
|
|
938
|
+
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
|
939
|
+
host.appendChild(img);
|
|
940
|
+
};
|
|
941
|
+
set(this.shadowRoot?.querySelector(".hd .logo"));
|
|
942
|
+
set(this.shadowRoot?.querySelector(".teaser-btn .logo"));
|
|
943
|
+
}
|
|
863
944
|
disconnectedCallback() {
|
|
864
945
|
this.off?.();
|
|
865
946
|
void this.client?.end();
|
|
947
|
+
this.teardownIdle();
|
|
866
948
|
for (const t of this.timers) clearTimeout(t);
|
|
867
949
|
for (const url of this.imagePreviewCache.values()) {
|
|
868
950
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -876,18 +958,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
876
958
|
key(k) {
|
|
877
959
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
878
960
|
}
|
|
879
|
-
|
|
961
|
+
// Static fallback chips from the `suggestions` attribute (for embedders without flow-driven ones).
|
|
962
|
+
get attrSuggestions() {
|
|
880
963
|
return (this.getAttribute("suggestions") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
881
964
|
}
|
|
965
|
+
// Effective chips: server (flow) suggestions once received, else the static attribute fallback.
|
|
966
|
+
get chipLabels() {
|
|
967
|
+
return this.serverSuggestions ?? this.attrSuggestions;
|
|
968
|
+
}
|
|
882
969
|
// ── Shell (built once) ─────────────────────────────────────────────────────────────────────────
|
|
883
970
|
renderShell() {
|
|
884
971
|
const root = this.shadowRoot;
|
|
885
|
-
const hasTeaser = !!this.getAttribute("teaser");
|
|
886
972
|
const hasPrivacy = !!this.getAttribute("privacy-url");
|
|
887
|
-
const teaserText = this.
|
|
973
|
+
const teaserText = this.teaserText;
|
|
888
974
|
root.innerHTML = `
|
|
889
975
|
<style>${STYLES}</style>
|
|
890
|
-
<div class="oc
|
|
976
|
+
<div class="oc has-teaser${hasPrivacy ? " has-privacy" : ""}">
|
|
891
977
|
<div class="panel" role="dialog" aria-label="Chat">
|
|
892
978
|
<div class="hd">
|
|
893
979
|
<span class="hd-grid"></span>
|
|
@@ -896,7 +982,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
896
982
|
<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>
|
|
897
983
|
<button type="button" class="hd-btn min" aria-label="Minimise">${ICON.minus}</button>
|
|
898
984
|
</div>
|
|
899
|
-
<div class="err" hidden></div>
|
|
900
985
|
<div class="body"></div>
|
|
901
986
|
<span class="emoji-scrim"></span>
|
|
902
987
|
<div class="emoji-panel">
|
|
@@ -940,7 +1025,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
940
1025
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
941
1026
|
this.titleEl = root.querySelector(".hd-title");
|
|
942
1027
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
943
|
-
this.errEl = root.querySelector(".err");
|
|
944
1028
|
this.emojiListEl = root.querySelector(".emoji-list");
|
|
945
1029
|
this.emojiSearchEl = root.querySelector(".emoji-search input");
|
|
946
1030
|
root.querySelector(".launcher").addEventListener("click", () => this.toggle());
|
|
@@ -1021,6 +1105,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1021
1105
|
this.uiOpen = true;
|
|
1022
1106
|
this.teaserActive = false;
|
|
1023
1107
|
this.unseen = 0;
|
|
1108
|
+
this.teardownIdle();
|
|
1024
1109
|
try {
|
|
1025
1110
|
sessionStorage.setItem(this.key("open"), "1");
|
|
1026
1111
|
} catch {
|
|
@@ -1063,6 +1148,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1063
1148
|
this.updateHeader();
|
|
1064
1149
|
} else if (e.type === "handoff") this.handoffState = e.state;
|
|
1065
1150
|
else if (e.type === "agentTyping") this.agentTyping = e.active;
|
|
1151
|
+
else if (e.type === "suggestions") this.serverSuggestions = e.items;
|
|
1066
1152
|
else if (e.type === "error") this.lastError = e.error.message;
|
|
1067
1153
|
this.updateStatus();
|
|
1068
1154
|
this.renderMessages();
|
|
@@ -1077,16 +1163,6 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1077
1163
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1078
1164
|
const text = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1079
1165
|
if (this.statusEl) this.statusEl.textContent = text;
|
|
1080
|
-
if (this.state === "error" && this.lastError) {
|
|
1081
|
-
this.errEl.hidden = false;
|
|
1082
|
-
this.errEl.innerHTML = `<span>Couldn't start chat: ${escapeHtml(this.lastError)}</span><button class="retry" type="button">Retry</button>`;
|
|
1083
|
-
this.errEl.querySelector(".retry").addEventListener("click", () => {
|
|
1084
|
-
void this.client?.start();
|
|
1085
|
-
});
|
|
1086
|
-
} else {
|
|
1087
|
-
this.errEl.hidden = true;
|
|
1088
|
-
this.errEl.innerHTML = "";
|
|
1089
|
-
}
|
|
1090
1166
|
}
|
|
1091
1167
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
1092
1168
|
renderMessages() {
|
|
@@ -1104,10 +1180,13 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1104
1180
|
})();
|
|
1105
1181
|
let html = this.messages.map((m, i) => this.turnHtml(m, i === lastBot && !this.agentTyping)).join("");
|
|
1106
1182
|
if (this.agentTyping) html += '<span class="typing"><span></span><span></span><span></span></span>';
|
|
1107
|
-
const sugg = this.
|
|
1183
|
+
const sugg = this.chipLabels;
|
|
1108
1184
|
if (sugg.length > 0 && !this.agentTyping && this.state === "ready") {
|
|
1109
1185
|
html += `<span class="chips">${sugg.map((s, i) => `<button type="button" class="chip" data-chip="${i}">${escapeHtml(s)}</button>`).join("")}</span>`;
|
|
1110
1186
|
}
|
|
1187
|
+
if (this.state === "error" && this.lastError) {
|
|
1188
|
+
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>`;
|
|
1189
|
+
}
|
|
1111
1190
|
body.innerHTML = html;
|
|
1112
1191
|
this.hydrateAttachmentImages();
|
|
1113
1192
|
this.bindChips();
|
|
@@ -1185,12 +1264,15 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1185
1264
|
this.updateClasses();
|
|
1186
1265
|
}
|
|
1187
1266
|
bindChips() {
|
|
1188
|
-
const sugg = this.
|
|
1267
|
+
const sugg = this.chipLabels;
|
|
1189
1268
|
this.bodyEl.querySelectorAll(".chip[data-chip]").forEach((btn) => {
|
|
1190
1269
|
const i = Number(btn.getAttribute("data-chip"));
|
|
1191
1270
|
const text = sugg[i];
|
|
1192
1271
|
if (text) btn.addEventListener("click", () => this.sendChip(text));
|
|
1193
1272
|
});
|
|
1273
|
+
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
1274
|
+
void this.client?.start();
|
|
1275
|
+
});
|
|
1194
1276
|
}
|
|
1195
1277
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
1196
1278
|
toggleEmoji() {
|