@xinghunm/ai-chat 1.3.0 → 1.3.2

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.mts CHANGED
@@ -441,6 +441,9 @@ interface AiChatLabels {
441
441
  questionnaireMultiSelectHint?: string;
442
442
  questionnaireOtherOptionLabel?: string;
443
443
  questionnaireOtherPlaceholder?: string;
444
+ modelLoading?: string;
445
+ modelLoadFailed?: string;
446
+ modelUnavailable?: string;
444
447
  skillLoading?: string;
445
448
  skillEmpty?: string;
446
449
  removeSkillAriaLabel?: string;
@@ -653,6 +656,9 @@ interface ChatComposerViewProps {
653
656
  skillLoadingLabel: string;
654
657
  skillEmptyLabel: string;
655
658
  removeSkillAriaLabel: string;
659
+ modelLoadingLabel: string;
660
+ modelLoadFailedLabel: string;
661
+ modelUnavailableLabel: string;
656
662
  onValueChange: (value: string) => void;
657
663
  onPickImages: (files: FileList | File[]) => void;
658
664
  onPasteImages: (files: File[]) => void;
package/dist/index.d.ts CHANGED
@@ -441,6 +441,9 @@ interface AiChatLabels {
441
441
  questionnaireMultiSelectHint?: string;
442
442
  questionnaireOtherOptionLabel?: string;
443
443
  questionnaireOtherPlaceholder?: string;
444
+ modelLoading?: string;
445
+ modelLoadFailed?: string;
446
+ modelUnavailable?: string;
444
447
  skillLoading?: string;
445
448
  skillEmpty?: string;
446
449
  removeSkillAriaLabel?: string;
@@ -653,6 +656,9 @@ interface ChatComposerViewProps {
653
656
  skillLoadingLabel: string;
654
657
  skillEmptyLabel: string;
655
658
  removeSkillAriaLabel: string;
659
+ modelLoadingLabel: string;
660
+ modelLoadFailedLabel: string;
661
+ modelUnavailableLabel: string;
656
662
  onValueChange: (value: string) => void;
657
663
  onPickImages: (files: FileList | File[]) => void;
658
664
  onPasteImages: (files: File[]) => void;
package/dist/index.js CHANGED
@@ -95,6 +95,9 @@ var DEFAULT_AI_CHAT_LABELS = {
95
95
  questionnaireMultiSelectHint: "Multiple choice",
96
96
  questionnaireOtherOptionLabel: "Other",
97
97
  questionnaireOtherPlaceholder: "Other",
98
+ modelLoading: "Loading models...",
99
+ modelLoadFailed: "Failed to load models",
100
+ modelUnavailable: "No model available",
98
101
  skillLoading: "Loading skills...",
99
102
  skillEmpty: "No matching skills",
100
103
  removeSkillAriaLabel: "Remove skill"
@@ -849,6 +852,21 @@ var AiChatProvider = (props) => {
849
852
  defaultAuthToken,
850
853
  defaultTransformStreamPacket
851
854
  ]);
855
+ const latestTransportRef = (0, import_react2.useRef)(transport);
856
+ (0, import_react2.useLayoutEffect)(() => {
857
+ latestTransportRef.current = transport;
858
+ }, [transport]);
859
+ (0, import_react2.useEffect)(() => {
860
+ return () => {
861
+ const { isStreamingBySession } = store.getState();
862
+ const streamingIds = Object.entries(isStreamingBySession).filter(([, streaming]) => streaming).map(([id]) => id);
863
+ for (const sessionId of streamingIds) {
864
+ const terminateId = isDraftChatSessionId(sessionId) ? void 0 : sessionId;
865
+ void latestTransportRef.current.terminateStream(terminateId).catch(() => {
866
+ });
867
+ }
868
+ };
869
+ }, [store]);
852
870
  const contextValue = (0, import_react2.useMemo)(
853
871
  () => ({
854
872
  store,
@@ -7221,6 +7239,9 @@ var useChatComposer = () => {
7221
7239
  }
7222
7240
  if (isDraftChatSessionId(sessionId)) {
7223
7241
  finalizeStop(sessionId);
7242
+ void transport.terminateStream(void 0).catch((err) => {
7243
+ console.error("Failed to terminate draft chat session", err);
7244
+ });
7224
7245
  return;
7225
7246
  }
7226
7247
  requestStopStreaming(sessionId);
@@ -7513,6 +7534,9 @@ var ChatModelControl = ({
7513
7534
  isModelsLoading,
7514
7535
  isModelsError,
7515
7536
  hasModels,
7537
+ loadingLabel,
7538
+ loadFailedLabel,
7539
+ unavailableLabel,
7516
7540
  onSelectedModelChange,
7517
7541
  onReloadModels
7518
7542
  }) => {
@@ -7525,7 +7549,7 @@ var ChatModelControl = ({
7525
7549
  "aria-label": "Reload",
7526
7550
  onClick: onReloadModels,
7527
7551
  children: [
7528
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: "Failed to load models" }),
7552
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { children: loadFailedLabel }),
7529
7553
  /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
7530
7554
  ReloadIcon,
7531
7555
  {
@@ -7547,7 +7571,7 @@ var ChatModelControl = ({
7547
7571
  );
7548
7572
  }
7549
7573
  if (isModelsLoading) {
7550
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ModelBadge, { children: "Loading models..." });
7574
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ModelBadge, { "data-testid": "chat-model-loading", children: loadingLabel });
7551
7575
  }
7552
7576
  if (hasModels && selectedModel) {
7553
7577
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
@@ -7565,9 +7589,14 @@ var ChatModelControl = ({
7565
7589
  }
7566
7590
  );
7567
7591
  }
7568
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ModelBadge, { children: "No model available" });
7592
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(ModelBadge, { "data-testid": "chat-model-unavailable", children: unavailableLabel });
7569
7593
  };
7570
7594
  var ModelBadge = import_styled11.default.span`
7595
+ display: inline-flex;
7596
+ align-items: center;
7597
+ min-width: 0;
7598
+ max-width: 100%;
7599
+ overflow: hidden;
7571
7600
  border-radius: 999px;
7572
7601
  border: 1px solid var(--border-hover);
7573
7602
  padding: 5px 12px;
@@ -7575,6 +7604,9 @@ var ModelBadge = import_styled11.default.span`
7575
7604
  font-size: 12px;
7576
7605
  color: var(--text-secondary);
7577
7606
  line-height: 12px;
7607
+ white-space: nowrap;
7608
+ text-overflow: ellipsis;
7609
+ flex-shrink: 1;
7578
7610
  `;
7579
7611
  var ModelReloadButton = import_styled11.default.button`
7580
7612
  display: inline-flex;
@@ -7979,6 +8011,9 @@ var ChatComposerView = ({
7979
8011
  skillLoadingLabel,
7980
8012
  skillEmptyLabel,
7981
8013
  removeSkillAriaLabel,
8014
+ modelLoadingLabel,
8015
+ modelLoadFailedLabel,
8016
+ modelUnavailableLabel,
7982
8017
  onValueChange,
7983
8018
  onPickImages,
7984
8019
  onPasteImages,
@@ -8020,9 +8055,10 @@ var ChatComposerView = ({
8020
8055
  }) : [];
8021
8056
  const showSkillMenu = Boolean(skillQueryMatch);
8022
8057
  const activeSkillQueryKey = skillQueryMatch ? `${skillQueryMatch.start}:${skillQueryMatch.end}:${skillQueryMatch.query}:${selectedSkills.join("\0")}` : "";
8023
- const { refs, floatingStyles } = useFloating2({
8058
+ const { refs, floatingStyles, placement } = useFloating2({
8024
8059
  open: showSkillMenu,
8025
8060
  placement: "bottom-start",
8061
+ strategy: "fixed",
8026
8062
  middleware: [
8027
8063
  offset3(3),
8028
8064
  flip3({ padding: 8 }),
@@ -8137,7 +8173,7 @@ var ChatComposerView = ({
8137
8173
  });
8138
8174
  };
8139
8175
  return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Container2, { children: [
8140
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Surface, { "data-testid": "chat-composer-surface", children: [
8176
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(Surface, { ref: setSkillMenuReference, "data-testid": "chat-composer-surface", children: [
8141
8177
  enableImageAttachments ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
8142
8178
  "input",
8143
8179
  {
@@ -8171,7 +8207,7 @@ var ChatComposerView = ({
8171
8207
  }
8172
8208
  )
8173
8209
  ] }, skill)) }) : null,
8174
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(InputArea, { ref: setSkillMenuReference, "data-testid": "chat-composer-input-area", children: [
8210
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(InputArea, { "data-testid": "chat-composer-input-area", children: [
8175
8211
  isComposerExpanded || isComposerExpandable ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
8176
8212
  ComposerExpandButton,
8177
8213
  {
@@ -8238,6 +8274,9 @@ var ChatComposerView = ({
8238
8274
  isModelsLoading,
8239
8275
  isModelsError,
8240
8276
  hasModels,
8277
+ loadingLabel: modelLoadingLabel,
8278
+ loadFailedLabel: modelLoadFailedLabel,
8279
+ unavailableLabel: modelUnavailableLabel,
8241
8280
  onSelectedModelChange,
8242
8281
  onReloadModels
8243
8282
  }
@@ -8261,6 +8300,8 @@ var ChatComposerView = ({
8261
8300
  ref: setSkillMenuFloating,
8262
8301
  style: floatingStyles,
8263
8302
  "data-testid": "chat-composer-skill-menu",
8303
+ "data-placement": placement,
8304
+ "data-floating-strategy": "fixed",
8264
8305
  role: "listbox",
8265
8306
  children: isSkillsLoading ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(SkillMenuState, { "data-testid": "chat-composer-skill-loading", children: skillLoadingLabel }) : filteredSkills.length ? filteredSkills.map((skill, index3) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
8266
8307
  SkillMenuItem,
@@ -8322,6 +8363,9 @@ var ChatComposer = () => {
8322
8363
  skillLoadingLabel: labels.skillLoading,
8323
8364
  skillEmptyLabel: labels.skillEmpty,
8324
8365
  removeSkillAriaLabel: labels.removeSkillAriaLabel,
8366
+ modelLoadingLabel: labels.modelLoading,
8367
+ modelLoadFailedLabel: labels.modelLoadFailed,
8368
+ modelUnavailableLabel: labels.modelUnavailable,
8325
8369
  onValueChange: actions.setValue,
8326
8370
  onPickImages: actions.pickImages,
8327
8371
  onPasteImages: actions.pasteImages,
package/dist/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  // src/components/ai-chat/index.tsx
2
- import { useEffect as useEffect9 } from "react";
2
+ import { useEffect as useEffect10 } from "react";
3
3
  import styled17 from "@emotion/styled";
4
4
  import { ConfigProvider } from "@xinghunm/compass-ui";
5
5
 
6
6
  // src/components/ai-chat-provider/index.tsx
7
- import { useRef, useMemo, useState } from "react";
7
+ import { useRef, useMemo, useState, useEffect, useLayoutEffect } from "react";
8
8
  import axios2 from "axios";
9
9
 
10
10
  // src/context/chat-context.ts
@@ -48,6 +48,9 @@ var DEFAULT_AI_CHAT_LABELS = {
48
48
  questionnaireMultiSelectHint: "Multiple choice",
49
49
  questionnaireOtherOptionLabel: "Other",
50
50
  questionnaireOtherPlaceholder: "Other",
51
+ modelLoading: "Loading models...",
52
+ modelLoadFailed: "Failed to load models",
53
+ modelUnavailable: "No model available",
51
54
  skillLoading: "Loading skills...",
52
55
  skillEmpty: "No matching skills",
53
56
  removeSkillAriaLabel: "Remove skill"
@@ -802,6 +805,21 @@ var AiChatProvider = (props) => {
802
805
  defaultAuthToken,
803
806
  defaultTransformStreamPacket
804
807
  ]);
808
+ const latestTransportRef = useRef(transport);
809
+ useLayoutEffect(() => {
810
+ latestTransportRef.current = transport;
811
+ }, [transport]);
812
+ useEffect(() => {
813
+ return () => {
814
+ const { isStreamingBySession } = store.getState();
815
+ const streamingIds = Object.entries(isStreamingBySession).filter(([, streaming]) => streaming).map(([id]) => id);
816
+ for (const sessionId of streamingIds) {
817
+ const terminateId = isDraftChatSessionId(sessionId) ? void 0 : sessionId;
818
+ void latestTransportRef.current.terminateStream(terminateId).catch(() => {
819
+ });
820
+ }
821
+ };
822
+ }, [store]);
805
823
  const contextValue = useMemo(
806
824
  () => ({
807
825
  store,
@@ -842,7 +860,7 @@ var AiChatProvider = (props) => {
842
860
  };
843
861
 
844
862
  // src/components/chat-thread/index.tsx
845
- import { useCallback as useCallback3, useLayoutEffect as useLayoutEffect2, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
863
+ import { useCallback as useCallback3, useLayoutEffect as useLayoutEffect3, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
846
864
  import styled9 from "@emotion/styled";
847
865
 
848
866
  // src/context/use-chat-context.ts
@@ -863,7 +881,7 @@ var useChatStore = (selector) => {
863
881
  var CHAT_THREAD_SCROLL_TOP_GAP = 16;
864
882
 
865
883
  // src/components/chat-thread/components/chat-message-item.tsx
866
- import { Fragment, memo, useCallback as useCallback2, useLayoutEffect, useState as useState3 } from "react";
884
+ import { Fragment, memo, useCallback as useCallback2, useLayoutEffect as useLayoutEffect2, useState as useState3 } from "react";
867
885
  import styled7 from "@emotion/styled";
868
886
  import { keyframes } from "@emotion/react";
869
887
  import ReactMarkdown from "react-markdown";
@@ -872,7 +890,7 @@ import remarkMath from "remark-math";
872
890
  import rehypeKatex from "rehype-katex";
873
891
 
874
892
  // src/components/chat-thread/hooks/use-chat-message-reveal.ts
875
- import { useCallback, useEffect, useMemo as useMemo2, useReducer, useRef as useRef2 } from "react";
893
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useReducer, useRef as useRef2 } from "react";
876
894
 
877
895
  // src/components/chat-thread/lib/message-reveal.ts
878
896
  var STREAM_REVEAL_TICK_MS = 36;
@@ -1007,7 +1025,7 @@ var useChatMessageReveal = (message) => {
1007
1025
  },
1008
1026
  [isAssistantStreaming, message.role]
1009
1027
  );
1010
- useEffect(() => {
1028
+ useEffect2(() => {
1011
1029
  if (previousMessageIdRef.current === message.id) {
1012
1030
  return;
1013
1031
  }
@@ -1025,7 +1043,7 @@ var useChatMessageReveal = (message) => {
1025
1043
  targetUnitCount: targetUnits.length
1026
1044
  });
1027
1045
  }, [isAssistantStreaming, message.id, targetUnits.length]);
1028
- useEffect(() => {
1046
+ useEffect2(() => {
1029
1047
  pendingTargetUnitCountRef.current = targetUnits.length;
1030
1048
  if (message.role !== "assistant" || !isAssistantStreaming) {
1031
1049
  if (inputBatchTimeoutRef.current !== null) {
@@ -1061,7 +1079,7 @@ var useChatMessageReveal = (message) => {
1061
1079
  [displayedUnitCount, targetUnits]
1062
1080
  );
1063
1081
  const contentBlocks = useMemo2(() => splitMarkdownBlocks(displayedContent), [displayedContent]);
1064
- useEffect(() => {
1082
+ useEffect2(() => {
1065
1083
  const hasNewDisplayedBlock = message.role === "assistant" && contentBlocks.length > 1 && contentBlocks.length > lastDisplayedBlockCountRef.current;
1066
1084
  lastDisplayedBlockCountRef.current = contentBlocks.length;
1067
1085
  if (!hasNewDisplayedBlock) {
@@ -1075,7 +1093,7 @@ var useChatMessageReveal = (message) => {
1075
1093
  window.clearTimeout(timer);
1076
1094
  };
1077
1095
  }, [contentBlocks.length, message.role]);
1078
- useEffect(() => {
1096
+ useEffect2(() => {
1079
1097
  const shouldAnimateReveal = message.role === "assistant" && displayedUnitCount < batchedTargetUnitCount && (isAssistantStreaming || displayedUnitCount > 0);
1080
1098
  if (!shouldAnimateReveal) {
1081
1099
  if (displayedUnitCount !== batchedTargetUnitCount) {
@@ -1112,7 +1130,7 @@ var useChatMessageReveal = (message) => {
1112
1130
  };
1113
1131
 
1114
1132
  // src/components/chat-thread/hooks/use-timeline-block-anchors.ts
1115
- import { useEffect as useEffect2, useMemo as useMemo3, useReducer as useReducer2 } from "react";
1133
+ import { useEffect as useEffect3, useMemo as useMemo3, useReducer as useReducer2 } from "react";
1116
1134
 
1117
1135
  // src/components/chat-thread/lib/chat-message-timeline.ts
1118
1136
  var stringifyTimelineKeyPart = (value) => {
@@ -1391,14 +1409,14 @@ var useTimelineBlockAnchors = ({
1391
1409
  state.timelineBlockAnchors,
1392
1410
  timelineTextStreamLength
1393
1411
  ]);
1394
- useEffect2(() => {
1412
+ useEffect3(() => {
1395
1413
  dispatch({
1396
1414
  type: "reset-message",
1397
1415
  messageId: message.id,
1398
1416
  currentBlockKeys: currentTimelineBlockKeys
1399
1417
  });
1400
1418
  }, [currentTimelineBlockKeys, message.id]);
1401
- useEffect2(() => {
1419
+ useEffect3(() => {
1402
1420
  if (messageRenderOrder !== "timeline" || !isAssistantStreaming) {
1403
1421
  return;
1404
1422
  }
@@ -1408,7 +1426,7 @@ var useTimelineBlockAnchors = ({
1408
1426
  timelineTextStreamLength
1409
1427
  });
1410
1428
  }, [currentTimelineBlockKeys, isAssistantStreaming, messageRenderOrder, timelineTextStreamLength]);
1411
- useEffect2(() => {
1429
+ useEffect3(() => {
1412
1430
  if (messageRenderOrder !== "timeline") {
1413
1431
  return;
1414
1432
  }
@@ -1633,7 +1651,7 @@ var Value = styled3.span`
1633
1651
 
1634
1652
  // src/components/chat-thread/components/questionnaire-card.tsx
1635
1653
  import {
1636
- useEffect as useEffect3,
1654
+ useEffect as useEffect4,
1637
1655
  useRef as useRef3,
1638
1656
  useState as useState2
1639
1657
  } from "react";
@@ -2026,11 +2044,11 @@ var QuestionnaireCardInner = ({
2026
2044
  const visibleErrorMessage = questionnaire.statusMessage ?? errorMessage;
2027
2045
  const isInteractionLocked = !interactive || isSubmitting || isSubmitted || hasExternalFailureStatus;
2028
2046
  questionnaireRef.current = questionnaire;
2029
- useEffect3(() => {
2047
+ useEffect4(() => {
2030
2048
  setAnswers(createInitialAnswers(questionnaireRef.current));
2031
2049
  setOtherDrafts(createInitialOtherDrafts(questionnaireRef.current));
2032
2050
  }, [questionnaire.answers]);
2033
- useEffect3(() => {
2051
+ useEffect4(() => {
2034
2052
  if (!pendingFocusQuestionId || isInteractionLocked) {
2035
2053
  return;
2036
2054
  }
@@ -2554,7 +2572,7 @@ var Detail = styled5.li`
2554
2572
 
2555
2573
  // src/components/chat-thread/components/image-viewer.tsx
2556
2574
  import styled6 from "@emotion/styled";
2557
- import { useEffect as useEffect4, useRef as useRef4 } from "react";
2575
+ import { useEffect as useEffect5, useRef as useRef4 } from "react";
2558
2576
  import { jsx as jsx7 } from "@emotion/react/jsx-runtime";
2559
2577
  var Overlay = styled6.div`
2560
2578
  position: fixed;
@@ -2574,7 +2592,7 @@ var Img = styled6.img`
2574
2592
  `;
2575
2593
  var ImageViewer = ({ src, alt, onClose }) => {
2576
2594
  const overlayRef = useRef4(null);
2577
- useEffect4(() => {
2595
+ useEffect5(() => {
2578
2596
  const handleKey = (e) => {
2579
2597
  if (e.key === "Escape")
2580
2598
  onClose();
@@ -2582,7 +2600,7 @@ var ImageViewer = ({ src, alt, onClose }) => {
2582
2600
  document.addEventListener("keydown", handleKey);
2583
2601
  return () => document.removeEventListener("keydown", handleKey);
2584
2602
  }, [onClose]);
2585
- useEffect4(() => {
2603
+ useEffect5(() => {
2586
2604
  overlayRef.current?.focus();
2587
2605
  }, []);
2588
2606
  const stopPropagation = (e) => e.stopPropagation();
@@ -2668,7 +2686,7 @@ var useUserMessageCollapse = ({
2668
2686
  },
2669
2687
  [syncCollapseState]
2670
2688
  );
2671
- useLayoutEffect(() => {
2689
+ useLayoutEffect2(() => {
2672
2690
  if (!bodyStackElement) {
2673
2691
  return;
2674
2692
  }
@@ -2688,7 +2706,7 @@ var useUserMessageCollapse = ({
2688
2706
  settledContent,
2689
2707
  syncCollapseState
2690
2708
  ]);
2691
- useLayoutEffect(() => {
2709
+ useLayoutEffect2(() => {
2692
2710
  if (!bodyStackElement || !enabled || typeof ResizeObserver === "undefined") {
2693
2711
  return;
2694
2712
  }
@@ -3625,7 +3643,7 @@ var ChatThreadView = ({
3625
3643
  setPendingNewMessageCount(0);
3626
3644
  }
3627
3645
  }, []);
3628
- useLayoutEffect2(() => {
3646
+ useLayoutEffect3(() => {
3629
3647
  const nextHistoryMessageId = latestHistoryMessage?.id;
3630
3648
  if (lastHistoryMessageIdRef.current === nextHistoryMessageId) {
3631
3649
  return;
@@ -3649,7 +3667,7 @@ var ChatThreadView = ({
3649
3667
  });
3650
3668
  }
3651
3669
  }, [latestHistoryMessage, markThreadPinned, scrollToBottom]);
3652
- useLayoutEffect2(() => {
3670
+ useLayoutEffect3(() => {
3653
3671
  const nextStreamingMessageId = streamingMessage?.id;
3654
3672
  if (lastStreamingMessageIdRef.current === nextStreamingMessageId) {
3655
3673
  return;
@@ -3664,7 +3682,7 @@ var ChatThreadView = ({
3664
3682
  });
3665
3683
  }
3666
3684
  }, [streamingMessage]);
3667
- useLayoutEffect2(() => {
3685
+ useLayoutEffect3(() => {
3668
3686
  if (reservedSpaceFrameRef.current !== null) {
3669
3687
  window.cancelAnimationFrame(reservedSpaceFrameRef.current);
3670
3688
  reservedSpaceFrameRef.current = null;
@@ -3694,7 +3712,7 @@ var ChatThreadView = ({
3694
3712
  }
3695
3713
  };
3696
3714
  }, [latestTurn, latestUserMessageId, error2, measureLatestTurnMinHeight, scrollToBottom]);
3697
- useLayoutEffect2(() => {
3715
+ useLayoutEffect3(() => {
3698
3716
  if (!latestTurn)
3699
3717
  return;
3700
3718
  const handleResize = () => {
@@ -3720,7 +3738,7 @@ var ChatThreadView = ({
3720
3738
  window.removeEventListener("resize", handleResize);
3721
3739
  };
3722
3740
  }, [latestTurn, latestUserMessageId, measureLatestTurnMinHeight, scrollToBottom]);
3723
- useLayoutEffect2(() => {
3741
+ useLayoutEffect3(() => {
3724
3742
  const latestTurnElement = latestTurnRef.current;
3725
3743
  if (!latestTurnElement || typeof ResizeObserver === "undefined") {
3726
3744
  return;
@@ -3733,7 +3751,7 @@ var ChatThreadView = ({
3733
3751
  observer.disconnect();
3734
3752
  };
3735
3753
  }, [latestTurn, scrollToBottom]);
3736
- useLayoutEffect2(() => {
3754
+ useLayoutEffect3(() => {
3737
3755
  const latestTurnElement = latestTurnRef.current;
3738
3756
  if (!latestTurnElement || typeof MutationObserver === "undefined") {
3739
3757
  return;
@@ -4027,7 +4045,7 @@ var ScrollToLatestBadge = styled9.span`
4027
4045
  `;
4028
4046
 
4029
4047
  // src/components/chat-composer/index.tsx
4030
- import { useCallback as useCallback8, useEffect as useEffect8, useLayoutEffect as useLayoutEffect5, useRef as useRef11, useState as useState10 } from "react";
4048
+ import { useCallback as useCallback8, useEffect as useEffect9, useLayoutEffect as useLayoutEffect6, useRef as useRef11, useState as useState10 } from "react";
4031
4049
  import styled14 from "@emotion/styled";
4032
4050
 
4033
4051
  // ../../node_modules/.pnpm/@floating-ui+react@0.27.16_react-dom@18.3.1_react@18.3.1/node_modules/@floating-ui/react/dist/floating-ui.react.mjs
@@ -4189,7 +4207,7 @@ function getFrameElement(win) {
4189
4207
 
4190
4208
  // ../../node_modules/.pnpm/@floating-ui+react@0.27.16_react-dom@18.3.1_react@18.3.1/node_modules/@floating-ui/react/dist/floating-ui.react.utils.mjs
4191
4209
  import * as React from "react";
4192
- import { useLayoutEffect as useLayoutEffect3 } from "react";
4210
+ import { useLayoutEffect as useLayoutEffect4 } from "react";
4193
4211
 
4194
4212
  // ../../node_modules/.pnpm/@floating-ui+utils@0.2.10/node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs
4195
4213
  var min = Math.min;
@@ -4676,7 +4694,7 @@ function getDocument(node) {
4676
4694
  var isClient = typeof document !== "undefined";
4677
4695
  var noop = function noop2() {
4678
4696
  };
4679
- var index = isClient ? useLayoutEffect3 : noop;
4697
+ var index = isClient ? useLayoutEffect4 : noop;
4680
4698
  var SafeReact = {
4681
4699
  ...React
4682
4700
  };
@@ -5885,12 +5903,12 @@ var computePosition2 = (reference, floating, options) => {
5885
5903
 
5886
5904
  // ../../node_modules/.pnpm/@floating-ui+react-dom@2.1.6_react-dom@18.3.1_react@18.3.1/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.mjs
5887
5905
  import * as React2 from "react";
5888
- import { useLayoutEffect as useLayoutEffect4 } from "react";
5906
+ import { useLayoutEffect as useLayoutEffect5 } from "react";
5889
5907
  import * as ReactDOM from "react-dom";
5890
5908
  var isClient2 = typeof document !== "undefined";
5891
5909
  var noop3 = function noop4() {
5892
5910
  };
5893
- var index2 = isClient2 ? useLayoutEffect4 : noop3;
5911
+ var index2 = isClient2 ? useLayoutEffect5 : noop3;
5894
5912
  function deepEqual(a, b) {
5895
5913
  if (a === b) {
5896
5914
  return true;
@@ -6627,10 +6645,10 @@ var resolveSendSession = ({
6627
6645
  };
6628
6646
 
6629
6647
  // src/components/chat-composer/hooks/use-chat-composer.ts
6630
- import { useCallback as useCallback7, useEffect as useEffect7, useRef as useRef10, useState as useState8 } from "react";
6648
+ import { useCallback as useCallback7, useEffect as useEffect8, useRef as useRef10, useState as useState8 } from "react";
6631
6649
 
6632
6650
  // src/components/chat-composer/hooks/use-composer-attachments.ts
6633
- import { useEffect as useEffect6, useRef as useRef9, useState as useState7 } from "react";
6651
+ import { useEffect as useEffect7, useRef as useRef9, useState as useState7 } from "react";
6634
6652
  var SUPPORTED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
6635
6653
  var MAX_COMPOSER_ATTACHMENTS = 10;
6636
6654
  var createObjectUrl = (file) => typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : "";
@@ -6646,10 +6664,10 @@ var releaseComposerAttachments = (attachments) => {
6646
6664
  var useComposerAttachments = () => {
6647
6665
  const [attachments, setAttachments] = useState7([]);
6648
6666
  const attachmentsRef = useRef9([]);
6649
- useEffect6(() => {
6667
+ useEffect7(() => {
6650
6668
  attachmentsRef.current = attachments;
6651
6669
  }, [attachments]);
6652
- useEffect6(
6670
+ useEffect7(
6653
6671
  () => () => {
6654
6672
  releaseComposerAttachments(attachmentsRef.current);
6655
6673
  },
@@ -6822,10 +6840,10 @@ var useChatComposer = () => {
6822
6840
  setIsModelsLoading(false);
6823
6841
  }
6824
6842
  }, [modelsLoader]);
6825
- useEffect7(() => {
6843
+ useEffect8(() => {
6826
6844
  void fetchModels();
6827
6845
  }, [fetchModels]);
6828
- useEffect7(() => {
6846
+ useEffect8(() => {
6829
6847
  activeSkillsLoaderRef.current = skillsLoader;
6830
6848
  const cachedSkills = getCachedSkills(skillsLoader);
6831
6849
  setAvailableSkills(cachedSkills.skills);
@@ -6861,7 +6879,7 @@ var useChatComposer = () => {
6861
6879
  }
6862
6880
  }
6863
6881
  }, [skillsLoader]);
6864
- useEffect7(() => {
6882
+ useEffect8(() => {
6865
6883
  void fetchSkills();
6866
6884
  }, [fetchSkills]);
6867
6885
  const hasModels = availableModels.length > 0;
@@ -6875,25 +6893,25 @@ var useChatComposer = () => {
6875
6893
  const stopRequestBySessionRef = useRef10(/* @__PURE__ */ new Map());
6876
6894
  const lastRequestBySessionRef = useRef10(/* @__PURE__ */ new Map());
6877
6895
  const previousActiveSessionIdRef = useRef10(activeSessionId);
6878
- useEffect7(() => {
6896
+ useEffect8(() => {
6879
6897
  setSelectedModel(
6880
6898
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
6881
6899
  );
6882
6900
  }, [availableModels, isModelsLoading]);
6883
- useEffect7(() => {
6901
+ useEffect8(() => {
6884
6902
  if (activeSession) {
6885
6903
  setSelectedModeLocal(activeSession.mode ?? DEFAULT_CHAT_AGENT_MODE);
6886
6904
  return;
6887
6905
  }
6888
6906
  setSelectedModeLocal(preferredMode ?? DEFAULT_CHAT_AGENT_MODE);
6889
6907
  }, [activeSession, preferredMode]);
6890
- useEffect7(() => {
6908
+ useEffect8(() => {
6891
6909
  if (previousActiveSessionIdRef.current !== activeSessionId) {
6892
6910
  setSelectedSkills([]);
6893
6911
  previousActiveSessionIdRef.current = activeSessionId;
6894
6912
  }
6895
6913
  }, [activeSessionId]);
6896
- useEffect7(() => {
6914
+ useEffect8(() => {
6897
6915
  if (!attachmentNotice)
6898
6916
  return;
6899
6917
  const timeoutId = window.setTimeout(
@@ -7178,6 +7196,9 @@ var useChatComposer = () => {
7178
7196
  }
7179
7197
  if (isDraftChatSessionId(sessionId)) {
7180
7198
  finalizeStop(sessionId);
7199
+ void transport.terminateStream(void 0).catch((err) => {
7200
+ console.error("Failed to terminate draft chat session", err);
7201
+ });
7181
7202
  return;
7182
7203
  }
7183
7204
  requestStopStreaming(sessionId);
@@ -7470,6 +7491,9 @@ var ChatModelControl = ({
7470
7491
  isModelsLoading,
7471
7492
  isModelsError,
7472
7493
  hasModels,
7494
+ loadingLabel,
7495
+ loadFailedLabel,
7496
+ unavailableLabel,
7473
7497
  onSelectedModelChange,
7474
7498
  onReloadModels
7475
7499
  }) => {
@@ -7482,7 +7506,7 @@ var ChatModelControl = ({
7482
7506
  "aria-label": "Reload",
7483
7507
  onClick: onReloadModels,
7484
7508
  children: [
7485
- /* @__PURE__ */ jsx13("span", { children: "Failed to load models" }),
7509
+ /* @__PURE__ */ jsx13("span", { children: loadFailedLabel }),
7486
7510
  /* @__PURE__ */ jsxs10(
7487
7511
  ReloadIcon,
7488
7512
  {
@@ -7504,7 +7528,7 @@ var ChatModelControl = ({
7504
7528
  );
7505
7529
  }
7506
7530
  if (isModelsLoading) {
7507
- return /* @__PURE__ */ jsx13(ModelBadge, { children: "Loading models..." });
7531
+ return /* @__PURE__ */ jsx13(ModelBadge, { "data-testid": "chat-model-loading", children: loadingLabel });
7508
7532
  }
7509
7533
  if (hasModels && selectedModel) {
7510
7534
  return /* @__PURE__ */ jsx13(
@@ -7522,9 +7546,14 @@ var ChatModelControl = ({
7522
7546
  }
7523
7547
  );
7524
7548
  }
7525
- return /* @__PURE__ */ jsx13(ModelBadge, { children: "No model available" });
7549
+ return /* @__PURE__ */ jsx13(ModelBadge, { "data-testid": "chat-model-unavailable", children: unavailableLabel });
7526
7550
  };
7527
7551
  var ModelBadge = styled11.span`
7552
+ display: inline-flex;
7553
+ align-items: center;
7554
+ min-width: 0;
7555
+ max-width: 100%;
7556
+ overflow: hidden;
7528
7557
  border-radius: 999px;
7529
7558
  border: 1px solid var(--border-hover);
7530
7559
  padding: 5px 12px;
@@ -7532,6 +7561,9 @@ var ModelBadge = styled11.span`
7532
7561
  font-size: 12px;
7533
7562
  color: var(--text-secondary);
7534
7563
  line-height: 12px;
7564
+ white-space: nowrap;
7565
+ text-overflow: ellipsis;
7566
+ flex-shrink: 1;
7535
7567
  `;
7536
7568
  var ModelReloadButton = styled11.button`
7537
7569
  display: inline-flex;
@@ -7936,6 +7968,9 @@ var ChatComposerView = ({
7936
7968
  skillLoadingLabel,
7937
7969
  skillEmptyLabel,
7938
7970
  removeSkillAriaLabel,
7971
+ modelLoadingLabel,
7972
+ modelLoadFailedLabel,
7973
+ modelUnavailableLabel,
7939
7974
  onValueChange,
7940
7975
  onPickImages,
7941
7976
  onPasteImages,
@@ -7977,9 +8012,10 @@ var ChatComposerView = ({
7977
8012
  }) : [];
7978
8013
  const showSkillMenu = Boolean(skillQueryMatch);
7979
8014
  const activeSkillQueryKey = skillQueryMatch ? `${skillQueryMatch.start}:${skillQueryMatch.end}:${skillQueryMatch.query}:${selectedSkills.join("\0")}` : "";
7980
- const { refs, floatingStyles } = useFloating2({
8015
+ const { refs, floatingStyles, placement } = useFloating2({
7981
8016
  open: showSkillMenu,
7982
8017
  placement: "bottom-start",
8018
+ strategy: "fixed",
7983
8019
  middleware: [
7984
8020
  offset3(3),
7985
8021
  flip3({ padding: 8 }),
@@ -8008,7 +8044,7 @@ var ChatComposerView = ({
8008
8044
  [refs]
8009
8045
  );
8010
8046
  const activeSkillIndex = activeSkillNavigation.queryKey === activeSkillQueryKey ? activeSkillNavigation.index : 0;
8011
- useLayoutEffect5(() => {
8047
+ useLayoutEffect6(() => {
8012
8048
  const element = inputRef.current;
8013
8049
  if (!element) {
8014
8050
  return;
@@ -8094,7 +8130,7 @@ var ChatComposerView = ({
8094
8130
  });
8095
8131
  };
8096
8132
  return /* @__PURE__ */ jsxs11(Container2, { children: [
8097
- /* @__PURE__ */ jsxs11(Surface, { "data-testid": "chat-composer-surface", children: [
8133
+ /* @__PURE__ */ jsxs11(Surface, { ref: setSkillMenuReference, "data-testid": "chat-composer-surface", children: [
8098
8134
  enableImageAttachments ? /* @__PURE__ */ jsx16(
8099
8135
  "input",
8100
8136
  {
@@ -8128,7 +8164,7 @@ var ChatComposerView = ({
8128
8164
  }
8129
8165
  )
8130
8166
  ] }, skill)) }) : null,
8131
- /* @__PURE__ */ jsxs11(InputArea, { ref: setSkillMenuReference, "data-testid": "chat-composer-input-area", children: [
8167
+ /* @__PURE__ */ jsxs11(InputArea, { "data-testid": "chat-composer-input-area", children: [
8132
8168
  isComposerExpanded || isComposerExpandable ? /* @__PURE__ */ jsx16(
8133
8169
  ComposerExpandButton,
8134
8170
  {
@@ -8195,6 +8231,9 @@ var ChatComposerView = ({
8195
8231
  isModelsLoading,
8196
8232
  isModelsError,
8197
8233
  hasModels,
8234
+ loadingLabel: modelLoadingLabel,
8235
+ loadFailedLabel: modelLoadFailedLabel,
8236
+ unavailableLabel: modelUnavailableLabel,
8198
8237
  onSelectedModelChange,
8199
8238
  onReloadModels
8200
8239
  }
@@ -8218,6 +8257,8 @@ var ChatComposerView = ({
8218
8257
  ref: setSkillMenuFloating,
8219
8258
  style: floatingStyles,
8220
8259
  "data-testid": "chat-composer-skill-menu",
8260
+ "data-placement": placement,
8261
+ "data-floating-strategy": "fixed",
8221
8262
  role: "listbox",
8222
8263
  children: isSkillsLoading ? /* @__PURE__ */ jsx16(SkillMenuState, { "data-testid": "chat-composer-skill-loading", children: skillLoadingLabel }) : filteredSkills.length ? filteredSkills.map((skill, index3) => /* @__PURE__ */ jsx16(
8223
8264
  SkillMenuItem,
@@ -8241,7 +8282,7 @@ var ChatComposer = () => {
8241
8282
  const { labels, sendRef, retryRef, stopRef, enableImageAttachments } = useChatContext();
8242
8283
  const { state, actions } = useChatComposer();
8243
8284
  const { send, retry } = actions;
8244
- useEffect8(() => {
8285
+ useEffect9(() => {
8245
8286
  sendRef.current = send;
8246
8287
  retryRef.current = async (sessionId) => {
8247
8288
  retry(sessionId);
@@ -8279,6 +8320,9 @@ var ChatComposer = () => {
8279
8320
  skillLoadingLabel: labels.skillLoading,
8280
8321
  skillEmptyLabel: labels.skillEmpty,
8281
8322
  removeSkillAriaLabel: labels.removeSkillAriaLabel,
8323
+ modelLoadingLabel: labels.modelLoading,
8324
+ modelLoadFailedLabel: labels.modelLoadFailed,
8325
+ modelUnavailableLabel: labels.modelUnavailable,
8282
8326
  onValueChange: actions.setValue,
8283
8327
  onPickImages: actions.pickImages,
8284
8328
  onPasteImages: actions.pasteImages,
@@ -8753,7 +8797,7 @@ var AiChatWorkspaceContent = ({
8753
8797
  })
8754
8798
  );
8755
8799
  const shouldShowComposerOnly = showComposerOnlyBeforeFirstMessage && !showConversationList && !isConversationStarted;
8756
- useEffect9(() => {
8800
+ useEffect10(() => {
8757
8801
  onConversationStartedChange?.(isConversationStarted);
8758
8802
  }, [isConversationStarted, onConversationStartedChange]);
8759
8803
  return /* @__PURE__ */ jsxs14(Root, { "data-testid": "ai-chat", children: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinghunm/ai-chat",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "AI chat React component library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",