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