@xinghunm/ai-chat 0.2.2 → 0.4.0

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.mjs CHANGED
@@ -16,6 +16,7 @@ import { createStore } from "zustand/vanilla";
16
16
  // src/types/index.ts
17
17
  var CHAT_AGENT_MODES = ["ask", "plan", "agent"];
18
18
  var DEFAULT_CHAT_AGENT_MODE = "agent";
19
+ var CHAT_MESSAGE_RENDER_ORDERS = ["blocks-first", "timeline"];
19
20
  var DEFAULT_AI_CHAT_LABELS = {
20
21
  sendButton: "Send",
21
22
  stopButton: "Stop",
@@ -517,7 +518,14 @@ var createDefaultChatTransport = ({
517
518
  // src/components/ai-chat-provider/index.tsx
518
519
  import { jsx } from "@emotion/react/jsx-runtime";
519
520
  var AiChatProvider = (props) => {
520
- const { defaultMode, labels, renderMessageBlock, enableImageAttachments = true, children } = props;
521
+ const {
522
+ defaultMode,
523
+ labels,
524
+ renderMessageBlock,
525
+ messageRenderOrder,
526
+ enableImageAttachments = true,
527
+ children
528
+ } = props;
521
529
  const [store] = useState(
522
530
  () => createChatStore(defaultMode ? { preferredMode: defaultMode } : void 0)
523
531
  );
@@ -569,6 +577,7 @@ var AiChatProvider = (props) => {
569
577
  sendRef,
570
578
  retryRef,
571
579
  renderMessageBlock,
580
+ messageRenderOrder,
572
581
  transformStreamPacket: defaultTransformStreamPacket,
573
582
  enableImageAttachments
574
583
  }),
@@ -579,6 +588,7 @@ var AiChatProvider = (props) => {
579
588
  defaultTransformStreamPacket,
580
589
  enableImageAttachments,
581
590
  labels,
591
+ messageRenderOrder,
582
592
  renderMessageBlock,
583
593
  sendRef,
584
594
  retryRef,
@@ -590,7 +600,7 @@ var AiChatProvider = (props) => {
590
600
  };
591
601
 
592
602
  // src/components/chat-thread/index.tsx
593
- import { useCallback as useCallback2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
603
+ import { useCallback as useCallback2, useLayoutEffect, useMemo as useMemo4, useRef as useRef4, useState as useState4 } from "react";
594
604
  import styled9 from "@emotion/styled";
595
605
 
596
606
  // src/context/use-chat-context.ts
@@ -851,6 +861,7 @@ var useChatMessageReveal = (message) => {
851
861
  ];
852
862
  return {
853
863
  isAssistantStreaming,
864
+ isFreshBlockActive,
854
865
  displayedContent,
855
866
  settledContent,
856
867
  freshContent,
@@ -858,6 +869,325 @@ var useChatMessageReveal = (message) => {
858
869
  };
859
870
  };
860
871
 
872
+ // src/components/chat-thread/hooks/use-timeline-block-anchors.ts
873
+ import { useEffect as useEffect2, useMemo as useMemo3, useReducer as useReducer2 } from "react";
874
+
875
+ // src/components/chat-thread/lib/chat-message-timeline.ts
876
+ var stringifyTimelineKeyPart = (value) => {
877
+ if (value === null || value === void 0) {
878
+ return String(value);
879
+ }
880
+ if (Array.isArray(value)) {
881
+ return `[${value.map((item) => stringifyTimelineKeyPart(item)).join(",")}]`;
882
+ }
883
+ if (typeof value === "object") {
884
+ return `{${Object.entries(value).sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)).map(([key, nestedValue]) => `${key}:${stringifyTimelineKeyPart(nestedValue)}`).join(",")}}`;
885
+ }
886
+ return String(value);
887
+ };
888
+ var getTimelineBlockKey = (block, index) => {
889
+ switch (block.type) {
890
+ case "markdown":
891
+ return null;
892
+ case "notice":
893
+ return `${index}:notice:${block.tone}:${block.text}`;
894
+ case "parameter_summary":
895
+ return `${index}:parameter_summary:${block.items.map((item) => `${item.label}:${item.value}:${item.fieldPath ?? ""}`).join("|")}`;
896
+ case "confirmation_card":
897
+ return `${index}:confirmation_card:${block.proposal.proposalId}`;
898
+ case "result_summary":
899
+ return `${index}:result_summary:${block.summary.taskId}:${block.summary.status}`;
900
+ case "questionnaire":
901
+ return `${index}:questionnaire:${block.questionnaire.questionnaireId}`;
902
+ case "custom":
903
+ return `${index}:custom:${block.kind}:${stringifyTimelineKeyPart(block.data)}`;
904
+ default:
905
+ return null;
906
+ }
907
+ };
908
+ var getTimelineConsumedText = (blocks) => blocks.filter(
909
+ (block) => block.type === "markdown"
910
+ ).map((block) => block.text).join("\n\n");
911
+ var getTimelineTextStream = (content, blocks) => {
912
+ const consumedText = getTimelineConsumedText(blocks);
913
+ if (consumedText.length > 0 && content.startsWith(consumedText)) {
914
+ return content.slice(consumedText.length);
915
+ }
916
+ return content;
917
+ };
918
+ var buildTimelineTextDisplay = (content, isAssistantStreaming, isFreshBlockActive = isAssistantStreaming) => {
919
+ const contentBlocks = splitMarkdownBlocks(content);
920
+ const settledContent = isAssistantStreaming && isFreshBlockActive && contentBlocks.length > 1 ? contentBlocks.slice(0, -1).join("\n\n") : content;
921
+ const freshContent = isAssistantStreaming && isFreshBlockActive && contentBlocks.length > 1 ? contentBlocks[contentBlocks.length - 1] ?? "" : "";
922
+ const displayedBlocks = contentBlocks.length > 1 ? contentBlocks.map((blockContent, index) => ({
923
+ content: blockContent,
924
+ tone: isAssistantStreaming && isFreshBlockActive && freshContent && index === contentBlocks.length - 1 ? "fresh" : "settled"
925
+ })) : [{ content, tone: "settled" }];
926
+ return {
927
+ settledContent,
928
+ freshContent,
929
+ displayedBlocks
930
+ };
931
+ };
932
+ var getTimelineDisplayUnitCount = (content) => splitMarkdownBlocks(content).reduce((count, block) => count + Array.from(block).length, 0);
933
+ var buildAnchoredTimelineSegments = ({
934
+ blocks,
935
+ timelineBlockAnchors,
936
+ timelineDisplayedBlocks,
937
+ visibleTimelineBlockKeys
938
+ }) => {
939
+ const orderedTimelineSegments = [];
940
+ const totalTimelineUnits = timelineDisplayedBlocks.reduce(
941
+ (count, block) => count + Array.from(block.content).length,
942
+ 0
943
+ );
944
+ let textCursor = 0;
945
+ const buildTextSegment = (start, end, options) => {
946
+ if (end <= start) {
947
+ return null;
948
+ }
949
+ const displayedBlocks = [];
950
+ let blockCursor = 0;
951
+ for (const block of timelineDisplayedBlocks) {
952
+ const blockUnits = Array.from(block.content);
953
+ const blockStart = blockCursor;
954
+ const blockEnd = blockCursor + blockUnits.length;
955
+ if (blockEnd <= start) {
956
+ blockCursor = blockEnd;
957
+ continue;
958
+ }
959
+ if (blockStart >= end) {
960
+ break;
961
+ }
962
+ const sliceStart = Math.max(0, start - blockStart);
963
+ const sliceEnd = Math.min(blockUnits.length, end - blockStart);
964
+ const slicedContent = blockUnits.slice(sliceStart, sliceEnd).join("");
965
+ if (slicedContent) {
966
+ displayedBlocks.push({
967
+ content: slicedContent,
968
+ tone: options?.forceSettled ? "settled" : block.tone
969
+ });
970
+ }
971
+ blockCursor = blockEnd;
972
+ }
973
+ const content = displayedBlocks.map((block) => block.content).join("\n\n");
974
+ if (!content) {
975
+ return null;
976
+ }
977
+ return {
978
+ type: "text",
979
+ content,
980
+ displayedBlocks,
981
+ useTimelineSegmentation: true
982
+ };
983
+ };
984
+ let trailingCutoff = totalTimelineUnits;
985
+ for (const [index, block] of blocks.entries()) {
986
+ if (block.type === "markdown") {
987
+ orderedTimelineSegments.push({
988
+ type: "markdown",
989
+ content: block.text
990
+ });
991
+ continue;
992
+ }
993
+ const blockKey = getTimelineBlockKey(block, index);
994
+ const anchor = blockKey !== null ? timelineBlockAnchors[blockKey] ?? totalTimelineUnits : totalTimelineUnits;
995
+ const isBlockVisible = blockKey !== null && visibleTimelineBlockKeys?.[blockKey] ? true : anchor <= totalTimelineUnits;
996
+ if (anchor > textCursor) {
997
+ const textSegment = buildTextSegment(textCursor, Math.min(anchor, totalTimelineUnits), {
998
+ forceSettled: isBlockVisible
999
+ });
1000
+ if (textSegment) {
1001
+ orderedTimelineSegments.push(textSegment);
1002
+ }
1003
+ }
1004
+ if (!isBlockVisible) {
1005
+ textCursor = Math.min(anchor, totalTimelineUnits);
1006
+ trailingCutoff = Math.min(trailingCutoff, textCursor);
1007
+ continue;
1008
+ }
1009
+ trailingCutoff = totalTimelineUnits;
1010
+ orderedTimelineSegments.push({
1011
+ type: "block",
1012
+ block,
1013
+ index
1014
+ });
1015
+ textCursor = Math.max(textCursor, anchor);
1016
+ }
1017
+ const trailingTextSegment = buildTextSegment(textCursor, trailingCutoff);
1018
+ if (trailingTextSegment) {
1019
+ orderedTimelineSegments.push(trailingTextSegment);
1020
+ }
1021
+ return orderedTimelineSegments;
1022
+ };
1023
+
1024
+ // src/components/chat-thread/hooks/use-timeline-block-anchors.ts
1025
+ var createTimelineAnchorState = ({
1026
+ messageId,
1027
+ currentBlockKeys
1028
+ }) => ({
1029
+ messageId,
1030
+ previousBlockKeys: currentBlockKeys,
1031
+ timelineBlockAnchors: {},
1032
+ visibleTimelineBlockKeys: {}
1033
+ });
1034
+ var timelineAnchorReducer = (state, action) => {
1035
+ switch (action.type) {
1036
+ case "reset-message":
1037
+ if (state.messageId === action.messageId) {
1038
+ return state;
1039
+ }
1040
+ return createTimelineAnchorState(action);
1041
+ case "sync-anchors": {
1042
+ const previousBlockKeys = new Set(state.previousBlockKeys);
1043
+ const nextAnchors = action.currentBlockKeys.reduce(
1044
+ (acc, blockKey) => {
1045
+ const existingAnchor = state.timelineBlockAnchors[blockKey];
1046
+ if (existingAnchor !== void 0) {
1047
+ acc[blockKey] = existingAnchor;
1048
+ return acc;
1049
+ }
1050
+ if (!previousBlockKeys.has(blockKey)) {
1051
+ acc[blockKey] = action.timelineTextStreamLength;
1052
+ }
1053
+ return acc;
1054
+ },
1055
+ {}
1056
+ );
1057
+ const hasAnchorChanged = Object.keys(nextAnchors).length !== Object.keys(state.timelineBlockAnchors).length || Object.entries(nextAnchors).some(
1058
+ ([blockKey, anchor]) => state.timelineBlockAnchors[blockKey] !== anchor
1059
+ );
1060
+ const hasPreviousKeysChanged = action.currentBlockKeys.length !== state.previousBlockKeys.length || action.currentBlockKeys.some(
1061
+ (blockKey, index) => state.previousBlockKeys[index] !== blockKey
1062
+ );
1063
+ if (!hasAnchorChanged && !hasPreviousKeysChanged) {
1064
+ return state;
1065
+ }
1066
+ return {
1067
+ ...state,
1068
+ previousBlockKeys: action.currentBlockKeys,
1069
+ timelineBlockAnchors: hasAnchorChanged ? nextAnchors : state.timelineBlockAnchors
1070
+ };
1071
+ }
1072
+ case "sync-visible": {
1073
+ const nextVisibleBlockKeys = action.currentBlockKeys.reduce(
1074
+ (acc, blockKey) => {
1075
+ if (state.visibleTimelineBlockKeys[blockKey]) {
1076
+ acc[blockKey] = true;
1077
+ return acc;
1078
+ }
1079
+ const anchor = action.effectiveTimelineBlockAnchors[blockKey];
1080
+ if (anchor !== void 0 && anchor <= action.displayedTimelineTextLength) {
1081
+ acc[blockKey] = true;
1082
+ }
1083
+ return acc;
1084
+ },
1085
+ {}
1086
+ );
1087
+ const hasVisibleBlockChanged = Object.keys(nextVisibleBlockKeys).length !== Object.keys(state.visibleTimelineBlockKeys).length || Object.keys(nextVisibleBlockKeys).some(
1088
+ (blockKey) => !state.visibleTimelineBlockKeys[blockKey]
1089
+ );
1090
+ if (!hasVisibleBlockChanged) {
1091
+ return state;
1092
+ }
1093
+ return {
1094
+ ...state,
1095
+ visibleTimelineBlockKeys: nextVisibleBlockKeys
1096
+ };
1097
+ }
1098
+ default:
1099
+ return state;
1100
+ }
1101
+ };
1102
+ var useTimelineBlockAnchors = ({
1103
+ blocks,
1104
+ displayedTimelineTextLength,
1105
+ isAssistantStreaming,
1106
+ message,
1107
+ messageRenderOrder
1108
+ }) => {
1109
+ const currentTimelineBlockKeys = useMemo3(
1110
+ () => blocks.map((block, index) => getTimelineBlockKey(block, index)).filter((blockKey) => Boolean(blockKey)),
1111
+ [blocks]
1112
+ );
1113
+ const timelineTextStreamLength = useMemo3(
1114
+ () => getTimelineDisplayUnitCount(getTimelineTextStream(message.content, blocks)),
1115
+ [blocks, message.content]
1116
+ );
1117
+ const [state, dispatch] = useReducer2(
1118
+ timelineAnchorReducer,
1119
+ {
1120
+ messageId: message.id,
1121
+ currentBlockKeys: currentTimelineBlockKeys
1122
+ },
1123
+ createTimelineAnchorState
1124
+ );
1125
+ const effectiveTimelineBlockAnchors = useMemo3(() => {
1126
+ if (messageRenderOrder !== "timeline" || !isAssistantStreaming) {
1127
+ return state.timelineBlockAnchors;
1128
+ }
1129
+ const previousBlockKeys = new Set(state.previousBlockKeys);
1130
+ return currentTimelineBlockKeys.reduce(
1131
+ (acc, blockKey) => {
1132
+ const existingAnchor = state.timelineBlockAnchors[blockKey];
1133
+ if (existingAnchor !== void 0) {
1134
+ acc[blockKey] = existingAnchor;
1135
+ return acc;
1136
+ }
1137
+ if (!previousBlockKeys.has(blockKey)) {
1138
+ acc[blockKey] = timelineTextStreamLength;
1139
+ }
1140
+ return acc;
1141
+ },
1142
+ { ...state.timelineBlockAnchors }
1143
+ );
1144
+ }, [
1145
+ currentTimelineBlockKeys,
1146
+ isAssistantStreaming,
1147
+ messageRenderOrder,
1148
+ state.previousBlockKeys,
1149
+ state.timelineBlockAnchors,
1150
+ timelineTextStreamLength
1151
+ ]);
1152
+ useEffect2(() => {
1153
+ dispatch({
1154
+ type: "reset-message",
1155
+ messageId: message.id,
1156
+ currentBlockKeys: currentTimelineBlockKeys
1157
+ });
1158
+ }, [currentTimelineBlockKeys, message.id]);
1159
+ useEffect2(() => {
1160
+ if (messageRenderOrder !== "timeline" || !isAssistantStreaming) {
1161
+ return;
1162
+ }
1163
+ dispatch({
1164
+ type: "sync-anchors",
1165
+ currentBlockKeys: currentTimelineBlockKeys,
1166
+ timelineTextStreamLength
1167
+ });
1168
+ }, [currentTimelineBlockKeys, isAssistantStreaming, messageRenderOrder, timelineTextStreamLength]);
1169
+ useEffect2(() => {
1170
+ if (messageRenderOrder !== "timeline") {
1171
+ return;
1172
+ }
1173
+ dispatch({
1174
+ type: "sync-visible",
1175
+ currentBlockKeys: currentTimelineBlockKeys,
1176
+ effectiveTimelineBlockAnchors,
1177
+ displayedTimelineTextLength
1178
+ });
1179
+ }, [
1180
+ currentTimelineBlockKeys,
1181
+ displayedTimelineTextLength,
1182
+ effectiveTimelineBlockAnchors,
1183
+ messageRenderOrder
1184
+ ]);
1185
+ return {
1186
+ timelineBlockAnchors: messageRenderOrder === "timeline" ? effectiveTimelineBlockAnchors : {},
1187
+ visibleTimelineBlockKeys: messageRenderOrder === "timeline" ? state.visibleTimelineBlockKeys : {}
1188
+ };
1189
+ };
1190
+
861
1191
  // src/components/chat-thread/components/pde-ai-execution-confirmation-card.tsx
862
1192
  import styled from "@emotion/styled";
863
1193
  import { jsx as jsx2, jsxs } from "@emotion/react/jsx-runtime";
@@ -1609,7 +1939,7 @@ var Detail = styled5.li`
1609
1939
 
1610
1940
  // src/components/chat-thread/components/image-viewer.tsx
1611
1941
  import styled6 from "@emotion/styled";
1612
- import { useEffect as useEffect2, useRef as useRef3 } from "react";
1942
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
1613
1943
  import { jsx as jsx7 } from "@emotion/react/jsx-runtime";
1614
1944
  var Overlay = styled6.div`
1615
1945
  position: fixed;
@@ -1629,7 +1959,7 @@ var Img = styled6.img`
1629
1959
  `;
1630
1960
  var ImageViewer = ({ src, alt, onClose }) => {
1631
1961
  const overlayRef = useRef3(null);
1632
- useEffect2(() => {
1962
+ useEffect3(() => {
1633
1963
  const handleKey = (e) => {
1634
1964
  if (e.key === "Escape")
1635
1965
  onClose();
@@ -1637,7 +1967,7 @@ var ImageViewer = ({ src, alt, onClose }) => {
1637
1967
  document.addEventListener("keydown", handleKey);
1638
1968
  return () => document.removeEventListener("keydown", handleKey);
1639
1969
  }, [onClose]);
1640
- useEffect2(() => {
1970
+ useEffect3(() => {
1641
1971
  overlayRef.current?.focus();
1642
1972
  }, []);
1643
1973
  const stopPropagation = (e) => e.stopPropagation();
@@ -1800,9 +2130,16 @@ var ChatMessageItemView = ({
1800
2130
  onQuestionnaireSubmit,
1801
2131
  renderMessageBlock
1802
2132
  }) => {
1803
- const { labels } = useChatContext();
2133
+ const { labels, messageRenderOrder = "blocks-first" } = useChatContext();
1804
2134
  const [activeImage, setActiveImage] = useState3(void 0);
1805
- const { displayedBlocks, displayedContent, freshContent, isAssistantStreaming, settledContent } = useChatMessageReveal(message);
2135
+ const {
2136
+ displayedBlocks,
2137
+ displayedContent,
2138
+ freshContent,
2139
+ isAssistantStreaming,
2140
+ isFreshBlockActive,
2141
+ settledContent
2142
+ } = useChatMessageReveal(message);
1806
2143
  const isStoppedAssistant = message.role === "assistant" && message.status === "stopped";
1807
2144
  const attachments = message.attachments ?? [];
1808
2145
  const blocks = message.blocks ?? [];
@@ -1814,6 +2151,22 @@ var ChatMessageItemView = ({
1814
2151
  const canSubmitConfirmation = isPlanMode && typeof onConfirmationSubmit === "function";
1815
2152
  const canSubmitQuestionnaire = isPlanMode && typeof onQuestionnaireSubmit === "function";
1816
2153
  const shouldShowStreamingCaret = isAssistantStreaming && (!shouldRenderStructuredBlocks || hasTextContent);
2154
+ const timelineConsumedText = messageRenderOrder === "timeline" ? getTimelineConsumedText(blocks) : "";
2155
+ const hasConsumedTimelineText = timelineConsumedText.length > 0 && displayedContent.startsWith(timelineConsumedText);
2156
+ const timelineDisplayedContent = hasConsumedTimelineText ? displayedContent.slice(timelineConsumedText.length) : displayedContent;
2157
+ const timelineTextDisplay = buildTimelineTextDisplay(
2158
+ timelineDisplayedContent,
2159
+ isAssistantStreaming,
2160
+ isFreshBlockActive
2161
+ );
2162
+ const displayedTimelineTextLength = getTimelineDisplayUnitCount(timelineDisplayedContent);
2163
+ const { timelineBlockAnchors, visibleTimelineBlockKeys } = useTimelineBlockAnchors({
2164
+ blocks,
2165
+ displayedTimelineTextLength,
2166
+ isAssistantStreaming,
2167
+ message,
2168
+ messageRenderOrder
2169
+ });
1817
2170
  const renderChatMessageBlock = (block, index) => {
1818
2171
  switch (block.type) {
1819
2172
  case "markdown":
@@ -1870,19 +2223,69 @@ var ChatMessageItemView = ({
1870
2223
  return null;
1871
2224
  }
1872
2225
  };
1873
- const renderTextContent = () => /* @__PURE__ */ jsxs5(Fragment2, { children: [
1874
- displayedBlocks.filter((block) => block.content).map((block, index) => /* @__PURE__ */ jsx8(
1875
- ContentBlock,
1876
- {
1877
- "data-testid": block.tone === "fresh" ? "chat-message-fresh-block" : "chat-message-settled-block",
1878
- "data-block-tone": block.tone,
1879
- "data-block-index": index,
1880
- children: renderMarkdownContent(block.content)
1881
- },
1882
- `${block.tone}-${index}`
1883
- )),
1884
- !displayedBlocks.some((block) => block.content) && !settledContent && !freshContent && hasTextContent ? /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(displayedContent) }) : null
1885
- ] });
2226
+ const renderTextContent = (options) => {
2227
+ const textContent = options?.content ?? displayedContent;
2228
+ const localTimelineTextDisplay = options?.displayedBlocks ? void 0 : options?.useTimelineSegmentation && options.content !== void 0 ? buildTimelineTextDisplay(options.content, isAssistantStreaming, isFreshBlockActive) : void 0;
2229
+ const textBlocks = options?.displayedBlocks ?? localTimelineTextDisplay?.displayedBlocks ?? displayedBlocks;
2230
+ const settledText = localTimelineTextDisplay?.settledContent ?? settledContent;
2231
+ const freshText = localTimelineTextDisplay?.freshContent ?? freshContent;
2232
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
2233
+ textBlocks.filter((block) => block.content).map((block, index) => /* @__PURE__ */ jsx8(
2234
+ ContentBlock,
2235
+ {
2236
+ "data-testid": block.tone === "fresh" ? "chat-message-fresh-block" : "chat-message-settled-block",
2237
+ "data-block-tone": block.tone,
2238
+ "data-block-index": index,
2239
+ children: renderMarkdownContent(block.content)
2240
+ },
2241
+ `${block.tone}-${index}`
2242
+ )),
2243
+ !textBlocks.some((block) => block.content) && !settledText && !freshText && Boolean(textContent) ? /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(textContent) }) : null
2244
+ ] });
2245
+ };
2246
+ const renderStaticTextSegment = (content) => /* @__PURE__ */ jsx8(ContentBlock, { "data-testid": "chat-message-settled-block", "data-block-tone": "settled", children: renderMarkdownContent(content) });
2247
+ const bodySegments = (() => {
2248
+ if (!shouldRenderStructuredBlocks && hasTextContent) {
2249
+ return [{ type: "text" }];
2250
+ }
2251
+ if (!shouldRenderStructuredBlocks) {
2252
+ return [];
2253
+ }
2254
+ if (messageRenderOrder === "timeline" && hasTextContent) {
2255
+ const hasAnchoredStructuredBlocks = blocks.some((block, index) => {
2256
+ const blockKey = getTimelineBlockKey(block, index);
2257
+ return blockKey ? timelineBlockAnchors[blockKey] !== void 0 : false;
2258
+ });
2259
+ if (hasAnchoredStructuredBlocks) {
2260
+ return buildAnchoredTimelineSegments({
2261
+ blocks,
2262
+ timelineBlockAnchors,
2263
+ timelineDisplayedBlocks: timelineTextDisplay.displayedBlocks,
2264
+ visibleTimelineBlockKeys
2265
+ });
2266
+ }
2267
+ const orderedTimelineSegments = blocks.map(
2268
+ (block, index) => block.type === "markdown" ? {
2269
+ type: "markdown",
2270
+ content: block.text
2271
+ } : {
2272
+ type: "block",
2273
+ block,
2274
+ index
2275
+ }
2276
+ );
2277
+ if (!timelineConsumedText) {
2278
+ return displayedContent ? [{ type: "text", content: displayedContent }, ...orderedTimelineSegments] : orderedTimelineSegments;
2279
+ }
2280
+ return timelineDisplayedContent ? [...orderedTimelineSegments, { type: "text", content: timelineDisplayedContent }] : orderedTimelineSegments;
2281
+ }
2282
+ const orderedBlocks = blocks.map((block, index) => ({
2283
+ type: "block",
2284
+ block,
2285
+ index
2286
+ }));
2287
+ return hasTextContent ? [...orderedBlocks, { type: "text" }] : orderedBlocks;
2288
+ })();
1886
2289
  return /* @__PURE__ */ jsxs5(Fragment2, { children: [
1887
2290
  /* @__PURE__ */ jsxs5(Bubble, { "data-role": message.role, "data-status": message.status ?? "done", children: [
1888
2291
  /* @__PURE__ */ jsxs5(Header2, { children: [
@@ -1901,17 +2304,18 @@ var ChatMessageItemView = ({
1901
2304
  isStoppedAssistant ? /* @__PURE__ */ jsx8(StatusTag, { "data-testid": "chat-message-stopped-tag", children: labels.stoppedResponse }) : null
1902
2305
  ] }),
1903
2306
  /* @__PURE__ */ jsxs5(Content, { "data-testid": "chat-message-content", children: [
1904
- shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ jsxs5(ContentStack, { "data-testid": "chat-message-body-stack", children: [
1905
- shouldRenderStructuredBlocks ? blocks.map((block, index) => /* @__PURE__ */ jsx8(
1906
- ContentSegment,
1907
- {
1908
- "data-testid": "chat-message-content-segment",
1909
- children: renderChatMessageBlock(block, index)
1910
- },
1911
- `${block.type}-${index}`
1912
- )) : null,
1913
- hasTextContent ? /* @__PURE__ */ jsx8(ContentSegment, { "data-testid": "chat-message-content-segment", children: renderTextContent() }) : null
1914
- ] }) : null,
2307
+ shouldRenderStructuredBlocks || hasTextContent ? /* @__PURE__ */ jsx8(ContentStack, { "data-testid": "chat-message-body-stack", children: bodySegments.map((segment, index) => /* @__PURE__ */ jsx8(
2308
+ ContentSegment,
2309
+ {
2310
+ "data-testid": "chat-message-content-segment",
2311
+ children: segment.type === "block" ? renderChatMessageBlock(segment.block, segment.index) : segment.type === "text" ? segment.content !== void 0 ? segment.useTimelineSegmentation ? renderTextContent({
2312
+ content: segment.content,
2313
+ displayedBlocks: segment.displayedBlocks,
2314
+ useTimelineSegmentation: true
2315
+ }) : renderStaticTextSegment(segment.content) : renderTextContent() : renderStaticTextSegment(segment.content)
2316
+ },
2317
+ segment.type === "text" ? `text-${index}` : segment.type === "markdown" ? `markdown-${index}` : `${segment.block.type}-${segment.index}`
2318
+ )) }) : null,
1915
2319
  attachments.length ? /* @__PURE__ */ jsx8(AttachmentGrid, { "data-testid": "chat-message-attachment-grid", children: attachments.map((attachment) => /* @__PURE__ */ jsx8(
1916
2320
  AttachmentButton,
1917
2321
  {
@@ -2304,7 +2708,7 @@ var ChatThreadView = ({
2304
2708
  renderMessageBlock
2305
2709
  }) => {
2306
2710
  const containerRef = useRef4(null);
2307
- const conversationTurns = useMemo3(
2711
+ const conversationTurns = useMemo4(
2308
2712
  () => groupConversationTurns(historyMessages, streamingMessage),
2309
2713
  [historyMessages, streamingMessage]
2310
2714
  );
@@ -2566,7 +2970,7 @@ var RetryButton = styled9.button`
2566
2970
  `;
2567
2971
 
2568
2972
  // src/components/chat-composer/index.tsx
2569
- import { useEffect as useEffect5, useRef as useRef7 } from "react";
2973
+ import { useEffect as useEffect6, useRef as useRef7 } from "react";
2570
2974
  import styled14 from "@emotion/styled";
2571
2975
 
2572
2976
  // src/components/chat-composer/lib/chat-composer.ts
@@ -2678,10 +3082,10 @@ var resolveSendSession = ({
2678
3082
  };
2679
3083
 
2680
3084
  // src/components/chat-composer/hooks/use-chat-composer.ts
2681
- import { useCallback as useCallback3, useEffect as useEffect4, useRef as useRef6, useState as useState6 } from "react";
3085
+ import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef6, useState as useState6 } from "react";
2682
3086
 
2683
3087
  // src/components/chat-composer/hooks/use-composer-attachments.ts
2684
- import { useEffect as useEffect3, useRef as useRef5, useState as useState5 } from "react";
3088
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState5 } from "react";
2685
3089
  var SUPPORTED_IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
2686
3090
  var MAX_COMPOSER_ATTACHMENTS = 10;
2687
3091
  var createObjectUrl = (file) => typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : "";
@@ -2697,10 +3101,10 @@ var releaseComposerAttachments = (attachments) => {
2697
3101
  var useComposerAttachments = () => {
2698
3102
  const [attachments, setAttachments] = useState5([]);
2699
3103
  const attachmentsRef = useRef5([]);
2700
- useEffect3(() => {
3104
+ useEffect4(() => {
2701
3105
  attachmentsRef.current = attachments;
2702
3106
  }, [attachments]);
2703
- useEffect3(
3107
+ useEffect4(
2704
3108
  () => () => {
2705
3109
  releaseComposerAttachments(attachmentsRef.current);
2706
3110
  },
@@ -2828,7 +3232,7 @@ var useChatComposer = () => {
2828
3232
  setIsModelsLoading(false);
2829
3233
  }
2830
3234
  }, [transport]);
2831
- useEffect4(() => {
3235
+ useEffect5(() => {
2832
3236
  void fetchModels();
2833
3237
  }, [fetchModels]);
2834
3238
  const hasModels = availableModels.length > 0;
@@ -2840,19 +3244,19 @@ var useChatComposer = () => {
2840
3244
  const abortControllerRef = useRef6(null);
2841
3245
  const stopRequestRef = useRef6(null);
2842
3246
  const lastRequestRef = useRef6(null);
2843
- useEffect4(() => {
3247
+ useEffect5(() => {
2844
3248
  setSelectedModel(
2845
3249
  (current) => resolveSelectedChatModel({ currentModel: current, availableModels, isModelsLoading })
2846
3250
  );
2847
3251
  }, [availableModels, isModelsLoading]);
2848
- useEffect4(() => {
3252
+ useEffect5(() => {
2849
3253
  if (activeSession) {
2850
3254
  setSelectedModeLocal(activeSession.mode ?? DEFAULT_CHAT_AGENT_MODE);
2851
3255
  return;
2852
3256
  }
2853
3257
  setSelectedModeLocal(preferredMode ?? DEFAULT_CHAT_AGENT_MODE);
2854
3258
  }, [activeSession, preferredMode]);
2855
- useEffect4(() => {
3259
+ useEffect5(() => {
2856
3260
  if (!attachmentNotice)
2857
3261
  return;
2858
3262
  const timeoutId = window.setTimeout(
@@ -3742,7 +4146,7 @@ var ChatComposer = () => {
3742
4146
  const { labels, sendRef, retryRef, enableImageAttachments } = useChatContext();
3743
4147
  const { state, actions } = useChatComposer();
3744
4148
  const { send, retry } = actions;
3745
- useEffect5(() => {
4149
+ useEffect6(() => {
3746
4150
  sendRef.current = send;
3747
4151
  retryRef.current = async () => {
3748
4152
  retry();
@@ -4061,6 +4465,7 @@ export {
4061
4465
  AiChat,
4062
4466
  AiChatProvider,
4063
4467
  CHAT_AGENT_MODES,
4468
+ CHAT_MESSAGE_RENDER_ORDERS,
4064
4469
  ChatComposer,
4065
4470
  ChatConversationList,
4066
4471
  ChatThread,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xinghunm/ai-chat",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "AI chat React component library",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",