@sensiblestats/widget-react 0.6.0 → 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/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];
@@ -224,9 +270,12 @@ function applyEvent(state, event) {
224
270
  return { ...state, status: event.data.message };
225
271
  case "answer":
226
272
  return { ...state, messages: mapActive(state, (t) => ({ ...t, text: event.data.text })) };
227
- case "entity_card":
273
+ case "entity_card": {
228
274
  if (event.data.card.isUsed === false) return state;
229
- return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, toCardVM(event.data.card)] })) };
275
+ const card = toCardVM(event.data.card);
276
+ if (card.kind === "generic") return state;
277
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, card] })) };
278
+ }
230
279
  case "action_button":
231
280
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
232
281
  case "betting_insight":
@@ -241,7 +290,15 @@ function applyEvent(state, event) {
241
290
  const active = activeTurn(state);
242
291
  const text = active?.text.trim() ?? "";
243
292
  const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
244
- 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
+ };
245
302
  }
246
303
  case "error": {
247
304
  const suppress = activeTurn(state)?.isGreeting === true;
@@ -384,7 +441,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
384
441
  }
385
442
 
386
443
  // src/components/ChatPanel.tsx
387
- var import_react6 = require("react");
444
+ var import_react8 = require("react");
388
445
 
389
446
  // src/components/ChatHeader.tsx
390
447
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -397,7 +454,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
397
454
  }
398
455
 
399
456
  // src/components/MessageList.tsx
400
- var import_react4 = require("react");
457
+ var import_react6 = require("react");
401
458
 
402
459
  // src/components/Markdown.tsx
403
460
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -451,94 +508,194 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
451
508
  }
452
509
 
453
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
454
546
  var import_react3 = require("react");
455
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");
456
612
  var MAX_ROWS = 3;
457
613
  function StatRows({ stats, ui }) {
458
- const [expanded, setExpanded] = (0, import_react3.useState)(false);
614
+ const [expanded, setExpanded] = (0, import_react5.useState)(false);
459
615
  const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
460
616
  const overflow = stats.length - MAX_ROWS;
461
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-rows", children: [
462
- shown.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-row", children: [
463
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
464
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
465
- /* @__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 })
466
622
  ] }, `${s.label}:${s.value}`)),
467
- 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
468
624
  ] });
469
625
  }
470
626
  function ScopedStats({ scopes, ui }) {
471
- const [active, setActive] = (0, import_react3.useState)(0);
627
+ const [active, setActive] = (0, import_react5.useState)(0);
472
628
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
473
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-scoped", children: [
474
- /* @__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)) }),
475
- /* @__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)
476
632
  ] });
477
633
  }
478
- function InsightCard({ it, ui }) {
634
+ function InsightCard({ it, ui, slip }) {
479
635
  const live = it.isLive || it.phase === "live";
480
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
481
- live || it.fixture || it.competition ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-status", children: [
482
- live ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-live", children: [
483
- /* @__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" }),
484
640
  ui?.liveLabel ?? "Live"
485
641
  ] }) : null,
486
- it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
487
- 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
488
644
  ] }) : null,
489
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
490
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
491
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
492
- 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
493
649
  ] }),
494
- it.odds ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-odds", children: [
495
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
496
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
497
- 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
498
654
  ] }) : null
499
655
  ] }),
500
- it.text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
501
- 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,
502
- 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 })
503
660
  ] });
504
661
  }
505
- function BettingInsightList({ insights, ui }) {
662
+ function BettingInsightList({ insights, ui, slip }) {
506
663
  if (!insights || insights.length === 0) return null;
507
- 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)) });
508
665
  }
509
666
 
510
667
  // src/components/ActionButtons.tsx
511
- var import_jsx_runtime8 = require("react/jsx-runtime");
668
+ var import_jsx_runtime10 = require("react/jsx-runtime");
512
669
  function ActionButtons({ actions, loading, disabled, onAct }) {
513
670
  if (actions.length === 0 && !loading) return null;
514
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-actions", children: [
515
- 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}`)),
516
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
517
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" }),
518
- /* @__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" })
519
676
  ] }) : null
520
677
  ] });
521
678
  }
522
679
 
523
680
  // src/components/ChatMessage.tsx
524
- var import_jsx_runtime9 = require("react/jsx-runtime");
525
- function ChatMessage({ message, disabled, onAct, ui }) {
681
+ var import_jsx_runtime11 = require("react/jsx-runtime");
682
+ function ChatMessage({ message, disabled, onAct, ui, slip }) {
526
683
  if (message.role === "user") {
527
- 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 }) });
528
685
  }
529
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
530
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
531
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
532
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights, ui }),
533
- /* @__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 })
534
691
  ] });
535
692
  }
536
693
 
537
694
  // src/components/IntentChips.tsx
538
- var import_jsx_runtime10 = require("react/jsx-runtime");
695
+ var import_jsx_runtime12 = require("react/jsx-runtime");
539
696
  function IntentChips({ chips, onPick }) {
540
697
  if (chips.length === 0) return null;
541
- 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}`)) });
542
699
  }
543
700
 
544
701
  // src/scroll.ts
@@ -548,17 +705,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
548
705
  }
549
706
 
550
707
  // src/components/MessageList.tsx
551
- var import_jsx_runtime11 = require("react/jsx-runtime");
552
- function MessageList({ controller, ui }) {
708
+ var import_jsx_runtime13 = require("react/jsx-runtime");
709
+ function MessageList({ controller, ui, slip }) {
553
710
  const { state, send } = controller;
554
- const listRef = (0, import_react4.useRef)(null);
555
- const endRef = (0, import_react4.useRef)(null);
556
- const pinnedRef = (0, import_react4.useRef)(true);
557
- (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)(() => {
558
715
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
559
716
  }, [state.messages, state.status]);
560
717
  const empty = state.messages.length === 0;
561
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
718
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
562
719
  "div",
563
720
  {
564
721
  className: "ss-w-list",
@@ -568,35 +725,36 @@ function MessageList({ controller, ui }) {
568
725
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
569
726
  },
570
727
  children: [
571
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
728
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
572
729
  ChatMessage,
573
730
  {
574
731
  message: m,
575
732
  disabled: state.isStreaming,
576
733
  onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
577
- ui
734
+ ui,
735
+ slip
578
736
  },
579
737
  m.id
580
738
  )),
581
- 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: () => {
582
740
  } }) : null,
583
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
584
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
585
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
586
- /* @__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 })
587
745
  ] }) : null,
588
- empty ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
589
- /* @__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 })
590
748
  ]
591
749
  }
592
750
  );
593
751
  }
594
752
 
595
753
  // src/components/ChatInput.tsx
596
- var import_react5 = require("react");
597
- var import_jsx_runtime12 = require("react/jsx-runtime");
754
+ var import_react7 = require("react");
755
+ var import_jsx_runtime14 = require("react/jsx-runtime");
598
756
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
599
- const [value, setValue] = (0, import_react5.useState)("");
757
+ const [value, setValue] = (0, import_react7.useState)("");
600
758
  const submit = () => {
601
759
  const t = value.trim();
602
760
  if (!t) return;
@@ -609,18 +767,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
609
767
  submit();
610
768
  }
611
769
  };
612
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-input", children: [
613
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
614
- 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, {}) })
615
773
  ] });
616
774
  }
617
775
 
618
776
  // src/components/ChatPanel.tsx
619
- var import_jsx_runtime13 = require("react/jsx-runtime");
777
+ var import_jsx_runtime15 = require("react/jsx-runtime");
620
778
  var FULLSCREEN_QUERY = "(max-width: 639px)";
621
779
  function useIsModal() {
622
- const [modal, setModal] = (0, import_react6.useState)(false);
623
- (0, import_react6.useEffect)(() => {
780
+ const [modal, setModal] = (0, import_react8.useState)(false);
781
+ (0, import_react8.useEffect)(() => {
624
782
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
625
783
  const mql = window.matchMedia(FULLSCREEN_QUERY);
626
784
  const sync = () => setModal(mql.matches);
@@ -630,20 +788,20 @@ function useIsModal() {
630
788
  }, []);
631
789
  return modal;
632
790
  }
633
- function ChatPanel({ controller, ui }) {
791
+ function ChatPanel({ controller, ui, slip }) {
634
792
  const { state, close, stop, send } = controller;
635
793
  const isModal = useIsModal();
636
- (0, import_react6.useEffect)(() => {
794
+ (0, import_react8.useEffect)(() => {
637
795
  const onKey = (e) => {
638
796
  if (e.key === "Escape") close();
639
797
  };
640
798
  window.addEventListener("keydown", onKey);
641
799
  return () => window.removeEventListener("keydown", onKey);
642
800
  }, [close]);
643
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
644
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
645
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageList, { controller, ui }),
646
- /* @__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 })
647
805
  ] });
648
806
  }
649
807
 
@@ -796,20 +954,28 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
796
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; }
797
955
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
798
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
+
799
965
  @media (prefers-reduced-motion: reduce) {
800
966
  .ss-w-abtn-shimmer { animation: none; }
801
967
  }
802
968
  `;
803
969
 
804
970
  // src/StatsWidget.tsx
805
- var import_jsx_runtime14 = require("react/jsx-runtime");
971
+ var import_jsx_runtime16 = require("react/jsx-runtime");
806
972
  function buildClient(props) {
807
973
  try {
808
974
  if ("client" in props && props.client) {
809
975
  return { client: props.client, conversation: props.client.conversation() };
810
976
  }
811
977
  const cfg = props.config;
812
- const client = new import_widget_sdk3.StatsWidgetClient(toClientOptions(cfg));
978
+ const client = new import_widget_sdk4.StatsWidgetClient(toClientOptions(cfg));
813
979
  return { client, conversation: client.conversation(toUserContext(cfg)) };
814
980
  } catch (err) {
815
981
  console.error("[StatsWidget] failed to initialize:", err instanceof Error ? err.message : err);
@@ -827,18 +993,31 @@ function injectStyles(root) {
827
993
  function StatsWidget(props) {
828
994
  const injectedClient = "client" in props ? props.client : void 0;
829
995
  const cfg = props.config;
830
- const built = (0, import_react7.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
831
- const ui = (0, import_react7.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
832
- 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);
833
999
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
834
- (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)(() => {
835
1014
  if (!built || !ui.injectStyles) return;
836
1015
  const node = rootRef.current?.getRootNode();
837
1016
  if (node) injectStyles(node);
838
1017
  }, [built, ui.injectStyles]);
839
1018
  if (!built) return null;
840
1019
  const attrs = themeAttrs(ui);
841
- 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)(
842
1021
  ChatFab,
843
1022
  {
844
1023
  label: ui.launcherLabel,