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