@sub-zero/cocoa 0.2.0 → 0.2.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/README.md +8 -0
- package/dist/cocoa.js +71 -2
- package/dist/index.cjs +68 -2
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +68 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,3 +169,11 @@ may be private.
|
|
|
169
169
|
The visible **Get further help** action opens a review form. The visitor chooses a correspondence email and explicitly submits an incident; neither opening chat nor mentioning an email creates one. The current visitor session authorizes creation in the chatbot's project. A confirmed reference survives reload, and failed submissions retain their text and idempotency key for retry. The team's follow-up is by email. The email is not verified account identity.
|
|
170
170
|
|
|
171
171
|
Requires the SubZero chat-escalation API. Closed conversations can still submit while their visitor credential is valid; paused deployments cannot accept new submissions. No model call is needed for handoff. `reset()` clears the chat, pending form and visitor credential; call it on logout/account changes. SMTP acknowledgement delivery retries are at-least-once; a stable Message-ID assists threading but cannot guarantee exactly-once delivery by an external mail server.
|
|
172
|
+
|
|
173
|
+
### Greeting and reply feedback
|
|
174
|
+
|
|
175
|
+
The launcher shows “Hi! Ask me anything.” after five seconds, once per browser tab. Click to chat or dismiss it. Opening chat first suppresses the greeting. A soft chime plays only if a previous user interaction has enabled browser audio; otherwise the greeting appears silently. Nothing starts a conversation until the visitor sends a message or explicitly submits an incident.
|
|
176
|
+
|
|
177
|
+
Customize with `greetingMessage` (string, or `false` to disable), `greetingDelay` (milliseconds), and `greetingSound` (`false` for silent greetings). Script embeds accept `data-greeting-message`, `data-greeting-delay` and `data-greeting-sound`.
|
|
178
|
+
|
|
179
|
+
Animated dots indicate a reply is pending while the agent is online. They clear when an answer arrives, a person takes over, the chat is paused/closed, an incident is submitted, or a request fails. Reduced-motion preferences disable animation. The indicator reflects an unanswered message, not provider-level token streaming.
|
package/dist/cocoa.js
CHANGED
|
@@ -23,6 +23,8 @@ function createCocoa(options = {}) {
|
|
|
23
23
|
if (!Number.isInteger(zIndex) || zIndex < 0 || zIndex > 2147483647) throw new Error('Invalid zIndex.');
|
|
24
24
|
const pollInterval = options.pollInterval ?? 2500;
|
|
25
25
|
if (!Number.isFinite(pollInterval) || pollInterval < 2500) throw new Error('pollInterval must be at least 2500ms.');
|
|
26
|
+
const greetingDelay = options.greetingDelay ?? 5000;
|
|
27
|
+
if (!Number.isFinite(greetingDelay) || greetingDelay < 0) throw new Error('greetingDelay must be a nonnegative number.');
|
|
26
28
|
const host = document.createElement('div');
|
|
27
29
|
host.dataset.cocoaWidget = '';
|
|
28
30
|
const root = host.attachShadow({ mode: 'open' });
|
|
@@ -33,6 +35,9 @@ function createCocoa(options = {}) {
|
|
|
33
35
|
*,*::before,*::after{box-sizing:border-box} [hidden]{display:none!important}
|
|
34
36
|
button,input,textarea{font:inherit}button{cursor:pointer;border:1px solid #647078;border-radius:12px;padding:10px 14px;background:#26343a;color:#fff}button:hover{filter:brightness(1.15)}button:disabled{opacity:.55;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #99dbc4;outline-offset:2px}
|
|
35
37
|
.launcher{display:block;margin-left:auto;width:72px;height:72px;padding:0;border:2px solid #e1f4fa;border-radius:50%;overflow:hidden;background:#92c5d5;box-shadow:0 6px 24px #0005}.launcher img{display:block;width:100%;height:100%;object-fit:cover;transform:scale(1.18);pointer-events:none}.launcher:hover{filter:brightness(1.06)}
|
|
38
|
+
.greeting{position:relative;margin:0 0 12px auto;display:flex;align-items:center;max-width:calc(100vw - 32px);width:250px;background:#fff5e9;color:#392516;border-radius:18px;box-shadow:0 6px 24px #0003;padding:6px}.greeting::after{content:'';position:absolute;right:28px;bottom:-8px;border:8px solid transparent;border-top-color:#fff5e9;border-bottom:0}.greeting button{color:inherit;background:transparent;border:0}.greeting .greeting-open{text-align:left;flex:1}.greeting .greeting-dismiss{padding:8px;min-width:40px;min-height:40px}
|
|
39
|
+
.thinking{display:flex;align-items:center;gap:8px;padding:8px 16px;color:#c8d4dd;font-size:12px}.dots{display:flex;gap:4px}.dots i{width:5px;height:5px;border-radius:50%;background:#92c5d5}
|
|
40
|
+
@media(prefers-reduced-motion:no-preference){.greeting{animation:appear .25s ease-out}.dots i{animation:think 1.2s ease-in-out infinite}.dots i:nth-child(2){animation-delay:.15s}.dots i:nth-child(3){animation-delay:.3s}@keyframes think{0%,60%,100%{opacity:.4;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}}
|
|
36
41
|
.panel{display:flex;flex-direction:column;width:370px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100dvh - 116px);background:#141c22;border:1px solid #44515b;border-radius:18px;box-shadow:0 12px 50px #0005;overflow:hidden;margin-bottom:12px}
|
|
37
42
|
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid #39454e}header strong{font-size:16px;overflow-wrap:anywhere}header small{display:block;color:#bac9d2}header button{min-width:44px;min-height:44px;font-size:22px;background:transparent}
|
|
38
43
|
.messages{flex:1;min-height:50px;overflow:auto;overscroll-behavior:contain;padding:16px;display:flex;flex-direction:column;gap:12px}.message{flex-shrink:0;align-self:flex-start;max-width:92%;background:#27333d;padding:10px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.customer{align-self:flex-end;background:${accent}}.message small{display:block;color:#fff;font-weight:600;font-size:11px;margin-bottom:4px}
|
|
@@ -84,6 +89,12 @@ function createCocoa(options = {}) {
|
|
|
84
89
|
handoff.append(handoffError, handoffActions);
|
|
85
90
|
const handoffResult = element('p', {class:'handoff-result',role:'status',tabindex:'-1',hidden:''});
|
|
86
91
|
panel.append(help, handoff, handoffResult);
|
|
92
|
+
const thinking = element('div', {class:'thinking',role:'status',hidden:''});
|
|
93
|
+
const dots = element('span', {class:'dots','aria-hidden':'true'});
|
|
94
|
+
dots.append(element('i'),element('i'),element('i'));
|
|
95
|
+
const thinkingLabel = element('span', {}, 'Cocoa is thinking…');
|
|
96
|
+
thinking.append(dots, thinkingLabel);
|
|
97
|
+
composer.before(thinking);
|
|
87
98
|
panel.querySelector('strong').textContent = options.title || 'Cocoa support';
|
|
88
99
|
const launcher = document.createElement('button');
|
|
89
100
|
launcher.type = 'button';
|
|
@@ -98,7 +109,11 @@ function createCocoa(options = {}) {
|
|
|
98
109
|
launcher.setAttribute('aria-expanded', 'false');
|
|
99
110
|
launcher.setAttribute('aria-controls', panel.id);
|
|
100
111
|
launcher.setAttribute('aria-haspopup', 'dialog');
|
|
101
|
-
|
|
112
|
+
const greeting = element('div', {class:'greeting',hidden:''});
|
|
113
|
+
const greetingOpen = element('button', {type:'button',class:'greeting-open'}, options.greetingMessage || 'Hi! Ask me anything.');
|
|
114
|
+
const greetingDismiss = element('button', {type:'button',class:'greeting-dismiss','aria-label':'Dismiss greeting'}, '×');
|
|
115
|
+
greeting.append(greetingOpen,greetingDismiss);
|
|
116
|
+
root.append(panel, greeting, launcher);
|
|
102
117
|
document.body.append(host);
|
|
103
118
|
const messages = panel.querySelector('.messages');
|
|
104
119
|
const status = panel.querySelector('.status');
|
|
@@ -111,6 +126,41 @@ function createCocoa(options = {}) {
|
|
|
111
126
|
let session = null, pending = null, timer, destroyed = false, sending = false, refreshing = false;
|
|
112
127
|
let generation = 0, terminal = false, paused = false, version = -1, failures = 0;
|
|
113
128
|
let incident = null, incidentPending = null, submittingIncident = false, expired = false;
|
|
129
|
+
let greetingTimer, audioContext, greetingSeen = false;
|
|
130
|
+
const greetingKey = key + ':greeted';
|
|
131
|
+
try { greetingSeen = sessionStorage.getItem(greetingKey) === '1'; } catch {}
|
|
132
|
+
const dismissGreeting = (remember = true) => {
|
|
133
|
+
clearTimeout(greetingTimer); greeting.hidden = true;
|
|
134
|
+
if (remember) { greetingSeen = true; try { sessionStorage.setItem(greetingKey,'1'); } catch {} }
|
|
135
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
136
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
137
|
+
if (audioContext) { void audioContext.close().catch(()=>{}); audioContext = null; }
|
|
138
|
+
};
|
|
139
|
+
function unlockAudio() {
|
|
140
|
+
if (destroyed || greetingSeen || options.greetingSound === false || audioContext) return;
|
|
141
|
+
try {
|
|
142
|
+
const Audio = window.AudioContext || window.webkitAudioContext;
|
|
143
|
+
if (Audio) { audioContext = new Audio(); void audioContext.resume().catch(()=>{}); }
|
|
144
|
+
} catch { /* Greeting still works when audio is unavailable. */ }
|
|
145
|
+
}
|
|
146
|
+
function showGreeting() {
|
|
147
|
+
if (destroyed || greetingSeen || !panel.hidden || session || options.greetingMessage === false || document.hidden) return;
|
|
148
|
+
greeting.hidden = false; greetingSeen = true;
|
|
149
|
+
try { sessionStorage.setItem(greetingKey,'1'); } catch {}
|
|
150
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
151
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
152
|
+
// Only play after a prior user gesture unlocked audio. Never queue a surprise sound.
|
|
153
|
+
if (audioContext?.state === 'running') {
|
|
154
|
+
try {
|
|
155
|
+
const tone = audioContext.createOscillator(), volume = audioContext.createGain();
|
|
156
|
+
const now = audioContext.currentTime;
|
|
157
|
+
tone.type = 'sine'; tone.frequency.setValueAtTime(660,now); tone.frequency.exponentialRampToValueAtTime(880,now+.1);
|
|
158
|
+
volume.gain.setValueAtTime(0,now); volume.gain.linearRampToValueAtTime(.045,now+.015); volume.gain.exponentialRampToValueAtTime(.001,now+.2);
|
|
159
|
+
tone.connect(volume); volume.connect(audioContext.destination); tone.start(now); tone.stop(now+.22);
|
|
160
|
+
tone.onended = () => { tone.disconnect(); volume.disconnect(); if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } };
|
|
161
|
+
} catch { if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } }
|
|
162
|
+
} else if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; }
|
|
163
|
+
}
|
|
114
164
|
const rendered = new Set();
|
|
115
165
|
const requests = new Set();
|
|
116
166
|
const persist = () => {
|
|
@@ -146,6 +196,7 @@ function createCocoa(options = {}) {
|
|
|
146
196
|
welcome();
|
|
147
197
|
const controls = () => {
|
|
148
198
|
const focusWasInComposer = root.activeElement === input || root.activeElement === send;
|
|
199
|
+
if (incident || !handoff.hidden || terminal || paused) thinking.hidden = true;
|
|
149
200
|
help.disabled = sending || submittingIncident || expired || paused;
|
|
150
201
|
help.hidden = !!incident || !handoff.hidden;
|
|
151
202
|
handoffResult.hidden = !incident;
|
|
@@ -162,6 +213,7 @@ function createCocoa(options = {}) {
|
|
|
162
213
|
};
|
|
163
214
|
controls();
|
|
164
215
|
const report = (error) => {
|
|
216
|
+
thinking.hidden = true;
|
|
165
217
|
status.classList.add('error');
|
|
166
218
|
status.textContent = error.message;
|
|
167
219
|
try { options.onError?.(error); } catch { /* Host callback must not break chat. */ }
|
|
@@ -230,6 +282,8 @@ function createCocoa(options = {}) {
|
|
|
230
282
|
status.textContent = incident ? 'Your incident is with the support team. Follow-up will be by email.' : terminal ? 'This conversation is closed.' : paused ? 'This chat is paused.' :
|
|
231
283
|
data.state === 'human' ? 'A person is handling your conversation.' :
|
|
232
284
|
data.agentOnline ? 'Connected to support.' : 'The agent is offline. Your messages are saved for the team.';
|
|
285
|
+
const last = data.messages.filter(m=>['customer','agent','human'].includes(m.actor)).at(-1);
|
|
286
|
+
thinking.hidden = !(last?.actor === 'customer' && data.agentOnline && data.state === 'agent' && !terminal && !paused && !incident && handoff.hidden);
|
|
233
287
|
controls();
|
|
234
288
|
}
|
|
235
289
|
function schedule() {
|
|
@@ -262,6 +316,7 @@ function createCocoa(options = {}) {
|
|
|
262
316
|
}
|
|
263
317
|
function open() {
|
|
264
318
|
if (destroyed) return;
|
|
319
|
+
dismissGreeting();
|
|
265
320
|
panel.hidden = false;
|
|
266
321
|
launcher.setAttribute('aria-expanded', 'true');
|
|
267
322
|
(!handoff.hidden ? fields.email : terminal ? fresh : input).focus();
|
|
@@ -271,6 +326,7 @@ function createCocoa(options = {}) {
|
|
|
271
326
|
if (destroyed) return;
|
|
272
327
|
generation++;
|
|
273
328
|
sending = refreshing = false;
|
|
329
|
+
thinking.hidden = true;
|
|
274
330
|
for (const request of requests) request.abort();
|
|
275
331
|
clearTimeout(timer);
|
|
276
332
|
session = pending = incident = incidentPending = null;
|
|
@@ -293,12 +349,13 @@ function createCocoa(options = {}) {
|
|
|
293
349
|
}
|
|
294
350
|
const visibility = () => {
|
|
295
351
|
clearTimeout(timer);
|
|
296
|
-
if (!document.hidden) void refresh();
|
|
352
|
+
if (!document.hidden) { void refresh(); if (!greetingSeen) { clearTimeout(greetingTimer); greetingTimer=setTimeout(showGreeting,greetingDelay); } }
|
|
297
353
|
};
|
|
298
354
|
document.addEventListener('visibilitychange', visibility);
|
|
299
355
|
const widget = { open, close, reset, toggle: () => panel.hidden ? open() : close(), destroy() {
|
|
300
356
|
if (destroyed) return;
|
|
301
357
|
destroyed = true;
|
|
358
|
+
dismissGreeting(false);
|
|
302
359
|
generation++;
|
|
303
360
|
clearTimeout(timer);
|
|
304
361
|
for (const request of requests) request.abort();
|
|
@@ -307,6 +364,8 @@ function createCocoa(options = {}) {
|
|
|
307
364
|
instances.delete(key);
|
|
308
365
|
} };
|
|
309
366
|
launcher.onclick = widget.toggle;
|
|
367
|
+
greetingOpen.onclick = open;
|
|
368
|
+
greetingDismiss.onclick = () => { dismissGreeting(); launcher.focus(); };
|
|
310
369
|
panel.querySelector('header button').onclick = close;
|
|
311
370
|
fresh.onclick = reset;
|
|
312
371
|
root.addEventListener('keydown', (event) => {
|
|
@@ -317,6 +376,7 @@ function createCocoa(options = {}) {
|
|
|
317
376
|
const body = input.value.trim();
|
|
318
377
|
if (!body || sending || submittingIncident || terminal || paused || destroyed) return;
|
|
319
378
|
sending = true;
|
|
379
|
+
thinking.hidden = true;
|
|
320
380
|
controls();
|
|
321
381
|
clearTimeout(timer);
|
|
322
382
|
const current = generation;
|
|
@@ -361,6 +421,7 @@ function createCocoa(options = {}) {
|
|
|
361
421
|
if (!fields.summary.value) fields.summary.value = (last || 'Help with ' + (options.title || 'this application')).slice(0,200);
|
|
362
422
|
if (!fields.details.value) fields.details.value = last || '';
|
|
363
423
|
if (!fields.name.value) fields.name.value = name.value;
|
|
424
|
+
thinking.hidden = true;
|
|
364
425
|
handoff.hidden = false; composer.hidden = messages.hidden = true; controls(); fields.email.focus();
|
|
365
426
|
};
|
|
366
427
|
back.onclick = showChat;
|
|
@@ -399,6 +460,11 @@ function createCocoa(options = {}) {
|
|
|
399
460
|
}
|
|
400
461
|
}
|
|
401
462
|
};
|
|
463
|
+
if (!greetingSeen && !session && options.greetingMessage !== false) {
|
|
464
|
+
document.addEventListener('pointerdown', unlockAudio);
|
|
465
|
+
document.addEventListener('keydown', unlockAudio);
|
|
466
|
+
greetingTimer=setTimeout(showGreeting,greetingDelay);
|
|
467
|
+
}
|
|
402
468
|
instances.set(key, widget);
|
|
403
469
|
return widget;
|
|
404
470
|
}
|
|
@@ -413,6 +479,9 @@ if (script?.dataset.chatbot) {
|
|
|
413
479
|
launcherLabel: script.dataset.launcherLabel,
|
|
414
480
|
accentColor: script.dataset.accentColor,
|
|
415
481
|
nonce: script.nonce,
|
|
482
|
+
greetingMessage: script.dataset.greetingMessage === 'false' ? false : script.dataset.greetingMessage,
|
|
483
|
+
greetingDelay: script.dataset.greetingDelay === undefined ? undefined : Number(script.dataset.greetingDelay),
|
|
484
|
+
greetingSound: script.dataset.greetingSound !== 'false',
|
|
416
485
|
};
|
|
417
486
|
const mount = () => { window.Cocoa.widget = createCocoa(options); };
|
|
418
487
|
if (document.body) mount();
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,8 @@ function createCocoa(options = {}) {
|
|
|
21
21
|
if (!Number.isInteger(zIndex) || zIndex < 0 || zIndex > 2147483647) throw new Error('Invalid zIndex.');
|
|
22
22
|
const pollInterval = options.pollInterval ?? 2500;
|
|
23
23
|
if (!Number.isFinite(pollInterval) || pollInterval < 2500) throw new Error('pollInterval must be at least 2500ms.');
|
|
24
|
+
const greetingDelay = options.greetingDelay ?? 5000;
|
|
25
|
+
if (!Number.isFinite(greetingDelay) || greetingDelay < 0) throw new Error('greetingDelay must be a nonnegative number.');
|
|
24
26
|
const host = document.createElement('div');
|
|
25
27
|
host.dataset.cocoaWidget = '';
|
|
26
28
|
const root = host.attachShadow({ mode: 'open' });
|
|
@@ -31,6 +33,9 @@ function createCocoa(options = {}) {
|
|
|
31
33
|
*,*::before,*::after{box-sizing:border-box} [hidden]{display:none!important}
|
|
32
34
|
button,input,textarea{font:inherit}button{cursor:pointer;border:1px solid #647078;border-radius:12px;padding:10px 14px;background:#26343a;color:#fff}button:hover{filter:brightness(1.15)}button:disabled{opacity:.55;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #99dbc4;outline-offset:2px}
|
|
33
35
|
.launcher{display:block;margin-left:auto;width:72px;height:72px;padding:0;border:2px solid #e1f4fa;border-radius:50%;overflow:hidden;background:#92c5d5;box-shadow:0 6px 24px #0005}.launcher img{display:block;width:100%;height:100%;object-fit:cover;transform:scale(1.18);pointer-events:none}.launcher:hover{filter:brightness(1.06)}
|
|
36
|
+
.greeting{position:relative;margin:0 0 12px auto;display:flex;align-items:center;max-width:calc(100vw - 32px);width:250px;background:#fff5e9;color:#392516;border-radius:18px;box-shadow:0 6px 24px #0003;padding:6px}.greeting::after{content:'';position:absolute;right:28px;bottom:-8px;border:8px solid transparent;border-top-color:#fff5e9;border-bottom:0}.greeting button{color:inherit;background:transparent;border:0}.greeting .greeting-open{text-align:left;flex:1}.greeting .greeting-dismiss{padding:8px;min-width:40px;min-height:40px}
|
|
37
|
+
.thinking{display:flex;align-items:center;gap:8px;padding:8px 16px;color:#c8d4dd;font-size:12px}.dots{display:flex;gap:4px}.dots i{width:5px;height:5px;border-radius:50%;background:#92c5d5}
|
|
38
|
+
@media(prefers-reduced-motion:no-preference){.greeting{animation:appear .25s ease-out}.dots i{animation:think 1.2s ease-in-out infinite}.dots i:nth-child(2){animation-delay:.15s}.dots i:nth-child(3){animation-delay:.3s}@keyframes think{0%,60%,100%{opacity:.4;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}}
|
|
34
39
|
.panel{display:flex;flex-direction:column;width:370px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100dvh - 116px);background:#141c22;border:1px solid #44515b;border-radius:18px;box-shadow:0 12px 50px #0005;overflow:hidden;margin-bottom:12px}
|
|
35
40
|
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid #39454e}header strong{font-size:16px;overflow-wrap:anywhere}header small{display:block;color:#bac9d2}header button{min-width:44px;min-height:44px;font-size:22px;background:transparent}
|
|
36
41
|
.messages{flex:1;min-height:50px;overflow:auto;overscroll-behavior:contain;padding:16px;display:flex;flex-direction:column;gap:12px}.message{flex-shrink:0;align-self:flex-start;max-width:92%;background:#27333d;padding:10px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.customer{align-self:flex-end;background:${accent}}.message small{display:block;color:#fff;font-weight:600;font-size:11px;margin-bottom:4px}
|
|
@@ -82,6 +87,12 @@ function createCocoa(options = {}) {
|
|
|
82
87
|
handoff.append(handoffError, handoffActions);
|
|
83
88
|
const handoffResult = element('p', {class:'handoff-result',role:'status',tabindex:'-1',hidden:''});
|
|
84
89
|
panel.append(help, handoff, handoffResult);
|
|
90
|
+
const thinking = element('div', {class:'thinking',role:'status',hidden:''});
|
|
91
|
+
const dots = element('span', {class:'dots','aria-hidden':'true'});
|
|
92
|
+
dots.append(element('i'),element('i'),element('i'));
|
|
93
|
+
const thinkingLabel = element('span', {}, 'Cocoa is thinking…');
|
|
94
|
+
thinking.append(dots, thinkingLabel);
|
|
95
|
+
composer.before(thinking);
|
|
85
96
|
panel.querySelector('strong').textContent = options.title || 'Cocoa support';
|
|
86
97
|
const launcher = document.createElement('button');
|
|
87
98
|
launcher.type = 'button';
|
|
@@ -96,7 +107,11 @@ function createCocoa(options = {}) {
|
|
|
96
107
|
launcher.setAttribute('aria-expanded', 'false');
|
|
97
108
|
launcher.setAttribute('aria-controls', panel.id);
|
|
98
109
|
launcher.setAttribute('aria-haspopup', 'dialog');
|
|
99
|
-
|
|
110
|
+
const greeting = element('div', {class:'greeting',hidden:''});
|
|
111
|
+
const greetingOpen = element('button', {type:'button',class:'greeting-open'}, options.greetingMessage || 'Hi! Ask me anything.');
|
|
112
|
+
const greetingDismiss = element('button', {type:'button',class:'greeting-dismiss','aria-label':'Dismiss greeting'}, '×');
|
|
113
|
+
greeting.append(greetingOpen,greetingDismiss);
|
|
114
|
+
root.append(panel, greeting, launcher);
|
|
100
115
|
document.body.append(host);
|
|
101
116
|
const messages = panel.querySelector('.messages');
|
|
102
117
|
const status = panel.querySelector('.status');
|
|
@@ -109,6 +124,41 @@ function createCocoa(options = {}) {
|
|
|
109
124
|
let session = null, pending = null, timer, destroyed = false, sending = false, refreshing = false;
|
|
110
125
|
let generation = 0, terminal = false, paused = false, version = -1, failures = 0;
|
|
111
126
|
let incident = null, incidentPending = null, submittingIncident = false, expired = false;
|
|
127
|
+
let greetingTimer, audioContext, greetingSeen = false;
|
|
128
|
+
const greetingKey = key + ':greeted';
|
|
129
|
+
try { greetingSeen = sessionStorage.getItem(greetingKey) === '1'; } catch {}
|
|
130
|
+
const dismissGreeting = (remember = true) => {
|
|
131
|
+
clearTimeout(greetingTimer); greeting.hidden = true;
|
|
132
|
+
if (remember) { greetingSeen = true; try { sessionStorage.setItem(greetingKey,'1'); } catch {} }
|
|
133
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
134
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
135
|
+
if (audioContext) { void audioContext.close().catch(()=>{}); audioContext = null; }
|
|
136
|
+
};
|
|
137
|
+
function unlockAudio() {
|
|
138
|
+
if (destroyed || greetingSeen || options.greetingSound === false || audioContext) return;
|
|
139
|
+
try {
|
|
140
|
+
const Audio = window.AudioContext || window.webkitAudioContext;
|
|
141
|
+
if (Audio) { audioContext = new Audio(); void audioContext.resume().catch(()=>{}); }
|
|
142
|
+
} catch { /* Greeting still works when audio is unavailable. */ }
|
|
143
|
+
}
|
|
144
|
+
function showGreeting() {
|
|
145
|
+
if (destroyed || greetingSeen || !panel.hidden || session || options.greetingMessage === false || document.hidden) return;
|
|
146
|
+
greeting.hidden = false; greetingSeen = true;
|
|
147
|
+
try { sessionStorage.setItem(greetingKey,'1'); } catch {}
|
|
148
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
149
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
150
|
+
// Only play after a prior user gesture unlocked audio. Never queue a surprise sound.
|
|
151
|
+
if (audioContext?.state === 'running') {
|
|
152
|
+
try {
|
|
153
|
+
const tone = audioContext.createOscillator(), volume = audioContext.createGain();
|
|
154
|
+
const now = audioContext.currentTime;
|
|
155
|
+
tone.type = 'sine'; tone.frequency.setValueAtTime(660,now); tone.frequency.exponentialRampToValueAtTime(880,now+.1);
|
|
156
|
+
volume.gain.setValueAtTime(0,now); volume.gain.linearRampToValueAtTime(.045,now+.015); volume.gain.exponentialRampToValueAtTime(.001,now+.2);
|
|
157
|
+
tone.connect(volume); volume.connect(audioContext.destination); tone.start(now); tone.stop(now+.22);
|
|
158
|
+
tone.onended = () => { tone.disconnect(); volume.disconnect(); if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } };
|
|
159
|
+
} catch { if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } }
|
|
160
|
+
} else if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; }
|
|
161
|
+
}
|
|
112
162
|
const rendered = new Set();
|
|
113
163
|
const requests = new Set();
|
|
114
164
|
const persist = () => {
|
|
@@ -144,6 +194,7 @@ function createCocoa(options = {}) {
|
|
|
144
194
|
welcome();
|
|
145
195
|
const controls = () => {
|
|
146
196
|
const focusWasInComposer = root.activeElement === input || root.activeElement === send;
|
|
197
|
+
if (incident || !handoff.hidden || terminal || paused) thinking.hidden = true;
|
|
147
198
|
help.disabled = sending || submittingIncident || expired || paused;
|
|
148
199
|
help.hidden = !!incident || !handoff.hidden;
|
|
149
200
|
handoffResult.hidden = !incident;
|
|
@@ -160,6 +211,7 @@ function createCocoa(options = {}) {
|
|
|
160
211
|
};
|
|
161
212
|
controls();
|
|
162
213
|
const report = (error) => {
|
|
214
|
+
thinking.hidden = true;
|
|
163
215
|
status.classList.add('error');
|
|
164
216
|
status.textContent = error.message;
|
|
165
217
|
try { options.onError?.(error); } catch { /* Host callback must not break chat. */ }
|
|
@@ -228,6 +280,8 @@ function createCocoa(options = {}) {
|
|
|
228
280
|
status.textContent = incident ? 'Your incident is with the support team. Follow-up will be by email.' : terminal ? 'This conversation is closed.' : paused ? 'This chat is paused.' :
|
|
229
281
|
data.state === 'human' ? 'A person is handling your conversation.' :
|
|
230
282
|
data.agentOnline ? 'Connected to support.' : 'The agent is offline. Your messages are saved for the team.';
|
|
283
|
+
const last = data.messages.filter(m=>['customer','agent','human'].includes(m.actor)).at(-1);
|
|
284
|
+
thinking.hidden = !(last?.actor === 'customer' && data.agentOnline && data.state === 'agent' && !terminal && !paused && !incident && handoff.hidden);
|
|
231
285
|
controls();
|
|
232
286
|
}
|
|
233
287
|
function schedule() {
|
|
@@ -260,6 +314,7 @@ function createCocoa(options = {}) {
|
|
|
260
314
|
}
|
|
261
315
|
function open() {
|
|
262
316
|
if (destroyed) return;
|
|
317
|
+
dismissGreeting();
|
|
263
318
|
panel.hidden = false;
|
|
264
319
|
launcher.setAttribute('aria-expanded', 'true');
|
|
265
320
|
(!handoff.hidden ? fields.email : terminal ? fresh : input).focus();
|
|
@@ -269,6 +324,7 @@ function createCocoa(options = {}) {
|
|
|
269
324
|
if (destroyed) return;
|
|
270
325
|
generation++;
|
|
271
326
|
sending = refreshing = false;
|
|
327
|
+
thinking.hidden = true;
|
|
272
328
|
for (const request of requests) request.abort();
|
|
273
329
|
clearTimeout(timer);
|
|
274
330
|
session = pending = incident = incidentPending = null;
|
|
@@ -291,12 +347,13 @@ function createCocoa(options = {}) {
|
|
|
291
347
|
}
|
|
292
348
|
const visibility = () => {
|
|
293
349
|
clearTimeout(timer);
|
|
294
|
-
if (!document.hidden) void refresh();
|
|
350
|
+
if (!document.hidden) { void refresh(); if (!greetingSeen) { clearTimeout(greetingTimer); greetingTimer=setTimeout(showGreeting,greetingDelay); } }
|
|
295
351
|
};
|
|
296
352
|
document.addEventListener('visibilitychange', visibility);
|
|
297
353
|
const widget = { open, close, reset, toggle: () => panel.hidden ? open() : close(), destroy() {
|
|
298
354
|
if (destroyed) return;
|
|
299
355
|
destroyed = true;
|
|
356
|
+
dismissGreeting(false);
|
|
300
357
|
generation++;
|
|
301
358
|
clearTimeout(timer);
|
|
302
359
|
for (const request of requests) request.abort();
|
|
@@ -305,6 +362,8 @@ function createCocoa(options = {}) {
|
|
|
305
362
|
instances.delete(key);
|
|
306
363
|
} };
|
|
307
364
|
launcher.onclick = widget.toggle;
|
|
365
|
+
greetingOpen.onclick = open;
|
|
366
|
+
greetingDismiss.onclick = () => { dismissGreeting(); launcher.focus(); };
|
|
308
367
|
panel.querySelector('header button').onclick = close;
|
|
309
368
|
fresh.onclick = reset;
|
|
310
369
|
root.addEventListener('keydown', (event) => {
|
|
@@ -315,6 +374,7 @@ function createCocoa(options = {}) {
|
|
|
315
374
|
const body = input.value.trim();
|
|
316
375
|
if (!body || sending || submittingIncident || terminal || paused || destroyed) return;
|
|
317
376
|
sending = true;
|
|
377
|
+
thinking.hidden = true;
|
|
318
378
|
controls();
|
|
319
379
|
clearTimeout(timer);
|
|
320
380
|
const current = generation;
|
|
@@ -359,6 +419,7 @@ function createCocoa(options = {}) {
|
|
|
359
419
|
if (!fields.summary.value) fields.summary.value = (last || 'Help with ' + (options.title || 'this application')).slice(0,200);
|
|
360
420
|
if (!fields.details.value) fields.details.value = last || '';
|
|
361
421
|
if (!fields.name.value) fields.name.value = name.value;
|
|
422
|
+
thinking.hidden = true;
|
|
362
423
|
handoff.hidden = false; composer.hidden = messages.hidden = true; controls(); fields.email.focus();
|
|
363
424
|
};
|
|
364
425
|
back.onclick = showChat;
|
|
@@ -397,6 +458,11 @@ function createCocoa(options = {}) {
|
|
|
397
458
|
}
|
|
398
459
|
}
|
|
399
460
|
};
|
|
461
|
+
if (!greetingSeen && !session && options.greetingMessage !== false) {
|
|
462
|
+
document.addEventListener('pointerdown', unlockAudio);
|
|
463
|
+
document.addEventListener('keydown', unlockAudio);
|
|
464
|
+
greetingTimer=setTimeout(showGreeting,greetingDelay);
|
|
465
|
+
}
|
|
400
466
|
instances.set(key, widget);
|
|
401
467
|
return widget;
|
|
402
468
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -7,6 +7,12 @@ export interface CocoaOptions {
|
|
|
7
7
|
/** Accessible name and hover tooltip for the circular Cocoa portrait button. */
|
|
8
8
|
launcherLabel?: string;
|
|
9
9
|
welcomeMessage?: string;
|
|
10
|
+
/** Once-per-tab launcher greeting; false disables it. Default: Hi! Ask me anything. */
|
|
11
|
+
greetingMessage?: string | false;
|
|
12
|
+
/** Delay before greeting, in milliseconds. Defaults to 5000. */
|
|
13
|
+
greetingDelay?: number;
|
|
14
|
+
/** Soft chime if prior interaction permits audio. Defaults to true. */
|
|
15
|
+
greetingSound?: boolean;
|
|
10
16
|
/** Unverified display name, never an authentication credential. */
|
|
11
17
|
visitorLabel?: string;
|
|
12
18
|
/** Six-digit hex color, e.g. #385b50. */
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface CocoaOptions {
|
|
|
7
7
|
/** Accessible name and hover tooltip for the circular Cocoa portrait button. */
|
|
8
8
|
launcherLabel?: string;
|
|
9
9
|
welcomeMessage?: string;
|
|
10
|
+
/** Once-per-tab launcher greeting; false disables it. Default: Hi! Ask me anything. */
|
|
11
|
+
greetingMessage?: string | false;
|
|
12
|
+
/** Delay before greeting, in milliseconds. Defaults to 5000. */
|
|
13
|
+
greetingDelay?: number;
|
|
14
|
+
/** Soft chime if prior interaction permits audio. Defaults to true. */
|
|
15
|
+
greetingSound?: boolean;
|
|
10
16
|
/** Unverified display name, never an authentication credential. */
|
|
11
17
|
visitorLabel?: string;
|
|
12
18
|
/** Six-digit hex color, e.g. #385b50. */
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,8 @@ export function createCocoa(options = {}) {
|
|
|
21
21
|
if (!Number.isInteger(zIndex) || zIndex < 0 || zIndex > 2147483647) throw new Error('Invalid zIndex.');
|
|
22
22
|
const pollInterval = options.pollInterval ?? 2500;
|
|
23
23
|
if (!Number.isFinite(pollInterval) || pollInterval < 2500) throw new Error('pollInterval must be at least 2500ms.');
|
|
24
|
+
const greetingDelay = options.greetingDelay ?? 5000;
|
|
25
|
+
if (!Number.isFinite(greetingDelay) || greetingDelay < 0) throw new Error('greetingDelay must be a nonnegative number.');
|
|
24
26
|
const host = document.createElement('div');
|
|
25
27
|
host.dataset.cocoaWidget = '';
|
|
26
28
|
const root = host.attachShadow({ mode: 'open' });
|
|
@@ -31,6 +33,9 @@ export function createCocoa(options = {}) {
|
|
|
31
33
|
*,*::before,*::after{box-sizing:border-box} [hidden]{display:none!important}
|
|
32
34
|
button,input,textarea{font:inherit}button{cursor:pointer;border:1px solid #647078;border-radius:12px;padding:10px 14px;background:#26343a;color:#fff}button:hover{filter:brightness(1.15)}button:disabled{opacity:.55;cursor:default}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #99dbc4;outline-offset:2px}
|
|
33
35
|
.launcher{display:block;margin-left:auto;width:72px;height:72px;padding:0;border:2px solid #e1f4fa;border-radius:50%;overflow:hidden;background:#92c5d5;box-shadow:0 6px 24px #0005}.launcher img{display:block;width:100%;height:100%;object-fit:cover;transform:scale(1.18);pointer-events:none}.launcher:hover{filter:brightness(1.06)}
|
|
36
|
+
.greeting{position:relative;margin:0 0 12px auto;display:flex;align-items:center;max-width:calc(100vw - 32px);width:250px;background:#fff5e9;color:#392516;border-radius:18px;box-shadow:0 6px 24px #0003;padding:6px}.greeting::after{content:'';position:absolute;right:28px;bottom:-8px;border:8px solid transparent;border-top-color:#fff5e9;border-bottom:0}.greeting button{color:inherit;background:transparent;border:0}.greeting .greeting-open{text-align:left;flex:1}.greeting .greeting-dismiss{padding:8px;min-width:40px;min-height:40px}
|
|
37
|
+
.thinking{display:flex;align-items:center;gap:8px;padding:8px 16px;color:#c8d4dd;font-size:12px}.dots{display:flex;gap:4px}.dots i{width:5px;height:5px;border-radius:50%;background:#92c5d5}
|
|
38
|
+
@media(prefers-reduced-motion:no-preference){.greeting{animation:appear .25s ease-out}.dots i{animation:think 1.2s ease-in-out infinite}.dots i:nth-child(2){animation-delay:.15s}.dots i:nth-child(3){animation-delay:.3s}@keyframes think{0%,60%,100%{opacity:.4;transform:translateY(0)}30%{opacity:1;transform:translateY(-3px)}}}
|
|
34
39
|
.panel{display:flex;flex-direction:column;width:370px;max-width:calc(100vw - 32px);height:520px;max-height:calc(100dvh - 116px);background:#141c22;border:1px solid #44515b;border-radius:18px;box-shadow:0 12px 50px #0005;overflow:hidden;margin-bottom:12px}
|
|
35
40
|
header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid #39454e}header strong{font-size:16px;overflow-wrap:anywhere}header small{display:block;color:#bac9d2}header button{min-width:44px;min-height:44px;font-size:22px;background:transparent}
|
|
36
41
|
.messages{flex:1;min-height:50px;overflow:auto;overscroll-behavior:contain;padding:16px;display:flex;flex-direction:column;gap:12px}.message{flex-shrink:0;align-self:flex-start;max-width:92%;background:#27333d;padding:10px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.customer{align-self:flex-end;background:${accent}}.message small{display:block;color:#fff;font-weight:600;font-size:11px;margin-bottom:4px}
|
|
@@ -82,6 +87,12 @@ export function createCocoa(options = {}) {
|
|
|
82
87
|
handoff.append(handoffError, handoffActions);
|
|
83
88
|
const handoffResult = element('p', {class:'handoff-result',role:'status',tabindex:'-1',hidden:''});
|
|
84
89
|
panel.append(help, handoff, handoffResult);
|
|
90
|
+
const thinking = element('div', {class:'thinking',role:'status',hidden:''});
|
|
91
|
+
const dots = element('span', {class:'dots','aria-hidden':'true'});
|
|
92
|
+
dots.append(element('i'),element('i'),element('i'));
|
|
93
|
+
const thinkingLabel = element('span', {}, 'Cocoa is thinking…');
|
|
94
|
+
thinking.append(dots, thinkingLabel);
|
|
95
|
+
composer.before(thinking);
|
|
85
96
|
panel.querySelector('strong').textContent = options.title || 'Cocoa support';
|
|
86
97
|
const launcher = document.createElement('button');
|
|
87
98
|
launcher.type = 'button';
|
|
@@ -96,7 +107,11 @@ export function createCocoa(options = {}) {
|
|
|
96
107
|
launcher.setAttribute('aria-expanded', 'false');
|
|
97
108
|
launcher.setAttribute('aria-controls', panel.id);
|
|
98
109
|
launcher.setAttribute('aria-haspopup', 'dialog');
|
|
99
|
-
|
|
110
|
+
const greeting = element('div', {class:'greeting',hidden:''});
|
|
111
|
+
const greetingOpen = element('button', {type:'button',class:'greeting-open'}, options.greetingMessage || 'Hi! Ask me anything.');
|
|
112
|
+
const greetingDismiss = element('button', {type:'button',class:'greeting-dismiss','aria-label':'Dismiss greeting'}, '×');
|
|
113
|
+
greeting.append(greetingOpen,greetingDismiss);
|
|
114
|
+
root.append(panel, greeting, launcher);
|
|
100
115
|
document.body.append(host);
|
|
101
116
|
const messages = panel.querySelector('.messages');
|
|
102
117
|
const status = panel.querySelector('.status');
|
|
@@ -109,6 +124,41 @@ export function createCocoa(options = {}) {
|
|
|
109
124
|
let session = null, pending = null, timer, destroyed = false, sending = false, refreshing = false;
|
|
110
125
|
let generation = 0, terminal = false, paused = false, version = -1, failures = 0;
|
|
111
126
|
let incident = null, incidentPending = null, submittingIncident = false, expired = false;
|
|
127
|
+
let greetingTimer, audioContext, greetingSeen = false;
|
|
128
|
+
const greetingKey = key + ':greeted';
|
|
129
|
+
try { greetingSeen = sessionStorage.getItem(greetingKey) === '1'; } catch {}
|
|
130
|
+
const dismissGreeting = (remember = true) => {
|
|
131
|
+
clearTimeout(greetingTimer); greeting.hidden = true;
|
|
132
|
+
if (remember) { greetingSeen = true; try { sessionStorage.setItem(greetingKey,'1'); } catch {} }
|
|
133
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
134
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
135
|
+
if (audioContext) { void audioContext.close().catch(()=>{}); audioContext = null; }
|
|
136
|
+
};
|
|
137
|
+
function unlockAudio() {
|
|
138
|
+
if (destroyed || greetingSeen || options.greetingSound === false || audioContext) return;
|
|
139
|
+
try {
|
|
140
|
+
const Audio = window.AudioContext || window.webkitAudioContext;
|
|
141
|
+
if (Audio) { audioContext = new Audio(); void audioContext.resume().catch(()=>{}); }
|
|
142
|
+
} catch { /* Greeting still works when audio is unavailable. */ }
|
|
143
|
+
}
|
|
144
|
+
function showGreeting() {
|
|
145
|
+
if (destroyed || greetingSeen || !panel.hidden || session || options.greetingMessage === false || document.hidden) return;
|
|
146
|
+
greeting.hidden = false; greetingSeen = true;
|
|
147
|
+
try { sessionStorage.setItem(greetingKey,'1'); } catch {}
|
|
148
|
+
document.removeEventListener('pointerdown', unlockAudio);
|
|
149
|
+
document.removeEventListener('keydown', unlockAudio);
|
|
150
|
+
// Only play after a prior user gesture unlocked audio. Never queue a surprise sound.
|
|
151
|
+
if (audioContext?.state === 'running') {
|
|
152
|
+
try {
|
|
153
|
+
const tone = audioContext.createOscillator(), volume = audioContext.createGain();
|
|
154
|
+
const now = audioContext.currentTime;
|
|
155
|
+
tone.type = 'sine'; tone.frequency.setValueAtTime(660,now); tone.frequency.exponentialRampToValueAtTime(880,now+.1);
|
|
156
|
+
volume.gain.setValueAtTime(0,now); volume.gain.linearRampToValueAtTime(.045,now+.015); volume.gain.exponentialRampToValueAtTime(.001,now+.2);
|
|
157
|
+
tone.connect(volume); volume.connect(audioContext.destination); tone.start(now); tone.stop(now+.22);
|
|
158
|
+
tone.onended = () => { tone.disconnect(); volume.disconnect(); if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } };
|
|
159
|
+
} catch { if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; } }
|
|
160
|
+
} else if (audioContext) { void audioContext.close().catch(()=>{}); audioContext=null; }
|
|
161
|
+
}
|
|
112
162
|
const rendered = new Set();
|
|
113
163
|
const requests = new Set();
|
|
114
164
|
const persist = () => {
|
|
@@ -144,6 +194,7 @@ export function createCocoa(options = {}) {
|
|
|
144
194
|
welcome();
|
|
145
195
|
const controls = () => {
|
|
146
196
|
const focusWasInComposer = root.activeElement === input || root.activeElement === send;
|
|
197
|
+
if (incident || !handoff.hidden || terminal || paused) thinking.hidden = true;
|
|
147
198
|
help.disabled = sending || submittingIncident || expired || paused;
|
|
148
199
|
help.hidden = !!incident || !handoff.hidden;
|
|
149
200
|
handoffResult.hidden = !incident;
|
|
@@ -160,6 +211,7 @@ export function createCocoa(options = {}) {
|
|
|
160
211
|
};
|
|
161
212
|
controls();
|
|
162
213
|
const report = (error) => {
|
|
214
|
+
thinking.hidden = true;
|
|
163
215
|
status.classList.add('error');
|
|
164
216
|
status.textContent = error.message;
|
|
165
217
|
try { options.onError?.(error); } catch { /* Host callback must not break chat. */ }
|
|
@@ -228,6 +280,8 @@ export function createCocoa(options = {}) {
|
|
|
228
280
|
status.textContent = incident ? 'Your incident is with the support team. Follow-up will be by email.' : terminal ? 'This conversation is closed.' : paused ? 'This chat is paused.' :
|
|
229
281
|
data.state === 'human' ? 'A person is handling your conversation.' :
|
|
230
282
|
data.agentOnline ? 'Connected to support.' : 'The agent is offline. Your messages are saved for the team.';
|
|
283
|
+
const last = data.messages.filter(m=>['customer','agent','human'].includes(m.actor)).at(-1);
|
|
284
|
+
thinking.hidden = !(last?.actor === 'customer' && data.agentOnline && data.state === 'agent' && !terminal && !paused && !incident && handoff.hidden);
|
|
231
285
|
controls();
|
|
232
286
|
}
|
|
233
287
|
function schedule() {
|
|
@@ -260,6 +314,7 @@ export function createCocoa(options = {}) {
|
|
|
260
314
|
}
|
|
261
315
|
function open() {
|
|
262
316
|
if (destroyed) return;
|
|
317
|
+
dismissGreeting();
|
|
263
318
|
panel.hidden = false;
|
|
264
319
|
launcher.setAttribute('aria-expanded', 'true');
|
|
265
320
|
(!handoff.hidden ? fields.email : terminal ? fresh : input).focus();
|
|
@@ -269,6 +324,7 @@ export function createCocoa(options = {}) {
|
|
|
269
324
|
if (destroyed) return;
|
|
270
325
|
generation++;
|
|
271
326
|
sending = refreshing = false;
|
|
327
|
+
thinking.hidden = true;
|
|
272
328
|
for (const request of requests) request.abort();
|
|
273
329
|
clearTimeout(timer);
|
|
274
330
|
session = pending = incident = incidentPending = null;
|
|
@@ -291,12 +347,13 @@ export function createCocoa(options = {}) {
|
|
|
291
347
|
}
|
|
292
348
|
const visibility = () => {
|
|
293
349
|
clearTimeout(timer);
|
|
294
|
-
if (!document.hidden) void refresh();
|
|
350
|
+
if (!document.hidden) { void refresh(); if (!greetingSeen) { clearTimeout(greetingTimer); greetingTimer=setTimeout(showGreeting,greetingDelay); } }
|
|
295
351
|
};
|
|
296
352
|
document.addEventListener('visibilitychange', visibility);
|
|
297
353
|
const widget = { open, close, reset, toggle: () => panel.hidden ? open() : close(), destroy() {
|
|
298
354
|
if (destroyed) return;
|
|
299
355
|
destroyed = true;
|
|
356
|
+
dismissGreeting(false);
|
|
300
357
|
generation++;
|
|
301
358
|
clearTimeout(timer);
|
|
302
359
|
for (const request of requests) request.abort();
|
|
@@ -305,6 +362,8 @@ export function createCocoa(options = {}) {
|
|
|
305
362
|
instances.delete(key);
|
|
306
363
|
} };
|
|
307
364
|
launcher.onclick = widget.toggle;
|
|
365
|
+
greetingOpen.onclick = open;
|
|
366
|
+
greetingDismiss.onclick = () => { dismissGreeting(); launcher.focus(); };
|
|
308
367
|
panel.querySelector('header button').onclick = close;
|
|
309
368
|
fresh.onclick = reset;
|
|
310
369
|
root.addEventListener('keydown', (event) => {
|
|
@@ -315,6 +374,7 @@ export function createCocoa(options = {}) {
|
|
|
315
374
|
const body = input.value.trim();
|
|
316
375
|
if (!body || sending || submittingIncident || terminal || paused || destroyed) return;
|
|
317
376
|
sending = true;
|
|
377
|
+
thinking.hidden = true;
|
|
318
378
|
controls();
|
|
319
379
|
clearTimeout(timer);
|
|
320
380
|
const current = generation;
|
|
@@ -359,6 +419,7 @@ export function createCocoa(options = {}) {
|
|
|
359
419
|
if (!fields.summary.value) fields.summary.value = (last || 'Help with ' + (options.title || 'this application')).slice(0,200);
|
|
360
420
|
if (!fields.details.value) fields.details.value = last || '';
|
|
361
421
|
if (!fields.name.value) fields.name.value = name.value;
|
|
422
|
+
thinking.hidden = true;
|
|
362
423
|
handoff.hidden = false; composer.hidden = messages.hidden = true; controls(); fields.email.focus();
|
|
363
424
|
};
|
|
364
425
|
back.onclick = showChat;
|
|
@@ -397,6 +458,11 @@ export function createCocoa(options = {}) {
|
|
|
397
458
|
}
|
|
398
459
|
}
|
|
399
460
|
};
|
|
461
|
+
if (!greetingSeen && !session && options.greetingMessage !== false) {
|
|
462
|
+
document.addEventListener('pointerdown', unlockAudio);
|
|
463
|
+
document.addEventListener('keydown', unlockAudio);
|
|
464
|
+
greetingTimer=setTimeout(showGreeting,greetingDelay);
|
|
465
|
+
}
|
|
400
466
|
instances.set(key, widget);
|
|
401
467
|
return widget;
|
|
402
468
|
}
|