@copilotz/chat-ui 0.1.8 → 0.1.10

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.cjs CHANGED
@@ -119,7 +119,9 @@ var defaultChatConfig = {
119
119
  daysAgo: "days ago",
120
120
  inputHelpText: "Press Enter to send, Shift+Enter to add a new line.",
121
121
  thinking: "Thinking...",
122
- defaultThreadName: "Main Thread"
122
+ defaultThreadName: "Main Thread",
123
+ showMoreMessage: "Show more",
124
+ showLessMessage: "Show less"
123
125
  },
124
126
  features: {
125
127
  enableThreads: true,
@@ -138,7 +140,12 @@ var defaultChatConfig = {
138
140
  showTimestamps: false,
139
141
  showAvatars: true,
140
142
  compactMode: false,
141
- showWordCount: false
143
+ showWordCount: false,
144
+ collapseLongMessages: false,
145
+ collapseLongMessagesForUserOnly: false,
146
+ longMessagePreviewChars: 4e3,
147
+ longMessageChunkChars: 12e3,
148
+ renderUserMarkdown: true
142
149
  },
143
150
  customComponent: {},
144
151
  headerActions: null
@@ -293,7 +300,7 @@ var configUtils = {
293
300
  };
294
301
 
295
302
  // src/components/chat/Message.tsx
296
- var import_react = require("react");
303
+ var import_react = __toESM(require("react"), 1);
297
304
  var import_react_markdown = __toESM(require("react-markdown"), 1);
298
305
  var import_remark_gfm = __toESM(require("remark-gfm"), 1);
299
306
  var import_rehype_highlight = __toESM(require("rehype-highlight"), 1);
@@ -614,21 +621,89 @@ var markdownComponents = {
614
621
  var remarkPluginsDefault = [import_remark_gfm.default];
615
622
  var rehypePluginsDefault = [import_rehype_highlight.default];
616
623
  var rehypePluginsEmpty = [];
624
+ var getPlainTextChunks = (content, chunkSize) => {
625
+ if (chunkSize <= 0 || content.length <= chunkSize) {
626
+ return [content];
627
+ }
628
+ const chunks = [];
629
+ let start = 0;
630
+ while (start < content.length) {
631
+ let end = Math.min(start + chunkSize, content.length);
632
+ if (end < content.length) {
633
+ const splitAt = content.lastIndexOf("\n", end);
634
+ if (splitAt > start + Math.floor(chunkSize / 2)) {
635
+ end = splitAt + 1;
636
+ }
637
+ }
638
+ chunks.push(content.slice(start, end));
639
+ start = end;
640
+ }
641
+ return chunks;
642
+ };
643
+ var hasCodeBlocks = (content) => /(^|\n)(```|~~~)/.test(content);
644
+ var getCollapsedPreview = (content, previewChars, previewOverride) => {
645
+ if (previewOverride && previewOverride.trim().length > 0) {
646
+ const normalizedPreview = previewOverride.trimEnd();
647
+ return normalizedPreview.endsWith("...") ? normalizedPreview : `${normalizedPreview}...`;
648
+ }
649
+ if (content.length <= previewChars) {
650
+ return content;
651
+ }
652
+ return `${content.slice(0, previewChars).trimEnd()}...`;
653
+ };
654
+ var LongContentShell = (0, import_react.memo)(function LongContentShell2({ children, className, style }) {
655
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className, style, children });
656
+ });
657
+ var PlainTextContent = (0, import_react.memo)(function PlainTextContent2({
658
+ content,
659
+ className = "",
660
+ chunkSize = 12e3,
661
+ style
662
+ }) {
663
+ const chunks = (0, import_react.useMemo)(() => getPlainTextChunks(content, chunkSize), [content, chunkSize]);
664
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
665
+ LongContentShell,
666
+ {
667
+ className: `text-sm leading-6 whitespace-pre-wrap break-words ${className}`.trim(),
668
+ style,
669
+ children: chunks.map((chunk, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_react.default.Fragment, { children: chunk }, index))
670
+ }
671
+ );
672
+ });
617
673
  var StreamingText = (0, import_react.memo)(function StreamingText2({
618
674
  content,
619
675
  isStreaming = false,
620
676
  thinkingLabel = "Thinking...",
621
- className = ""
677
+ className = "",
678
+ renderMarkdown = true,
679
+ plainTextChunkChars = 12e3,
680
+ contentStyle
622
681
  }) {
623
682
  const hasContent = content.trim().length > 0;
624
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`, children: [
625
- hasContent ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
626
- import_react_markdown.default,
683
+ const enableSyntaxHighlight = renderMarkdown && !isStreaming && hasCodeBlocks(content);
684
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
685
+ hasContent ? renderMarkdown ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
686
+ LongContentShell,
687
+ {
688
+ className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`.trim(),
689
+ style: contentStyle,
690
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
691
+ import_react_markdown.default,
692
+ {
693
+ remarkPlugins: remarkPluginsDefault,
694
+ rehypePlugins: enableSyntaxHighlight ? rehypePluginsDefault : rehypePluginsEmpty,
695
+ components: markdownComponents,
696
+ children: content
697
+ }
698
+ )
699
+ }
700
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
701
+ PlainTextContent,
627
702
  {
628
- remarkPlugins: remarkPluginsDefault,
629
- rehypePlugins: isStreaming ? rehypePluginsEmpty : rehypePluginsDefault,
630
- components: markdownComponents,
631
- children: content
703
+ content,
704
+ className,
705
+ chunkSize: plainTextChunkChars,
706
+ style: contentStyle
632
707
  }
633
708
  ) : isStreaming ? (
634
709
  // Show thinking indicator while waiting for first token
@@ -798,6 +873,15 @@ var arePropsEqual = (prevProps, nextProps) => {
798
873
  if (prevProps.className !== nextProps.className) return false;
799
874
  if (prevProps.toolUsedLabel !== nextProps.toolUsedLabel) return false;
800
875
  if (prevProps.thinkingLabel !== nextProps.thinkingLabel) return false;
876
+ if (prevProps.showMoreLabel !== nextProps.showMoreLabel) return false;
877
+ if (prevProps.showLessLabel !== nextProps.showLessLabel) return false;
878
+ if (prevProps.collapseLongMessages !== nextProps.collapseLongMessages) return false;
879
+ if (prevProps.collapseLongMessagesForUserOnly !== nextProps.collapseLongMessagesForUserOnly) return false;
880
+ if (prevProps.longMessagePreviewChars !== nextProps.longMessagePreviewChars) return false;
881
+ if (prevProps.longMessageChunkChars !== nextProps.longMessageChunkChars) return false;
882
+ if (prevProps.renderUserMarkdown !== nextProps.renderUserMarkdown) return false;
883
+ if (prevProps.isExpanded !== nextProps.isExpanded) return false;
884
+ if (prevProps.onToggleExpanded !== nextProps.onToggleExpanded) return false;
801
885
  if (prevProps.isGrouped !== nextProps.isGrouped) return false;
802
886
  if (prevProps.assistantAvatar !== nextProps.assistantAvatar) return false;
803
887
  return true;
@@ -820,6 +904,15 @@ var Message = (0, import_react.memo)(({
820
904
  className = "",
821
905
  toolUsedLabel,
822
906
  thinkingLabel = "Thinking...",
907
+ showMoreLabel = "Show more",
908
+ showLessLabel = "Show less",
909
+ collapseLongMessages = false,
910
+ collapseLongMessagesForUserOnly = false,
911
+ longMessagePreviewChars = 4e3,
912
+ longMessageChunkChars = 12e3,
913
+ renderUserMarkdown = true,
914
+ isExpanded = false,
915
+ onToggleExpanded,
823
916
  isGrouped = false
824
917
  }) => {
825
918
  const [isEditing, setIsEditing] = (0, import_react.useState)(false);
@@ -829,6 +922,18 @@ var Message = (0, import_react.memo)(({
829
922
  const messageIsUser = isUser ?? message.role === "user";
830
923
  const canEdit = enableEdit && messageIsUser;
831
924
  const canRegenerate = enableRegenerate && !messageIsUser;
925
+ const normalizedPreviewChars = Math.max(longMessagePreviewChars, 1);
926
+ const normalizedChunkChars = Math.max(longMessageChunkChars, 1);
927
+ const previewOverride = typeof message.metadata?.previewContent === "string" ? message.metadata.previewContent : void 0;
928
+ const canCollapseMessage = collapseLongMessages && !message.isStreaming && message.content.length > normalizedPreviewChars && (!collapseLongMessagesForUserOnly || messageIsUser);
929
+ const isCollapsed = canCollapseMessage && !isExpanded;
930
+ const contentToRender = isCollapsed ? getCollapsedPreview(message.content, normalizedPreviewChars, previewOverride) : message.content;
931
+ const shouldRenderMarkdown = !isCollapsed && (!messageIsUser || renderUserMarkdown);
932
+ const shouldApplyLargeContentContainment = !isCollapsed && message.content.length > normalizedChunkChars;
933
+ const contentStyle = shouldApplyLargeContentContainment ? {
934
+ contentVisibility: "auto",
935
+ containIntrinsicSize: "1px 400px"
936
+ } : void 0;
832
937
  const handleCopy = async () => {
833
938
  try {
834
939
  await navigator.clipboard.writeText(message.content);
@@ -857,6 +962,9 @@ var Message = (0, import_react.memo)(({
857
962
  const handleRegenerate = () => {
858
963
  onAction?.({ action: "regenerate", messageId: message.id });
859
964
  };
965
+ const handleToggleExpanded = () => {
966
+ onToggleExpanded?.(message.id);
967
+ };
860
968
  const formatTime = (timestamp) => {
861
969
  return new Date(timestamp).toLocaleTimeString("pt-BR", {
862
970
  hour: "2-digit",
@@ -881,7 +989,7 @@ var Message = (0, import_react.memo)(({
881
989
  message.isEdited && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Badge, { variant: "outline", className: "text-xs", children: "editado" })
882
990
  ] })
883
991
  ] }),
884
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `flex-1 min-w-0 ${messageIsUser ? "text-right" : "text-left"} ${isGrouped && showAvatar && !messageIsUser ? compactMode ? "ml-9" : "ml-11" : ""} ${isGrouped && showAvatar && messageIsUser ? compactMode ? "mr-9" : "mr-11" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `relative inline-flex flex-col overflow-hidden ${messageIsUser ? "rounded-lg p-3 bg-primary text-primary-foreground ml-auto max-w-[85%]" : "max-w-full"}`, children: [
992
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: `flex-1 min-w-0 ${messageIsUser ? "text-right" : "text-left"} ${isGrouped && showAvatar && !messageIsUser ? compactMode ? "ml-9" : "ml-11" : ""} ${isGrouped && showAvatar && messageIsUser ? compactMode ? "mr-9" : "mr-11" : ""}`, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `relative inline-flex flex-col overflow-hidden text-left ${messageIsUser ? "rounded-lg p-3 bg-primary text-primary-foreground ml-auto max-w-[85%]" : "max-w-full"}`, children: [
885
993
  isEditing ? /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "space-y-2", children: [
886
994
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
887
995
  Textarea,
@@ -907,12 +1015,26 @@ var Message = (0, import_react.memo)(({
907
1015
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
908
1016
  StreamingText,
909
1017
  {
910
- content: message.content,
1018
+ content: contentToRender,
911
1019
  isStreaming: message.isStreaming,
912
1020
  thinkingLabel,
913
- className: messageIsUser ? "[&_*]:text-right" : ""
1021
+ renderMarkdown: shouldRenderMarkdown,
1022
+ plainTextChunkChars: normalizedChunkChars,
1023
+ contentStyle
914
1024
  }
915
1025
  ),
1026
+ canCollapseMessage && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1027
+ Button,
1028
+ {
1029
+ type: "button",
1030
+ variant: "ghost",
1031
+ size: "sm",
1032
+ className: "h-auto px-0 text-xs font-medium text-current hover:bg-transparent hover:opacity-80",
1033
+ "aria-expanded": !isCollapsed,
1034
+ onClick: handleToggleExpanded,
1035
+ children: isCollapsed ? showMoreLabel : showLessLabel
1036
+ }
1037
+ ) }),
916
1038
  message.attachments && message.attachments.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mt-3 space-y-2", children: message.attachments.map((attachment, index) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(MediaRenderer, { attachment }, index)) })
917
1039
  ] }),
918
1040
  !isEditing && (showActions || copied) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `absolute -top-2 flex gap-1 ${messageIsUser ? "-left-2" : "-right-2"}`, children: [
@@ -3735,6 +3857,7 @@ var ChatUI = ({
3735
3857
  };
3736
3858
  const [inputValue, setInputValue] = (0, import_react7.useState)("");
3737
3859
  const [attachments, setAttachments] = (0, import_react7.useState)([]);
3860
+ const [expandedMessageIds, setExpandedMessageIds] = (0, import_react7.useState)({});
3738
3861
  const [state, setState] = (0, import_react7.useState)({
3739
3862
  isRecording: false,
3740
3863
  selectedThreadId: currentThreadId,
@@ -3812,8 +3935,23 @@ var ChatUI = ({
3812
3935
  return () => clearTimeout(t);
3813
3936
  }
3814
3937
  }, [state.showSidebar, isMobile, config.customComponent]);
3938
+ const prevMessageCountRef = (0, import_react7.useRef)(0);
3815
3939
  (0, import_react7.useEffect)(() => {
3816
- if (!state.isAtBottom || messages.length === 0) return;
3940
+ if (messages.length === 0) {
3941
+ prevMessageCountRef.current = 0;
3942
+ return;
3943
+ }
3944
+ const wasEmpty = prevMessageCountRef.current === 0;
3945
+ prevMessageCountRef.current = messages.length;
3946
+ if (wasEmpty) {
3947
+ requestAnimationFrame(() => {
3948
+ requestAnimationFrame(() => {
3949
+ virtualizer.scrollToIndex(messages.length - 1, { align: "end" });
3950
+ });
3951
+ });
3952
+ return;
3953
+ }
3954
+ if (!state.isAtBottom) return;
3817
3955
  requestAnimationFrame(() => {
3818
3956
  const viewport = scrollAreaRef.current;
3819
3957
  if (!viewport) return;
@@ -3823,7 +3961,25 @@ var ChatUI = ({
3823
3961
  viewport.scrollTop = viewport.scrollHeight;
3824
3962
  }
3825
3963
  });
3826
- }, [messages, state.isAtBottom]);
3964
+ }, [messages, state.isAtBottom, virtualizer]);
3965
+ (0, import_react7.useEffect)(() => {
3966
+ virtualizer.measure();
3967
+ }, [expandedMessageIds, virtualizer]);
3968
+ (0, import_react7.useEffect)(() => {
3969
+ const validMessageIds = new Set(messages.map((message) => message.id));
3970
+ setExpandedMessageIds((prev) => {
3971
+ const activeIds = Object.keys(prev);
3972
+ const staleIds = activeIds.filter((messageId) => !validMessageIds.has(messageId));
3973
+ if (staleIds.length === 0) {
3974
+ return prev;
3975
+ }
3976
+ const next = { ...prev };
3977
+ staleIds.forEach((messageId) => {
3978
+ delete next[messageId];
3979
+ });
3980
+ return next;
3981
+ });
3982
+ }, [messages]);
3827
3983
  const handleScroll = (0, import_react7.useCallback)((e) => {
3828
3984
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3829
3985
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
@@ -3861,6 +4017,19 @@ var ChatUI = ({
3861
4017
  break;
3862
4018
  }
3863
4019
  }, [callbacks, createStateCallback]);
4020
+ const handleToggleMessageExpansion = (0, import_react7.useCallback)((messageId) => {
4021
+ setExpandedMessageIds((prev) => {
4022
+ if (prev[messageId]) {
4023
+ const next = { ...prev };
4024
+ delete next[messageId];
4025
+ return next;
4026
+ }
4027
+ return {
4028
+ ...prev,
4029
+ [messageId]: true
4030
+ };
4031
+ });
4032
+ }, []);
3864
4033
  const handleCreateThread = (0, import_react7.useCallback)((title) => {
3865
4034
  callbacks.onCreateThread?.(title, createStateCallback());
3866
4035
  }, [callbacks, createStateCallback]);
@@ -3965,7 +4134,15 @@ var ChatUI = ({
3965
4134
  compactMode: config.ui.compactMode,
3966
4135
  onAction: handleMessageAction,
3967
4136
  toolUsedLabel: config.labels.toolUsed,
3968
- thinkingLabel: config.labels.thinking
4137
+ thinkingLabel: config.labels.thinking,
4138
+ showMoreLabel: config.labels.showMoreMessage,
4139
+ showLessLabel: config.labels.showLessMessage,
4140
+ collapseLongMessages: config.ui.collapseLongMessages,
4141
+ collapseLongMessagesForUserOnly: config.ui.collapseLongMessagesForUserOnly,
4142
+ longMessagePreviewChars: config.ui.longMessagePreviewChars,
4143
+ longMessageChunkChars: config.ui.longMessageChunkChars,
4144
+ renderUserMarkdown: config.ui.renderUserMarkdown,
4145
+ onToggleExpanded: handleToggleMessageExpansion
3969
4146
  }), [
3970
4147
  user?.avatar,
3971
4148
  user?.name,
@@ -3980,7 +4157,15 @@ var ChatUI = ({
3980
4157
  config.features.enableToolCallsDisplay,
3981
4158
  config.labels.toolUsed,
3982
4159
  config.labels.thinking,
3983
- handleMessageAction
4160
+ config.labels.showMoreMessage,
4161
+ config.labels.showLessMessage,
4162
+ config.ui.collapseLongMessages,
4163
+ config.ui.collapseLongMessagesForUserOnly,
4164
+ config.ui.longMessagePreviewChars,
4165
+ config.ui.longMessageChunkChars,
4166
+ config.ui.renderUserMarkdown,
4167
+ handleMessageAction,
4168
+ handleToggleMessageExpansion
3984
4169
  ]);
3985
4170
  const shouldShowAgentSelector = Boolean(
3986
4171
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4072,7 +4257,8 @@ var ChatUI = ({
4072
4257
  {
4073
4258
  message,
4074
4259
  ...messageProps,
4075
- isGrouped
4260
+ isGrouped,
4261
+ isExpanded: Boolean(expandedMessageIds[message.id])
4076
4262
  }
4077
4263
  ),
4078
4264
  message.role === "assistant" && renderInlineSuggestions(message.id)