@sensiblestats/widget-react 0.3.0 → 0.5.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,26 @@
1
1
  # @sensiblestats/widget-react
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Redesigned betting-insight card with scoped supporting stats.
8
+
9
+ - **widget-sdk:** extend `BettingInsight` with `homeTeamName`, `awayTeamName`, and `scopes` (`{ key, label, stats }[]`), so consumers can type the fixture line and the per-scope stat sets (this league / all competitions / head-to-head) the server now streams.
10
+ - **widget-react:** rebuild the betting card around the new payload — a phase-aware live/pre-match status line with the fixture, a promoted market with clearly labeled odds (and an optional bookmaker name), and scope chips that switch the supporting stats in place (shown only when ≥2 scopes are present, otherwise flat stat rows with a "show more" expander). The responsible-gambling disclaimer always renders. Type scale and spacing match the surrounding chat cards.
11
+ - **widget-embed:** rebuild against `@sensiblestats/widget-react` so the hosted embed renders the redesigned card.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+ - @sensiblestats/widget-sdk@0.4.0
17
+
18
+ ## 0.4.0
19
+
20
+ ### Minor Changes
21
+
22
+ - Render `betting_insight` events as insight cards (market, selection + line, odds with an optional live badge, supporting stats, and the responsible-gambling disclaimer). **Bug fix:** the reducer previously had no case for `betting_insight` and no fallthrough, so any unmodelled event returned `undefined` and corrupted state — the next event then crashed the widget (`Cannot read properties of undefined (reading 'length')`). Unknown event types are now ignored safely and can never break the UI.
23
+
3
24
  ## 0.3.0
4
25
 
5
26
  ### Minor 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_react6 = require("react");
39
+ var import_react7 = require("react");
40
40
  var import_widget_sdk3 = require("@sensiblestats/widget-sdk");
41
41
 
42
42
  // src/i18n.ts
@@ -52,7 +52,9 @@ var STRINGS = {
52
52
  somethingWentWrong: "Something went wrong.",
53
53
  retry: "Retry",
54
54
  showLess: "Show less",
55
- showMore: (n) => `Show ${n} more`
55
+ showMore: (n) => `Show ${n} more`,
56
+ oddsLabel: "Odds",
57
+ liveLabel: "Live"
56
58
  },
57
59
  tr: {
58
60
  launcherLabel: "SensibleStats ile sohbet et",
@@ -65,7 +67,9 @@ var STRINGS = {
65
67
  somethingWentWrong: "Bir \u015Feyler ters gitti.",
66
68
  retry: "Tekrar dene",
67
69
  showLess: "Daha az g\xF6ster",
68
- showMore: (n) => `${n} tane daha g\xF6ster`
70
+ showMore: (n) => `${n} tane daha g\xF6ster`,
71
+ oddsLabel: "Oran",
72
+ liveLabel: "Canl\u0131"
69
73
  }
70
74
  };
71
75
  function resolveLocaleStrings(locale) {
@@ -95,7 +99,9 @@ function normalizeUi(cfg) {
95
99
  somethingWentWrong: strings.somethingWentWrong,
96
100
  retry: strings.retry,
97
101
  showLess: strings.showLess,
98
- showMore: strings.showMore
102
+ showMore: strings.showMore,
103
+ oddsLabel: strings.oddsLabel,
104
+ liveLabel: strings.liveLabel
99
105
  };
100
106
  }
101
107
  function toClientOptions(cfg) {
@@ -132,16 +138,19 @@ function str(v) {
132
138
  function num(v) {
133
139
  return typeof v === "number" && Number.isFinite(v) ? String(v) : void 0;
134
140
  }
135
- function readStats(card) {
136
- if (!Array.isArray(card.stats)) return [];
141
+ function readStatCells(raw) {
142
+ if (!Array.isArray(raw)) return [];
137
143
  const cells = [];
138
- for (const entry of card.stats) {
144
+ for (const entry of raw) {
139
145
  const label = str(entry.label);
140
146
  const value = num(entry.value) ?? str(entry.value);
141
147
  if (label !== void 0 && value !== void 0) cells.push({ label, value });
142
148
  }
143
149
  return cells;
144
150
  }
151
+ function readStats(card) {
152
+ return readStatCells(card.stats);
153
+ }
145
154
  function toCardVM(card) {
146
155
  const entityType = str(card.entityType)?.toLowerCase();
147
156
  const kind = entityType === "player" ? "player" : entityType === "team" ? "team" : "generic";
@@ -167,6 +176,31 @@ function toChipVM(intent) {
167
176
  function chipFromText(text) {
168
177
  return { text };
169
178
  }
179
+ function toInsightVM(insight) {
180
+ const market = str(insight.marketLabel) ?? str(insight.marketDeveloperName) ?? "";
181
+ const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
182
+ const value = insight.odds?.value;
183
+ const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
184
+ const home = str(insight.homeTeamName);
185
+ const away = str(insight.awayTeamName);
186
+ const fixture = home && away ? `${home} vs ${away}` : void 0;
187
+ const scopes = Array.isArray(insight.scopes) ? insight.scopes.map((s) => ({ key: str(s.key), label: str(s.label), stats: readStatCells(s.stats) })).filter((s) => !!s.key && !!s.label) : void 0;
188
+ return {
189
+ id: str(insight.insightId) ?? `${str(insight.ruleId) ?? "insight"}:${num(insight.matchId) ?? ""}`,
190
+ market,
191
+ selection,
192
+ odds,
193
+ // The wire payload carries bookmakerId (not a name); render a name only if one is present.
194
+ bookmaker: str(insight.odds?.bookmakerName),
195
+ isLive: insight.odds?.isLive === true,
196
+ phase: str(insight.phase),
197
+ fixture,
198
+ scopes,
199
+ text: str(insight.text) ?? "",
200
+ stats: readStatCells(insight.stats),
201
+ disclaimer: str(insight.disclaimer)
202
+ };
203
+ }
170
204
 
171
205
  // src/reducer.ts
172
206
  var POPUP_MAX = 160;
@@ -193,6 +227,8 @@ function applyEvent(state, event) {
193
227
  return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, toCardVM(event.data.card)] })) };
194
228
  case "action_button":
195
229
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
230
+ case "betting_insight":
231
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
196
232
  case "intent_chip": {
197
233
  const chip = toChipVM(event.data.intent);
198
234
  return chip ? { ...state, starters: [...state.starters, chip] } : state;
@@ -210,6 +246,7 @@ function applyEvent(state, event) {
210
246
  return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
211
247
  }
212
248
  }
249
+ return state;
213
250
  }
214
251
  function reducer(state, action) {
215
252
  switch (action.kind) {
@@ -219,11 +256,11 @@ function reducer(state, action) {
219
256
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
220
257
  case "USER_SENT": {
221
258
  const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
222
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false };
259
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false };
223
260
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
224
261
  }
225
262
  case "GREETING_SENT": {
226
- const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false, isGreeting: true };
263
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], done: false, isGreeting: true };
227
264
  return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
228
265
  }
229
266
  case "DISMISS_POPUP":
@@ -345,7 +382,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
345
382
  }
346
383
 
347
384
  // src/components/ChatPanel.tsx
348
- var import_react5 = require("react");
385
+ var import_react6 = require("react");
349
386
 
350
387
  // src/components/ChatHeader.tsx
351
388
  var import_jsx_runtime4 = require("react/jsx-runtime");
@@ -358,7 +395,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
358
395
  }
359
396
 
360
397
  // src/components/MessageList.tsx
361
- var import_react3 = require("react");
398
+ var import_react4 = require("react");
362
399
 
363
400
  // src/components/Markdown.tsx
364
401
  var import_react_markdown = __toESM(require("react-markdown"), 1);
@@ -411,37 +448,94 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
411
448
  ] });
412
449
  }
413
450
 
414
- // src/components/ActionButtons.tsx
451
+ // src/components/BettingInsightList.tsx
452
+ var import_react3 = require("react");
415
453
  var import_jsx_runtime7 = require("react/jsx-runtime");
454
+ var MAX_ROWS = 3;
455
+ function StatRows({ stats, ui }) {
456
+ const [expanded, setExpanded] = (0, import_react3.useState)(false);
457
+ const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
458
+ const overflow = stats.length - MAX_ROWS;
459
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-rows", children: [
460
+ shown.map((s) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-row", children: [
461
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-lbl", children: s.label }),
462
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
463
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-row-num", children: s.value })
464
+ ] }, `${s.label}:${s.value}`)),
465
+ 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
466
+ ] });
467
+ }
468
+ function ScopedStats({ scopes, ui }) {
469
+ const [active, setActive] = (0, import_react3.useState)(0);
470
+ const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
471
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-scoped", children: [
472
+ /* @__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)) }),
473
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(StatRows, { stats: current.stats, ui }, current.key)
474
+ ] });
475
+ }
476
+ function InsightCard({ it, ui }) {
477
+ const live = it.isLive || it.phase === "live";
478
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-binsight", children: [
479
+ live || it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-status", children: [
480
+ live ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-live", children: [
481
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
482
+ ui?.liveLabel ?? "Live"
483
+ ] }) : null,
484
+ it.fixture ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null
485
+ ] }) : null,
486
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-bi-head", children: [
487
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-mkt", children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-market", children: it.market }),
489
+ it.selection ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-sel", children: it.selection }) : null
490
+ ] }),
491
+ it.odds ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "ss-w-bi-odds", children: [
492
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
493
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("b", { children: it.odds }),
494
+ it.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
495
+ ] }) : null
496
+ ] }),
497
+ it.text ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-text", children: it.text }) : null,
498
+ 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,
499
+ it.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
500
+ ] });
501
+ }
502
+ function BettingInsightList({ insights, ui }) {
503
+ if (!insights || insights.length === 0) return null;
504
+ 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)) });
505
+ }
506
+
507
+ // src/components/ActionButtons.tsx
508
+ var import_jsx_runtime8 = require("react/jsx-runtime");
416
509
  function ActionButtons({ actions, loading, disabled, onAct }) {
417
510
  if (actions.length === 0 && !loading) return null;
418
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "ss-w-actions", children: [
419
- actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
420
- actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
421
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" }),
422
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "ss-w-abtn-shimmer" })
511
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-actions", children: [
512
+ 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}`)),
513
+ actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
514
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" }),
515
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "ss-w-abtn-shimmer" })
423
516
  ] }) : null
424
517
  ] });
425
518
  }
426
519
 
427
520
  // src/components/ChatMessage.tsx
428
- var import_jsx_runtime8 = require("react/jsx-runtime");
521
+ var import_jsx_runtime9 = require("react/jsx-runtime");
429
522
  function ChatMessage({ message, disabled, onAct, ui }) {
430
523
  if (message.role === "user") {
431
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
524
+ 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 }) });
432
525
  }
433
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
434
- message.text ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Markdown, { text: message.text }) : null,
435
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
436
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
526
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
527
+ message.text ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(Markdown, { text: message.text }) : null,
528
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
529
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BettingInsightList, { insights: message.insights, ui }),
530
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
437
531
  ] });
438
532
  }
439
533
 
440
534
  // src/components/IntentChips.tsx
441
- var import_jsx_runtime9 = require("react/jsx-runtime");
535
+ var import_jsx_runtime10 = require("react/jsx-runtime");
442
536
  function IntentChips({ chips, onPick }) {
443
537
  if (chips.length === 0) return null;
444
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
538
+ 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}`)) });
445
539
  }
446
540
 
447
541
  // src/scroll.ts
@@ -451,17 +545,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
451
545
  }
452
546
 
453
547
  // src/components/MessageList.tsx
454
- var import_jsx_runtime10 = require("react/jsx-runtime");
548
+ var import_jsx_runtime11 = require("react/jsx-runtime");
455
549
  function MessageList({ controller, ui }) {
456
550
  const { state, send } = controller;
457
- const listRef = (0, import_react3.useRef)(null);
458
- const endRef = (0, import_react3.useRef)(null);
459
- const pinnedRef = (0, import_react3.useRef)(true);
460
- (0, import_react3.useEffect)(() => {
551
+ const listRef = (0, import_react4.useRef)(null);
552
+ const endRef = (0, import_react4.useRef)(null);
553
+ const pinnedRef = (0, import_react4.useRef)(true);
554
+ (0, import_react4.useEffect)(() => {
461
555
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
462
556
  }, [state.messages, state.status]);
463
557
  const empty = state.messages.length === 0;
464
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
558
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
465
559
  "div",
466
560
  {
467
561
  className: "ss-w-list",
@@ -471,7 +565,7 @@ function MessageList({ controller, ui }) {
471
565
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
472
566
  },
473
567
  children: [
474
- state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
568
+ state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
475
569
  ChatMessage,
476
570
  {
477
571
  message: m,
@@ -481,25 +575,25 @@ function MessageList({ controller, ui }) {
481
575
  },
482
576
  m.id
483
577
  )),
484
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
578
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
485
579
  } }) : null,
486
- state.status ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
487
- state.error ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
488
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
489
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
580
+ state.status ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
581
+ state.error ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
582
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
583
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
490
584
  ] }) : null,
491
- empty ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
492
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { ref: endRef })
585
+ empty ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
586
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { ref: endRef })
493
587
  ]
494
588
  }
495
589
  );
496
590
  }
497
591
 
498
592
  // src/components/ChatInput.tsx
499
- var import_react4 = require("react");
500
- var import_jsx_runtime11 = require("react/jsx-runtime");
593
+ var import_react5 = require("react");
594
+ var import_jsx_runtime12 = require("react/jsx-runtime");
501
595
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
502
- const [value, setValue] = (0, import_react4.useState)("");
596
+ const [value, setValue] = (0, import_react5.useState)("");
503
597
  const submit = () => {
504
598
  const t = value.trim();
505
599
  if (!t) return;
@@ -512,18 +606,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
512
606
  submit();
513
607
  }
514
608
  };
515
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "ss-w-input", children: [
516
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
517
- streaming ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(SendIcon, {}) })
609
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-input", children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
611
+ 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, {}) })
518
612
  ] });
519
613
  }
520
614
 
521
615
  // src/components/ChatPanel.tsx
522
- var import_jsx_runtime12 = require("react/jsx-runtime");
616
+ var import_jsx_runtime13 = require("react/jsx-runtime");
523
617
  var FULLSCREEN_QUERY = "(max-width: 639px)";
524
618
  function useIsModal() {
525
- const [modal, setModal] = (0, import_react5.useState)(false);
526
- (0, import_react5.useEffect)(() => {
619
+ const [modal, setModal] = (0, import_react6.useState)(false);
620
+ (0, import_react6.useEffect)(() => {
527
621
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
528
622
  const mql = window.matchMedia(FULLSCREEN_QUERY);
529
623
  const sync = () => setModal(mql.matches);
@@ -536,17 +630,17 @@ function useIsModal() {
536
630
  function ChatPanel({ controller, ui }) {
537
631
  const { state, close, stop, send } = controller;
538
632
  const isModal = useIsModal();
539
- (0, import_react5.useEffect)(() => {
633
+ (0, import_react6.useEffect)(() => {
540
634
  const onKey = (e) => {
541
635
  if (e.key === "Escape") close();
542
636
  };
543
637
  window.addEventListener("keydown", onKey);
544
638
  return () => window.removeEventListener("keydown", onKey);
545
639
  }, [close]);
546
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
547
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
548
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(MessageList, { controller, ui }),
549
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
640
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
641
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
642
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(MessageList, { controller, ui }),
643
+ /* @__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 })
550
644
  ] });
551
645
  }
552
646
 
@@ -645,6 +739,36 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
645
739
  .ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }
646
740
  .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
647
741
 
742
+ /* betting insights */
743
+ .ss-w-binsights { display: flex; flex-direction: column; gap: 7px; }
744
+ .ss-w-binsight { 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; }
745
+ .ss-w-bi-status { display: flex; align-items: center; gap: 6px; min-width: 0; }
746
+ .ss-w-bi-fixture { font-size: 11px; color: var(--ss-w-ink); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
747
+ .ss-w-bi-live { font-size: 8.5px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: #fff; background: #c0392b; border-radius: 4px; padding: 1px 5px; display: inline-flex; align-items: center; gap: 4px; flex: none; }
748
+ .ss-w-bi-live-dot { width: 5px; height: 5px; border-radius: 50%; background: #fff; }
749
+ .ss-w-bi-head { display: flex; align-items: flex-start; gap: 10px; }
750
+ .ss-w-bi-mkt { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
751
+ .ss-w-bi-market { font-size: 13px; font-weight: 660; line-height: 1.3; color: var(--ss-w-ink); }
752
+ .ss-w-bi-sel { font-size: 11px; line-height: 1.3; color: var(--ss-w-faint); }
753
+ .ss-w-bi-odds { margin-left: auto; flex: none; display: flex; flex-direction: column; align-items: center; justify-content: center; background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 24%, var(--ss-w-line)); border-radius: 8px; padding: 3px 9px; min-width: 56px; }
754
+ .ss-w-bi-odds-cap { font-size: 8px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--ss-w-faint); }
755
+ .ss-w-bi-odds b { font-size: 15px; font-family: ui-monospace, monospace; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); line-height: 1.15; }
756
+ .ss-w-bi-book { font-size: 9.5px; color: var(--ss-w-faint); }
757
+ .ss-w-bi-text { font-size: 12px; color: var(--ss-w-ink); }
758
+ .ss-w-bi-rows { display: flex; flex-direction: column; }
759
+ .ss-w-bi-row { display: flex; align-items: baseline; gap: 8px; padding: 4px 0; border-top: 1px solid var(--ss-w-line); }
760
+ .ss-w-bi-row:first-child { border-top: none; }
761
+ .ss-w-bi-row-lbl { font-size: 11px; color: var(--ss-w-faint); min-width: 0; }
762
+ .ss-w-bi-row-dots { flex: 1; border-bottom: 1px dotted var(--ss-w-line); transform: translateY(-3px); min-width: 12px; }
763
+ .ss-w-bi-row-num { font-family: ui-monospace, monospace; font-weight: 700; font-variant-numeric: tabular-nums; font-size: 12.5px; flex: none; color: var(--ss-w-ink); }
764
+ .ss-w-bi-more { align-self: flex-start; background: none; border: none; color: var(--ss-w-accent); font: inherit; font-size: 11px; font-weight: 560; padding: 2px 0; cursor: pointer; }
765
+ .ss-w-bi-more:hover { text-decoration: underline; }
766
+ .ss-w-bi-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
767
+ .ss-w-bi-scoped { display: flex; flex-direction: column; gap: 6px; }
768
+ .ss-w-bi-scopes { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
769
+ .ss-w-bi-chip { font-size: 11px; font-weight: 550; border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 3px 10px; color: var(--ss-w-ink); background: var(--ss-w-bubble); cursor: pointer; }
770
+ .ss-w-bi-chip[aria-pressed="true"] { color: #fff; background: var(--ss-w-accent); border-color: var(--ss-w-accent); }
771
+
648
772
  /* action buttons */
649
773
  .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
650
774
  .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; }
@@ -674,7 +798,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
674
798
  `;
675
799
 
676
800
  // src/StatsWidget.tsx
677
- var import_jsx_runtime13 = require("react/jsx-runtime");
801
+ var import_jsx_runtime14 = require("react/jsx-runtime");
678
802
  function buildClient(props) {
679
803
  try {
680
804
  if ("client" in props && props.client) {
@@ -699,18 +823,18 @@ function injectStyles(root) {
699
823
  function StatsWidget(props) {
700
824
  const injectedClient = "client" in props ? props.client : void 0;
701
825
  const cfg = props.config;
702
- const built = (0, import_react6.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
703
- const ui = (0, import_react6.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
704
- const rootRef = (0, import_react6.useRef)(null);
826
+ const built = (0, import_react7.useMemo)(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
827
+ const ui = (0, import_react7.useMemo)(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
828
+ const rootRef = (0, import_react7.useRef)(null);
705
829
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
706
- (0, import_react6.useEffect)(() => {
830
+ (0, import_react7.useEffect)(() => {
707
831
  if (!built || !ui.injectStyles) return;
708
832
  const node = rootRef.current?.getRootNode();
709
833
  if (node) injectStyles(node);
710
834
  }, [built, ui.injectStyles]);
711
835
  if (!built) return null;
712
836
  const attrs = themeAttrs(ui);
713
- return /* @__PURE__ */ (0, import_jsx_runtime13.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_runtime13.jsx)(ChatPanel, { controller, ui }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
837
+ 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)(
714
838
  ChatFab,
715
839
  {
716
840
  label: ui.launcherLabel,