@pixygon/chatbot-react 0.1.3 → 0.1.4

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.d.ts CHANGED
@@ -213,6 +213,7 @@ declare function createChatbotComponents(cfg: ChatbotComponentConfig): {
213
213
  }) => Promise<void>;
214
214
  embedScriptUrl?: string;
215
215
  }) => react.JSX.Element | null;
216
+ ChatLauncher: () => react.JSX.Element;
216
217
  KnowledgePageBase: (props: {
217
218
  tenantId?: string | null;
218
219
  }) => react.JSX.Element;
package/dist/index.js CHANGED
@@ -1044,8 +1044,248 @@ function createChatbotSettings(cfg) {
1044
1044
  };
1045
1045
  }
1046
1046
 
1047
+ // src/pages/ChatLauncher.tsx
1048
+ import { useEffect as useEffect4, useRef as useRef3, useState as useState6 } from "react";
1049
+ import {
1050
+ Box as Box6,
1051
+ Typography as Typography6,
1052
+ IconButton as IconButton5,
1053
+ Fab,
1054
+ TextField as TextField5,
1055
+ Stack as Stack6,
1056
+ Tooltip as Tooltip5,
1057
+ Chip as Chip6,
1058
+ Slide,
1059
+ Paper,
1060
+ Divider as Divider3,
1061
+ Alert as Alert5
1062
+ } from "@mui/material";
1063
+ import {
1064
+ ChatBubble,
1065
+ Close as Close3,
1066
+ Send as Send3,
1067
+ RestartAlt as RestartAlt2,
1068
+ ThumbUp as ThumbUp4,
1069
+ ThumbDown as ThumbDown4
1070
+ } from "@mui/icons-material";
1071
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1072
+ function createChatLauncher(cfg, hooks) {
1073
+ return function ChatLauncher() {
1074
+ const tenantId = cfg.useTenantId();
1075
+ const [open, setOpen] = useState6(false);
1076
+ const [sessionId, setSessionId] = useState6(null);
1077
+ const [draft, setDraft] = useState6("");
1078
+ const [optimistic, setOptimistic] = useState6([]);
1079
+ const [error, setError] = useState6(null);
1080
+ const scrollRef = useRef3(null);
1081
+ const [sendMessage, { isLoading: sending }] = hooks.useSendMessageMutation();
1082
+ const [rateMessage] = hooks.useRateMessageMutation();
1083
+ const { data: conversation } = hooks.useGetConversationQuery(
1084
+ { tenantId, sessionId },
1085
+ { skip: !tenantId || !sessionId || !open }
1086
+ );
1087
+ const display = [
1088
+ ...conversation?.messages || [],
1089
+ ...optimistic.filter((o) => !(conversation?.messages || []).some(
1090
+ (m) => m.content === o.content && m.role === o.role
1091
+ ))
1092
+ ];
1093
+ useEffect4(() => {
1094
+ if (!open) return;
1095
+ scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
1096
+ }, [display.length, open]);
1097
+ const handleSend = async () => {
1098
+ if (!draft.trim() || sending || !tenantId) return;
1099
+ setError(null);
1100
+ const localUser = { _localId: `local-${Date.now()}`, role: "user", content: draft.trim() };
1101
+ const localPending = { _localId: `pending-${Date.now()}`, role: "assistant", content: "Thinking\u2026", pending: true };
1102
+ setOptimistic((prev) => [...prev, localUser, localPending]);
1103
+ setDraft("");
1104
+ try {
1105
+ const res = await sendMessage({
1106
+ tenantId,
1107
+ sessionId: sessionId || void 0,
1108
+ message: localUser.content
1109
+ }).unwrap();
1110
+ if (!sessionId) setSessionId(res.sessionId);
1111
+ setOptimistic((prev) => prev.filter((p) => p._localId !== localPending._localId));
1112
+ } catch (e) {
1113
+ setError(e?.data?.error || "Send failed");
1114
+ setOptimistic((prev) => prev.filter((p) => !p.pending));
1115
+ }
1116
+ };
1117
+ const handleRate = async (messageId, rating) => {
1118
+ if (!conversation || !tenantId) return;
1119
+ await rateMessage({ tenantId, conversationId: conversation._id, messageId, rating });
1120
+ };
1121
+ const handleNewChat = () => {
1122
+ setSessionId(null);
1123
+ setOptimistic([]);
1124
+ setError(null);
1125
+ };
1126
+ const primary = cfg.brand.primaryColor;
1127
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1128
+ /* @__PURE__ */ jsx6(
1129
+ Fab,
1130
+ {
1131
+ color: "primary",
1132
+ "aria-label": open ? "Close assistant" : "Open assistant",
1133
+ onClick: () => setOpen((o) => !o),
1134
+ sx: {
1135
+ position: "fixed",
1136
+ right: 24,
1137
+ bottom: 24,
1138
+ zIndex: 1300,
1139
+ bgcolor: primary,
1140
+ "&:hover": { bgcolor: primary, filter: "brightness(0.92)" }
1141
+ },
1142
+ children: open ? /* @__PURE__ */ jsx6(Close3, {}) : /* @__PURE__ */ jsx6(ChatBubble, {})
1143
+ }
1144
+ ),
1145
+ /* @__PURE__ */ jsx6(Slide, { direction: "up", in: open, mountOnEnter: true, unmountOnExit: true, children: /* @__PURE__ */ jsxs6(
1146
+ Paper,
1147
+ {
1148
+ elevation: 8,
1149
+ sx: {
1150
+ position: "fixed",
1151
+ right: 24,
1152
+ bottom: 96,
1153
+ zIndex: 1299,
1154
+ width: { xs: "calc(100vw - 48px)", sm: 380 },
1155
+ height: { xs: "calc(100vh - 144px)", sm: 580 },
1156
+ maxHeight: "80vh",
1157
+ display: "flex",
1158
+ flexDirection: "column",
1159
+ borderRadius: 2,
1160
+ overflow: "hidden"
1161
+ },
1162
+ children: [
1163
+ /* @__PURE__ */ jsxs6(Box6, { sx: {
1164
+ p: 1.5,
1165
+ bgcolor: primary,
1166
+ color: "white",
1167
+ display: "flex",
1168
+ alignItems: "center",
1169
+ gap: 1
1170
+ }, children: [
1171
+ /* @__PURE__ */ jsxs6(Box6, { sx: { flex: 1 }, children: [
1172
+ /* @__PURE__ */ jsxs6(Typography6, { variant: "subtitle2", fontWeight: 700, children: [
1173
+ cfg.brand.name,
1174
+ " Assistant"
1175
+ ] }),
1176
+ cfg.brand.tagline && /* @__PURE__ */ jsx6(Typography6, { variant: "caption", sx: { opacity: 0.85, display: "block" }, children: cfg.brand.tagline })
1177
+ ] }),
1178
+ /* @__PURE__ */ jsx6(Tooltip5, { title: "New chat", children: /* @__PURE__ */ jsx6(IconButton5, { size: "small", onClick: handleNewChat, sx: { color: "white" }, children: /* @__PURE__ */ jsx6(RestartAlt2, { fontSize: "small" }) }) }),
1179
+ /* @__PURE__ */ jsx6(Tooltip5, { title: "Close", children: /* @__PURE__ */ jsx6(IconButton5, { size: "small", onClick: () => setOpen(false), sx: { color: "white" }, children: /* @__PURE__ */ jsx6(Close3, { fontSize: "small" }) }) })
1180
+ ] }),
1181
+ /* @__PURE__ */ jsx6(
1182
+ Box6,
1183
+ {
1184
+ ref: scrollRef,
1185
+ sx: { flex: 1, overflowY: "auto", p: 1.5, bgcolor: "background.default" },
1186
+ children: !tenantId ? /* @__PURE__ */ jsx6(Alert5, { severity: "info", children: "Select a tenant to start chatting." }) : display.length === 0 ? /* @__PURE__ */ jsxs6(Stack6, { alignItems: "center", sx: { py: 6 }, spacing: 1.5, children: [
1187
+ /* @__PURE__ */ jsx6(ChatBubble, { sx: { fontSize: 40, color: "text.secondary" } }),
1188
+ /* @__PURE__ */ jsx6(Typography6, { variant: "body2", color: "text.secondary", align: "center", children: "Ask anything \u2014 answers come from your knowledge base only." })
1189
+ ] }) : /* @__PURE__ */ jsx6(Stack6, { spacing: 1.5, children: display.map((turn, i) => /* @__PURE__ */ jsx6(
1190
+ CompactTurn,
1191
+ {
1192
+ turn,
1193
+ onRate: turn._id ? (r) => handleRate(turn._id, r) : void 0
1194
+ },
1195
+ turn._id || turn._localId || String(i)
1196
+ )) })
1197
+ }
1198
+ ),
1199
+ /* @__PURE__ */ jsx6(Divider3, {}),
1200
+ /* @__PURE__ */ jsxs6(Box6, { sx: { p: 1, bgcolor: "background.paper" }, children: [
1201
+ error && /* @__PURE__ */ jsx6(Alert5, { severity: "error", sx: { mb: 1 }, children: error }),
1202
+ /* @__PURE__ */ jsxs6(Stack6, { direction: "row", spacing: 0.5, alignItems: "flex-end", children: [
1203
+ /* @__PURE__ */ jsx6(
1204
+ TextField5,
1205
+ {
1206
+ fullWidth: true,
1207
+ size: "small",
1208
+ multiline: true,
1209
+ maxRows: 4,
1210
+ placeholder: "Ask the assistant\u2026",
1211
+ value: draft,
1212
+ onChange: (e) => setDraft(e.target.value),
1213
+ onKeyDown: (e) => {
1214
+ if (e.key === "Enter" && !e.shiftKey) {
1215
+ e.preventDefault();
1216
+ handleSend();
1217
+ }
1218
+ },
1219
+ disabled: sending || !tenantId
1220
+ }
1221
+ ),
1222
+ /* @__PURE__ */ jsx6(
1223
+ IconButton5,
1224
+ {
1225
+ color: "primary",
1226
+ onClick: handleSend,
1227
+ disabled: !draft.trim() || sending || !tenantId,
1228
+ children: /* @__PURE__ */ jsx6(Send3, {})
1229
+ }
1230
+ )
1231
+ ] })
1232
+ ] })
1233
+ ]
1234
+ }
1235
+ ) })
1236
+ ] });
1237
+ };
1238
+ }
1239
+ function CompactTurn({ turn, onRate }) {
1240
+ const isUser = turn.role === "user";
1241
+ return /* @__PURE__ */ jsx6(Box6, { sx: { display: "flex", justifyContent: isUser ? "flex-end" : "flex-start" }, children: /* @__PURE__ */ jsxs6(Box6, { sx: { maxWidth: "85%" }, children: [
1242
+ /* @__PURE__ */ jsx6(
1243
+ Box6,
1244
+ {
1245
+ sx: {
1246
+ bgcolor: isUser ? "primary.main" : "background.paper",
1247
+ color: isUser ? "primary.contrastText" : "text.primary",
1248
+ px: 1.5,
1249
+ py: 1,
1250
+ borderRadius: 1.5,
1251
+ border: "1px solid",
1252
+ borderColor: isUser ? "primary.main" : "divider",
1253
+ whiteSpace: "pre-wrap",
1254
+ wordBreak: "break-word",
1255
+ opacity: turn.pending ? 0.6 : 1,
1256
+ fontSize: 14
1257
+ },
1258
+ children: /* @__PURE__ */ jsx6(Typography6, { variant: "body2", children: turn.content })
1259
+ }
1260
+ ),
1261
+ !isUser && turn.citations?.length > 0 && /* @__PURE__ */ jsx6(Stack6, { direction: "row", spacing: 0.5, sx: { mt: 0.5, flexWrap: "wrap", gap: 0.5 }, children: turn.citations.map((c, i) => /* @__PURE__ */ jsx6(Tooltip5, { title: c.snippet, children: /* @__PURE__ */ jsx6(Chip6, { label: `[${i + 1}]`, size: "small", variant: "outlined" }) }, c.chunkId || i)) }),
1262
+ !isUser && onRate && /* @__PURE__ */ jsxs6(Stack6, { direction: "row", spacing: 0.5, sx: { mt: 0.25 }, children: [
1263
+ /* @__PURE__ */ jsx6(
1264
+ IconButton5,
1265
+ {
1266
+ size: "small",
1267
+ onClick: () => onRate(1),
1268
+ color: turn.rating === 1 ? "primary" : "default",
1269
+ sx: { p: 0.25 },
1270
+ children: /* @__PURE__ */ jsx6(ThumbUp4, { sx: { fontSize: 14 } })
1271
+ }
1272
+ ),
1273
+ /* @__PURE__ */ jsx6(
1274
+ IconButton5,
1275
+ {
1276
+ size: "small",
1277
+ onClick: () => onRate(-1),
1278
+ color: turn.rating === -1 ? "error" : "default",
1279
+ sx: { p: 0.25 },
1280
+ children: /* @__PURE__ */ jsx6(ThumbDown4, { sx: { fontSize: 14 } })
1281
+ }
1282
+ )
1283
+ ] })
1284
+ ] }) });
1285
+ }
1286
+
1047
1287
  // src/index.tsx
1048
- import { jsx as jsx6 } from "react/jsx-runtime";
1288
+ import { jsx as jsx7 } from "react/jsx-runtime";
1049
1289
  function createChatbotComponents(cfg) {
1050
1290
  const resolved = resolveConfig(cfg);
1051
1291
  const hooks = injectChatbotEndpoints(cfg.apiSlice, { pathPrefix: cfg.pathPrefix });
@@ -1054,17 +1294,18 @@ function createChatbotComponents(cfg) {
1054
1294
  const ChatAnalyticsPageBase = createChatAnalyticsPage(resolved, hooks);
1055
1295
  const EmbedChatPageBase = createEmbedChatPage(resolved);
1056
1296
  const ChatbotSettings = createChatbotSettings(resolved);
1297
+ const ChatLauncher = createChatLauncher(resolved, hooks);
1057
1298
  const KnowledgePage = () => {
1058
1299
  const tenantId = cfg.useTenantId();
1059
- return /* @__PURE__ */ jsx6(KnowledgePageBase, { tenantId });
1300
+ return /* @__PURE__ */ jsx7(KnowledgePageBase, { tenantId });
1060
1301
  };
1061
1302
  const ChatPage = () => {
1062
1303
  const tenantId = cfg.useTenantId();
1063
- return /* @__PURE__ */ jsx6(ChatPageBase, { tenantId });
1304
+ return /* @__PURE__ */ jsx7(ChatPageBase, { tenantId });
1064
1305
  };
1065
1306
  const ChatAnalyticsPage = () => {
1066
1307
  const tenantId = cfg.useTenantId();
1067
- return /* @__PURE__ */ jsx6(ChatAnalyticsPageBase, { tenantId });
1308
+ return /* @__PURE__ */ jsx7(ChatAnalyticsPageBase, { tenantId });
1068
1309
  };
1069
1310
  return {
1070
1311
  hooks,
@@ -1073,6 +1314,7 @@ function createChatbotComponents(cfg) {
1073
1314
  ChatAnalyticsPage,
1074
1315
  EmbedChatPage: EmbedChatPageBase,
1075
1316
  ChatbotSettings,
1317
+ ChatLauncher,
1076
1318
  // Base variants if the host wants to pass tenantId explicitly.
1077
1319
  KnowledgePageBase,
1078
1320
  ChatPageBase,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixygon/chatbot-react",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "React + MUI pages for the Pixygon chatbot: knowledge base admin, chat, analytics, embeddable widget.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",