@sensiblestats/widget-react 0.6.1 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add to slip on betting insight cards.
8
+
9
+ The operator supplies an `onAddToSlip` callback; the widget hands it one selection
10
+ keyed by `matchOddsId` (the operator's own odds-row id) and maps the callback's
11
+ typed result to a localized (en/tr) toast. Two server fields drive it:
12
+ `odds.matchOddsId` and the per-operator gate `addToSlipEnabled` — the button
13
+ renders only when the gate is true, a handler is supplied, and the insight carries
14
+ a usable odds id. The widget never touches stake, balance, or bet placement.
15
+
16
+ - **widget-sdk:** add `SlipSelection`, `AddToSlipResult`, `AddToSlipRejectReason`,
17
+ and the `isAddToSlipResult` runtime guard; extend `BettingInsight` with
18
+ `addToSlipEnabled` and `odds.matchOddsId`.
19
+ - **widget-react:** add `AddToSlipButton` and `Toast`, the `slip.ts` result→toast
20
+ mapping (which honours `price_changed.accepted`), en/tr copy, `conversationId`
21
+ capture on `turn_complete`, and the `onAddToSlip` config entry. Malformed
22
+ results, throws, and a 10s timeout all fail closed to a retryable button.
23
+ - **widget-embed:** re-bundles widget-react, so it is republished. Add-to-slip is
24
+ not exposed to script-tag hosts, because a callback cannot be expressed in a
25
+ `data-*` attribute.
26
+
27
+ This shipped to master in `dd2da0e1` (#248) but never reached npm, because the
28
+ version was not bumped in the merged commit and the release workflow's
29
+ `--tolerate-republish` therefore skipped every package.
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies
34
+ - @sensiblestats/widget-sdk@0.6.0
35
+
3
36
  ## 0.6.1
4
37
 
5
38
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -36,8 +36,8 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/StatsWidget.tsx
39
- var import_react7 = require("react");
40
- var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
39
+ var import_react9 = require("react");
40
+ var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/i18n.ts
43
43
  var STRINGS = {
@@ -54,7 +54,16 @@ var STRINGS = {
54
54
  showLess: "Show less",
55
55
  showMore: (n) => `Show ${n} more`,
56
56
  oddsLabel: "Odds",
57
- liveLabel: "Live"
57
+ liveLabel: "Live",
58
+ addToSlip: "Add to slip",
59
+ inSlip: "In slip",
60
+ slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
61
+ slipDuplicate: "Already in your slip",
62
+ slipPriceChanged: (from, to) => `Added at ${to} (was ${from})`,
63
+ slipPriceNotAccepted: (from, to) => `Price moved to ${to} (was ${from}). Tap to add.`,
64
+ slipLoginRequired: "Sign in to add to your slip",
65
+ slipSuspended: "Market suspended",
66
+ slipFailed: "Couldn't add to slip. Try again."
58
67
  },
59
68
  tr: {
60
69
  launcherLabel: "SensibleStats ile sohbet et",
@@ -69,7 +78,16 @@ var STRINGS = {
69
78
  showLess: "Daha az g\xF6ster",
70
79
  showMore: (n) => `${n} tane daha g\xF6ster`,
71
80
  oddsLabel: "Oran",
72
- liveLabel: "Canl\u0131"
81
+ liveLabel: "Canl\u0131",
82
+ addToSlip: "Kupona ekle",
83
+ inSlip: "Kuponda",
84
+ slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
85
+ slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
86
+ slipPriceChanged: (from, to) => `${to} oran\u0131yla eklendi (\xF6nceki ${from})`,
87
+ slipPriceNotAccepted: (from, to) => `Oran ${to} oldu (\xF6nceki ${from}). Eklemek i\xE7in dokunun.`,
88
+ slipLoginRequired: "Kupona eklemek i\xE7in giri\u015F yap\u0131n",
89
+ slipSuspended: "Bu market ask\u0131ya al\u0131nd\u0131",
90
+ slipFailed: "Kupona eklenemedi. Tekrar deneyin."
73
91
  }
74
92
  };
75
93
  function resolveLocaleStrings(locale) {
@@ -101,7 +119,16 @@ function normalizeUi(cfg) {
101
119
  showLess: strings.showLess,
102
120
  showMore: strings.showMore,
103
121
  oddsLabel: strings.oddsLabel,
104
- liveLabel: strings.liveLabel
122
+ liveLabel: strings.liveLabel,
123
+ addToSlip: strings.addToSlip,
124
+ inSlip: strings.inSlip,
125
+ slipAdded: strings.slipAdded,
126
+ slipDuplicate: strings.slipDuplicate,
127
+ slipPriceChanged: strings.slipPriceChanged,
128
+ slipPriceNotAccepted: strings.slipPriceNotAccepted,
129
+ slipLoginRequired: strings.slipLoginRequired,
130
+ slipSuspended: strings.slipSuspended,
131
+ slipFailed: strings.slipFailed
105
132
  };
106
133
  }
107
134
  function toClientOptions(cfg) {
@@ -181,6 +208,8 @@ function toInsightVM(insight) {
181
208
  const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
182
209
  const value = insight.odds?.value;
183
210
  const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
211
+ const oddsValue = insight.odds?.value;
212
+ const matchOddsId = typeof insight.odds?.matchOddsId === "number" ? insight.odds.matchOddsId : void 0;
184
213
  const home = str(insight.homeTeamName);
185
214
  const away = str(insight.awayTeamName);
186
215
  const fixture = home && away ? `${home} vs ${away}` : void 0;
@@ -200,14 +229,31 @@ function toInsightVM(insight) {
200
229
  scopes,
201
230
  text: str(insight.text) ?? "",
202
231
  stats: readStatCells(insight.stats),
203
- disclaimer: str(insight.disclaimer)
232
+ disclaimer: str(insight.disclaimer),
233
+ matchId: typeof insight.matchId === "number" ? insight.matchId : 0,
234
+ matchOddsId,
235
+ addToSlipEnabled: insight.addToSlipEnabled === true,
236
+ oddsDecimal: typeof oddsValue === "number" && Number.isFinite(oddsValue) ? oddsValue : void 0,
237
+ quotedAtUtc: str(insight.odds?.quotedAtUtc)
238
+ };
239
+ }
240
+ function toSlipSelection(vm, conversationId) {
241
+ if (vm.matchOddsId === void 0 || vm.oddsDecimal === void 0) return null;
242
+ const selection = {
243
+ matchOddsId: vm.matchOddsId,
244
+ matchId: vm.matchId,
245
+ oddsDecimal: vm.oddsDecimal
204
246
  };
247
+ if (vm.quotedAtUtc !== void 0) selection.quotedAtUtc = vm.quotedAtUtc;
248
+ if (vm.id !== void 0) selection.insightId = vm.id;
249
+ if (conversationId !== void 0) selection.conversationId = conversationId;
250
+ return selection;
205
251
  }
206
252
 
207
253
  // src/reducer.ts
208
254
  var POPUP_MAX = 160;
209
255
  function initialState(starters) {
210
- return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null };
256
+ return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
211
257
  }
212
258
  function activeTurn(state) {
213
259
  const last = state.messages[state.messages.length - 1];
@@ -244,7 +290,15 @@ function applyEvent(state, event) {
244
290
  const active = activeTurn(state);
245
291
  const text = active?.text.trim() ?? "";
246
292
  const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
247
- return { ...state, isStreaming: false, status: null, actionsLoading: false, popupText, messages: mapActive(state, (t) => ({ ...t, done: true })) };
293
+ return {
294
+ ...state,
295
+ isStreaming: false,
296
+ status: null,
297
+ actionsLoading: false,
298
+ popupText,
299
+ conversationId: event.data.conversationId ?? state.conversationId,
300
+ messages: mapActive(state, (t) => ({ ...t, done: true }))
301
+ };
248
302
  }
249
303
  case "error": {
250
304
  const suppress = activeTurn(state)?.isGreeting === true;
@@ -387,7 +441,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
387
441
  }
388
442
 
389
443
  // src/components/ChatPanel.tsx
390
- var import_react6 = require("react");
444
+ var import_react8 = require("react");
391
445
 
392
446
  // src/components/ChatHeader.tsx
393
447
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -400,7 +454,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
400
454
  }
401
455
 
402
456
  // src/components/MessageList.tsx
403
- var import_react4 = require("react");
457
+ var import_react6 = require("react");
404
458
 
405
459
  // src/components/Markdown.tsx
406
460
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -454,94 +508,194 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
454
508
  }
455
509
 
456
510
  // src/components/BettingInsightList.tsx
511
+ var import_react5 = require("react");
512
+
513
+ // src/components/AddToSlipButton.tsx
514
+ var import_react4 = require("react");
515
+ var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
516
+
517
+ // src/slip.ts
518
+ var ADD_TO_SLIP_TIMEOUT_MS = 1e4;
519
+ function wasAdded(result) {
520
+ if (!result) return true;
521
+ if (result.status === "price_changed") return result.accepted;
522
+ return result.status === "added";
523
+ }
524
+ function isTerminal(result) {
525
+ if (!result) return true;
526
+ if (result.status === "price_changed") return result.accepted;
527
+ return result.status === "added" || result.status === "duplicate";
528
+ }
529
+ function resultToToast(result, shownOdds, ui) {
530
+ if (!result) return { kind: "success", text: ui.slipAdded(void 0) };
531
+ switch (result.status) {
532
+ case "added":
533
+ return { kind: "success", text: ui.slipAdded(result.slip?.selectionCount) };
534
+ case "duplicate":
535
+ return { kind: "success", text: ui.slipDuplicate };
536
+ case "price_changed":
537
+ return result.accepted ? { kind: "success", text: ui.slipPriceChanged(shownOdds, result.newOddsDecimal.toFixed(2)) } : { kind: "error", text: ui.slipPriceNotAccepted(shownOdds, result.newOddsDecimal.toFixed(2)) };
538
+ case "rejected":
539
+ if (result.reason === "login_required") return { kind: "error", text: ui.slipLoginRequired };
540
+ if (result.reason === "suspended") return { kind: "error", text: ui.slipSuspended };
541
+ return { kind: "error", text: result.message ?? ui.slipFailed };
542
+ }
543
+ }
544
+
545
+ // src/components/Toast.tsx
457
546
  var import_react3 = require("react");
458
547
  var import_jsx_runtime7 = require("react/jsx-runtime");
548
+ var DISMISS_MS = 4e3;
549
+ function Toast({ toast, onDismiss }) {
550
+ (0, import_react3.useEffect)(() => {
551
+ if (!toast) return;
552
+ const timer = setTimeout(onDismiss, DISMISS_MS);
553
+ return () => clearTimeout(timer);
554
+ }, [toast, onDismiss]);
555
+ if (!toast) return null;
556
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `ss-w-toast ss-w-toast-${toast.kind}`, role: "status", "aria-live": "polite", children: toast.text });
557
+ }
558
+
559
+ // src/components/AddToSlipButton.tsx
560
+ var import_jsx_runtime8 = require("react/jsx-runtime");
561
+ function withTimeout(value) {
562
+ return Promise.race([
563
+ value,
564
+ new Promise((_, reject) => setTimeout(() => reject(new Error("add-to-slip timed out")), ADD_TO_SLIP_TIMEOUT_MS))
565
+ ]);
566
+ }
567
+ function AddToSlipButton({ it, slip, ui }) {
568
+ const [pending, setPending] = (0, import_react4.useState)(false);
569
+ const [settled, setSettled] = (0, import_react4.useState)(false);
570
+ const [toast, setToast] = (0, import_react4.useState)(null);
571
+ const dismiss = (0, import_react4.useCallback)(() => setToast(null), []);
572
+ const handler = slip.onAddToSlip;
573
+ const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
574
+ if (!ui || !handler || !selection) return null;
575
+ const onClick = async () => {
576
+ setPending(true);
577
+ try {
578
+ const raw = await withTimeout(Promise.resolve(handler(selection)));
579
+ if (raw != null && !(0, import_widget_sdk3.isAddToSlipResult)(raw)) throw new Error("malformed add-to-slip result");
580
+ const result = raw ?? void 0;
581
+ setToast(resultToToast(result, it.odds ?? selection.oddsDecimal.toFixed(2), ui));
582
+ setSettled(isTerminal(result));
583
+ if (wasAdded(result) && slip.onMarketClicked) {
584
+ try {
585
+ slip.onMarketClicked(it.market);
586
+ } catch {
587
+ }
588
+ }
589
+ } catch {
590
+ setToast({ kind: "error", text: ui.slipFailed });
591
+ } finally {
592
+ setPending(false);
593
+ }
594
+ };
595
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-slip", children: [
596
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
597
+ "button",
598
+ {
599
+ type: "button",
600
+ className: "ss-w-slip-btn",
601
+ disabled: pending || settled,
602
+ onClick,
603
+ children: settled ? ui.inSlip : ui.addToSlip
604
+ }
605
+ ),
606
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Toast, { toast, onDismiss: dismiss })
607
+ ] });
608
+ }
609
+
610
+ // src/components/BettingInsightList.tsx
611
+ var import_jsx_runtime9 = require("react/jsx-runtime");
459
612
  var MAX_ROWS = 3;
460
613
  function StatRows({ stats, ui }) {
461
- const [expanded, setExpanded] = (0, import_react3.useState)(false);
614
+ const [expanded, setExpanded] = (0, import_react5.useState)(false);
462
615
  const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
463
616
  const overflow = stats.length - MAX_ROWS;
464
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-rows", children: [
465
- shown.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-row", children: [
466
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
467
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
468
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
617
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-rows", children: [
618
+ shown.map((s) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-row", children: [
619
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
621
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
469
622
  ] }, `${s.label}:${s.value}`)),
470
- overflow > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-bi-more", onClick: () => setExpanded((v) => !v), children: expanded ? ui?.showLess ?? "Show less" : (ui?.showMore ?? ((n) => `Show ${n} more`))(overflow) }) : null
623
+ overflow > 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-bi-more", onClick: () => setExpanded((v) => !v), children: expanded ? ui?.showLess ?? "Show less" : (ui?.showMore ?? ((n) => `Show ${n} more`))(overflow) }) : null
471
624
  ] });
472
625
  }
473
626
  function ScopedStats({ scopes, ui }) {
474
- const [active, setActive] = (0, import_react3.useState)(0);
627
+ const [active, setActive] = (0, import_react5.useState)(0);
475
628
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
476
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-scoped", children: [
477
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
478
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatRows, { stats: current.stats, ui }, current.key)
629
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-scoped", children: [
630
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
631
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatRows, { stats: current.stats, ui }, current.key)
479
632
  ] });
480
633
  }
481
- function InsightCard({ it, ui }) {
634
+ function InsightCard({ it, ui, slip }) {
482
635
  const live = it.isLive || it.phase === "live";
483
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
484
- live || it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-status", children: [
485
- live ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-live", children: [
486
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
636
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-binsight", children: [
637
+ live || it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-status", children: [
638
+ live ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-live", children: [
639
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
487
640
  ui?.liveLabel ?? "Live"
488
641
  ] }) : null,
489
- it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
490
- it.competition ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-comp", children: it.competition }) : null
642
+ it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
643
+ it.competition ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-comp", children: it.competition }) : null
491
644
  ] }) : null,
492
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
493
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
494
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
495
- it.selection ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
645
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-bi-head", children: [
646
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-mkt", children: [
647
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
648
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
496
649
  ] }),
497
- it.odds ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-odds", children: [
498
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
499
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
500
- it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
650
+ it.odds ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-bi-odds", children: [
651
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
652
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("b", { children: it.odds }),
653
+ it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
501
654
  ] }) : null
502
655
  ] }),
503
- it.text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
504
- it.scopes && it.scopes.length >= 2 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ScopedStats, { scopes: it.scopes, ui }) : it.stats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatRows, { stats: it.stats, ui }) : null,
505
- it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
656
+ it.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
657
+ it.scopes && it.scopes.length >= 2 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ScopedStats, { scopes: it.scopes, ui }) : it.stats.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(StatRows, { stats: it.stats, ui }) : null,
658
+ it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null,
659
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AddToSlipButton, { it, slip, ui })
506
660
  ] });
507
661
  }
508
- function BettingInsightList({ insights, ui }) {
662
+ function BettingInsightList({ insights, ui, slip }) {
509
663
  if (!insights || insights.length === 0) return null;
510
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InsightCard, { it, ui }, it.id)) });
664
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(InsightCard, { it, ui, slip }, it.id)) });
511
665
  }
512
666
 
513
667
  // src/components/ActionButtons.tsx
514
- var import_jsx_runtime8 = require("react/jsx-runtime");
668
+ var import_jsx_runtime10 = require("react/jsx-runtime");
515
669
  function ActionButtons({ actions, loading, disabled, onAct }) {
516
670
  if (actions.length === 0 && !loading) return null;
517
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-actions", children: [
518
- actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
519
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
520
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" }),
521
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" })
671
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-actions", children: [
672
+ actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
673
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
674
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-abtn-shimmer" }),
675
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-abtn-shimmer" })
522
676
  ] }) : null
523
677
  ] });
524
678
  }
525
679
 
526
680
  // src/components/ChatMessage.tsx
527
- var import_jsx_runtime9 = require("react/jsx-runtime");
528
- function ChatMessage({ message, disabled, onAct, ui }) {
681
+ var import_jsx_runtime11 = require("react/jsx-runtime");
682
+ function ChatMessage({ message, disabled, onAct, ui, slip }) {
529
683
  if (message.role === "user") {
530
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
684
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
531
685
  }
532
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
533
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
534
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
535
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights, ui }),
536
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
686
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
687
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text }) : null,
688
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
689
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
690
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
537
691
  ] });
538
692
  }
539
693
 
540
694
  // src/components/IntentChips.tsx
541
- var import_jsx_runtime10 = require("react/jsx-runtime");
695
+ var import_jsx_runtime12 = require("react/jsx-runtime");
542
696
  function IntentChips({ chips, onPick }) {
543
697
  if (chips.length === 0) return null;
544
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
698
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
545
699
  }
546
700
 
547
701
  // src/scroll.ts
@@ -551,17 +705,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
551
705
  }
552
706
 
553
707
  // src/components/MessageList.tsx
554
- var import_jsx_runtime11 = require("react/jsx-runtime");
555
- function MessageList({ controller, ui }) {
708
+ var import_jsx_runtime13 = require("react/jsx-runtime");
709
+ function MessageList({ controller, ui, slip }) {
556
710
  const { state, send } = controller;
557
- const listRef = (0, import_react4.useRef)(null);
558
- const endRef = (0, import_react4.useRef)(null);
559
- const pinnedRef = (0, import_react4.useRef)(true);
560
- (0, import_react4.useEffect)(() => {
711
+ const listRef = (0, import_react6.useRef)(null);
712
+ const endRef = (0, import_react6.useRef)(null);
713
+ const pinnedRef = (0, import_react6.useRef)(true);
714
+ (0, import_react6.useEffect)(() => {
561
715
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
562
716
  }, [state.messages, state.status]);
563
717
  const empty = state.messages.length === 0;
564
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
718
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
565
719
  "div",
566
720
  {
567
721
  className: "ss-w-list",
@@ -571,35 +725,36 @@ function MessageList({ controller, ui }) {
571
725
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
572
726
  },
573
727
  children: [
574
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
728
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
575
729
  ChatMessage,
576
730
  {
577
731
  message: m,
578
732
  disabled: state.isStreaming,
579
733
  onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
580
- ui
734
+ ui,
735
+ slip
581
736
  },
582
737
  m.id
583
738
  )),
584
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
739
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
585
740
  } }) : null,
586
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
587
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
588
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
589
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
741
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
742
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
743
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
744
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
590
745
  ] }) : null,
591
- empty ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
592
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: endRef })
746
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
747
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { ref: endRef })
593
748
  ]
594
749
  }
595
750
  );
596
751
  }
597
752
 
598
753
  // src/components/ChatInput.tsx
599
- var import_react5 = require("react");
600
- var import_jsx_runtime12 = require("react/jsx-runtime");
754
+ var import_react7 = require("react");
755
+ var import_jsx_runtime14 = require("react/jsx-runtime");
601
756
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
602
- const [value, setValue] = (0, import_react5.useState)("");
757
+ const [value, setValue] = (0, import_react7.useState)("");
603
758
  const submit = () => {
604
759
  const t = value.trim();
605
760
  if (!t) return;
@@ -612,18 +767,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
612
767
  submit();
613
768
  }
614
769
  };
615
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-input", children: [
616
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
617
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(SendIcon, {}) })
770
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-input", children: [
771
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
772
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SendIcon, {}) })
618
773
  ] });
619
774
  }
620
775
 
621
776
  // src/components/ChatPanel.tsx
622
- var import_jsx_runtime13 = require("react/jsx-runtime");
777
+ var import_jsx_runtime15 = require("react/jsx-runtime");
623
778
  var FULLSCREEN_QUERY = "(max-width: 639px)";
624
779
  function useIsModal() {
625
- const [modal, setModal] = (0, import_react6.useState)(false);
626
- (0, import_react6.useEffect)(() => {
780
+ const [modal, setModal] = (0, import_react8.useState)(false);
781
+ (0, import_react8.useEffect)(() => {
627
782
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
628
783
  const mql = window.matchMedia(FULLSCREEN_QUERY);
629
784
  const sync = () => setModal(mql.matches);
@@ -633,20 +788,20 @@ function useIsModal() {
633
788
  }, []);
634
789
  return modal;
635
790
  }
636
- function ChatPanel({ controller, ui }) {
791
+ function ChatPanel({ controller, ui, slip }) {
637
792
  const { state, close, stop, send } = controller;
638
793
  const isModal = useIsModal();
639
- (0, import_react6.useEffect)(() => {
794
+ (0, import_react8.useEffect)(() => {
640
795
  const onKey = (e) => {
641
796
  if (e.key === "Escape") close();
642
797
  };
643
798
  window.addEventListener("keydown", onKey);
644
799
  return () => window.removeEventListener("keydown", onKey);
645
800
  }, [close]);
646
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
647
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
648
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageList, { controller, ui }),
649
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
801
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
802
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
803
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MessageList, { controller, ui, slip }),
804
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
650
805
  ] });
651
806
  }
652
807
 
@@ -799,20 +954,28 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
799
954
  .ss-w-send { width: 36px; height: 36px; flex: none; border: none; border-radius: 50%; background: var(--ss-w-accent); color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 16px; }
800
955
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
801
956
 
957
+ /* add to slip */
958
+ .ss-w-slip { position: relative; display: flex; }
959
+ .ss-w-slip-btn { font-size: 11.5px; font-weight: 620; color: #fff; background: var(--ss-w-accent); border: 1px solid var(--ss-w-accent); border-radius: 14px; padding: 5px 12px; cursor: pointer; }
960
+ .ss-w-slip-btn:disabled { opacity: .6; cursor: default; background: var(--ss-w-bubble); color: var(--ss-w-faint); border-color: var(--ss-w-line); }
961
+ .ss-w-toast { position: absolute; top: calc(100% + 6px); left: 0; z-index: 1; max-width: 100%; font-size: 11px; font-weight: 560; border-radius: 10px; padding: 5px 10px; overflow-wrap: anywhere; box-shadow: 0 4px 14px rgba(0,0,0,.14); }
962
+ .ss-w-toast-success { color: var(--ss-w-accent); background: color-mix(in srgb, var(--ss-w-accent) 14%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-line)); }
963
+ .ss-w-toast-error { color: #c0392b; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); }
964
+
802
965
  @media (prefers-reduced-motion: reduce) {
803
966
  .ss-w-abtn-shimmer { animation: none; }
804
967
  }
805
968
  `;
806
969
 
807
970
  // src/StatsWidget.tsx
808
- var import_jsx_runtime14 = require("react/jsx-runtime");
971
+ var import_jsx_runtime16 = require("react/jsx-runtime");
809
972
  function buildClient(props) {
810
973
  try {
811
974
  if ("client" in props && props.client) {
812
975
  return { client: props.client, conversation: props.client.conversation() };
813
976
  }
814
977
  const cfg = props.config;
815
- const client = new import_widget_sdk3.StatsWidgetClient(toClientOptions(cfg));
978
+ const client = new import_widget_sdk4.StatsWidgetClient(toClientOptions(cfg));
816
979
  return { client, conversation: client.conversation(toUserContext(cfg)) };
817
980
  } catch (err) {
818
981
  console.error("[StatsWidget] failed to initialize:", err instanceof Error ? err.message : err);
@@ -830,18 +993,31 @@ function injectStyles(root) {
830
993
  function StatsWidget(props) {
831
994
  const injectedClient = "client" in props ? props.client : void 0;
832
995
  const cfg = props.config;
833
- const built = (0, import_react7.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
834
- const ui = (0, import_react7.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
835
- const rootRef = (0, import_react7.useRef)(null);
996
+ const built = (0, import_react9.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
997
+ const ui = (0, import_react9.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
998
+ const rootRef = (0, import_react9.useRef)(null);
836
999
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
837
- (0, import_react7.useEffect)(() => {
1000
+ const slip = (0, import_react9.useMemo)(
1001
+ () => ({
1002
+ onAddToSlip: props.config?.onAddToSlip,
1003
+ conversationId: controller.state.conversationId ?? void 0,
1004
+ onMarketClicked: (marketName) => {
1005
+ try {
1006
+ void built?.conversation.sendSignal({ signalType: "InsightMarketClicked", entityType: "Market", entityName: marketName }).catch(() => void 0);
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ }),
1011
+ [props.config?.onAddToSlip, controller.state.conversationId, built]
1012
+ );
1013
+ (0, import_react9.useEffect)(() => {
838
1014
  if (!built || !ui.injectStyles) return;
839
1015
  const node = rootRef.current?.getRootNode();
840
1016
  if (node) injectStyles(node);
841
1017
  }, [built, ui.injectStyles]);
842
1018
  if (!built) return null;
843
1019
  const attrs = themeAttrs(ui);
844
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
1020
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
845
1021
  ChatFab,
846
1022
  {
847
1023
  label: ui.launcherLabel,