@sensiblestats/widget-react 0.7.2 → 0.8.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,16 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 8af5152: Render the new market_odds card: hero main-line rows, collapsible ladder with per-row add-to-slip, bookmaker/updated footer. Older servers simply never emit the event; older widgets ignore it.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [8af5152]
12
+ - @sensiblestats/widget-sdk@0.7.0
13
+
3
14
  ## 0.7.2
4
15
 
5
16
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ __export(index_exports, {
36
36
  module.exports = __toCommonJS(index_exports);
37
37
 
38
38
  // src/StatsWidget.tsx
39
- var import_react9 = require("react");
39
+ var import_react10 = require("react");
40
40
  var import_widget_sdk4 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/i18n.ts
@@ -58,6 +58,10 @@ var STRINGS = {
58
58
  oddsLabel: "Odds",
59
59
  liveLabel: "Live",
60
60
  addToSlip: "Add to slip",
61
+ marketPrices: "Market prices",
62
+ moreLines: "More lines",
63
+ hideLines: "Hide lines",
64
+ updatedLabel: "updated",
61
65
  inSlip: "In slip",
62
66
  slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
63
67
  slipDuplicate: "Already in your slip",
@@ -84,6 +88,10 @@ var STRINGS = {
84
88
  oddsLabel: "Oran",
85
89
  liveLabel: "Canl\u0131",
86
90
  addToSlip: "Kupona ekle",
91
+ marketPrices: "Piyasa fiyatlar\u0131",
92
+ moreLines: "Di\u011Fer \xE7izgiler",
93
+ hideLines: "\xC7izgileri gizle",
94
+ updatedLabel: "g\xFCncellendi",
87
95
  inSlip: "Kuponda",
88
96
  slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
89
97
  slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
@@ -126,6 +134,10 @@ function normalizeUi(cfg) {
126
134
  hideStats: strings.hideStats,
127
135
  oddsLabel: strings.oddsLabel,
128
136
  liveLabel: strings.liveLabel,
137
+ marketPrices: strings.marketPrices,
138
+ moreLines: strings.moreLines,
139
+ hideLines: strings.hideLines,
140
+ updatedLabel: strings.updatedLabel,
129
141
  addToSlip: strings.addToSlip,
130
142
  inSlip: strings.inSlip,
131
143
  slipAdded: strings.slipAdded,
@@ -255,6 +267,47 @@ function toSlipSelection(vm, conversationId) {
255
267
  if (conversationId !== void 0) selection.conversationId = conversationId;
256
268
  return selection;
257
269
  }
270
+ function toMarketOddsVM(card) {
271
+ const rows = Array.isArray(card.rows) ? card.rows : [];
272
+ const toRow = (r, i) => {
273
+ const selection = [str(r.selectionLabel), str(r.line)].filter(Boolean).join(" ");
274
+ const price = typeof r.price === "number" && Number.isFinite(r.price) ? r.price : void 0;
275
+ return {
276
+ key: `${selection}:${str(r.line) ?? ""}:${i}`,
277
+ selection,
278
+ odds: price !== void 0 ? price.toFixed(2) : "",
279
+ oddsDecimal: price,
280
+ matchOddsId: typeof r.matchOddsId === "number" ? r.matchOddsId : void 0,
281
+ quotedAtUtc: str(r.quotedAtUtc),
282
+ isMain: r.isMainLine !== false
283
+ };
284
+ };
285
+ const vms = rows.map(toRow).filter((r) => r.selection && r.odds);
286
+ const home = str(card.homeTeamName);
287
+ const away = str(card.awayTeamName);
288
+ const updated = rows.map((r) => str(r.quotedAtUtc)).filter((v) => !!v).sort();
289
+ return {
290
+ id: `${typeof card.matchId === "number" ? card.matchId : 0}:${str(card.marketDeveloperName) ?? ""}`,
291
+ market: str(card.marketLabel) ?? str(card.marketDeveloperName) ?? "",
292
+ fixture: home && away ? `${home} vs ${away}` : void 0,
293
+ competition: str(card.competitionName),
294
+ bookmaker: rows.map((r) => str(r.bookmakerName)).find(Boolean),
295
+ updatedAt: updated.length > 0 ? updated[updated.length - 1] : void 0,
296
+ isLive: rows.some((r) => r.isLive === true),
297
+ matchId: typeof card.matchId === "number" ? card.matchId : 0,
298
+ mainRows: vms.filter((r) => r.isMain),
299
+ moreRows: vms.filter((r) => !r.isMain),
300
+ disclaimer: str(card.disclaimer),
301
+ addToSlipEnabled: card.addToSlipEnabled === true
302
+ };
303
+ }
304
+ function marketOddsRowToSlipSelection(vm, row, conversationId) {
305
+ if (row.matchOddsId === void 0 || row.oddsDecimal === void 0) return null;
306
+ const selection = { matchOddsId: row.matchOddsId, matchId: vm.matchId, oddsDecimal: row.oddsDecimal };
307
+ if (row.quotedAtUtc !== void 0) selection.quotedAtUtc = row.quotedAtUtc;
308
+ if (conversationId !== void 0) selection.conversationId = conversationId;
309
+ return selection;
310
+ }
258
311
 
259
312
  // src/reducer.ts
260
313
  var POPUP_MAX = 160;
@@ -286,6 +339,8 @@ function applyEvent(state, event) {
286
339
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
287
340
  case "betting_insight":
288
341
  return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
342
+ case "market_odds":
343
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, marketOdds: [...t.marketOdds, toMarketOddsVM(event.data.card)] })) };
289
344
  case "intent_chip": {
290
345
  const chip = toChipVM(event.data.intent);
291
346
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -321,11 +376,11 @@ function reducer(state, action) {
321
376
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
322
377
  case "USER_SENT": {
323
378
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
324
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false };
379
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false };
325
380
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
326
381
  }
327
382
  case "GREETING_SENT": {
328
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false, isGreeting: true };
383
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false, isGreeting: true };
329
384
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
330
385
  }
331
386
  case "DISMISS_POPUP":
@@ -447,7 +502,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
447
502
  }
448
503
 
449
504
  // src/components/ChatPanel.tsx
450
- var import_react8 = require("react");
505
+ var import_react9 = require("react");
451
506
 
452
507
  // src/components/ChatHeader.tsx
453
508
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -460,7 +515,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
460
515
  }
461
516
 
462
517
  // src/components/MessageList.tsx
463
- var import_react6 = require("react");
518
+ var import_react7 = require("react");
464
519
 
465
520
  // src/components/Markdown.tsx
466
521
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -570,13 +625,12 @@ function withTimeout(value) {
570
625
  new Promise((_, reject) => setTimeout(() => reject(new Error("add-to-slip timed out")), ADD_TO_SLIP_TIMEOUT_MS))
571
626
  ]);
572
627
  }
573
- function AddToSlipButton({ it, slip, ui }) {
628
+ function SlipButton({ selection, shownOdds, marketName, slip, ui }) {
574
629
  const [pending, setPending] = (0, import_react4.useState)(false);
575
630
  const [settled, setSettled] = (0, import_react4.useState)(false);
576
631
  const [toast, setToast] = (0, import_react4.useState)(null);
577
632
  const dismiss = (0, import_react4.useCallback)(() => setToast(null), []);
578
633
  const handler = slip.onAddToSlip;
579
- const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
580
634
  if (!ui || !handler || !selection) return null;
581
635
  const onClick = async () => {
582
636
  setPending(true);
@@ -584,11 +638,11 @@ function AddToSlipButton({ it, slip, ui }) {
584
638
  const raw = await withTimeout(Promise.resolve(handler(selection)));
585
639
  if (raw != null && !(0, import_widget_sdk3.isAddToSlipResult)(raw)) throw new Error("malformed add-to-slip result");
586
640
  const result = raw ?? void 0;
587
- setToast(resultToToast(result, it.odds ?? selection.oddsDecimal.toFixed(2), ui));
641
+ setToast(resultToToast(result, shownOdds || selection.oddsDecimal.toFixed(2), ui));
588
642
  setSettled(isTerminal(result));
589
643
  if (wasAdded(result) && slip.onMarketClicked) {
590
644
  try {
591
- slip.onMarketClicked(it.market);
645
+ slip.onMarketClicked(marketName);
592
646
  } catch {
593
647
  }
594
648
  }
@@ -612,6 +666,10 @@ function AddToSlipButton({ it, slip, ui }) {
612
666
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Toast, { toast, onDismiss: dismiss })
613
667
  ] });
614
668
  }
669
+ function AddToSlipButton({ it, slip, ui }) {
670
+ const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
671
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SlipButton, { selection, shownOdds: it.odds ?? "", marketName: it.market, slip, ui });
672
+ }
615
673
 
616
674
  // src/components/BettingInsightList.tsx
617
675
  var import_jsx_runtime9 = require("react/jsx-runtime");
@@ -684,38 +742,88 @@ function BettingInsightList({ insights, ui, slip }) {
684
742
  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)) });
685
743
  }
686
744
 
687
- // src/components/ActionButtons.tsx
745
+ // src/components/MarketOddsCardList.tsx
746
+ var import_react6 = require("react");
688
747
  var import_jsx_runtime10 = require("react/jsx-runtime");
748
+ function OddsRow({ vm, row, ui, slip }) {
749
+ const selection = vm.addToSlipEnabled ? marketOddsRowToSlipSelection(vm, row, slip.conversationId) : null;
750
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-row", children: [
751
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-sel", children: row.selection }),
752
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-price", children: row.odds }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
754
+ ] });
755
+ }
756
+ function formatUpdated(iso) {
757
+ if (!iso) return void 0;
758
+ const date = new Date(iso);
759
+ if (Number.isNaN(date.getTime())) return void 0;
760
+ return date.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" });
761
+ }
762
+ function MarketOddsCard({ vm, ui, slip }) {
763
+ const [open, setOpen] = (0, import_react6.useState)(false);
764
+ const updated = formatUpdated(vm.updatedAt);
765
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mocard", children: [
766
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-head", children: [
767
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-market", children: vm.market }),
768
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-tag", children: ui?.marketPrices ?? "Market prices" })
769
+ ] }),
770
+ vm.fixture || vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-fixture", children: [
771
+ vm.fixture ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: vm.fixture }) : null,
772
+ vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-mo-comp", children: vm.competition }) : null
773
+ ] }) : null,
774
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-rows", children: [
775
+ vm.mainRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OddsRow, { vm, row, ui, slip }, row.key)),
776
+ open ? vm.moreRows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OddsRow, { vm, row, ui, slip }, row.key)) : null
777
+ ] }),
778
+ vm.moreRows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("button", { type: "button", className: "ss-w-bi-toggle", "aria-expanded": open, onClick: () => setOpen((v) => !v), children: [
779
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: open ? ui?.hideLines ?? "Hide lines" : `${ui?.moreLines ?? "More lines"} (${vm.moreRows.length})` }),
780
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: `ss-w-bi-chev${open ? " ss-w-bi-chev-up" : ""}`, "aria-hidden": "true" })
781
+ ] }) : null,
782
+ vm.bookmaker || updated ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-mo-foot", children: [
783
+ vm.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: vm.bookmaker }) : null,
784
+ updated ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: `${ui?.updatedLabel ?? "updated"} ${updated}` }) : null
785
+ ] }) : null,
786
+ vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
787
+ ] });
788
+ }
789
+ function MarketOddsCardList({ cards, ui, slip }) {
790
+ if (!cards || cards.length === 0) return null;
791
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(MarketOddsCard, { vm, ui, slip }, vm.id)) });
792
+ }
793
+
794
+ // src/components/ActionButtons.tsx
795
+ var import_jsx_runtime11 = require("react/jsx-runtime");
689
796
  function ActionButtons({ actions, loading, disabled, onAct }) {
690
797
  if (actions.length === 0 && !loading) return null;
691
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-actions", children: [
692
- 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}`)),
693
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
694
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-abtn-shimmer" }),
695
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "ss-w-abtn-shimmer" })
798
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-actions", children: [
799
+ actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
800
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(import_jsx_runtime11.Fragment, { children: [
801
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-abtn-shimmer" }),
802
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-abtn-shimmer" })
696
803
  ] }) : null
697
804
  ] });
698
805
  }
699
806
 
700
807
  // src/components/ChatMessage.tsx
701
- var import_jsx_runtime11 = require("react/jsx-runtime");
808
+ var import_jsx_runtime12 = require("react/jsx-runtime");
702
809
  function ChatMessage({ message, disabled, onAct, ui, slip }) {
703
810
  if (message.role === "user") {
704
- 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 }) });
811
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
705
812
  }
706
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
707
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Markdown, { text: message.text }) : null,
708
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
709
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
710
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
813
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
814
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Markdown, { text: message.text }) : null,
815
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
816
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
817
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
711
819
  ] });
712
820
  }
713
821
 
714
822
  // src/components/IntentChips.tsx
715
- var import_jsx_runtime12 = require("react/jsx-runtime");
823
+ var import_jsx_runtime13 = require("react/jsx-runtime");
716
824
  function IntentChips({ chips, onPick }) {
717
825
  if (chips.length === 0) return null;
718
- 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}`)) });
826
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
719
827
  }
720
828
 
721
829
  // src/scroll.ts
@@ -725,17 +833,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
725
833
  }
726
834
 
727
835
  // src/components/MessageList.tsx
728
- var import_jsx_runtime13 = require("react/jsx-runtime");
836
+ var import_jsx_runtime14 = require("react/jsx-runtime");
729
837
  function MessageList({ controller, ui, slip }) {
730
838
  const { state, send } = controller;
731
- const listRef = (0, import_react6.useRef)(null);
732
- const endRef = (0, import_react6.useRef)(null);
733
- const pinnedRef = (0, import_react6.useRef)(true);
734
- (0, import_react6.useEffect)(() => {
839
+ const listRef = (0, import_react7.useRef)(null);
840
+ const endRef = (0, import_react7.useRef)(null);
841
+ const pinnedRef = (0, import_react7.useRef)(true);
842
+ (0, import_react7.useEffect)(() => {
735
843
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
736
844
  }, [state.messages, state.status]);
737
845
  const empty = state.messages.length === 0;
738
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
846
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
739
847
  "div",
740
848
  {
741
849
  className: "ss-w-list",
@@ -745,7 +853,7 @@ function MessageList({ controller, ui, slip }) {
745
853
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
746
854
  },
747
855
  children: [
748
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
856
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
749
857
  ChatMessage,
750
858
  {
751
859
  message: m,
@@ -756,25 +864,25 @@ function MessageList({ controller, ui, slip }) {
756
864
  },
757
865
  m.id
758
866
  )),
759
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
867
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
760
868
  } }) : null,
761
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
762
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
763
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
764
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
869
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
870
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
871
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
872
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
765
873
  ] }) : null,
766
- empty ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
767
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { ref: endRef })
874
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
875
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { ref: endRef })
768
876
  ]
769
877
  }
770
878
  );
771
879
  }
772
880
 
773
881
  // src/components/ChatInput.tsx
774
- var import_react7 = require("react");
775
- var import_jsx_runtime14 = require("react/jsx-runtime");
882
+ var import_react8 = require("react");
883
+ var import_jsx_runtime15 = require("react/jsx-runtime");
776
884
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
777
- const [value, setValue] = (0, import_react7.useState)("");
885
+ const [value, setValue] = (0, import_react8.useState)("");
778
886
  const submit = () => {
779
887
  const t = value.trim();
780
888
  if (!t) return;
@@ -787,18 +895,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
787
895
  submit();
788
896
  }
789
897
  };
790
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-input", children: [
791
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
792
- 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, {}) })
898
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ss-w-input", children: [
899
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
900
+ streaming ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SendIcon, {}) })
793
901
  ] });
794
902
  }
795
903
 
796
904
  // src/components/ChatPanel.tsx
797
- var import_jsx_runtime15 = require("react/jsx-runtime");
905
+ var import_jsx_runtime16 = require("react/jsx-runtime");
798
906
  var FULLSCREEN_QUERY = "(max-width: 639px)";
799
907
  function useIsModal() {
800
- const [modal, setModal] = (0, import_react8.useState)(false);
801
- (0, import_react8.useEffect)(() => {
908
+ const [modal, setModal] = (0, import_react9.useState)(false);
909
+ (0, import_react9.useEffect)(() => {
802
910
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
803
911
  const mql = window.matchMedia(FULLSCREEN_QUERY);
804
912
  const sync = () => setModal(mql.matches);
@@ -811,17 +919,17 @@ function useIsModal() {
811
919
  function ChatPanel({ controller, ui, slip }) {
812
920
  const { state, close, stop, send } = controller;
813
921
  const isModal = useIsModal();
814
- (0, import_react8.useEffect)(() => {
922
+ (0, import_react9.useEffect)(() => {
815
923
  const onKey = (e) => {
816
924
  if (e.key === "Escape") close();
817
925
  };
818
926
  window.addEventListener("keydown", onKey);
819
927
  return () => window.removeEventListener("keydown", onKey);
820
928
  }, [close]);
821
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
822
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
823
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MessageList, { controller, ui, slip }),
824
- /* @__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 })
929
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
930
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
931
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(MessageList, { controller, ui, slip }),
932
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
825
933
  ] });
826
934
  }
827
935
 
@@ -957,6 +1065,22 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
957
1065
  .ss-w-bi-chip { font-size: 11px; font-weight: 600; border: none; border-radius: 7px; padding: 4px 10px; color: var(--ss-w-faint); background: none; cursor: pointer; }
958
1066
  .ss-w-bi-chip[aria-pressed="true"] { color: var(--ss-w-ink); background: var(--ss-w-panel); box-shadow: 0 1px 2px rgba(0,0,0,.08); }
959
1067
 
1068
+ /* market odds card */
1069
+ .ss-w-mocards { display: flex; flex-direction: column; gap: 7px; }
1070
+ .ss-w-mocard { background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; border-left: 3px solid var(--ss-w-accent); padding: 9px 11px; display: flex; flex-direction: column; gap: 6px; }
1071
+ .ss-w-mo-head { display: flex; align-items: center; gap: 8px; }
1072
+ .ss-w-mo-market { font-size: 13px; font-weight: 700; color: var(--ss-w-ink); }
1073
+ .ss-w-mo-tag { margin-left: auto; flex: none; font-size: 9px; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--ss-w-faint); border: 1px solid var(--ss-w-line); border-radius: 8px; padding: 2px 7px; }
1074
+ .ss-w-mo-fixture { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ss-w-faint); min-width: 0; }
1075
+ .ss-w-mo-comp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1076
+ .ss-w-mo-rows { display: flex; flex-direction: column; gap: 4px; }
1077
+ .ss-w-mo-row { display: flex; align-items: center; gap: 10px; padding: 4px 0; border-bottom: 1px dashed var(--ss-w-line); }
1078
+ .ss-w-mo-row:last-child { border-bottom: none; }
1079
+ .ss-w-mo-sel { font-size: 12.5px; font-weight: 600; color: var(--ss-w-ink); }
1080
+ .ss-w-mo-price { margin-left: auto; font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 14px; font-weight: 700; color: var(--ss-w-accent); }
1081
+ .ss-w-mo-row .ss-w-slip { margin: 0; }
1082
+ .ss-w-mo-foot { display: flex; align-items: center; gap: 8px; font-size: 10.5px; color: var(--ss-w-faint); }
1083
+
960
1084
  /* action buttons */
961
1085
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
962
1086
  .ss-w-abtn { font-size: 12px; font-weight: 560; color: var(--ss-w-accent); background: var(--ss-w-panel); border: 1px solid var(--ss-w-accent); border-radius: 16px; padding: 6px 12px; cursor: pointer; }
@@ -994,7 +1118,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
994
1118
  `;
995
1119
 
996
1120
  // src/StatsWidget.tsx
997
- var import_jsx_runtime16 = require("react/jsx-runtime");
1121
+ var import_jsx_runtime17 = require("react/jsx-runtime");
998
1122
  function buildClient(props) {
999
1123
  try {
1000
1124
  if ("client" in props && props.client) {
@@ -1019,11 +1143,11 @@ function injectStyles(root) {
1019
1143
  function StatsWidget(props) {
1020
1144
  const injectedClient = "client" in props ? props.client : void 0;
1021
1145
  const cfg = props.config;
1022
- const built = (0, import_react9.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
1023
- const ui = (0, import_react9.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
1024
- const rootRef = (0, import_react9.useRef)(null);
1146
+ const built = (0, import_react10.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
1147
+ const ui = (0, import_react10.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
1148
+ const rootRef = (0, import_react10.useRef)(null);
1025
1149
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
1026
- const slip = (0, import_react9.useMemo)(
1150
+ const slip = (0, import_react10.useMemo)(
1027
1151
  () => ({
1028
1152
  onAddToSlip: props.config?.onAddToSlip,
1029
1153
  conversationId: controller.state.conversationId ?? void 0,
@@ -1036,14 +1160,14 @@ function StatsWidget(props) {
1036
1160
  }),
1037
1161
  [props.config?.onAddToSlip, controller.state.conversationId, built]
1038
1162
  );
1039
- (0, import_react9.useEffect)(() => {
1163
+ (0, import_react10.useEffect)(() => {
1040
1164
  if (!built || !ui.injectStyles) return;
1041
1165
  const node = rootRef.current?.getRootNode();
1042
1166
  if (node) injectStyles(node);
1043
1167
  }, [built, ui.injectStyles]);
1044
1168
  if (!built) return null;
1045
1169
  const attrs = themeAttrs(ui);
1046
- 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)(
1170
+ return /* @__PURE__ */ (0, import_jsx_runtime17.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_runtime17.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1047
1171
  ChatFab,
1048
1172
  {
1049
1173
  label: ui.launcherLabel,