@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.d.cts CHANGED
@@ -117,6 +117,8 @@ interface ChatConfig {
117
117
  inputHelpText?: string;
118
118
  thinking?: string;
119
119
  defaultThreadName?: string;
120
+ showMoreMessage?: string;
121
+ showLessMessage?: string;
120
122
  };
121
123
  features?: {
122
124
  enableThreads?: boolean;
@@ -135,6 +137,11 @@ interface ChatConfig {
135
137
  showAvatars?: boolean;
136
138
  compactMode?: boolean;
137
139
  showWordCount?: boolean;
140
+ collapseLongMessages?: boolean;
141
+ collapseLongMessagesForUserOnly?: boolean;
142
+ longMessagePreviewChars?: number;
143
+ longMessageChunkChars?: number;
144
+ renderUserMarkdown?: boolean;
138
145
  };
139
146
  customComponent?: {
140
147
  label?: string;
@@ -359,6 +366,15 @@ interface MessageProps {
359
366
  className?: string;
360
367
  toolUsedLabel?: string;
361
368
  thinkingLabel?: string;
369
+ showMoreLabel?: string;
370
+ showLessLabel?: string;
371
+ collapseLongMessages?: boolean;
372
+ collapseLongMessagesForUserOnly?: boolean;
373
+ longMessagePreviewChars?: number;
374
+ longMessageChunkChars?: number;
375
+ renderUserMarkdown?: boolean;
376
+ isExpanded?: boolean;
377
+ onToggleExpanded?: (messageId: string) => void;
362
378
  /** When true, hides the avatar and name (for grouped consecutive messages from same sender) */
363
379
  isGrouped?: boolean;
364
380
  }
@@ -565,6 +581,11 @@ declare const configUtils: {
565
581
  showAvatars?: boolean;
566
582
  compactMode?: boolean;
567
583
  showWordCount?: boolean;
584
+ collapseLongMessages?: boolean;
585
+ collapseLongMessagesForUserOnly?: boolean;
586
+ longMessagePreviewChars?: number;
587
+ longMessageChunkChars?: number;
588
+ renderUserMarkdown?: boolean;
568
589
  };
569
590
  };
570
591
  };
package/dist/index.d.ts CHANGED
@@ -117,6 +117,8 @@ interface ChatConfig {
117
117
  inputHelpText?: string;
118
118
  thinking?: string;
119
119
  defaultThreadName?: string;
120
+ showMoreMessage?: string;
121
+ showLessMessage?: string;
120
122
  };
121
123
  features?: {
122
124
  enableThreads?: boolean;
@@ -135,6 +137,11 @@ interface ChatConfig {
135
137
  showAvatars?: boolean;
136
138
  compactMode?: boolean;
137
139
  showWordCount?: boolean;
140
+ collapseLongMessages?: boolean;
141
+ collapseLongMessagesForUserOnly?: boolean;
142
+ longMessagePreviewChars?: number;
143
+ longMessageChunkChars?: number;
144
+ renderUserMarkdown?: boolean;
138
145
  };
139
146
  customComponent?: {
140
147
  label?: string;
@@ -359,6 +366,15 @@ interface MessageProps {
359
366
  className?: string;
360
367
  toolUsedLabel?: string;
361
368
  thinkingLabel?: string;
369
+ showMoreLabel?: string;
370
+ showLessLabel?: string;
371
+ collapseLongMessages?: boolean;
372
+ collapseLongMessagesForUserOnly?: boolean;
373
+ longMessagePreviewChars?: number;
374
+ longMessageChunkChars?: number;
375
+ renderUserMarkdown?: boolean;
376
+ isExpanded?: boolean;
377
+ onToggleExpanded?: (messageId: string) => void;
362
378
  /** When true, hides the avatar and name (for grouped consecutive messages from same sender) */
363
379
  isGrouped?: boolean;
364
380
  }
@@ -565,6 +581,11 @@ declare const configUtils: {
565
581
  showAvatars?: boolean;
566
582
  compactMode?: boolean;
567
583
  showWordCount?: boolean;
584
+ collapseLongMessages?: boolean;
585
+ collapseLongMessagesForUserOnly?: boolean;
586
+ longMessagePreviewChars?: number;
587
+ longMessageChunkChars?: number;
588
+ renderUserMarkdown?: boolean;
568
589
  };
569
590
  };
570
591
  };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/chat/ChatUI.tsx
2
- import { useState as useState8, useEffect as useEffect9, useRef as useRef7, useCallback as useCallback4, useMemo as useMemo3 } from "react";
2
+ import { useState as useState8, useEffect as useEffect9, useRef as useRef7, useCallback as useCallback4, useMemo as useMemo4 } from "react";
3
3
  import { useVirtualizer } from "@tanstack/react-virtual";
4
4
 
5
5
  // src/config/chatConfig.ts
@@ -64,7 +64,9 @@ var defaultChatConfig = {
64
64
  daysAgo: "days ago",
65
65
  inputHelpText: "Press Enter to send, Shift+Enter to add a new line.",
66
66
  thinking: "Thinking...",
67
- defaultThreadName: "Main Thread"
67
+ defaultThreadName: "Main Thread",
68
+ showMoreMessage: "Show more",
69
+ showLessMessage: "Show less"
68
70
  },
69
71
  features: {
70
72
  enableThreads: true,
@@ -83,7 +85,12 @@ var defaultChatConfig = {
83
85
  showTimestamps: false,
84
86
  showAvatars: true,
85
87
  compactMode: false,
86
- showWordCount: false
88
+ showWordCount: false,
89
+ collapseLongMessages: false,
90
+ collapseLongMessagesForUserOnly: false,
91
+ longMessagePreviewChars: 4e3,
92
+ longMessageChunkChars: 12e3,
93
+ renderUserMarkdown: true
87
94
  },
88
95
  customComponent: {},
89
96
  headerActions: null
@@ -238,7 +245,7 @@ var configUtils = {
238
245
  };
239
246
 
240
247
  // src/components/chat/Message.tsx
241
- import { useState, useRef, memo } from "react";
248
+ import React, { useState, useRef, useMemo, memo } from "react";
242
249
  import ReactMarkdown from "react-markdown";
243
250
  import remarkGfm from "remark-gfm";
244
251
  import rehypeHighlight from "rehype-highlight";
@@ -569,21 +576,89 @@ var markdownComponents = {
569
576
  var remarkPluginsDefault = [remarkGfm];
570
577
  var rehypePluginsDefault = [rehypeHighlight];
571
578
  var rehypePluginsEmpty = [];
579
+ var getPlainTextChunks = (content, chunkSize) => {
580
+ if (chunkSize <= 0 || content.length <= chunkSize) {
581
+ return [content];
582
+ }
583
+ const chunks = [];
584
+ let start = 0;
585
+ while (start < content.length) {
586
+ let end = Math.min(start + chunkSize, content.length);
587
+ if (end < content.length) {
588
+ const splitAt = content.lastIndexOf("\n", end);
589
+ if (splitAt > start + Math.floor(chunkSize / 2)) {
590
+ end = splitAt + 1;
591
+ }
592
+ }
593
+ chunks.push(content.slice(start, end));
594
+ start = end;
595
+ }
596
+ return chunks;
597
+ };
598
+ var hasCodeBlocks = (content) => /(^|\n)(```|~~~)/.test(content);
599
+ var getCollapsedPreview = (content, previewChars, previewOverride) => {
600
+ if (previewOverride && previewOverride.trim().length > 0) {
601
+ const normalizedPreview = previewOverride.trimEnd();
602
+ return normalizedPreview.endsWith("...") ? normalizedPreview : `${normalizedPreview}...`;
603
+ }
604
+ if (content.length <= previewChars) {
605
+ return content;
606
+ }
607
+ return `${content.slice(0, previewChars).trimEnd()}...`;
608
+ };
609
+ var LongContentShell = memo(function LongContentShell2({ children, className, style }) {
610
+ return /* @__PURE__ */ jsx7("div", { className, style, children });
611
+ });
612
+ var PlainTextContent = memo(function PlainTextContent2({
613
+ content,
614
+ className = "",
615
+ chunkSize = 12e3,
616
+ style
617
+ }) {
618
+ const chunks = useMemo(() => getPlainTextChunks(content, chunkSize), [content, chunkSize]);
619
+ return /* @__PURE__ */ jsx7(
620
+ LongContentShell,
621
+ {
622
+ className: `text-sm leading-6 whitespace-pre-wrap break-words ${className}`.trim(),
623
+ style,
624
+ children: chunks.map((chunk, index) => /* @__PURE__ */ jsx7(React.Fragment, { children: chunk }, index))
625
+ }
626
+ );
627
+ });
572
628
  var StreamingText = memo(function StreamingText2({
573
629
  content,
574
630
  isStreaming = false,
575
631
  thinkingLabel = "Thinking...",
576
- className = ""
632
+ className = "",
633
+ renderMarkdown = true,
634
+ plainTextChunkChars = 12e3,
635
+ contentStyle
577
636
  }) {
578
637
  const hasContent = content.trim().length > 0;
579
- return /* @__PURE__ */ jsxs2("div", { className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`, children: [
580
- hasContent ? /* @__PURE__ */ jsx7(
581
- ReactMarkdown,
638
+ const enableSyntaxHighlight = renderMarkdown && !isStreaming && hasCodeBlocks(content);
639
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
640
+ hasContent ? renderMarkdown ? /* @__PURE__ */ jsx7(
641
+ LongContentShell,
642
+ {
643
+ className: `prose prose-sm max-w-none dark:prose-invert break-words ${className}`.trim(),
644
+ style: contentStyle,
645
+ children: /* @__PURE__ */ jsx7(
646
+ ReactMarkdown,
647
+ {
648
+ remarkPlugins: remarkPluginsDefault,
649
+ rehypePlugins: enableSyntaxHighlight ? rehypePluginsDefault : rehypePluginsEmpty,
650
+ components: markdownComponents,
651
+ children: content
652
+ }
653
+ )
654
+ }
655
+ ) : /* @__PURE__ */ jsx7(
656
+ PlainTextContent,
582
657
  {
583
- remarkPlugins: remarkPluginsDefault,
584
- rehypePlugins: isStreaming ? rehypePluginsEmpty : rehypePluginsDefault,
585
- components: markdownComponents,
586
- children: content
658
+ content,
659
+ className,
660
+ chunkSize: plainTextChunkChars,
661
+ style: contentStyle
587
662
  }
588
663
  ) : isStreaming ? (
589
664
  // Show thinking indicator while waiting for first token
@@ -753,6 +828,15 @@ var arePropsEqual = (prevProps, nextProps) => {
753
828
  if (prevProps.className !== nextProps.className) return false;
754
829
  if (prevProps.toolUsedLabel !== nextProps.toolUsedLabel) return false;
755
830
  if (prevProps.thinkingLabel !== nextProps.thinkingLabel) return false;
831
+ if (prevProps.showMoreLabel !== nextProps.showMoreLabel) return false;
832
+ if (prevProps.showLessLabel !== nextProps.showLessLabel) return false;
833
+ if (prevProps.collapseLongMessages !== nextProps.collapseLongMessages) return false;
834
+ if (prevProps.collapseLongMessagesForUserOnly !== nextProps.collapseLongMessagesForUserOnly) return false;
835
+ if (prevProps.longMessagePreviewChars !== nextProps.longMessagePreviewChars) return false;
836
+ if (prevProps.longMessageChunkChars !== nextProps.longMessageChunkChars) return false;
837
+ if (prevProps.renderUserMarkdown !== nextProps.renderUserMarkdown) return false;
838
+ if (prevProps.isExpanded !== nextProps.isExpanded) return false;
839
+ if (prevProps.onToggleExpanded !== nextProps.onToggleExpanded) return false;
756
840
  if (prevProps.isGrouped !== nextProps.isGrouped) return false;
757
841
  if (prevProps.assistantAvatar !== nextProps.assistantAvatar) return false;
758
842
  return true;
@@ -775,6 +859,15 @@ var Message = memo(({
775
859
  className = "",
776
860
  toolUsedLabel,
777
861
  thinkingLabel = "Thinking...",
862
+ showMoreLabel = "Show more",
863
+ showLessLabel = "Show less",
864
+ collapseLongMessages = false,
865
+ collapseLongMessagesForUserOnly = false,
866
+ longMessagePreviewChars = 4e3,
867
+ longMessageChunkChars = 12e3,
868
+ renderUserMarkdown = true,
869
+ isExpanded = false,
870
+ onToggleExpanded,
778
871
  isGrouped = false
779
872
  }) => {
780
873
  const [isEditing, setIsEditing] = useState(false);
@@ -784,6 +877,18 @@ var Message = memo(({
784
877
  const messageIsUser = isUser ?? message.role === "user";
785
878
  const canEdit = enableEdit && messageIsUser;
786
879
  const canRegenerate = enableRegenerate && !messageIsUser;
880
+ const normalizedPreviewChars = Math.max(longMessagePreviewChars, 1);
881
+ const normalizedChunkChars = Math.max(longMessageChunkChars, 1);
882
+ const previewOverride = typeof message.metadata?.previewContent === "string" ? message.metadata.previewContent : void 0;
883
+ const canCollapseMessage = collapseLongMessages && !message.isStreaming && message.content.length > normalizedPreviewChars && (!collapseLongMessagesForUserOnly || messageIsUser);
884
+ const isCollapsed = canCollapseMessage && !isExpanded;
885
+ const contentToRender = isCollapsed ? getCollapsedPreview(message.content, normalizedPreviewChars, previewOverride) : message.content;
886
+ const shouldRenderMarkdown = !isCollapsed && (!messageIsUser || renderUserMarkdown);
887
+ const shouldApplyLargeContentContainment = !isCollapsed && message.content.length > normalizedChunkChars;
888
+ const contentStyle = shouldApplyLargeContentContainment ? {
889
+ contentVisibility: "auto",
890
+ containIntrinsicSize: "1px 400px"
891
+ } : void 0;
787
892
  const handleCopy = async () => {
788
893
  try {
789
894
  await navigator.clipboard.writeText(message.content);
@@ -812,6 +917,9 @@ var Message = memo(({
812
917
  const handleRegenerate = () => {
813
918
  onAction?.({ action: "regenerate", messageId: message.id });
814
919
  };
920
+ const handleToggleExpanded = () => {
921
+ onToggleExpanded?.(message.id);
922
+ };
815
923
  const formatTime = (timestamp) => {
816
924
  return new Date(timestamp).toLocaleTimeString("pt-BR", {
817
925
  hour: "2-digit",
@@ -862,11 +970,26 @@ var Message = memo(({
862
970
  /* @__PURE__ */ jsx7(
863
971
  StreamingText,
864
972
  {
865
- content: message.content,
973
+ content: contentToRender,
866
974
  isStreaming: message.isStreaming,
867
- thinkingLabel
975
+ thinkingLabel,
976
+ renderMarkdown: shouldRenderMarkdown,
977
+ plainTextChunkChars: normalizedChunkChars,
978
+ contentStyle
868
979
  }
869
980
  ),
981
+ canCollapseMessage && /* @__PURE__ */ jsx7("div", { className: "mt-3", children: /* @__PURE__ */ jsx7(
982
+ Button,
983
+ {
984
+ type: "button",
985
+ variant: "ghost",
986
+ size: "sm",
987
+ className: "h-auto px-0 text-xs font-medium text-current hover:bg-transparent hover:opacity-80",
988
+ "aria-expanded": !isCollapsed,
989
+ onClick: handleToggleExpanded,
990
+ children: isCollapsed ? showMoreLabel : showLessLabel
991
+ }
992
+ ) }),
870
993
  message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx7("div", { className: "mt-3 space-y-2", children: message.attachments.map((attachment, index) => /* @__PURE__ */ jsx7(MediaRenderer, { attachment }, index)) })
871
994
  ] }),
872
995
  !isEditing && (showActions || copied) && /* @__PURE__ */ jsxs2("div", { className: `absolute -top-2 flex gap-1 ${messageIsUser ? "-left-2" : "-right-2"}`, children: [
@@ -2588,7 +2711,7 @@ var ChatHeader = ({
2588
2711
  import { useState as useState6, useRef as useRef6, useCallback as useCallback3, useEffect as useEffect8, memo as memo2 } from "react";
2589
2712
 
2590
2713
  // src/components/chat/UserContext.tsx
2591
- import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo2, useState as useState5 } from "react";
2714
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo3, useState as useState5 } from "react";
2592
2715
  import { jsx as jsx19 } from "react/jsx-runtime";
2593
2716
  var Ctx = createContext2(void 0);
2594
2717
  var ChatUserContextProvider = ({ children, initial }) => {
@@ -2610,7 +2733,7 @@ var ChatUserContextProvider = ({ children, initial }) => {
2610
2733
  return { ...prev, ...partial, updatedAt: Date.now() };
2611
2734
  });
2612
2735
  }, []);
2613
- const value = useMemo2(() => ({
2736
+ const value = useMemo3(() => ({
2614
2737
  context: ctx,
2615
2738
  setContext: setPartial,
2616
2739
  resetContext: () => setCtx({ updatedAt: Date.now() })
@@ -3735,7 +3858,7 @@ var ChatUI = ({
3735
3858
  initialInput,
3736
3859
  onInitialInputConsumed
3737
3860
  }) => {
3738
- const config = useMemo3(
3861
+ const config = useMemo4(
3739
3862
  () => mergeConfig(defaultChatConfig, userConfig),
3740
3863
  [userConfig]
3741
3864
  );
@@ -3756,6 +3879,7 @@ var ChatUI = ({
3756
3879
  };
3757
3880
  const [inputValue, setInputValue] = useState8("");
3758
3881
  const [attachments, setAttachments] = useState8([]);
3882
+ const [expandedMessageIds, setExpandedMessageIds] = useState8({});
3759
3883
  const [state, setState] = useState8({
3760
3884
  isRecording: false,
3761
3885
  selectedThreadId: currentThreadId,
@@ -3860,6 +3984,24 @@ var ChatUI = ({
3860
3984
  }
3861
3985
  });
3862
3986
  }, [messages, state.isAtBottom, virtualizer]);
3987
+ useEffect9(() => {
3988
+ virtualizer.measure();
3989
+ }, [expandedMessageIds, virtualizer]);
3990
+ useEffect9(() => {
3991
+ const validMessageIds = new Set(messages.map((message) => message.id));
3992
+ setExpandedMessageIds((prev) => {
3993
+ const activeIds = Object.keys(prev);
3994
+ const staleIds = activeIds.filter((messageId) => !validMessageIds.has(messageId));
3995
+ if (staleIds.length === 0) {
3996
+ return prev;
3997
+ }
3998
+ const next = { ...prev };
3999
+ staleIds.forEach((messageId) => {
4000
+ delete next[messageId];
4001
+ });
4002
+ return next;
4003
+ });
4004
+ }, [messages]);
3863
4005
  const handleScroll = useCallback4((e) => {
3864
4006
  const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
3865
4007
  const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
@@ -3897,6 +4039,19 @@ var ChatUI = ({
3897
4039
  break;
3898
4040
  }
3899
4041
  }, [callbacks, createStateCallback]);
4042
+ const handleToggleMessageExpansion = useCallback4((messageId) => {
4043
+ setExpandedMessageIds((prev) => {
4044
+ if (prev[messageId]) {
4045
+ const next = { ...prev };
4046
+ delete next[messageId];
4047
+ return next;
4048
+ }
4049
+ return {
4050
+ ...prev,
4051
+ [messageId]: true
4052
+ };
4053
+ });
4054
+ }, []);
3900
4055
  const handleCreateThread = useCallback4((title) => {
3901
4056
  callbacks.onCreateThread?.(title, createStateCallback());
3902
4057
  }, [callbacks, createStateCallback]);
@@ -3987,7 +4142,7 @@ var ChatUI = ({
3987
4142
  `message-skeleton-${index}`
3988
4143
  );
3989
4144
  }) });
3990
- const messageProps = useMemo3(() => ({
4145
+ const messageProps = useMemo4(() => ({
3991
4146
  userAvatar: user?.avatar,
3992
4147
  userName: user?.name,
3993
4148
  assistantAvatar: assistant?.avatar,
@@ -4001,7 +4156,15 @@ var ChatUI = ({
4001
4156
  compactMode: config.ui.compactMode,
4002
4157
  onAction: handleMessageAction,
4003
4158
  toolUsedLabel: config.labels.toolUsed,
4004
- thinkingLabel: config.labels.thinking
4159
+ thinkingLabel: config.labels.thinking,
4160
+ showMoreLabel: config.labels.showMoreMessage,
4161
+ showLessLabel: config.labels.showLessMessage,
4162
+ collapseLongMessages: config.ui.collapseLongMessages,
4163
+ collapseLongMessagesForUserOnly: config.ui.collapseLongMessagesForUserOnly,
4164
+ longMessagePreviewChars: config.ui.longMessagePreviewChars,
4165
+ longMessageChunkChars: config.ui.longMessageChunkChars,
4166
+ renderUserMarkdown: config.ui.renderUserMarkdown,
4167
+ onToggleExpanded: handleToggleMessageExpansion
4005
4168
  }), [
4006
4169
  user?.avatar,
4007
4170
  user?.name,
@@ -4016,7 +4179,15 @@ var ChatUI = ({
4016
4179
  config.features.enableToolCallsDisplay,
4017
4180
  config.labels.toolUsed,
4018
4181
  config.labels.thinking,
4019
- handleMessageAction
4182
+ config.labels.showMoreMessage,
4183
+ config.labels.showLessMessage,
4184
+ config.ui.collapseLongMessages,
4185
+ config.ui.collapseLongMessagesForUserOnly,
4186
+ config.ui.longMessagePreviewChars,
4187
+ config.ui.longMessageChunkChars,
4188
+ config.ui.renderUserMarkdown,
4189
+ handleMessageAction,
4190
+ handleToggleMessageExpansion
4020
4191
  ]);
4021
4192
  const shouldShowAgentSelector = Boolean(
4022
4193
  config.agentSelector?.enabled && onSelectAgent && agentOptions.length > 0 && (!config.agentSelector?.hideIfSingle || agentOptions.length > 1)
@@ -4108,7 +4279,8 @@ var ChatUI = ({
4108
4279
  {
4109
4280
  message,
4110
4281
  ...messageProps,
4111
- isGrouped
4282
+ isGrouped,
4283
+ isExpanded: Boolean(expandedMessageIds[message.id])
4112
4284
  }
4113
4285
  ),
4114
4286
  message.role === "assistant" && renderInlineSuggestions(message.id)