@sensiblestats/widget-react 0.25.0 → 0.26.1

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
@@ -1,5 +1,5 @@
1
1
  // src/StatsWidget.tsx
2
- import { useEffect as useEffect6, useMemo as useMemo2, useRef as useRef3 } from "react";
2
+ import { useEffect as useEffect7, useMemo as useMemo2, useRef as useRef4 } from "react";
3
3
  import { StatsWidgetClient } from "@sensiblestats/widget-sdk";
4
4
 
5
5
  // src/kickoff.ts
@@ -225,7 +225,7 @@ function themeAttrs(ui) {
225
225
  }
226
226
 
227
227
  // src/useChatStream.ts
228
- import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
228
+ import { useCallback, useEffect as useEffect2, useMemo, useReducer, useRef as useRef2 } from "react";
229
229
  import { WidgetSdkError as WidgetSdkError2 } from "@sensiblestats/widget-sdk";
230
230
 
231
231
  // src/reducer.ts
@@ -567,7 +567,50 @@ function reducer(state, action) {
567
567
  return { ...state, error: action.error, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
568
568
  case "STOP":
569
569
  return { ...state, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
570
+ case "LIVE_REFRESH":
571
+ return {
572
+ ...state,
573
+ messages: state.messages.map(
574
+ (m) => m.role === "assistant" && m.id === action.messageId && m.clubFanDashboard ? { ...m, clubFanDashboard: { ...m.clubFanDashboard, cards: action.cards } } : m
575
+ )
576
+ };
577
+ }
578
+ }
579
+
580
+ // src/useLiveDashboardPoll.ts
581
+ import { useEffect, useRef } from "react";
582
+ var POLL_INTERVAL_MS = 6e4;
583
+ function findLiveMessageId(messages) {
584
+ for (const m of messages.slice().reverse()) {
585
+ if (m.role === "assistant" && m.clubFanDashboard?.phase === "live") return m.id;
570
586
  }
587
+ return null;
588
+ }
589
+ function useLiveDashboardPoll({ messages, dispatch, getLiveDashboard }) {
590
+ const messageId = findLiveMessageId(messages);
591
+ const inFlightRef = useRef(false);
592
+ useEffect(() => {
593
+ if (!messageId) return;
594
+ const tick = async () => {
595
+ if (inFlightRef.current) return;
596
+ inFlightRef.current = true;
597
+ try {
598
+ const payload = await getLiveDashboard();
599
+ if (payload && payload.phase === "live") {
600
+ dispatch({ kind: "LIVE_REFRESH", messageId, cards: payload.cards });
601
+ } else {
602
+ window.clearInterval(id);
603
+ }
604
+ } catch {
605
+ } finally {
606
+ inFlightRef.current = false;
607
+ }
608
+ };
609
+ const id = window.setInterval(() => {
610
+ void tick();
611
+ }, POLL_INTERVAL_MS);
612
+ return () => window.clearInterval(id);
613
+ }, [messageId, dispatch, getLiveDashboard]);
571
614
  }
572
615
 
573
616
  // src/useChatStream.ts
@@ -575,12 +618,12 @@ var STREAM_FALLBACK_TIMEOUT_MS = 3e4;
575
618
  function useChatStream(conversation, starterTexts, greetingMessage) {
576
619
  const seed = useMemo(() => initialState(starterTexts.map(chipFromText)), []);
577
620
  const [state, dispatch] = useReducer(reducer, seed);
578
- const idRef = useRef(0);
579
- const handleRef = useRef(null);
580
- const fallbackTimerRef = useRef(null);
581
- const stateRef = useRef(state);
621
+ const idRef = useRef2(0);
622
+ const handleRef = useRef2(null);
623
+ const fallbackTimerRef = useRef2(null);
624
+ const stateRef = useRef2(state);
582
625
  stateRef.current = state;
583
- const greetedRef = useRef(false);
626
+ const greetedRef = useRef2(false);
584
627
  const nextId = () => `m${idRef.current++}`;
585
628
  const clearFallbackTimer = useCallback(() => {
586
629
  if (fallbackTimerRef.current != null) {
@@ -636,7 +679,7 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
636
679
  const last = stateRef.current.lastUserInput;
637
680
  if (last) run(last, stateRef.current.lastDisplayText ?? void 0);
638
681
  }, [run]);
639
- useEffect(() => {
682
+ useEffect2(() => {
640
683
  const message = greetingMessage?.trim();
641
684
  if (!message || greetedRef.current) return;
642
685
  greetedRef.current = true;
@@ -670,6 +713,8 @@ function useChatStream(conversation, starterTexts, greetingMessage) {
670
713
  else open();
671
714
  }, [close, open]);
672
715
  const dismissPopup = useCallback(() => dispatch({ kind: "DISMISS_POPUP" }), []);
716
+ const getLiveDashboard = useCallback(() => conversation.getLiveDashboard(), [conversation]);
717
+ useLiveDashboardPoll({ messages: state.messages, dispatch, getLiveDashboard });
673
718
  return { state, open, close, stop, toggle, send, retry, dismissPopup };
674
719
  }
675
720
 
@@ -730,7 +775,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
730
775
  }
731
776
 
732
777
  // src/components/ChatPanel.tsx
733
- import { useEffect as useEffect5, useState as useState8 } from "react";
778
+ import { useEffect as useEffect6, useState as useState8 } from "react";
734
779
 
735
780
  // src/components/ChatHeader.tsx
736
781
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -743,7 +788,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
743
788
  }
744
789
 
745
790
  // src/components/MessageList.tsx
746
- import { useEffect as useEffect4, useRef as useRef2 } from "react";
791
+ import { useEffect as useEffect5, useRef as useRef3 } from "react";
747
792
 
748
793
  // src/components/Markdown.tsx
749
794
  import ReactMarkdown from "react-markdown";
@@ -898,11 +943,11 @@ function resultToToast(result, shownOdds, ui) {
898
943
  }
899
944
 
900
945
  // src/components/Toast.tsx
901
- import { useEffect as useEffect2 } from "react";
946
+ import { useEffect as useEffect3 } from "react";
902
947
  import { jsx as jsx7 } from "react/jsx-runtime";
903
948
  var DISMISS_MS = 4e3;
904
949
  function Toast({ toast, onDismiss }) {
905
- useEffect2(() => {
950
+ useEffect3(() => {
906
951
  if (!toast) return;
907
952
  const timer = setTimeout(onDismiss, DISMISS_MS);
908
953
  return () => clearTimeout(timer);
@@ -1370,7 +1415,7 @@ function FixtureCard({ card, onTap }) {
1370
1415
  }
1371
1416
 
1372
1417
  // src/components/dashboard/CountdownCard.tsx
1373
- import { Fragment as Fragment6, useEffect as useEffect3, useState as useState6 } from "react";
1418
+ import { Fragment as Fragment6, useEffect as useEffect4, useState as useState6 } from "react";
1374
1419
  import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
1375
1420
  var DAY_MS = 864e5;
1376
1421
  function remainingSegments(kickoffUnix, nowMs, withSeconds) {
@@ -1394,7 +1439,7 @@ function CountdownCard({ card }) {
1394
1439
  () => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
1395
1440
  );
1396
1441
  const withinDay = data.kickoffUnix * 1e3 - nowMs < DAY_MS;
1397
- useEffect3(() => {
1442
+ useEffect4(() => {
1398
1443
  if (reduced) return;
1399
1444
  const id = window.setInterval(() => setNowMs(Date.now()), withinDay ? 1e3 : 6e4);
1400
1445
  return () => window.clearInterval(id);
@@ -1650,85 +1695,139 @@ function RestingFormCard({ card }) {
1650
1695
  ] });
1651
1696
  }
1652
1697
 
1653
- // src/components/dashboard/ClubFanDashboard.tsx
1698
+ // src/components/dashboard/LiveScoreCard.tsx
1654
1699
  import { jsx as jsx34, jsxs as jsxs30 } from "react/jsx-runtime";
1700
+ function LiveScoreCard({ card }) {
1701
+ const { data } = card;
1702
+ return /* @__PURE__ */ jsxs30("div", { className: "ss-w-fandash-card ss-w-fandash-live-score", children: [
1703
+ card.title ? /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-eyebrow", children: card.title }) : null,
1704
+ /* @__PURE__ */ jsxs30("div", { className: "ss-w-fandash-postmatch-scoreboard", children: [
1705
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-postmatch-team", children: data.homeName }),
1706
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-postmatch-score", children: data.homeScore }),
1707
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-postmatch-sep", children: "\u2013" }),
1708
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-postmatch-score", children: data.awayScore }),
1709
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-postmatch-team ss-w-fandash-postmatch-team--away", children: data.awayName })
1710
+ ] }),
1711
+ /* @__PURE__ */ jsxs30("div", { className: "ss-w-fandash-live-meta", children: [
1712
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-live-minute", children: data.minuteLabel }),
1713
+ /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-live-status", children: data.statusLabel }),
1714
+ data.competition ? /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-live-competition", children: data.competition }) : null
1715
+ ] })
1716
+ ] });
1717
+ }
1718
+
1719
+ // src/components/dashboard/LiveTimelineCard.tsx
1720
+ import { jsx as jsx35, jsxs as jsxs31 } from "react/jsx-runtime";
1721
+ function LiveTimelineCard({ card }) {
1722
+ return /* @__PURE__ */ jsxs31("div", { className: "ss-w-fandash-card ss-w-fandash-live-timeline", children: [
1723
+ card.title ? /* @__PURE__ */ jsx35("span", { className: "ss-w-fandash-eyebrow", children: card.title }) : null,
1724
+ /* @__PURE__ */ jsx35("ul", { className: "ss-w-fandash-live-event-list", children: card.data.events.map((e, i) => /* @__PURE__ */ jsxs31("li", { className: e.scoredByClub ? "ss-w-fandash-live-event ss-w-fandash-live-event--club" : "ss-w-fandash-live-event", children: [
1725
+ /* @__PURE__ */ jsx35("span", { className: "ss-w-fandash-live-event-minute", children: e.minuteLabel }),
1726
+ /* @__PURE__ */ jsx35("span", { className: "ss-w-fandash-live-event-text", children: e.text })
1727
+ ] }, `${e.minuteLabel}-${i}`)) })
1728
+ ] });
1729
+ }
1730
+
1731
+ // src/components/dashboard/LiveStatsCard.tsx
1732
+ import { jsx as jsx36, jsxs as jsxs32 } from "react/jsx-runtime";
1733
+ function LiveStatsCard({ card }) {
1734
+ const { data } = card;
1735
+ return /* @__PURE__ */ jsxs32("div", { className: "ss-w-fandash-card ss-w-fandash-live-stats", children: [
1736
+ card.title ? /* @__PURE__ */ jsx36("span", { className: "ss-w-fandash-eyebrow", children: card.title }) : null,
1737
+ /* @__PURE__ */ jsx36("div", { className: "ss-w-fandash-live-stat-rows", children: data.rows.map((r, i) => /* @__PURE__ */ jsxs32("div", { className: "ss-w-fandash-live-stat-row", children: [
1738
+ /* @__PURE__ */ jsx36("span", { className: "ss-w-fandash-live-stat-value", children: r.clubValue }),
1739
+ /* @__PURE__ */ jsx36("span", { className: "ss-w-fandash-live-stat-label", children: r.label }),
1740
+ /* @__PURE__ */ jsx36("span", { className: "ss-w-fandash-live-stat-value", children: r.oppValue })
1741
+ ] }, `${r.label}-${i}`)) }),
1742
+ data.momentumLabel ? /* @__PURE__ */ jsx36("span", { className: "ss-w-fandash-live-momentum", children: data.momentumLabel }) : null
1743
+ ] });
1744
+ }
1745
+
1746
+ // src/components/dashboard/ClubFanDashboard.tsx
1747
+ import { jsx as jsx37, jsxs as jsxs33 } from "react/jsx-runtime";
1655
1748
  function ClubFanDashboard({ dashboard, onTap, ui }) {
1656
1749
  const orderedCards = [
1657
1750
  ...dashboard.cards.filter((c) => c.cardType === "countdown"),
1658
1751
  ...dashboard.cards.filter((c) => c.cardType !== "countdown")
1659
1752
  ];
1660
- return /* @__PURE__ */ jsxs30("div", { className: "ss-w-fandash", children: [
1661
- dashboard.greeting ? /* @__PURE__ */ jsx34("span", { className: "ss-w-fandash-greeting", children: dashboard.greeting }) : null,
1662
- /* @__PURE__ */ jsx34("div", { className: "ss-w-fandash-cards", children: orderedCards.map((card, i) => /* @__PURE__ */ jsx34(FanDashboardCard, { card, onTap, ui }, `${card.cardType}-${i}`)) })
1753
+ return /* @__PURE__ */ jsxs33("div", { className: "ss-w-fandash", children: [
1754
+ dashboard.greeting ? /* @__PURE__ */ jsx37("span", { className: "ss-w-fandash-greeting", children: dashboard.greeting }) : null,
1755
+ /* @__PURE__ */ jsx37("div", { className: "ss-w-fandash-cards", children: orderedCards.map((card, i) => /* @__PURE__ */ jsx37(FanDashboardCard, { card, onTap, ui }, `${card.cardType}-${i}`)) })
1663
1756
  ] });
1664
1757
  }
1665
1758
  function FanDashboardCard({ card, onTap, ui }) {
1666
1759
  switch (card.cardType) {
1667
1760
  case "fixture":
1668
- return /* @__PURE__ */ jsx34(FixtureCard, { card, onTap });
1761
+ return /* @__PURE__ */ jsx37(FixtureCard, { card, onTap });
1669
1762
  case "countdown":
1670
- return /* @__PURE__ */ jsx34(CountdownCard, { card });
1763
+ return /* @__PURE__ */ jsx37(CountdownCard, { card });
1671
1764
  case "injuries":
1672
- return /* @__PURE__ */ jsx34(InjuriesCard, { card });
1765
+ return /* @__PURE__ */ jsx37(InjuriesCard, { card });
1673
1766
  case "reasonToBet":
1674
- return /* @__PURE__ */ jsx34(ReasonToBetCard, { card });
1767
+ return /* @__PURE__ */ jsx37(ReasonToBetCard, { card });
1675
1768
  case "whereToWatch":
1676
- return /* @__PURE__ */ jsx34(WhereToWatchCard, { card });
1769
+ return /* @__PURE__ */ jsx37(WhereToWatchCard, { card });
1677
1770
  case "congestedWeek":
1678
- return /* @__PURE__ */ jsx34(CongestedWeekCard, { card, onTap });
1771
+ return /* @__PURE__ */ jsx37(CongestedWeekCard, { card, onTap });
1679
1772
  case "transfers":
1680
- return /* @__PURE__ */ jsx34(TransfersCard, { card });
1773
+ return /* @__PURE__ */ jsx37(TransfersCard, { card });
1681
1774
  case "links":
1682
- return /* @__PURE__ */ jsx34(LinksCard, { card });
1775
+ return /* @__PURE__ */ jsx37(LinksCard, { card });
1683
1776
  case "postMatchResult":
1684
- return /* @__PURE__ */ jsx34(PostMatchResultCard, { card });
1777
+ return /* @__PURE__ */ jsx37(PostMatchResultCard, { card });
1685
1778
  case "postMatchGoals":
1686
- return /* @__PURE__ */ jsx34(PostMatchGoalsCard, { card, onTap });
1779
+ return /* @__PURE__ */ jsx37(PostMatchGoalsCard, { card, onTap });
1687
1780
  case "postMatchStats":
1688
- return /* @__PURE__ */ jsx34(PostMatchStatsCard, { card, onTap });
1781
+ return /* @__PURE__ */ jsx37(PostMatchStatsCard, { card, onTap });
1689
1782
  case "postMatchTopPerformer":
1690
- return /* @__PURE__ */ jsx34(PostMatchTopPerformerCard, { card, onTap });
1783
+ return /* @__PURE__ */ jsx37(PostMatchTopPerformerCard, { card, onTap });
1691
1784
  case "postMatchNext":
1692
- return /* @__PURE__ */ jsx34(PostMatchNextCard, { card });
1785
+ return /* @__PURE__ */ jsx37(PostMatchNextCard, { card });
1693
1786
  case "restingNext":
1694
- return /* @__PURE__ */ jsx34(RestingNextCard, { card, onTap });
1787
+ return /* @__PURE__ */ jsx37(RestingNextCard, { card, onTap });
1695
1788
  case "restingStandings":
1696
- return /* @__PURE__ */ jsx34(RestingStandingsCard, { card, onTap });
1789
+ return /* @__PURE__ */ jsx37(RestingStandingsCard, { card, onTap });
1697
1790
  case "restingSeason":
1698
- return /* @__PURE__ */ jsx34(RestingSeasonCard, { card, onTap });
1791
+ return /* @__PURE__ */ jsx37(RestingSeasonCard, { card, onTap });
1699
1792
  case "restingForm":
1700
- return /* @__PURE__ */ jsx34(RestingFormCard, { card });
1793
+ return /* @__PURE__ */ jsx37(RestingFormCard, { card });
1794
+ case "liveScore":
1795
+ return /* @__PURE__ */ jsx37(LiveScoreCard, { card });
1796
+ case "liveTimeline":
1797
+ return /* @__PURE__ */ jsx37(LiveTimelineCard, { card });
1798
+ case "liveStats":
1799
+ return /* @__PURE__ */ jsx37(LiveStatsCard, { card });
1701
1800
  default:
1702
1801
  return null;
1703
1802
  }
1704
1803
  }
1705
1804
 
1706
1805
  // src/components/ChatMessage.tsx
1707
- import { jsx as jsx35, jsxs as jsxs31 } from "react/jsx-runtime";
1806
+ import { jsx as jsx38, jsxs as jsxs34 } from "react/jsx-runtime";
1708
1807
  function ChatMessage({ message, disabled, onAct, onTap, ui, slip }) {
1709
1808
  if (message.role === "user") {
1710
- return /* @__PURE__ */ jsx35("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx35("div", { className: "ss-w-bubble", children: message.text }) });
1809
+ return /* @__PURE__ */ jsx38("div", { className: "ss-w-msg ss-w-user", "data-msg-id": message.id, children: /* @__PURE__ */ jsx38("div", { className: "ss-w-bubble", children: message.text }) });
1711
1810
  }
1712
1811
  const onFanDashboardTap = (card) => {
1713
1812
  onAct({ message: card.actionLabel, actionRef: card.actionRef, label: card.actionLabel || card.title || "" });
1714
1813
  };
1715
- return /* @__PURE__ */ jsxs31("div", { className: "ss-w-msg ss-w-bot", children: [
1716
- message.text && !message.greetingDashboard && !message.clubFanDashboard ? /* @__PURE__ */ jsx35(Markdown, { text: message.text }) : null,
1717
- message.greetingDashboard && !message.clubFanDashboard ? /* @__PURE__ */ jsx35(GreetingDashboard, { dashboard: message.greetingDashboard, onTap }) : null,
1718
- message.clubFanDashboard ? /* @__PURE__ */ jsx35(ClubFanDashboard, { dashboard: message.clubFanDashboard, onTap: onFanDashboardTap, ui }) : null,
1719
- /* @__PURE__ */ jsx35(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1720
- /* @__PURE__ */ jsx35(BettingInsightList, { insights: message.insights, ui, slip }),
1721
- /* @__PURE__ */ jsx35(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1722
- /* @__PURE__ */ jsx35(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1723
- /* @__PURE__ */ jsx35(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1814
+ return /* @__PURE__ */ jsxs34("div", { className: "ss-w-msg ss-w-bot", "data-msg-id": message.id, children: [
1815
+ message.text && !message.greetingDashboard && !message.clubFanDashboard ? /* @__PURE__ */ jsx38(Markdown, { text: message.text }) : null,
1816
+ message.greetingDashboard && !message.clubFanDashboard ? /* @__PURE__ */ jsx38(GreetingDashboard, { dashboard: message.greetingDashboard, onTap }) : null,
1817
+ message.clubFanDashboard ? /* @__PURE__ */ jsx38(ClubFanDashboard, { dashboard: message.clubFanDashboard, onTap: onFanDashboardTap, ui }) : null,
1818
+ /* @__PURE__ */ jsx38(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
1819
+ /* @__PURE__ */ jsx38(BettingInsightList, { insights: message.insights, ui, slip }),
1820
+ /* @__PURE__ */ jsx38(MarketOddsCardList, { cards: message.marketOdds, ui, slip }),
1821
+ /* @__PURE__ */ jsx38(OddsMovementCardList, { cards: message.oddsMovement, ui }),
1822
+ /* @__PURE__ */ jsx38(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
1724
1823
  ] });
1725
1824
  }
1726
1825
 
1727
1826
  // src/components/IntentChips.tsx
1728
- import { jsx as jsx36 } from "react/jsx-runtime";
1827
+ import { jsx as jsx39 } from "react/jsx-runtime";
1729
1828
  function IntentChips({ chips, onPick }) {
1730
1829
  if (chips.length === 0) return null;
1731
- return /* @__PURE__ */ jsx36("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx36("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1830
+ return /* @__PURE__ */ jsx39("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx39("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
1732
1831
  }
1733
1832
 
1734
1833
  // src/scroll.ts
@@ -1738,18 +1837,32 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
1738
1837
  }
1739
1838
 
1740
1839
  // src/components/MessageList.tsx
1741
- import { jsx as jsx37, jsxs as jsxs32 } from "react/jsx-runtime";
1840
+ import { jsx as jsx40, jsxs as jsxs35 } from "react/jsx-runtime";
1841
+ function turnAnchor(messages) {
1842
+ const last = messages[messages.length - 1];
1843
+ if (!last || last.role !== "assistant") return null;
1844
+ const prev = messages[messages.length - 2];
1845
+ return { turnId: last.id, anchorId: prev && prev.role === "user" ? prev.id : last.id };
1846
+ }
1742
1847
  function MessageList({ controller, ui, slip, onDashboardTap }) {
1743
1848
  const { state, send } = controller;
1744
- const listRef = useRef2(null);
1745
- const endRef = useRef2(null);
1746
- const pinnedRef = useRef2(true);
1747
- useEffect4(() => {
1849
+ const listRef = useRef3(null);
1850
+ const endRef = useRef3(null);
1851
+ const pinnedRef = useRef3(true);
1852
+ const anchoredTurnRef = useRef3(null);
1853
+ useEffect5(() => {
1854
+ const anchor = turnAnchor(state.messages);
1855
+ if (anchor && anchor.turnId !== anchoredTurnRef.current) {
1856
+ anchoredTurnRef.current = anchor.turnId;
1857
+ pinnedRef.current = false;
1858
+ listRef.current?.querySelector(`[data-msg-id="${anchor.anchorId}"]`)?.scrollIntoView({ block: "start" });
1859
+ return;
1860
+ }
1748
1861
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
1749
1862
  }, [state.messages, state.status]);
1750
1863
  const hasUserMessage = state.messages.some((m) => m.role === "user");
1751
1864
  const hasDashboard = state.messages.some((m) => m.role === "assistant" && (m.greetingDashboard || m.clubFanDashboard));
1752
- return /* @__PURE__ */ jsxs32(
1865
+ return /* @__PURE__ */ jsxs35(
1753
1866
  "div",
1754
1867
  {
1755
1868
  className: "ss-w-list",
@@ -1759,7 +1872,7 @@ function MessageList({ controller, ui, slip, onDashboardTap }) {
1759
1872
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
1760
1873
  },
1761
1874
  children: [
1762
- state.messages.map((m) => /* @__PURE__ */ jsx37(
1875
+ state.messages.map((m) => /* @__PURE__ */ jsx40(
1763
1876
  ChatMessage,
1764
1877
  {
1765
1878
  message: m,
@@ -1774,15 +1887,15 @@ function MessageList({ controller, ui, slip, onDashboardTap }) {
1774
1887
  },
1775
1888
  m.id
1776
1889
  )),
1777
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx37(ActionButtons, { actions: [], loading: true, onAct: () => {
1890
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx40(ActionButtons, { actions: [], loading: true, onAct: () => {
1778
1891
  } }) : null,
1779
- state.status ? /* @__PURE__ */ jsx37("div", { className: "ss-w-status", children: state.status }) : null,
1780
- state.error ? /* @__PURE__ */ jsxs32("div", { className: "ss-w-error", role: "alert", children: [
1781
- /* @__PURE__ */ jsx37("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1782
- /* @__PURE__ */ jsx37("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1892
+ state.status ? /* @__PURE__ */ jsx40("div", { className: "ss-w-status", children: state.status }) : null,
1893
+ state.error ? /* @__PURE__ */ jsxs35("div", { className: "ss-w-error", role: "alert", children: [
1894
+ /* @__PURE__ */ jsx40("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
1895
+ /* @__PURE__ */ jsx40("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
1783
1896
  ] }) : null,
1784
- !hasUserMessage && !hasDashboard ? /* @__PURE__ */ jsx37(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1785
- /* @__PURE__ */ jsx37("div", { ref: endRef })
1897
+ !hasUserMessage && !hasDashboard ? /* @__PURE__ */ jsx40(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
1898
+ /* @__PURE__ */ jsx40("div", { ref: endRef })
1786
1899
  ]
1787
1900
  }
1788
1901
  );
@@ -1790,7 +1903,7 @@ function MessageList({ controller, ui, slip, onDashboardTap }) {
1790
1903
 
1791
1904
  // src/components/ChatInput.tsx
1792
1905
  import { useState as useState7 } from "react";
1793
- import { jsx as jsx38, jsxs as jsxs33 } from "react/jsx-runtime";
1906
+ import { jsx as jsx41, jsxs as jsxs36 } from "react/jsx-runtime";
1794
1907
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
1795
1908
  const [value, setValue] = useState7("");
1796
1909
  const submit = () => {
@@ -1805,18 +1918,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
1805
1918
  submit();
1806
1919
  }
1807
1920
  };
1808
- return /* @__PURE__ */ jsxs33("div", { className: "ss-w-input", children: [
1809
- /* @__PURE__ */ jsx38("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1810
- streaming ? /* @__PURE__ */ jsx38("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx38("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx38("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx38(SendIcon, {}) })
1921
+ return /* @__PURE__ */ jsxs36("div", { className: "ss-w-input", children: [
1922
+ /* @__PURE__ */ jsx41("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
1923
+ streaming ? /* @__PURE__ */ jsx41("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx41("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx41("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx41(SendIcon, {}) })
1811
1924
  ] });
1812
1925
  }
1813
1926
 
1814
1927
  // src/components/ChatPanel.tsx
1815
- import { jsx as jsx39, jsxs as jsxs34 } from "react/jsx-runtime";
1928
+ import { jsx as jsx42, jsxs as jsxs37 } from "react/jsx-runtime";
1816
1929
  var FULLSCREEN_QUERY = "(max-width: 639px)";
1817
1930
  function useIsModal() {
1818
1931
  const [modal, setModal] = useState8(false);
1819
- useEffect5(() => {
1932
+ useEffect6(() => {
1820
1933
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
1821
1934
  const mql = window.matchMedia(FULLSCREEN_QUERY);
1822
1935
  const sync = () => setModal(mql.matches);
@@ -1829,17 +1942,17 @@ function useIsModal() {
1829
1942
  function ChatPanel({ controller, ui, slip, onDashboardTap }) {
1830
1943
  const { state, close, stop, send } = controller;
1831
1944
  const isModal = useIsModal();
1832
- useEffect5(() => {
1945
+ useEffect6(() => {
1833
1946
  const onKey = (e) => {
1834
1947
  if (e.key === "Escape") close();
1835
1948
  };
1836
1949
  window.addEventListener("keydown", onKey);
1837
1950
  return () => window.removeEventListener("keydown", onKey);
1838
1951
  }, [close]);
1839
- return /* @__PURE__ */ jsxs34("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1840
- /* @__PURE__ */ jsx39(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1841
- /* @__PURE__ */ jsx39(MessageList, { controller, ui, slip, onDashboardTap }),
1842
- /* @__PURE__ */ jsx39(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1952
+ return /* @__PURE__ */ jsxs37("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
1953
+ /* @__PURE__ */ jsx42(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
1954
+ /* @__PURE__ */ jsx42(MessageList, { controller, ui, slip, onDashboardTap }),
1955
+ /* @__PURE__ */ jsx42(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
1843
1956
  ] });
1844
1957
  }
1845
1958
 
@@ -1903,7 +2016,7 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
1903
2016
  .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; }
1904
2017
 
1905
2018
  /* messages */
1906
- .ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
2019
+ .ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; scroll-padding-top: 14px; }
1907
2020
  .ss-w-msg { display: flex; }
1908
2021
  .ss-w-user { justify-content: flex-end; }
1909
2022
  .ss-w-user .ss-w-bubble { background: color-mix(in srgb, var(--ss-w-accent) 14%, var(--ss-w-panel)); border: 1px solid color-mix(in srgb, var(--ss-w-accent) 30%, var(--ss-w-line)); border-radius: 14px; padding: 8px 12px; max-width: 80%; }
@@ -2351,10 +2464,35 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
2351
2464
 
2352
2465
  /* resting form: reuses the fixture last-five pill classes (ss-w-fandash-form / -form-pill) */
2353
2466
  .ss-w-fandash-resting-form .ss-w-fandash-form { margin-top: 2px; }
2467
+
2468
+ /* live score: reuses the post-match scoreboard classes for the team/score row, with its own
2469
+ meta row -- minute called out in the accent color, status and competition beside it */
2470
+ .ss-w-fandash-live-meta { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 11.5px; color: var(--ss-w-faint); }
2471
+ .ss-w-fandash-live-minute { flex: none; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ss-w-accent); }
2472
+ .ss-w-fandash-live-status { font-weight: 650; color: var(--ss-w-ink); }
2473
+ .ss-w-fandash-live-competition { color: var(--ss-w-faint); }
2474
+
2475
+ /* live timeline: a plain event list, the club's own events picked out in the accent color --
2476
+ same mechanism as the post-match goal list, plus a minute column */
2477
+ .ss-w-fandash-live-event-list { list-style: none; display: flex; flex-direction: column; gap: 4px; }
2478
+ .ss-w-fandash-live-event { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; color: var(--ss-w-faint); }
2479
+ .ss-w-fandash-live-event--club { color: var(--ss-w-ink); font-weight: 650; }
2480
+ .ss-w-fandash-live-event-minute { flex: none; font-variant-numeric: tabular-nums; color: var(--ss-w-faint); }
2481
+ .ss-w-fandash-live-event--club .ss-w-fandash-live-event-minute { color: var(--ss-w-accent); }
2482
+ .ss-w-fandash-live-event-text { min-width: 0; }
2483
+
2484
+ /* live stats: club value / stat label / opponent value, same 3-column layout as post-match
2485
+ stats, plus an optional momentum line underneath */
2486
+ .ss-w-fandash-live-stat-rows { display: flex; flex-direction: column; gap: 5px; }
2487
+ .ss-w-fandash-live-stat-row { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; }
2488
+ .ss-w-fandash-live-stat-value { font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ss-w-ink); }
2489
+ .ss-w-fandash-live-stat-row .ss-w-fandash-live-stat-value:last-child { text-align: right; }
2490
+ .ss-w-fandash-live-stat-label { font-size: 10.5px; text-align: center; color: var(--ss-w-faint); text-transform: uppercase; letter-spacing: .03em; }
2491
+ .ss-w-fandash-live-momentum { font-size: 11.5px; font-weight: 650; color: var(--ss-w-accent); }
2354
2492
  `;
2355
2493
 
2356
2494
  // src/StatsWidget.tsx
2357
- import { jsx as jsx40 } from "react/jsx-runtime";
2495
+ import { jsx as jsx43 } from "react/jsx-runtime";
2358
2496
  function buildClient(props) {
2359
2497
  try {
2360
2498
  if ("client" in props && props.client) {
@@ -2381,7 +2519,7 @@ function StatsWidget(props) {
2381
2519
  const cfg = props.config;
2382
2520
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
2383
2521
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
2384
- const rootRef = useRef3(null);
2522
+ const rootRef = useRef4(null);
2385
2523
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
2386
2524
  const slip = useMemo2(
2387
2525
  () => ({
@@ -2400,14 +2538,14 @@ function StatsWidget(props) {
2400
2538
  () => built ? createDashboardTapSignal((input) => built.conversation.sendSignal(input)) : void 0,
2401
2539
  [built]
2402
2540
  );
2403
- useEffect6(() => {
2541
+ useEffect7(() => {
2404
2542
  if (!built || !ui.injectStyles) return;
2405
2543
  const node = rootRef.current?.getRootNode();
2406
2544
  if (node) injectStyles(node);
2407
2545
  }, [built, ui.injectStyles]);
2408
2546
  if (!built) return null;
2409
2547
  const attrs = themeAttrs(ui);
2410
- return /* @__PURE__ */ jsx40("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx40(ChatPanel, { controller, ui, slip, onDashboardTap }) : /* @__PURE__ */ jsx40(
2548
+ return /* @__PURE__ */ jsx43("div", { ref: rootRef, className: "ss-w-root", "data-ss-w-theme": attrs["data-ss-w-theme"], "data-ss-w-pos": attrs["data-ss-w-pos"], style: attrs.style, children: controller.state.isOpen ? /* @__PURE__ */ jsx43(ChatPanel, { controller, ui, slip, onDashboardTap }) : /* @__PURE__ */ jsx43(
2411
2549
  ChatFab,
2412
2550
  {
2413
2551
  label: ui.launcherLabel,