@sensiblestats/widget-react 0.37.3 → 0.38.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 +6 -0
- package/dist/index.cjs +173 -16
- 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 +173 -16
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sensiblestats/widget-react
|
|
2
2
|
|
|
3
|
+
## 0.38.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Persist the chat conversation in the browser for a sliding hour: on return within the hour the widget silently restores the prior thread (and continues the same server thread), otherwise it starts fresh. Adds a "New conversation" control to clear and start over. Client-only (localStorage); no SDK or server change. Conversations now sync live across open browser tabs (a write or "New conversation" in one tab mirrors into the others).
|
|
8
|
+
|
|
3
9
|
## 0.37.3
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,7 @@ var STRINGS = {
|
|
|
57
57
|
launcherLabel: "Chat with SensibleStats",
|
|
58
58
|
placeholder: "Ask about a team, player or match\u2026",
|
|
59
59
|
closeChat: "Close chat",
|
|
60
|
+
newConversation: "New conversation",
|
|
60
61
|
chatDialogLabel: "SensibleStats chat",
|
|
61
62
|
stopResponse: "Stop response",
|
|
62
63
|
sendMessage: "Send message",
|
|
@@ -129,6 +130,7 @@ var STRINGS = {
|
|
|
129
130
|
launcherLabel: "SensibleStats ile sohbet et",
|
|
130
131
|
placeholder: "Bir tak\u0131m, oyuncu veya ma\xE7 hakk\u0131nda sor\u2026",
|
|
131
132
|
closeChat: "Sohbeti kapat",
|
|
133
|
+
newConversation: "Yeni sohbet",
|
|
132
134
|
chatDialogLabel: "SensibleStats sohbet",
|
|
133
135
|
stopResponse: "Yan\u0131t\u0131 durdur",
|
|
134
136
|
sendMessage: "Mesaj g\xF6nder",
|
|
@@ -224,6 +226,7 @@ function normalizeUi(cfg) {
|
|
|
224
226
|
starters: cfg.starters ?? [],
|
|
225
227
|
injectStyles: cfg.injectStyles ?? true,
|
|
226
228
|
closeChat: strings.closeChat,
|
|
229
|
+
newConversation: strings.newConversation,
|
|
227
230
|
chatDialogLabel: strings.chatDialogLabel,
|
|
228
231
|
stopResponse: strings.stopResponse,
|
|
229
232
|
sendMessage: strings.sendMessage,
|
|
@@ -642,7 +645,7 @@ function marketOddsRowToSlipSelection(vm, row, conversationId) {
|
|
|
642
645
|
// src/reducer.ts
|
|
643
646
|
var POPUP_MAX = 160;
|
|
644
647
|
function initialState(starters) {
|
|
645
|
-
return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
|
|
648
|
+
return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, configuredStarters: starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
|
|
646
649
|
}
|
|
647
650
|
function activeTurn(state) {
|
|
648
651
|
const last = state.messages[state.messages.length - 1];
|
|
@@ -722,6 +725,19 @@ function reducer(state, action) {
|
|
|
722
725
|
}
|
|
723
726
|
case "DISMISS_POPUP":
|
|
724
727
|
return { ...state, popupText: null };
|
|
728
|
+
case "RESET":
|
|
729
|
+
return { ...initialState(state.configuredStarters), isOpen: state.isOpen };
|
|
730
|
+
case "RESTORE":
|
|
731
|
+
return {
|
|
732
|
+
...state,
|
|
733
|
+
messages: action.messages,
|
|
734
|
+
conversationId: action.conversationId,
|
|
735
|
+
isStreaming: false,
|
|
736
|
+
status: null,
|
|
737
|
+
actionsLoading: false,
|
|
738
|
+
starters: action.messages.length ? [] : state.configuredStarters,
|
|
739
|
+
error: null
|
|
740
|
+
};
|
|
725
741
|
case "EVENT":
|
|
726
742
|
return applyEvent(state, action.event);
|
|
727
743
|
case "STREAM_ERROR":
|
|
@@ -841,17 +857,82 @@ function useLiveDashboardPoll({ messages, dispatch, getLiveDashboard }) {
|
|
|
841
857
|
}, [anchorKey, messages, forceUpdate]);
|
|
842
858
|
}
|
|
843
859
|
|
|
860
|
+
// src/conversationStore.ts
|
|
861
|
+
var TTL_MS = 36e5;
|
|
862
|
+
var MAX_MESSAGES = 30;
|
|
863
|
+
function readConversation(storageKey, now) {
|
|
864
|
+
let raw;
|
|
865
|
+
try {
|
|
866
|
+
raw = window.localStorage.getItem(storageKey);
|
|
867
|
+
} catch {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
if (!raw) return null;
|
|
871
|
+
let parsed;
|
|
872
|
+
try {
|
|
873
|
+
parsed = JSON.parse(raw);
|
|
874
|
+
} catch {
|
|
875
|
+
return "corrupt";
|
|
876
|
+
}
|
|
877
|
+
if (parsed?.v !== 1 || !Array.isArray(parsed.messages) || typeof parsed.updatedAt !== "number") return "corrupt";
|
|
878
|
+
if (now - parsed.updatedAt > TTL_MS) return "expired";
|
|
879
|
+
return { conversationId: parsed.conversationId ?? null, messages: parsed.messages };
|
|
880
|
+
}
|
|
881
|
+
function writeConversation(storageKey, conversationId, messages, now) {
|
|
882
|
+
let capped = messages.slice(-MAX_MESSAGES);
|
|
883
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
884
|
+
try {
|
|
885
|
+
window.localStorage.setItem(storageKey, JSON.stringify({ v: 1, updatedAt: now, conversationId, messages: capped }));
|
|
886
|
+
return;
|
|
887
|
+
} catch {
|
|
888
|
+
if (capped.length <= 1) return;
|
|
889
|
+
capped = capped.slice(Math.ceil(capped.length / 2));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function clearConversation(storageKey) {
|
|
894
|
+
try {
|
|
895
|
+
window.localStorage.removeItem(storageKey);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
844
900
|
// src/useChatStream.ts
|
|
901
|
+
function hasAnswerContent(t) {
|
|
902
|
+
return !!t.text || t.cards.length > 0 || t.insights.length > 0 || t.marketOdds.length > 0 || t.oddsMovement.length > 0 || !!t.greetingDashboard || !!t.clubFanDashboard;
|
|
903
|
+
}
|
|
845
904
|
var STREAM_FALLBACK_TIMEOUT_MS = 3e4;
|
|
846
|
-
function
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
905
|
+
function seedIdCounter(messages) {
|
|
906
|
+
let max = -1;
|
|
907
|
+
for (const m of messages) {
|
|
908
|
+
const n = Number(m.id.slice(1));
|
|
909
|
+
if (m.id[0] === "m" && Number.isFinite(n) && n > max) max = n;
|
|
910
|
+
}
|
|
911
|
+
return max + 1;
|
|
912
|
+
}
|
|
913
|
+
function useChatStream(conversation, starterTexts, greetingMessage, storageKey) {
|
|
914
|
+
const boot = (0, import_react2.useMemo)(() => {
|
|
915
|
+
const payload = storageKey ? readConversation(storageKey, Date.now()) : null;
|
|
916
|
+
const restored = payload != null && payload !== "expired" && payload !== "corrupt";
|
|
917
|
+
const base = initialState(starterTexts.map(chipFromText));
|
|
918
|
+
if (restored) {
|
|
919
|
+
return { state: { ...base, messages: payload.messages, conversationId: payload.conversationId, starters: [] }, restored: true, stale: false };
|
|
920
|
+
}
|
|
921
|
+
return { state: base, restored: false, stale: payload === "expired" || payload === "corrupt" };
|
|
922
|
+
}, []);
|
|
923
|
+
const [state, dispatch] = (0, import_react2.useReducer)(reducer, boot.state);
|
|
924
|
+
const idRef = (0, import_react2.useRef)(seedIdCounter(boot.state.messages));
|
|
850
925
|
const handleRef = (0, import_react2.useRef)(null);
|
|
851
926
|
const fallbackTimerRef = (0, import_react2.useRef)(null);
|
|
852
927
|
const stateRef = (0, import_react2.useRef)(state);
|
|
853
928
|
stateRef.current = state;
|
|
854
|
-
const greetedRef = (0, import_react2.useRef)(
|
|
929
|
+
const greetedRef = (0, import_react2.useRef)(boot.restored);
|
|
930
|
+
const hydratedRef = (0, import_react2.useRef)(false);
|
|
931
|
+
const restoreSeededRef = (0, import_react2.useRef)(false);
|
|
932
|
+
if (!restoreSeededRef.current) {
|
|
933
|
+
restoreSeededRef.current = true;
|
|
934
|
+
if (boot.restored && boot.state.conversationId) conversation.conversationId = boot.state.conversationId;
|
|
935
|
+
}
|
|
855
936
|
const nextId = () => `m${idRef.current++}`;
|
|
856
937
|
const clearFallbackTimer = (0, import_react2.useCallback)(() => {
|
|
857
938
|
if (fallbackTimerRef.current != null) {
|
|
@@ -907,10 +988,9 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
907
988
|
const last = stateRef.current.lastUserInput;
|
|
908
989
|
if (last) run(last, stateRef.current.lastDisplayText ?? void 0);
|
|
909
990
|
}, [run]);
|
|
910
|
-
(0, import_react2.
|
|
991
|
+
const fireGreeting = (0, import_react2.useCallback)(() => {
|
|
911
992
|
const message = greetingMessage?.trim();
|
|
912
|
-
if (!message
|
|
913
|
-
greetedRef.current = true;
|
|
993
|
+
if (!message) return;
|
|
914
994
|
try {
|
|
915
995
|
handleRef.current?.cancel();
|
|
916
996
|
clearFallbackTimer();
|
|
@@ -923,6 +1003,55 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
923
1003
|
dispatch({ kind: "STOP" });
|
|
924
1004
|
}
|
|
925
1005
|
}, [greetingMessage, conversation, consume, clearFallbackTimer, armFallbackTimer]);
|
|
1006
|
+
(0, import_react2.useEffect)(() => {
|
|
1007
|
+
if (greetedRef.current) return;
|
|
1008
|
+
greetedRef.current = true;
|
|
1009
|
+
fireGreeting();
|
|
1010
|
+
}, [fireGreeting]);
|
|
1011
|
+
(0, import_react2.useEffect)(() => {
|
|
1012
|
+
if (boot.stale && storageKey) clearConversation(storageKey);
|
|
1013
|
+
}, []);
|
|
1014
|
+
(0, import_react2.useEffect)(() => {
|
|
1015
|
+
if (!storageKey || state.isStreaming) return;
|
|
1016
|
+
if (hydratedRef.current) {
|
|
1017
|
+
hydratedRef.current = false;
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const msgs = state.messages;
|
|
1021
|
+
const persistable = msgs.filter((m, i) => {
|
|
1022
|
+
if (m.role === "user") {
|
|
1023
|
+
const next = msgs[i + 1];
|
|
1024
|
+
return !!next && next.role === "assistant" && next.done && !next.isGreeting && hasAnswerContent(next);
|
|
1025
|
+
}
|
|
1026
|
+
return m.done && !m.isGreeting && hasAnswerContent(m);
|
|
1027
|
+
});
|
|
1028
|
+
if (persistable.length === 0) return;
|
|
1029
|
+
writeConversation(storageKey, state.conversationId, persistable, Date.now());
|
|
1030
|
+
}, [storageKey, state.isStreaming, state.messages, state.conversationId]);
|
|
1031
|
+
(0, import_react2.useEffect)(() => {
|
|
1032
|
+
if (!storageKey) return;
|
|
1033
|
+
const onStorage = (e) => {
|
|
1034
|
+
if (e.key !== storageKey || e.storageArea !== window.localStorage) return;
|
|
1035
|
+
if (stateRef.current.isStreaming) return;
|
|
1036
|
+
if (e.newValue == null) {
|
|
1037
|
+
idRef.current = 0;
|
|
1038
|
+
conversation.conversationId = void 0;
|
|
1039
|
+
hydratedRef.current = true;
|
|
1040
|
+
dispatch({ kind: "RESET" });
|
|
1041
|
+
greetedRef.current = true;
|
|
1042
|
+
fireGreeting();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const parsed = readConversation(storageKey, Date.now());
|
|
1046
|
+
if (!parsed || parsed === "expired" || parsed === "corrupt") return;
|
|
1047
|
+
idRef.current = seedIdCounter(parsed.messages);
|
|
1048
|
+
conversation.conversationId = parsed.conversationId ?? void 0;
|
|
1049
|
+
hydratedRef.current = true;
|
|
1050
|
+
dispatch({ kind: "RESTORE", messages: parsed.messages, conversationId: parsed.conversationId });
|
|
1051
|
+
};
|
|
1052
|
+
window.addEventListener("storage", onStorage);
|
|
1053
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
1054
|
+
}, [storageKey, conversation, fireGreeting]);
|
|
926
1055
|
const open = (0, import_react2.useCallback)(() => dispatch({ kind: "OPEN" }), []);
|
|
927
1056
|
const close = (0, import_react2.useCallback)(() => {
|
|
928
1057
|
handleRef.current?.cancel();
|
|
@@ -941,9 +1070,20 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
941
1070
|
else open();
|
|
942
1071
|
}, [close, open]);
|
|
943
1072
|
const dismissPopup = (0, import_react2.useCallback)(() => dispatch({ kind: "DISMISS_POPUP" }), []);
|
|
1073
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
1074
|
+
handleRef.current?.cancel();
|
|
1075
|
+
handleRef.current = null;
|
|
1076
|
+
clearFallbackTimer();
|
|
1077
|
+
if (storageKey) clearConversation(storageKey);
|
|
1078
|
+
idRef.current = 0;
|
|
1079
|
+
conversation.conversationId = void 0;
|
|
1080
|
+
dispatch({ kind: "RESET" });
|
|
1081
|
+
greetedRef.current = true;
|
|
1082
|
+
fireGreeting();
|
|
1083
|
+
}, [storageKey, conversation, clearFallbackTimer, fireGreeting]);
|
|
944
1084
|
const getLiveDashboard = (0, import_react2.useCallback)(() => conversation.getLiveDashboard(), [conversation]);
|
|
945
1085
|
useLiveDashboardPoll({ messages: state.messages, dispatch, getLiveDashboard });
|
|
946
|
-
return { state, open, close, stop, toggle, send, retry, dismissPopup };
|
|
1086
|
+
return { state, open, close, stop, toggle, send, retry, dismissPopup, reset };
|
|
947
1087
|
}
|
|
948
1088
|
|
|
949
1089
|
// src/useLiveMonitor.ts
|
|
@@ -1288,6 +1428,9 @@ function CloseIcon() {
|
|
|
1288
1428
|
function ChevronIcon() {
|
|
1289
1429
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
|
|
1290
1430
|
}
|
|
1431
|
+
function NewChatIcon() {
|
|
1432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { fill: "none", d: "M12 5v14M5 12h14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) });
|
|
1433
|
+
}
|
|
1291
1434
|
var DASHBOARD_EMOJI = {
|
|
1292
1435
|
football: "\u26BD",
|
|
1293
1436
|
"goal-net": "\u{1F945}",
|
|
@@ -1356,10 +1499,11 @@ var import_react22 = require("react");
|
|
|
1356
1499
|
|
|
1357
1500
|
// src/components/ChatHeader.tsx
|
|
1358
1501
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
1359
|
-
function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }) {
|
|
1502
|
+
function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle, onNewConversation, canReset, newConversationLabel = "New conversation" }) {
|
|
1360
1503
|
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("header", { className: "ss-w-header", children: [
|
|
1361
1504
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BrandMark, { logoUrl, className: "ss-w-header-mark" }),
|
|
1362
1505
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "ss-w-header-title", children: headerTitle ?? "SensibleStats" }),
|
|
1506
|
+
canReset && onNewConversation ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "ss-w-newconv", "aria-label": newConversationLabel, onClick: onNewConversation, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NewChatIcon, {}) }) : null,
|
|
1363
1507
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "ss-w-close", "aria-label": closeLabel, onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CloseIcon, {}) })
|
|
1364
1508
|
] });
|
|
1365
1509
|
}
|
|
@@ -3231,7 +3375,7 @@ function useIsModal() {
|
|
|
3231
3375
|
return modal;
|
|
3232
3376
|
}
|
|
3233
3377
|
function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
|
|
3234
|
-
const { state, close, stop, send } = controller;
|
|
3378
|
+
const { state, close, stop, send, reset } = controller;
|
|
3235
3379
|
const isModal = useIsModal();
|
|
3236
3380
|
(0, import_react22.useEffect)(() => {
|
|
3237
3381
|
const onKey = (e) => {
|
|
@@ -3248,7 +3392,18 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
|
|
|
3248
3392
|
if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
|
|
3249
3393
|
}, [monitor, state.isStreaming, state.messages.length]);
|
|
3250
3394
|
return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
|
|
3251
|
-
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
|
|
3395
|
+
/* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
|
|
3396
|
+
ChatHeader,
|
|
3397
|
+
{
|
|
3398
|
+
onClose: close,
|
|
3399
|
+
closeLabel: ui.closeChat,
|
|
3400
|
+
logoUrl: ui.logoUrl,
|
|
3401
|
+
headerTitle: ui.headerTitle,
|
|
3402
|
+
onNewConversation: reset,
|
|
3403
|
+
canReset: state.messages.length > 0,
|
|
3404
|
+
newConversationLabel: ui.newConversation
|
|
3405
|
+
}
|
|
3406
|
+
),
|
|
3252
3407
|
monitor && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
|
|
3253
3408
|
TabBar,
|
|
3254
3409
|
{
|
|
@@ -3363,8 +3518,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
3363
3518
|
}
|
|
3364
3519
|
.ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
|
|
3365
3520
|
.ss-w-header-mark { width: 18px; height: 18px; display: block; object-fit: contain; }
|
|
3366
|
-
.ss-w-header-title { font-weight: 660; }
|
|
3367
|
-
.ss-w-
|
|
3521
|
+
.ss-w-header-title { font-weight: 660; flex: 1; }
|
|
3522
|
+
.ss-w-newconv { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
|
|
3523
|
+
.ss-w-close { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
|
|
3368
3524
|
|
|
3369
3525
|
/* messages */
|
|
3370
3526
|
/* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
|
|
@@ -4111,7 +4267,8 @@ function StatsWidget(props) {
|
|
|
4111
4267
|
const markIntroDone = (0, import_react23.useCallback)(() => {
|
|
4112
4268
|
introDoneRef.current = true;
|
|
4113
4269
|
}, []);
|
|
4114
|
-
const
|
|
4270
|
+
const convStorageKey = `ss-w-conv:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`;
|
|
4271
|
+
const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0, convStorageKey);
|
|
4115
4272
|
const monitor = useLiveMonitor({
|
|
4116
4273
|
getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
|
|
4117
4274
|
getMarketOdds: (matchId, dev) => built ? built.conversation.getMarketOdds(matchId, dev) : Promise.resolve(null),
|