@hubtalk-ai/web-sdk 0.2.0 → 0.3.1

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.
@@ -0,0 +1,597 @@
1
+ /* VPT-857 §1.1: the shipped widget styles — a verbatim port of the example
2
+ widget of VPT-250 (example/widget.css) so the shipped component and the
3
+ hand-written /free page look the same (user decision 2026-10-13).
4
+
5
+ Token values are a copy of the editor palette (editor/src/styles.css,
6
+ ТЗ №113/№115: OKLCH, hue 286.98). Tokens live on the .ht-widget root and
7
+ never on :root, so loading this file never repaints the host page; every
8
+ class carries the ht- prefix for the same reason. Dark by default, light
9
+ under prefers-color-scheme: light — the editor's own behaviour. */
10
+ .ht-widget {
11
+ --bg: oklch(0.145 0.004 286.98);
12
+ --bg-panel: oklch(0.205 0.006 286.98);
13
+ --bg-elev: oklch(0.269 0.008 286.98);
14
+ --bg-input: oklch(0.145 0.004 286.98);
15
+ --border: oklch(1 0.004 286.98 / 0.1);
16
+ --border-strong: oklch(1 0.005 286.98 / 0.22);
17
+ --text: oklch(0.985 0 0);
18
+ --text-dim: oklch(0.82 0.008 286.98);
19
+ --accent: oklch(0.82 0.065 286.98);
20
+ --accent-soft: oklch(0.82 0.065 286.98 / 0.14);
21
+ --accent-contrast: oklch(0.21 0.01 286.98);
22
+ --ring: oklch(0.7 0.045 286.98);
23
+ --green: oklch(0.85 0.168 155);
24
+ --green-soft: oklch(0.85 0.168 155 / 0.13);
25
+ --amber: oklch(0.85 0.103 75);
26
+ --amber-soft: oklch(0.85 0.103 75 / 0.15);
27
+ --red: oklch(0.85 0.065 25);
28
+ --shadow-panel: oklch(0.1 0.005 286.98 / 0.62);
29
+ --radius: 14px;
30
+ --radius-sm: 10px;
31
+ --dur-1: 120ms;
32
+ --dur-2: 180ms;
33
+ --dur-3: 260ms;
34
+ --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
35
+ --font-sans: 'Geist Variable', 'Geist', -apple-system, 'Segoe UI', Roboto, sans-serif;
36
+ --mono: ui-monospace, 'SF Mono', Menlo, monospace;
37
+ color-scheme: dark;
38
+ }
39
+
40
+ @media (prefers-color-scheme: light) {
41
+ .ht-widget {
42
+ --bg: oklch(0.965 0.002 286.98);
43
+ --bg-panel: oklch(1 0 0);
44
+ --bg-elev: oklch(0.97 0.002 286.98);
45
+ --bg-input: oklch(1 0 0);
46
+ --border: oklch(0.92 0.003 286.98);
47
+ --border-strong: oklch(0.85 0.005 286.98);
48
+ --text: oklch(0.145 0.006 286.98);
49
+ --text-dim: oklch(0.5 0.012 286.98);
50
+ --accent: oklch(0.248 0.086 286.98);
51
+ --accent-soft: oklch(0.248 0.086 286.98 / 0.08);
52
+ --accent-contrast: oklch(0.985 0 0);
53
+ --ring: oklch(0.42 0.06 286.98);
54
+ --green: oklch(0.52 0.103 155);
55
+ --green-soft: oklch(0.52 0.103 155 / 0.1);
56
+ --amber: oklch(0.52 0.088 75);
57
+ --amber-soft: oklch(0.52 0.088 75 / 0.12);
58
+ --red: oklch(0.55 0.178 25);
59
+ --shadow-panel: oklch(0.25 0.01 286.98 / 0.22);
60
+ color-scheme: light;
61
+ }
62
+ }
63
+
64
+ /* --- the widget ---------------------------------------------------------- */
65
+
66
+ .ht-widget {
67
+ position: fixed;
68
+ right: 20px;
69
+ bottom: 20px;
70
+ z-index: 2147483000;
71
+ font-family: var(--font-sans);
72
+ font-size: 13px;
73
+ line-height: 1.5;
74
+ color: var(--text);
75
+ }
76
+ .ht-widget * {
77
+ box-sizing: border-box;
78
+ }
79
+ /* The UA rule for [hidden] is `display: none` at zero specificity, so any class
80
+ rule that sets display (.ht-panel, .ht-launcher) silently wins and the
81
+ collapsed state renders anyway. Restate it for the whole widget. */
82
+ .ht-widget [hidden] {
83
+ display: none !important;
84
+ }
85
+ .ht-widget :focus-visible {
86
+ outline: 2px solid var(--ring);
87
+ outline-offset: 2px;
88
+ }
89
+
90
+ /* launcher: the collapsed state */
91
+ .ht-widget .ht-launcher {
92
+ position: relative;
93
+ display: grid;
94
+ place-items: center;
95
+ width: 56px;
96
+ height: 56px;
97
+ margin-left: auto;
98
+ border: 1px solid var(--accent);
99
+ border-radius: 999px;
100
+ background: var(--accent);
101
+ color: var(--accent-contrast);
102
+ cursor: pointer;
103
+ box-shadow: 0 6px 20px var(--accent-soft);
104
+ transition:
105
+ filter var(--dur-1) var(--ease-out),
106
+ transform var(--dur-1) var(--ease-out);
107
+ }
108
+ .ht-widget .ht-launcher:hover {
109
+ filter: brightness(1.08);
110
+ }
111
+ .ht-widget .ht-launcher:active {
112
+ transform: scale(0.97);
113
+ }
114
+ /* an active call must stay visible while the panel is collapsed (§4.3) */
115
+ .ht-widget .ht-live-dot {
116
+ position: absolute;
117
+ top: 4px;
118
+ right: 4px;
119
+ width: 10px;
120
+ height: 10px;
121
+ border-radius: 999px;
122
+ background: var(--green);
123
+ box-shadow: 0 0 0 3px var(--green-soft);
124
+ }
125
+
126
+ /* panel: the expanded state */
127
+ .ht-widget .ht-panel {
128
+ display: flex;
129
+ flex-direction: column;
130
+ width: 360px;
131
+ /* taller than the log alone would need: the panel also holds the call
132
+ details form, and a squeezed log reads as broken */
133
+ height: min(600px, 100dvh - 32px);
134
+ background: var(--bg-panel);
135
+ border: 1px solid var(--border);
136
+ border-radius: var(--radius);
137
+ box-shadow: 0 12px 40px var(--shadow-panel);
138
+ overflow: hidden;
139
+ animation: ht-rise var(--dur-3) var(--ease-out);
140
+ }
141
+ @keyframes ht-rise {
142
+ from {
143
+ opacity: 0;
144
+ transform: translateY(8px);
145
+ }
146
+ }
147
+ .ht-widget .ht-head {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ padding: 10px 12px;
152
+ border-bottom: 1px solid var(--border);
153
+ }
154
+ .ht-widget .ht-title {
155
+ font-weight: 600;
156
+ margin-right: auto;
157
+ }
158
+
159
+ /* agent state pill — the editor's .voice-state */
160
+ .ht-widget .ht-pill {
161
+ font-size: 12px;
162
+ color: var(--text-dim);
163
+ padding: 2px 10px;
164
+ border: 1px solid var(--border);
165
+ border-radius: 999px;
166
+ white-space: nowrap;
167
+ }
168
+ .ht-widget .ht-pill.listening {
169
+ color: var(--green);
170
+ border-color: var(--green);
171
+ }
172
+ .ht-widget .ht-pill.speaking {
173
+ color: var(--accent);
174
+ border-color: var(--accent);
175
+ }
176
+ .ht-widget .ht-pill.thinking {
177
+ color: var(--text);
178
+ }
179
+ .ht-widget .ht-timer {
180
+ font-family: var(--mono);
181
+ font-size: 12px;
182
+ font-variant-numeric: tabular-nums;
183
+ color: var(--text-dim);
184
+ }
185
+
186
+ /* icon buttons — the base button of the editor */
187
+ .ht-widget .ht-icon {
188
+ display: grid;
189
+ place-items: center;
190
+ width: 26px;
191
+ height: 26px;
192
+ padding: 0;
193
+ font: inherit;
194
+ color: inherit;
195
+ background: var(--bg-elev);
196
+ border: 1px solid var(--border);
197
+ border-radius: var(--radius-sm);
198
+ cursor: pointer;
199
+ transition:
200
+ border-color var(--dur-1) var(--ease-out),
201
+ transform var(--dur-1) var(--ease-out);
202
+ }
203
+ .ht-widget .ht-icon:hover {
204
+ border-color: var(--accent);
205
+ }
206
+ .ht-widget .ht-icon:active:not(:disabled) {
207
+ transform: scale(0.97);
208
+ }
209
+ .ht-widget .ht-icon:disabled {
210
+ opacity: 0.45;
211
+ cursor: default;
212
+ }
213
+
214
+ /* transcript — the editor's .chat with .msg-agent / .msg-user / .msg-system */
215
+ .ht-widget .ht-log {
216
+ flex: 1;
217
+ min-height: 96px;
218
+ overflow-y: auto;
219
+ padding: 10px 14px;
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 6px;
223
+ }
224
+ .ht-widget .ht-msg {
225
+ max-width: 85%;
226
+ padding: 7px 11px;
227
+ border-radius: var(--radius);
228
+ white-space: pre-wrap;
229
+ word-break: break-word;
230
+ }
231
+ .ht-widget .ht-msg.agent {
232
+ background: var(--bg-elev);
233
+ align-self: flex-start;
234
+ }
235
+ .ht-widget .ht-msg.user {
236
+ background: var(--accent-soft);
237
+ align-self: flex-end;
238
+ }
239
+ .ht-widget .ht-msg.interim {
240
+ opacity: 0.6;
241
+ font-style: italic;
242
+ }
243
+ .ht-widget .ht-msg.system {
244
+ max-width: 100%;
245
+ align-self: center;
246
+ background: none;
247
+ color: var(--text-dim);
248
+ font-size: 12px;
249
+ text-align: center;
250
+ }
251
+ .ht-widget .ht-empty {
252
+ color: var(--text-dim);
253
+ font-size: 12px;
254
+ margin: auto;
255
+ }
256
+
257
+ /* raw SDK state: the page is also a debugging tool for the integration */
258
+ .ht-widget .ht-debug {
259
+ font-family: var(--mono);
260
+ font-size: 11.5px;
261
+ color: var(--text-dim);
262
+ padding: 6px 14px;
263
+ border-top: 1px solid var(--border);
264
+ white-space: pre-wrap;
265
+ max-height: 5.5em;
266
+ overflow-y: auto;
267
+ }
268
+
269
+ /* composer — the editor's .chat-input-row on a .test-input strip */
270
+ .ht-widget .ht-composer {
271
+ display: flex;
272
+ gap: 6px;
273
+ align-items: center;
274
+ padding: 8px 10px;
275
+ border-top: 1px solid var(--border);
276
+ }
277
+ .ht-widget .ht-composer input {
278
+ flex: 1;
279
+ min-width: 0;
280
+ font: inherit;
281
+ color: var(--text);
282
+ background: var(--bg-input);
283
+ border: 1px solid var(--border);
284
+ border-radius: var(--radius-sm);
285
+ padding: 6px 10px;
286
+ transition: border-color var(--dur-1) var(--ease-out);
287
+ }
288
+ .ht-widget .ht-composer input:focus {
289
+ border-color: var(--accent);
290
+ outline: none;
291
+ }
292
+ .ht-widget .ht-composer input:disabled {
293
+ opacity: 0.45;
294
+ }
295
+
296
+ /* call controls — the editor's .run-btn and .run-btn.danger */
297
+ .ht-widget .ht-actions {
298
+ display: flex;
299
+ gap: 8px;
300
+ padding: 0 10px 12px;
301
+ }
302
+ .ht-widget .ht-btn {
303
+ flex: 1;
304
+ font: inherit;
305
+ font-weight: 600;
306
+ white-space: nowrap;
307
+ color: var(--accent-contrast);
308
+ background: var(--accent);
309
+ border: 1px solid var(--accent);
310
+ border-radius: 999px;
311
+ padding: 7px 14px;
312
+ cursor: pointer;
313
+ box-shadow: 0 1px 8px var(--accent-soft);
314
+ transition:
315
+ filter var(--dur-1) var(--ease-out),
316
+ box-shadow var(--dur-1) var(--ease-out);
317
+ }
318
+ .ht-widget .ht-btn:hover:not(:disabled) {
319
+ filter: brightness(1.08);
320
+ }
321
+ .ht-widget .ht-btn:disabled {
322
+ opacity: 0.45;
323
+ cursor: default;
324
+ box-shadow: none;
325
+ }
326
+ .ht-widget .ht-btn-danger {
327
+ background: var(--red);
328
+ border-color: var(--red);
329
+ box-shadow: none;
330
+ }
331
+
332
+ @media (max-width: 480px) {
333
+ .ht-widget {
334
+ right: 8px;
335
+ left: 8px;
336
+ bottom: 8px;
337
+ }
338
+ .ht-widget .ht-panel {
339
+ width: 100%;
340
+ height: min(560px, 100dvh - 16px);
341
+ }
342
+ }
343
+
344
+ /* --- dynamic-variable form: shared by the widget and the call page ------- */
345
+
346
+ /* Native <details> instead of a scripted toggle — the browser already owns
347
+ the open state, the keyboard and the semantics. */
348
+ .ht-widget .ht-details {
349
+ border-bottom: 1px solid var(--border);
350
+ /* shrinkable, so however many fields are declared the panel still ends
351
+ where it should — the block scrolls instead of pushing the composer out.
352
+ During a call it is hidden altogether and the log takes the room back. */
353
+ flex: 0 1 auto;
354
+ max-height: 58%;
355
+ overflow-y: auto;
356
+ }
357
+ .ht-widget .ht-details > summary {
358
+ padding: 8px 14px;
359
+ font-size: 12px;
360
+ font-weight: 600;
361
+ letter-spacing: 0.02em;
362
+ color: var(--text-dim);
363
+ cursor: pointer;
364
+ list-style: none;
365
+ }
366
+ .ht-widget .ht-details > summary::marker,
367
+ .ht-widget .ht-details > summary::-webkit-details-marker {
368
+ display: none;
369
+ }
370
+ .ht-widget .ht-details > summary::before {
371
+ content: '▸';
372
+ display: inline-block;
373
+ width: 1em;
374
+ transition: transform var(--dur-1) var(--ease-out);
375
+ }
376
+ .ht-widget .ht-details[open] > summary::before {
377
+ transform: rotate(90deg);
378
+ }
379
+ .ht-widget .ht-details > summary:hover {
380
+ color: var(--text);
381
+ }
382
+ .ht-widget .ht-details .ht-fields {
383
+ padding: 0 14px 12px;
384
+ }
385
+ .ht-widget .ht-fields {
386
+ display: flex;
387
+ flex-wrap: wrap;
388
+ gap: 10px;
389
+ }
390
+ .ht-widget .ht-field {
391
+ display: flex;
392
+ flex: 1 1 100%;
393
+ flex-direction: column;
394
+ gap: 5px;
395
+ min-width: 0;
396
+ }
397
+ .ht-widget .ht-field.half {
398
+ flex-basis: calc(50% - 5px);
399
+ }
400
+ .ht-widget .ht-field-label {
401
+ font-size: 12px;
402
+ font-weight: 600;
403
+ color: var(--text-dim);
404
+ }
405
+ .ht-widget .ht-field input {
406
+ font: inherit;
407
+ width: 100%;
408
+ color: var(--text);
409
+ background: var(--bg-input);
410
+ border: 1px solid var(--border-strong);
411
+ border-radius: var(--radius-sm);
412
+ padding: 8px 11px;
413
+ transition: border-color var(--dur-1) var(--ease-out);
414
+ }
415
+ .ht-widget .ht-field input:hover:not(:read-only) {
416
+ border-color: var(--ring);
417
+ }
418
+ .ht-widget .ht-field input:focus {
419
+ border-color: var(--accent);
420
+ outline: none;
421
+ box-shadow: 0 0 0 3px var(--accent-soft);
422
+ }
423
+ .ht-widget .ht-field input:read-only {
424
+ color: var(--text-dim);
425
+ background: var(--bg-elev);
426
+ cursor: default;
427
+ }
428
+ /* free-form page: a variable is a key/value row the visitor types */
429
+ .ht-widget .ht-pair {
430
+ flex-direction: row;
431
+ align-items: center;
432
+ gap: 6px;
433
+ }
434
+ .ht-widget .ht-pair input {
435
+ flex: 1 1 0;
436
+ }
437
+ .ht-widget .ht-pair .ht-icon {
438
+ flex: none;
439
+ }
440
+ .ht-widget .ht-details .ht-add-var {
441
+ margin: 0 14px 12px;
442
+ }
443
+
444
+ .ht-widget .ht-field-note {
445
+ flex: 1 1 100%;
446
+ margin: 0;
447
+ font-size: 12px;
448
+ color: var(--text-dim);
449
+ }
450
+
451
+ /* the secondary action next to Call: chat-only, no microphone, no audio */
452
+ .ht-widget .ht-btn-ghost {
453
+ color: var(--text);
454
+ background: none;
455
+ border-color: var(--border-strong);
456
+ box-shadow: none;
457
+ }
458
+ .ht-widget .ht-btn-ghost:hover:not(:disabled) {
459
+ border-color: var(--accent);
460
+ filter: none;
461
+ }
462
+
463
+ /* --- avatar mode (VPT-857 §2) ------------------------------------------- */
464
+
465
+ /* the panel sits above the chat launcher, which stays fixed in the corner */
466
+ .ht-widget[data-mode='avatar'] .ht-panel {
467
+ position: absolute;
468
+ bottom: 72px;
469
+ right: 0;
470
+ height: min(540px, 100dvh - 270px);
471
+ }
472
+ .ht-widget .ht-enable-sound {
473
+ margin: 8px 10px 0;
474
+ }
475
+ .ht-widget .ht-avatar {
476
+ overflow: visible;
477
+ position: fixed;
478
+ /* --ht-avatar-size: the circle diameter, set by `avatar.size` or by the
479
+ host page on .ht-widget; phones scale it by 6/7 below */
480
+ width: var(--ht-avatar-size, 140px);
481
+ height: var(--ht-avatar-size, 140px);
482
+ padding: 0;
483
+ border-radius: 50%;
484
+ border: 3px solid var(--bg-panel);
485
+ outline: 2px solid var(--border-strong);
486
+ box-shadow: 0 12px 40px var(--shadow-panel);
487
+ touch-action: none;
488
+ user-select: none;
489
+ background: var(--bg-elev);
490
+ cursor: pointer;
491
+ z-index: 1;
492
+ }
493
+ .ht-widget .ht-avatar img,
494
+ .ht-widget .ht-avatar-video,
495
+ .ht-widget .ht-avatar-live,
496
+ .ht-widget .video-js,
497
+ .ht-widget .vjs-tech {
498
+ position: absolute;
499
+ inset: 0;
500
+ width: 100%;
501
+ height: 100%;
502
+ object-fit: cover;
503
+ border-radius: 50%;
504
+ overflow: hidden;
505
+ pointer-events: none;
506
+ }
507
+ .ht-widget .ht-avatar-video {
508
+ display: block;
509
+ }
510
+ .ht-widget .ht-avatar-live {
511
+ background: var(--bg-elev);
512
+ }
513
+ /* status under the circle and the AI label above it — the pill's look */
514
+ .ht-widget .ht-avatar-status,
515
+ .ht-widget .ht-avatar-ai {
516
+ position: absolute;
517
+ left: 50%;
518
+ transform: translateX(-50%);
519
+ white-space: nowrap;
520
+ font-size: 11px;
521
+ color: var(--text-dim);
522
+ padding: 2px 9px;
523
+ border: 1px solid var(--border);
524
+ border-radius: 999px;
525
+ background: var(--bg-panel);
526
+ box-shadow: 0 4px 14px var(--shadow-panel);
527
+ }
528
+ .ht-widget .ht-avatar-status {
529
+ top: calc(100% + 10px);
530
+ }
531
+ .ht-widget .ht-avatar-ai {
532
+ top: -10px;
533
+ font-size: 10px;
534
+ letter-spacing: 0.02em;
535
+ }
536
+ .ht-widget .ht-mic {
537
+ position: absolute;
538
+ bottom: 0;
539
+ right: 0;
540
+ display: grid;
541
+ place-items: center;
542
+ width: 32px;
543
+ height: 32px;
544
+ border-radius: 50%;
545
+ background: var(--accent);
546
+ color: var(--accent-contrast);
547
+ border: 2px solid var(--bg-panel);
548
+ }
549
+ .ht-widget .ht-mic svg {
550
+ width: 18px;
551
+ height: 18px;
552
+ }
553
+ .ht-widget .ht-mic-slash {
554
+ display: none;
555
+ }
556
+ .ht-widget .ht-muted {
557
+ background: var(--bg-elev);
558
+ color: var(--text-dim);
559
+ }
560
+ .ht-widget .ht-muted .ht-mic-slash {
561
+ display: block;
562
+ }
563
+ .ht-widget .ht-avatar[data-state='listening'] {
564
+ outline: 3px solid var(--green);
565
+ }
566
+ .ht-widget .ht-avatar[data-state='speaking'] {
567
+ outline: 3px solid var(--accent);
568
+ animation: ht-speaking 1.4s ease-in-out infinite;
569
+ }
570
+ .ht-widget .ht-avatar:focus-visible {
571
+ outline: 2px solid var(--ring);
572
+ outline-offset: 6px;
573
+ }
574
+ @keyframes ht-speaking {
575
+ 50% {
576
+ box-shadow: 0 0 0 9px var(--accent-soft);
577
+ }
578
+ }
579
+ @media (prefers-reduced-motion: reduce) {
580
+ .ht-widget .ht-avatar[data-state],
581
+ .ht-widget .ht-panel {
582
+ animation: none;
583
+ transition: none;
584
+ }
585
+ }
586
+ @media (max-width: 480px) {
587
+ .ht-widget .ht-avatar {
588
+ width: calc(var(--ht-avatar-size, 140px) * 6 / 7);
589
+ height: calc(var(--ht-avatar-size, 140px) * 6 / 7);
590
+ }
591
+ }
592
+ @media (max-height: 500px) and (min-width: 500px) {
593
+ .ht-widget[data-mode='avatar'] .ht-panel {
594
+ height: calc(100dvh - 104px);
595
+ width: min(360px, calc(100vw - 160px));
596
+ }
597
+ }
@@ -0,0 +1,40 @@
1
+ import type { StartCallParams } from './types';
2
+ /**
3
+ * VPT-857 D1: the clip FAILOVER of avatar mode — poster and silent loops the
4
+ * integrator hosts, shown whenever the live avatar video is absent.
5
+ */
6
+ export interface AvatarMedia {
7
+ posterUrl: string;
8
+ listeningVideoUrl: string;
9
+ speakingVideoUrl: string;
10
+ /**
11
+ * Diameter of the avatar circle in CSS pixels; default 140 (phones scale
12
+ * it down by the same ratio the default does). The microphone badge and
13
+ * the status pill keep their size.
14
+ */
15
+ size?: number;
16
+ }
17
+ /** The default circle diameter — DEFAULT_AVATAR_SIZE × 6/7 on phones. */
18
+ export declare const DEFAULT_AVATAR_SIZE = 140;
19
+ /**
20
+ * VPT-857 §1.1: what the widget tells the integrator's backend about the
21
+ * call it is about to start. `avatar: true` in avatar mode — the backend
22
+ * forwards it as `avatar: true` of `POST /v1/calls/web`; the live avatar then
23
+ * starts only if the agent's avatar setting is on too.
24
+ */
25
+ export interface CreateSessionContext {
26
+ avatar: boolean;
27
+ }
28
+ export interface WidgetOptions {
29
+ container: HTMLElement;
30
+ mode?: 'standard' | 'avatar';
31
+ /** Zero-argument callbacks keep working: the context is an extra argument. */
32
+ createSession: (context: CreateSessionContext) => Promise<StartCallParams>;
33
+ renderCallDetails?: (container: HTMLElement) => void;
34
+ avatar?: AvatarMedia;
35
+ }
36
+ export interface WidgetHandle {
37
+ destroy(): void;
38
+ }
39
+ /** Mount one isolated widget; credentials remain in the host's backend. */
40
+ export declare function mountWidget(options: WidgetOptions): WidgetHandle;