@eshal-bot/chat-widget 0.2.54 → 0.2.56

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.
@@ -88719,6 +88719,125 @@
88719
88719
  return null;
88720
88720
  }
88721
88721
 
88722
+ // ── Option lists (choice cards) ────────────────────────────────────────
88723
+ //
88724
+ // One card per option the customer can pick: flights from a search, time
88725
+ // slots, rooms, products. Aurelia demo call, dev, 13 Nov 2026: the agent said
88726
+ // "XA 1 at 7:45 a.m. for USD 588.50, or XA 3 at 2:30 p.m. for USD 585.50" and
88727
+ // the caller had to remember both. The card shows each option with a Select
88728
+ // button, and the pick goes into the conversation as the customer's own words
88729
+ // (`reply`), on chat and on a call alike.
88730
+ //
88731
+ // The spec, every field but `options[].title` optional:
88732
+ // {"type":"options","title":"Dubai to London, Fri 4 Dec","subtitle":"1 adult",
88733
+ // "actionLabel":"Select","moreLabel":"Show more","selectedLabel":"Selected",
88734
+ // "visible":3,
88735
+ // "options":[{"title":"XA 1","subtitle":"Aurelia Airways","start":"7:45 AM",
88736
+ // "end":"11:50 AM","duration":"7h 05m","tags":["Nonstop","Economy"],
88737
+ // "price":"USD 588.50","priceNote":"total","badge":"Earliest",
88738
+ // "image":"https://...","reply":"XA 1, please","value":"XA1"}]}
88739
+ //
88740
+ // Plain text only: every field is coerced to a capped string here and drawn
88741
+ // as a text node, and `image` still goes through each renderer's safeUrl and
88742
+ // image policy.
88743
+
88744
+ /** Component types drawn as an option list. `choices` and `option-list` are aliases. */
88745
+ const A2UI_OPTION_LIST_TYPES = Object.freeze(['options', 'option-list', 'choices']);
88746
+
88747
+ /** At most this many options are kept from one card. */
88748
+ const OPTION_LIST_MAX = 12;
88749
+ /** Options shown before "show more", unless the card asks for 1 to 4. */
88750
+ const OPTION_LIST_VISIBLE = 3;
88751
+ const OPTION_TEXT_MAX = 120;
88752
+ const OPTION_TAG_MAX = 6;
88753
+ function optionText(value, max) {
88754
+ if (value === null || value === undefined) return '';
88755
+ if (typeof value !== 'string' && typeof value !== 'number') return '';
88756
+ const s = String(value).replace(/\s+/g, ' ').trim();
88757
+ const cap = max || OPTION_TEXT_MAX;
88758
+ return s.length > cap ? s.slice(0, cap - 1).trim() + '…' : s;
88759
+ }
88760
+ function firstText(obj, keys, max) {
88761
+ for (let i = 0; i < keys.length; i += 1) {
88762
+ const t = optionText(obj[keys[i]], max);
88763
+ if (t) return t;
88764
+ }
88765
+ return '';
88766
+ }
88767
+
88768
+ /** Is `type` (any case, `_` or `-`) an option list? */
88769
+ function isOptionListType(type) {
88770
+ return A2UI_OPTION_LIST_TYPES.indexOf(normalizeA2uiType(type)) !== -1;
88771
+ }
88772
+
88773
+ /**
88774
+ * The option list a spec describes, with every field a capped plain string,
88775
+ * or null when it has no option with a title. `reply` is the message a pick
88776
+ * sends: the option's own `reply`, else its title.
88777
+ */
88778
+ function optionList(spec) {
88779
+ if (!spec || typeof spec !== 'object') return null;
88780
+ const raw = Array.isArray(spec.options) ? spec.options : Array.isArray(spec.items) ? spec.items : Array.isArray(spec.choices) ? spec.choices : [];
88781
+ const options = [];
88782
+ for (let i = 0; i < raw.length && options.length < OPTION_LIST_MAX; i += 1) {
88783
+ const o = raw[i];
88784
+ if (!o || typeof o !== 'object') continue;
88785
+ const title = firstText(o, ['title', 'name', 'label'], 80);
88786
+ if (!title) continue;
88787
+ const tagSource = Array.isArray(o.tags) ? o.tags : Array.isArray(o.features) ? o.features : [];
88788
+ const tags = [];
88789
+ for (let j = 0; j < tagSource.length && tags.length < OPTION_TAG_MAX; j += 1) {
88790
+ const t = optionText(tagSource[j], 40);
88791
+ if (t) tags.push(t);
88792
+ }
88793
+ options.push({
88794
+ title: title,
88795
+ subtitle: firstText(o, ['subtitle', 'description'], OPTION_TEXT_MAX),
88796
+ start: firstText(o, ['start', 'depart', 'from'], 40),
88797
+ end: firstText(o, ['end', 'arrive', 'to'], 40),
88798
+ duration: optionText(o.duration, 40),
88799
+ tags: tags,
88800
+ price: optionText(o.price, 40),
88801
+ priceNote: optionText(o.priceNote, 40),
88802
+ badge: optionText(o.badge, 30),
88803
+ image: typeof o.image === 'string' ? o.image : '',
88804
+ value: optionText(o.value, OPTION_TEXT_MAX),
88805
+ reply: firstText(o, ['reply'], 200) || title
88806
+ });
88807
+ }
88808
+ if (options.length === 0) return null;
88809
+ const asked = Number(spec.visible);
88810
+ const visible = Number.isFinite(asked) && asked >= 1 ? Math.min(4, Math.floor(asked)) : OPTION_LIST_VISIBLE;
88811
+ return {
88812
+ title: optionText(spec.title, 120),
88813
+ subtitle: optionText(spec.subtitle, 160),
88814
+ actionLabel: optionText(spec.actionLabel, 30) || 'Select',
88815
+ moreLabel: optionText(spec.moreLabel, 30) || 'Show more',
88816
+ lessLabel: optionText(spec.lessLabel, 30) || 'Show less',
88817
+ selectedLabel: optionText(spec.selectedLabel, 30) || 'Selected',
88818
+ visible: visible,
88819
+ options: options
88820
+ };
88821
+ }
88822
+
88823
+ /**
88824
+ * Which option a recorded answer picked: its index, or -1 when unanswered
88825
+ * or the customer moved on by typing something else. Matches the option's
88826
+ * reply, title or value, as a pick sends the reply.
88827
+ */
88828
+ function matchAnsweredOption(options, answer) {
88829
+ if (answer === undefined || answer === null) return -1;
88830
+ const a = normAnswer(answer);
88831
+ if (!a) return -1;
88832
+ const list = options || [];
88833
+ for (let i = 0; i < list.length; i += 1) {
88834
+ const o = list[i];
88835
+ if (!o) continue;
88836
+ if (normAnswer(o.reply) === a || normAnswer(o.title) === a || o.value && normAnswer(o.value) === a) return i;
88837
+ }
88838
+ return -1;
88839
+ }
88840
+
88722
88841
  /**
88723
88842
  * The same rule for images a card draws itself (A2UI card, list item,
88724
88843
  * avatar, gallery), which never pass through markdown. Returns `src`
@@ -89488,6 +89607,13 @@
89488
89607
  spec: spec,
89489
89608
  isDark: isDark
89490
89609
  });
89610
+ // Option list (choice cards): flights, time slots, products. A pick is the
89611
+ // customer's next turn, on chat and on a call.
89612
+ if (isOptionListType(t)) return /*#__PURE__*/jsxRuntimeExports.jsx(OptionList, {
89613
+ spec: spec,
89614
+ isDark: isDark,
89615
+ onButtonClick: onButtonClick
89616
+ });
89491
89617
 
89492
89618
  // Unrecognised type - render the data (if any) as a labeled list. Never
89493
89619
  // fall through to raw JSON; that's the bug we're fixing.
@@ -90524,6 +90650,7 @@
90524
90650
  // versions but hand-rolled here (no Tailwind runtime in the widget bundle).
90525
90651
 
90526
90652
  const BRAND_BLUE = '#2563eb';
90653
+ const BRAND_BLUE_HOVER = '#1d4ed8';
90527
90654
  function tagChipStyle(isDark) {
90528
90655
  return {
90529
90656
  display: 'inline-flex',
@@ -91175,6 +91302,298 @@
91175
91302
  });
91176
91303
  }
91177
91304
 
91305
+ // ── Option list (choice cards) ──
91306
+ //
91307
+ // One card per option the customer can pick, from a tool's results: flights
91308
+ // (number, depart and arrive, duration, stops, cabin, total), time slots,
91309
+ // rooms, products. The spec is read by the shared chat core's optionList
91310
+ // (packages/shared/src/chatCore/a2ui.js), which caps every field to plain
91311
+ // text. Select sends the option's `reply` ("XA 1, please") as the customer's
91312
+ // own message, so the bubble reads as words, not an id (EV-4471). The first
91313
+ // `visible` options show, the rest behind "show more". Once picked, or once
91314
+ // the conversation history records the answer, the card locks.
91315
+ //
91316
+ // RTL: no physical left/right. The rows are flex rows that follow the
91317
+ // bubble's direction, and times, prices and flight numbers sit in <bdi> so an
91318
+ // Arabic reply keeps "7:45 AM" and "USD 588.50" whole.
91319
+
91320
+ function OptionList({
91321
+ spec,
91322
+ isDark,
91323
+ onButtonClick
91324
+ }) {
91325
+ const list = optionList(spec);
91326
+ const consumedBy = React.useContext(A2uiAnsweredContext);
91327
+ const [localChosen, setLocalChosen] = React.useState(-1);
91328
+ const [expanded, setExpanded] = React.useState(false);
91329
+ const sentRef = React.useRef(false);
91330
+ if (!list) return null;
91331
+ const answered = matchAnsweredOption(list.options, consumedBy);
91332
+ const chosen = localChosen !== -1 ? localChosen : answered;
91333
+ const locked = chosen !== -1 || consumedBy !== undefined;
91334
+ const hidden = list.options.length - list.visible;
91335
+ // the picked option stays in view even when it sat behind "show more"
91336
+ const showAll = expanded || chosen >= list.visible;
91337
+ const shown = showAll ? list.options : list.options.slice(0, list.visible);
91338
+ const subColor = isDark ? '#9ca3af' : '#6b7280';
91339
+ const border = isDark ? '#374151' : '#e5e7eb';
91340
+ const choose = index => {
91341
+ if (!onButtonClick || sentRef.current || locked) return;
91342
+ sentRef.current = true;
91343
+ setLocalChosen(index);
91344
+ const o = list.options[index];
91345
+ onButtonClick(o.reply, o.title);
91346
+ };
91347
+ return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91348
+ style: {
91349
+ margin: '8px 0',
91350
+ minWidth: 0
91351
+ },
91352
+ "data-a2ui": "options",
91353
+ children: [(list.title || list.subtitle) && /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91354
+ style: {
91355
+ marginBottom: 8
91356
+ },
91357
+ children: [list.title && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91358
+ style: {
91359
+ fontSize: 13,
91360
+ fontWeight: 700,
91361
+ overflowWrap: 'anywhere'
91362
+ },
91363
+ children: list.title
91364
+ }), list.subtitle && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91365
+ style: {
91366
+ fontSize: 11,
91367
+ color: subColor
91368
+ },
91369
+ children: list.subtitle
91370
+ })]
91371
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91372
+ role: "list",
91373
+ style: {
91374
+ display: 'flex',
91375
+ flexDirection: 'column',
91376
+ gap: 8
91377
+ },
91378
+ children: shown.map((o, i) => {
91379
+ const picked = chosen === i;
91380
+ const imageUrl = cardImageSrc(o.image);
91381
+ // on the picked card's tint the chips keep a solid fill, or they melt into it
91382
+ const chip = picked ? {
91383
+ ...tagChipStyle(isDark),
91384
+ background: isDark ? '#1f2937' : '#ffffff'
91385
+ } : tagChipStyle(isDark);
91386
+ return /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91387
+ role: "listitem",
91388
+ "aria-label": o.title,
91389
+ "data-option-index": i,
91390
+ style: {
91391
+ border: '1px solid ' + (picked ? BRAND_BLUE : border),
91392
+ background: picked ? isDark ? 'rgba(37,99,235,0.12)' : '#eff6ff' : isDark ? '#1f2937' : '#ffffff',
91393
+ borderRadius: 8,
91394
+ padding: 10,
91395
+ display: 'flex',
91396
+ gap: 10,
91397
+ alignItems: 'flex-start',
91398
+ opacity: locked && !picked ? 0.6 : 1,
91399
+ minWidth: 0
91400
+ },
91401
+ children: [imageUrl && /*#__PURE__*/jsxRuntimeExports.jsx("img", {
91402
+ src: imageUrl,
91403
+ alt: o.title,
91404
+ referrerPolicy: "no-referrer",
91405
+ onError: e => {
91406
+ e.currentTarget.style.display = 'none';
91407
+ },
91408
+ style: {
91409
+ width: 56,
91410
+ height: 56,
91411
+ borderRadius: 6,
91412
+ objectFit: 'cover',
91413
+ flexShrink: 0,
91414
+ background: isDark ? '#111827' : '#f9fafb'
91415
+ }
91416
+ }), /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91417
+ style: {
91418
+ flex: 1,
91419
+ minWidth: 0,
91420
+ display: 'flex',
91421
+ flexDirection: 'column',
91422
+ gap: 6
91423
+ },
91424
+ children: [/*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91425
+ style: {
91426
+ display: 'flex',
91427
+ alignItems: 'flex-start',
91428
+ justifyContent: 'space-between',
91429
+ gap: 8
91430
+ },
91431
+ children: [/*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91432
+ style: {
91433
+ minWidth: 0
91434
+ },
91435
+ children: [/*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91436
+ style: {
91437
+ display: 'flex',
91438
+ alignItems: 'center',
91439
+ flexWrap: 'wrap',
91440
+ gap: 6
91441
+ },
91442
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("span", {
91443
+ style: {
91444
+ fontSize: 13,
91445
+ fontWeight: 700,
91446
+ overflowWrap: 'anywhere'
91447
+ },
91448
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91449
+ children: o.title
91450
+ })
91451
+ }), o.badge && /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91452
+ style: chip,
91453
+ children: o.badge
91454
+ })]
91455
+ }), o.subtitle && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91456
+ style: {
91457
+ fontSize: 11,
91458
+ color: subColor,
91459
+ marginTop: 2
91460
+ },
91461
+ children: o.subtitle
91462
+ })]
91463
+ }), o.price && /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91464
+ style: {
91465
+ textAlign: 'end',
91466
+ flexShrink: 0
91467
+ },
91468
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("div", {
91469
+ style: {
91470
+ fontSize: 14,
91471
+ fontWeight: 700,
91472
+ color: isDark ? '#93c5fd' : BRAND_BLUE_HOVER
91473
+ },
91474
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91475
+ children: o.price
91476
+ })
91477
+ }), o.priceNote && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91478
+ style: {
91479
+ fontSize: 10,
91480
+ color: subColor
91481
+ },
91482
+ children: o.priceNote
91483
+ })]
91484
+ })]
91485
+ }), (o.start || o.end) && /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91486
+ style: {
91487
+ display: 'flex',
91488
+ alignItems: 'center',
91489
+ gap: 8
91490
+ },
91491
+ children: [o.start && /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91492
+ style: {
91493
+ fontSize: 14,
91494
+ fontWeight: 600
91495
+ },
91496
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91497
+ children: o.start
91498
+ })
91499
+ }), o.start && o.end && /*#__PURE__*/jsxRuntimeExports.jsxs("span", {
91500
+ style: {
91501
+ flex: 1,
91502
+ minWidth: 24,
91503
+ display: 'flex',
91504
+ flexDirection: 'column',
91505
+ alignItems: 'center',
91506
+ gap: 2
91507
+ },
91508
+ children: [o.duration && /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91509
+ style: {
91510
+ fontSize: 10,
91511
+ color: subColor
91512
+ },
91513
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91514
+ children: o.duration
91515
+ })
91516
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91517
+ "aria-hidden": "true",
91518
+ style: {
91519
+ display: 'block',
91520
+ width: '100%',
91521
+ height: 1,
91522
+ background: isDark ? '#4b5563' : '#d1d5db'
91523
+ }
91524
+ })]
91525
+ }), o.end && /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91526
+ style: {
91527
+ fontSize: 14,
91528
+ fontWeight: 600
91529
+ },
91530
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91531
+ children: o.end
91532
+ })
91533
+ })]
91534
+ }), !(o.start && o.end) && o.duration && /*#__PURE__*/jsxRuntimeExports.jsx("div", {
91535
+ style: {
91536
+ fontSize: 11,
91537
+ color: subColor
91538
+ },
91539
+ children: /*#__PURE__*/jsxRuntimeExports.jsx("bdi", {
91540
+ children: o.duration
91541
+ })
91542
+ }), /*#__PURE__*/jsxRuntimeExports.jsxs("div", {
91543
+ style: {
91544
+ display: 'flex',
91545
+ alignItems: 'center',
91546
+ justifyContent: 'space-between',
91547
+ flexWrap: 'wrap',
91548
+ gap: 6
91549
+ },
91550
+ children: [/*#__PURE__*/jsxRuntimeExports.jsx("div", {
91551
+ style: {
91552
+ display: 'flex',
91553
+ flexWrap: 'wrap',
91554
+ gap: 4,
91555
+ minWidth: 0
91556
+ },
91557
+ children: o.tags.map((t, ti) => /*#__PURE__*/jsxRuntimeExports.jsx("span", {
91558
+ style: chip,
91559
+ children: t
91560
+ }, ti))
91561
+ }), /*#__PURE__*/jsxRuntimeExports.jsx("button", {
91562
+ type: "button",
91563
+ onClick: onButtonClick && !locked ? () => choose(i) : undefined,
91564
+ disabled: !onButtonClick || locked,
91565
+ "aria-label": picked ? `${list.selectedLabel}: ${o.title}` : `${list.actionLabel}: ${o.title}`,
91566
+ "aria-pressed": picked,
91567
+ style: {
91568
+ ...primaryButtonStyle(isDark, !onButtonClick || locked && !picked),
91569
+ marginInlineStart: 'auto',
91570
+ minWidth: 72
91571
+ },
91572
+ children: picked ? `${list.selectedLabel} ✓` : list.actionLabel
91573
+ })]
91574
+ })]
91575
+ })]
91576
+ }, i);
91577
+ })
91578
+ }), hidden > 0 && chosen < list.visible && /*#__PURE__*/jsxRuntimeExports.jsx("button", {
91579
+ type: "button",
91580
+ onClick: () => setExpanded(v => !v),
91581
+ "aria-expanded": expanded,
91582
+ style: {
91583
+ marginTop: 6,
91584
+ padding: '4px 0',
91585
+ border: 'none',
91586
+ background: 'none',
91587
+ fontSize: 12,
91588
+ fontWeight: 600,
91589
+ color: isDark ? '#93c5fd' : BRAND_BLUE,
91590
+ cursor: 'pointer'
91591
+ },
91592
+ children: expanded ? list.lessLabel : `${list.moreLabel} (${hidden})`
91593
+ })]
91594
+ });
91595
+ }
91596
+
91178
91597
  // ── 6. Gallery (horizontal image strip) ──
91179
91598
 
91180
91599
  function Gallery({