@xinghunm/ai-chat 0.1.0 → 0.2.1

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.
Files changed (3) hide show
  1. package/dist/index.js +98 -114
  2. package/dist/index.mjs +118 -134
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -729,96 +729,112 @@ var getNextDisplayedUnitCount = ({
729
729
  var splitMarkdownBlocks = (content) => content.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
730
730
 
731
731
  // src/components/chat-thread/hooks/use-chat-message-reveal.ts
732
+ var createRevealState = ({
733
+ isAssistantStreaming,
734
+ targetUnitCount
735
+ }) => {
736
+ const initialDisplayedUnitCount = isAssistantStreaming ? 0 : targetUnitCount;
737
+ return {
738
+ batchedTargetUnitCount: initialDisplayedUnitCount,
739
+ displayedUnitCount: initialDisplayedUnitCount,
740
+ isFreshBlockActive: false
741
+ };
742
+ };
743
+ var revealReducer = (state, action) => {
744
+ switch (action.type) {
745
+ case "reset-message":
746
+ return createRevealState(action);
747
+ case "commit-batched-target": {
748
+ const nextDisplayedUnitCount = action.role === "assistant" ? getNextDisplayedUnitCount({
749
+ currentUnits: state.displayedUnitCount,
750
+ targetUnits: action.nextTargetUnitCount,
751
+ isStreaming: action.isAssistantStreaming,
752
+ minimumStep: state.displayedUnitCount > 0 && action.isAssistantStreaming ? 2 : 1
753
+ }) : action.nextTargetUnitCount;
754
+ return {
755
+ ...state,
756
+ batchedTargetUnitCount: action.nextTargetUnitCount,
757
+ displayedUnitCount: nextDisplayedUnitCount
758
+ };
759
+ }
760
+ case "set-fresh-block-active":
761
+ return state.isFreshBlockActive === action.isActive ? state : {
762
+ ...state,
763
+ isFreshBlockActive: action.isActive
764
+ };
765
+ case "sync-displayed-target":
766
+ return state.displayedUnitCount === state.batchedTargetUnitCount ? state : {
767
+ ...state,
768
+ displayedUnitCount: state.batchedTargetUnitCount
769
+ };
770
+ case "advance-reveal": {
771
+ if (state.displayedUnitCount >= state.batchedTargetUnitCount) {
772
+ return state;
773
+ }
774
+ return {
775
+ ...state,
776
+ displayedUnitCount: Math.min(
777
+ state.batchedTargetUnitCount,
778
+ getNextDisplayedUnitCount({
779
+ currentUnits: state.displayedUnitCount,
780
+ targetUnits: state.batchedTargetUnitCount,
781
+ isStreaming: action.isAssistantStreaming
782
+ })
783
+ )
784
+ };
785
+ }
786
+ default:
787
+ return state;
788
+ }
789
+ };
732
790
  var useChatMessageReveal = (message) => {
733
791
  const isAssistantStreaming = message.role === "assistant" && message.status === "streaming";
734
792
  const targetContent = message.content || "";
735
793
  const targetUnits = (0, import_react5.useMemo)(() => Array.from(targetContent), [targetContent]);
736
- const resetSnapshot = (0, import_react5.useMemo)(
737
- () => ({
738
- messageId: message.id,
739
- initialTargetUnitCount: targetUnits.length,
740
- initialBatchedTargetUnitCount: isAssistantStreaming ? 0 : targetUnits.length
741
- }),
742
- [isAssistantStreaming, message.id, targetUnits.length]
743
- );
744
794
  const pendingTargetUnitCountRef = (0, import_react5.useRef)(targetUnits.length);
745
795
  const batchedTargetUnitCountRef = (0, import_react5.useRef)(isAssistantStreaming ? 0 : targetUnits.length);
746
796
  const inputBatchTimeoutRef = (0, import_react5.useRef)(null);
747
- const commitAnimationFrameRef = (0, import_react5.useRef)(null);
748
- const freshBlockActivationFrameRef = (0, import_react5.useRef)(null);
749
- const displayedUnitSyncFrameRef = (0, import_react5.useRef)(null);
750
- const resetAnimationFrameRef = (0, import_react5.useRef)(null);
751
- const [batchedTargetUnitCount, setBatchedTargetUnitCount] = (0, import_react5.useState)(
752
- () => isAssistantStreaming ? 0 : targetUnits.length
753
- );
754
797
  const lastDisplayedBlockCountRef = (0, import_react5.useRef)(0);
755
- const [displayedUnitCount, setDisplayedUnitCount] = (0, import_react5.useState)(
756
- () => isAssistantStreaming ? 0 : targetUnits.length
798
+ const previousMessageIdRef = (0, import_react5.useRef)(message.id);
799
+ const [state, dispatch] = (0, import_react5.useReducer)(
800
+ revealReducer,
801
+ {
802
+ isAssistantStreaming,
803
+ targetUnitCount: targetUnits.length
804
+ },
805
+ createRevealState
757
806
  );
758
- const [isFreshBlockActive, setIsFreshBlockActive] = (0, import_react5.useState)(false);
807
+ const { batchedTargetUnitCount, displayedUnitCount, isFreshBlockActive } = state;
759
808
  const commitBatchedTargetUnitCount = (0, import_react5.useCallback)(
760
809
  (nextTargetUnitCount) => {
761
810
  batchedTargetUnitCountRef.current = nextTargetUnitCount;
762
- setBatchedTargetUnitCount(nextTargetUnitCount);
763
- setDisplayedUnitCount(
764
- (current) => message.role === "assistant" ? getNextDisplayedUnitCount({
765
- currentUnits: current,
766
- targetUnits: nextTargetUnitCount,
767
- isStreaming: isAssistantStreaming,
768
- minimumStep: current > 0 && isAssistantStreaming ? 2 : 1
769
- }) : nextTargetUnitCount
770
- );
771
- },
772
- [isAssistantStreaming, message.role]
773
- );
774
- const scheduleBatchedTargetUnitCountCommit = (0, import_react5.useCallback)(
775
- (nextTargetUnitCount) => {
776
- if (commitAnimationFrameRef.current !== null) {
777
- window.cancelAnimationFrame(commitAnimationFrameRef.current);
778
- }
779
- commitAnimationFrameRef.current = window.requestAnimationFrame(() => {
780
- commitAnimationFrameRef.current = null;
781
- commitBatchedTargetUnitCount(nextTargetUnitCount);
811
+ dispatch({
812
+ type: "commit-batched-target",
813
+ isAssistantStreaming,
814
+ nextTargetUnitCount,
815
+ role: message.role
782
816
  });
783
817
  },
784
- [commitBatchedTargetUnitCount]
818
+ [isAssistantStreaming, message.role]
785
819
  );
786
820
  (0, import_react5.useEffect)(() => {
787
- pendingTargetUnitCountRef.current = resetSnapshot.initialTargetUnitCount;
788
- batchedTargetUnitCountRef.current = resetSnapshot.initialBatchedTargetUnitCount;
821
+ if (previousMessageIdRef.current === message.id) {
822
+ return;
823
+ }
824
+ previousMessageIdRef.current = message.id;
825
+ pendingTargetUnitCountRef.current = targetUnits.length;
826
+ batchedTargetUnitCountRef.current = isAssistantStreaming ? 0 : targetUnits.length;
789
827
  lastDisplayedBlockCountRef.current = 0;
790
828
  if (inputBatchTimeoutRef.current !== null) {
791
829
  window.clearTimeout(inputBatchTimeoutRef.current);
792
830
  inputBatchTimeoutRef.current = null;
793
831
  }
794
- if (commitAnimationFrameRef.current !== null) {
795
- window.cancelAnimationFrame(commitAnimationFrameRef.current);
796
- commitAnimationFrameRef.current = null;
797
- }
798
- if (freshBlockActivationFrameRef.current !== null) {
799
- window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
800
- freshBlockActivationFrameRef.current = null;
801
- }
802
- if (displayedUnitSyncFrameRef.current !== null) {
803
- window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
804
- displayedUnitSyncFrameRef.current = null;
805
- }
806
- if (resetAnimationFrameRef.current !== null) {
807
- window.cancelAnimationFrame(resetAnimationFrameRef.current);
808
- }
809
- resetAnimationFrameRef.current = window.requestAnimationFrame(() => {
810
- resetAnimationFrameRef.current = null;
811
- setBatchedTargetUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
812
- setDisplayedUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
813
- setIsFreshBlockActive(false);
832
+ dispatch({
833
+ type: "reset-message",
834
+ isAssistantStreaming,
835
+ targetUnitCount: targetUnits.length
814
836
  });
815
- return () => {
816
- if (resetAnimationFrameRef.current !== null) {
817
- window.cancelAnimationFrame(resetAnimationFrameRef.current);
818
- resetAnimationFrameRef.current = null;
819
- }
820
- };
821
- }, [resetSnapshot]);
837
+ }, [isAssistantStreaming, message.id, targetUnits.length]);
822
838
  (0, import_react5.useEffect)(() => {
823
839
  pendingTargetUnitCountRef.current = targetUnits.length;
824
840
  if (message.role !== "assistant" || !isAssistantStreaming) {
@@ -826,14 +842,14 @@ var useChatMessageReveal = (message) => {
826
842
  window.clearTimeout(inputBatchTimeoutRef.current);
827
843
  inputBatchTimeoutRef.current = null;
828
844
  }
829
- scheduleBatchedTargetUnitCountCommit(targetUnits.length);
845
+ commitBatchedTargetUnitCount(targetUnits.length);
830
846
  return;
831
847
  }
832
848
  if (targetUnits.length <= batchedTargetUnitCountRef.current) {
833
849
  return;
834
850
  }
835
851
  if (batchedTargetUnitCountRef.current === 0) {
836
- scheduleBatchedTargetUnitCountCommit(targetUnits.length);
852
+ commitBatchedTargetUnitCount(targetUnits.length);
837
853
  return;
838
854
  }
839
855
  if (inputBatchTimeoutRef.current !== null) {
@@ -849,13 +865,7 @@ var useChatMessageReveal = (message) => {
849
865
  inputBatchTimeoutRef.current = null;
850
866
  }
851
867
  };
852
- }, [
853
- commitBatchedTargetUnitCount,
854
- isAssistantStreaming,
855
- message.role,
856
- scheduleBatchedTargetUnitCountCommit,
857
- targetUnits.length
858
- ]);
868
+ }, [commitBatchedTargetUnitCount, isAssistantStreaming, message.role, targetUnits.length]);
859
869
  const displayedContent = (0, import_react5.useMemo)(
860
870
  () => targetUnits.slice(0, displayedUnitCount).join(""),
861
871
  [displayedUnitCount, targetUnits]
@@ -867,18 +877,11 @@ var useChatMessageReveal = (message) => {
867
877
  if (!hasNewDisplayedBlock) {
868
878
  return;
869
879
  }
870
- freshBlockActivationFrameRef.current = window.requestAnimationFrame(() => {
871
- freshBlockActivationFrameRef.current = null;
872
- setIsFreshBlockActive(true);
873
- });
880
+ dispatch({ type: "set-fresh-block-active", isActive: true });
874
881
  const timer = window.setTimeout(() => {
875
- setIsFreshBlockActive(false);
882
+ dispatch({ type: "set-fresh-block-active", isActive: false });
876
883
  }, STREAM_FRESH_BLOCK_SETTLE_MS);
877
884
  return () => {
878
- if (freshBlockActivationFrameRef.current !== null) {
879
- window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
880
- freshBlockActivationFrameRef.current = null;
881
- }
882
885
  window.clearTimeout(timer);
883
886
  };
884
887
  }, [contentBlocks.length, message.role]);
@@ -886,40 +889,19 @@ var useChatMessageReveal = (message) => {
886
889
  const shouldAnimateReveal = message.role === "assistant" && displayedUnitCount < batchedTargetUnitCount && (isAssistantStreaming || displayedUnitCount > 0);
887
890
  if (!shouldAnimateReveal) {
888
891
  if (displayedUnitCount !== batchedTargetUnitCount) {
889
- displayedUnitSyncFrameRef.current = window.requestAnimationFrame(() => {
890
- displayedUnitSyncFrameRef.current = null;
891
- setDisplayedUnitCount(batchedTargetUnitCount);
892
- });
892
+ dispatch({ type: "sync-displayed-target" });
893
893
  }
894
- return () => {
895
- if (displayedUnitSyncFrameRef.current !== null) {
896
- window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
897
- displayedUnitSyncFrameRef.current = null;
898
- }
899
- };
894
+ return;
900
895
  }
901
896
  const timer = window.setInterval(() => {
902
- setDisplayedUnitCount((current) => {
903
- if (current >= batchedTargetUnitCount) {
904
- window.clearInterval(timer);
905
- return current;
906
- }
907
- return Math.min(
908
- batchedTargetUnitCount,
909
- getNextDisplayedUnitCount({
910
- currentUnits: current,
911
- targetUnits: batchedTargetUnitCount,
912
- isStreaming: isAssistantStreaming
913
- })
914
- );
915
- });
897
+ dispatch({ type: "advance-reveal", isAssistantStreaming });
916
898
  }, STREAM_REVEAL_TICK_MS);
917
899
  return () => {
918
900
  window.clearInterval(timer);
919
901
  };
920
902
  }, [batchedTargetUnitCount, displayedUnitCount, isAssistantStreaming, message.role]);
921
903
  const settledContent = isFreshBlockActive ? contentBlocks.slice(0, -1).join("\n\n") : displayedContent;
922
- const freshContent = isFreshBlockActive ? contentBlocks.at(-1) ?? "" : "";
904
+ const freshContent = isFreshBlockActive ? contentBlocks[contentBlocks.length - 1] ?? "" : "";
923
905
  return {
924
906
  isAssistantStreaming,
925
907
  displayedContent,
@@ -1877,11 +1859,13 @@ var ChatMessageItemView = ({
1877
1859
  const attachments = message.attachments ?? [];
1878
1860
  const blocks = message.blocks ?? [];
1879
1861
  const hasStructuredBlocks = blocks.length > 0;
1862
+ const hasMarkdownOnlyBlocks = hasStructuredBlocks && blocks.every((block) => block.type === "markdown");
1880
1863
  const hasTextContent = Boolean(settledContent || freshContent || displayedContent);
1864
+ const shouldRenderStructuredBlocks = hasStructuredBlocks && !(isAssistantStreaming && hasMarkdownOnlyBlocks && hasTextContent);
1881
1865
  const isPlanMode = mode === "plan";
1882
1866
  const canSubmitConfirmation = isPlanMode && typeof onConfirmationSubmit === "function";
1883
1867
  const canSubmitQuestionnaire = isPlanMode && typeof onQuestionnaireSubmit === "function";
1884
- const shouldShowStreamingCaret = isAssistantStreaming && (!hasStructuredBlocks || hasTextContent);
1868
+ const shouldShowStreamingCaret = isAssistantStreaming && (!shouldRenderStructuredBlocks || hasTextContent);
1885
1869
  const renderChatMessageBlock = (block, index) => {
1886
1870
  switch (block.type) {
1887
1871
  case "markdown":
@@ -1961,8 +1945,8 @@ var ChatMessageItemView = ({
1961
1945
  isStoppedAssistant ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(StatusTag, { "data-testid": "chat-message-stopped-tag", children: labels.stoppedResponse }) : null
1962
1946
  ] }),
1963
1947
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(Content, { "data-testid": "chat-message-content", children: [
1964
- hasStructuredBlocks || hasTextContent ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1965
- hasStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1948
+ shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1949
+ shouldRenderStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1966
1950
  ContentSegment,
1967
1951
  {
1968
1952
  "data-testid": "chat-message-content-segment",
package/dist/index.mjs CHANGED
@@ -590,7 +590,7 @@ var AiChatProvider = (props) => {
590
590
  };
591
591
 
592
592
  // src/components/chat-thread/index.tsx
593
- import { useCallback as useCallback2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4, useState as useState5 } from "react";
593
+ import { useCallback as useCallback2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
594
594
  import styled10 from "@emotion/styled";
595
595
 
596
596
  // src/context/use-chat-context.ts
@@ -627,7 +627,7 @@ var calculateChatThreadScrollSpacerHeight = ({
627
627
  );
628
628
 
629
629
  // src/components/chat-thread/components/chat-message-item.tsx
630
- import { Fragment, memo, useState as useState4 } from "react";
630
+ import { Fragment, memo, useState as useState3 } from "react";
631
631
  import styled7 from "@emotion/styled";
632
632
  import { keyframes } from "@emotion/react";
633
633
  import ReactMarkdown from "react-markdown";
@@ -636,7 +636,7 @@ import remarkMath from "remark-math";
636
636
  import rehypeKatex from "rehype-katex";
637
637
 
638
638
  // src/components/chat-thread/hooks/use-chat-message-reveal.ts
639
- import { useCallback, useEffect, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
639
+ import { useCallback, useEffect, useMemo as useMemo2, useReducer, useRef as useRef2 } from "react";
640
640
 
641
641
  // src/components/chat-thread/lib/message-reveal.ts
642
642
  var STREAM_REVEAL_TICK_MS = 36;
@@ -683,96 +683,112 @@ var getNextDisplayedUnitCount = ({
683
683
  var splitMarkdownBlocks = (content) => content.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
684
684
 
685
685
  // src/components/chat-thread/hooks/use-chat-message-reveal.ts
686
+ var createRevealState = ({
687
+ isAssistantStreaming,
688
+ targetUnitCount
689
+ }) => {
690
+ const initialDisplayedUnitCount = isAssistantStreaming ? 0 : targetUnitCount;
691
+ return {
692
+ batchedTargetUnitCount: initialDisplayedUnitCount,
693
+ displayedUnitCount: initialDisplayedUnitCount,
694
+ isFreshBlockActive: false
695
+ };
696
+ };
697
+ var revealReducer = (state, action) => {
698
+ switch (action.type) {
699
+ case "reset-message":
700
+ return createRevealState(action);
701
+ case "commit-batched-target": {
702
+ const nextDisplayedUnitCount = action.role === "assistant" ? getNextDisplayedUnitCount({
703
+ currentUnits: state.displayedUnitCount,
704
+ targetUnits: action.nextTargetUnitCount,
705
+ isStreaming: action.isAssistantStreaming,
706
+ minimumStep: state.displayedUnitCount > 0 && action.isAssistantStreaming ? 2 : 1
707
+ }) : action.nextTargetUnitCount;
708
+ return {
709
+ ...state,
710
+ batchedTargetUnitCount: action.nextTargetUnitCount,
711
+ displayedUnitCount: nextDisplayedUnitCount
712
+ };
713
+ }
714
+ case "set-fresh-block-active":
715
+ return state.isFreshBlockActive === action.isActive ? state : {
716
+ ...state,
717
+ isFreshBlockActive: action.isActive
718
+ };
719
+ case "sync-displayed-target":
720
+ return state.displayedUnitCount === state.batchedTargetUnitCount ? state : {
721
+ ...state,
722
+ displayedUnitCount: state.batchedTargetUnitCount
723
+ };
724
+ case "advance-reveal": {
725
+ if (state.displayedUnitCount >= state.batchedTargetUnitCount) {
726
+ return state;
727
+ }
728
+ return {
729
+ ...state,
730
+ displayedUnitCount: Math.min(
731
+ state.batchedTargetUnitCount,
732
+ getNextDisplayedUnitCount({
733
+ currentUnits: state.displayedUnitCount,
734
+ targetUnits: state.batchedTargetUnitCount,
735
+ isStreaming: action.isAssistantStreaming
736
+ })
737
+ )
738
+ };
739
+ }
740
+ default:
741
+ return state;
742
+ }
743
+ };
686
744
  var useChatMessageReveal = (message) => {
687
745
  const isAssistantStreaming = message.role === "assistant" && message.status === "streaming";
688
746
  const targetContent = message.content || "";
689
747
  const targetUnits = useMemo2(() => Array.from(targetContent), [targetContent]);
690
- const resetSnapshot = useMemo2(
691
- () => ({
692
- messageId: message.id,
693
- initialTargetUnitCount: targetUnits.length,
694
- initialBatchedTargetUnitCount: isAssistantStreaming ? 0 : targetUnits.length
695
- }),
696
- [isAssistantStreaming, message.id, targetUnits.length]
697
- );
698
748
  const pendingTargetUnitCountRef = useRef2(targetUnits.length);
699
749
  const batchedTargetUnitCountRef = useRef2(isAssistantStreaming ? 0 : targetUnits.length);
700
750
  const inputBatchTimeoutRef = useRef2(null);
701
- const commitAnimationFrameRef = useRef2(null);
702
- const freshBlockActivationFrameRef = useRef2(null);
703
- const displayedUnitSyncFrameRef = useRef2(null);
704
- const resetAnimationFrameRef = useRef2(null);
705
- const [batchedTargetUnitCount, setBatchedTargetUnitCount] = useState2(
706
- () => isAssistantStreaming ? 0 : targetUnits.length
707
- );
708
751
  const lastDisplayedBlockCountRef = useRef2(0);
709
- const [displayedUnitCount, setDisplayedUnitCount] = useState2(
710
- () => isAssistantStreaming ? 0 : targetUnits.length
752
+ const previousMessageIdRef = useRef2(message.id);
753
+ const [state, dispatch] = useReducer(
754
+ revealReducer,
755
+ {
756
+ isAssistantStreaming,
757
+ targetUnitCount: targetUnits.length
758
+ },
759
+ createRevealState
711
760
  );
712
- const [isFreshBlockActive, setIsFreshBlockActive] = useState2(false);
761
+ const { batchedTargetUnitCount, displayedUnitCount, isFreshBlockActive } = state;
713
762
  const commitBatchedTargetUnitCount = useCallback(
714
763
  (nextTargetUnitCount) => {
715
764
  batchedTargetUnitCountRef.current = nextTargetUnitCount;
716
- setBatchedTargetUnitCount(nextTargetUnitCount);
717
- setDisplayedUnitCount(
718
- (current) => message.role === "assistant" ? getNextDisplayedUnitCount({
719
- currentUnits: current,
720
- targetUnits: nextTargetUnitCount,
721
- isStreaming: isAssistantStreaming,
722
- minimumStep: current > 0 && isAssistantStreaming ? 2 : 1
723
- }) : nextTargetUnitCount
724
- );
725
- },
726
- [isAssistantStreaming, message.role]
727
- );
728
- const scheduleBatchedTargetUnitCountCommit = useCallback(
729
- (nextTargetUnitCount) => {
730
- if (commitAnimationFrameRef.current !== null) {
731
- window.cancelAnimationFrame(commitAnimationFrameRef.current);
732
- }
733
- commitAnimationFrameRef.current = window.requestAnimationFrame(() => {
734
- commitAnimationFrameRef.current = null;
735
- commitBatchedTargetUnitCount(nextTargetUnitCount);
765
+ dispatch({
766
+ type: "commit-batched-target",
767
+ isAssistantStreaming,
768
+ nextTargetUnitCount,
769
+ role: message.role
736
770
  });
737
771
  },
738
- [commitBatchedTargetUnitCount]
772
+ [isAssistantStreaming, message.role]
739
773
  );
740
774
  useEffect(() => {
741
- pendingTargetUnitCountRef.current = resetSnapshot.initialTargetUnitCount;
742
- batchedTargetUnitCountRef.current = resetSnapshot.initialBatchedTargetUnitCount;
775
+ if (previousMessageIdRef.current === message.id) {
776
+ return;
777
+ }
778
+ previousMessageIdRef.current = message.id;
779
+ pendingTargetUnitCountRef.current = targetUnits.length;
780
+ batchedTargetUnitCountRef.current = isAssistantStreaming ? 0 : targetUnits.length;
743
781
  lastDisplayedBlockCountRef.current = 0;
744
782
  if (inputBatchTimeoutRef.current !== null) {
745
783
  window.clearTimeout(inputBatchTimeoutRef.current);
746
784
  inputBatchTimeoutRef.current = null;
747
785
  }
748
- if (commitAnimationFrameRef.current !== null) {
749
- window.cancelAnimationFrame(commitAnimationFrameRef.current);
750
- commitAnimationFrameRef.current = null;
751
- }
752
- if (freshBlockActivationFrameRef.current !== null) {
753
- window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
754
- freshBlockActivationFrameRef.current = null;
755
- }
756
- if (displayedUnitSyncFrameRef.current !== null) {
757
- window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
758
- displayedUnitSyncFrameRef.current = null;
759
- }
760
- if (resetAnimationFrameRef.current !== null) {
761
- window.cancelAnimationFrame(resetAnimationFrameRef.current);
762
- }
763
- resetAnimationFrameRef.current = window.requestAnimationFrame(() => {
764
- resetAnimationFrameRef.current = null;
765
- setBatchedTargetUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
766
- setDisplayedUnitCount(resetSnapshot.initialBatchedTargetUnitCount);
767
- setIsFreshBlockActive(false);
786
+ dispatch({
787
+ type: "reset-message",
788
+ isAssistantStreaming,
789
+ targetUnitCount: targetUnits.length
768
790
  });
769
- return () => {
770
- if (resetAnimationFrameRef.current !== null) {
771
- window.cancelAnimationFrame(resetAnimationFrameRef.current);
772
- resetAnimationFrameRef.current = null;
773
- }
774
- };
775
- }, [resetSnapshot]);
791
+ }, [isAssistantStreaming, message.id, targetUnits.length]);
776
792
  useEffect(() => {
777
793
  pendingTargetUnitCountRef.current = targetUnits.length;
778
794
  if (message.role !== "assistant" || !isAssistantStreaming) {
@@ -780,14 +796,14 @@ var useChatMessageReveal = (message) => {
780
796
  window.clearTimeout(inputBatchTimeoutRef.current);
781
797
  inputBatchTimeoutRef.current = null;
782
798
  }
783
- scheduleBatchedTargetUnitCountCommit(targetUnits.length);
799
+ commitBatchedTargetUnitCount(targetUnits.length);
784
800
  return;
785
801
  }
786
802
  if (targetUnits.length <= batchedTargetUnitCountRef.current) {
787
803
  return;
788
804
  }
789
805
  if (batchedTargetUnitCountRef.current === 0) {
790
- scheduleBatchedTargetUnitCountCommit(targetUnits.length);
806
+ commitBatchedTargetUnitCount(targetUnits.length);
791
807
  return;
792
808
  }
793
809
  if (inputBatchTimeoutRef.current !== null) {
@@ -803,13 +819,7 @@ var useChatMessageReveal = (message) => {
803
819
  inputBatchTimeoutRef.current = null;
804
820
  }
805
821
  };
806
- }, [
807
- commitBatchedTargetUnitCount,
808
- isAssistantStreaming,
809
- message.role,
810
- scheduleBatchedTargetUnitCountCommit,
811
- targetUnits.length
812
- ]);
822
+ }, [commitBatchedTargetUnitCount, isAssistantStreaming, message.role, targetUnits.length]);
813
823
  const displayedContent = useMemo2(
814
824
  () => targetUnits.slice(0, displayedUnitCount).join(""),
815
825
  [displayedUnitCount, targetUnits]
@@ -821,18 +831,11 @@ var useChatMessageReveal = (message) => {
821
831
  if (!hasNewDisplayedBlock) {
822
832
  return;
823
833
  }
824
- freshBlockActivationFrameRef.current = window.requestAnimationFrame(() => {
825
- freshBlockActivationFrameRef.current = null;
826
- setIsFreshBlockActive(true);
827
- });
834
+ dispatch({ type: "set-fresh-block-active", isActive: true });
828
835
  const timer = window.setTimeout(() => {
829
- setIsFreshBlockActive(false);
836
+ dispatch({ type: "set-fresh-block-active", isActive: false });
830
837
  }, STREAM_FRESH_BLOCK_SETTLE_MS);
831
838
  return () => {
832
- if (freshBlockActivationFrameRef.current !== null) {
833
- window.cancelAnimationFrame(freshBlockActivationFrameRef.current);
834
- freshBlockActivationFrameRef.current = null;
835
- }
836
839
  window.clearTimeout(timer);
837
840
  };
838
841
  }, [contentBlocks.length, message.role]);
@@ -840,40 +843,19 @@ var useChatMessageReveal = (message) => {
840
843
  const shouldAnimateReveal = message.role === "assistant" && displayedUnitCount < batchedTargetUnitCount && (isAssistantStreaming || displayedUnitCount > 0);
841
844
  if (!shouldAnimateReveal) {
842
845
  if (displayedUnitCount !== batchedTargetUnitCount) {
843
- displayedUnitSyncFrameRef.current = window.requestAnimationFrame(() => {
844
- displayedUnitSyncFrameRef.current = null;
845
- setDisplayedUnitCount(batchedTargetUnitCount);
846
- });
846
+ dispatch({ type: "sync-displayed-target" });
847
847
  }
848
- return () => {
849
- if (displayedUnitSyncFrameRef.current !== null) {
850
- window.cancelAnimationFrame(displayedUnitSyncFrameRef.current);
851
- displayedUnitSyncFrameRef.current = null;
852
- }
853
- };
848
+ return;
854
849
  }
855
850
  const timer = window.setInterval(() => {
856
- setDisplayedUnitCount((current) => {
857
- if (current >= batchedTargetUnitCount) {
858
- window.clearInterval(timer);
859
- return current;
860
- }
861
- return Math.min(
862
- batchedTargetUnitCount,
863
- getNextDisplayedUnitCount({
864
- currentUnits: current,
865
- targetUnits: batchedTargetUnitCount,
866
- isStreaming: isAssistantStreaming
867
- })
868
- );
869
- });
851
+ dispatch({ type: "advance-reveal", isAssistantStreaming });
870
852
  }, STREAM_REVEAL_TICK_MS);
871
853
  return () => {
872
854
  window.clearInterval(timer);
873
855
  };
874
856
  }, [batchedTargetUnitCount, displayedUnitCount, isAssistantStreaming, message.role]);
875
857
  const settledContent = isFreshBlockActive ? contentBlocks.slice(0, -1).join("\n\n") : displayedContent;
876
- const freshContent = isFreshBlockActive ? contentBlocks.at(-1) ?? "" : "";
858
+ const freshContent = isFreshBlockActive ? contentBlocks[contentBlocks.length - 1] ?? "" : "";
877
859
  return {
878
860
  isAssistantStreaming,
879
861
  displayedContent,
@@ -1092,7 +1074,7 @@ var Value = styled3.span`
1092
1074
  `;
1093
1075
 
1094
1076
  // src/components/chat-thread/components/pde-ai-questionnaire-card.tsx
1095
- import { useState as useState3 } from "react";
1077
+ import { useState as useState2 } from "react";
1096
1078
  import styled4 from "@emotion/styled";
1097
1079
  import { jsx as jsx5, jsxs as jsxs3 } from "@emotion/react/jsx-runtime";
1098
1080
  var OTHER_OPTION_VALUE = "__other__";
@@ -1194,10 +1176,10 @@ var PDEAIQuestionnaireCardInner = ({
1194
1176
  interactive = false,
1195
1177
  onSubmit
1196
1178
  }) => {
1197
- const [answers, setAnswers] = useState3(
1179
+ const [answers, setAnswers] = useState2(
1198
1180
  () => createInitialAnswers(questionnaire)
1199
1181
  );
1200
- const [errorMessage, setErrorMessage] = useState3(null);
1182
+ const [errorMessage, setErrorMessage] = useState2(null);
1201
1183
  const handleSubmit = () => {
1202
1184
  const missingQuestions = questionnaire.questions.filter(
1203
1185
  (question) => question.required && isMissingRequiredAnswer(question, answers)
@@ -1825,17 +1807,19 @@ var ChatMessageItemView = ({
1825
1807
  renderMessageBlock
1826
1808
  }) => {
1827
1809
  const { labels } = useChatContext();
1828
- const [activeImage, setActiveImage] = useState4(void 0);
1810
+ const [activeImage, setActiveImage] = useState3(void 0);
1829
1811
  const { displayedContent, freshContent, isAssistantStreaming, settledContent } = useChatMessageReveal(message);
1830
1812
  const isStoppedAssistant = message.role === "assistant" && message.status === "stopped";
1831
1813
  const attachments = message.attachments ?? [];
1832
1814
  const blocks = message.blocks ?? [];
1833
1815
  const hasStructuredBlocks = blocks.length > 0;
1816
+ const hasMarkdownOnlyBlocks = hasStructuredBlocks && blocks.every((block) => block.type === "markdown");
1834
1817
  const hasTextContent = Boolean(settledContent || freshContent || displayedContent);
1818
+ const shouldRenderStructuredBlocks = hasStructuredBlocks && !(isAssistantStreaming && hasMarkdownOnlyBlocks && hasTextContent);
1835
1819
  const isPlanMode = mode === "plan";
1836
1820
  const canSubmitConfirmation = isPlanMode && typeof onConfirmationSubmit === "function";
1837
1821
  const canSubmitQuestionnaire = isPlanMode && typeof onQuestionnaireSubmit === "function";
1838
- const shouldShowStreamingCaret = isAssistantStreaming && (!hasStructuredBlocks || hasTextContent);
1822
+ const shouldShowStreamingCaret = isAssistantStreaming && (!shouldRenderStructuredBlocks || hasTextContent);
1839
1823
  const renderChatMessageBlock = (block, index) => {
1840
1824
  switch (block.type) {
1841
1825
  case "markdown":
@@ -1915,8 +1899,8 @@ var ChatMessageItemView = ({
1915
1899
  isStoppedAssistant ? /* @__PURE__ */ jsx8(StatusTag, { "data-testid": "chat-message-stopped-tag", children: labels.stoppedResponse }) : null
1916
1900
  ] }),
1917
1901
  /* @__PURE__ */ jsxs5(Content, { "data-testid": "chat-message-content", children: [
1918
- hasStructuredBlocks || hasTextContent ? /* @__PURE__ */ jsxs5(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1919
- hasStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ jsx8(
1902
+ shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ jsxs5(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1903
+ shouldRenderStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ jsx8(
1920
1904
  ContentSegment,
1921
1905
  {
1922
1906
  "data-testid": "chat-message-content-segment",
@@ -2302,7 +2286,7 @@ var ChatThreadView = ({
2302
2286
  const latestUserMessageRef = useRef4(null);
2303
2287
  const pendingScrollUserMessageIdRef = useRef4(void 0);
2304
2288
  const reservedSpaceFrameRef = useRef4(null);
2305
- const [latestUserMessageReservedSpace, setLatestUserMessageReservedSpace] = useState5({ messageId: void 0, value: 0 });
2289
+ const [latestUserMessageReservedSpace, setLatestUserMessageReservedSpace] = useState4({ messageId: void 0, value: 0 });
2306
2290
  const reservedPaddingBottom = 24 + (latestUserMessageReservedSpace.messageId === latestUserMessageId ? latestUserMessageReservedSpace.value : 0);
2307
2291
  const measureLatestUserMessageReservedSpace = useCallback2((messageId) => {
2308
2292
  const container = containerRef.current;
@@ -2628,10 +2612,10 @@ var resolveSendSession = ({
2628
2612
  };
2629
2613
 
2630
2614
  // src/components/chat-composer/hooks/use-chat-composer.ts
2631
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState7 } from "react";
2615
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState6 } from "react";
2632
2616
 
2633
2617
  // src/components/chat-composer/hooks/use-composer-attachments.ts
2634
- import { useEffect as useEffect3, useRef as useRef5, useState as useState6 } from "react";
2618
+ import { useEffect as useEffect3, useRef as useRef5, useState as useState5 } from "react";
2635
2619
  var SUPPORTED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
2636
2620
  var MAX_COMPOSER_ATTACHMENTS = 10;
2637
2621
  var createObjectUrl = (file) => typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : "";
@@ -2645,7 +2629,7 @@ var releaseComposerAttachments = (attachments) => {
2645
2629
  attachments.forEach((attachment) => revokeObjectUrl(attachment.previewUrl));
2646
2630
  };
2647
2631
  var useComposerAttachments = () => {
2648
- const [attachments, setAttachments] = useState6([]);
2632
+ const [attachments, setAttachments] = useState5([]);
2649
2633
  const attachmentsRef = useRef5([]);
2650
2634
  useEffect3(() => {
2651
2635
  attachmentsRef.current = attachments;
@@ -2763,9 +2747,9 @@ var useChatComposer = () => {
2763
2747
  const clearSessionError = useChatStore((s) => s.clearSessionError);
2764
2748
  const setPreferredMode = useChatStore((s) => s.setPreferredMode);
2765
2749
  const setSessionMode = useChatStore((s) => s.setSessionMode);
2766
- const [availableModels, setAvailableModels] = useState7([]);
2767
- const [isModelsLoading, setIsModelsLoading] = useState7(true);
2768
- const [isModelsError, setIsModelsError] = useState7(false);
2750
+ const [availableModels, setAvailableModels] = useState6([]);
2751
+ const [isModelsLoading, setIsModelsLoading] = useState6(true);
2752
+ const [isModelsError, setIsModelsError] = useState6(false);
2769
2753
  const fetchModels = useCallback3(async () => {
2770
2754
  setIsModelsLoading(true);
2771
2755
  setIsModelsError(false);
@@ -2782,10 +2766,10 @@ var useChatComposer = () => {
2782
2766
  void fetchModels();
2783
2767
  }, [fetchModels]);
2784
2768
  const hasModels = availableModels.length > 0;
2785
- const [value, setValue] = useState7("");
2786
- const [selectedModel, setSelectedModel] = useState7("");
2787
- const [selectedMode, setSelectedModeLocal] = useState7(DEFAULT_CHAT_AGENT_MODE);
2788
- const [attachmentNotice, setAttachmentNotice] = useState7(null);
2769
+ const [value, setValue] = useState6("");
2770
+ const [selectedModel, setSelectedModel] = useState6("");
2771
+ const [selectedMode, setSelectedModeLocal] = useState6(DEFAULT_CHAT_AGENT_MODE);
2772
+ const [attachmentNotice, setAttachmentNotice] = useState6(null);
2789
2773
  const { attachments, appendFiles, removeAttachment, takeMessageAttachments } = useComposerAttachments();
2790
2774
  const abortControllerRef = useRef6(null);
2791
2775
  const stopRequestRef = useRef6(null);
@@ -3081,14 +3065,14 @@ var useChatComposer = () => {
3081
3065
  };
3082
3066
 
3083
3067
  // src/components/chat-composer/components/chat-composer-attachment-list.tsx
3084
- import { useState as useState8 } from "react";
3068
+ import { useState as useState7 } from "react";
3085
3069
  import styled11 from "@emotion/styled";
3086
3070
  import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs8 } from "@emotion/react/jsx-runtime";
3087
3071
  var ChatComposerAttachmentList = ({
3088
3072
  attachments,
3089
3073
  onRemoveAttachment
3090
3074
  }) => {
3091
- const [activeImage, setActiveImage] = useState8(null);
3075
+ const [activeImage, setActiveImage] = useState7(null);
3092
3076
  if (!attachments.length) {
3093
3077
  return null;
3094
3078
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinghunm/ai-chat",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "AI chat React component library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",