@sensiblestats/widget-react 0.1.1 → 0.2.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 useEffect3, useMemo as useMemo2, useRef as useRef3 } from "react";
2
+ import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3 } from "react";
3
3
  import { StatsWidgetClient } from "@sensiblestats/widget-sdk";
4
4
 
5
5
  // src/config.ts
@@ -12,6 +12,7 @@ function normalizeUi(cfg) {
12
12
  radius: cfg.radius ?? 16,
13
13
  launcherLabel: cfg.launcherLabel ?? "Chat with SensibleStats",
14
14
  greeting: cfg.greeting,
15
+ greetingMessage: cfg.greetingMessage,
15
16
  placeholder: cfg.placeholder ?? "Ask about a team, player or match\u2026",
16
17
  starters: cfg.starters ?? [],
17
18
  injectStyles: cfg.injectStyles ?? true
@@ -38,7 +39,7 @@ function themeAttrs(ui) {
38
39
  }
39
40
 
40
41
  // src/useChatStream.ts
41
- import { useCallback, useMemo, useReducer, useRef } from "react";
42
+ import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
42
43
  import { WidgetSdkError as WidgetSdkError2 } from "@sensiblestats/widget-sdk";
43
44
 
44
45
  // src/reducer.ts
@@ -89,8 +90,13 @@ function chipFromText(text) {
89
90
  }
90
91
 
91
92
  // src/reducer.ts
93
+ var POPUP_MAX = 160;
92
94
  function initialState(starters) {
93
- return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null };
95
+ return { isOpen: false, messages: [], isStreaming: false, status: null, actionsLoading: false, starters, error: null, lastUserInput: null, lastDisplayText: null, popupText: null };
96
+ }
97
+ function activeTurn(state) {
98
+ const last = state.messages[state.messages.length - 1];
99
+ return last && last.role === "assistant" && !last.done ? last : null;
94
100
  }
95
101
  function mapActive(state, fn) {
96
102
  const last = state.messages[state.messages.length - 1];
@@ -114,16 +120,22 @@ function applyEvent(state, event) {
114
120
  }
115
121
  case "actions_loading":
116
122
  return { ...state, actionsLoading: true, status: event.data.message };
117
- case "turn_complete":
118
- return { ...state, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
119
- case "error":
120
- return { ...state, error: event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
123
+ case "turn_complete": {
124
+ const active = activeTurn(state);
125
+ const text = active?.text.trim() ?? "";
126
+ const popupText = active?.isGreeting && !state.isOpen && text ? text.slice(0, POPUP_MAX) : state.popupText;
127
+ return { ...state, isStreaming: false, status: null, actionsLoading: false, popupText, messages: mapActive(state, (t) => ({ ...t, done: true })) };
128
+ }
129
+ case "error": {
130
+ const suppress = activeTurn(state)?.isGreeting === true;
131
+ return { ...state, error: suppress ? state.error : event.data, isStreaming: false, status: null, actionsLoading: false, messages: mapActive(state, (t) => ({ ...t, done: true })) };
132
+ }
121
133
  }
122
134
  }
123
135
  function reducer(state, action) {
124
136
  switch (action.kind) {
125
137
  case "OPEN":
126
- return { ...state, isOpen: true };
138
+ return { ...state, isOpen: true, popupText: null };
127
139
  case "CLOSE":
128
140
  return { ...state, isOpen: false, isStreaming: false, status: null, actionsLoading: false };
129
141
  case "USER_SENT": {
@@ -131,6 +143,12 @@ function reducer(state, action) {
131
143
  const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false };
132
144
  return { ...state, messages: [...state.messages, user, assistant], isStreaming: true, status: null, actionsLoading: false, starters: [], error: null, lastUserInput: action.input, lastDisplayText: action.displayText ?? null };
133
145
  }
146
+ case "GREETING_SENT": {
147
+ const assistant = { role: "assistant", id: action.assistantId, text: "", cards: [], actions: [], done: false, isGreeting: true };
148
+ return { ...state, messages: [...state.messages, assistant], isStreaming: true, status: null, error: null };
149
+ }
150
+ case "DISMISS_POPUP":
151
+ return { ...state, popupText: null };
134
152
  case "EVENT":
135
153
  return applyEvent(state, action.event);
136
154
  case "STREAM_ERROR":
@@ -141,31 +159,36 @@ function reducer(state, action) {
141
159
  }
142
160
 
143
161
  // src/useChatStream.ts
144
- function useChatStream(conversation, starterTexts) {
162
+ function useChatStream(conversation, starterTexts, greetingMessage) {
145
163
  const seed = useMemo(() => initialState(starterTexts.map(chipFromText)), []);
146
164
  const [state, dispatch] = useReducer(reducer, seed);
147
165
  const idRef = useRef(0);
148
166
  const handleRef = useRef(null);
149
167
  const stateRef = useRef(state);
150
168
  stateRef.current = state;
169
+ const greetedRef = useRef(false);
151
170
  const nextId = () => `m${idRef.current++}`;
152
- const run = useCallback((input, displayText) => {
153
- handleRef.current?.cancel();
154
- dispatch({ kind: "USER_SENT", input, userId: nextId(), assistantId: nextId(), displayText });
155
- const handle = conversation.send(input);
156
- handleRef.current = handle;
171
+ const consume = useCallback((handle, onError) => {
172
+ ;
157
173
  (async () => {
158
174
  try {
159
175
  for await (const event of handle) dispatch({ kind: "EVENT", event });
160
176
  } catch (err) {
161
- if (err instanceof WidgetSdkError2) dispatch({ kind: "STREAM_ERROR", error: err });
177
+ if (err instanceof WidgetSdkError2) onError(err);
162
178
  else if (err?.name === "AbortError") {
163
179
  } else throw err;
164
180
  } finally {
165
181
  if (handleRef.current === handle) handleRef.current = null;
166
182
  }
167
183
  })();
168
- }, [conversation]);
184
+ }, []);
185
+ const run = useCallback((input, displayText) => {
186
+ handleRef.current?.cancel();
187
+ dispatch({ kind: "USER_SENT", input, userId: nextId(), assistantId: nextId(), displayText });
188
+ const handle = conversation.send(input);
189
+ handleRef.current = handle;
190
+ consume(handle, (err) => dispatch({ kind: "STREAM_ERROR", error: err }));
191
+ }, [conversation, consume]);
169
192
  const send = useCallback((text, opts) => {
170
193
  const trimmed = text.trim();
171
194
  if (!trimmed) return;
@@ -175,6 +198,20 @@ function useChatStream(conversation, starterTexts) {
175
198
  const last = stateRef.current.lastUserInput;
176
199
  if (last) run(last, stateRef.current.lastDisplayText ?? void 0);
177
200
  }, [run]);
201
+ useEffect(() => {
202
+ const message = greetingMessage?.trim();
203
+ if (!message || greetedRef.current) return;
204
+ greetedRef.current = true;
205
+ try {
206
+ handleRef.current?.cancel();
207
+ dispatch({ kind: "GREETING_SENT", assistantId: nextId() });
208
+ const handle = conversation.send({ message });
209
+ handleRef.current = handle;
210
+ consume(handle, () => dispatch({ kind: "STOP" }));
211
+ } catch {
212
+ dispatch({ kind: "STOP" });
213
+ }
214
+ }, [greetingMessage, conversation, consume]);
178
215
  const open = useCallback(() => dispatch({ kind: "OPEN" }), []);
179
216
  const close = useCallback(() => {
180
217
  handleRef.current?.cancel();
@@ -190,7 +227,8 @@ function useChatStream(conversation, starterTexts) {
190
227
  if (stateRef.current.isOpen) close();
191
228
  else open();
192
229
  }, [close, open]);
193
- return { state, open, close, stop, toggle, send, retry };
230
+ const dismissPopup = useCallback(() => dispatch({ kind: "DISMISS_POPUP" }), []);
231
+ return { state, open, close, stop, toggle, send, retry, dismissPopup };
194
232
  }
195
233
 
196
234
  // src/components/Icons.tsx
@@ -215,27 +253,31 @@ function ChevronIcon() {
215
253
  }
216
254
 
217
255
  // src/components/GreetingBubble.tsx
218
- import { jsx as jsx2 } from "react/jsx-runtime";
219
- function GreetingBubble({ text }) {
220
- return /* @__PURE__ */ jsx2("div", { className: "ss-w-greeting", role: "note", children: text });
256
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
257
+ function GreetingBubble({ text, onOpen, onDismiss }) {
258
+ return /* @__PURE__ */ jsxs2("div", { className: "ss-w-greeting", role: "note", children: [
259
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "ss-w-greeting-body", onClick: onOpen, children: text }),
260
+ onDismiss ? /* @__PURE__ */ jsx2("button", { type: "button", className: "ss-w-greeting-x", "aria-label": "Dismiss", onClick: onDismiss, children: "\xD7" }) : null
261
+ ] });
221
262
  }
222
263
 
223
264
  // src/components/ChatFab.tsx
224
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
225
- function ChatFab({ label, open, greeting, onToggle }) {
226
- return /* @__PURE__ */ jsxs2("div", { className: "ss-w-fab-wrap", children: [
227
- !open && greeting ? /* @__PURE__ */ jsx3(GreetingBubble, { text: greeting }) : null,
228
- /* @__PURE__ */ jsx3("button", { type: "button", className: "ss-w-fab", "aria-label": label, "aria-expanded": open, onClick: onToggle, children: /* @__PURE__ */ jsx3(SsMark, { className: "ss-w-fab-mark" }) })
265
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
266
+ function ChatFab({ label, greeting, popupText, onOpen, onDismissPopup }) {
267
+ const bubble = popupText ?? greeting;
268
+ return /* @__PURE__ */ jsxs3("div", { className: "ss-w-fab-wrap", children: [
269
+ bubble ? /* @__PURE__ */ jsx3(GreetingBubble, { text: bubble, onOpen, onDismiss: popupText ? onDismissPopup : void 0 }) : null,
270
+ /* @__PURE__ */ jsx3("button", { type: "button", className: "ss-w-fab", "aria-label": label, onClick: onOpen, children: /* @__PURE__ */ jsx3(SsMark, { className: "ss-w-fab-mark" }) })
229
271
  ] });
230
272
  }
231
273
 
232
274
  // src/components/ChatPanel.tsx
233
- import { useEffect as useEffect2, useState as useState3 } from "react";
275
+ import { useEffect as useEffect3, useState as useState3 } from "react";
234
276
 
235
277
  // src/components/ChatHeader.tsx
236
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
278
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
237
279
  function ChatHeader({ onClose }) {
238
- return /* @__PURE__ */ jsxs3("header", { className: "ss-w-header", children: [
280
+ return /* @__PURE__ */ jsxs4("header", { className: "ss-w-header", children: [
239
281
  /* @__PURE__ */ jsx4(SsMark, { className: "ss-w-header-mark" }),
240
282
  /* @__PURE__ */ jsx4("span", { className: "ss-w-header-title", children: "SensibleStats" }),
241
283
  /* @__PURE__ */ jsx4("button", { type: "button", className: "ss-w-close", "aria-label": "Close chat", onClick: onClose, children: /* @__PURE__ */ jsx4(CloseIcon, {}) })
@@ -243,7 +285,7 @@ function ChatHeader({ onClose }) {
243
285
  }
244
286
 
245
287
  // src/components/MessageList.tsx
246
- import { useEffect, useRef as useRef2 } from "react";
288
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
247
289
 
248
290
  // src/components/Markdown.tsx
249
291
  import ReactMarkdown from "react-markdown";
@@ -262,7 +304,7 @@ function Markdown({ text }) {
262
304
 
263
305
  // src/components/EntityCardList.tsx
264
306
  import { useState } from "react";
265
- import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
307
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
266
308
  var MAX_VISIBLE = 3;
267
309
  function initials(name) {
268
310
  return name.split(/\s+/).slice(0, 2).map((w) => w[0] ?? "").join("").toUpperCase();
@@ -272,8 +314,8 @@ function EntityCardList({ cards, disabled, onSelect }) {
272
314
  if (cards.length === 0) return null;
273
315
  const visible = expanded ? cards : cards.slice(0, MAX_VISIBLE);
274
316
  const overflow = cards.length - MAX_VISIBLE;
275
- return /* @__PURE__ */ jsxs4("div", { className: "ss-w-cards", children: [
276
- visible.map((c) => /* @__PURE__ */ jsxs4("button", { type: "button", className: "ss-w-ecard", "data-kind": c.kind, disabled, onClick: () => onSelect(c.query), children: [
317
+ return /* @__PURE__ */ jsxs5("div", { className: "ss-w-cards", children: [
318
+ visible.map((c) => /* @__PURE__ */ jsxs5("button", { type: "button", className: "ss-w-ecard", "data-kind": c.kind, disabled, onClick: () => onSelect(c.query), children: [
277
319
  /* @__PURE__ */ jsx6(
278
320
  "span",
279
321
  {
@@ -282,12 +324,12 @@ function EntityCardList({ cards, disabled, onSelect }) {
282
324
  children: c.image ? "" : initials(c.name)
283
325
  }
284
326
  ),
285
- /* @__PURE__ */ jsxs4("span", { className: "ss-w-meta", children: [
327
+ /* @__PURE__ */ jsxs5("span", { className: "ss-w-meta", children: [
286
328
  c.kind !== "generic" ? /* @__PURE__ */ jsx6("span", { className: "ss-w-kind", children: c.kind }) : null,
287
329
  /* @__PURE__ */ jsx6("span", { className: "ss-w-nm", children: c.name }),
288
330
  c.sub ? /* @__PURE__ */ jsx6("span", { className: "ss-w-sub", children: c.sub }) : null
289
331
  ] }),
290
- c.stats.map((s) => /* @__PURE__ */ jsxs4("span", { className: "ss-w-stat", children: [
332
+ c.stats.map((s) => /* @__PURE__ */ jsxs5("span", { className: "ss-w-stat", children: [
291
333
  /* @__PURE__ */ jsx6("b", { children: s.value }),
292
334
  /* @__PURE__ */ jsx6("span", { children: s.label })
293
335
  ] }, s.label)),
@@ -298,12 +340,12 @@ function EntityCardList({ cards, disabled, onSelect }) {
298
340
  }
299
341
 
300
342
  // src/components/ActionButtons.tsx
301
- import { Fragment, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
343
+ import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
302
344
  function ActionButtons({ actions, loading, disabled, onAct }) {
303
345
  if (actions.length === 0 && !loading) return null;
304
- return /* @__PURE__ */ jsxs5("div", { className: "ss-w-actions", children: [
346
+ return /* @__PURE__ */ jsxs6("div", { className: "ss-w-actions", children: [
305
347
  actions.map((a, i) => /* @__PURE__ */ jsx7("button", { type: "button", className: "ss-w-abtn", disabled, onClick: () => onAct(a), children: a.label }, `${a.label}-${i}`)),
306
- actions.length === 0 && loading ? /* @__PURE__ */ jsxs5(Fragment, { children: [
348
+ actions.length === 0 && loading ? /* @__PURE__ */ jsxs6(Fragment, { children: [
307
349
  /* @__PURE__ */ jsx7("span", { className: "ss-w-abtn-shimmer" }),
308
350
  /* @__PURE__ */ jsx7("span", { className: "ss-w-abtn-shimmer" })
309
351
  ] }) : null
@@ -311,12 +353,12 @@ function ActionButtons({ actions, loading, disabled, onAct }) {
311
353
  }
312
354
 
313
355
  // src/components/ChatMessage.tsx
314
- import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
356
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
315
357
  function ChatMessage({ message, disabled, onAct, onSelect }) {
316
358
  if (message.role === "user") {
317
359
  return /* @__PURE__ */ jsx8("div", { className: "ss-w-msg ss-w-user", children: /* @__PURE__ */ jsx8("div", { className: "ss-w-bubble", children: message.text }) });
318
360
  }
319
- return /* @__PURE__ */ jsxs6("div", { className: "ss-w-msg ss-w-bot", children: [
361
+ return /* @__PURE__ */ jsxs7("div", { className: "ss-w-msg ss-w-bot", children: [
320
362
  message.text ? /* @__PURE__ */ jsx8(Markdown, { text: message.text }) : null,
321
363
  /* @__PURE__ */ jsx8(EntityCardList, { cards: message.cards, disabled, onSelect }),
322
364
  /* @__PURE__ */ jsx8(ActionButtons, { actions: message.actions, loading: false, disabled, onAct })
@@ -331,16 +373,16 @@ function IntentChips({ chips, onPick }) {
331
373
  }
332
374
 
333
375
  // src/components/MessageList.tsx
334
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
376
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
335
377
  function MessageList({ controller, ui }) {
336
378
  void ui;
337
379
  const { state, send } = controller;
338
380
  const endRef = useRef2(null);
339
- useEffect(() => {
381
+ useEffect2(() => {
340
382
  endRef.current?.scrollIntoView({ block: "end" });
341
383
  }, [state.messages, state.status]);
342
384
  const empty = state.messages.length === 0;
343
- return /* @__PURE__ */ jsxs7("div", { className: "ss-w-list", "aria-live": "polite", children: [
385
+ return /* @__PURE__ */ jsxs8("div", { className: "ss-w-list", "aria-live": "polite", children: [
344
386
  state.messages.map((m) => /* @__PURE__ */ jsx10(
345
387
  ChatMessage,
346
388
  {
@@ -354,7 +396,7 @@ function MessageList({ controller, ui }) {
354
396
  state.isStreaming && state.actionsLoading ? /* @__PURE__ */ jsx10(ActionButtons, { actions: [], loading: true, onAct: () => {
355
397
  } }) : null,
356
398
  state.status ? /* @__PURE__ */ jsx10("div", { className: "ss-w-status", children: state.status }) : null,
357
- state.error ? /* @__PURE__ */ jsxs7("div", { className: "ss-w-error", role: "alert", children: [
399
+ state.error ? /* @__PURE__ */ jsxs8("div", { className: "ss-w-error", role: "alert", children: [
358
400
  /* @__PURE__ */ jsx10("span", { children: "message" in state.error ? state.error.message : "Something went wrong." }),
359
401
  /* @__PURE__ */ jsx10("button", { type: "button", className: "ss-w-retry", onClick: () => controller.retry(), children: "Retry" })
360
402
  ] }) : null,
@@ -365,7 +407,7 @@ function MessageList({ controller, ui }) {
365
407
 
366
408
  // src/components/ChatInput.tsx
367
409
  import { useState as useState2 } from "react";
368
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
410
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
369
411
  function ChatInput({ placeholder, streaming, onSend, onStop }) {
370
412
  const [value, setValue] = useState2("");
371
413
  const submit = () => {
@@ -380,18 +422,18 @@ function ChatInput({ placeholder, streaming, onSend, onStop }) {
380
422
  submit();
381
423
  }
382
424
  };
383
- return /* @__PURE__ */ jsxs8("div", { className: "ss-w-input", children: [
425
+ return /* @__PURE__ */ jsxs9("div", { className: "ss-w-input", children: [
384
426
  /* @__PURE__ */ jsx11("textarea", { className: "ss-w-textarea", rows: 1, placeholder, value, disabled: streaming, onChange: (e) => setValue(e.target.value), onKeyDown }),
385
427
  streaming ? /* @__PURE__ */ jsx11("button", { type: "button", className: "ss-w-send ss-w-stop", "aria-label": "Stop response", onClick: onStop, children: /* @__PURE__ */ jsx11("span", { className: "ss-w-stop-glyph" }) }) : /* @__PURE__ */ jsx11("button", { type: "button", className: "ss-w-send", "aria-label": "Send message", onClick: submit, children: /* @__PURE__ */ jsx11(SendIcon, {}) })
386
428
  ] });
387
429
  }
388
430
 
389
431
  // src/components/ChatPanel.tsx
390
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
432
+ import { jsx as jsx12, jsxs as jsxs10 } from "react/jsx-runtime";
391
433
  var FULLSCREEN_QUERY = "(max-width: 639px)";
392
434
  function useIsModal() {
393
435
  const [modal, setModal] = useState3(false);
394
- useEffect2(() => {
436
+ useEffect3(() => {
395
437
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
396
438
  const mql = window.matchMedia(FULLSCREEN_QUERY);
397
439
  const sync = () => setModal(mql.matches);
@@ -404,14 +446,14 @@ function useIsModal() {
404
446
  function ChatPanel({ controller, ui }) {
405
447
  const { state, close, stop, send } = controller;
406
448
  const isModal = useIsModal();
407
- useEffect2(() => {
449
+ useEffect3(() => {
408
450
  const onKey = (e) => {
409
451
  if (e.key === "Escape") close();
410
452
  };
411
453
  window.addEventListener("keydown", onKey);
412
454
  return () => window.removeEventListener("keydown", onKey);
413
455
  }, [close]);
414
- return /* @__PURE__ */ jsxs9("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": "SensibleStats chat", children: [
456
+ return /* @__PURE__ */ jsxs10("div", { className: "ss-w-panel", role: "dialog", "aria-modal": isModal, "aria-label": "SensibleStats chat", children: [
415
457
  /* @__PURE__ */ jsx12(ChatHeader, { onClose: close }),
416
458
  /* @__PURE__ */ jsx12(MessageList, { controller, ui }),
417
459
  /* @__PURE__ */ jsx12(ChatInput, { placeholder: ui.placeholder, streaming: state.isStreaming, onSend: (t) => send(t), onStop: stop })
@@ -419,10 +461,126 @@ function ChatPanel({ controller, ui }) {
419
461
  }
420
462
 
421
463
  // src/styles/css.generated.ts
422
- var WIDGET_CSS = '.ss-w-root { all: revert; }\n.ss-w-root, .ss-w-root * { box-sizing: border-box; margin: 0; padding: 0; }\n.ss-w-root {\n --ss-w-accent: #17936a;\n --ss-w-ink: #14181d; --ss-w-faint: #6b7580; --ss-w-line: #e4e8ec;\n --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;\n --ss-w-radius: 16px; --ss-w-offset-x: 20px; --ss-w-offset-y: 20px;\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;\n font-size: 13px; line-height: 1.5; color: var(--ss-w-ink);\n}\n@media (prefers-color-scheme: dark) {\n .ss-w-root[data-ss-w-theme="auto"] {\n --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;\n --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;\n }\n}\n.ss-w-root[data-ss-w-theme="dark"] {\n --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;\n --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;\n}\n.ss-w-root[data-ss-w-theme="light"] {\n --ss-w-accent: #17936a; --ss-w-ink: #14181d; --ss-w-faint: #6b7580;\n --ss-w-line: #e4e8ec; --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;\n}\n\n/* launcher */\n.ss-w-fab-wrap { position: fixed; bottom: var(--ss-w-offset-y); z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }\n.ss-w-root[data-ss-w-pos="right"] .ss-w-fab-wrap { right: var(--ss-w-offset-x); }\n.ss-w-root[data-ss-w-pos="left"] .ss-w-fab-wrap { left: var(--ss-w-offset-x); align-items: flex-start; }\n.ss-w-fab { width: 56px; height: 56px; border: none; border-radius: 50%; background: var(--ss-w-nav); color: #fff; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0,0,0,.28); }\n.ss-w-fab-mark { font-size: 30px; }\n.ss-w-greeting { max-width: 220px; background: var(--ss-w-panel); color: var(--ss-w-ink); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 9px 12px; box-shadow: 0 4px 14px rgba(0,0,0,.14); }\n\n/* panel */\n.ss-w-panel { position: fixed; bottom: calc(var(--ss-w-offset-y) + 72px); z-index: 2147483000; display: flex; flex-direction: column; width: min(400px, calc(100vw - 40px)); height: min(620px, calc(100vh - 120px)); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: var(--ss-w-radius); overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.24); }\n.ss-w-root[data-ss-w-pos="right"] .ss-w-panel { right: var(--ss-w-offset-x); }\n.ss-w-root[data-ss-w-pos="left"] .ss-w-panel { left: var(--ss-w-offset-x); }\n@media (max-width: 639px) {\n .ss-w-panel { inset: 0; width: 100vw; height: 100vh; border: none; border-radius: 0; }\n}\n.ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }\n.ss-w-header-mark { font-size: 18px; }\n.ss-w-header-title { font-weight: 660; }\n.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; }\n\n/* messages */\n.ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }\n.ss-w-msg { display: flex; }\n.ss-w-user { justify-content: flex-end; }\n.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%; }\n.ss-w-bot { flex-direction: column; gap: 8px; }\n.ss-w-status { font-size: 11px; color: var(--ss-w-faint); font-style: italic; }\n.ss-w-error { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #c0392b; }\n.ss-w-retry { background: none; border: 1px solid currentColor; border-radius: 10px; padding: 2px 8px; cursor: pointer; color: inherit; }\n\n/* markdown */\n.ss-w-md { display: flex; flex-direction: column; gap: 8px; }\n.ss-w-md :where(p, ul, ol, table, pre, blockquote, h1, h2, h3, h4) { margin: 0; }\n.ss-w-md > *:first-child { margin-top: 0; }\n.ss-w-md > *:last-child { margin-bottom: 0; }\n.ss-w-md-ul { list-style: disc outside; padding-left: 18px; }\n.ss-w-md-ol { list-style: decimal outside; padding-left: 18px; }\n.ss-w-md-ul li::marker, .ss-w-md-ol li::marker { color: var(--ss-w-accent); }\n.ss-w-md-link { color: var(--ss-w-accent); text-decoration: underline; }\n.ss-w-table-wrap { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; }\n.ss-w-md-table { border-collapse: collapse; width: max-content; min-width: 100%; font-variant-numeric: tabular-nums; }\n.ss-w-md-table td, .ss-w-md-table th { border: 1px solid var(--ss-w-line); padding: 4px 8px; text-align: left; white-space: nowrap; }\n.ss-w-md-table th { background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); font-weight: 700; }\n.ss-w-md-table tbody tr:nth-child(even) { background: var(--ss-w-bubble); }\n.ss-w-md-code { background: var(--ss-w-bubble); border-radius: 5px; padding: 1px 5px; font-family: ui-monospace, monospace; }\n\n/* entity cards */\n.ss-w-cards { display: flex; flex-direction: column; gap: 7px; }\n.ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; cursor: pointer; }\n.ss-w-thumb { width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff; background-color: var(--ss-w-nav); background-size: cover; background-position: center; }\n.ss-w-thumb.ss-w-team { border-radius: 10px; }\n.ss-w-ecard[data-kind="player"] .ss-w-thumb { background-color: var(--ss-w-accent); }\n.ss-w-meta { display: flex; flex-direction: column; min-width: 0; }\n.ss-w-kind { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }\n.ss-w-nm { font-weight: 660; }\n.ss-w-sub { font-size: 11px; color: var(--ss-w-faint); }\n.ss-w-stat { margin-left: auto; text-align: center; }\n.ss-w-stat b { display: block; font-variant-numeric: tabular-nums; }\n.ss-w-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }\n.ss-w-chev { color: var(--ss-w-faint); font-size: 16px; display: grid; place-items: center; }\n.ss-w-cards-more { align-self: flex-start; font-size: 11.5px; font-weight: 560; color: var(--ss-w-accent); background: none; border: none; cursor: pointer; padding: 3px 2px; }\n.ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }\n.ss-w-ecard:disabled, .ss-w-cards-more:disabled { opacity: .5; cursor: default; }\n\n/* action buttons */\n.ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }\n.ss-w-abtn { font-size: 12px; font-weight: 560; color: var(--ss-w-accent); background: var(--ss-w-panel); border: 1px solid var(--ss-w-accent); border-radius: 16px; padding: 6px 12px; cursor: pointer; }\n.ss-w-abtn:disabled { opacity: .5; cursor: default; }\n.ss-w-abtn-shimmer { width: 140px; height: 30px; border-radius: 16px; background: linear-gradient(90deg, var(--ss-w-bubble), var(--ss-w-line), var(--ss-w-bubble)); background-size: 200% 100%; animation: ss-w-shimmer 1.2s infinite; }\n@keyframes ss-w-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }\n\n/* intent chips */\n.ss-w-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.ss-w-chip { font-size: 11.5px; font-weight: 550; color: var(--ss-w-ink); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 5px 11px; cursor: pointer; }\n\n/* input */\n.ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }\n.ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }\n.ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }\n.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; }\n.ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }\n\n@media (prefers-reduced-motion: reduce) {\n .ss-w-abtn-shimmer { animation: none; }\n}\n';
464
+ var WIDGET_CSS = `.ss-w-root { all: revert; }
465
+ .ss-w-root, .ss-w-root * { box-sizing: border-box; margin: 0; padding: 0; }
466
+ .ss-w-root {
467
+ --ss-w-accent: #17936a;
468
+ --ss-w-ink: #14181d; --ss-w-faint: #6b7580; --ss-w-line: #e4e8ec;
469
+ --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;
470
+ --ss-w-radius: 16px; --ss-w-offset-x: 20px; --ss-w-offset-y: 20px;
471
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
472
+ font-size: 13px; line-height: 1.5; color: var(--ss-w-ink);
473
+ }
474
+ @media (prefers-color-scheme: dark) {
475
+ .ss-w-root[data-ss-w-theme="auto"] {
476
+ --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;
477
+ --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;
478
+ }
479
+ }
480
+ .ss-w-root[data-ss-w-theme="dark"] {
481
+ --ss-w-accent: #37c891; --ss-w-ink: #e8edf2; --ss-w-faint: #8b95a1;
482
+ --ss-w-line: #2a3441; --ss-w-panel: #141b24; --ss-w-bubble: #1e2732; --ss-w-nav: #0a0f16;
483
+ }
484
+ .ss-w-root[data-ss-w-theme="light"] {
485
+ --ss-w-accent: #17936a; --ss-w-ink: #14181d; --ss-w-faint: #6b7580;
486
+ --ss-w-line: #e4e8ec; --ss-w-panel: #ffffff; --ss-w-bubble: #f3f5f7; --ss-w-nav: #0e1622;
487
+ }
488
+
489
+ /* launcher */
490
+ .ss-w-fab-wrap { position: fixed; bottom: var(--ss-w-offset-y); z-index: 2147483000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px; }
491
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-fab-wrap { right: var(--ss-w-offset-x); }
492
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-fab-wrap { left: var(--ss-w-offset-x); align-items: flex-start; }
493
+ .ss-w-fab { width: 56px; height: 56px; border: none; border-radius: 50%; background: var(--ss-w-nav); color: #fff; cursor: pointer; display: grid; place-items: center; box-shadow: 0 6px 20px rgba(0,0,0,.28); }
494
+ .ss-w-fab-mark { font-size: 30px; }
495
+ .ss-w-greeting { max-width: 240px; display: flex; align-items: flex-start; background: var(--ss-w-panel); color: var(--ss-w-ink); border: 1px solid var(--ss-w-line); border-radius: 12px; box-shadow: 0 4px 14px rgba(0,0,0,.14); }
496
+ .ss-w-greeting-body { flex: 1; min-width: 0; text-align: left; background: none; border: none; color: inherit; font: inherit; cursor: pointer; padding: 9px 4px 9px 12px; }
497
+ .ss-w-greeting-x { flex: none; background: none; border: none; color: var(--ss-w-faint); cursor: pointer; font-size: 17px; line-height: 1; padding: 7px 9px 0 2px; }
498
+
499
+ /* panel */
500
+ .ss-w-panel { position: fixed; bottom: calc(var(--ss-w-offset-y) + 72px); z-index: 2147483000; display: flex; flex-direction: column; width: min(400px, calc(100vw - 40px)); height: min(620px, calc(100vh - 120px)); background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: var(--ss-w-radius); overflow: hidden; box-shadow: 0 12px 40px rgba(0,0,0,.24); }
501
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-panel { right: var(--ss-w-offset-x); }
502
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-panel { left: var(--ss-w-offset-x); }
503
+ @media (max-width: 639px) {
504
+ /* Fill via inset with no explicit height (like the site's working panel) so the
505
+ input stays visible when the mobile keyboard/URL-bar shrinks the viewport. The
506
+ position-specific selectors override the right/left offsets set above. */
507
+ .ss-w-root[data-ss-w-pos="right"] .ss-w-panel,
508
+ .ss-w-root[data-ss-w-pos="left"] .ss-w-panel { inset: 0; width: auto; height: auto; border: none; border-radius: 0; }
509
+ }
510
+ .ss-w-header { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--ss-w-line); }
511
+ .ss-w-header-mark { font-size: 18px; }
512
+ .ss-w-header-title { font-weight: 660; }
513
+ .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; }
514
+
515
+ /* messages */
516
+ .ss-w-list { flex: 1; overflow-y: auto; padding: 14px; display: flex; flex-direction: column; gap: 12px; }
517
+ .ss-w-msg { display: flex; }
518
+ .ss-w-user { justify-content: flex-end; }
519
+ .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%; }
520
+ .ss-w-bot { flex-direction: column; gap: 8px; }
521
+ .ss-w-status { font-size: 11px; color: var(--ss-w-faint); font-style: italic; }
522
+ .ss-w-error { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #c0392b; }
523
+ .ss-w-retry { background: none; border: 1px solid currentColor; border-radius: 10px; padding: 2px 8px; cursor: pointer; color: inherit; }
524
+
525
+ /* markdown */
526
+ .ss-w-md { display: flex; flex-direction: column; gap: 8px; }
527
+ .ss-w-md :where(p, ul, ol, table, pre, blockquote, h1, h2, h3, h4) { margin: 0; }
528
+ .ss-w-md > *:first-child { margin-top: 0; }
529
+ .ss-w-md > *:last-child { margin-bottom: 0; }
530
+ .ss-w-md-ul { list-style: disc outside; padding-left: 18px; }
531
+ .ss-w-md-ol { list-style: decimal outside; padding-left: 18px; }
532
+ .ss-w-md-ul li::marker, .ss-w-md-ol li::marker { color: var(--ss-w-accent); }
533
+ .ss-w-md-link { color: var(--ss-w-accent); text-decoration: underline; }
534
+ .ss-w-table-wrap { overflow-x: auto; max-width: 100%; -webkit-overflow-scrolling: touch; }
535
+ .ss-w-md-table { border-collapse: collapse; width: max-content; min-width: 100%; font-variant-numeric: tabular-nums; }
536
+ .ss-w-md-table td, .ss-w-md-table th { border: 1px solid var(--ss-w-line); padding: 4px 8px; text-align: left; white-space: nowrap; }
537
+ .ss-w-md-table th { background: color-mix(in srgb, var(--ss-w-accent) 12%, var(--ss-w-panel)); font-weight: 700; }
538
+ .ss-w-md-table tbody tr:nth-child(even) { background: var(--ss-w-bubble); }
539
+ .ss-w-md-code { background: var(--ss-w-bubble); border-radius: 5px; padding: 1px 5px; font-family: ui-monospace, monospace; }
540
+
541
+ /* entity cards */
542
+ .ss-w-cards { display: flex; flex-direction: column; gap: 7px; }
543
+ .ss-w-ecard { display: flex; align-items: center; gap: 11px; width: 100%; background: var(--ss-w-panel); border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 11px; text-align: left; cursor: pointer; }
544
+ .ss-w-thumb { width: 40px; height: 40px; flex: none; border-radius: 50%; display: grid; place-items: center; font-size: 13px; font-weight: 700; color: #fff; background-color: var(--ss-w-nav); background-size: cover; background-position: center; }
545
+ .ss-w-thumb.ss-w-team { border-radius: 10px; }
546
+ .ss-w-ecard[data-kind="player"] .ss-w-thumb { background-color: var(--ss-w-accent); }
547
+ .ss-w-meta { display: flex; flex-direction: column; min-width: 0; }
548
+ .ss-w-kind { font-size: 8.5px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--ss-w-faint); line-height: 1.3; }
549
+ .ss-w-nm { font-weight: 660; }
550
+ .ss-w-sub { font-size: 11px; color: var(--ss-w-faint); }
551
+ .ss-w-stat { margin-left: auto; text-align: center; }
552
+ .ss-w-stat b { display: block; font-variant-numeric: tabular-nums; }
553
+ .ss-w-stat span { font-size: 9px; text-transform: uppercase; color: var(--ss-w-faint); }
554
+ .ss-w-chev { color: var(--ss-w-faint); font-size: 16px; display: grid; place-items: center; }
555
+ .ss-w-cards-more { align-self: flex-start; font-size: 11.5px; font-weight: 560; color: var(--ss-w-accent); background: none; border: none; cursor: pointer; padding: 3px 2px; }
556
+ .ss-w-cards-more:hover:not(:disabled) { text-decoration: underline; }
557
+ .ss-w-ecard:disabled, .ss-w-cards-more:disabled { opacity: .5; cursor: default; }
558
+
559
+ /* action buttons */
560
+ .ss-w-actions { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
561
+ .ss-w-abtn { font-size: 12px; font-weight: 560; color: var(--ss-w-accent); background: var(--ss-w-panel); border: 1px solid var(--ss-w-accent); border-radius: 16px; padding: 6px 12px; cursor: pointer; }
562
+ .ss-w-abtn:disabled { opacity: .5; cursor: default; }
563
+ .ss-w-abtn-shimmer { width: 140px; height: 30px; border-radius: 16px; background: linear-gradient(90deg, var(--ss-w-bubble), var(--ss-w-line), var(--ss-w-bubble)); background-size: 200% 100%; animation: ss-w-shimmer 1.2s infinite; }
564
+ @keyframes ss-w-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
565
+
566
+ /* intent chips */
567
+ .ss-w-chips { display: flex; flex-wrap: wrap; gap: 6px; }
568
+ .ss-w-chip { font-size: 11.5px; font-weight: 550; color: var(--ss-w-ink); background: var(--ss-w-bubble); border: 1px solid var(--ss-w-line); border-radius: 14px; padding: 5px 11px; cursor: pointer; }
569
+
570
+ /* input */
571
+ .ss-w-input { display: flex; align-items: flex-end; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--ss-w-line); }
572
+ .ss-w-textarea { flex: 1; resize: none; border: 1px solid var(--ss-w-line); border-radius: 12px; padding: 8px 10px; font: inherit; color: inherit; background: var(--ss-w-panel); max-height: 96px; }
573
+ .ss-w-textarea:disabled { opacity: .55; cursor: not-allowed; }
574
+ .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; }
575
+ .ss-w-stop-glyph { width: 11px; height: 11px; background: currentColor; border-radius: 2px; }
576
+
577
+ @media (prefers-reduced-motion: reduce) {
578
+ .ss-w-abtn-shimmer { animation: none; }
579
+ }
580
+ `;
423
581
 
424
582
  // src/StatsWidget.tsx
425
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
583
+ import { jsx as jsx13 } from "react/jsx-runtime";
426
584
  function buildClient(props) {
427
585
  try {
428
586
  if ("client" in props && props.client) {
@@ -450,18 +608,24 @@ function StatsWidget(props) {
450
608
  const built = useMemo2(() => buildClient(props), [injectedClient, cfg?.operatorId, cfg?.publicKey, cfg?.baseUrl]);
451
609
  const ui = useMemo2(() => normalizeUi(props.config ?? { operatorId: "", publicKey: "" }), [props.config]);
452
610
  const rootRef = useRef3(null);
453
- const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters);
454
- useEffect3(() => {
611
+ const controller = useChatStream(built?.conversation ?? emptyConversation(), ui.starters, built ? ui.greetingMessage : void 0);
612
+ useEffect4(() => {
455
613
  if (!built || !ui.injectStyles) return;
456
614
  const node = rootRef.current?.getRootNode();
457
615
  if (node) injectStyles(node);
458
616
  }, [built, ui.injectStyles]);
459
617
  if (!built) return null;
460
618
  const attrs = themeAttrs(ui);
461
- return /* @__PURE__ */ jsxs10("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: [
462
- controller.state.isOpen ? /* @__PURE__ */ jsx13(ChatPanel, { controller, ui }) : null,
463
- /* @__PURE__ */ jsx13(ChatFab, { label: ui.launcherLabel, open: controller.state.isOpen, greeting: ui.greeting, onToggle: controller.toggle })
464
- ] });
619
+ return /* @__PURE__ */ jsx13("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__ */ jsx13(ChatPanel, { controller, ui }) : /* @__PURE__ */ jsx13(
620
+ ChatFab,
621
+ {
622
+ label: ui.launcherLabel,
623
+ greeting: ui.greeting,
624
+ popupText: controller.state.popupText,
625
+ onOpen: controller.open,
626
+ onDismissPopup: controller.dismissPopup
627
+ }
628
+ ) });
465
629
  }
466
630
  function emptyConversation() {
467
631
  return { send: () => {