@sensiblestats/widget-react 0.37.2 → 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/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 useChatStream(conversation, starterTexts, greetingMessage) {
810
- const seed = useMemo(() => initialState(starterTexts.map(chipFromText)), []);
811
- const [state, dispatch] = useReducer2(reducer, seed);
812
- const idRef = useRef2(0);
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(false);
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
- useEffect2(() => {
954
+ const fireGreeting = useCallback(() => {
874
955
  const message = greetingMessage?.trim();
875
- if (!message || greetedRef.current) return;
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
  }
@@ -1764,7 +1908,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1764
1908
  /* @__PURE__ */ jsx14(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
1765
1909
  ] });
1766
1910
  }
1767
- function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1911
+ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
1768
1912
  const [open, setOpen] = useState9(false);
1769
1913
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1770
1914
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
@@ -1792,7 +1936,7 @@ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1792
1936
  {
1793
1937
  ui,
1794
1938
  monitor,
1795
- pin: { kind: "market", matchId: vm.matchId, marketDeveloperName: vm.marketDeveloperName, line: vm.mainLine }
1939
+ pin: pinRef ?? { kind: "market", matchId: vm.matchId, marketDeveloperName: vm.marketDeveloperName, line: vm.mainLine }
1796
1940
  }
1797
1941
  ) : null
1798
1942
  ] })
@@ -1820,9 +1964,9 @@ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1820
1964
  vm.disclaimer ? /* @__PURE__ */ jsx14("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
1821
1965
  ] });
1822
1966
  }
1823
- function MarketOddsCardList({ cards, ui, slip, monitor, inSlipIds, hideHeader }) {
1967
+ function MarketOddsCardList({ cards, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
1824
1968
  if (!cards || cards.length === 0) return null;
1825
- return /* @__PURE__ */ jsx14("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ jsx14(MarketOddsCard, { vm, ui, slip, monitor, inSlipIds, hideHeader }, vm.id)) });
1969
+ return /* @__PURE__ */ jsx14("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ jsx14(MarketOddsCard, { vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }, vm.id)) });
1826
1970
  }
1827
1971
 
1828
1972
  // src/components/OddsMovementCard.tsx
@@ -3107,36 +3251,24 @@ function MonitorMatchCard({ match, promotedPins, monitor, ui, slip }) {
3107
3251
 
3108
3252
  // src/components/MonitorTabs.tsx
3109
3253
  import { jsx as jsx48, jsxs as jsxs42 } from "react/jsx-runtime";
3110
- function ContextLine({ m, monitor, ui }) {
3111
- const kick = m.kickoffUtc ? ui.bettingKickoff(m.kickoffUtc) : void 0;
3112
- return /* @__PURE__ */ jsxs42("div", { className: "ss-w-mk-ctx", children: [
3113
- /* @__PURE__ */ jsxs42("span", { className: "ss-w-mk-ctx-side", children: [
3114
- /* @__PURE__ */ jsx48(Crest2, { kind: "team", url: m.homeCrestUrl ?? void 0, name: m.homeTeamName ?? void 0 }),
3115
- /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-name", children: m.homeTeamName })
3116
- ] }),
3117
- m.phase === "preMatch" ? kick && /* @__PURE__ */ jsxs42("span", { className: "ss-w-mon-pill", children: [
3118
- kick.dayLabel,
3254
+ function OddsLiveStrip({ m, ui }) {
3255
+ const showScore = m.phase !== "preMatch" && typeof m.homeScore === "number";
3256
+ const stale = m.status === "stale";
3257
+ if (!showScore && !m.minuteLabel && !(m.isSuspended && !m.isSettled) && !m.isSettled && !stale) return null;
3258
+ return /* @__PURE__ */ jsxs42("div", { className: "ss-w-mk-live", children: [
3259
+ showScore && /* @__PURE__ */ jsxs42("b", { className: "ss-w-mk-live-score", children: [
3260
+ m.homeTeamName,
3119
3261
  " ",
3120
- kick.time
3121
- ] }) : typeof m.homeScore === "number" && /* @__PURE__ */ jsxs42("b", { children: [
3122
3262
  m.homeScore,
3123
3263
  "\u2013",
3124
- m.awayScore
3125
- ] }),
3126
- /* @__PURE__ */ jsxs42("span", { className: "ss-w-mk-ctx-side", children: [
3127
- /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-name", children: m.awayTeamName }),
3128
- /* @__PURE__ */ jsx48(Crest2, { kind: "team", url: m.awayCrestUrl ?? void 0, name: m.awayTeamName ?? void 0 })
3264
+ m.awayScore,
3265
+ " ",
3266
+ m.awayTeamName
3129
3267
  ] }),
3130
3268
  m.minuteLabel && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-min", children: m.minuteLabel }),
3131
- /* @__PURE__ */ jsxs42("span", { className: "ss-w-mk-ctx-right", children: [
3132
- m.isSuspended && !m.isSettled && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-susp", children: ui.suspendedLabel }),
3133
- m.isSettled && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-pill", children: ui.fullTimeLabel }),
3134
- m.status === "stale" && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-asof", children: ui.asOf(new Date(m.asOfUtc).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) }),
3135
- (() => {
3136
- const pin = monitor.pins.find((p) => p.key === m.key);
3137
- return pin ? /* @__PURE__ */ jsx48(PinToggle, { ui, monitor, pin }) : null;
3138
- })()
3139
- ] })
3269
+ m.isSuspended && !m.isSettled && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-susp", children: ui.suspendedLabel }),
3270
+ m.isSettled && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-pill", children: ui.fullTimeLabel }),
3271
+ stale && /* @__PURE__ */ jsx48("span", { className: "ss-w-mon-asof", children: ui.asOf(new Date(m.asOfUtc).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) })
3140
3272
  ] });
3141
3273
  }
3142
3274
  function MatchesPanel({ monitor, ui, slip }) {
@@ -3169,20 +3301,25 @@ function withSinceViewMovement(vm, prev) {
3169
3301
  function OddsPanel({ monitor, ui, slip }) {
3170
3302
  const markets = (monitor.snapshot?.markets ?? []).filter((m) => monitor.pins.some((p) => p.key === m.key));
3171
3303
  if (markets.length === 0) return /* @__PURE__ */ jsx48("div", { className: "ss-w-mon-empty", children: ui.emptyOddsTab });
3172
- return /* @__PURE__ */ jsx48("div", { className: "ss-w-mon-list", children: markets.map((m) => /* @__PURE__ */ jsxs42("div", { className: "ss-w-mon-oddspin", "data-settled": m.isSettled || void 0, children: [
3173
- /* @__PURE__ */ jsx48(ContextLine, { m, monitor, ui }),
3174
- m.card && /* @__PURE__ */ jsx48(
3175
- MarketOddsCardList,
3176
- {
3177
- cards: [withSinceViewMovement(toMarketOddsVM(m.card), monitor.lastViewedPrices[m.key])],
3178
- ui,
3179
- slip,
3180
- inSlipIds: monitor.inSlip
3181
- }
3182
- ),
3183
- m.history && /* @__PURE__ */ jsx48(OddsMovementCardList, { cards: [toOddsMovementVM(m.history)], ui }),
3184
- !m.card && !m.history && /* @__PURE__ */ jsx48("div", { className: "ss-w-mon-empty-card", children: ui.oddsUnavailable })
3185
- ] }, m.key)) });
3304
+ return /* @__PURE__ */ jsx48("div", { className: "ss-w-mon-list", children: markets.map((m) => {
3305
+ const pin = monitor.pins.find((p) => p.key === m.key);
3306
+ return /* @__PURE__ */ jsxs42("div", { className: "ss-w-mon-oddspin", "data-settled": m.isSettled || void 0, children: [
3307
+ /* @__PURE__ */ jsx48(OddsLiveStrip, { m, ui }),
3308
+ m.card && /* @__PURE__ */ jsx48(
3309
+ MarketOddsCardList,
3310
+ {
3311
+ cards: [withSinceViewMovement(toMarketOddsVM(m.card), monitor.lastViewedPrices[m.key])],
3312
+ ui,
3313
+ slip,
3314
+ monitor,
3315
+ pinRef: pin ?? void 0,
3316
+ inSlipIds: monitor.inSlip
3317
+ }
3318
+ ),
3319
+ m.history && /* @__PURE__ */ jsx48(OddsMovementCardList, { cards: [toOddsMovementVM(m.history)], ui }),
3320
+ !m.card && !m.history && /* @__PURE__ */ jsx48("div", { className: "ss-w-mon-empty-card", children: ui.oddsUnavailable })
3321
+ ] }, m.key);
3322
+ }) });
3186
3323
  }
3187
3324
 
3188
3325
  // src/components/ChatPanel.tsx
@@ -3201,7 +3338,7 @@ function useIsModal() {
3201
3338
  return modal;
3202
3339
  }
3203
3340
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3204
- const { state, close, stop, send } = controller;
3341
+ const { state, close, stop, send, reset } = controller;
3205
3342
  const isModal = useIsModal();
3206
3343
  useEffect9(() => {
3207
3344
  const onKey = (e) => {
@@ -3218,7 +3355,18 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3218
3355
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3219
3356
  }, [monitor, state.isStreaming, state.messages.length]);
3220
3357
  return /* @__PURE__ */ jsxs43("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
3221
- /* @__PURE__ */ jsx49(ChatHeader, { onClose: close, closeLabel: ui.closeChat, logoUrl: ui.logoUrl, headerTitle: ui.headerTitle }),
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
+ ),
3222
3370
  monitor && /* @__PURE__ */ jsx49(
3223
3371
  TabBar,
3224
3372
  {
@@ -3333,8 +3481,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3333
3481
  }
3334
3482
  .ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
3335
3483
  .ss-w-header-mark { width: 18px; height: 18px; display: block; object-fit: contain; }
3336
- .ss-w-header-title { font-weight: 660; }
3337
- .ss-w-close { margin-left: auto; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 16px; display: grid; place-items: center; }
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; }
3338
3487
 
3339
3488
  /* messages */
3340
3489
  /* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
@@ -4022,7 +4171,11 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4022
4171
  a bare context line with nothing under it. */
4023
4172
  .ss-w-mon-empty-card { padding: 8px 0 2px; font-size: 12px; color: var(--ss-w-faint); }
4024
4173
  .ss-w-mon-inslip { display: inline-flex; flex: none; align-items: center; gap: 3px; font-size: 10.5px; font-weight: 700; color: var(--ss-w-pos); }
4025
- .ss-w-mon-oddspin { display: flex; flex-direction: column; gap: 10px; padding: 12px; border: 1px solid var(--ss-w-line); border-radius: 12px; background: var(--ss-w-panel); }
4174
+ /* Odds-tab item is bare: the visible card is the market-odds card itself (no outer wrapper card),
4175
+ with an optional slim live strip above it. */
4176
+ .ss-w-mon-oddspin { display: flex; flex-direction: column; gap: 8px; }
4177
+ .ss-w-mk-live { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 0 2px; font-size: 11.5px; color: var(--ss-w-faint); }
4178
+ .ss-w-mk-live-score { color: var(--ss-w-ink); font-weight: 700; font-variant-numeric: tabular-nums; }
4026
4179
  .ss-w-mon-oddspin[data-settled] .ss-w-mo-price,
4027
4180
  .ss-w-mon-oddspin[data-settled] .ss-w-mon-price { text-decoration: line-through; color: var(--ss-w-faint); }
4028
4181
 
@@ -4077,7 +4230,8 @@ function StatsWidget(props) {
4077
4230
  const markIntroDone = useCallback5(() => {
4078
4231
  introDoneRef.current = true;
4079
4232
  }, []);
4080
- const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
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);
4081
4235
  const monitor = useLiveMonitor({
4082
4236
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
4083
4237
  getMarketOdds: (matchId, dev) => built ? built.conversation.getMarketOdds(matchId, dev) : Promise.resolve(null),