@sensiblestats/widget-react 0.1.0 → 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/CHANGELOG.md +24 -0
- package/dist/index.cjs +190 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +221 -57
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -2
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/StatsWidget.tsx
|
|
2
|
-
import { useEffect as
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
153
|
-
|
|
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)
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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__ */
|
|
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
|
|
225
|
-
function ChatFab({ label,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
/* @__PURE__ */ jsx3(
|
|
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
|
|
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
|
|
278
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
237
279
|
function ChatHeader({ onClose }) {
|
|
238
|
-
return /* @__PURE__ */
|
|
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
|
|
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__ */
|
|
276
|
-
visible.map((c) => /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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__ */
|
|
462
|
-
|
|
463
|
-
|
|
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: () => {
|