@sensiblestats/widget-react 0.79.0 → 0.80.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
@@ -30,6 +30,9 @@ var STRINGS = {
30
30
  dismissGreeting: "Dismiss",
31
31
  somethingWentWrong: "Something went wrong.",
32
32
  retry: "Retry",
33
+ limitDailyCap: "You've asked all your questions for today. You can ask again tomorrow.",
34
+ limitQuotaExhausted: "The assistant can't take new questions right now. Please try again later.",
35
+ limitAssistantPaused: "The assistant isn't available right now. Please try again later.",
33
36
  showLess: "Show less",
34
37
  showMore: (n) => `Show ${n} more`,
35
38
  allMarkets: (n) => `All markets (${n})`,
@@ -379,6 +382,9 @@ var STRINGS = {
379
382
  dismissGreeting: "Kapat",
380
383
  somethingWentWrong: "Bir \u015Feyler ters gitti.",
381
384
  retry: "Tekrar dene",
385
+ limitDailyCap: "Bug\xFCnl\xFCk sorular\u0131n\u0131 tamamlad\u0131n. Yar\u0131n yeniden sorabilirsin.",
386
+ limitQuotaExhausted: "Asistan \u015Fu an yeni soru alam\u0131yor. L\xFCtfen daha sonra tekrar dene.",
387
+ limitAssistantPaused: "Asistan \u015Fu an kullan\u0131lam\u0131yor. L\xFCtfen daha sonra tekrar dene.",
382
388
  showLess: "Daha az g\xF6ster",
383
389
  showMore: (n) => `${n} tane daha g\xF6ster`,
384
390
  allMarkets: (n) => `T\xFCm bahisler (${n})`,
@@ -757,6 +763,9 @@ function normalizeUi(cfg) {
757
763
  dismissGreeting: strings.dismissGreeting,
758
764
  somethingWentWrong: strings.somethingWentWrong,
759
765
  retry: strings.retry,
766
+ limitDailyCap: strings.limitDailyCap,
767
+ limitQuotaExhausted: strings.limitQuotaExhausted,
768
+ limitAssistantPaused: strings.limitAssistantPaused,
760
769
  showLess: strings.showLess,
761
770
  showMore: strings.showMore,
762
771
  allMarkets: strings.allMarkets,
@@ -1704,6 +1713,51 @@ import { WidgetSdkError as WidgetSdkError2 } from "@sensiblestats/widget-sdk";
1704
1713
 
1705
1714
  // src/reducer.ts
1706
1715
  import "@sensiblestats/widget-sdk";
1716
+
1717
+ // src/limit.ts
1718
+ import { LimitError, isLimitCode } from "@sensiblestats/widget-sdk";
1719
+ var LIMIT_TIMER_MAX_MS = 60 * 60 * 1e3;
1720
+ var ZONED = /(?:Z|[+-]\d{2}:?\d{2})$/i;
1721
+ function build(code, message, resetsAt, retryAfterSeconds) {
1722
+ return {
1723
+ code,
1724
+ paused: code === "assistant_paused",
1725
+ message: message?.trim() || null,
1726
+ resetsAt: resetsAt ?? null,
1727
+ retryAfterSeconds: typeof retryAfterSeconds === "number" && Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : null
1728
+ };
1729
+ }
1730
+ function limitFromEvent(data) {
1731
+ return isLimitCode(data.code) ? build(data.code, data.message, data.resetsAt, data.retryAfterSeconds) : null;
1732
+ }
1733
+ function limitFromError(error) {
1734
+ return error instanceof LimitError ? build(error.limitCode, error.limitMessage, error.resetsAt, error.retryAfter) : null;
1735
+ }
1736
+ function isLimitNotice(data) {
1737
+ return data.terminal === false && isLimitCode(data.code);
1738
+ }
1739
+ function isPaused(limit) {
1740
+ return limit?.paused === true;
1741
+ }
1742
+ function limitText(limit, ui) {
1743
+ if (limit.message) return limit.message;
1744
+ switch (limit.code) {
1745
+ case "daily_cap":
1746
+ return ui.limitDailyCap;
1747
+ case "quota_exhausted":
1748
+ return ui.limitQuotaExhausted;
1749
+ case "assistant_paused":
1750
+ return ui.limitAssistantPaused;
1751
+ }
1752
+ }
1753
+ function limitDelayMs(limit, nowMs) {
1754
+ const retry = limit.retryAfterSeconds;
1755
+ if (retry != null && Number.isFinite(retry) && retry >= 0) return Math.min(retry * 1e3, LIMIT_TIMER_MAX_MS);
1756
+ const at = limit.resetsAt ? Date.parse(ZONED.test(limit.resetsAt) ? limit.resetsAt : `${limit.resetsAt}Z`) : Number.NaN;
1757
+ return Number.isNaN(at) ? null : Math.min(Math.max(at - nowMs, 0), LIMIT_TIMER_MAX_MS);
1758
+ }
1759
+
1760
+ // src/reducer.ts
1707
1761
  var POPUP_MAX = 160;
1708
1762
  function markAwaitingLiveUpdate(messages, held = []) {
1709
1763
  if (!messages.some((m) => m.role === "assistant" && m.marketOdds.some((vm) => vm.live != null))) return messages;
@@ -1721,12 +1775,18 @@ function markAwaitingLiveUpdate(messages, held = []) {
1721
1775
  });
1722
1776
  }
1723
1777
  function initialState(starters) {
1724
- return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, configuredStarters: starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
1778
+ return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, configuredStarters: starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null, limit: null };
1725
1779
  }
1726
1780
  function activeTurn(state) {
1727
1781
  const last = state.messages[state.messages.length - 1];
1728
1782
  return last && last.role === "assistant" && !last.done ? last : null;
1729
1783
  }
1784
+ function settleRefused(state, limit) {
1785
+ return { ...state, limit, error: null, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
1786
+ }
1787
+ function isPlainInput(input) {
1788
+ return !!input && !input.actionRef && !input.actionId && !input.grounding && !input.preset;
1789
+ }
1730
1790
  function mapActive(state, fn) {
1731
1791
  const last = state.messages[state.messages.length - 1];
1732
1792
  if (!last || last.role !== "assistant" || last.done) return state.messages;
@@ -1766,6 +1826,7 @@ function applyEvent(state, event) {
1766
1826
  case "turn_complete": {
1767
1827
  const active = activeTurn(state);
1768
1828
  const greetingLine = event.data.clubFanDashboard?.greeting?.trim() || (active?.text.trim() ?? "");
1829
+ const clearsLimit = !active?.isGreeting && isPlainInput(state.lastUserInput);
1769
1830
  const popupText = active?.isGreeting && !state.isOpen && greetingLine ? greetingLine.slice(0, POPUP_MAX) : state.popupText;
1770
1831
  return {
1771
1832
  ...state,
@@ -1773,6 +1834,7 @@ function applyEvent(state, event) {
1773
1834
  status: null,
1774
1835
  actionsLoading: false,
1775
1836
  popupText,
1837
+ limit: clearsLimit ? null : state.limit,
1776
1838
  conversationId: event.data.conversationId ?? state.conversationId,
1777
1839
  messages: mapActive(state, (t) => ({
1778
1840
  ...t,
@@ -1783,6 +1845,9 @@ function applyEvent(state, event) {
1783
1845
  };
1784
1846
  }
1785
1847
  case "error": {
1848
+ const limit = limitFromEvent(event.data);
1849
+ if (limit && event.data.terminal === false) return { ...state, limit };
1850
+ if (limit) return settleRefused(state, limit);
1786
1851
  const suppress = activeTurn(state)?.isGreeting === true;
1787
1852
  return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
1788
1853
  }
@@ -1807,7 +1872,7 @@ function reducer(state, action) {
1807
1872
  case "DISMISS_POPUP":
1808
1873
  return { ...state, popupText: null };
1809
1874
  case "RESET":
1810
- return { ...initialState(state.configuredStarters), isOpen: state.isOpen };
1875
+ return { ...initialState(state.configuredStarters), isOpen: state.isOpen, limit: state.limit };
1811
1876
  case "RESTORE":
1812
1877
  return {
1813
1878
  ...state,
@@ -1821,8 +1886,13 @@ function reducer(state, action) {
1821
1886
  };
1822
1887
  case "EVENT":
1823
1888
  return applyEvent(state, action.event);
1824
- case "STREAM_ERROR":
1889
+ case "STREAM_ERROR": {
1890
+ const limit = limitFromError(action.error);
1891
+ if (limit) return settleRefused(state, limit);
1825
1892
  return { ...state, error: action.error, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
1893
+ }
1894
+ case "LIMIT_EXPIRED":
1895
+ return { ...state, limit: null };
1826
1896
  case "STOP":
1827
1897
  return { ...state, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
1828
1898
  case "UPDATE_LIVE_CARD": {
@@ -1995,7 +2065,7 @@ function clearConversation(storageKey) {
1995
2065
  }
1996
2066
 
1997
2067
  // src/presets.ts
1998
- var CLIENT_CAPABILITIES = ["price_presets", "price_presets_open"];
2068
+ var CLIENT_CAPABILITIES = ["price_presets", "price_presets_open", "limit_notice"];
1999
2069
  var PRICE_TILE_PREFIX = "goal-preset-";
2000
2070
  function isPriceTile(tile) {
2001
2071
  const id = tile.signal?.entityId;
@@ -2093,7 +2163,7 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2093
2163
  try {
2094
2164
  for await (const event of handle) {
2095
2165
  dispatch({ kind: "EVENT", event });
2096
- if (event.type === "turn_complete" || event.type === "error") clearFallbackTimer();
2166
+ if (event.type === "turn_complete" || event.type === "error" && !isLimitNotice(event.data)) clearFallbackTimer();
2097
2167
  }
2098
2168
  } catch (err) {
2099
2169
  if (handleRef.current === handle) clearFallbackTimer();
@@ -2106,6 +2176,7 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2106
2176
  })();
2107
2177
  }, [clearFallbackTimer]);
2108
2178
  const run = useCallback2((input, displayText) => {
2179
+ if (isPaused(stateRef.current.limit)) return;
2109
2180
  handleRef.current?.cancel();
2110
2181
  clearFallbackTimer();
2111
2182
  dispatch({ kind: "USER_SENT", input, userId: nextId(), assistantId: nextId(), displayText });
@@ -2153,6 +2224,13 @@ function useChatStream(conversation, starterTexts, greetingMessage, storageKey)
2153
2224
  greetedRef.current = true;
2154
2225
  fireGreeting();
2155
2226
  }, [fireGreeting]);
2227
+ useEffect3(() => {
2228
+ if (!state.limit) return;
2229
+ const delay = limitDelayMs(state.limit, Date.now());
2230
+ if (delay === null) return;
2231
+ const timer = setTimeout(() => dispatch({ kind: "LIMIT_EXPIRED" }), delay);
2232
+ return () => clearTimeout(timer);
2233
+ }, [state.limit]);
2156
2234
  useEffect3(() => {
2157
2235
  if (boot.stale && storageKey) clearConversation(storageKey);
2158
2236
  }, []);
@@ -5343,7 +5421,7 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
5343
5421
  ChatMessage,
5344
5422
  {
5345
5423
  message: m,
5346
- disabled: state.isStreaming,
5424
+ disabled: state.isStreaming || isPaused(state.limit),
5347
5425
  onAct: (a) => send(a.message, { actionId: a.actionId, actionRef: a.actionRef, displayText: a.label }),
5348
5426
  onTap: (tile, index) => {
5349
5427
  const displayText = tileDisplayText(tile);
@@ -5363,7 +5441,7 @@ function MessageList({ controller, ui, slip, onDashboardTap, monitor }) {
5363
5441
  /* @__PURE__ */ jsx48("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
5364
5442
  /* @__PURE__ */ jsx48("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
5365
5443
  ] }) : null,
5366
- !hasUserMessage && !hasDashboard ? /* @__PURE__ */ jsx48(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
5444
+ !hasUserMessage && !hasDashboard && !isPaused(state.limit) ? /* @__PURE__ */ jsx48(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
5367
5445
  /* @__PURE__ */ jsx48("div", { ref: endRef, className: "ss-w-turn-spacer", "aria-hidden": "true" })
5368
5446
  ]
5369
5447
  }
@@ -9310,7 +9388,7 @@ function markToolSeen() {
9310
9388
  }
9311
9389
 
9312
9390
  // src/components/ChatPanel.tsx
9313
- import { jsx as jsx95, jsxs as jsxs80 } from "react/jsx-runtime";
9391
+ import { Fragment as Fragment23, jsx as jsx95, jsxs as jsxs80 } from "react/jsx-runtime";
9314
9392
  var FULLSCREEN_QUERY = "(max-width: 639px)";
9315
9393
  function useIsModal() {
9316
9394
  const [modal, setModal] = useState44(false);
@@ -9336,6 +9414,7 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9336
9414
  return () => window.removeEventListener("keydown", onKey);
9337
9415
  }, [close]);
9338
9416
  const handleSend = (text) => {
9417
+ typedSendRef.current = true;
9339
9418
  monitor?.setActiveTab("chat");
9340
9419
  send(text);
9341
9420
  };
@@ -9351,6 +9430,8 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9351
9430
  const dialRef = useRef22(null);
9352
9431
  const dialButtonRef = useRef22(null);
9353
9432
  const inputRef = useRef22(null);
9433
+ const limitRef = useRef22(null);
9434
+ const typedSendRef = useRef22(false);
9354
9435
  const focusAfterCloseRef = useRef22(null);
9355
9436
  const [tool, setTool] = useState44(null);
9356
9437
  const [toolSeen, setToolSeen] = useState44(readToolSeen);
@@ -9363,7 +9444,16 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9363
9444
  activeTab: monitor?.activeTab ?? "chat"
9364
9445
  });
9365
9446
  const { fold } = dial;
9366
- const showDial = !!presetGrid && onChatTab && !tool;
9447
+ const paused = isPaused(state.limit);
9448
+ const showDial = !!presetGrid && onChatTab && !tool && !paused;
9449
+ useEffect26(() => {
9450
+ if (paused) setTool(null);
9451
+ }, [paused]);
9452
+ useEffect26(() => {
9453
+ if (!paused) return;
9454
+ if (typedSendRef.current) limitRef.current?.focus();
9455
+ typedSendRef.current = false;
9456
+ }, [paused]);
9367
9457
  const openTool = useCallback15((next) => {
9368
9458
  fold();
9369
9459
  markToolSeen();
@@ -9372,8 +9462,8 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9372
9462
  }, [fold]);
9373
9463
  const onPill = useCallback15((kind) => openTool(kind === "numbers" ? { kind: "numbers" } : { kind: "combo" }), [openTool]);
9374
9464
  const toolsValue = useMemo4(
9375
- () => presetGrid ? { openBuilder: (total, legs) => openTool({ kind: "combo", total, legs }), changeLabel: ui.presets.changeCombo } : null,
9376
- [presetGrid, openTool, ui.presets.changeCombo]
9465
+ () => presetGrid && !paused ? { openBuilder: (total, legs) => openTool({ kind: "combo", total, legs }), changeLabel: ui.presets.changeCombo } : null,
9466
+ [presetGrid, paused, openTool, ui.presets.changeCombo]
9377
9467
  );
9378
9468
  const closeTool = useCallback15(() => {
9379
9469
  focusAfterCloseRef.current = "dial";
@@ -9441,10 +9531,10 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9441
9531
  buttonRef: dialButtonRef
9442
9532
  }
9443
9533
  ) : null,
9444
- tool && presetGrid && onChatTab ? tool.kind === "numbers" ? /* @__PURE__ */ jsx95(SetNumbersTool, { grid: presetGrid, strings: ui.presets, streaming: state.isStreaming, onSend: sendAsk, onClose: closeTool }) : /* @__PURE__ */ jsx95(BuildComboTool, { grid: presetGrid, strings: ui.presets, streaming: state.isStreaming, formatKickoff, initialTotal: tool.total, initialLegs: tool.legs, onSend: sendAsk, onClose: closeTool }) : null
9534
+ tool && presetGrid && onChatTab && !paused ? tool.kind === "numbers" ? /* @__PURE__ */ jsx95(SetNumbersTool, { grid: presetGrid, strings: ui.presets, streaming: state.isStreaming, onSend: sendAsk, onClose: closeTool }) : /* @__PURE__ */ jsx95(BuildComboTool, { grid: presetGrid, strings: ui.presets, streaming: state.isStreaming, formatKickoff, initialTotal: tool.total, initialLegs: tool.legs, onSend: sendAsk, onClose: closeTool }) : null
9445
9535
  ] }),
9446
9536
  monitor && discovery && monitor.activeTab === "today" && /* @__PURE__ */ jsx95(TodayPanel, { discovery, ui, monitor }),
9447
- monitor && monitor.activeTab === "following" && /* @__PURE__ */ jsx95(MatchesPanel, { monitor, ui, slip, onAsk: handleAsk }),
9537
+ monitor && monitor.activeTab === "following" && /* @__PURE__ */ jsx95(MatchesPanel, { monitor, ui, slip, onAsk: paused ? void 0 : handleAsk }),
9448
9538
  monitor && monitor.activeTab === "odds" && /* @__PURE__ */ jsx95(OddsPanel, { monitor, ui, slip }),
9449
9539
  monitor && monitor.activeTab === "games" && gamesResult && /* @__PURE__ */ jsx95(
9450
9540
  GamesPanel,
@@ -9470,20 +9560,23 @@ function ChatPanel({ controller, ui, slip, onDashboardTap, monitor, discovery, g
9470
9560
  ui
9471
9561
  }
9472
9562
  ),
9473
- (!monitor || monitor.activeTab !== "games" && monitor.activeTab !== "leaderboard") && /* @__PURE__ */ jsx95(
9474
- ChatInput,
9475
- {
9476
- ref: inputRef,
9477
- placeholder: ui.placeholder,
9478
- streaming: state.isStreaming,
9479
- onSend: handleSend,
9480
- onStop: stop,
9481
- stopLabel: ui.stopResponse,
9482
- sendLabel: ui.sendMessage,
9483
- disabled: !!tool,
9484
- onType: fold
9485
- }
9486
- )
9563
+ (!monitor || monitor.activeTab !== "games" && monitor.activeTab !== "leaderboard") && /* @__PURE__ */ jsxs80(Fragment23, { children: [
9564
+ /* @__PURE__ */ jsx95("div", { ref: limitRef, className: "ss-w-limit", role: "status", tabIndex: -1, children: state.limit ? limitText(state.limit, ui) : null }),
9565
+ paused ? null : /* @__PURE__ */ jsx95(
9566
+ ChatInput,
9567
+ {
9568
+ ref: inputRef,
9569
+ placeholder: ui.placeholder,
9570
+ streaming: state.isStreaming,
9571
+ onSend: handleSend,
9572
+ onStop: stop,
9573
+ stopLabel: ui.stopResponse,
9574
+ sendLabel: ui.sendMessage,
9575
+ disabled: !!tool,
9576
+ onType: fold
9577
+ }
9578
+ )
9579
+ ] })
9487
9580
  ] });
9488
9581
  }
9489
9582
 
@@ -10081,6 +10174,10 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
10081
10174
 
10082
10175
  /* input */
10083
10176
  .ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }
10177
+ .ss-w-limit { padding: 10px 12px; border-top: 1px solid var(--ss-w-line); font-size: 13px; line-height: 1.4; color: var(--ss-w-ink); overflow-wrap: anywhere; }
10178
+ .ss-w-limit:empty { padding: 0; border-top: 0; }
10179
+ .ss-w-limit:focus { outline: none; }
10180
+ .ss-w-limit:not(:empty) + .ss-w-input { border-top: none; }
10084
10181
  .ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }
10085
10182
  .ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }
10086
10183
  /* iOS Safari zooms the page when a focused field's font-size is < 16px. Bump it