@smart-cloud/ai-kit-ui 1.1.10 → 1.1.12

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.10",
3
+ "version": "1.1.12",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -19,7 +19,7 @@
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
+ "@smart-cloud/ai-kit-core": "^1.1.5",
23
23
  "@smart-cloud/wpsuite-core": "^2.0.5",
24
24
  "@tabler/icons-react": "^3.36.1",
25
25
  "react-markdown": "^10.1.0",
@@ -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,9 +1123,13 @@ 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
- "ai-chat-container" +
1131
+ rootClassName +
1132
+ " ai-chat-container" +
1069
1133
  (isMaximized ? " maximized" : "") +
1070
1134
  (isMaximized && maxEnter ? " ai-max-enter" : "")
1071
1135
  }
@@ -1073,7 +1137,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1073
1137
  data-ai-kit-theme={colorMode}
1074
1138
  data-ai-kit-variation="modal"
1075
1139
  >
1076
- <Modal.Body className="ai-chat-container-internal">
1140
+ <div className="ai-chat-container-internal" ref={setWheelHostEl}>
1077
1141
  <Modal.Header className="ai-chat-header-bar">
1078
1142
  <Modal.Title className="ai-chat-title">{modalTitle}</Modal.Title>
1079
1143
  <Group gap="4px" align="center" justify="center">
@@ -1106,7 +1170,11 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1106
1170
  </Group>
1107
1171
  </Modal.Header>
1108
1172
 
1109
- <Modal.Body className="ai-chat-scroll" ref={chatScrollRef}>
1173
+ <Modal.Body
1174
+ className="ai-chat-scroll"
1175
+ ref={setScrollerEl}
1176
+ data-scrollable={bodyScrollable ? "true" : "false"}
1177
+ >
1110
1178
  {messages.map((msg) => {
1111
1179
  const isUser = msg.role === "user";
1112
1180
  const isLastCanceled =
@@ -1128,7 +1196,9 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1128
1196
  >
1129
1197
  <Stack
1130
1198
  gap={4}
1131
- style={{ alignItems: isUser ? "flex-end" : "flex-start" }}
1199
+ style={{
1200
+ alignItems: isUser ? "flex-end" : "flex-start",
1201
+ }}
1132
1202
  >
1133
1203
  <Stack className="ai-chat-bubble">
1134
1204
  <Text className="ai-chat-header">
@@ -1388,7 +1458,7 @@ const AiChatbotBase: FC<AiChatbotProps & AiKitShellInjectedProps> = (props) => {
1388
1458
  </Group>
1389
1459
  )}
1390
1460
  </Stack>
1391
- </Modal.Body>
1461
+ </div>
1392
1462
  </Modal.Root>
1393
1463
  )}
1394
1464
  </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 */
@@ -601,8 +612,10 @@
601
612
  display: flex;
602
613
  align-items: center;
603
614
  justify-content: space-between;
604
- padding: 0 var(--ai-kit-space-3) var(--ai-kit-space-3) var(--ai-kit-space-4);
615
+ padding: 0 var(--ai-kit-space-3);
605
616
  border-bottom: 1px solid var(--ai-kit-chat-border-color);
617
+ margin-left: calc(-1 * var(--ai-kit-button-padding-x));
618
+ margin-right: calc(-1 * var(--ai-kit-button-padding-x));
606
619
  }
607
620
 
608
621
  .ai-status-line {
@@ -655,6 +668,11 @@
655
668
  flex: 1;
656
669
  overflow-y: auto;
657
670
  padding: var(--ai-kit-space-4);
671
+ overscroll-behavior: auto;
672
+ }
673
+
674
+ .ai-chat-scroll[data-scrollable="true"] {
675
+ overscroll-behavior: contain;
658
676
  }
659
677
 
660
678
  .ai-chat-placeholder {
@@ -676,7 +694,6 @@
676
694
  }
677
695
 
678
696
  .ai-chat-bubble {
679
- max-width: 85%;
680
697
  padding: 10px 12px;
681
698
  border-radius: var(--ai-kit-radius-sm);
682
699
  background: var(--ai-kit-assistant-bubble-bg);
@@ -781,7 +798,7 @@
781
798
 
782
799
  .ai-status-text {
783
800
  font-size: 0.85rem;
784
- color: var(--ai-kit-color-text-muted);
801
+ color: var(--ai-kit-color-text);
785
802
  }
786
803
 
787
804
  .ai-feedback {
@@ -183,16 +183,24 @@ export function withAiKitShell<P extends object>(
183
183
  rootElement.setAttribute("lang", currentLanguage);
184
184
  }
185
185
 
186
+ const resolved =
187
+ colorMode === "auto"
188
+ ? window.matchMedia?.("(prefers-color-scheme: dark)")?.matches
189
+ ? "dark"
190
+ : "light"
191
+ : colorMode;
192
+
186
193
  return (
187
194
  <DirectionProvider initialDirection={currentDirection}>
188
195
  <MantineProvider
189
- defaultColorScheme={colorMode}
196
+ forceColorScheme={resolved}
190
197
  theme={theme}
191
198
  cssVariablesSelector={`#${rootElement.id}`}
192
199
  getRootElement={() => rootElement as unknown as HTMLElement}
193
200
  >
194
201
  <RootComponent
195
202
  {...props}
203
+ colorMode={resolved}
196
204
  language={currentLanguage}
197
205
  rootElement={rootElement}
198
206
  />