@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/dist/index.js
CHANGED
|
@@ -28,6 +28,20 @@ var STRINGS = {
|
|
|
28
28
|
oddsShortened: "shortened",
|
|
29
29
|
oddsDrifted: "drifted",
|
|
30
30
|
updatedLabel: "updated",
|
|
31
|
+
priceMovement: "Price movement",
|
|
32
|
+
openingLabel: "Opening",
|
|
33
|
+
currentLabel: "Current",
|
|
34
|
+
movementWindow: (hours) => hours % 24 === 0 ? `Last ${hours / 24}d` : `Last ${hours}h`,
|
|
35
|
+
notEnoughHistory: "No price history recorded yet for this selection.",
|
|
36
|
+
singlePricePoint: "Only one price recorded so far for this selection.",
|
|
37
|
+
selectionSuspended: "This selection is not currently available to bet.",
|
|
38
|
+
excludedSuspended: (n) => `${n} suspended price${n === 1 ? "" : "s"} excluded from this chart.`,
|
|
39
|
+
chartTimeLabel: (ms) => new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
|
|
40
|
+
chartAria: (opening, current, direction, hours) => {
|
|
41
|
+
const span = hours % 24 === 0 ? `${hours / 24} day${hours / 24 === 1 ? "" : "s"}` : `${hours} hour${hours === 1 ? "" : "s"}`;
|
|
42
|
+
const drift = direction ? `, ${direction}` : "";
|
|
43
|
+
return `Price movement over the last ${span}: opened at ${opening}, currently ${current}${drift}.`;
|
|
44
|
+
},
|
|
31
45
|
inSlip: "In slip",
|
|
32
46
|
slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
|
|
33
47
|
slipDuplicate: "Already in your slip",
|
|
@@ -61,6 +75,20 @@ var STRINGS = {
|
|
|
61
75
|
oddsShortened: "d\xFC\u015Ft\xFC",
|
|
62
76
|
oddsDrifted: "y\xFCkseldi",
|
|
63
77
|
updatedLabel: "g\xFCncellendi",
|
|
78
|
+
priceMovement: "Oran hareketi",
|
|
79
|
+
openingLabel: "A\xE7\u0131l\u0131\u015F",
|
|
80
|
+
currentLabel: "G\xFCncel",
|
|
81
|
+
movementWindow: (hours) => hours % 24 === 0 ? `Son ${hours / 24} g\xFCn` : `Son ${hours} saat`,
|
|
82
|
+
notEnoughHistory: "Bu se\xE7im i\xE7in hen\xFCz oran ge\xE7mi\u015Fi kaydedilmedi.",
|
|
83
|
+
singlePricePoint: "Bu se\xE7im i\xE7in \u015Fimdiye kadar yaln\u0131zca bir fiyat kaydedildi.",
|
|
84
|
+
selectionSuspended: "Bu se\xE7im \u015Fu anda bahse kapal\u0131.",
|
|
85
|
+
excludedSuspended: (n) => `${n} ask\u0131ya al\u0131nm\u0131\u015F fiyat bu grafikten hari\xE7 tutuldu.`,
|
|
86
|
+
chartTimeLabel: (ms) => new Intl.DateTimeFormat("tr-TR", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)),
|
|
87
|
+
chartAria: (opening, current, direction, hours) => {
|
|
88
|
+
const span = hours % 24 === 0 ? `${hours / 24} g\xFCn` : `${hours} saat`;
|
|
89
|
+
const drift = direction ? `, ${direction}` : "";
|
|
90
|
+
return `Son ${span} i\xE7indeki oran hareketi: ${opening} ile a\xE7\u0131ld\u0131, \u015Fu anda ${current}${drift}.`;
|
|
91
|
+
},
|
|
64
92
|
inSlip: "Kuponda",
|
|
65
93
|
slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
|
|
66
94
|
slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
|
|
@@ -110,6 +138,16 @@ function normalizeUi(cfg) {
|
|
|
110
138
|
oddsShortened: strings.oddsShortened,
|
|
111
139
|
oddsDrifted: strings.oddsDrifted,
|
|
112
140
|
updatedLabel: strings.updatedLabel,
|
|
141
|
+
priceMovement: strings.priceMovement,
|
|
142
|
+
openingLabel: strings.openingLabel,
|
|
143
|
+
currentLabel: strings.currentLabel,
|
|
144
|
+
movementWindow: strings.movementWindow,
|
|
145
|
+
notEnoughHistory: strings.notEnoughHistory,
|
|
146
|
+
singlePricePoint: strings.singlePricePoint,
|
|
147
|
+
selectionSuspended: strings.selectionSuspended,
|
|
148
|
+
excludedSuspended: strings.excludedSuspended,
|
|
149
|
+
chartTimeLabel: strings.chartTimeLabel,
|
|
150
|
+
chartAria: strings.chartAria,
|
|
113
151
|
addToSlip: strings.addToSlip,
|
|
114
152
|
inSlip: strings.inSlip,
|
|
115
153
|
slipAdded: strings.slipAdded,
|
|
@@ -148,6 +186,54 @@ import { WidgetSdkError as WidgetSdkError2 } from "@sensiblestats/widget-sdk";
|
|
|
148
186
|
// src/reducer.ts
|
|
149
187
|
import "@sensiblestats/widget-sdk";
|
|
150
188
|
|
|
189
|
+
// src/chart.ts
|
|
190
|
+
var CHART_VIEW_W = 320;
|
|
191
|
+
var CHART_VIEW_H = 140;
|
|
192
|
+
var PLOT_X0 = 40;
|
|
193
|
+
var PLOT_X1 = 312;
|
|
194
|
+
var PLOT_Y0 = 10;
|
|
195
|
+
var PLOT_Y1 = 110;
|
|
196
|
+
var MARKER_MAX_POINTS = 8;
|
|
197
|
+
var FLAT_DOMAIN_PAD = 0.05;
|
|
198
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
199
|
+
function projectSeries(samples) {
|
|
200
|
+
if (samples.length < 2) return null;
|
|
201
|
+
const tMin = samples[0].atMs;
|
|
202
|
+
const tMax = samples[samples.length - 1].atMs;
|
|
203
|
+
if (tMax <= tMin) return null;
|
|
204
|
+
const prices = samples.map((s) => s.price);
|
|
205
|
+
const pMin = Math.min(...prices);
|
|
206
|
+
const pMax = Math.max(...prices);
|
|
207
|
+
let yMin;
|
|
208
|
+
let yMax;
|
|
209
|
+
if (pMax === pMin) {
|
|
210
|
+
yMin = pMin - FLAT_DOMAIN_PAD;
|
|
211
|
+
yMax = pMax + FLAT_DOMAIN_PAD;
|
|
212
|
+
} else {
|
|
213
|
+
const pad = (pMax - pMin) * 0.1;
|
|
214
|
+
yMin = pMin - pad;
|
|
215
|
+
yMax = pMax + pad;
|
|
216
|
+
}
|
|
217
|
+
const x = (atMs) => round2(PLOT_X0 + (atMs - tMin) / (tMax - tMin) * (PLOT_X1 - PLOT_X0));
|
|
218
|
+
const y = (price) => round2(PLOT_Y1 - (price - yMin) / (yMax - yMin) * (PLOT_Y1 - PLOT_Y0));
|
|
219
|
+
const projected = samples.map((s) => ({ x: x(s.atMs), y: y(s.price) }));
|
|
220
|
+
const path = projected.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
|
|
221
|
+
return {
|
|
222
|
+
path,
|
|
223
|
+
markers: samples.length <= MARKER_MAX_POINTS ? projected : [],
|
|
224
|
+
// The real extremes, never the padded bounds: a gridline label is a price, and the padded
|
|
225
|
+
// bounds are prices nobody was ever offered. A flat series has one real extreme, not two —
|
|
226
|
+
// two identical gridlines would collide on both React key and rendered text.
|
|
227
|
+
gridlines: pMax === pMin ? [{ y: y(pMax), price: pMax }] : [
|
|
228
|
+
{ y: y(pMax), price: pMax },
|
|
229
|
+
{ y: y(pMin), price: pMin }
|
|
230
|
+
],
|
|
231
|
+
viewBox: `0 0 ${CHART_VIEW_W} ${CHART_VIEW_H}`,
|
|
232
|
+
startMs: tMin,
|
|
233
|
+
endMs: tMax
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
151
237
|
// src/model.ts
|
|
152
238
|
function str(v) {
|
|
153
239
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
@@ -290,6 +376,41 @@ function toMarketOddsVM(card) {
|
|
|
290
376
|
addToSlipEnabled: card.addToSlipEnabled === true
|
|
291
377
|
};
|
|
292
378
|
}
|
|
379
|
+
function toOddsMovementVM(card) {
|
|
380
|
+
const raw = Array.isArray(card.points) ? card.points : [];
|
|
381
|
+
const samples = [];
|
|
382
|
+
for (const p of raw) {
|
|
383
|
+
const price = typeof p.price === "number" && Number.isFinite(p.price) ? p.price : void 0;
|
|
384
|
+
const atMs = typeof p.atUtc === "string" ? Date.parse(p.atUtc) : NaN;
|
|
385
|
+
if (price === void 0 || Number.isNaN(atMs)) continue;
|
|
386
|
+
samples.push({ atMs, price });
|
|
387
|
+
}
|
|
388
|
+
samples.sort((a, b) => a.atMs - b.atMs);
|
|
389
|
+
const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
390
|
+
const opening = asNumber(card.openingPrice);
|
|
391
|
+
const current = asNumber(card.currentPrice);
|
|
392
|
+
const home = str(card.homeTeamName);
|
|
393
|
+
const away = str(card.awayTeamName);
|
|
394
|
+
const selection = [str(card.selectionLabel), str(card.line)].filter(Boolean).join(" ");
|
|
395
|
+
const bookmakerId = typeof card.bookmakerId === "number" ? card.bookmakerId : "";
|
|
396
|
+
return {
|
|
397
|
+
id: `${typeof card.matchId === "number" ? card.matchId : 0}:${str(card.marketDeveloperName) ?? ""}:${selection}:${bookmakerId}`,
|
|
398
|
+
market: str(card.marketLabel) ?? str(card.marketDeveloperName) ?? "",
|
|
399
|
+
selection,
|
|
400
|
+
fixture: home && away ? `${home} vs ${away}` : void 0,
|
|
401
|
+
competition: str(card.competitionName),
|
|
402
|
+
bookmaker: str(card.bookmakerName),
|
|
403
|
+
opening: opening !== void 0 ? opening.toFixed(2) : "",
|
|
404
|
+
current: current !== void 0 ? current.toFixed(2) : "",
|
|
405
|
+
direction: card.movementDirection === "shortened" || card.movementDirection === "drifted" ? card.movementDirection : void 0,
|
|
406
|
+
windowHours: asNumber(card.windowHours) ?? 0,
|
|
407
|
+
excludedCount: asNumber(card.excludedSuspendedCount) ?? 0,
|
|
408
|
+
isSuspended: card.isSuspended === true,
|
|
409
|
+
disclaimer: str(card.disclaimer),
|
|
410
|
+
geometry: projectSeries(samples),
|
|
411
|
+
observationCount: samples.length
|
|
412
|
+
};
|
|
413
|
+
}
|
|
293
414
|
function marketOddsRowToSlipSelection(vm, row, conversationId) {
|
|
294
415
|
if (row.matchOddsId === void 0 || row.oddsDecimal === void 0) return null;
|
|
295
416
|
const selection = { matchOddsId: row.matchOddsId, matchId: vm.matchId, oddsDecimal: row.oddsDecimal };
|
|
@@ -330,6 +451,8 @@ function applyEvent(state, event) {
|
|
|
330
451
|
return { ...state, messages: mapActive(state, (t) => ({ ...t, insights: [...t.insights, toInsightVM(event.data.insight)] })) };
|
|
331
452
|
case "market_odds":
|
|
332
453
|
return { ...state, messages: mapActive(state, (t) => ({ ...t, marketOdds: [...t.marketOdds, toMarketOddsVM(event.data.card)] })) };
|
|
454
|
+
case "odds_movement":
|
|
455
|
+
return { ...state, messages: mapActive(state, (t) => ({ ...t, oddsMovement: [...t.oddsMovement, toOddsMovementVM(event.data.card)] })) };
|
|
333
456
|
case "intent_chip": {
|
|
334
457
|
const chip = toChipVM(event.data.intent);
|
|
335
458
|
return chip ? { ...state, starters: [...state.starters, chip] } : state;
|
|
@@ -365,11 +488,11 @@ function reducer(state, action) {
|
|
|
365
488
|
return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
|
|
366
489
|
case "USER_SENT": {
|
|
367
490
|
const user = { role: "user", id: action.userId, text: action.displayText ?? action.input.message };
|
|
368
|
-
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false };
|
|
491
|
+
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false };
|
|
369
492
|
return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
|
|
370
493
|
}
|
|
371
494
|
case "GREETING_SENT": {
|
|
372
|
-
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], done: false, isGreeting: true };
|
|
495
|
+
const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], insights: [], marketOdds: [], oddsMovement: [], done: false, isGreeting: true };
|
|
373
496
|
return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
|
|
374
497
|
}
|
|
375
498
|
case "DISMISS_POPUP":
|
|
@@ -735,8 +858,8 @@ function OddsMovementBadge({
|
|
|
735
858
|
ui
|
|
736
859
|
}) {
|
|
737
860
|
if (previous === void 0 || direction === void 0) return null;
|
|
738
|
-
const
|
|
739
|
-
const label = ui?.oddsMoved?.(previous, current,
|
|
861
|
+
const directionWord2 = direction === "shortened" ? ui?.oddsShortened ?? "shortened" : ui?.oddsDrifted ?? "drifted";
|
|
862
|
+
const label = ui?.oddsMoved?.(previous, current, directionWord2) ?? `was ${previous}, now ${current}, ${directionWord2}`;
|
|
740
863
|
return (
|
|
741
864
|
// Strikethrough is invisible to screen readers, so the whole was/now/direction sentence rides
|
|
742
865
|
// on one aria-label and the visual glyphs are hidden from the accessibility tree.
|
|
@@ -861,39 +984,107 @@ function MarketOddsCardList({ cards, ui, slip }) {
|
|
|
861
984
|
return /* @__PURE__ */ jsx11("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ jsx11(MarketOddsCard, { vm, ui, slip }, vm.id)) });
|
|
862
985
|
}
|
|
863
986
|
|
|
864
|
-
// src/components/
|
|
987
|
+
// src/components/OddsMovementCard.tsx
|
|
865
988
|
import { Fragment, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
989
|
+
function directionWord(vm, ui) {
|
|
990
|
+
if (vm.direction === "shortened") return ui?.oddsShortened ?? "shortened";
|
|
991
|
+
if (vm.direction === "drifted") return ui?.oddsDrifted ?? "drifted";
|
|
992
|
+
return "";
|
|
993
|
+
}
|
|
994
|
+
function OddsMovementCard({ vm, ui }) {
|
|
995
|
+
const word = directionWord(vm, ui);
|
|
996
|
+
const drift = word ? `, ${word}` : "";
|
|
997
|
+
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}.`;
|
|
998
|
+
return /* @__PURE__ */ jsxs9("div", { className: "ss-w-omcard", children: [
|
|
999
|
+
/* @__PURE__ */ jsxs9("div", { className: "ss-w-om-head", children: [
|
|
1000
|
+
/* @__PURE__ */ jsx12("span", { className: "ss-w-om-market", children: vm.market }),
|
|
1001
|
+
/* @__PURE__ */ jsx12("span", { className: "ss-w-om-tag", children: ui?.priceMovement ?? "Price movement" })
|
|
1002
|
+
] }),
|
|
1003
|
+
vm.fixture ? /* @__PURE__ */ jsxs9("div", { className: "ss-w-om-fixture", children: [
|
|
1004
|
+
/* @__PURE__ */ jsx12("span", { children: vm.fixture }),
|
|
1005
|
+
vm.competition ? /* @__PURE__ */ jsx12("span", { className: "ss-w-om-comp", children: vm.competition }) : null
|
|
1006
|
+
] }) : null,
|
|
1007
|
+
/* @__PURE__ */ jsx12("div", { className: "ss-w-om-sel", children: vm.selection }),
|
|
1008
|
+
/* @__PURE__ */ jsx12(OddsMovementBadge, { previous: vm.opening, current: vm.current, direction: vm.direction, ui }),
|
|
1009
|
+
vm.geometry === null ? /* @__PURE__ */ jsx12("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__ */ jsxs9(
|
|
1010
|
+
"svg",
|
|
1011
|
+
{
|
|
1012
|
+
className: "ss-w-om-chart",
|
|
1013
|
+
viewBox: vm.geometry.viewBox,
|
|
1014
|
+
width: "100%",
|
|
1015
|
+
role: "img",
|
|
1016
|
+
"aria-label": aria,
|
|
1017
|
+
children: [
|
|
1018
|
+
vm.geometry.gridlines.map((g) => /* @__PURE__ */ jsxs9("g", { children: [
|
|
1019
|
+
/* @__PURE__ */ jsx12("line", { className: "ss-w-om-grid", x1: 40, y1: g.y, x2: CHART_VIEW_W - 8, y2: g.y }),
|
|
1020
|
+
/* @__PURE__ */ jsx12("text", { className: "ss-w-om-ylab", x: 34, y: g.y + 3, textAnchor: "end", children: g.price.toFixed(2) })
|
|
1021
|
+
] }, g.price)),
|
|
1022
|
+
/* @__PURE__ */ jsx12("path", { className: "ss-w-om-line", d: vm.geometry.path, fill: "none" }),
|
|
1023
|
+
vm.geometry.markers.map((m, i) => /* @__PURE__ */ jsx12("circle", { className: "ss-w-om-dot", cx: m.x, cy: m.y, r: 2.5 }, i)),
|
|
1024
|
+
ui?.chartTimeLabel ? /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
1025
|
+
/* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, textAnchor: "start", children: ui.chartTimeLabel(vm.geometry.startMs) }),
|
|
1026
|
+
/* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: CHART_VIEW_W - 8, y: CHART_VIEW_H - 8, textAnchor: "end", children: ui.chartTimeLabel(vm.geometry.endMs) })
|
|
1027
|
+
] }) : /* @__PURE__ */ jsx12("text", { className: "ss-w-om-xlab", x: 40, y: CHART_VIEW_H - 8, children: ui?.movementWindow ? ui.movementWindow(vm.windowHours) : `Last ${vm.windowHours}h` })
|
|
1028
|
+
]
|
|
1029
|
+
}
|
|
1030
|
+
),
|
|
1031
|
+
/* @__PURE__ */ jsxs9("div", { className: "ss-w-om-foot", children: [
|
|
1032
|
+
/* @__PURE__ */ jsxs9("span", { children: [
|
|
1033
|
+
ui?.openingLabel ?? "Opening",
|
|
1034
|
+
" ",
|
|
1035
|
+
/* @__PURE__ */ jsx12("b", { children: vm.opening })
|
|
1036
|
+
] }),
|
|
1037
|
+
/* @__PURE__ */ jsxs9("span", { children: [
|
|
1038
|
+
ui?.currentLabel ?? "Current",
|
|
1039
|
+
" ",
|
|
1040
|
+
/* @__PURE__ */ jsx12("b", { children: vm.current })
|
|
1041
|
+
] })
|
|
1042
|
+
] }),
|
|
1043
|
+
vm.bookmaker ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-foot", children: /* @__PURE__ */ jsx12("span", { children: vm.bookmaker }) }) : null,
|
|
1044
|
+
vm.excludedCount > 0 ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-excl", children: ui?.excludedSuspended ? ui.excludedSuspended(vm.excludedCount) : `${vm.excludedCount} suspended price(s) excluded from this chart.` }) : null,
|
|
1045
|
+
vm.isSuspended ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-susp", children: ui?.selectionSuspended ?? "This selection is not currently available to bet." }) : null,
|
|
1046
|
+
vm.disclaimer ? /* @__PURE__ */ jsx12("div", { className: "ss-w-om-disc", children: vm.disclaimer }) : null
|
|
1047
|
+
] });
|
|
1048
|
+
}
|
|
1049
|
+
function OddsMovementCardList({ cards, ui }) {
|
|
1050
|
+
if (!cards || cards.length === 0) return null;
|
|
1051
|
+
return /* @__PURE__ */ jsx12("div", { className: "ss-w-omcards", children: cards.map((vm) => /* @__PURE__ */ jsx12(OddsMovementCard, { vm, ui }, vm.id)) });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/components/ActionButtons.tsx
|
|
1055
|
+
import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
866
1056
|
function ActionButtons({ actions, loading, disabled, onAct }) {
|
|
867
1057
|
if (actions.length === 0 && !loading) return null;
|
|
868
|
-
return /* @__PURE__ */
|
|
869
|
-
actions.map((a, i) => /* @__PURE__ */
|
|
870
|
-
actions.length === 0 && loading ? /* @__PURE__ */
|
|
871
|
-
/* @__PURE__ */
|
|
872
|
-
/* @__PURE__ */
|
|
1058
|
+
return /* @__PURE__ */ jsxs10("div", { className: "ss-w-actions", children: [
|
|
1059
|
+
actions.map((a, i) => /* @__PURE__ */ jsx13("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
|
|
1060
|
+
actions.length === 0 && loading ? /* @__PURE__ */ jsxs10(Fragment2, { children: [
|
|
1061
|
+
/* @__PURE__ */ jsx13("span", { className: "ss-w-abtn-shimmer" }),
|
|
1062
|
+
/* @__PURE__ */ jsx13("span", { className: "ss-w-abtn-shimmer" })
|
|
873
1063
|
] }) : null
|
|
874
1064
|
] });
|
|
875
1065
|
}
|
|
876
1066
|
|
|
877
1067
|
// src/components/ChatMessage.tsx
|
|
878
|
-
import { jsx as
|
|
1068
|
+
import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
879
1069
|
function ChatMessage({ message, disabled, onAct, ui, slip }) {
|
|
880
1070
|
if (message.role === "user") {
|
|
881
|
-
return /* @__PURE__ */
|
|
1071
|
+
return /* @__PURE__ */ jsx14("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx14("div", { className: "ss-w-bubble", children: message.text }) });
|
|
882
1072
|
}
|
|
883
|
-
return /* @__PURE__ */
|
|
884
|
-
message.text ? /* @__PURE__ */
|
|
885
|
-
/* @__PURE__ */
|
|
886
|
-
/* @__PURE__ */
|
|
887
|
-
/* @__PURE__ */
|
|
888
|
-
/* @__PURE__ */
|
|
1073
|
+
return /* @__PURE__ */ jsxs11("div", { className: "ss-w-msg ss-w-bot", children: [
|
|
1074
|
+
message.text ? /* @__PURE__ */ jsx14(Markdown, { text: message.text }) : null,
|
|
1075
|
+
/* @__PURE__ */ jsx14(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
|
|
1076
|
+
/* @__PURE__ */ jsx14(BettingInsightList, { insights: message.insights, ui, slip }),
|
|
1077
|
+
/* @__PURE__ */ jsx14(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
|
|
1078
|
+
/* @__PURE__ */ jsx14(OddsMovementCardList, { cards: message.oddsMovement, ui }),
|
|
1079
|
+
/* @__PURE__ */ jsx14(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
|
|
889
1080
|
] });
|
|
890
1081
|
}
|
|
891
1082
|
|
|
892
1083
|
// src/components/IntentChips.tsx
|
|
893
|
-
import { jsx as
|
|
1084
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
894
1085
|
function IntentChips({ chips, onPick }) {
|
|
895
1086
|
if (chips.length === 0) return null;
|
|
896
|
-
return /* @__PURE__ */
|
|
1087
|
+
return /* @__PURE__ */ jsx15("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx15("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
|
|
897
1088
|
}
|
|
898
1089
|
|
|
899
1090
|
// src/scroll.ts
|
|
@@ -903,7 +1094,7 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
|
|
|
903
1094
|
}
|
|
904
1095
|
|
|
905
1096
|
// src/components/MessageList.tsx
|
|
906
|
-
import { jsx as
|
|
1097
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
907
1098
|
function MessageList({ controller, ui, slip }) {
|
|
908
1099
|
const { state, send } = controller;
|
|
909
1100
|
const listRef = useRef2(null);
|
|
@@ -913,7 +1104,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
913
1104
|
if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
|
|
914
1105
|
}, [state.messages, state.status]);
|
|
915
1106
|
const empty = state.messages.length === 0;
|
|
916
|
-
return /* @__PURE__ */
|
|
1107
|
+
return /* @__PURE__ */ jsxs12(
|
|
917
1108
|
"div",
|
|
918
1109
|
{
|
|
919
1110
|
className: "ss-w-list",
|
|
@@ -923,7 +1114,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
923
1114
|
if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
|
|
924
1115
|
},
|
|
925
1116
|
children: [
|
|
926
|
-
state.messages.map((m) => /* @__PURE__ */
|
|
1117
|
+
state.messages.map((m) => /* @__PURE__ */ jsx16(
|
|
927
1118
|
ChatMessage,
|
|
928
1119
|
{
|
|
929
1120
|
message: m,
|
|
@@ -934,15 +1125,15 @@ function MessageList({ controller, ui, slip }) {
|
|
|
934
1125
|
},
|
|
935
1126
|
m.id
|
|
936
1127
|
)),
|
|
937
|
-
state.isStreaming && state.actionsLoading ? /* @__PURE__ */
|
|
1128
|
+
state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx16(ActionButtons, { actions: [], loading: true, onAct: () => {
|
|
938
1129
|
} }) : null,
|
|
939
|
-
state.status ? /* @__PURE__ */
|
|
940
|
-
state.error ? /* @__PURE__ */
|
|
941
|
-
/* @__PURE__ */
|
|
942
|
-
/* @__PURE__ */
|
|
1130
|
+
state.status ? /* @__PURE__ */ jsx16("div", { className: "ss-w-status", children: state.status }) : null,
|
|
1131
|
+
state.error ? /* @__PURE__ */ jsxs12("div", { className: "ss-w-error", role: "alert", children: [
|
|
1132
|
+
/* @__PURE__ */ jsx16("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
|
|
1133
|
+
/* @__PURE__ */ jsx16("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
|
|
943
1134
|
] }) : null,
|
|
944
|
-
empty ? /* @__PURE__ */
|
|
945
|
-
/* @__PURE__ */
|
|
1135
|
+
empty ? /* @__PURE__ */ jsx16(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
|
|
1136
|
+
/* @__PURE__ */ jsx16("div", { ref: endRef })
|
|
946
1137
|
]
|
|
947
1138
|
}
|
|
948
1139
|
);
|
|
@@ -950,7 +1141,7 @@ function MessageList({ controller, ui, slip }) {
|
|
|
950
1141
|
|
|
951
1142
|
// src/components/ChatInput.tsx
|
|
952
1143
|
import { useState as useState5 } from "react";
|
|
953
|
-
import { jsx as
|
|
1144
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
954
1145
|
function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
|
|
955
1146
|
const [value, setValue] = useState5("");
|
|
956
1147
|
const submit = () => {
|
|
@@ -965,14 +1156,14 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
|
|
|
965
1156
|
submit();
|
|
966
1157
|
}
|
|
967
1158
|
};
|
|
968
|
-
return /* @__PURE__ */
|
|
969
|
-
/* @__PURE__ */
|
|
970
|
-
streaming ? /* @__PURE__ */
|
|
1159
|
+
return /* @__PURE__ */ jsxs13("div", { className: "ss-w-input", children: [
|
|
1160
|
+
/* @__PURE__ */ jsx17("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
|
|
1161
|
+
streaming ? /* @__PURE__ */ jsx17("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx17("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx17("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx17(SendIcon, {}) })
|
|
971
1162
|
] });
|
|
972
1163
|
}
|
|
973
1164
|
|
|
974
1165
|
// src/components/ChatPanel.tsx
|
|
975
|
-
import { jsx as
|
|
1166
|
+
import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
976
1167
|
var FULLSCREEN_QUERY = "(max-width: 639px)";
|
|
977
1168
|
function useIsModal() {
|
|
978
1169
|
const [modal, setModal] = useState6(false);
|
|
@@ -996,10 +1187,10 @@ function ChatPanel({ controller, ui, slip }) {
|
|
|
996
1187
|
window.addEventListener("keydown", onKey);
|
|
997
1188
|
return () => window.removeEventListener("keydown", onKey);
|
|
998
1189
|
}, [close]);
|
|
999
|
-
return /* @__PURE__ */
|
|
1000
|
-
/* @__PURE__ */
|
|
1001
|
-
/* @__PURE__ */
|
|
1002
|
-
/* @__PURE__ */
|
|
1190
|
+
return /* @__PURE__ */ jsxs14("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
|
|
1191
|
+
/* @__PURE__ */ jsx18(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
|
|
1192
|
+
/* @__PURE__ */ jsx18(MessageList, { controller, ui, slip }),
|
|
1193
|
+
/* @__PURE__ */ jsx18(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
|
|
1003
1194
|
] });
|
|
1004
1195
|
}
|
|
1005
1196
|
|
|
@@ -1198,6 +1389,30 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
1198
1389
|
.ss-w-mo-row .ss-w-slip { margin: 0; }
|
|
1199
1390
|
.ss-w-mo-foot { display: flex; align-items: center; gap: 8px; font-size: 10.5px; color: var(--ss-w-faint); }
|
|
1200
1391
|
|
|
1392
|
+
/* odds movement card */
|
|
1393
|
+
.ss-w-omcards { display: flex; flex-direction: column; gap: 7px; }
|
|
1394
|
+
.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; }
|
|
1395
|
+
.ss-w-om-head { display: flex; align-items: center; gap: 8px; }
|
|
1396
|
+
.ss-w-om-market { font-size: 13px; font-weight: 700; color: var(--ss-w-ink); }
|
|
1397
|
+
.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; }
|
|
1398
|
+
.ss-w-om-fixture { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ss-w-faint); min-width: 0; }
|
|
1399
|
+
.ss-w-om-comp { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1400
|
+
.ss-w-om-sel { font-size: 12.5px; font-weight: 600; color: var(--ss-w-ink); }
|
|
1401
|
+
.ss-w-om-chart { display: block; width: 100%; height: auto; }
|
|
1402
|
+
/* Single neutral accent in BOTH directions, deliberately. Colouring a shortening price green
|
|
1403
|
+
frames it as a discount, which inverts what it means for the bettor -- the same reason
|
|
1404
|
+
.ss-w-move keeps both directions on --ss-w-faint. */
|
|
1405
|
+
.ss-w-om-line { stroke: var(--ss-w-accent); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
|
|
1406
|
+
.ss-w-om-dot { fill: var(--ss-w-accent); }
|
|
1407
|
+
.ss-w-om-grid { stroke: var(--ss-w-line); stroke-width: 1; }
|
|
1408
|
+
.ss-w-om-ylab, .ss-w-om-xlab { fill: var(--ss-w-faint); font-size: 9px; font-family: ui-monospace, monospace; }
|
|
1409
|
+
.ss-w-om-empty { font-size: 11.5px; color: var(--ss-w-faint); padding: 6px 0; }
|
|
1410
|
+
.ss-w-om-foot { display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--ss-w-faint); }
|
|
1411
|
+
.ss-w-om-foot b { font-family: ui-monospace, monospace; font-variant-numeric: tabular-nums; font-size: 13px; color: var(--ss-w-ink); }
|
|
1412
|
+
.ss-w-om-excl { font-size: 10.5px; color: var(--ss-w-faint); }
|
|
1413
|
+
.ss-w-om-susp { font-size: 11px; font-weight: 600; color: #c0392b; }
|
|
1414
|
+
.ss-w-om-disc { font-size: 10px; font-style: italic; color: var(--ss-w-faint); }
|
|
1415
|
+
|
|
1201
1416
|
/* action buttons */
|
|
1202
1417
|
.ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
|
|
1203
1418
|
.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; }
|
|
@@ -1235,7 +1450,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
1235
1450
|
`;
|
|
1236
1451
|
|
|
1237
1452
|
// src/StatsWidget.tsx
|
|
1238
|
-
import { jsx as
|
|
1453
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1239
1454
|
function buildClient(props) {
|
|
1240
1455
|
try {
|
|
1241
1456
|
if ("client" in props && props.client) {
|
|
@@ -1284,7 +1499,7 @@ function StatsWidget(props) {
|
|
|
1284
1499
|
}, [built, ui.injectStyles]);
|
|
1285
1500
|
if (!built) return null;
|
|
1286
1501
|
const attrs = themeAttrs(ui);
|
|
1287
|
-
return /* @__PURE__ */
|
|
1502
|
+
return /* @__PURE__ */ jsx19("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__ */ jsx19(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ jsx19(
|
|
1288
1503
|
ChatFab,
|
|
1289
1504
|
{
|
|
1290
1505
|
label: ui.launcherLabel,
|