@sensiblestats/widget-react 0.12.0 → 0.13.1
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 +21 -0
- package/dist/index.cjs +256 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +256 -41
- package/dist/index.js.map +1 -1
- package/dist/styles.css +24 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @sensiblestats/widget-react
|
|
2
2
|
|
|
3
|
+
## 0.13.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Odds movement chart: drop the direction clause from the chart's `aria-label` when the price is flat. It used to read "…currently 1.44, ." — a screen reader announced the dangling punctuation on exactly the no-movement case.
|
|
8
|
+
|
|
9
|
+
## 0.13.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- Odds movement price chart card (SP4 widget half).
|
|
14
|
+
|
|
15
|
+
- `widget-sdk`: `odds_movement` chat event — `OddsMovementCard`, `OddsMovementPoint`, `OddsMovementEventData`.
|
|
16
|
+
- `widget-react`: renders the card — a time-proportional inline SVG price chart with bookmaker, direction badge, suspended state and excluded-observation disclosure.
|
|
17
|
+
- `widget-embed`: re-bundles `widget-react`, so it ships the new card too.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @sensiblestats/widget-sdk@0.11.0
|
|
23
|
+
|
|
3
24
|
## 0.12.0
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -65,6 +65,20 @@ var STRINGS = {
|
|
|
65
65
|
oddsShortened: "shortened",
|
|
66
66
|
oddsDrifted: "drifted",
|
|
67
67
|
updatedLabel: "updated",
|
|
68
|
+
priceMovement: "Price movement",
|
|
69
|
+
openingLabel: "Opening",
|
|
70
|
+
currentLabel: "Current",
|
|
71
|
+
movementWindow: (hours) => hours % 24 === 0 ? `Last ${hours / 24}d` : `Last ${hours}h`,
|
|
72
|
+
notEnoughHistory: "No price history recorded yet for this selection.",
|
|
73
|
+
singlePricePoint: "Only one price recorded so far for this selection.",
|
|
74
|
+
selectionSuspended: "This selection is not currently available to bet.",
|
|
75
|
+
excludedSuspended: (n) => `${n} suspended price${n === 1 ? "" : "s"} excluded from this chart.`,
|
|
76
|
+
chartTimeLabel: (ms) => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
|
|
77
|
+
chartAria: (opening, current, direction, hours) => {
|
|
78
|
+
const span = hours % 24 === 0 ? `${hours / 24} day${hours / 24 === 1 ? "" : "s"}` : `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
79
|
+
const drift = direction ? `, ${direction}` : "";
|
|
80
|
+
return `Price movement over the last ${span}: opened at ${opening}, currently ${current}${drift}.`;
|
|
81
|
+
},
|
|
68
82
|
inSlip: "In slip",
|
|
69
83
|
slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
|
|
70
84
|
slipDuplicate: "Already in your slip",
|
|
@@ -98,6 +112,20 @@ var STRINGS = {
|
|
|
98
112
|
oddsShortened: "d\xFC\u015Ft\xFC",
|
|
99
113
|
oddsDrifted: "y\xFCkseldi",
|
|
100
114
|
updatedLabel: "g\xFCncellendi",
|
|
115
|
+
priceMovement: "Oran hareketi",
|
|
116
|
+
openingLabel: "A\xE7\u0131l\u0131\u015F",
|
|
117
|
+
currentLabel: "G\xFCncel",
|
|
118
|
+
movementWindow: (hours) => hours % 24 === 0 ? `Son ${hours / 24} g\xFCn` : `Son ${hours} saat`,
|
|
119
|
+
notEnoughHistory: "Bu se\xE7im i\xE7in hen\xFCz oran ge\xE7mi\u015Fi kaydedilmedi.",
|
|
120
|
+
singlePricePoint: "Bu se\xE7im i\xE7in \u015Fimdiye kadar yaln\u0131zca bir fiyat kaydedildi.",
|
|
121
|
+
selectionSuspended: "Bu se\xE7im \u015Fu anda bahse kapal\u0131.",
|
|
122
|
+
excludedSuspended: (n) => `${n} ask\u0131ya al\u0131nm\u0131\u015F fiyat bu grafikten hari\xE7 tutuldu.`,
|
|
123
|
+
chartTimeLabel: (ms) => new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
|
|
124
|
+
chartAria: (opening, current, direction, hours) => {
|
|
125
|
+
const span = hours % 24 === 0 ? `${hours / 24} g\xFCn` : `${hours} saat`;
|
|
126
|
+
const drift = direction ? `, ${direction}` : "";
|
|
127
|
+
return `Son ${span} i\xE7indeki oran hareketi: ${opening} ile a\xE7\u0131ld\u0131, \u015Fu anda ${current}${drift}.`;
|
|
128
|
+
},
|
|
101
129
|
inSlip: "Kuponda",
|
|
102
130
|
slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
|
|
103
131
|
slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
|
|
@@ -147,6 +175,16 @@ function normalizeUi(cfg) {
|
|
|
147
175
|
oddsShortened: strings.oddsShortened,
|
|
148
176
|
oddsDrifted: strings.oddsDrifted,
|
|
149
177
|
updatedLabel: strings.updatedLabel,
|
|
178
|
+
priceMovement: strings.priceMovement,
|
|
179
|
+
openingLabel: strings.openingLabel,
|
|
180
|
+
currentLabel: strings.currentLabel,
|
|
181
|
+
movementWindow: strings.movementWindow,
|
|
182
|
+
notEnoughHistory: strings.notEnoughHistory,
|
|
183
|
+
singlePricePoint: strings.singlePricePoint,
|
|
184
|
+
selectionSuspended: strings.selectionSuspended,
|
|
185
|
+
excludedSuspended: strings.excludedSuspended,
|
|
186
|
+
chartTimeLabel: strings.chartTimeLabel,
|
|
187
|
+
chartAria: strings.chartAria,
|
|
150
188
|
addToSlip: strings.addToSlip,
|
|
151
189
|
inSlip: strings.inSlip,
|
|
152
190
|
slipAdded: strings.slipAdded,
|
|
@@ -185,6 +223,54 @@ var import_widget_sdk2 = require("@sensiblestats/widget-sdk");
|
|
|
185
223
|
// src/reducer.ts
|
|
186
224
|
var import_widget_sdk = require("@sensiblestats/widget-sdk");
|
|
187
225
|
|
|
226
|
+
// src/chart.ts
|
|
227
|
+
var CHART_VIEW_W = 320;
|
|
228
|
+
var CHART_VIEW_H = 140;
|
|
229
|
+
var PLOT_X0 = 40;
|
|
230
|
+
var PLOT_X1 = 312;
|
|
231
|
+
var PLOT_Y0 = 10;
|
|
232
|
+
var PLOT_Y1 = 110;
|
|
233
|
+
var MARKER_MAX_POINTS = 8;
|
|
234
|
+
var FLAT_DOMAIN_PAD = 0.05;
|
|
235
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
236
|
+
function projectSeries(samples) {
|
|
237
|
+
if (samples.length < 2) return null;
|
|
238
|
+
const tMin = samples[0].atMs;
|
|
239
|
+
const tMax = samples[samples.length - 1].atMs;
|
|
240
|
+
if (tMax <= tMin) return null;
|
|
241
|
+
const prices = samples.map((s) => s.price);
|
|
242
|
+
const pMin = Math.min(...prices);
|
|
243
|
+
const pMax = Math.max(...prices);
|
|
244
|
+
let yMin;
|
|
245
|
+
let yMax;
|
|
246
|
+
if (pMax === pMin) {
|
|
247
|
+
yMin = pMin - FLAT_DOMAIN_PAD;
|
|
248
|
+
yMax = pMax + FLAT_DOMAIN_PAD;
|
|
249
|
+
} else {
|
|
250
|
+
const pad = (pMax - pMin) * 0.1;
|
|
251
|
+
yMin = pMin - pad;
|
|
252
|
+
yMax = pMax + pad;
|
|
253
|
+
}
|
|
254
|
+
const x = (atMs) => round2(PLOT_X0 + (atMs - tMin) / (tMax - tMin) * (PLOT_X1 - PLOT_X0));
|
|
255
|
+
const y = (price) => round2(PLOT_Y1 - (price - yMin) / (yMax - yMin) * (PLOT_Y1 - PLOT_Y0));
|
|
256
|
+
const projected = samples.map((s) => ({ x: x(s.atMs), y: y(s.price) }));
|
|
257
|
+
const path = projected.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
|
|
258
|
+
return {
|
|
259
|
+
path,
|
|
260
|
+
markers: samples.length <= MARKER_MAX_POINTS ? projected : [],
|
|
261
|
+
// The real extremes, never the padded bounds: a gridline label is a price, and the padded
|
|
262
|
+
// bounds are prices nobody was ever offered. A flat series has one real extreme, not two —
|
|
263
|
+
// two identical gridlines would collide on both React key and rendered text.
|
|
264
|
+
gridlines: pMax === pMin ? [{ y: y(pMax), price: pMax }] : [
|
|
265
|
+
{ y: y(pMax), price: pMax },
|
|
266
|
+
{ y: y(pMin), price: pMin }
|
|
267
|
+
],
|
|
268
|
+
viewBox: `0 0 ${CHART_VIEW_W} ${CHART_VIEW_H}`,
|
|
269
|
+
startMs: tMin,
|
|
270
|
+
endMs: tMax
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
188
274
|
// src/model.ts
|
|
189
275
|
function str(v) {
|
|
190
276
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -327,6 +413,41 @@ function toMarketOddsVM(card) {
|
|
|
327
413
|
addToSlipEnabled: card.addToSlipEnabled === true
|
|
328
414
|
};
|
|
329
415
|
}
|
|
416
|
+
function toOddsMovementVM(card) {
|
|
417
|
+
const raw = Array.isArray(card.points) ? card.points : [];
|
|
418
|
+
const samples = [];
|
|
419
|
+
for (const p of raw) {
|
|
420
|
+
const price = typeof p.price === "number" && Number.isFinite(p.price) ? p.price : void 0;
|
|
421
|
+
const atMs = typeof p.atUtc === "string" ? Date.parse(p.atUtc) : NaN;
|
|
422
|
+
if (price === void 0 || Number.isNaN(atMs)) continue;
|
|
423
|
+
samples.push({ atMs, price });
|
|
424
|
+
}
|
|
425
|
+
samples.sort((a, b) => a.atMs - b.atMs);
|
|
426
|
+
const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
427
|
+
const opening = asNumber(card.openingPrice);
|
|
428
|
+
const current = asNumber(card.currentPrice);
|
|
429
|
+
const home = str(card.homeTeamName);
|
|
430
|
+
const away = str(card.awayTeamName);
|
|
431
|
+
const selection = [str(card.selectionLabel), str(card.line)].filter(Boolean).join(" ");
|
|
432
|
+
const bookmakerId = typeof card.bookmakerId === "number" ? card.bookmakerId : "";
|
|
433
|
+
return {
|
|
434
|
+
id: `${typeof card.matchId === "number" ? card.matchId : 0}:${str(card.marketDeveloperName) ?? ""}:${selection}:${bookmakerId}`,
|
|
435
|
+
market: str(card.marketLabel) ?? str(card.marketDeveloperName) ?? "",
|
|
436
|
+
selection,
|
|
437
|
+
fixture: home && away ? `${home} vs ${away}` : void 0,
|
|
438
|
+
competition: str(card.competitionName),
|
|
439
|
+
bookmaker: str(card.bookmakerName),
|
|
440
|
+
opening: opening !== void 0 ? opening.toFixed(2) : "",
|
|
441
|
+
current: current !== void 0 ? current.toFixed(2) : "",
|
|
442
|
+
direction: card.movementDirection === "shortened" || card.movementDirection === "drifted" ? card.movementDirection : void 0,
|
|
443
|
+
windowHours: asNumber(card.windowHours) ?? 0,
|
|
444
|
+
excludedCount: asNumber(card.excludedSuspendedCount) ?? 0,
|
|
445
|
+
isSuspended: card.isSuspended === true,
|
|
446
|
+
disclaimer: str(card.disclaimer),
|
|
447
|
+
geometry: projectSeries(samples),
|
|
448
|
+
observationCount: samples.length
|
|
449
|
+
};
|
|
450
|
+
}
|
|
330
451
|
function marketOddsRowToSlipSelection(vm, row, conversationId) {
|
|
331
452
|
if (row.matchOddsId === void 0 || row.oddsDecimal === void 0) return null;
|
|
332
453
|
const selection = { matchOddsId: row.matchOddsId, matchId: vm.matchId, oddsDecimal: row.oddsDecimal };
|
|
@@ -367,6 +488,8 @@ function applyEvent(state, event) {
|
|
|
367
488
|
return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
|
|
368
489
|
case "market_odds":
|
|
369
490
|
return { ...state, messages: mapActive(state, (t) => ({ ...t, marketOdds: [...t.marketOdds, toMarketOddsVM(event.data.card)] })) };
|
|
491
|
+
case "odds_movement":
|
|
492
|
+
return { ...state, messages: mapActive(state, (t) => ({ ...t, oddsMovement: [...t.oddsMovement, toOddsMovementVM(event.data.card)] })) };
|
|
370
493
|
case "intent_chip": {
|
|
371
494
|
const chip = toChipVM(event.data.intent);
|
|
372
495
|
return chip ? { ...state, starters: [...state.starters, chip] } : state;
|
|
@@ -402,11 +525,11 @@ function reducer(state, action) {
|
|
|
402
525
|
return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
|
|
403
526
|
case "USER_SENT": {
|
|
404
527
|
const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
|
|
405
|
-
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false };
|
|
528
|
+
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false };
|
|
406
529
|
return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
|
|
407
530
|
}
|
|
408
531
|
case "GREETING_SENT": {
|
|
409
|
-
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false, isGreeting: true };
|
|
532
|
+
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false, isGreeting: true };
|
|
410
533
|
return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
|
|
411
534
|
}
|
|
412
535
|
case "DISMISS_POPUP":
|
|
@@ -776,8 +899,8 @@ function OddsMovementBadge({
|
|
|
776
899
|
ui
|
|
777
900
|
}) {
|
|
778
901
|
if (previous === void 0 || direction === void 0) return null;
|
|
779
|
-
const
|
|
780
|
-
const label = ui?.oddsMoved?.(previous, current,
|
|
902
|
+
const directionWord2 = direction === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
|
|
903
|
+
const label = ui?.oddsMoved?.(previous, current, directionWord2) ?? `was ${previous}, now ${current}, ${directionWord2}`;
|
|
781
904
|
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "ss-w-move", role: "img", "aria-label": label, children: [
|
|
782
905
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-move-prev", "aria-hidden": "true", children: previous }),
|
|
783
906
|
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "ss-w-move-arrow", "aria-hidden": "true", children: direction === "shortened" ? "\u25BC" : "\u25B2" })
|
|
@@ -898,39 +1021,107 @@ function MarketOddsCardList({ cards, ui, slip }) {
|
|
|
898
1021
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(MarketOddsCard, { vm, ui, slip }, vm.id)) });
|
|
899
1022
|
}
|
|
900
1023
|
|
|
901
|
-
// src/components/
|
|
1024
|
+
// src/components/OddsMovementCard.tsx
|
|
902
1025
|
var import_jsx_runtime12 = require("react/jsx-runtime");
|
|
1026
|
+
function directionWord(vm, ui) {
|
|
1027
|
+
if (vm.direction === "shortened") return ui?.oddsShortened ?? "shortened";
|
|
1028
|
+
if (vm.direction === "drifted") return ui?.oddsDrifted ?? "drifted";
|
|
1029
|
+
return "";
|
|
1030
|
+
}
|
|
1031
|
+
function OddsMovementCard({ vm, ui }) {
|
|
1032
|
+
const word = directionWord(vm, ui);
|
|
1033
|
+
const drift = word ? `, ${word}` : "";
|
|
1034
|
+
const aria = ui?.chartAria ? ui.chartAria(vm.opening, vm.current, word, vm.windowHours) : `Price movement over the last ${vm.windowHours} hours: opened at ${vm.opening}, currently ${vm.current}${drift}.`;
|
|
1035
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-omcard", children: [
|
|
1036
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-head", children: [
|
|
1037
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-market", children: vm.market }),
|
|
1038
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-tag", children: ui?.priceMovement ?? "Price movement" })
|
|
1039
|
+
] }),
|
|
1040
|
+
vm.fixture ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-fixture", children: [
|
|
1041
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: vm.fixture }),
|
|
1042
|
+
vm.competition ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "ss-w-om-comp", children: vm.competition }) : null
|
|
1043
|
+
] }) : null,
|
|
1044
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-sel", children: vm.selection }),
|
|
1045
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OddsMovementBadge, { previous: vm.opening, current: vm.current, direction: vm.direction, ui }),
|
|
1046
|
+
vm.geometry === null ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-empty", children: vm.observationCount === 1 ? ui?.singlePricePoint ?? "Only one price recorded so far for this selection." : ui?.notEnoughHistory ?? "No price history recorded yet for this selection." }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
|
|
1047
|
+
"svg",
|
|
1048
|
+
{
|
|
1049
|
+
className: "ss-w-om-chart",
|
|
1050
|
+
viewBox: vm.geometry.viewBox,
|
|
1051
|
+
width: "100%",
|
|
1052
|
+
role: "img",
|
|
1053
|
+
"aria-label": aria,
|
|
1054
|
+
children: [
|
|
1055
|
+
vm.geometry.gridlines.map((g) => /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("g", { children: [
|
|
1056
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("line", { className: "ss-w-om-grid", x1: 40, y1: g.y, x2: CHART_VIEW_W - 8, y2: g.y }),
|
|
1057
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-ylab", x: 34, y: g.y + 3, textAnchor: "end", children: g.price.toFixed(2) })
|
|
1058
|
+
] }, g.price)),
|
|
1059
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { className: "ss-w-om-line", d: vm.geometry.path, fill: "none" }),
|
|
1060
|
+
vm.geometry.markers.map((m, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("circle", { className: "ss-w-om-dot", cx: m.x, cy: m.y, r: 2.5 }, i)),
|
|
1061
|
+
ui?.chartTimeLabel ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
|
|
1062
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, textAnchor: "start", children: ui.chartTimeLabel(vm.geometry.startMs) }),
|
|
1063
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: CHART_VIEW_W - 8, y: CHART_VIEW_H - 8, textAnchor: "end", children: ui.chartTimeLabel(vm.geometry.endMs) })
|
|
1064
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, children: ui?.movementWindow ? ui.movementWindow(vm.windowHours) : `Last ${vm.windowHours}h` })
|
|
1065
|
+
]
|
|
1066
|
+
}
|
|
1067
|
+
),
|
|
1068
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "ss-w-om-foot", children: [
|
|
1069
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
1070
|
+
ui?.openingLabel ?? "Opening",
|
|
1071
|
+
" ",
|
|
1072
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("b", { children: vm.opening })
|
|
1073
|
+
] }),
|
|
1074
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("span", { children: [
|
|
1075
|
+
ui?.currentLabel ?? "Current",
|
|
1076
|
+
" ",
|
|
1077
|
+
/* @__PURE__ */ (0, import_jsx_runtime12.jsx)("b", { children: vm.current })
|
|
1078
|
+
] })
|
|
1079
|
+
] }),
|
|
1080
|
+
vm.bookmaker ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-foot", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { children: vm.bookmaker }) }) : null,
|
|
1081
|
+
vm.excludedCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-excl", children: ui?.excludedSuspended ? ui.excludedSuspended(vm.excludedCount) : `${vm.excludedCount} suspended price(s) excluded from this chart.` }) : null,
|
|
1082
|
+
vm.isSuspended ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-susp", children: ui?.selectionSuspended ?? "This selection is not currently available to bet." }) : null,
|
|
1083
|
+
vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-om-disc", children: vm.disclaimer }) : null
|
|
1084
|
+
] });
|
|
1085
|
+
}
|
|
1086
|
+
function OddsMovementCardList({ cards, ui }) {
|
|
1087
|
+
if (!cards || cards.length === 0) return null;
|
|
1088
|
+
return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "ss-w-omcards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(OddsMovementCard, { vm, ui }, vm.id)) });
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/components/ActionButtons.tsx
|
|
1092
|
+
var import_jsx_runtime13 = require("react/jsx-runtime");
|
|
903
1093
|
function ActionButtons({ actions, loading, disabled, onAct }) {
|
|
904
1094
|
if (actions.length === 0 && !loading) return null;
|
|
905
|
-
return /* @__PURE__ */ (0,
|
|
906
|
-
actions.map((a, i) => /* @__PURE__ */ (0,
|
|
907
|
-
actions.length === 0 && loading ? /* @__PURE__ */ (0,
|
|
908
|
-
/* @__PURE__ */ (0,
|
|
909
|
-
/* @__PURE__ */ (0,
|
|
1095
|
+
return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "ss-w-actions", children: [
|
|
1096
|
+
actions.map((a, i) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
|
|
1097
|
+
actions.length === 0 && loading ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
|
|
1098
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "ss-w-abtn-shimmer" }),
|
|
1099
|
+
/* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "ss-w-abtn-shimmer" })
|
|
910
1100
|
] }) : null
|
|
911
1101
|
] });
|
|
912
1102
|
}
|
|
913
1103
|
|
|
914
1104
|
// src/components/ChatMessage.tsx
|
|
915
|
-
var
|
|
1105
|
+
var import_jsx_runtime14 = require("react/jsx-runtime");
|
|
916
1106
|
function ChatMessage({ message, disabled, onAct, ui, slip }) {
|
|
917
1107
|
if (message.role === "user") {
|
|
918
|
-
return /* @__PURE__ */ (0,
|
|
1108
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-bubble", children: message.text }) });
|
|
919
1109
|
}
|
|
920
|
-
return /* @__PURE__ */ (0,
|
|
921
|
-
message.text ? /* @__PURE__ */ (0,
|
|
922
|
-
/* @__PURE__ */ (0,
|
|
923
|
-
/* @__PURE__ */ (0,
|
|
924
|
-
/* @__PURE__ */ (0,
|
|
925
|
-
/* @__PURE__ */ (0,
|
|
1110
|
+
return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "ss-w-msg ss-w-bot", children: [
|
|
1111
|
+
message.text ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(Markdown, { text: message.text }) : null,
|
|
1112
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
|
|
1113
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(BettingInsightList, { insights: message.insights, ui, slip }),
|
|
1114
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
|
|
1115
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(OddsMovementCardList, { cards: message.oddsMovement, ui }),
|
|
1116
|
+
/* @__PURE__ */ (0, import_jsx_runtime14.jsx)(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
|
|
926
1117
|
] });
|
|
927
1118
|
}
|
|
928
1119
|
|
|
929
1120
|
// src/components/IntentChips.tsx
|
|
930
|
-
var
|
|
1121
|
+
var import_jsx_runtime15 = require("react/jsx-runtime");
|
|
931
1122
|
function IntentChips({ chips, onPick }) {
|
|
932
1123
|
if (chips.length === 0) return null;
|
|
933
|
-
return /* @__PURE__ */ (0,
|
|
1124
|
+
return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
|
|
934
1125
|
}
|
|
935
1126
|
|
|
936
1127
|
// src/scroll.ts
|
|
@@ -940,7 +1131,7 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
|
|
|
940
1131
|
}
|
|
941
1132
|
|
|
942
1133
|
// src/components/MessageList.tsx
|
|
943
|
-
var
|
|
1134
|
+
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
944
1135
|
function MessageList({ controller, ui, slip }) {
|
|
945
1136
|
const { state, send } = controller;
|
|
946
1137
|
const listRef = (0, import_react7.useRef)(null);
|
|
@@ -950,7 +1141,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
950
1141
|
if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
|
|
951
1142
|
}, [state.messages, state.status]);
|
|
952
1143
|
const empty = state.messages.length === 0;
|
|
953
|
-
return /* @__PURE__ */ (0,
|
|
1144
|
+
return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
954
1145
|
"div",
|
|
955
1146
|
{
|
|
956
1147
|
className: "ss-w-list",
|
|
@@ -960,7 +1151,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
960
1151
|
if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
|
|
961
1152
|
},
|
|
962
1153
|
children: [
|
|
963
|
-
state.messages.map((m) => /* @__PURE__ */ (0,
|
|
1154
|
+
state.messages.map((m) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
964
1155
|
ChatMessage,
|
|
965
1156
|
{
|
|
966
1157
|
message: m,
|
|
@@ -971,15 +1162,15 @@ function MessageList({ controller, ui, slip }) {
|
|
|
971
1162
|
},
|
|
972
1163
|
m.id
|
|
973
1164
|
)),
|
|
974
|
-
state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0,
|
|
1165
|
+
state.isStreaming && state.actionsLoading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ActionButtons, { actions: [], loading: true, onAct: () => {
|
|
975
1166
|
} }) : null,
|
|
976
|
-
state.status ? /* @__PURE__ */ (0,
|
|
977
|
-
state.error ? /* @__PURE__ */ (0,
|
|
978
|
-
/* @__PURE__ */ (0,
|
|
979
|
-
/* @__PURE__ */ (0,
|
|
1167
|
+
state.status ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "ss-w-status", children: state.status }) : null,
|
|
1168
|
+
state.error ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "ss-w-error", role: "alert", children: [
|
|
1169
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
|
|
1170
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
|
|
980
1171
|
] }) : null,
|
|
981
|
-
empty ? /* @__PURE__ */ (0,
|
|
982
|
-
/* @__PURE__ */ (0,
|
|
1172
|
+
empty ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
|
|
1173
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { ref: endRef })
|
|
983
1174
|
]
|
|
984
1175
|
}
|
|
985
1176
|
);
|
|
@@ -987,7 +1178,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
987
1178
|
|
|
988
1179
|
// src/components/ChatInput.tsx
|
|
989
1180
|
var import_react8 = require("react");
|
|
990
|
-
var
|
|
1181
|
+
var import_jsx_runtime17 = require("react/jsx-runtime");
|
|
991
1182
|
function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
|
|
992
1183
|
const [value, setValue] = (0, import_react8.useState)("");
|
|
993
1184
|
const submit = () => {
|
|
@@ -1002,14 +1193,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
|
|
|
1002
1193
|
submit();
|
|
1003
1194
|
}
|
|
1004
1195
|
};
|
|
1005
|
-
return /* @__PURE__ */ (0,
|
|
1006
|
-
/* @__PURE__ */ (0,
|
|
1007
|
-
streaming ? /* @__PURE__ */ (0,
|
|
1196
|
+
return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "ss-w-input", children: [
|
|
1197
|
+
/* @__PURE__ */ (0, import_jsx_runtime17.jsx)("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
|
|
1198
|
+
streaming ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SendIcon, {}) })
|
|
1008
1199
|
] });
|
|
1009
1200
|
}
|
|
1010
1201
|
|
|
1011
1202
|
// src/components/ChatPanel.tsx
|
|
1012
|
-
var
|
|
1203
|
+
var import_jsx_runtime18 = require("react/jsx-runtime");
|
|
1013
1204
|
var FULLSCREEN_QUERY = "(max-width: 639px)";
|
|
1014
1205
|
function useIsModal() {
|
|
1015
1206
|
const [modal, setModal] = (0, import_react9.useState)(false);
|
|
@@ -1033,10 +1224,10 @@ function ChatPanel({ controller, ui, slip }) {
|
|
|
1033
1224
|
window.addEventListener("keydown", onKey);
|
|
1034
1225
|
return () => window.removeEventListener("keydown", onKey);
|
|
1035
1226
|
}, [close]);
|
|
1036
|
-
return /* @__PURE__ */ (0,
|
|
1037
|
-
/* @__PURE__ */ (0,
|
|
1038
|
-
/* @__PURE__ */ (0,
|
|
1039
|
-
/* @__PURE__ */ (0,
|
|
1227
|
+
return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
|
|
1228
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
|
|
1229
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(MessageList, { controller, ui, slip }),
|
|
1230
|
+
/* @__PURE__ */ (0, import_jsx_runtime18.jsx)(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
|
|
1040
1231
|
] });
|
|
1041
1232
|
}
|
|
1042
1233
|
|
|
@@ -1235,6 +1426,30 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
1235
1426
|
.ss-w-mo-row .ss-w-slip { margin: 0; }
|
|
1236
1427
|
.ss-w-mo-foot { display: flex; align-items: center; gap: 8px; font-size: 10.5px; color: var(--ss-w-faint); }
|
|
1237
1428
|
|
|
1429
|
+
/* odds movement card */
|
|
1430
|
+
.ss-w-omcards { display: flex; flex-direction: column; gap: 7px; }
|
|
1431
|
+
.ss-w-omcard { 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; }
|
|
1432
|
+
.ss-w-om-head { display: flex; align-items: center; gap: 8px; }
|
|
1433
|
+
.ss-w-om-market { font-size: 13px; font-weight: 700; color: var(--ss-w-ink); }
|
|
1434
|
+
.ss-w-om-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; }
|
|
1435
|
+
.ss-w-om-fixture { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ss-w-faint); min-width: 0; }
|
|
1436
|
+
.ss-w-om-comp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1437
|
+
.ss-w-om-sel { font-size: 12.5px; font-weight: 600; color: var(--ss-w-ink); }
|
|
1438
|
+
.ss-w-om-chart { display: block; width: 100%; height: auto; }
|
|
1439
|
+
/* Single neutral accent in BOTH directions, deliberately. Colouring a shortening price green
|
|
1440
|
+
frames it as a discount, which inverts what it means for the bettor -- the same reason
|
|
1441
|
+
.ss-w-move keeps both directions on --ss-w-faint. */
|
|
1442
|
+
.ss-w-om-line { stroke: var(--ss-w-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
1443
|
+
.ss-w-om-dot { fill: var(--ss-w-accent); }
|
|
1444
|
+
.ss-w-om-grid { stroke: var(--ss-w-line); stroke-width: 1; }
|
|
1445
|
+
.ss-w-om-ylab, .ss-w-om-xlab { fill: var(--ss-w-faint); font-size: 9px; font-family: ui-monospace, monospace; }
|
|
1446
|
+
.ss-w-om-empty { font-size: 11.5px; color: var(--ss-w-faint); padding: 6px 0; }
|
|
1447
|
+
.ss-w-om-foot { display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--ss-w-faint); }
|
|
1448
|
+
.ss-w-om-foot b { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 13px; color: var(--ss-w-ink); }
|
|
1449
|
+
.ss-w-om-excl { font-size: 10.5px; color: var(--ss-w-faint); }
|
|
1450
|
+
.ss-w-om-susp { font-size: 11px; font-weight: 600; color: #c0392b; }
|
|
1451
|
+
.ss-w-om-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
|
|
1452
|
+
|
|
1238
1453
|
/* action buttons */
|
|
1239
1454
|
.ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
|
1240
1455
|
.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; }
|
|
@@ -1272,7 +1487,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
1272
1487
|
`;
|
|
1273
1488
|
|
|
1274
1489
|
// src/StatsWidget.tsx
|
|
1275
|
-
var
|
|
1490
|
+
var import_jsx_runtime19 = require("react/jsx-runtime");
|
|
1276
1491
|
function buildClient(props) {
|
|
1277
1492
|
try {
|
|
1278
1493
|
if ("client" in props && props.client) {
|
|
@@ -1321,7 +1536,7 @@ function StatsWidget(props) {
|
|
|
1321
1536
|
}, [built, ui.injectStyles]);
|
|
1322
1537
|
if (!built) return null;
|
|
1323
1538
|
const attrs = themeAttrs(ui);
|
|
1324
|
-
return /* @__PURE__ */ (0,
|
|
1539
|
+
return /* @__PURE__ */ (0, import_jsx_runtime19.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_runtime19.jsx)(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
|
|
1325
1540
|
ChatFab,
|
|
1326
1541
|
{
|
|
1327
1542
|
label: ui.launcherLabel,
|