@sensiblestats/widget-react 0.6.0 → 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];
@@ -187,9 +233,12 @@ function applyEvent(state, event) {
187
233
  return { ...state, status: event.data.message };
188
234
  case "answer":
189
235
  return { ...state, messages: mapActive(state, (t) => ({ ...t, text: event.data.text })) };
190
- case "entity_card":
236
+ case "entity_card": {
191
237
  if (event.data.card.isUsed === false) return state;
192
- return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, toCardVM(event.data.card)] })) };
238
+ const card = toCardVM(event.data.card);
239
+ if (card.kind === "generic") return state;
240
+ return { ...state, messages: mapActive(state, (t) => ({ ...t, cards: [...t.cards, card] })) };
241
+ }
193
242
  case "action_button":
194
243
  return { ...state, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, actions: [...t.actions, toActionVM(event.data.action)] })) };
195
244
  case "betting_insight":
@@ -204,7 +253,15 @@ function applyEvent(state, event) {
204
253
  const active = activeTurn(state);
205
254
  const text = active?.text.trim() ?? "";
206
255
  const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
207
- 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
+ };
208
265
  }
209
266
  case "error": {
210
267
  const suppress = activeTurn(state)?.isGreeting === true;
@@ -347,7 +404,7 @@ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup, dismissLa
347
404
  }
348
405
 
349
406
  // src/components/ChatPanel.tsx
350
- import { useEffect as useEffect3, useState as useState4 } from "react";
407
+ import { useEffect as useEffect4, useState as useState5 } from "react";
351
408
 
352
409
  // src/components/ChatHeader.tsx
353
410
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
@@ -360,7 +417,7 @@ function ChatHeader({ onClose, closeLabel = "Close chat" }) {
360
417
  }
361
418
 
362
419
  // src/components/MessageList.tsx
363
- import { useEffect as useEffect2, useRef as useRef2 } from "react";
420
+ import { useEffect as useEffect3, useRef as useRef2 } from "react";
364
421
 
365
422
  // src/components/Markdown.tsx
366
423
  import ReactMarkdown from "react-markdown";
@@ -414,94 +471,194 @@ function EntityCardList({ cards, disabled, showLessLabel = "Show less", showMore
414
471
  }
415
472
 
416
473
  // src/components/BettingInsightList.tsx
417
- import { useState as useState2 } from "react";
418
- 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";
419
575
  var MAX_ROWS = 3;
420
576
  function StatRows({ stats, ui }) {
421
- const [expanded, setExpanded] = useState2(false);
577
+ const [expanded, setExpanded] = useState3(false);
422
578
  const shown = expanded ? stats : stats.slice(0, MAX_ROWS);
423
579
  const overflow = stats.length - MAX_ROWS;
424
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-rows", children: [
425
- shown.map((s) => /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-row", children: [
426
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-row-lbl", children: s.label }),
427
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-row-dots", "aria-hidden": "true" }),
428
- /* @__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 })
429
585
  ] }, `${s.label}:${s.value}`)),
430
- 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
431
587
  ] });
432
588
  }
433
589
  function ScopedStats({ scopes, ui }) {
434
- const [active, setActive] = useState2(0);
590
+ const [active, setActive] = useState3(0);
435
591
  const current = scopes[active] ?? scopes[0] ?? { key: "", label: "", stats: [] };
436
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-scoped", children: [
437
- /* @__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)) }),
438
- /* @__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)
439
595
  ] });
440
596
  }
441
- function InsightCard({ it, ui }) {
597
+ function InsightCard({ it, ui, slip }) {
442
598
  const live = it.isLive || it.phase === "live";
443
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-binsight", children: [
444
- live || it.fixture || it.competition ? /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-status", children: [
445
- live ? /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-live", children: [
446
- /* @__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" }),
447
603
  ui?.liveLabel ?? "Live"
448
604
  ] }) : null,
449
- it.fixture ? /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-fixture", children: it.fixture }) : null,
450
- 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
451
607
  ] }) : null,
452
- /* @__PURE__ */ jsxs5("div", { className: "ss-w-bi-head", children: [
453
- /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-mkt", children: [
454
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-market", children: it.market }),
455
- 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
456
612
  ] }),
457
- it.odds ? /* @__PURE__ */ jsxs5("span", { className: "ss-w-bi-odds", children: [
458
- /* @__PURE__ */ jsx7("span", { className: "ss-w-bi-odds-cap", children: ui?.oddsLabel ?? "Odds" }),
459
- /* @__PURE__ */ jsx7("b", { children: it.odds }),
460
- 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
461
617
  ] }) : null
462
618
  ] }),
463
- it.text ? /* @__PURE__ */ jsx7("p", { className: "ss-w-bi-text", children: it.text }) : null,
464
- 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,
465
- 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 })
466
623
  ] });
467
624
  }
468
- function BettingInsightList({ insights, ui }) {
625
+ function BettingInsightList({ insights, ui, slip }) {
469
626
  if (!insights || insights.length === 0) return null;
470
- 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)) });
471
628
  }
472
629
 
473
630
  // src/components/ActionButtons.tsx
474
- 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";
475
632
  function ActionButtons({ actions, loading, disabled, onAct }) {
476
633
  if (actions.length === 0 && !loading) return null;
477
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-actions", children: [
478
- actions.map((a, i) => /* @__PURE__ */ jsx8("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
479
- actions.length === 0 && loading ? /* @__PURE__ */ jsxs6(Fragment, { children: [
480
- /* @__PURE__ */ jsx8("span", { className: "ss-w-abtn-shimmer" }),
481
- /* @__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" })
482
639
  ] }) : null
483
640
  ] });
484
641
  }
485
642
 
486
643
  // src/components/ChatMessage.tsx
487
- import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
488
- 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 }) {
489
646
  if (message.role === "user") {
490
- 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 }) });
491
648
  }
492
- return /* @__PURE__ */ jsxs7("div", { className: "ss-w-msg ss-w-bot", children: [
493
- message.text ? /* @__PURE__ */ jsx9(Markdown, { text: message.text }) : null,
494
- /* @__PURE__ */ jsx9(EntityCardList, { cards: message.cards, disabled, showLessLabel: ui?.showLess, showMoreLabel: ui?.showMore }),
495
- /* @__PURE__ */ jsx9(BettingInsightList, { insights: message.insights, ui }),
496
- /* @__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 })
497
654
  ] });
498
655
  }
499
656
 
500
657
  // src/components/IntentChips.tsx
501
- import { jsx as jsx10 } from "react/jsx-runtime";
658
+ import { jsx as jsx12 } from "react/jsx-runtime";
502
659
  function IntentChips({ chips, onPick }) {
503
660
  if (chips.length === 0) return null;
504
- 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}`)) });
505
662
  }
506
663
 
507
664
  // src/scroll.ts
@@ -511,17 +668,17 @@ function isPinnedToBottom(el, threshold = PIN_THRESHOLD) {
511
668
  }
512
669
 
513
670
  // src/components/MessageList.tsx
514
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
515
- function MessageList({ controller, ui }) {
671
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
672
+ function MessageList({ controller, ui, slip }) {
516
673
  const { state, send } = controller;
517
674
  const listRef = useRef2(null);
518
675
  const endRef = useRef2(null);
519
676
  const pinnedRef = useRef2(true);
520
- useEffect2(() => {
677
+ useEffect3(() => {
521
678
  if (pinnedRef.current) endRef.current?.scrollIntoView({ block: "end" });
522
679
  }, [state.messages, state.status]);
523
680
  const empty = state.messages.length === 0;
524
- return /* @__PURE__ */ jsxs8(
681
+ return /* @__PURE__ */ jsxs9(
525
682
  "div",
526
683
  {
527
684
  className: "ss-w-list",
@@ -531,35 +688,36 @@ function MessageList({ controller, ui }) {
531
688
  if (listRef.current) pinnedRef.current = isPinnedToBottom(listRef.current);
532
689
  },
533
690
  children: [
534
- state.messages.map((m) => /* @__PURE__ */ jsx11(
691
+ state.messages.map((m) => /* @__PURE__ */ jsx13(
535
692
  ChatMessage,
536
693
  {
537
694
  message: m,
538
695
  disabled: state.isStreaming,
539
696
  onAct: (a) => send(a.message, { actionId: a.actionId, displayText: a.label }),
540
- ui
697
+ ui,
698
+ slip
541
699
  },
542
700
  m.id
543
701
  )),
544
- state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx11(ActionButtons, { actions: [], loading: true, onAct: () => {
702
+ state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx13(ActionButtons, { actions: [], loading: true, onAct: () => {
545
703
  } }) : null,
546
- state.status ? /* @__PURE__ */ jsx11("div", { className: "ss-w-status", children: state.status }) : null,
547
- state.error ? /* @__PURE__ */ jsxs8("div", { className: "ss-w-error", role: "alert", children: [
548
- /* @__PURE__ */ jsx11("span", { children: "message" in state.error ? state.error.message : ui.somethingWentWrong }),
549
- /* @__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 })
550
708
  ] }) : null,
551
- empty ? /* @__PURE__ */ jsx11(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
552
- /* @__PURE__ */ jsx11("div", { ref: endRef })
709
+ empty ? /* @__PURE__ */ jsx13(IntentChips, { chips: state.starters, onPick: (t) => send(t) }) : null,
710
+ /* @__PURE__ */ jsx13("div", { ref: endRef })
553
711
  ]
554
712
  }
555
713
  );
556
714
  }
557
715
 
558
716
  // src/components/ChatInput.tsx
559
- import { useState as useState3 } from "react";
560
- 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";
561
719
  function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop response", sendLabel = "Send message" }) {
562
- const [value, setValue] = useState3("");
720
+ const [value, setValue] = useState4("");
563
721
  const submit = () => {
564
722
  const t = value.trim();
565
723
  if (!t) return;
@@ -572,18 +730,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop, stopLabel = "Stop r
572
730
  submit();
573
731
  }
574
732
  };
575
- return /* @__PURE__ */ jsxs9("div", { className: "ss-w-input", children: [
576
- /* @__PURE__ */ jsx12("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
577
- 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, {}) })
578
736
  ] });
579
737
  }
580
738
 
581
739
  // src/components/ChatPanel.tsx
582
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
740
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
583
741
  var FULLSCREEN_QUERY = "(max-width: 639px)";
584
742
  function useIsModal() {
585
- const [modal, setModal] = useState4(false);
586
- useEffect3(() => {
743
+ const [modal, setModal] = useState5(false);
744
+ useEffect4(() => {
587
745
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
588
746
  const mql = window.matchMedia(FULLSCREEN_QUERY);
589
747
  const sync = () => setModal(mql.matches);
@@ -593,20 +751,20 @@ function useIsModal() {
593
751
  }, []);
594
752
  return modal;
595
753
  }
596
- function ChatPanel({ controller, ui }) {
754
+ function ChatPanel({ controller, ui, slip }) {
597
755
  const { state, close, stop, send } = controller;
598
756
  const isModal = useIsModal();
599
- useEffect3(() => {
757
+ useEffect4(() => {
600
758
  const onKey = (e) => {
601
759
  if (e.key === "Escape") close();
602
760
  };
603
761
  window.addEventListener("keydown", onKey);
604
762
  return () => window.removeEventListener("keydown", onKey);
605
763
  }, [close]);
606
- return /* @__PURE__ */ jsxs10("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": ui.chatDialogLabel, children: [
607
- /* @__PURE__ */ jsx13(ChatHeader, { onClose: close, closeLabel: ui.closeChat }),
608
- /* @__PURE__ */ jsx13(MessageList, { controller, ui }),
609
- /* @__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 })
610
768
  ] });
611
769
  }
612
770
 
@@ -759,13 +917,21 @@ var WIDGET_CSS = `.ss-w-root { all: revert; }
759
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; }
760
918
  .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
761
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
+
762
928
  @media (prefers-reduced-motion: reduce) {
763
929
  .ss-w-abtn-shimmer { animation: none; }
764
930
  }
765
931
  `;
766
932
 
767
933
  // src/StatsWidget.tsx
768
- import { jsx as jsx14 } from "react/jsx-runtime";
934
+ import { jsx as jsx16 } from "react/jsx-runtime";
769
935
  function buildClient(props) {
770
936
  try {
771
937
  if ("client" in props && props.client) {
@@ -794,14 +960,27 @@ function StatsWidget(props) {
794
960
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
795
961
  const rootRef = useRef3(null);
796
962
  const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
797
- 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(() => {
798
977
  if (!built || !ui.injectStyles) return;
799
978
  const node = rootRef.current?.getRootNode();
800
979
  if (node) injectStyles(node);
801
980
  }, [built, ui.injectStyles]);
802
981
  if (!built) return null;
803
982
  const attrs = themeAttrs(ui);
804
- 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(
805
984
  ChatFab,
806
985
  {
807
986
  label: ui.launcherLabel,