@sensiblestats/widget-react 0.6.1 → 0.7.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
@@ -1,5 +1,5 @@
1
1
  // src/StatsWidget.tsx
2
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3 } from "react";
2
+ import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef3 } from "react";
3
3
  import { StatsWidgetClient } from "@sensiblestats/widget-sdk";
4
4
 
5
5
  // src/i18n.ts
@@ -17,7 +17,16 @@ var STRINGS = {
17
17
  showLess: "Show less",
18
18
  showMore: (n) => `Show ${n} more`,
19
19
  oddsLabel: "Odds",
20
- liveLabel: "Live"
20
+ liveLabel: "Live",
21
+ addToSlip: "Add to slip",
22
+ inSlip: "In slip",
23
+ slipAdded: (n) => n === void 0 ? "Added to slip" : `Added to slip (${n} selection${n === 1 ? "" : "s"})`,
24
+ slipDuplicate: "Already in your slip",
25
+ slipPriceChanged: (from, to) => `Added at ${to} (was ${from})`,
26
+ slipPriceNotAccepted: (from, to) => `Price moved to ${to} (was ${from}). Tap to add.`,
27
+ slipLoginRequired: "Sign in to add to your slip",
28
+ slipSuspended: "Market suspended",
29
+ slipFailed: "Couldn't add to slip. Try again."
21
30
  },
22
31
  tr: {
23
32
  launcherLabel: "SensibleStats ile sohbet et",
@@ -32,7 +41,16 @@ var STRINGS = {
32
41
  showLess: "Daha az g\xF6ster",
33
42
  showMore: (n) => `${n} tane daha g\xF6ster`,
34
43
  oddsLabel: "Oran",
35
- liveLabel: "Canl\u0131"
44
+ liveLabel: "Canl\u0131",
45
+ addToSlip: "Kupona ekle",
46
+ inSlip: "Kuponda",
47
+ slipAdded: (n) => n === void 0 ? "Kupona eklendi" : `Kupona eklendi (${n} se\xE7im)`,
48
+ slipDuplicate: "Bu se\xE7im kuponunuzda zaten var",
49
+ slipPriceChanged: (from, to) => `${to} oran\u0131yla eklendi (\xF6nceki ${from})`,
50
+ slipPriceNotAccepted: (from, to) => `Oran ${to} oldu (\xF6nceki ${from}). Eklemek i\xE7in dokunun.`,
51
+ slipLoginRequired: "Kupona eklemek i\xE7in giri\u015F yap\u0131n",
52
+ slipSuspended: "Bu market ask\u0131ya al\u0131nd\u0131",
53
+ slipFailed: "Kupona eklenemedi. Tekrar deneyin."
36
54
  }
37
55
  };
38
56
  function resolveLocaleStrings(locale) {
@@ -64,7 +82,16 @@ function normalizeUi(cfg) {
64
82
  showLess: strings.showLess,
65
83
  showMore: strings.showMore,
66
84
  oddsLabel: strings.oddsLabel,
67
- liveLabel: strings.liveLabel
85
+ liveLabel: strings.liveLabel,
86
+ addToSlip: strings.addToSlip,
87
+ inSlip: strings.inSlip,
88
+ slipAdded: strings.slipAdded,
89
+ slipDuplicate: strings.slipDuplicate,
90
+ slipPriceChanged: strings.slipPriceChanged,
91
+ slipPriceNotAccepted: strings.slipPriceNotAccepted,
92
+ slipLoginRequired: strings.slipLoginRequired,
93
+ slipSuspended: strings.slipSuspended,
94
+ slipFailed: strings.slipFailed
68
95
  };
69
96
  }
70
97
  function toClientOptions(cfg) {
@@ -144,6 +171,8 @@ function toInsightVM(insight) {
144
171
  const selection = [str(insight.selectionLabel), str(insight.line)].filter(Boolean).join(" ") || void 0;
145
172
  const value = insight.odds?.value;
146
173
  const odds = typeof value === "number" && Number.isFinite(value) ? value.toFixed(2) : void 0;
174
+ const oddsValue = insight.odds?.value;
175
+ const matchOddsId = typeof insight.odds?.matchOddsId === "number" ? insight.odds.matchOddsId : void 0;
147
176
  const home = str(insight.homeTeamName);
148
177
  const away = str(insight.awayTeamName);
149
178
  const fixture = home && away ? `${home} vs ${away}` : void 0;
@@ -163,14 +192,31 @@ function toInsightVM(insight) {
163
192
  scopes,
164
193
  text: str(insight.text) ?? "",
165
194
  stats: readStatCells(insight.stats),
166
- disclaimer: str(insight.disclaimer)
195
+ disclaimer: str(insight.disclaimer),
196
+ matchId: typeof insight.matchId === "number" ? insight.matchId : 0,
197
+ matchOddsId,
198
+ addToSlipEnabled: insight.addToSlipEnabled === true,
199
+ oddsDecimal: typeof oddsValue === "number" && Number.isFinite(oddsValue) ? oddsValue : void 0,
200
+ quotedAtUtc: str(insight.odds?.quotedAtUtc)
167
201
  };
168
202
  }
203
+ function toSlipSelection(vm, conversationId) {
204
+ if (vm.matchOddsId === void 0 || vm.oddsDecimal === void 0) return null;
205
+ const selection = {
206
+ matchOddsId: vm.matchOddsId,
207
+ matchId: vm.matchId,
208
+ oddsDecimal: vm.oddsDecimal
209
+ };
210
+ if (vm.quotedAtUtc !== void 0) selection.quotedAtUtc = vm.quotedAtUtc;
211
+ if (vm.id !== void 0) selection.insightId = vm.id;
212
+ if (conversationId !== void 0) selection.conversationId = conversationId;
213
+ return selection;
214
+ }
169
215
 
170
216
  // src/reducer.ts
171
217
  var POPUP_MAX = 160;
172
218
  function initialState(starters) {
173
- return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null };
219
+ return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null, conversationId: null };
174
220
  }
175
221
  function activeTurn(state) {
176
222
  const last = state.messages[state.messages.length - 1];
@@ -207,7 +253,15 @@ function applyEvent(state, event) {
207
253
  const active = activeTurn(state);
208
254
  const text = active?.text.trim() ?? "";
209
255
  const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
210
- return { ...state, isStreaming: false, status: null, actionsLoading: false, popupText, messages: mapActive(state, (t) => ({ ...t, done: true })) };
256
+ return {
257
+ ...state,
258
+ isStreaming: false,
259
+ status: null,
260
+ actionsLoading: false,
261
+ popupText,
262
+ conversationId: event.data.conversationId ?? state.conversationId,
263
+ messages: mapActive(state, (t) => ({ ...t, done: true }))
264
+ };
211
265
  }
212
266
  case "error": {
213
267
  const suppress = activeTurn(state)?.isGreeting === true;
@@ -350,7 +404,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
350
404
  }
351
405
 
352
406
  // src/components/ChatPanel.tsx
353
- import { useEffect as useEffect3, useState as useState4 } from "react";
407
+ import { useEffect as useEffect4, useState as useState5 } from "react";
354
408
 
355
409
  // src/components/ChatHeader.tsx
356
410
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -363,7 +417,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
363
417
  }
364
418
 
365
419
  // src/components/MessageList.tsx
366
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
420
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
367
421
 
368
422
  // src/components/Markdown.tsx
369
423
  import ReactMarkdown from "react-markdown";
@@ -417,94 +471,194 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
417
471
  }
418
472
 
419
473
  // src/components/BettingInsightList.tsx
420
- import { useState as useState2 } from "react";
421
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
474
+ import { useState as useState3 } from "react";
475
+
476
+ // src/components/AddToSlipButton.tsx
477
+ import { useCallback as useCallback2, useState as useState2 } from "react";
478
+ import { isAddToSlipResult } from "@sensiblestats/widget-sdk";
479
+
480
+ // src/slip.ts
481
+ var ADD_TO_SLIP_TIMEOUT_MS = 1e4;
482
+ function wasAdded(result) {
483
+ if (!result) return true;
484
+ if (result.status === "price_changed") return result.accepted;
485
+ return result.status === "added";
486
+ }
487
+ function isTerminal(result) {
488
+ if (!result) return true;
489
+ if (result.status === "price_changed") return result.accepted;
490
+ return result.status === "added" || result.status === "duplicate";
491
+ }
492
+ function resultToToast(result, shownOdds, ui) {
493
+ if (!result) return { kind: "success", text: ui.slipAdded(void 0) };
494
+ switch (result.status) {
495
+ case "added":
496
+ return { kind: "success", text: ui.slipAdded(result.slip?.selectionCount) };
497
+ case "duplicate":
498
+ return { kind: "success", text: ui.slipDuplicate };
499
+ case "price_changed":
500
+ return result.accepted ? { kind: "success", text: ui.slipPriceChanged(shownOdds, result.newOddsDecimal.toFixed(2)) } : { kind: "error", text: ui.slipPriceNotAccepted(shownOdds, result.newOddsDecimal.toFixed(2)) };
501
+ case "rejected":
502
+ if (result.reason === "login_required") return { kind: "error", text: ui.slipLoginRequired };
503
+ if (result.reason === "suspended") return { kind: "error", text: ui.slipSuspended };
504
+ return { kind: "error", text: result.message ?? ui.slipFailed };
505
+ }
506
+ }
507
+
508
+ // src/components/Toast.tsx
509
+ import { useEffect as useEffect2 } from "react";
510
+ import { jsx as jsx7 } from "react/jsx-runtime";
511
+ var DISMISS_MS = 4e3;
512
+ function Toast({ toast, onDismiss }) {
513
+ useEffect2(() => {
514
+ if (!toast) return;
515
+ const timer = setTimeout(onDismiss, DISMISS_MS);
516
+ return () => clearTimeout(timer);
517
+ }, [toast, onDismiss]);
518
+ if (!toast) return null;
519
+ return /* @__PURE__ */ jsx7("div", { className: `ss-w-toast ss-w-toast-${toast.kind}`, role: "status", "aria-live": "polite", children: toast.text });
520
+ }
521
+
522
+ // src/components/AddToSlipButton.tsx
523
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
524
+ function withTimeout(value) {
525
+ return Promise.race([
526
+ value,
527
+ new Promise((_, reject) => setTimeout(() => reject(new Error("add-to-slip timed out")), ADD_TO_SLIP_TIMEOUT_MS))
528
+ ]);
529
+ }
530
+ function AddToSlipButton({ it, slip, ui }) {
531
+ const [pending, setPending] = useState2(false);
532
+ const [settled, setSettled] = useState2(false);
533
+ const [toast, setToast] = useState2(null);
534
+ const dismiss = useCallback2(() => setToast(null), []);
535
+ const handler = slip.onAddToSlip;
536
+ const selection = it.addToSlipEnabled ? toSlipSelection(it, slip.conversationId) : null;
537
+ if (!ui || !handler || !selection) return null;
538
+ const onClick = async () => {
539
+ setPending(true);
540
+ try {
541
+ const raw = await withTimeout(Promise.resolve(handler(selection)));
542
+ if (raw != null && !isAddToSlipResult(raw)) throw new Error("malformed add-to-slip result");
543
+ const result = raw ?? void 0;
544
+ setToast(resultToToast(result, it.odds ?? selection.oddsDecimal.toFixed(2), ui));
545
+ setSettled(isTerminal(result));
546
+ if (wasAdded(result) && slip.onMarketClicked) {
547
+ try {
548
+ slip.onMarketClicked(it.market);
549
+ } catch {
550
+ }
551
+ }
552
+ } catch {
553
+ setToast({ kind: "error", text: ui.slipFailed });
554
+ } finally {
555
+ setPending(false);
556
+ }
557
+ };
558
+ return /* @__PURE__ */ jsxs5("div", { className: "ss-w-slip", children: [
559
+ /* @__PURE__ */ jsx8(
560
+ "button",
561
+ {
562
+ type: "button",
563
+ className: "ss-w-slip-btn",
564
+ disabled: pending || settled,
565
+ onClick,
566
+ children: settled ? ui.inSlip : ui.addToSlip
567
+ }
568
+ ),
569
+ /* @__PURE__ */ jsx8(Toast, { toast, onDismiss: dismiss })
570
+ ] });
571
+ }
572
+
573
+ // src/components/BettingInsightList.tsx
574
+ import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
422
575
  var MAX_ROWS = 3;
423
576
  function StatRows({ stats, ui }) {
424
- const [expanded, setExpanded] = useState2(false);
577
+ const [expanded, setExpanded] = useState3(false);
425
578
  const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
426
579
  const overflow = stats.length - MAX_ROWS;
427
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-rows", children: [
428
- shown.map((s) => /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-row", children: [
429
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-row-lbl", children: s.label }),
430
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
431
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-row-num", children: s.value })
580
+ return /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-rows", children: [
581
+ shown.map((s) => /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-row", children: [
582
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-row-lbl", children: s.label }),
583
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
584
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-row-num", children: s.value })
432
585
  ] }, `${s.label}:${s.value}`)),
433
- overflow > 0 ? /* @__PURE__ */ jsx7("button", { type: "button", className: "ss-w-bi-more", onClick: () => setExpanded((v) => !v), children: expanded ? ui?.showLess ?? "Show less" : (ui?.showMore ?? ((n) => `Show ${n} more`))(overflow) }) : null
586
+ overflow > 0 ? /* @__PURE__ */ jsx9("button", { type: "button", className: "ss-w-bi-more", onClick: () => setExpanded((v) => !v), children: expanded ? ui?.showLess ?? "Show less" : (ui?.showMore ?? ((n) => `Show ${n} more`))(overflow) }) : null
434
587
  ] });
435
588
  }
436
589
  function ScopedStats({ scopes, ui }) {
437
- const [active, setActive] = useState2(0);
590
+ const [active, setActive] = useState3(0);
438
591
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
439
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-scoped", children: [
440
- /* @__PURE__ */ jsx7("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ jsx7("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
441
- /* @__PURE__ */ jsx7(StatRows, { stats: current.stats, ui }, current.key)
592
+ return /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-scoped", children: [
593
+ /* @__PURE__ */ jsx9("div", { className: "ss-w-bi-scopes", role: "group", children: scopes.map((s, i) => /* @__PURE__ */ jsx9("button", { type: "button", className: "ss-w-bi-chip", "aria-pressed": i === active, onClick: () => setActive(i), children: s.label }, s.key)) }),
594
+ /* @__PURE__ */ jsx9(StatRows, { stats: current.stats, ui }, current.key)
442
595
  ] });
443
596
  }
444
- function InsightCard({ it, ui }) {
597
+ function InsightCard({ it, ui, slip }) {
445
598
  const live = it.isLive || it.phase === "live";
446
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-binsight", children: [
447
- live || it.fixture || it.competition ? /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-status", children: [
448
- live ? /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-live", children: [
449
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
599
+ return /* @__PURE__ */ jsxs6("div", { className: "ss-w-binsight", children: [
600
+ live || it.fixture || it.competition ? /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-status", children: [
601
+ live ? /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-live", children: [
602
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-live-dot", "aria-hidden": "true" }),
450
603
  ui?.liveLabel ?? "Live"
451
604
  ] }) : null,
452
- it.fixture ? /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
453
- it.competition ? /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-comp", children: it.competition }) : null
605
+ it.fixture ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
606
+ it.competition ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-comp", children: it.competition }) : null
454
607
  ] }) : null,
455
- /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-head", children: [
456
- /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-mkt", children: [
457
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-market", children: it.market }),
458
- it.selection ? /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-sel", children: it.selection }) : null
608
+ /* @__PURE__ */ jsxs6("div", { className: "ss-w-bi-head", children: [
609
+ /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-mkt", children: [
610
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-market", children: it.market }),
611
+ it.selection ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-sel", children: it.selection }) : null
459
612
  ] }),
460
- it.odds ? /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-odds", children: [
461
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
462
- /* @__PURE__ */ jsx7("b", { children: it.odds }),
463
- it.bookmaker ? /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
613
+ it.odds ? /* @__PURE__ */ jsxs6("span", { className: "ss-w-bi-odds", children: [
614
+ /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
615
+ /* @__PURE__ */ jsx9("b", { children: it.odds }),
616
+ it.bookmaker ? /* @__PURE__ */ jsx9("span", { className: "ss-w-bi-book", children: it.bookmaker }) : null
464
617
  ] }) : null
465
618
  ] }),
466
- it.text ? /* @__PURE__ */ jsx7("p", { className: "ss-w-bi-text", children: it.text }) : null,
467
- it.scopes && it.scopes.length >= 2 ? /* @__PURE__ */ jsx7(ScopedStats, { scopes: it.scopes, ui }) : it.stats.length > 0 ? /* @__PURE__ */ jsx7(StatRows, { stats: it.stats, ui }) : null,
468
- it.disclaimer ? /* @__PURE__ */ jsx7("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null
619
+ it.text ? /* @__PURE__ */ jsx9("p", { className: "ss-w-bi-text", children: it.text }) : null,
620
+ it.scopes && it.scopes.length >= 2 ? /* @__PURE__ */ jsx9(ScopedStats, { scopes: it.scopes, ui }) : it.stats.length > 0 ? /* @__PURE__ */ jsx9(StatRows, { stats: it.stats, ui }) : null,
621
+ it.disclaimer ? /* @__PURE__ */ jsx9("p", { className: "ss-w-bi-disc", children: it.disclaimer }) : null,
622
+ /* @__PURE__ */ jsx9(AddToSlipButton, { it, slip, ui })
469
623
  ] });
470
624
  }
471
- function BettingInsightList({ insights, ui }) {
625
+ function BettingInsightList({ insights, ui, slip }) {
472
626
  if (!insights || insights.length === 0) return null;
473
- return /* @__PURE__ */ jsx7("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ jsx7(InsightCard, { it, ui }, it.id)) });
627
+ return /* @__PURE__ */ jsx9("div", { className: "ss-w-binsights", children: insights.map((it) => /* @__PURE__ */ jsx9(InsightCard, { it, ui, slip }, it.id)) });
474
628
  }
475
629
 
476
630
  // src/components/ActionButtons.tsx
477
- import { Fragment, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
631
+ import { Fragment, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
478
632
  function ActionButtons({ actions, loading, disabled, onAct }) {
479
633
  if (actions.length === 0 && !loading) return null;
480
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-actions", children: [
481
- actions.map((a, i) => /* @__PURE__ */ jsx8("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
482
- actions.length === 0 && loading ? /* @__PURE__ */ jsxs6(Fragment, { children: [
483
- /* @__PURE__ */ jsx8("span", { className: "ss-w-abtn-shimmer" }),
484
- /* @__PURE__ */ jsx8("span", { className: "ss-w-abtn-shimmer" })
634
+ return /* @__PURE__ */ jsxs7("div", { className: "ss-w-actions", children: [
635
+ actions.map((a, i) => /* @__PURE__ */ jsx10("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
636
+ actions.length === 0 && loading ? /* @__PURE__ */ jsxs7(Fragment, { children: [
637
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-abtn-shimmer" }),
638
+ /* @__PURE__ */ jsx10("span", { className: "ss-w-abtn-shimmer" })
485
639
  ] }) : null
486
640
  ] });
487
641
  }
488
642
 
489
643
  // src/components/ChatMessage.tsx
490
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
491
- function ChatMessage({ message, disabled, onAct, ui }) {
644
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
645
+ function ChatMessage({ message, disabled, onAct, ui, slip }) {
492
646
  if (message.role === "user") {
493
- return /* @__PURE__ */ jsx9("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx9("div", { className: "ss-w-bubble", children: message.text }) });
647
+ return /* @__PURE__ */ jsx11("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx11("div", { className: "ss-w-bubble", children: message.text }) });
494
648
  }
495
- return /* @__PURE__ */ jsxs7("div", { className: "ss-w-msg ss-w-bot", children: [
496
- message.text ? /* @__PURE__ */ jsx9(Markdown, { text: message.text }) : null,
497
- /* @__PURE__ */ jsx9(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
498
- /* @__PURE__ */ jsx9(BettingInsightList, { insights: message.insights, ui }),
499
- /* @__PURE__ */ jsx9(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
649
+ return /* @__PURE__ */ jsxs8("div", { className: "ss-w-msg ss-w-bot", children: [
650
+ message.text ? /* @__PURE__ */ jsx11(Markdown, { text: message.text }) : null,
651
+ /* @__PURE__ */ jsx11(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
652
+ /* @__PURE__ */ jsx11(BettingInsightList, { insights: message.insights, ui, slip }),
653
+ /* @__PURE__ */ jsx11(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
500
654
  ] });
501
655
  }
502
656
 
503
657
  // src/components/IntentChips.tsx
504
- import { jsx as jsx10 } from "react/jsx-runtime";
658
+ import { jsx as jsx12 } from "react/jsx-runtime";
505
659
  function IntentChips({ chips, onPick }) {
506
660
  if (chips.length === 0) return null;
507
- return /* @__PURE__ */ jsx10("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx10("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
661
+ return /* @__PURE__ */ jsx12("div", { className: "ss-w-chips", children: chips.map((c, i) => /* @__PURE__ */ jsx12("button", { type: "button", className: "ss-w-chip", onClick: () => onPick(c.text), children: c.text }, `${c.text}-${i}`)) });
508
662
  }
509
663
 
510
664
  // src/scroll.ts
@@ -514,17 +668,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
514
668
  }
515
669
 
516
670
  // src/components/MessageList.tsx
517
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
518
- function MessageList({ controller, ui }) {
671
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
672
+ function MessageList({ controller, ui, slip }) {
519
673
  const { state, send } = controller;
520
674
  const listRef = useRef2(null);
521
675
  const endRef = useRef2(null);
522
676
  const pinnedRef = useRef2(true);
523
- useEffect2(() => {
677
+ useEffect3(() => {
524
678
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
525
679
  }, [state.messages, state.status]);
526
680
  const empty = state.messages.length === 0;
527
- return /* @__PURE__ */ jsxs8(
681
+ return /* @__PURE__ */ jsxs9(
528
682
  "div",
529
683
  {
530
684
  className: "ss-w-list",
@@ -534,35 +688,36 @@ function MessageList({ controller, ui }) {
534
688
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
535
689
  },
536
690
  children: [
537
- state.messages.map((m) => /* @__PURE__ */ jsx11(
691
+ state.messages.map((m) => /* @__PURE__ */ jsx13(
538
692
  ChatMessage,
539
693
  {
540
694
  message: m,
541
695
  disabled: state.isStreaming,
542
696
  onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
543
- ui
697
+ ui,
698
+ slip
544
699
  },
545
700
  m.id
546
701
  )),
547
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx11(ActionButtons, { actions: [], loading: true, onAct: () => {
702
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx13(ActionButtons, { actions: [], loading: true, onAct: () => {
548
703
  } }) : null,
549
- state.status ? /* @__PURE__ */ jsx11("div", { className: "ss-w-status", children: state.status }) : null,
550
- state.error ? /* @__PURE__ */ jsxs8("div", { className: "ss-w-error", role: "alert", children: [
551
- /* @__PURE__ */ jsx11("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
552
- /* @__PURE__ */ jsx11("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
704
+ state.status ? /* @__PURE__ */ jsx13("div", { className: "ss-w-status", children: state.status }) : null,
705
+ state.error ? /* @__PURE__ */ jsxs9("div", { className: "ss-w-error", role: "alert", children: [
706
+ /* @__PURE__ */ jsx13("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
707
+ /* @__PURE__ */ jsx13("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: ui.retry })
553
708
  ] }) : null,
554
- empty ? /* @__PURE__ */ jsx11(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
555
- /* @__PURE__ */ jsx11("div", { ref: endRef })
709
+ empty ? /* @__PURE__ */ jsx13(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
710
+ /* @__PURE__ */ jsx13("div", { ref: endRef })
556
711
  ]
557
712
  }
558
713
  );
559
714
  }
560
715
 
561
716
  // src/components/ChatInput.tsx
562
- import { useState as useState3 } from "react";
563
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
717
+ import { useState as useState4 } from "react";
718
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
564
719
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
565
- const [value, setValue] = useState3("");
720
+ const [value, setValue] = useState4("");
566
721
  const submit = () => {
567
722
  const t = value.trim();
568
723
  if (!t) return;
@@ -575,18 +730,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
575
730
  submit();
576
731
  }
577
732
  };
578
- return /* @__PURE__ */ jsxs9("div", { className: "ss-w-input", children: [
579
- /* @__PURE__ */ jsx12("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
580
- streaming ? /* @__PURE__ */ jsx12("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx12("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx12("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx12(SendIcon, {}) })
733
+ return /* @__PURE__ */ jsxs10("div", { className: "ss-w-input", children: [
734
+ /* @__PURE__ */ jsx14("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
735
+ streaming ? /* @__PURE__ */ jsx14("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": stopLabel, onClick: onStop, children: /* @__PURE__ */ jsx14("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx14("button", { type: "button", className: "ss-w-send", "aria-label": sendLabel, onClick: submit, children: /* @__PURE__ */ jsx14(SendIcon, {}) })
581
736
  ] });
582
737
  }
583
738
 
584
739
  // src/components/ChatPanel.tsx
585
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
740
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
586
741
  var FULLSCREEN_QUERY = "(max-width: 639px)";
587
742
  function useIsModal() {
588
- const [modal, setModal] = useState4(false);
589
- useEffect3(() => {
743
+ const [modal, setModal] = useState5(false);
744
+ useEffect4(() => {
590
745
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
591
746
  const mql = window.matchMedia(FULLSCREEN_QUERY);
592
747
  const sync = () => setModal(mql.matches);
@@ -596,20 +751,20 @@ function useIsModal() {
596
751
  }, []);
597
752
  return modal;
598
753
  }
599
- function ChatPanel({ controller, ui }) {
754
+ function ChatPanel({ controller, ui, slip }) {
600
755
  const { state, close, stop, send } = controller;
601
756
  const isModal = useIsModal();
602
- useEffect3(() => {
757
+ useEffect4(() => {
603
758
  const onKey = (e) => {
604
759
  if (e.key === "Escape") close();
605
760
  };
606
761
  window.addEventListener("keydown", onKey);
607
762
  return () => window.removeEventListener("keydown", onKey);
608
763
  }, [close]);
609
- return /* @__PURE__ */ jsxs10("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
610
- /* @__PURE__ */ jsx13(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
611
- /* @__PURE__ */ jsx13(MessageList, { controller, ui }),
612
- /* @__PURE__ */ jsx13(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
764
+ return /* @__PURE__ */ jsxs11("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
765
+ /* @__PURE__ */ jsx15(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
766
+ /* @__PURE__ */ jsx15(MessageList, { controller, ui, slip }),
767
+ /* @__PURE__ */ jsx15(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop, stopLabel: ui.stopResponse, sendLabel: ui.sendMessage })
613
768
  ] });
614
769
  }
615
770
 
@@ -762,13 +917,21 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
762
917
  .ss-w-send { width: 36px; height: 36px; flex: none; border: none; border-radius: 50%; background: var(--ss-w-accent); color: #fff; cursor: pointer; display: grid; place-items: center; font-size: 16px; }
763
918
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
764
919
 
920
+ /* add to slip */
921
+ .ss-w-slip { position: relative; display: flex; }
922
+ .ss-w-slip-btn { font-size: 11.5px; font-weight: 620; color: #fff; background: var(--ss-w-accent); border: 1px solid var(--ss-w-accent); border-radius: 14px; padding: 5px 12px; cursor: pointer; }
923
+ .ss-w-slip-btn:disabled { opacity: .6; cursor: default; background: var(--ss-w-bubble); color: var(--ss-w-faint); border-color: var(--ss-w-line); }
924
+ .ss-w-toast { position: absolute; top: calc(100% + 6px); left: 0; z-index: 1; max-width: 100%; font-size: 11px; font-weight: 560; border-radius: 10px; padding: 5px 10px; overflow-wrap: anywhere; box-shadow: 0 4px 14px rgba(0,0,0,.14); }
925
+ .ss-w-toast-success { color: var(--ss-w-accent); 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)); }
926
+ .ss-w-toast-error { color: #c0392b; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); }
927
+
765
928
  @media (prefers-reduced-motion: reduce) {
766
929
  .ss-w-abtn-shimmer { animation: none; }
767
930
  }
768
931
  `;
769
932
 
770
933
  // src/StatsWidget.tsx
771
- import { jsx as jsx14 } from "react/jsx-runtime";
934
+ import { jsx as jsx16 } from "react/jsx-runtime";
772
935
  function buildClient(props) {
773
936
  try {
774
937
  if ("client" in props && props.client) {
@@ -797,14 +960,27 @@ function StatsWidget(props) {
797
960
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
798
961
  const rootRef = useRef3(null);
799
962
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
800
- useEffect4(() => {
963
+ const slip = useMemo2(
964
+ () => ({
965
+ onAddToSlip: props.config?.onAddToSlip,
966
+ conversationId: controller.state.conversationId ?? void 0,
967
+ onMarketClicked: (marketName) => {
968
+ try {
969
+ void built?.conversation.sendSignal({ signalType: "InsightMarketClicked", entityType: "Market", entityName: marketName }).catch(() => void 0);
970
+ } catch {
971
+ }
972
+ }
973
+ }),
974
+ [props.config?.onAddToSlip, controller.state.conversationId, built]
975
+ );
976
+ useEffect5(() => {
801
977
  if (!built || !ui.injectStyles) return;
802
978
  const node = rootRef.current?.getRootNode();
803
979
  if (node) injectStyles(node);
804
980
  }, [built, ui.injectStyles]);
805
981
  if (!built) return null;
806
982
  const attrs = themeAttrs(ui);
807
- return /* @__PURE__ */ jsx14("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__ */ jsx14(ChatPanel, { controller, ui }) : /* @__PURE__ */ jsx14(
983
+ return /* @__PURE__ */ jsx16("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__ */ jsx16(ChatPanel, { controller, ui, slip }) : /* @__PURE__ */ jsx16(
808
984
  ChatFab,
809
985
  {
810
986
  label: ui.launcherLabel,