@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/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var STRINGS = {
|
|
|
20
20
|
launcherLabel: "Chat with SensibleStats",
|
|
21
21
|
placeholder: "Ask about a team, player or match\u2026",
|
|
22
22
|
closeChat: "Close chat",
|
|
23
|
+
newConversation: "New conversation",
|
|
23
24
|
chatDialogLabel: "SensibleStats chat",
|
|
24
25
|
stopResponse: "Stop response",
|
|
25
26
|
sendMessage: "Send message",
|
|
@@ -92,6 +93,7 @@ var STRINGS = {
|
|
|
92
93
|
launcherLabel: "SensibleStats ile sohbet et",
|
|
93
94
|
placeholder: "Bir tak\u0131m, oyuncu veya ma\xE7 hakk\u0131nda sor\u2026",
|
|
94
95
|
closeChat: "Sohbeti kapat",
|
|
96
|
+
newConversation: "Yeni sohbet",
|
|
95
97
|
chatDialogLabel: "SensibleStats sohbet",
|
|
96
98
|
stopResponse: "Yan\u0131t\u0131 durdur",
|
|
97
99
|
sendMessage: "Mesaj g\xF6nder",
|
|
@@ -187,6 +189,7 @@ function normalizeUi(cfg) {
|
|
|
187
189
|
starters: cfg.starters ?? [],
|
|
188
190
|
injectStyles: cfg.injectStyles ?? true,
|
|
189
191
|
closeChat: strings.closeChat,
|
|
192
|
+
newConversation: strings.newConversation,
|
|
190
193
|
chatDialogLabel: strings.chatDialogLabel,
|
|
191
194
|
stopResponse: strings.stopResponse,
|
|
192
195
|
sendMessage: strings.sendMessage,
|
|
@@ -605,7 +608,7 @@ function marketOddsRowToSlipSelection(vm, row, conversationId) {
|
|
|
605
608
|
// src/reducer.ts
|
|
606
609
|
var POPUP_MAX = 160;
|
|
607
610
|
function initialState(starters) {
|
|
608
|
-
return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
|
|
611
|
+
return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, configuredStarters: starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
|
|
609
612
|
}
|
|
610
613
|
function activeTurn(state) {
|
|
611
614
|
const last = state.messages[state.messages.length - 1];
|
|
@@ -685,6 +688,19 @@ function reducer(state, action) {
|
|
|
685
688
|
}
|
|
686
689
|
case "DISMISS_POPUP":
|
|
687
690
|
return { ...state, popupText: null };
|
|
691
|
+
case "RESET":
|
|
692
|
+
return { ...initialState(state.configuredStarters), isOpen: state.isOpen };
|
|
693
|
+
case "RESTORE":
|
|
694
|
+
return {
|
|
695
|
+
...state,
|
|
696
|
+
messages: action.messages,
|
|
697
|
+
conversationId: action.conversationId,
|
|
698
|
+
isStreaming: false,
|
|
699
|
+
status: null,
|
|
700
|
+
actionsLoading: false,
|
|
701
|
+
starters: action.messages.length ? [] : state.configuredStarters,
|
|
702
|
+
error: null
|
|
703
|
+
};
|
|
688
704
|
case "EVENT":
|
|
689
705
|
return applyEvent(state, action.event);
|
|
690
706
|
case "STREAM_ERROR":
|
|
@@ -804,17 +820,82 @@ function useLiveDashboardPoll({ messages, dispatch, getLiveDashboard }) {
|
|
|
804
820
|
}, [anchorKey, messages, forceUpdate]);
|
|
805
821
|
}
|
|
806
822
|
|
|
823
|
+
// src/conversationStore.ts
|
|
824
|
+
var TTL_MS = 36e5;
|
|
825
|
+
var MAX_MESSAGES = 30;
|
|
826
|
+
function readConversation(storageKey, now) {
|
|
827
|
+
let raw;
|
|
828
|
+
try {
|
|
829
|
+
raw = window.localStorage.getItem(storageKey);
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
if (!raw) return null;
|
|
834
|
+
let parsed;
|
|
835
|
+
try {
|
|
836
|
+
parsed = JSON.parse(raw);
|
|
837
|
+
} catch {
|
|
838
|
+
return "corrupt";
|
|
839
|
+
}
|
|
840
|
+
if (parsed?.v !== 1 || !Array.isArray(parsed.messages) || typeof parsed.updatedAt !== "number") return "corrupt";
|
|
841
|
+
if (now - parsed.updatedAt > TTL_MS) return "expired";
|
|
842
|
+
return { conversationId: parsed.conversationId ?? null, messages: parsed.messages };
|
|
843
|
+
}
|
|
844
|
+
function writeConversation(storageKey, conversationId, messages, now) {
|
|
845
|
+
let capped = messages.slice(-MAX_MESSAGES);
|
|
846
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
847
|
+
try {
|
|
848
|
+
window.localStorage.setItem(storageKey, JSON.stringify({ v: 1, updatedAt: now, conversationId, messages: capped }));
|
|
849
|
+
return;
|
|
850
|
+
} catch {
|
|
851
|
+
if (capped.length <= 1) return;
|
|
852
|
+
capped = capped.slice(Math.ceil(capped.length / 2));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function clearConversation(storageKey) {
|
|
857
|
+
try {
|
|
858
|
+
window.localStorage.removeItem(storageKey);
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
807
863
|
// src/useChatStream.ts
|
|
864
|
+
function hasAnswerContent(t) {
|
|
865
|
+
return !!t.text || t.cards.length > 0 || t.insights.length > 0 || t.marketOdds.length > 0 || t.oddsMovement.length > 0 || !!t.greetingDashboard || !!t.clubFanDashboard;
|
|
866
|
+
}
|
|
808
867
|
var STREAM_FALLBACK_TIMEOUT_MS = 3e4;
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
868
|
+
function seedIdCounter(messages) {
|
|
869
|
+
let max = -1;
|
|
870
|
+
for (const m of messages) {
|
|
871
|
+
const n = Number(m.id.slice(1));
|
|
872
|
+
if (m.id[0] === "m" && Number.isFinite(n) && n > max) max = n;
|
|
873
|
+
}
|
|
874
|
+
return max + 1;
|
|
875
|
+
}
|
|
876
|
+
function useChatStream(conversation, starterTexts, greetingMessage, storageKey) {
|
|
877
|
+
const boot = useMemo(() => {
|
|
878
|
+
const payload = storageKey ? readConversation(storageKey, Date.now()) : null;
|
|
879
|
+
const restored = payload != null && payload !== "expired" && payload !== "corrupt";
|
|
880
|
+
const base = initialState(starterTexts.map(chipFromText));
|
|
881
|
+
if (restored) {
|
|
882
|
+
return { state: { ...base, messages: payload.messages, conversationId: payload.conversationId, starters: [] }, restored: true, stale: false };
|
|
883
|
+
}
|
|
884
|
+
return { state: base, restored: false, stale: payload === "expired" || payload === "corrupt" };
|
|
885
|
+
}, []);
|
|
886
|
+
const [state, dispatch] = useReducer2(reducer, boot.state);
|
|
887
|
+
const idRef = useRef2(seedIdCounter(boot.state.messages));
|
|
813
888
|
const handleRef = useRef2(null);
|
|
814
889
|
const fallbackTimerRef = useRef2(null);
|
|
815
890
|
const stateRef = useRef2(state);
|
|
816
891
|
stateRef.current = state;
|
|
817
|
-
const greetedRef = useRef2(
|
|
892
|
+
const greetedRef = useRef2(boot.restored);
|
|
893
|
+
const hydratedRef = useRef2(false);
|
|
894
|
+
const restoreSeededRef = useRef2(false);
|
|
895
|
+
if (!restoreSeededRef.current) {
|
|
896
|
+
restoreSeededRef.current = true;
|
|
897
|
+
if (boot.restored && boot.state.conversationId) conversation.conversationId = boot.state.conversationId;
|
|
898
|
+
}
|
|
818
899
|
const nextId = () => `m${idRef.current++}`;
|
|
819
900
|
const clearFallbackTimer = useCallback(() => {
|
|
820
901
|
if (fallbackTimerRef.current != null) {
|
|
@@ -870,10 +951,9 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
870
951
|
const last = stateRef.current.lastUserInput;
|
|
871
952
|
if (last) run(last, stateRef.current.lastDisplayText ?? void 0);
|
|
872
953
|
}, [run]);
|
|
873
|
-
|
|
954
|
+
const fireGreeting = useCallback(() => {
|
|
874
955
|
const message = greetingMessage?.trim();
|
|
875
|
-
if (!message
|
|
876
|
-
greetedRef.current = true;
|
|
956
|
+
if (!message) return;
|
|
877
957
|
try {
|
|
878
958
|
handleRef.current?.cancel();
|
|
879
959
|
clearFallbackTimer();
|
|
@@ -886,6 +966,55 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
886
966
|
dispatch({ kind: "STOP" });
|
|
887
967
|
}
|
|
888
968
|
}, [greetingMessage, conversation, consume, clearFallbackTimer, armFallbackTimer]);
|
|
969
|
+
useEffect2(() => {
|
|
970
|
+
if (greetedRef.current) return;
|
|
971
|
+
greetedRef.current = true;
|
|
972
|
+
fireGreeting();
|
|
973
|
+
}, [fireGreeting]);
|
|
974
|
+
useEffect2(() => {
|
|
975
|
+
if (boot.stale && storageKey) clearConversation(storageKey);
|
|
976
|
+
}, []);
|
|
977
|
+
useEffect2(() => {
|
|
978
|
+
if (!storageKey || state.isStreaming) return;
|
|
979
|
+
if (hydratedRef.current) {
|
|
980
|
+
hydratedRef.current = false;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const msgs = state.messages;
|
|
984
|
+
const persistable = msgs.filter((m, i) => {
|
|
985
|
+
if (m.role === "user") {
|
|
986
|
+
const next = msgs[i + 1];
|
|
987
|
+
return !!next && next.role === "assistant" && next.done && !next.isGreeting && hasAnswerContent(next);
|
|
988
|
+
}
|
|
989
|
+
return m.done && !m.isGreeting && hasAnswerContent(m);
|
|
990
|
+
});
|
|
991
|
+
if (persistable.length === 0) return;
|
|
992
|
+
writeConversation(storageKey, state.conversationId, persistable, Date.now());
|
|
993
|
+
}, [storageKey, state.isStreaming, state.messages, state.conversationId]);
|
|
994
|
+
useEffect2(() => {
|
|
995
|
+
if (!storageKey) return;
|
|
996
|
+
const onStorage = (e) => {
|
|
997
|
+
if (e.key !== storageKey || e.storageArea !== window.localStorage) return;
|
|
998
|
+
if (stateRef.current.isStreaming) return;
|
|
999
|
+
if (e.newValue == null) {
|
|
1000
|
+
idRef.current = 0;
|
|
1001
|
+
conversation.conversationId = void 0;
|
|
1002
|
+
hydratedRef.current = true;
|
|
1003
|
+
dispatch({ kind: "RESET" });
|
|
1004
|
+
greetedRef.current = true;
|
|
1005
|
+
fireGreeting();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const parsed = readConversation(storageKey, Date.now());
|
|
1009
|
+
if (!parsed || parsed === "expired" || parsed === "corrupt") return;
|
|
1010
|
+
idRef.current = seedIdCounter(parsed.messages);
|
|
1011
|
+
conversation.conversationId = parsed.conversationId ?? void 0;
|
|
1012
|
+
hydratedRef.current = true;
|
|
1013
|
+
dispatch({ kind: "RESTORE", messages: parsed.messages, conversationId: parsed.conversationId });
|
|
1014
|
+
};
|
|
1015
|
+
window.addEventListener("storage", onStorage);
|
|
1016
|
+
return () => window.removeEventListener("storage", onStorage);
|
|
1017
|
+
}, [storageKey, conversation, fireGreeting]);
|
|
889
1018
|
const open = useCallback(() => dispatch({ kind: "OPEN" }), []);
|
|
890
1019
|
const close = useCallback(() => {
|
|
891
1020
|
handleRef.current?.cancel();
|
|
@@ -904,9 +1033,20 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
|
|
|
904
1033
|
else open();
|
|
905
1034
|
}, [close, open]);
|
|
906
1035
|
const dismissPopup = useCallback(() => dispatch({ kind: "DISMISS_POPUP" }), []);
|
|
1036
|
+
const reset = useCallback(() => {
|
|
1037
|
+
handleRef.current?.cancel();
|
|
1038
|
+
handleRef.current = null;
|
|
1039
|
+
clearFallbackTimer();
|
|
1040
|
+
if (storageKey) clearConversation(storageKey);
|
|
1041
|
+
idRef.current = 0;
|
|
1042
|
+
conversation.conversationId = void 0;
|
|
1043
|
+
dispatch({ kind: "RESET" });
|
|
1044
|
+
greetedRef.current = true;
|
|
1045
|
+
fireGreeting();
|
|
1046
|
+
}, [storageKey, conversation, clearFallbackTimer, fireGreeting]);
|
|
907
1047
|
const getLiveDashboard = useCallback(() => conversation.getLiveDashboard(), [conversation]);
|
|
908
1048
|
useLiveDashboardPoll({ messages: state.messages, dispatch, getLiveDashboard });
|
|
909
|
-
return { state, open, close, stop, toggle, send, retry, dismissPopup };
|
|
1049
|
+
return { state, open, close, stop, toggle, send, retry, dismissPopup, reset };
|
|
910
1050
|
}
|
|
911
1051
|
|
|
912
1052
|
// src/useLiveMonitor.ts
|
|
@@ -1251,6 +1391,9 @@ function CloseIcon() {
|
|
|
1251
1391
|
function ChevronIcon() {
|
|
1252
1392
|
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", d: "M9 5l7 7-7 7" }) });
|
|
1253
1393
|
}
|
|
1394
|
+
function NewChatIcon() {
|
|
1395
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "1em", height: "1em", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { fill: "none", d: "M12 5v14M5 12h14", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) });
|
|
1396
|
+
}
|
|
1254
1397
|
var DASHBOARD_EMOJI = {
|
|
1255
1398
|
football: "\u26BD",
|
|
1256
1399
|
"goal-net": "\u{1F945}",
|
|
@@ -1319,10 +1462,11 @@ import { useEffect as useEffect9, useState as useState17 } from "react";
|
|
|
1319
1462
|
|
|
1320
1463
|
// src/components/ChatHeader.tsx
|
|
1321
1464
|
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1322
|
-
function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle }) {
|
|
1465
|
+
function ChatHeader({ onClose, closeLabel = "Close chat", logoUrl, headerTitle, onNewConversation, canReset, newConversationLabel = "New conversation" }) {
|
|
1323
1466
|
return /* @__PURE__ */ jsxs3("header", { className: "ss-w-header", children: [
|
|
1324
1467
|
/* @__PURE__ */ jsx5(BrandMark, { logoUrl, className: "ss-w-header-mark" }),
|
|
1325
1468
|
/* @__PURE__ */ jsx5("span", { className: "ss-w-header-title", children: headerTitle ?? "SensibleStats" }),
|
|
1469
|
+
canReset && onNewConversation ? /* @__PURE__ */ jsx5("button", { type: "button", className: "ss-w-newconv", "aria-label": newConversationLabel, onClick: onNewConversation, children: /* @__PURE__ */ jsx5(NewChatIcon, {}) }) : null,
|
|
1326
1470
|
/* @__PURE__ */ jsx5("button", { type: "button", className: "ss-w-close", "aria-label": closeLabel, onClick: onClose, children: /* @__PURE__ */ jsx5(CloseIcon, {}) })
|
|
1327
1471
|
] });
|
|
1328
1472
|
}
|
|
@@ -3194,7 +3338,7 @@ function useIsModal() {
|
|
|
3194
3338
|
return modal;
|
|
3195
3339
|
}
|
|
3196
3340
|
function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
|
|
3197
|
-
const { state, close, stop, send } = controller;
|
|
3341
|
+
const { state, close, stop, send, reset } = controller;
|
|
3198
3342
|
const isModal = useIsModal();
|
|
3199
3343
|
useEffect9(() => {
|
|
3200
3344
|
const onKey = (e) => {
|
|
@@ -3211,7 +3355,18 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
|
|
|
3211
3355
|
if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
|
|
3212
3356
|
}, [monitor, state.isStreaming, state.messages.length]);
|
|
3213
3357
|
return /* @__PURE__ */ jsxs43("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
|
|
3214
|
-
/* @__PURE__ */ jsx49(
|
|
3358
|
+
/* @__PURE__ */ jsx49(
|
|
3359
|
+
ChatHeader,
|
|
3360
|
+
{
|
|
3361
|
+
onClose: close,
|
|
3362
|
+
closeLabel: ui.closeChat,
|
|
3363
|
+
logoUrl: ui.logoUrl,
|
|
3364
|
+
headerTitle: ui.headerTitle,
|
|
3365
|
+
onNewConversation: reset,
|
|
3366
|
+
canReset: state.messages.length > 0,
|
|
3367
|
+
newConversationLabel: ui.newConversation
|
|
3368
|
+
}
|
|
3369
|
+
),
|
|
3215
3370
|
monitor && /* @__PURE__ */ jsx49(
|
|
3216
3371
|
TabBar,
|
|
3217
3372
|
{
|
|
@@ -3326,8 +3481,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
|
|
|
3326
3481
|
}
|
|
3327
3482
|
.ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
|
|
3328
3483
|
.ss-w-header-mark { width: 18px; height: 18px; display: block; object-fit: contain; }
|
|
3329
|
-
.ss-w-header-title { font-weight: 660; }
|
|
3330
|
-
.ss-w-
|
|
3484
|
+
.ss-w-header-title { font-weight: 660; flex: 1; }
|
|
3485
|
+
.ss-w-newconv { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
|
|
3486
|
+
.ss-w-close { background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
|
|
3331
3487
|
|
|
3332
3488
|
/* messages */
|
|
3333
3489
|
/* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
|
|
@@ -4074,7 +4230,8 @@ function StatsWidget(props) {
|
|
|
4074
4230
|
const markIntroDone = useCallback5(() => {
|
|
4075
4231
|
introDoneRef.current = true;
|
|
4076
4232
|
}, []);
|
|
4077
|
-
const
|
|
4233
|
+
const convStorageKey = `ss-w-conv:${cfg?.operatorId ?? ""}:${cfg?.userContext?.operatorUserId ?? "anon"}`;
|
|
4234
|
+
const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0, convStorageKey);
|
|
4078
4235
|
const monitor = useLiveMonitor({
|
|
4079
4236
|
getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
|
|
4080
4237
|
getMarketOdds: (matchId, dev) => built ? built.conversation.getMarketOdds(matchId, dev) : Promise.resolve(null),
|