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