@copilotz/chat-ui 0.1.9 → 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",
@@ -907,11 +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
- thinkingLabel
1020
+ thinkingLabel,
1021
+ renderMarkdown: shouldRenderMarkdown,
1022
+ plainTextChunkChars: normalizedChunkChars,
1023
+ contentStyle
913
1024
  }
914
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
+ ) }),
915
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)) })
916
1039
  ] }),
917
1040
  !isEditing && (showActions || copied) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `absolute -top-2 flex gap-1 ${messageIsUser ? "-left-2" : "-right-2"}`, children: [
@@ -3734,6 +3857,7 @@ var ChatUI = ({
3734
3857
  };
3735
3858
  const [inputValue, setInputValue] = (0, import_react7.useState)("");
3736
3859
  const [attachments, setAttachments] = (0, import_react7.useState)([]);
3860
+ const [expandedMessageIds, setExpandedMessageIds] = (0, import_react7.useState)({});
3737
3861
  const [state, setState] = (0, import_react7.useState)({
3738
3862
  isRecording: false,
3739
3863
  selectedThreadId: currentThreadId,
@@ -3838,6 +3962,24 @@ var ChatUI = ({
3838
3962
  }
3839
3963
  });
3840
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]);
3841
3983
  const handleScroll = (0, import_react7.useCallback)((e) => {
3842
3984
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3843
3985
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
@@ -3875,6 +4017,19 @@ var ChatUI = ({
3875
4017
  break;
3876
4018
  }
3877
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
+ }, []);
3878
4033
  const handleCreateThread = (0, import_react7.useCallback)((title) => {
3879
4034
  callbacks.onCreateThread?.(title, createStateCallback());
3880
4035
  }, [callbacks, createStateCallback]);
@@ -3979,7 +4134,15 @@ var ChatUI = ({
3979
4134
  compactMode: config.ui.compactMode,
3980
4135
  onAction: handleMessageAction,
3981
4136
  toolUsedLabel: config.labels.toolUsed,
3982
- 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
3983
4146
  }), [
3984
4147
  user?.avatar,
3985
4148
  user?.name,
@@ -3994,7 +4157,15 @@ var ChatUI = ({
3994
4157
  config.features.enableToolCallsDisplay,
3995
4158
  config.labels.toolUsed,
3996
4159
  config.labels.thinking,
3997
- 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
3998
4169
  ]);
3999
4170
  const shouldShowAgentSelector = Boolean(
4000
4171
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4086,7 +4257,8 @@ var ChatUI = ({
4086
4257
  {
4087
4258
  message,
4088
4259
  ...messageProps,
4089
- isGrouped
4260
+ isGrouped,
4261
+ isExpanded: Boolean(expandedMessageIds[message.id])
4090
4262
  }
4091
4263
  ),
4092
4264
  message.role === "assistant" && renderInlineSuggestions(message.id)