@smart-cloud/ai-kit-ui 1.1.11 → 1.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smart-cloud/ai-kit-ui",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -19,9 +19,11 @@
19
19
  "dependencies": {
20
20
  "@emotion/cache": "^11.14.0",
21
21
  "@emotion/react": "^11.14.0",
22
- "@smart-cloud/ai-kit-core": "^1.1.4",
22
+ "@mantine/colors-generator": "^8.3.13",
23
+ "@smart-cloud/ai-kit-core": "^1.1.5",
23
24
  "@smart-cloud/wpsuite-core": "^2.0.5",
24
25
  "@tabler/icons-react": "^3.36.1",
26
+ "chroma-js": "^3.2.0",
25
27
  "react-markdown": "^10.1.0",
26
28
  "rehype-sanitize": "^6.0.0",
27
29
  "rehype-stringify": "^10.0.1",
@@ -12,9 +12,6 @@ export type ShadowBoundaryProps = {
12
12
  /** Optional class name applied to the host element. */
13
13
  className?: string;
14
14
 
15
- /** Optional raw CSS text injected into the shadow root (as <style>). */
16
- innerCSS?: string;
17
-
18
15
  /** ID of the element inside the shadow root used as the portal target. */
19
16
  rootElementId: string;
20
17
 
@@ -89,22 +86,9 @@ function installAiKitPropertyRegistry() {
89
86
  doc.head.appendChild(style);
90
87
  }
91
88
 
92
- // tiny stable hash to detect innerCSS changes without forcing huge deps churn
93
- function hashStringDjb2(str: string): string {
94
- let hash = 5381;
95
- for (let i = 0; i < str.length; i++) {
96
- hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
97
- }
98
- // unsigned + base36
99
- return (hash >>> 0).toString(36);
100
- }
101
-
102
- const STYLE_TEXT_ID = "ai-kit-style-text";
103
-
104
89
  export function ShadowBoundary({
105
90
  stylesheets,
106
91
  className,
107
- innerCSS,
108
92
  children,
109
93
  rootElementId,
110
94
  mode = "local",
@@ -121,10 +105,6 @@ export function ShadowBoundary({
121
105
  return all.join("|");
122
106
  }, [stylesheets]);
123
107
 
124
- const innerCSSHash = useMemo(() => {
125
- return innerCSS ? hashStringDjb2(innerCSS) : "";
126
- }, [innerCSS]);
127
-
128
108
  useLayoutEffect(() => {
129
109
  if (!hostRef.current) return;
130
110
 
@@ -206,29 +186,6 @@ export function ShadowBoundary({
206
186
  stylesKey ? stylesKey.split("|") : [],
207
187
  );
208
188
 
209
- // 6) Optional: inject raw style text into the shadow root
210
- const existingStyle = shadow.getElementById(
211
- STYLE_TEXT_ID,
212
- ) as HTMLStyleElement | null;
213
-
214
- if (innerCSS) {
215
- if (!existingStyle) {
216
- const s = doc.createElement("style");
217
- s.id = STYLE_TEXT_ID;
218
- s.setAttribute("data-hash", innerCSSHash);
219
- s.textContent = innerCSS;
220
- rootEl.appendChild(s);
221
- } else {
222
- const prevHash = existingStyle.getAttribute("data-hash") || "";
223
- if (prevHash !== innerCSSHash) {
224
- existingStyle.setAttribute("data-hash", innerCSSHash);
225
- existingStyle.textContent = innerCSS;
226
- }
227
- }
228
- } else if (existingStyle) {
229
- existingStyle.remove();
230
- }
231
-
232
189
  setShadowRoot(shadow);
233
190
  setPortalTarget(rootEl);
234
191
 
@@ -236,7 +193,7 @@ export function ShadowBoundary({
236
193
  mo.disconnect();
237
194
  mq?.removeEventListener?.("change", onMq);
238
195
  };
239
- }, [mode, overlayRootId, rootElementId, stylesKey, innerCSS, innerCSSHash]);
196
+ }, [mode, overlayRootId, rootElementId, stylesKey]);
240
197
 
241
198
  const emotionCache = useMemo(() => {
242
199
  if (!portalTarget) return null;
@@ -57,7 +57,7 @@ const HISTORY_STORAGE_KEY = `ai-kit-chatbot-history-v1:${
57
57
  typeof window !== "undefined" ? window.location.hostname : "unknown"
58
58
  }`;
59
59
 
60
- const DEFAULT_LABELS: Required<AiChatbotLabels> = {
60
+ export const DEFAULT_CHATBOT_LABELS: Required<AiChatbotLabels> = {
61
61
  modalTitle: "AI Assistant",
62
62
 
63
63
  userLabel: "User",
@@ -234,7 +234,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
234
234
  } = props;
235
235
 
236
236
  const labels = useMemo(
237
- () => ({ ...DEFAULT_LABELS, ...(labelsOverride || {}) }),
237
+ () => ({ ...DEFAULT_CHATBOT_LABELS, ...(labelsOverride || {}) }),
238
238
  [labelsOverride],
239
239
  );
240
240
 
@@ -253,6 +253,10 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
253
253
  const [opened, setOpened] = useState(false);
254
254
  const [stickToBottom, setStickToBottom] = useState(true);
255
255
 
256
+ const [wheelHostEl, setWheelHostEl] = useState<HTMLDivElement | null>(null);
257
+ const [scrollerEl, setScrollerEl] = useState<HTMLDivElement | null>(null);
258
+ const [bodyScrollable, setBodyScrollable] = useState(false);
259
+
256
260
  const [activeOp, setActiveOp] = useState<ActiveOp>(null);
257
261
  const activeOpRef = useRef<ActiveOp>(activeOp);
258
262
  useEffect(() => {
@@ -267,7 +271,6 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
267
271
  const fileInputRef = useRef<HTMLInputElement>(null);
268
272
  const questionInputRef = useRef<HTMLTextAreaElement>(null);
269
273
  const sessionRef = useRef<{ id: string; storedAt: number } | null>(null);
270
- const chatScrollRef = useRef<HTMLDivElement>(null);
271
274
  const chatContainerRef = useRef<HTMLDivElement>(null);
272
275
 
273
276
  // New: persist timestamp of last actually-sent user message
@@ -356,18 +359,13 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
356
359
  const viewportMax = Math.floor(vh * 0.8);
357
360
  const target = Math.max(minHeight, Math.min(viewportMax, maxHeightCap));
358
361
  el.style.height = `${target}px`;
359
- const scrollEl = chatScrollRef.current;
360
- if (scrollEl) {
361
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
362
- scrollEl.offsetHeight;
363
- }
364
362
  } catch {
365
363
  // ignore
366
364
  }
367
365
  }, []);
368
366
 
369
367
  const scrollToBottom = useCallback(() => {
370
- const el = chatScrollRef.current;
368
+ const el = scrollerEl;
371
369
  if (!el) return;
372
370
  window.setTimeout(() => {
373
371
  try {
@@ -376,7 +374,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
376
374
  // ignore
377
375
  }
378
376
  }, 50);
379
- }, []);
377
+ }, [scrollerEl]);
380
378
 
381
379
  const closeModal = useCallback(() => {
382
380
  setOpened(false);
@@ -393,19 +391,12 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
393
391
  }, [opened, adjustChatHeight]);
394
392
 
395
393
  useEffect(() => {
396
- if (!opened) return;
394
+ if (!opened || !isMaximized) return;
397
395
  document.body.style.overflow = "hidden";
398
- document.body.onkeydown = (e: KeyboardEvent) => {
399
- if (e.key === "Escape") {
400
- e.preventDefault();
401
- closeModal();
402
- }
403
- };
404
396
  return () => {
405
397
  document.body.style.overflow = "";
406
- document.body.onkeydown = null;
407
398
  };
408
- }, [opened, closeModal]);
399
+ }, [opened, isMaximized, closeModal]);
409
400
 
410
401
  const imagePreviews = useMemo(() => {
411
402
  if (
@@ -435,7 +426,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
435
426
  }, [imagePreviews]);
436
427
 
437
428
  useEffect(() => {
438
- const el = chatScrollRef.current;
429
+ const el = scrollerEl;
439
430
  if (!el) return;
440
431
  const handleScroll = () => {
441
432
  const distanceFromBottom =
@@ -446,16 +437,16 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
446
437
  return () => {
447
438
  el.removeEventListener("scroll", handleScroll);
448
439
  };
449
- }, [opened]);
440
+ }, [opened, scrollerEl]);
450
441
 
451
442
  useEffect(() => {
452
443
  if (!stickToBottom) return;
453
- const el = chatScrollRef.current;
444
+ const el = scrollerEl;
454
445
  if (!el) return;
455
446
  if (el.scrollHeight > el.clientHeight) {
456
447
  el.scrollTop = el.scrollHeight;
457
448
  }
458
- }, [messages, ai.busy, stickToBottom]);
449
+ }, [messages, ai.busy, stickToBottom, scrollerEl]);
459
450
 
460
451
  const statusText = useMemo(() => {
461
452
  if (!ai.busy) return null;
@@ -849,10 +840,16 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
849
840
  const renderOpenButtonIcon = useMemo(() => {
850
841
  if (!showOpenButtonIcon) return null;
851
842
  if (openButtonIcon) {
852
- return <span dangerouslySetInnerHTML={{ __html: openButtonIcon }} />;
843
+ return (
844
+ <img
845
+ src={openButtonIcon}
846
+ className="ai-open-btn-icon"
847
+ alt={I18n.get(labels.askMeLabel || openButtonLabel)}
848
+ />
849
+ );
853
850
  }
854
851
  return <IconMessage size={18} />;
855
- }, [showOpenButtonIcon, openButtonIcon]);
852
+ }, [showOpenButtonIcon, openButtonIcon, labels, openButtonLabel, language]);
856
853
 
857
854
  const openButtonContent = useMemo(() => {
858
855
  const iconEl = renderOpenButtonIcon;
@@ -942,6 +939,69 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
942
939
  void ask();
943
940
  }, [isChatBusy, cancelChat, ask]);
944
941
 
942
+ useEffect(() => {
943
+ if (!opened) return;
944
+ if (!wheelHostEl || !scrollerEl) return;
945
+
946
+ const isScrollableNow = () => {
947
+ const cs = window.getComputedStyle(scrollerEl);
948
+ const overflowY = cs.overflowY;
949
+ const canOverflow = overflowY === "auto" || overflowY === "scroll";
950
+
951
+ const sh = Math.ceil(scrollerEl.scrollHeight);
952
+ const ch = Math.floor(scrollerEl.clientHeight);
953
+ const hasOverflow = sh > ch;
954
+ return canOverflow && hasOverflow;
955
+ };
956
+ let enabled = false;
957
+
958
+ const onWheel = (e: WheelEvent) => {
959
+ if (!isScrollableNow()) return;
960
+
961
+ e.preventDefault();
962
+
963
+ const max = scrollerEl.scrollHeight - scrollerEl.clientHeight;
964
+ scrollerEl.scrollTop = Math.max(
965
+ 0,
966
+ Math.min(max, scrollerEl.scrollTop + e.deltaY),
967
+ );
968
+ };
969
+
970
+ const sync = () => {
971
+ const shouldEnable = isScrollableNow();
972
+ setBodyScrollable(shouldEnable);
973
+ if (shouldEnable === enabled) return;
974
+ enabled = shouldEnable;
975
+ if (enabled) {
976
+ wheelHostEl.addEventListener("wheel", onWheel, { passive: false });
977
+ } else {
978
+ wheelHostEl.removeEventListener("wheel", onWheel as EventListener);
979
+ }
980
+ };
981
+
982
+ sync();
983
+
984
+ const ro = new ResizeObserver(sync);
985
+ ro.observe(scrollerEl);
986
+
987
+ const mo = new MutationObserver(sync);
988
+ mo.observe(scrollerEl, {
989
+ childList: true,
990
+ subtree: true,
991
+ characterData: true,
992
+ });
993
+
994
+ window.addEventListener("resize", sync);
995
+
996
+ return () => {
997
+ if (enabled)
998
+ wheelHostEl.removeEventListener("wheel", onWheel as EventListener);
999
+ ro.disconnect();
1000
+ mo.disconnect();
1001
+ window.removeEventListener("resize", sync);
1002
+ };
1003
+ }, [opened, wheelHostEl, scrollerEl]);
1004
+
945
1005
  // -----------------------------
946
1006
  // History persistence
947
1007
  // -----------------------------
@@ -1063,6 +1123,9 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1063
1123
  <Modal.Root
1064
1124
  ref={chatContainerRef}
1065
1125
  opened={opened}
1126
+ lockScroll={false}
1127
+ trapFocus={false}
1128
+ closeOnEscape={true}
1066
1129
  onClose={closeModal}
1067
1130
  className={
1068
1131
  rootClassName +
@@ -1074,7 +1137,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1074
1137
  data-ai-kit-theme={colorMode}
1075
1138
  data-ai-kit-variation="modal"
1076
1139
  >
1077
- <Modal.Body className="ai-chat-container-internal">
1140
+ <div className="ai-chat-container-internal" ref={setWheelHostEl}>
1078
1141
  <Modal.Header className="ai-chat-header-bar">
1079
1142
  <Modal.Title className="ai-chat-title">{modalTitle}</Modal.Title>
1080
1143
  <Group gap="4px" align="center" justify="center">
@@ -1093,6 +1156,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1093
1156
  ? I18n.get(labels.restoreSizeLabel)
1094
1157
  : I18n.get(labels.maximizeLabel)
1095
1158
  }
1159
+ data-ai-kit-maximize-button
1096
1160
  >
1097
1161
  {isMaximized ? (
1098
1162
  <IconMinimize size={16} />
@@ -1107,7 +1171,11 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1107
1171
  </Group>
1108
1172
  </Modal.Header>
1109
1173
 
1110
- <Modal.Body className="ai-chat-scroll" ref={chatScrollRef}>
1174
+ <Modal.Body
1175
+ className="ai-chat-scroll"
1176
+ ref={setScrollerEl}
1177
+ data-scrollable={bodyScrollable ? "true" : "false"}
1178
+ >
1111
1179
  {messages.map((msg) => {
1112
1180
  const isUser = msg.role === "user";
1113
1181
  const isLastCanceled =
@@ -1129,16 +1197,23 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1129
1197
  >
1130
1198
  <Stack
1131
1199
  gap={4}
1132
- style={{ alignItems: isUser ? "flex-end" : "flex-start" }}
1200
+ style={{
1201
+ alignItems: isUser ? "flex-end" : "flex-start",
1202
+ }}
1133
1203
  >
1134
1204
  <Stack className="ai-chat-bubble">
1135
1205
  <Text className="ai-chat-header">
1136
- <Text fw="bolder" size="xs">
1206
+ <Text
1207
+ fw="bolder"
1208
+ size="xs"
1209
+ style={{ whiteSpace: "nowrap" }}
1210
+ >
1137
1211
  {isUser
1138
1212
  ? I18n.get(labels.userLabel)
1139
1213
  : I18n.get(labels.assistantLabel)}
1140
1214
  </Text>
1141
- <Text size="xs">
1215
+ &nbsp;
1216
+ <Text size="xs" style={{ whiteSpace: "nowrap" }}>
1142
1217
  {new Date(msg.createdAt).toLocaleTimeString([], {
1143
1218
  hour: "2-digit",
1144
1219
  minute: "2-digit",
@@ -1169,6 +1244,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1169
1244
  onClick={() => handleEditCanceled(msg)}
1170
1245
  title={I18n.get(labels.editLabel)}
1171
1246
  aria-label={I18n.get(labels.editLabel)}
1247
+ data-ai-kit-edit-button
1172
1248
  >
1173
1249
  <IconPencil size={14} />
1174
1250
  </ActionIcon>
@@ -1222,6 +1298,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1222
1298
  onClick={() => updateFeedback(msg.id, "accepted")}
1223
1299
  aria-label={I18n.get(labels.acceptResponseLabel)}
1224
1300
  disabled={ai.busy}
1301
+ data-ai-kit-feedback-accept-button
1225
1302
  >
1226
1303
  👍
1227
1304
  </Button>
@@ -1233,6 +1310,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1233
1310
  onClick={() => updateFeedback(msg.id, "rejected")}
1234
1311
  aria-label={I18n.get(labels.rejectResponseLabel)}
1235
1312
  disabled={ai.busy}
1313
+ data-ai-kit-feedback-reject-button
1236
1314
  >
1237
1315
  👎
1238
1316
  </Button>
@@ -1287,13 +1365,18 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1287
1365
  {I18n.get("Are you sure you want to reset the conversation?")}
1288
1366
  </Text>
1289
1367
  <Group justify="flex-end" mt="md">
1290
- <Button variant="default" onClick={cancelReset}>
1368
+ <Button
1369
+ variant="default"
1370
+ onClick={cancelReset}
1371
+ data-ai-kit-no-button
1372
+ >
1291
1373
  {I18n.get("No")}
1292
1374
  </Button>
1293
1375
  <Button
1294
- color="red"
1376
+ color="var(--ai-kit-color-danger, red)"
1295
1377
  onClick={confirmReset}
1296
1378
  disabled={!hasMessages && !isChatBusy}
1379
+ data-ai-kit-yes-button
1297
1380
  >
1298
1381
  {I18n.get("Yes")}
1299
1382
  </Button>
@@ -1321,6 +1404,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1321
1404
  leftSection={<IconTrash size={18} />}
1322
1405
  onClick={handleResetClick}
1323
1406
  disabled={!hasMessages && !isChatBusy}
1407
+ data-ai-kit-reset-button
1324
1408
  >
1325
1409
  {I18n.get(labels.resetLabel)}
1326
1410
  </Button>
@@ -1333,6 +1417,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1333
1417
  onClick={() => fileInputRef.current?.click()}
1334
1418
  disabled={images.length >= resolvedMaxImages}
1335
1419
  title={I18n.get(labels.addImageLabel)}
1420
+ data-ai-kit-add-image-button
1336
1421
  >
1337
1422
  {I18n.get(labels.addLabel)}
1338
1423
  </Button>
@@ -1351,6 +1436,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1351
1436
  variant="filled"
1352
1437
  onClick={onSendOrCancel}
1353
1438
  disabled={!isChatBusy && !canSend}
1439
+ data-ai-kit-send-button
1354
1440
  >
1355
1441
  {sendOrCancelLabel}
1356
1442
  </Button>
@@ -1381,6 +1467,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1381
1467
  p={0}
1382
1468
  className="remove-image-button"
1383
1469
  title={I18n.get(labels.removeImageLabel)}
1470
+ data-ai-kit-remove-image-button
1384
1471
  >
1385
1472
  X
1386
1473
  </Button>
@@ -1389,7 +1476,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1389
1476
  </Group>
1390
1477
  )}
1391
1478
  </Stack>
1392
- </Modal.Body>
1479
+ </div>
1393
1480
  </Modal.Root>
1394
1481
  )}
1395
1482
  </Group>
@@ -1 +1 @@
1
- export { AiChatbot } from "./AiChatbot";
1
+ export { AiChatbot, DEFAULT_CHATBOT_LABELS } from "./AiChatbot";
@@ -129,7 +129,7 @@
129
129
  /* Sizing */
130
130
  --ai-kit-chat-launcher-size: 60px;
131
131
 
132
- --ai-kit-chat-width: 400px;
132
+ --ai-kit-chat-width: 480px;
133
133
  --ai-kit-chat-height-min: 360px;
134
134
  --ai-kit-chat-height-max: 80vh;
135
135
  --ai-kit-chat-height-cap: 1000px;
@@ -376,6 +376,16 @@
376
376
  display: contents;
377
377
  }
378
378
 
379
+ .ai-docs-ask[data-ai-kit-theme="dark"] {
380
+ --ai-kit-chat-status-fg: var(--ai-kit-color-text);
381
+ --ai-kit-chat-status-bg: transparent;
382
+ }
383
+
384
+ .ai-open-btn-icon {
385
+ width: 24px;
386
+ height: 24px;
387
+ }
388
+
379
389
  .ai-docs-ask.ai-open-btn--bottom-right {
380
390
  --ai-kit-pos-top: auto;
381
391
  --ai-kit-pos-right: var(--ai-kit-open-button-offset-x);
@@ -428,8 +438,8 @@
428
438
 
429
439
  z-index: var(--ai-kit-position-z-index);
430
440
 
431
- width: var(--ai-kit-chat-launcher-size);
432
- height: var(--ai-kit-chat-launcher-size);
441
+ width: auto;
442
+ height: auto;
433
443
 
434
444
  /* Radius: apply both border-radius and Mantine variable */
435
445
  border-radius: var(--ai-kit-launcher-radius) !important;
@@ -453,7 +463,7 @@
453
463
  box-shadow 0.2s ease,
454
464
  background 0.2s ease;
455
465
 
456
- padding: 0;
466
+ padding: var(--ai-kit-space-3);
457
467
  pointer-events: auto;
458
468
  }
459
469
 
@@ -541,6 +551,7 @@
541
551
  margin-right: auto;
542
552
  margin-left: auto;
543
553
  flex-direction: column;
554
+ padding: 0 var(--ai-kit-space-3) var(--ai-kit-space-3) var(--ai-kit-space-3);
544
555
  }
545
556
 
546
557
  /* Input Box */
@@ -598,11 +609,14 @@
598
609
 
599
610
  /* Header / status */
600
611
  .ai-chat-header-bar {
612
+ background: var(--ai-kit-chat-surface);
601
613
  display: flex;
602
614
  align-items: center;
603
615
  justify-content: space-between;
604
- padding: 0 var(--ai-kit-space-3) var(--ai-kit-space-3) var(--ai-kit-space-4);
616
+ padding: 0 var(--ai-kit-space-3);
605
617
  border-bottom: 1px solid var(--ai-kit-chat-border-color);
618
+ margin-left: calc(-1 * var(--ai-kit-button-padding-x));
619
+ margin-right: calc(-1 * var(--ai-kit-button-padding-x));
606
620
  }
607
621
 
608
622
  .ai-status-line {
@@ -655,6 +669,11 @@
655
669
  flex: 1;
656
670
  overflow-y: auto;
657
671
  padding: var(--ai-kit-space-4);
672
+ overscroll-behavior: auto;
673
+ }
674
+
675
+ .ai-chat-scroll[data-scrollable="true"] {
676
+ overscroll-behavior: contain;
658
677
  }
659
678
 
660
679
  .ai-chat-placeholder {
@@ -676,7 +695,6 @@
676
695
  }
677
696
 
678
697
  .ai-chat-bubble {
679
- max-width: 85%;
680
698
  padding: 10px 12px;
681
699
  border-radius: var(--ai-kit-radius-sm);
682
700
  background: var(--ai-kit-assistant-bubble-bg);
@@ -687,6 +705,8 @@
687
705
  word-break: break-word;
688
706
  hyphens: auto;
689
707
  line-height: var(--ai-kit-line-height);
708
+ width: fit-content;
709
+ max-width: 80%;
690
710
  }
691
711
 
692
712
  .ai-chat-row.user .ai-chat-bubble {
@@ -781,7 +801,7 @@
781
801
 
782
802
  .ai-status-text {
783
803
  font-size: 0.85rem;
784
- color: var(--ai-kit-color-text-muted);
804
+ color: var(--ai-kit-color-text);
785
805
  }
786
806
 
787
807
  .ai-feedback {
@@ -1,3 +1,4 @@
1
+ import { generateColors } from "@mantine/colors-generator";
1
2
  import {
2
3
  colorsTuple,
3
4
  createTheme,
@@ -13,7 +14,7 @@ import {
13
14
  } from "@smart-cloud/ai-kit-core";
14
15
  import { useSelect } from "@wordpress/data";
15
16
  import { I18n } from "aws-amplify/utils";
16
- import { type ComponentType, useMemo, useState } from "react";
17
+ import { type ComponentType, useCallback, useMemo, useState } from "react";
17
18
  import { ShadowBoundary } from "./ShadowBoundary";
18
19
 
19
20
  export type AiKitShellInjectedProps = {
@@ -21,6 +22,17 @@ export type AiKitShellInjectedProps = {
21
22
  rootElement: HTMLElement;
22
23
  };
23
24
 
25
+ function hashStringDjb2(str: string): string {
26
+ let hash = 5381;
27
+ for (let i = 0; i < str.length; i++) {
28
+ hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
29
+ }
30
+ // unsigned + base36
31
+ return (hash >>> 0).toString(36);
32
+ }
33
+
34
+ const STYLE_TEXT_ID = "ai-kit-style-text";
35
+
24
36
  export function withAiKitShell<P extends object>(
25
37
  RootComponent: ComponentType<P & AiKitShellInjectedProps>,
26
38
  propOverrides?: Partial<AiWorkerProps>,
@@ -96,10 +108,16 @@ export function withAiKitShell<P extends object>(
96
108
  if (colors) {
97
109
  customColors = {};
98
110
  Object.keys(colors).forEach((c) => {
99
- customColors![c] = colorsTuple(colors[c]);
111
+ try {
112
+ customColors![c] = generateColors(colors[c]);
113
+ } catch {
114
+ customColors![c] = colorsTuple(colors[c]);
115
+ }
100
116
  });
101
117
  }
102
118
 
119
+ console.log("withAiKitShell rendered", { customColors });
120
+
103
121
  const theme = createTheme({
104
122
  respectReducedMotion: true,
105
123
  ...(customColors && { colors: customColors }),
@@ -159,13 +177,43 @@ export function withAiKitShell<P extends object>(
159
177
  },
160
178
  });
161
179
 
180
+ const innerCSSHash = useMemo(() => {
181
+ return innerCSS ? hashStringDjb2(innerCSS) : "";
182
+ }, [innerCSS]);
183
+
184
+ const injectStyle = useCallback(
185
+ (rootElement: HTMLDivElement) => {
186
+ const existingStyle = rootElement.ownerDocument.getElementById(
187
+ STYLE_TEXT_ID,
188
+ ) as HTMLStyleElement | null;
189
+
190
+ if (innerCSS) {
191
+ if (!existingStyle) {
192
+ const s = rootElement.ownerDocument.createElement("style");
193
+ s.id = STYLE_TEXT_ID;
194
+ s.setAttribute("data-hash", innerCSSHash);
195
+ s.textContent = innerCSS;
196
+ rootElement.appendChild(s);
197
+ } else {
198
+ const prevHash = existingStyle.getAttribute("data-hash") || "";
199
+ if (prevHash !== innerCSSHash) {
200
+ existingStyle.setAttribute("data-hash", innerCSSHash);
201
+ existingStyle.textContent = innerCSS;
202
+ }
203
+ }
204
+ } else if (existingStyle) {
205
+ existingStyle.remove();
206
+ }
207
+ },
208
+ [innerCSS, innerCSSHash],
209
+ );
210
+
162
211
  return (
163
212
  <ShadowBoundary
164
213
  mode={variation === "modal" && !showOpenButton ? "overlay" : "local"}
165
214
  variation={variation}
166
215
  overlayRootId="ai-kit-overlay-root"
167
216
  stylesheets={stylesheets}
168
- innerCSS={innerCSS}
169
217
  className={className}
170
218
  rootElementId={
171
219
  variation === "modal" && !showOpenButton
@@ -174,6 +222,7 @@ export function withAiKitShell<P extends object>(
174
222
  }
175
223
  >
176
224
  {({ rootElement }) => {
225
+ injectStyle(rootElement);
177
226
  rootElement.setAttribute(
178
227
  "data-ai-kit-variation",
179
228
  variation || "default",
@@ -183,16 +232,23 @@ export function withAiKitShell<P extends object>(
183
232
  rootElement.setAttribute("lang", currentLanguage);
184
233
  }
185
234
 
235
+ const resolved =
236
+ colorMode === "auto"
237
+ ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches
238
+ ? "dark"
239
+ : "light"
240
+ : colorMode;
241
+
186
242
  return (
187
243
  <DirectionProvider initialDirection={currentDirection}>
188
244
  <MantineProvider
189
- defaultColorScheme={colorMode}
245
+ forceColorScheme={resolved}
190
246
  theme={theme}
191
- cssVariablesSelector={`#${rootElement.id}`}
192
247
  getRootElement={() => rootElement as unknown as HTMLElement}
193
248
  >
194
249
  <RootComponent
195
250
  {...props}
251
+ colorMode={resolved}
196
252
  language={currentLanguage}
197
253
  rootElement={rootElement}
198
254
  />