@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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
9
+ ## 0.37.3
10
+
11
+ ### Patch Changes
12
+
13
+ - Odds tab: show a single market-odds card per pin instead of a card-inside-a-card. The fixture/date is no longer duplicated — each pinned market renders one betting card (competition, fixture, market, odds) with the unpin in its own header, plus a slim live strip (score, minute, suspended/settled) shown only for in-play markets and hidden for pre-match. The unpin threads the exact stored pin key so it always removes the right pin.
14
+
3
15
  ## 0.37.2
4
16
 
5
17
  ### 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 useChatStream(conversation, starterTexts, greetingMessage) {
847
- const seed = (0, import_react2.useMemo)(() => initialState(starterTexts.map(chipFromText)), []);
848
- const [state, dispatch] = (0, import_react2.useReducer)(reducer, seed);
849
- const idRef = (0, import_react2.useRef)(0);
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)(false);
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.useEffect)(() => {
991
+ const fireGreeting = (0, import_react2.useCallback)(() => {
911
992
  const message = greetingMessage?.trim();
912
- if (!message || greetedRef.current) return;
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
  }
@@ -1801,7 +1945,7 @@ function OddsRow({ vm, row, ui, slip, inSlipIds }) {
1801
1945
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(SlipButton, { selection, shownOdds: row.odds, marketName: vm.market, slip, ui })
1802
1946
  ] });
1803
1947
  }
1804
- function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1948
+ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
1805
1949
  const [open, setOpen] = (0, import_react12.useState)(false);
1806
1950
  const kickoff = vm.kickoffUtc && ui?.bettingKickoff ? ui.bettingKickoff(vm.kickoffUtc) : void 0;
1807
1951
  const hasNamedTeams = !!vm.homeTeamName && !!vm.awayTeamName;
@@ -1829,7 +1973,7 @@ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1829
1973
  {
1830
1974
  ui,
1831
1975
  monitor,
1832
- pin: { kind: "market", matchId: vm.matchId, marketDeveloperName: vm.marketDeveloperName, line: vm.mainLine }
1976
+ pin: pinRef ?? { kind: "market", matchId: vm.matchId, marketDeveloperName: vm.marketDeveloperName, line: vm.mainLine }
1833
1977
  }
1834
1978
  ) : null
1835
1979
  ] })
@@ -1857,9 +2001,9 @@ function MarketOddsCard({ vm, ui, slip, monitor, inSlipIds, hideHeader }) {
1857
2001
  vm.disclaimer ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "ss-w-bi-disc", children: vm.disclaimer }) : null
1858
2002
  ] });
1859
2003
  }
1860
- function MarketOddsCardList({ cards, ui, slip, monitor, inSlipIds, hideHeader }) {
2004
+ function MarketOddsCardList({ cards, ui, slip, monitor, inSlipIds, hideHeader, pinRef }) {
1861
2005
  if (!cards || cards.length === 0) return null;
1862
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MarketOddsCard, { vm, ui, slip, monitor, inSlipIds, hideHeader }, vm.id)) });
2006
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "ss-w-mocards", children: cards.map((vm) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(MarketOddsCard, { vm, ui, slip, monitor, inSlipIds, hideHeader, pinRef }, vm.id)) });
1863
2007
  }
1864
2008
 
1865
2009
  // src/components/OddsMovementCard.tsx
@@ -3144,36 +3288,24 @@ function MonitorMatchCard({ match, promotedPins, monitor, ui, slip }) {
3144
3288
 
3145
3289
  // src/components/MonitorTabs.tsx
3146
3290
  var import_jsx_runtime48 = require("react/jsx-runtime");
3147
- function ContextLine({ m, monitor, ui }) {
3148
- const kick = m.kickoffUtc ? ui.bettingKickoff(m.kickoffUtc) : void 0;
3149
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-mk-ctx", children: [
3150
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "ss-w-mk-ctx-side", children: [
3151
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(Crest2, { kind: "team", url: m.homeCrestUrl ?? void 0, name: m.homeTeamName ?? void 0 }),
3152
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-name", children: m.homeTeamName })
3153
- ] }),
3154
- m.phase === "preMatch" ? kick && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "ss-w-mon-pill", children: [
3155
- kick.dayLabel,
3291
+ function OddsLiveStrip({ m, ui }) {
3292
+ const showScore = m.phase !== "preMatch" && typeof m.homeScore === "number";
3293
+ const stale = m.status === "stale";
3294
+ if (!showScore && !m.minuteLabel && !(m.isSuspended && !m.isSettled) && !m.isSettled && !stale) return null;
3295
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-mk-live", children: [
3296
+ showScore && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("b", { className: "ss-w-mk-live-score", children: [
3297
+ m.homeTeamName,
3156
3298
  " ",
3157
- kick.time
3158
- ] }) : typeof m.homeScore === "number" && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("b", { children: [
3159
3299
  m.homeScore,
3160
3300
  "\u2013",
3161
- m.awayScore
3162
- ] }),
3163
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "ss-w-mk-ctx-side", children: [
3164
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-name", children: m.awayTeamName }),
3165
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(Crest2, { kind: "team", url: m.awayCrestUrl ?? void 0, name: m.awayTeamName ?? void 0 })
3301
+ m.awayScore,
3302
+ " ",
3303
+ m.awayTeamName
3166
3304
  ] }),
3167
3305
  m.minuteLabel && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-min", children: m.minuteLabel }),
3168
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("span", { className: "ss-w-mk-ctx-right", children: [
3169
- m.isSuspended && !m.isSettled && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-susp", children: ui.suspendedLabel }),
3170
- m.isSettled && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-pill", children: ui.fullTimeLabel }),
3171
- m.status === "stale" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-asof", children: ui.asOf(new Date(m.asOfUtc).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) }),
3172
- (() => {
3173
- const pin = monitor.pins.find((p) => p.key === m.key);
3174
- return pin ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(PinToggle, { ui, monitor, pin }) : null;
3175
- })()
3176
- ] })
3306
+ m.isSuspended && !m.isSettled && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-susp", children: ui.suspendedLabel }),
3307
+ m.isSettled && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-pill", children: ui.fullTimeLabel }),
3308
+ stale && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "ss-w-mon-asof", children: ui.asOf(new Date(m.asOfUtc).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })) })
3177
3309
  ] });
3178
3310
  }
3179
3311
  function MatchesPanel({ monitor, ui, slip }) {
@@ -3206,20 +3338,25 @@ function withSinceViewMovement(vm, prev) {
3206
3338
  function OddsPanel({ monitor, ui, slip }) {
3207
3339
  const markets = (monitor.snapshot?.markets ?? []).filter((m) => monitor.pins.some((p) => p.key === m.key));
3208
3340
  if (markets.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-mon-empty", children: ui.emptyOddsTab });
3209
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-mon-list", children: markets.map((m) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-mon-oddspin", "data-settled": m.isSettled || void 0, children: [
3210
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ContextLine, { m, monitor, ui }),
3211
- m.card && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3212
- MarketOddsCardList,
3213
- {
3214
- cards: [withSinceViewMovement(toMarketOddsVM(m.card), monitor.lastViewedPrices[m.key])],
3215
- ui,
3216
- slip,
3217
- inSlipIds: monitor.inSlip
3218
- }
3219
- ),
3220
- m.history && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(OddsMovementCardList, { cards: [toOddsMovementVM(m.history)], ui }),
3221
- !m.card && !m.history && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-mon-empty-card", children: ui.oddsUnavailable })
3222
- ] }, m.key)) });
3341
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-mon-list", children: markets.map((m) => {
3342
+ const pin = monitor.pins.find((p) => p.key === m.key);
3343
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "ss-w-mon-oddspin", "data-settled": m.isSettled || void 0, children: [
3344
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(OddsLiveStrip, { m, ui }),
3345
+ m.card && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3346
+ MarketOddsCardList,
3347
+ {
3348
+ cards: [withSinceViewMovement(toMarketOddsVM(m.card), monitor.lastViewedPrices[m.key])],
3349
+ ui,
3350
+ slip,
3351
+ monitor,
3352
+ pinRef: pin ?? void 0,
3353
+ inSlipIds: monitor.inSlip
3354
+ }
3355
+ ),
3356
+ m.history && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(OddsMovementCardList, { cards: [toOddsMovementVM(m.history)], ui }),
3357
+ !m.card && !m.history && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "ss-w-mon-empty-card", children: ui.oddsUnavailable })
3358
+ ] }, m.key);
3359
+ }) });
3223
3360
  }
3224
3361
 
3225
3362
  // src/components/ChatPanel.tsx
@@ -3238,7 +3375,7 @@ function useIsModal() {
3238
3375
  return modal;
3239
3376
  }
3240
3377
  function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3241
- const { state, close, stop, send } = controller;
3378
+ const { state, close, stop, send, reset } = controller;
3242
3379
  const isModal = useIsModal();
3243
3380
  (0, import_react22.useEffect)(() => {
3244
3381
  const onKey = (e) => {
@@ -3255,7 +3392,18 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor }) {
3255
3392
  if (monitor && monitor.activeTab !== "chat" && state.isStreaming) monitor.markChatActivity();
3256
3393
  }, [monitor, state.isStreaming, state.messages.length]);
3257
3394
  return /* @__PURE__ */ (0, import_jsx_runtime49.jsxs)("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
3258
- /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(ChatHeader, { onClose: close, closeLabel: ui.closeChat, logoUrl: ui.logoUrl, headerTitle: ui.headerTitle }),
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
+ ),
3259
3407
  monitor && /* @__PURE__ */ (0, import_jsx_runtime49.jsx)(
3260
3408
  TabBar,
3261
3409
  {
@@ -3370,8 +3518,9 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
3370
3518
  }
3371
3519
  .ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
3372
3520
  .ss-w-header-mark { width: 18px; height: 18px; display: block; object-fit: contain; }
3373
- .ss-w-header-title { font-weight: 660; }
3374
- .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; }
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; }
3375
3524
 
3376
3525
  /* messages */
3377
3526
  /* .ss-w-mon-list (Matches/Odds tab scrollers) shares .ss-w-list's box/scroll rules by design --
@@ -4059,7 +4208,11 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
4059
4208
  a bare context line with nothing under it. */
4060
4209
  .ss-w-mon-empty-card { padding: 8px 0 2px; font-size: 12px; color: var(--ss-w-faint); }
4061
4210
  .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); }
4062
- .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); }
4211
+ /* Odds-tab item is bare: the visible card is the market-odds card itself (no outer wrapper card),
4212
+ with an optional slim live strip above it. */
4213
+ .ss-w-mon-oddspin { display: flex; flex-direction: column; gap: 8px; }
4214
+ .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); }
4215
+ .ss-w-mk-live-score { color: var(--ss-w-ink); font-weight: 700; font-variant-numeric: tabular-nums; }
4063
4216
  .ss-w-mon-oddspin[data-settled] .ss-w-mo-price,
4064
4217
  .ss-w-mon-oddspin[data-settled] .ss-w-mon-price { text-decoration: line-through; color: var(--ss-w-faint); }
4065
4218
 
@@ -4114,7 +4267,8 @@ function StatsWidget(props) {
4114
4267
  const markIntroDone = (0, import_react23.useCallback)(() => {
4115
4268
  introDoneRef.current = true;
4116
4269
  }, []);
4117
- const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
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);
4118
4272
  const monitor = useLiveMonitor({
4119
4273
  getLiveMonitor: (pins) => built ? built.conversation.getLiveMonitor(pins) : Promise.resolve(null),
4120
4274
  getMarketOdds: (matchId, dev) => built ? built.conversation.getMarketOdds(matchId, dev) : Promise.resolve(null),